Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions backend/src/controllers/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,25 @@ export const deleteTransaction: RequestHandler = async (req, res) => {
res.status(500).json({ error: `Internal server error: ${error}` });
}
};

export const getMonthlySpending: RequestHandler = async (req, res) => {
const { user_id } = req.params;

const query = `
SELECT
TO_CHAR(date, 'YYYY-MM') AS month,
SUM(amount) AS total
FROM transactions
WHERE user_id = $1
GROUP BY month
ORDER BY month ASC;
`;

try {
const result = await client.query(query, [user_id]);
res.status(200).json(result.rows);
} catch (error) {
console.error("Error fetching monthly data:", error);
res.status(500).json({ error: `Internal server error: ${error}` });
}
};
24 changes: 12 additions & 12 deletions backend/src/db/setup_db.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,35 @@
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# Drop the database if it exists
psql -U postgres -c "DROP DATABASE IF EXISTS tritonspend;"
psql -U postgres -c "DROP DATABASE IF EXISTS postgres;"

# Create the new database
psql -U postgres -c "CREATE DATABASE tritonspend;"
psql -U postgres -c "CREATE DATABASE postgres;"

# Grant all privileges to the postgres user on the new database
psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE tritonspend TO postgres;"
psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE postgres TO postgres;"

# Run the schema SQL file to create the tables (using absolute path)
psql -U postgres -d tritonspend -f "${SCRIPT_DIR}/database.sql"
psql -U postgres -d postgres -f "${SCRIPT_DIR}/database.sql"

# Change ownership of all tables to postgres user
psql -U postgres -d tritonspend -c "ALTER DATABASE tritonspend OWNER TO postgres;"
psql -U postgres -d tritonspend -c "ALTER SCHEMA public OWNER TO postgres;"
psql -U postgres -d postgres -c "ALTER DATABASE postgres OWNER TO postgres;"
psql -U postgres -d postgres -c "ALTER SCHEMA public OWNER TO postgres;"

# Grant all privileges on the tables to postgres
psql -U postgres -d tritonspend -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO postgres;"
psql -U postgres -d postgres -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO postgres;"

# Change ownership of all existing tables to postgres
psql -U postgres -d tritonspend -c "DO \$\$ DECLARE r RECORD; BEGIN FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP EXECUTE 'ALTER TABLE ' || quote_ident(r.tablename) || ' OWNER TO postgres'; END LOOP; END \$\$;"
psql -U postgres -d postgres -c "DO \$\$ DECLARE r RECORD; BEGIN FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP EXECUTE 'ALTER TABLE ' || quote_ident(r.tablename) || ' OWNER TO postgres'; END LOOP; END \$\$;"

# Change ownership of all sequences to postgres
psql -U postgres -d tritonspend -c "DO \$\$ DECLARE r RECORD; BEGIN FOR r IN (SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'public') LOOP EXECUTE 'ALTER SEQUENCE ' || quote_ident(r.sequence_name) || ' OWNER TO postgres'; END LOOP; END \$\$;"
psql -U postgres -d postgres -c "DO \$\$ DECLARE r RECORD; BEGIN FOR r IN (SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'public') LOOP EXECUTE 'ALTER SEQUENCE ' || quote_ident(r.sequence_name) || ' OWNER TO postgres'; END LOOP; END \$\$;"

# Change ownership of all functions to postgres
psql -U postgres -d tritonspend -c "DO \$\$ DECLARE r RECORD; BEGIN FOR r IN (SELECT routine_name FROM information_schema.routines WHERE routine_schema = 'public' AND routine_type = 'FUNCTION') LOOP EXECUTE 'ALTER FUNCTION ' || quote_ident(r.routine_name) || ' OWNER TO postgres'; END LOOP; END \$\$;"
psql -U postgres -d postgres -c "DO \$\$ DECLARE r RECORD; BEGIN FOR r IN (SELECT routine_name FROM information_schema.routines WHERE routine_schema = 'public' AND routine_type = 'FUNCTION') LOOP EXECUTE 'ALTER FUNCTION ' || quote_ident(r.routine_name) || ' OWNER TO postgres'; END LOOP; END \$\$;"

# Ensure future tables also get privileges automatically
psql -U postgres -d tritonspend -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO postgres;"
psql -U postgres -d tritonspend -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres;"
psql -U postgres -d postgres -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO postgres;"
psql -U postgres -d postgres -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres;"

echo "✅ Database setup complete!"
9 changes: 8 additions & 1 deletion backend/src/routes/transactions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import express from "express";
import { addTransaction, getTransactions, deleteTransaction } from "../controllers/transactions";
import {
addTransaction,
getTransactions,
deleteTransaction,
getMonthlySpending,
} from "../controllers/transactions";

const router = express.Router();

Expand All @@ -10,4 +15,6 @@ router.get("/getTransactions/:user_id", getTransactions);
// DELETE /transactions/:user_id/:transaction_id
router.delete("/:user_id/:transaction_id", deleteTransaction);

router.get("/monthly/:user_id", getMonthlySpending);

export default router;
4 changes: 4 additions & 0 deletions env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// env.d.ts
declare module "@env" {
export const BACKEND_PORT: number;
}
59 changes: 56 additions & 3 deletions frontend/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,57 @@ import { useEffect, useState, useCallback } from "react";
import { BACKEND_PORT } from "@env";
import { useAuth } from "@/context/authContext";
import CustomPieChart from "@/components/Graphs/PieChart";
import BarChart from "@/components/Graphs/BarChart";
import { useFocusEffect } from "@react-navigation/native";
/*
this function is the structure for the home screen which includes a graph, option to add transaction, and recent transaction history.
*/

interface Category {
id: number;
category_name: string;
category_expense: string;
max_category_budget: string;
user_id: number;
}

