From 864e8041ecc33e4e0b1e1c016126b1f478926fb3 Mon Sep 17 00:00:00 2001 From: Yashmit Bhaverisetti Date: Thu, 20 Nov 2025 17:24:31 -0800 Subject: [PATCH] Created a new BarChart component --- backend/src/controllers/transactions.ts | 22 +++++ backend/src/db/setup_db.sh | 24 ++--- backend/src/routes/transactions.ts | 9 +- env.d.ts | 4 + frontend/app/(tabs)/index.tsx | 59 ++++++++++++- frontend/components/Graphs/BarChart.tsx | 112 ++++++++++++++++++++++++ 6 files changed, 214 insertions(+), 16 deletions(-) create mode 100644 env.d.ts create mode 100644 frontend/components/Graphs/BarChart.tsx diff --git a/backend/src/controllers/transactions.ts b/backend/src/controllers/transactions.ts index a24158e..82521b7 100644 --- a/backend/src/controllers/transactions.ts +++ b/backend/src/controllers/transactions.ts @@ -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}` }); + } +}; diff --git a/backend/src/db/setup_db.sh b/backend/src/db/setup_db.sh index 4a4545c..a5eaa58 100755 --- a/backend/src/db/setup_db.sh +++ b/backend/src/db/setup_db.sh @@ -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!" diff --git a/backend/src/routes/transactions.ts b/backend/src/routes/transactions.ts index dfcba3a..d36577f 100644 --- a/backend/src/routes/transactions.ts +++ b/backend/src/routes/transactions.ts @@ -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(); @@ -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; diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..34ca319 --- /dev/null +++ b/env.d.ts @@ -0,0 +1,4 @@ +// env.d.ts +declare module "@env" { + export const BACKEND_PORT: number; +} diff --git a/frontend/app/(tabs)/index.tsx b/frontend/app/(tabs)/index.tsx index 4842d75..a582c9e 100644 --- a/frontend/app/(tabs)/index.tsx +++ b/frontend/app/(tabs)/index.tsx @@ -5,10 +5,12 @@ 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; @@ -16,10 +18,44 @@ interface Category { 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([]); + const [monthlyData1, setMonthlyData1] = useState([]); + 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([]); @@ -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", }) @@ -97,6 +148,7 @@ export default function Home() { name: category.category_name, id: category.id, })); + console.log(categories); return ( <> @@ -109,8 +161,9 @@ export default function Home() { Total Spending {/* */} - - + {/* */} + + {/* {pieData.map((category) => { return ( @@ -124,7 +177,7 @@ export default function Home() { ); })} - + */} {/* components for the new transaction button and the list of transaction history. diff --git a/frontend/components/Graphs/BarChart.tsx b/frontend/components/Graphs/BarChart.tsx new file mode 100644 index 0000000..7cadb5c --- /dev/null +++ b/frontend/components/Graphs/BarChart.tsx @@ -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 = { + "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 ( + + + {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 ( + + {/* Value label */} + + {item.value} + + + {/* Bar */} + + + {/* Category label */} + + {formatMonth(item.name)} + + + ); + })} + + + Total: ${total.toFixed(2)} + + ); +} + +const styles = StyleSheet.create({ + container: { + width: "100%", + alignItems: "center", + paddingVertical: 10, + }, + totalText: { + marginTop: 10, + fontSize: 18, + fontWeight: "600", + }, +});