From 4fd7ea189907180a13d94320ef13a9435e3b5bcd Mon Sep 17 00:00:00 2001 From: RamonsArchive Date: Wed, 12 Nov 2025 20:43:47 -0800 Subject: [PATCH 1/8] changed the username from random to the google profile name --- backend/src/db/database.sql | 145 +++++++++++++++++----------------- backend/src/googleAuth.ts | 9 ++- frontend/app/(tabs)/index.tsx | 10 +-- 3 files changed, 84 insertions(+), 80 deletions(-) diff --git a/backend/src/db/database.sql b/backend/src/db/database.sql index 5042aee..a388337 100644 --- a/backend/src/db/database.sql +++ b/backend/src/db/database.sql @@ -1,99 +1,100 @@ -- Create Users Table CREATE TABLE users ( - id SERIAL PRIMARY KEY, -- User ID (Auto Increment) - email VARCHAR(255) UNIQUE NOT NULL, -- Email (Unique) - username VARCHAR(100) UNIQUE NOT NULL, -- Username (Unique) - profile_picture TEXT, -- Profile Picture (Optional) - total_budget DECIMAL(10,2) DEFAULT 0.00, -- Total Budget - total_expense DECIMAL(10,2) DEFAULT 0.00 -- Total Expenses + id SERIAL PRIMARY KEY, + -- User ID (Auto Increment) + email VARCHAR(255) UNIQUE NOT NULL, + -- Email (Unique) + username VARCHAR(100) UNIQUE NOT NULL, + -- Username (Unique) + profile_picture TEXT, + -- Profile Picture (Optional) + total_budget DECIMAL(10, 2) DEFAULT 0.00, + -- Total Budget + total_expense DECIMAL(10, 2) DEFAULT 0.00 -- Total Expenses ); - -- Create Categories Table CREATE TABLE categories ( - id SERIAL PRIMARY KEY, -- Category ID (Auto Increment) - user_id INT REFERENCES users(id) ON DELETE CASCADE, -- User ID (FK) - category_name VARCHAR(100) NOT NULL, -- Category Name - max_category_budget DECIMAL(10,2) DEFAULT 0.00, -- Max Category Budget - category_expense DECIMAL(10,2) DEFAULT 0.00 -- Category Expense + id SERIAL PRIMARY KEY, + -- Category ID (Auto Increment) + user_id INT REFERENCES users(id) ON DELETE CASCADE, + -- User ID (FK) + category_name VARCHAR(100) NOT NULL, + -- Category Name + max_category_budget DECIMAL(10, 2) DEFAULT 0.00, + -- Max Category Budget + category_expense DECIMAL(10, 2) DEFAULT 0.00 -- Category Expense ); - -- Create Transactions Table CREATE TABLE transactions ( - id SERIAL PRIMARY KEY, -- Transaction ID (Auto Increment) - user_id INT REFERENCES users(id) ON DELETE CASCADE, -- User ID (FK) - item_name VARCHAR(255) NOT NULL, -- Item Name - amount DECIMAL(10,2) NOT NULL, -- Amount of the Transaction - category_name VARCHAR(100) NOT NULL, -- Category Name - date TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Date of Transaction + id SERIAL PRIMARY KEY, + -- Transaction ID (Auto Increment) + user_id INT REFERENCES users(id) ON DELETE CASCADE, + -- User ID (FK) + item_name VARCHAR(255) NOT NULL, + -- Item Name + amount DECIMAL(10, 2) NOT NULL, + -- Amount of the Transaction + category_name VARCHAR(100) NOT NULL, + -- Category Name + date TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Date of Transaction ); - -- Create Goals Table CREATE TABLE goals ( - id SERIAL PRIMARY KEY, -- Goal ID (Auto Increment) - user_id INT REFERENCES users(id) ON DELETE CASCADE, -- User ID (FK) - title VARCHAR(100) NOT NULL, -- Goal Title - details TEXT, -- Goal Details (optional) - target_date DATE -- Target Date (optional) + id SERIAL PRIMARY KEY, + -- Goal ID (Auto Increment) + user_id INT REFERENCES users(id) ON DELETE CASCADE, + -- User ID (FK) + title VARCHAR(100) NOT NULL, + -- Goal Title + details TEXT, + -- Goal Details (optional) + target_date DATE -- Target Date (optional) ); - -CREATE OR REPLACE FUNCTION create_default_categories() -RETURNS TRIGGER AS $$ -BEGIN - INSERT INTO categories (user_id, category_name, max_category_budget, category_expense) - VALUES - (NEW.id, 'Food', 0.00, 0.00), - (NEW.id, 'Shopping', 0.00, 0.00), - (NEW.id, 'Transportation', 0.00, 0.00), - (NEW.id, 'Subscriptions', 0.00, 0.00), - (NEW.id, 'Other', 0.00, 0.00); - RETURN NEW; +CREATE OR REPLACE FUNCTION create_default_categories() RETURNS TRIGGER AS $$ BEGIN +INSERT INTO categories ( + user_id, + category_name, + max_category_budget, + category_expense + ) +VALUES (NEW.id, 'Food', 0.00, 0.00), + (NEW.id, 'Shopping', 0.00, 0.00), + (NEW.id, 'Transportation', 0.00, 0.00), + (NEW.id, 'Subscriptions', 0.00, 0.00), + (NEW.id, 'Other', 0.00, 0.00); +RETURN NEW; END; $$ LANGUAGE plpgsql; - CREATE TRIGGER trigger_create_default_categories -AFTER INSERT ON users -FOR EACH ROW -EXECUTE FUNCTION create_default_categories(); - -CREATE OR REPLACE FUNCTION recalculate_all_categories_expense(userid integer) -RETURNS void LANGUAGE plpgsql AS $$ -BEGIN - UPDATE categories - SET category_expense = COALESCE(( +AFTER +INSERT ON users FOR EACH ROW EXECUTE FUNCTION create_default_categories(); +CREATE OR REPLACE FUNCTION recalculate_all_categories_expense(userid integer) RETURNS void LANGUAGE plpgsql AS $$ BEGIN +UPDATE categories +SET category_expense = COALESCE( + ( SELECT SUM(amount) FROM transactions - WHERE user_id = categories.user_id AND category_name = categories.category_name - ), 0); + WHERE user_id = categories.user_id + AND category_name = categories.category_name + ), + 0 + ); END; $$; - -CREATE OR REPLACE FUNCTION trigger_recalculate_all_on_insert() -RETURNS trigger LANGUAGE plpgsql AS $$ -BEGIN - PERFORM recalculate_all_categories_expense(NEW.user_id); - PERFORM recalculate_all_categories_expense(NEW.user_id); - RETURN NEW; +CREATE OR REPLACE FUNCTION trigger_recalculate_all_on_insert() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN PERFORM recalculate_all_categories_expense(NEW.user_id); +PERFORM recalculate_all_categories_expense(NEW.user_id); +RETURN NEW; END; $$; - -CREATE OR REPLACE FUNCTION trigger_recalculate_all_on_delete() -RETURNS trigger LANGUAGE plpgsql AS $$ -BEGIN - PERFORM recalculate_all_categories_expense(OLD.user_id); - PERFORM recalculate_all_categories_expense(NEW.user_id); - RETURN OLD; +CREATE OR REPLACE FUNCTION trigger_recalculate_all_on_delete() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN PERFORM recalculate_all_categories_expense(OLD.user_id); +PERFORM recalculate_all_categories_expense(NEW.user_id); +RETURN OLD; END; $$; - DROP TRIGGER IF EXISTS recalc_expense_after_insert ON transactions; DROP TRIGGER IF EXISTS recalc_expense_after_delete ON transactions; - CREATE TRIGGER recalc_expense_after_insert -AFTER INSERT ON transactions -FOR EACH ROW -EXECUTE FUNCTION trigger_recalculate_all_on_insert(); - +AFTER +INSERT ON transactions FOR EACH ROW EXECUTE FUNCTION trigger_recalculate_all_on_insert(); CREATE TRIGGER recalc_expense_after_delete -AFTER DELETE ON transactions -FOR EACH ROW -EXECUTE FUNCTION trigger_recalculate_all_on_delete(); +AFTER DELETE ON transactions FOR EACH ROW EXECUTE FUNCTION trigger_recalculate_all_on_delete(); \ No newline at end of file diff --git a/backend/src/googleAuth.ts b/backend/src/googleAuth.ts index 32329a8..9a8064b 100644 --- a/backend/src/googleAuth.ts +++ b/backend/src/googleAuth.ts @@ -2,6 +2,7 @@ import passport from "passport"; import { Strategy as GoogleStrategy } from "passport-google-oauth20"; import db from "src/db/db"; import env from "src/util/validateEnv"; // Importing environment variables +// login to postregress psql -U postgres interface UserProfile { id: string; @@ -31,8 +32,10 @@ passport.use( return done(null, false, { message: "Only @ucsd.edu emails are allowed." }); } - // Generate a random username (for new users) - const randomUsername = `user_${Math.floor(Math.random() * 1000000)}`; + // Generate a random username (for new users) + console.log("Google Profile:", profile); + //const randomUsername = `user_${Math.floor(Math.random() * 1000000)}`; + const username = profile.displayName; // ADDED REAL USERNAME FROM GOOGLE PROFILE const photos = profile.photos; try { @@ -53,7 +56,7 @@ passport.use( `; const insertResult = await db.query(insertUserQuery, [ email, - randomUsername, + username, profilePicture, ]); diff --git a/frontend/app/(tabs)/index.tsx b/frontend/app/(tabs)/index.tsx index 4842d75..777027a 100644 --- a/frontend/app/(tabs)/index.tsx +++ b/frontend/app/(tabs)/index.tsx @@ -1,7 +1,7 @@ import { View, StyleSheet, Text, ScrollView } from "react-native"; import NewTransactionButton from "@/components/NewTransaction/NewTransactionButton"; import TransactionHistory from "@/components/TransactionHistory/TransactionHistory"; -import { useEffect, useState, useCallback } from "react"; +import { useState, useCallback } from "react"; import { BACKEND_PORT } from "@env"; import { useAuth } from "@/context/authContext"; import CustomPieChart from "@/components/Graphs/PieChart"; @@ -43,7 +43,7 @@ export default function Home() { Accept: "application/json", "Content-Type": "application/json", }, - }, + } ) .then((res) => { return res.json(); @@ -81,14 +81,14 @@ export default function Home() { data.reduce( (sum: number, category: { category_expense: string }) => sum + parseFloat(category.category_expense), - 0, - ), + 0 + ) ); }) .catch((error) => { console.error("API Error:", error); }); - }, [updateRecent]), + }, [updateRecent]) ); const pieData = categories.map((category) => ({ From 6145e809311ed5048180099b6f7190a78bbef0db Mon Sep 17 00:00:00 2001 From: RamonsArchive Date: Thu, 13 Nov 2025 17:16:38 -0800 Subject: [PATCH 2/8] ran npm run format and addeded template for AreaChart.tsx --- backend/src/googleAuth.ts | 10 +++------- frontend/app/(tabs)/index.tsx | 8 ++++---- frontend/components/Graphs/AreaChart.tsx | 7 +++++++ 3 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 frontend/components/Graphs/AreaChart.tsx diff --git a/backend/src/googleAuth.ts b/backend/src/googleAuth.ts index 9a8064b..cb2aaec 100644 --- a/backend/src/googleAuth.ts +++ b/backend/src/googleAuth.ts @@ -32,10 +32,10 @@ passport.use( return done(null, false, { message: "Only @ucsd.edu emails are allowed." }); } - // Generate a random username (for new users) + // Generate a random username (for new users) console.log("Google Profile:", profile); //const randomUsername = `user_${Math.floor(Math.random() * 1000000)}`; - const username = profile.displayName; // ADDED REAL USERNAME FROM GOOGLE PROFILE + const username = profile.displayName; // ADDED REAL USERNAME FROM GOOGLE PROFILE second merge request const photos = profile.photos; try { @@ -54,11 +54,7 @@ passport.use( INSERT INTO users (email, username, profile_picture) VALUES ($1, $2, $3) RETURNING id `; - const insertResult = await db.query(insertUserQuery, [ - email, - username, - profilePicture, - ]); + const insertResult = await db.query(insertUserQuery, [email, username, profilePicture]); profile.id = insertResult.rows[0].id; return done(null, profile); diff --git a/frontend/app/(tabs)/index.tsx b/frontend/app/(tabs)/index.tsx index 777027a..3f6512a 100644 --- a/frontend/app/(tabs)/index.tsx +++ b/frontend/app/(tabs)/index.tsx @@ -43,7 +43,7 @@ export default function Home() { Accept: "application/json", "Content-Type": "application/json", }, - } + }, ) .then((res) => { return res.json(); @@ -81,14 +81,14 @@ export default function Home() { data.reduce( (sum: number, category: { category_expense: string }) => sum + parseFloat(category.category_expense), - 0 - ) + 0, + ), ); }) .catch((error) => { console.error("API Error:", error); }); - }, [updateRecent]) + }, [updateRecent]), ); const pieData = categories.map((category) => ({ diff --git a/frontend/components/Graphs/AreaChart.tsx b/frontend/components/Graphs/AreaChart.tsx new file mode 100644 index 0000000..f1bab73 --- /dev/null +++ b/frontend/components/Graphs/AreaChart.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const AreaChart = () => { + return
AreaChart
; +}; + +export default AreaChart; From efe584b7296b9bc46789561cdcdd960d6d8461f0 Mon Sep 17 00:00:00 2001 From: RamonsArchive Date: Thu, 13 Nov 2025 17:21:48 -0800 Subject: [PATCH 3/8] added format with pretteir in readme --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 870512b..9e42aea 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,11 @@ We welcome contributions from the UCSD community and beyond! Please see our [CON 1. Fork the repository 2. Create a feature branch: `git checkout -b feature/amazing-feature` 3. Make your changes and test thoroughly -4. Run linting: `npm run lint-check` -5. Commit your changes: `git commit -m 'Add amazing feature'` -6. Push to your branch: `git push origin feature/amazing-feature` -7. Open a Pull Request +4. Format with Prettier: `npm run format`, +5. Run linting: `npm run lint-check` +6. Commit your changes: `git commit -m 'Add amazing feature'` +7. Push to your branch: `git push origin feature/amazing-feature` +8. Open a Pull Request ## 📊 Project Structure From d9a37ea8b658a788994f6aa66f9f8a43feddda03 Mon Sep 17 00:00:00 2001 From: RamonsArchive Date: Thu, 13 Nov 2025 21:04:58 -0800 Subject: [PATCH 4/8] added AreaChart along with a selection code in index.tsx inside tabs to toggle between Pie and AreaGraph. Also imporved styling of Total spending section making height dynamic to its contents and established frontend types in new folder utisl --- frontend/app/(tabs)/index.tsx | 84 ++++- frontend/components/Graphs/AreaChart.tsx | 441 ++++++++++++++++++++++- frontend/utils/FrontendTypes.ts | 10 + 3 files changed, 518 insertions(+), 17 deletions(-) create mode 100644 frontend/utils/FrontendTypes.ts diff --git a/frontend/app/(tabs)/index.tsx b/frontend/app/(tabs)/index.tsx index 3f6512a..7916d6e 100644 --- a/frontend/app/(tabs)/index.tsx +++ b/frontend/app/(tabs)/index.tsx @@ -1,4 +1,5 @@ import { View, StyleSheet, Text, ScrollView } from "react-native"; +import { Picker } from "@react-native-picker/picker"; import NewTransactionButton from "@/components/NewTransaction/NewTransactionButton"; import TransactionHistory from "@/components/TransactionHistory/TransactionHistory"; import { useState, useCallback } from "react"; @@ -6,6 +7,8 @@ import { BACKEND_PORT } from "@env"; import { useAuth } from "@/context/authContext"; import CustomPieChart from "@/components/Graphs/PieChart"; import { useFocusEffect } from "@react-navigation/native"; +import AreaChart from "@/components/Graphs/AreaChart"; +import { GraphType } from "@/utils/FrontendTypes"; /* this function is the structure for the home screen which includes a graph, option to add transaction, and recent transaction history. */ @@ -20,8 +23,10 @@ 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 [allTransactions, setAllTransactions] = useState([]); const [updateRecent, setUpdateRecent] = useState(false); const [total, setTotal] = useState(0); + const [totalBudget, setTotalBudget] = useState(0); // total budget not tied to categories const [categories, setCategories] = useState([]); const { userId } = useAuth(); const [username, setUsername] = useState(""); @@ -33,6 +38,9 @@ export default function Home() { ["Others", "#2b2d42"], //black ]); + // only two graph types for now: pie and area + const [graphType, setGraphType] = useState("pie"); + useFocusEffect( useCallback(() => { fetch( @@ -43,14 +51,15 @@ export default function Home() { Accept: "application/json", "Content-Type": "application/json", }, - }, + } ) .then((res) => { return res.json(); }) .then((data) => { - console.log(data); + console.log("transaction data", data); setThreeTransactions(data.slice(0, 5)); + setAllTransactions(data); }) .catch((error) => { console.error("API Error:", error); @@ -63,7 +72,9 @@ export default function Home() { return res.json(); }) .then((data) => { + console.log("user data", data); setUsername(data.username); + setTotalBudget(data.total_budget); }) .catch((error) => { console.error("API Error:", error); @@ -76,27 +87,29 @@ export default function Home() { return res.json(); }) .then((data) => { + console.log("category data", data); setCategories(data); setTotal( data.reduce( (sum: number, category: { category_expense: string }) => sum + parseFloat(category.category_expense), - 0, - ), + 0 + ) ); }) .catch((error) => { console.error("API Error:", error); }); - }, [updateRecent]), + }, [updateRecent]) ); - const pieData = categories.map((category) => ({ + const categoryData = categories.map((category) => ({ value: parseFloat(category.category_expense), color: categoryColors.get(category.category_name) || "#cccccc", name: category.category_name, id: category.id, })); + console.log(categories); return ( <> @@ -105,13 +118,38 @@ export default function Home() { Hello {username} - - Total Spending - + + + Total Spending + + + + setGraphType(itemValue) + } + style={styles.graphTypePicker} + dropdownIconColor="#00629B" + > + + + + + {/* */} - + {graphType === "pie" && ( + + )} + {graphType === "area" && ( + + )} + - {pieData.map((category) => { + {categoryData.map((category) => { return ( { - return
AreaChart
; +interface Transaction { + id: number; + user_id: number; + item_name: string; + amount: string; + category_name: string; + date: string; +} + +const AreaChart = (props: { + categories: CategoryType[]; + totalBudget: number; + transactions?: Transaction[]; +}) => { + const [selectedCategory, setSelectedCategory] = useState( + null + ); + const [selectedMonth, setSelectedMonth] = useState( + new Date().toISOString().substring(0, 7) + ); + const [selectedYear, setSelectedYear] = useState( + new Date().getFullYear().toString() + ); + const [useTotalOrCategory, setUseTotalOrCategory] = useState< + "total" | "category" + >("total"); + + const chartWidth = 280; + const chartHeight = 180; + const padding = 20; + const graphWidth = chartWidth - padding * 2; + const graphHeight = chartHeight - padding * 2; + + // Calculate cumulative spending data from transactions + const chartData = useMemo(() => { + if (!props.transactions || props.transactions.length === 0) { + // If no transactions, return empty or flat line + return []; + } + + // Filter transactions by selected month/year + const filteredTransactions = props.transactions.filter((transaction) => { + const transactionDate = new Date(transaction.date); + const transactionMonth = transactionDate.toISOString().substring(0, 7); + const transactionYear = transactionDate.getFullYear().toString(); + + // Filter by category if in category mode + if (useTotalOrCategory === "category") { + if (!selectedCategory) { + return false; + } + const matchesCategory = + transaction.category_name === selectedCategory.category_name; + const matchesMonth = transactionMonth === selectedMonth; + const matchesYear = transactionYear === selectedYear; + return matchesCategory && matchesMonth && matchesYear; + } else { + // Total mode - filter by month/year only + const matchesMonth = transactionMonth === selectedMonth; + const matchesYear = transactionYear === selectedYear; + return matchesMonth && matchesYear; + } + }); + + if (filteredTransactions.length === 0) return []; + + // Sort transactions by date + const sortedTransactions = [...filteredTransactions].sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() + ); + + // Get the date range for the selected month + const [year, month] = selectedMonth.split("-").map(Number); + const daysInMonth = new Date(year, month, 0).getDate(); + const startDate = new Date(year, month - 1, 1); + const endDate = new Date(year, month - 1, daysInMonth); + + // Create daily cumulative data + const dailyData: { date: Date; cumulative: number }[] = []; + let cumulative = 0; + + // Process each day of the month + for (let day = 1; day <= daysInMonth; day++) { + const currentDate = new Date(year, month - 1, day); + + // Add transactions for this day + const dayTransactions = sortedTransactions.filter((t) => { + const tDate = new Date(t.date); + return ( + tDate.getDate() === day && + tDate.getMonth() === month - 1 && + tDate.getFullYear() === year + ); + }); + + // Add amounts for this day + dayTransactions.forEach((t) => { + cumulative += parseFloat(t.amount); + }); + + dailyData.push({ + date: currentDate, + cumulative: cumulative, + }); + } + + // Get budget for scaling + const budget = + useTotalOrCategory === "total" + ? parseFloat(String(props.totalBudget)) || 1 + : selectedCategory + ? parseFloat(selectedCategory.max_category_budget) || 1 + : 1; + + const maxCumulative = Math.max( + ...dailyData.map((d) => d.cumulative), + budget + ); + + // Convert to chart coordinates + return dailyData.map((data, index) => { + const x = (index / (daysInMonth - 1)) * graphWidth; + const y = graphHeight - (data.cumulative / maxCumulative) * graphHeight; + return { + x, + y, + value: data.cumulative, + date: data.date, + }; + }); + }, [ + useTotalOrCategory, + selectedCategory, + selectedMonth, + selectedYear, + props.transactions, + props.categories, + props.totalBudget, + graphWidth, + graphHeight, + ]); + + // Create area path + const areaPath = useMemo(() => { + if (chartData.length === 0) return ""; + if (chartData.length === 1) { + // Single point - draw a line from bottom to the point + const point = chartData[0]; + const bottomY = graphHeight + padding; + return `M ${point.x + padding} ${bottomY} L ${point.x + padding} ${point.y + padding} L ${point.x + padding} ${bottomY} Z`; + } + const points = chartData.map((d) => `${d.x + padding},${d.y + padding}`); + const firstX = chartData[0].x + padding; + const lastX = chartData[chartData.length - 1].x + padding; + const bottomY = graphHeight + padding; + + return `M ${firstX} ${bottomY} L ${points.join(" L ")} L ${lastX} ${bottomY} Z`; + }, [chartData, padding, graphHeight]); + + const currentValue = useMemo(() => { + if (chartData.length === 0) { + // Fallback to category totals if no transaction data + if (useTotalOrCategory === "total") { + return props.categories.reduce( + (sum, cat) => sum + parseFloat(cat.category_expense), + 0 + ); + } else { + return selectedCategory + ? parseFloat(selectedCategory.category_expense) + : 0; + } + } + // Return the last cumulative value (total for the month) + return chartData[chartData.length - 1]?.value || 0; + }, [chartData, useTotalOrCategory, selectedCategory, props.categories]); + + const currentBudget = useMemo(() => { + if (useTotalOrCategory === "total") { + return parseFloat(String(props.totalBudget)); + } else { + return selectedCategory + ? parseFloat(selectedCategory.max_category_budget) + : 0; + } + }, [useTotalOrCategory, selectedCategory, props.totalBudget]); + + return ( + + {/* Controls */} + + {/* Combined Category/Total Picker */} + + { + if (itemValue === "total") { + setUseTotalOrCategory("total"); + setSelectedCategory(null); + } else { + setUseTotalOrCategory("category"); + const category = props.categories.find( + (c) => c.category_name === itemValue + ); + setSelectedCategory(category || null); + } + }} + style={styles.categoryPicker} + > + + {props.categories.map((cat) => ( + + ))} + + + + {/* Month/Year Selectors */} + + + Month: + { + // Ensure the month uses the currently selected year + const [year, month] = monthStr.split("-"); + const newMonthStr = `${selectedYear}-${month}`; + setSelectedMonth(newMonthStr); + }} + style={styles.datePicker} + > + {Array.from({ length: 12 }, (_, i) => { + const year = parseInt(selectedYear); + const month = new Date(year, i, 1); + const monthStr = month.toISOString().substring(0, 7); + return ( + + ); + })} + + + + Year: + { + setSelectedYear(year); + // Update month to use new year but keep same month number + const currentMonth = selectedMonth.split("-")[1]; + setSelectedMonth(`${year}-${currentMonth}`); + }} + style={styles.datePicker} + > + {Array.from({ length: 3 }, (_, i) => { + const year = new Date().getFullYear() - 2 + i; + return ( + + ); + })} + + + + + + {/* Chart */} + + + + {/* Grid lines */} + {[0, 0.25, 0.5, 0.75, 1].map((ratio) => { + const y = padding + graphHeight - ratio * graphHeight; + return ( + + ); + })} + + {/* Area */} + {areaPath && areaPath !== "" && chartData.length > 0 && ( + + )} + + {/* No data message */} + {chartData.length === 0 && ( + + No data for this period + + )} + + + + {/* Budget/Spending Info - Moved outside chart */} + {chartData.length > 0 && ( + + + Spent: + ${currentValue.toFixed(2)} + + + Budget: + ${currentBudget.toFixed(2)} + + + Remaining: + currentBudget && styles.infoValueOver, + ]} + > + ${(currentBudget - currentValue).toFixed(2)} + + + + )} + + + ); }; export default AreaChart; + +const styles = StyleSheet.create({ + AreaContainer: { + justifyContent: "flex-start", + width: "100%", + alignItems: "center", + gap: 15, + }, + controlsContainer: { + width: "100%", + gap: 10, + }, + pickerContainer: { + width: "100%", + }, + categoryPicker: { + width: "100%", + height: 45, + backgroundColor: "#F5F5F5", + borderRadius: 8, + borderWidth: 1, + borderColor: "#D0D0D0", + }, + chartContainer: { + alignItems: "center", + justifyContent: "center", + width: "100%", + }, + infoContainer: { + marginTop: 15, + width: "100%", + flexDirection: "row", + justifyContent: "space-around", + paddingVertical: 10, + backgroundColor: "#F8F8F8", + borderRadius: 8, + paddingHorizontal: 10, + }, + infoRow: { + flexDirection: "column", + alignItems: "center", + gap: 4, + }, + infoLabel: { + fontSize: 11, + color: "#666", + fontWeight: "500", + }, + infoValue: { + fontSize: 16, + color: "#333", + fontWeight: "bold", + }, + infoValueOver: { + color: "#D32F2F", + }, + dateRow: { + flexDirection: "row", + gap: 10, + width: "100%", + justifyContent: "space-between", + }, + datePickerContainer: { + flex: 1, + gap: 5, + }, + dateLabel: { + fontSize: 12, + color: "#666", + fontWeight: "500", + }, + datePicker: { + height: 40, + backgroundColor: "#F5F5F5", + borderRadius: 8, + borderWidth: 1, + borderColor: "#D0D0D0", + }, +}); diff --git a/frontend/utils/FrontendTypes.ts b/frontend/utils/FrontendTypes.ts new file mode 100644 index 0000000..c8c0e7e --- /dev/null +++ b/frontend/utils/FrontendTypes.ts @@ -0,0 +1,10 @@ + +export type CategoryType = { + category_expense: string; + category_name: string; + id: number; + max_category_budget: string; + user_id: number; +} + +export type GraphType = "pie" | "area"; \ No newline at end of file From 52f6571ea26f4fba642aa7e23f38c0f7592b9fb2 Mon Sep 17 00:00:00 2001 From: RamonsArchive Date: Thu, 13 Nov 2025 21:13:01 -0800 Subject: [PATCH 5/8] fixed linting errors --- README.md | 2 +- frontend/app/(tabs)/index.tsx | 8 ++++---- frontend/components/Graphs/AreaChart.tsx | 4 +--- frontend/utils/FrontendTypes.ts | 15 +++++++-------- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9e42aea..eaa10f5 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ We welcome contributions from the UCSD community and beyond! Please see our [CON 1. Fork the repository 2. Create a feature branch: `git checkout -b feature/amazing-feature` 3. Make your changes and test thoroughly -4. Format with Prettier: `npm run format`, +4. Format with Prettier in backend: `npm run format`, 5. Run linting: `npm run lint-check` 6. Commit your changes: `git commit -m 'Add amazing feature'` 7. Push to your branch: `git push origin feature/amazing-feature` diff --git a/frontend/app/(tabs)/index.tsx b/frontend/app/(tabs)/index.tsx index 7916d6e..3f9d276 100644 --- a/frontend/app/(tabs)/index.tsx +++ b/frontend/app/(tabs)/index.tsx @@ -51,7 +51,7 @@ export default function Home() { Accept: "application/json", "Content-Type": "application/json", }, - } + }, ) .then((res) => { return res.json(); @@ -93,14 +93,14 @@ export default function Home() { data.reduce( (sum: number, category: { category_expense: string }) => sum + parseFloat(category.category_expense), - 0 - ) + 0, + ), ); }) .catch((error) => { console.error("API Error:", error); }); - }, [updateRecent]) + }, [updateRecent]), ); const categoryData = categories.map((category) => ({ diff --git a/frontend/components/Graphs/AreaChart.tsx b/frontend/components/Graphs/AreaChart.tsx index d923ede..efc8721 100644 --- a/frontend/components/Graphs/AreaChart.tsx +++ b/frontend/components/Graphs/AreaChart.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo } from "react"; import { CategoryType } from "@/utils/FrontendTypes"; -import { View, StyleSheet, Text, TouchableOpacity } from "react-native"; +import { View, StyleSheet, Text } from "react-native"; import { Picker } from "@react-native-picker/picker"; import Svg, { Path, G, Line, Text as SvgText } from "react-native-svg"; @@ -78,8 +78,6 @@ const AreaChart = (props: { // Get the date range for the selected month const [year, month] = selectedMonth.split("-").map(Number); const daysInMonth = new Date(year, month, 0).getDate(); - const startDate = new Date(year, month - 1, 1); - const endDate = new Date(year, month - 1, daysInMonth); // Create daily cumulative data const dailyData: { date: Date; cumulative: number }[] = []; diff --git a/frontend/utils/FrontendTypes.ts b/frontend/utils/FrontendTypes.ts index c8c0e7e..099f5b1 100644 --- a/frontend/utils/FrontendTypes.ts +++ b/frontend/utils/FrontendTypes.ts @@ -1,10 +1,9 @@ - export type CategoryType = { - category_expense: string; - category_name: string; - id: number; - max_category_budget: string; - user_id: number; -} + category_expense: string; + category_name: string; + id: number; + max_category_budget: string; + user_id: number; +}; -export type GraphType = "pie" | "area"; \ No newline at end of file +export type GraphType = "pie" | "area"; From 90f8d467d841606b70ecdecbb5e814b32b79d563 Mon Sep 17 00:00:00 2001 From: RamonsArchive Date: Thu, 13 Nov 2025 21:29:11 -0800 Subject: [PATCH 6/8] fixed linting erros with prettier --write --- frontend/components/Graphs/AreaChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/Graphs/AreaChart.tsx b/frontend/components/Graphs/AreaChart.tsx index efc8721..406601f 100644 --- a/frontend/components/Graphs/AreaChart.tsx +++ b/frontend/components/Graphs/AreaChart.tsx @@ -223,7 +223,7 @@ const AreaChart = (props: { value={cat.category_name} /> ))} - +
{/* Month/Year Selectors */} From b2222710ef820e5427bef625856ec23fe93517cc Mon Sep 17 00:00:00 2001 From: RamonsArchive Date: Thu, 13 Nov 2025 21:30:19 -0800 Subject: [PATCH 7/8] ran prettier format again in AreaCheart --- frontend/components/Graphs/AreaChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/Graphs/AreaChart.tsx b/frontend/components/Graphs/AreaChart.tsx index 406601f..efc8721 100644 --- a/frontend/components/Graphs/AreaChart.tsx +++ b/frontend/components/Graphs/AreaChart.tsx @@ -223,7 +223,7 @@ const AreaChart = (props: { value={cat.category_name} /> ))} - +
{/* Month/Year Selectors */} From 43100eec2020280123c23bcad214b3aa696daa16 Mon Sep 17 00:00:00 2001 From: RamonsArchive Date: Thu, 13 Nov 2025 21:31:15 -0800 Subject: [PATCH 8/8] fixed AraeChart lint error --- frontend/components/Graphs/AreaChart.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/components/Graphs/AreaChart.tsx b/frontend/components/Graphs/AreaChart.tsx index efc8721..cc7b97e 100644 --- a/frontend/components/Graphs/AreaChart.tsx +++ b/frontend/components/Graphs/AreaChart.tsx @@ -19,13 +19,13 @@ const AreaChart = (props: { transactions?: Transaction[]; }) => { const [selectedCategory, setSelectedCategory] = useState( - null + null, ); const [selectedMonth, setSelectedMonth] = useState( - new Date().toISOString().substring(0, 7) + new Date().toISOString().substring(0, 7), ); const [selectedYear, setSelectedYear] = useState( - new Date().getFullYear().toString() + new Date().getFullYear().toString(), ); const [useTotalOrCategory, setUseTotalOrCategory] = useState< "total" | "category" @@ -72,7 +72,7 @@ const AreaChart = (props: { // Sort transactions by date const sortedTransactions = [...filteredTransactions].sort( - (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), ); // Get the date range for the selected month @@ -118,7 +118,7 @@ const AreaChart = (props: { const maxCumulative = Math.max( ...dailyData.map((d) => d.cumulative), - budget + budget, ); // Convert to chart coordinates @@ -167,7 +167,7 @@ const AreaChart = (props: { if (useTotalOrCategory === "total") { return props.categories.reduce( (sum, cat) => sum + parseFloat(cat.category_expense), - 0 + 0, ); } else { return selectedCategory @@ -208,7 +208,7 @@ const AreaChart = (props: { } else { setUseTotalOrCategory("category"); const category = props.categories.find( - (c) => c.category_name === itemValue + (c) => c.category_name === itemValue, ); setSelectedCategory(category || null); }