interface MonthlyRow {
month: string;
total: string;
}

interface MonthlyData {
name: string; // month
value: number; // total as number
}

export default function Home() {
//place holder array for us to map through
//passing it through props because I think it will be easier for us to call the API endpoints in the page and pass it through props
const [ThreeTransactions, setThreeTransactions] = useState([]);
const [monthlyData, setMonthlyData] = useState<MonthlyData[]>([]);
const [monthlyData1, setMonthlyData1] = useState<MonthlyData[]>([]);
const testData = [
{ month: "2024-01", total: 67.0 },
{ month: "2024-02", total: 750.0 },
{ month: "2024-03", total: 620.0 },
{ month: "2024-04", total: 810.0 },
{ month: "2024-05", total: 430.0 },
{ month: "2024-06", total: 970.0 },
];

useEffect(() => {
// Convert testData to BarChart format: name, value, color
const formatted = testData.map((item) => ({
name: item.month,
value: item.total,
color: "", // leave empty to auto-assign pastel color
}));

setMonthlyData1(formatted);
setTotal(testData.reduce((sum, item) => sum + item.total, 0));
}, []);

const [updateRecent, setUpdateRecent] = useState(false);
const [total, setTotal] = useState(0);
const [categories, setCategories] = useState<Category[]>([]);
Expand Down Expand Up @@ -69,6 +105,21 @@ export default function Home() {
console.error("API Error:", error);
});

fetch(`http://localhost:${BACKEND_PORT}/transactions/monthly/${userId}`)
.then((res) => res.json())
.then((data: MonthlyRow[]) => {
const formatted = data
.map((row) => ({
name: row.month,
value: Number(row.total),
}))
.sort(
(a, b) => new Date(a.name).getTime() - new Date(b.name).getTime(),
);
setMonthlyData(formatted);
})
.catch((error) => console.log("Monthly API Error:", error));

fetch(`http://localhost:${BACKEND_PORT}/users/category/${userId}`, {
method: "GET",
})
Expand Down Expand Up @@ -97,6 +148,7 @@ export default function Home() {
name: category.category_name,
id: category.id,
}));

console.log(categories);
return (
<>
Expand All @@ -109,8 +161,9 @@ export default function Home() {
Total Spending
</Text>
{/* <View style={styles.graph}></View> */}
<CustomPieChart data={pieData} size={250} total={total} />
<View style={styles.legendContainer}>
{/* <CustomPieChart data={pieData} size={250} total={total} /> */}
<BarChart data={monthlyData} size={250} total={total} />
{/* <View style={styles.legendContainer}>
{pieData.map((category) => {
return (
<View key={category.id} style={styles.legendItem}>
Expand All @@ -124,7 +177,7 @@ export default function Home() {
</View>
);
})}
</View>
</View> */}
</View>
{/*
components for the new transaction button and the list of transaction history.
Expand Down
112 changes: 112 additions & 0 deletions frontend/components/Graphs/BarChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from "react";
import { View, Text, StyleSheet } from "react-native";
import Svg, { Rect, Text as SvgText } from "react-native-svg";

export default function BarChart({
data,
size,
total,
}: {
data: any[];
size: number;
total: number;
}) {
const chartHeight = size;
const chartWidth = size * 1.2;

const barSpacing = 25; // consistent spacing
const barWidth = (chartWidth - barSpacing * (data.length + 1)) / data.length;

const maxValue = Math.max(...data.map((d: any) => d.value), 1);

// Convert "YYYY-MM" to "Mon"
const formatMonth = (monthStr: string) => {
const [year, month] = monthStr.split("-").map(Number);
const date = new Date(year, month - 1);
return date.toLocaleString("default", { month: "short" });
};

// Pastel colors for each month
const monthColors: Record<string, string> = {
"01": "#FFD1DC", // Jan
"02": "#FFE4B5", // Feb
"03": "#BFFCC6", // Mar
"04": "#C1F0F6", // Apr
"05": "#D8B4E2", // May
"06": "#FFFACD", // Jun
"07": "#FFB347", // Jul
"08": "#AEC6CF", // Aug
"09": "#FF6961", // Sep
"10": "#77DD77", // Oct
"11": "#CBAACB", // Nov
"12": "#FDFD96", // Dec
};

return (
<View style={styles.container}>
<Svg height={chartHeight} width={chartWidth}>
{data.map((item: any, index: number) => {
const x = barSpacing + index * (barWidth + barSpacing);
const barHeight = (item.value / maxValue) * (chartHeight - 90);
const y = chartHeight - barHeight - 50; // padding from bottom

// Assign color based on month if color not already set
const month = item.name.split("-")[1];
const fillColor = item.color || monthColors[month] || "#ccc";

return (
<React.Fragment key={index}>
{/* Value label */}
<SvgText
x={x + barWidth / 2}
y={y - 10}
fontSize="14"
fill="#333"
textAnchor="middle"
>
{item.value}
</SvgText>

{/* Bar */}
<Rect
x={x}
y={y}
width={barWidth}
height={barHeight}
fill={fillColor}
rx={8}
ry={8}
/>

{/* Category label */}
<SvgText
x={x + barWidth / 2}
y={chartHeight - 20}
fontSize="14"
fill="#000"
textAnchor="middle"
>
{formatMonth(item.name)}
</SvgText>
</React.Fragment>
);
})}
</Svg>

<Text style={styles.totalText}>Total: ${total.toFixed(2)}</Text>
</View>
);
}

const styles = StyleSheet.create({
container: {
width: "100%",
alignItems: "center",
paddingVertical: 10,
},
totalText: {
marginTop: 10,
fontSize: 18,
fontWeight: "600",
},
});