From 13486c5cd9ca0153d9c691c347f6146cbdbf1069 Mon Sep 17 00:00:00 2001 From: ryansoe Date: Thu, 23 Apr 2026 14:05:48 -0700 Subject: [PATCH 1/4] feat: homepage chart selection --- frontend/app/(tabs)/index.tsx | 147 +++++++++++++-- frontend/app/demo.tsx | 16 +- frontend/components/Graphs/BarChart.tsx | 107 +++++------ frontend/components/Graphs/LineChart.tsx | 177 ++++++++---------- frontend/components/Graphs/PieChart.tsx | 27 ++- .../components/Home/WeeklySpendingSection.tsx | 10 +- .../primitives/SegmentedControl.tsx | 48 +++-- 7 files changed, 313 insertions(+), 219 deletions(-) diff --git a/frontend/app/(tabs)/index.tsx b/frontend/app/(tabs)/index.tsx index fd9b483..666636a 100644 --- a/frontend/app/(tabs)/index.tsx +++ b/frontend/app/(tabs)/index.tsx @@ -1,5 +1,6 @@ import { ScrollView, XStack, YStack } from "tamagui"; import { useState, useCallback } from "react"; +import { useWindowDimensions } from "react-native"; import { BACKEND_PORT } from "@env"; import { useAuth } from "@/context/authContext"; import { useFocusEffect } from "@react-navigation/native"; @@ -7,11 +8,14 @@ import { Screen } from "@/components/primitives/Screen"; import { AppText } from "@/components/primitives/AppText"; import { Card } from "@/components/primitives/Card"; import { SectionTitle } from "@/components/primitives/SectionTitle"; +import { SegmentedControl } from "@/components/primitives/SegmentedControl"; import { QuickActionsSection } from "@/components/Home/QuickActionsSection"; import { WeeklySpendingSection } from "@/components/Home/WeeklySpendingSection"; import NewTransactionButton from "@/components/NewTransaction/NewTransactionButton"; import TransactionHistory from "@/components/TransactionHistory/TransactionHistory"; import CustomPieChart from "@/components/Graphs/PieChart"; +import CustomLineChart from "@/components/Graphs/LineChart"; +import CustomBarChart from "@/components/Graphs/BarChart"; interface Category { id: number; @@ -29,6 +33,19 @@ interface Transaction { date: string; } +type ChartType = "pie" | "line" | "bar"; +type Range = "1M" | "3M" | "6M" | "1Y"; + +const RANGE_CONFIG: Record< + Range, + { period: "daily" | "weekly"; months: number } +> = { + "1M": { period: "daily", months: 1 }, + "3M": { period: "weekly", months: 3 }, + "6M": { period: "weekly", months: 6 }, + "1Y": { period: "weekly", months: 12 }, +}; + const categoryColors = new Map([ ["Food", "#b8b8ff"], ["Shopping", "#fff3b0"], @@ -45,7 +62,18 @@ export default function Home() { const [categories, setCategories] = useState([]); const [username, setUsername] = useState(""); const [forceOpenTransaction, setForceOpenTransaction] = useState(false); + + const [chartType, setChartType] = useState("pie"); + const [range, setRange] = useState("3M"); + const [lineData, setLineData] = useState<{ date: string; total: number }[]>( + [] + ); + const [barData, setBarData] = useState<{ name: string; value: number }[]>([]); + const { userId } = useAuth(); + const screenWidth = useWindowDimensions().width; + // page px $4 (16) * 2 + card padding $4 (16) * 2 = 64 + const chartCardWidth = screenWidth - 64; useFocusEffect( useCallback(() => { @@ -57,7 +85,7 @@ export default function Home() { Accept: "application/json", "Content-Type": "application/json", }, - }, + } ) .then((res) => res.json()) .then((data) => { @@ -89,14 +117,48 @@ 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]), + + if (chartType === "line") { + const { period, months } = RANGE_CONFIG[range]; + fetch( + `http://localhost:${BACKEND_PORT}/transactions/spendingTrend/${userId}?period=${period}&months=${months}`, + { method: "GET" } + ) + .then((res) => res.json()) + .then((data) => setLineData(data)) + .catch((error) => { + console.error("API Error:", error); + }); + } + + if (chartType === "bar") { + const { months } = RANGE_CONFIG[range]; + fetch( + `http://localhost:${BACKEND_PORT}/transactions/monthly/${userId}`, + { + method: "GET", + } + ) + .then((res) => res.json()) + .then((data: { month: string; total: number | string }[]) => { + const mapped = data.map((d) => ({ + name: d.month, + value: parseFloat(String(d.total)), + })); + setBarData(mapped.slice(-months)); + }) + .catch((error) => { + console.error("API Error:", error); + }); + } + }, [updateRecent, chartType, range]) ); const pieData = categories.map((category) => ({ @@ -116,20 +178,69 @@ export default function Home() { - - - {pieData.map((category) => ( - - - {category.name} - - ))} - + + + + {chartType !== "pie" && ( + + + + )} + + + {chartType === "pie" && ( + + )} + {chartType === "line" && ( + + )} + {chartType === "bar" && ( + + )} + + + {chartType === "pie" && ( + + {pieData.map((category) => ( + + + {category.name} + + ))} + + )} diff --git a/frontend/app/demo.tsx b/frontend/app/demo.tsx index 5385693..55cfb23 100644 --- a/frontend/app/demo.tsx +++ b/frontend/app/demo.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { ScrollView } from "react-native"; import { Screen } from "../components/primitives/Screen"; import { AppText } from "../components/primitives/AppText"; @@ -10,7 +10,10 @@ import { SegmentedControl } from "../components/primitives/SegmentedControl"; import { YStack, XStack, Circle } from "tamagui"; import { Ionicons } from "@expo/vector-icons"; +type DemoPeriod = "1D" | "1W" | "1M" | "1Y"; + export default function DemoScreen() { + const [demoPeriod, setDemoPeriod] = useState("1W"); return ( @@ -101,7 +104,16 @@ export default function DemoScreen() { Segmented Control Example - + diff --git a/frontend/components/Graphs/BarChart.tsx b/frontend/components/Graphs/BarChart.tsx index 7cadb5c..aa0b3aa 100644 --- a/frontend/components/Graphs/BarChart.tsx +++ b/frontend/components/Graphs/BarChart.tsx @@ -1,90 +1,84 @@ import React from "react"; -import { View, Text, StyleSheet } from "react-native"; import Svg, { Rect, Text as SvgText } from "react-native-svg"; +import { YStack, useTheme } from "tamagui"; +import { AppText } from "@/components/primitives/AppText"; + +interface BarChartDatum { + name: string; + value: number; +} export default function BarChart({ data, - size, + width, + height, total, }: { - data: any[]; - size: number; + data: BarChartDatum[]; + width: number; + height: number; total: number; }) { - const chartHeight = size; - const chartWidth = size * 1.2; + const theme = useTheme(); + const primary = theme.primary?.val ?? "#395773"; + const mutedColor = theme.textMuted?.val ?? "#7B8A96"; + const textColor = theme.color?.val ?? "#1C252E"; - const barSpacing = 25; // consistent spacing - const barWidth = (chartWidth - barSpacing * (data.length + 1)) / data.length; + const topPadding = 28; + const bottomPadding = 32; + const barSpacing = 16; + const count = Math.max(data.length, 1); + const barWidth = (width - barSpacing * (count + 1)) / count; - const maxValue = Math.max(...data.map((d: any) => d.value), 1); + const maxValue = Math.max(...data.map((d) => d.value), 1); + const drawableHeight = height - topPadding - bottomPadding; - // 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) => { + + + {data.map((item, index) => { 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"; + const barHeight = (item.value / maxValue) * drawableHeight; + const y = topPadding + (drawableHeight - barHeight); return ( - - {/* Value label */} + - {item.value} + ${item.value.toFixed(0)} - {/* Bar */} - {/* Category label */} {formatMonth(item.name)} @@ -93,20 +87,9 @@ export default function BarChart({ })} - Total: ${total.toFixed(2)} - + + ${total.toFixed(2)} + + ); } - -const styles = StyleSheet.create({ - container: { - width: "100%", - alignItems: "center", - paddingVertical: 10, - }, - totalText: { - marginTop: 10, - fontSize: 18, - fontWeight: "600", - }, -}); diff --git a/frontend/components/Graphs/LineChart.tsx b/frontend/components/Graphs/LineChart.tsx index ba532ed..611d2d7 100644 --- a/frontend/components/Graphs/LineChart.tsx +++ b/frontend/components/Graphs/LineChart.tsx @@ -1,34 +1,38 @@ import React from "react"; -import { View, StyleSheet } from "react-native"; import Svg, { Path, Line, Text, G } from "react-native-svg"; +import { YStack, useTheme } from "tamagui"; +import { AppText } from "@/components/primitives/AppText"; -// GET /transactions/spendingTrend/:user_id -// Query Params: ?period=weekly&months=3 -// Response: [ -// { date: "2024-01-01", total: 150.00 }, -// { date: "2024-01-08", total: 200.00 }, -// ... -// ] +interface LineChartDatum { + date: string; + total: number; +} export default function LineChart(props: { - data: any[]; + data: LineChartDatum[]; width: number; height: number; + total?: number; }) { - const padding = 20; + const theme = useTheme(); + const primary = theme.primary?.val ?? "#395773"; + const mutedColor = theme.textMuted?.val ?? "#7B8A96"; + + const padding = 28; const chartWidth = props.width - 2 * padding; const chartHeight = props.height - 2 * padding; - function createLine(data: any[]) { + const values = props.data.map((item) => item.total); + const maxValue = values.length > 0 ? Math.max(...values) : 0; + const minValue = values.length > 0 ? Math.min(...values) : 0; + + function createLine(data: LineChartDatum[]) { if (data.length === 0) return ""; - const values = data.map((item) => item.total); - const maxValue = Math.max(...values); - const minValue = Math.min(...values); const valueRange = maxValue - minValue || 1; - const verticalMargin = chartHeight * 0.1; // 10% margin top and bottom + const verticalMargin = chartHeight * 0.1; - const pathData = data + return data .map((item, index) => { const x = padding + (index / (data.length - 1 || 1)) * chartWidth; const y = @@ -41,16 +45,13 @@ export default function LineChart(props: { return index === 0 ? `M ${x} ${y}` : `L ${x} ${y}`; }) .join(" "); - - return pathData; } - const values = props.data.map((item) => item.total); - const maxValue = values.length > 0 ? Math.max(...values) : 0; - const minValue = values.length > 0 ? Math.min(...values) : 0; + const numYLabels = 5; + const numXLabels = Math.min(5, props.data.length); return ( - + {/* Y-axis */} @@ -59,8 +60,8 @@ export default function LineChart(props: { y1={padding} x2={padding} y2={props.height - padding} - stroke="#333" - strokeWidth={2} + stroke={mutedColor} + strokeWidth={1} /> {/* X-axis */} {/* Y-axis labels */} - {(() => { - const numYLabels = 5; - const yLabelIndices = []; - - for (let i = 0; i < numYLabels; i++) { - yLabelIndices.push(i); - } + {Array.from({ length: numYLabels }, (_, i) => { + const value = + maxValue - (i / (numYLabels - 1)) * (maxValue - minValue); + const y = + padding + (i / (numYLabels - 1)) * (props.height - 2 * padding); + + return ( + + ${value.toFixed(0)} + + ); + })} - return yLabelIndices.map((i) => { - const value = - maxValue - (i / (numYLabels - 1)) * (maxValue - minValue); - const y = - padding + (i / (numYLabels - 1)) * (props.height - 2 * padding); + {/* X-axis labels */} + {props.data.length > 0 && + Array.from({ length: numXLabels }, (_, i) => { + const index = Math.floor( + (i / (numXLabels - 1 || 1)) * (props.data.length - 1) + ); + const x = + padding + (index / (props.data.length - 1 || 1)) * chartWidth; + const date = new Date(props.data[index].date); return ( - ${value.toFixed(0)} + {date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + })} ); - }); - })()} - - {/* X-axis labels */} - {props.data.length > 0 && ( - <> - {(() => { - const numLabels = Math.min(5, props.data.length); - const labelIndices = []; - - for (let i = 0; i < numLabels; i++) { - const index = Math.floor( - (i / (numLabels - 1 || 1)) * (props.data.length - 1), - ); - labelIndices.push(index); - } - - return labelIndices.map((index) => { - const x = - padding + - (index / (props.data.length - 1 || 1)) * chartWidth; - const date = new Date(props.data[index].date); - - return ( - - {date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - })} - - ); - }); - })()} - - )} + })} {/* Line path */} - + + {props.total !== undefined && ( + + ${props.total.toFixed(2)} + + )} + ); } - -const styles = StyleSheet.create({ - LineContainer: { - justifyContent: "flex-start", - width: "100%", - alignItems: "center", - }, -}); diff --git a/frontend/components/Graphs/PieChart.tsx b/frontend/components/Graphs/PieChart.tsx index d9aec93..abeefa4 100644 --- a/frontend/components/Graphs/PieChart.tsx +++ b/frontend/components/Graphs/PieChart.tsx @@ -1,22 +1,25 @@ -import { ColorValue } from "react-native"; import Svg, { Path, G } from "react-native-svg"; import { YStack } from "tamagui"; import { AppText } from "@/components/primitives/AppText"; +interface PieChartDatum { + value: number; + color: string; + name?: string; + id?: number; +} + export default function DoughnutChart(props: { total: number; size: number; - data: any[]; + data: PieChartDatum[]; }) { const radius = props.size / 2; const innerRadius = radius * 0.65; - const total = props.data.reduce( - (acc: any, item: { value: any }) => acc + item.value, - 0 - ); + const total = props.data.reduce((acc, item) => acc + item.value, 0); let startAngle = 0; - function createArc(value: number, color?: ColorValue) { + function createArc(value: number, color: string, key: number) { const angle = (value / total) * 2 * Math.PI; const endAngle = startAngle + angle; @@ -37,7 +40,7 @@ export default function DoughnutChart(props: { startAngle = endAngle; - return ; + return ; } return ( @@ -53,9 +56,13 @@ export default function DoughnutChart(props: { height={props.size} style={{ position: "absolute" }} > - {props.data.map((item) => createArc(item.value, item.color))} + + {props.data.map((item, index) => + createArc(item.value, item.color, index) + )} + - + ${props.total.toFixed(2)} diff --git a/frontend/components/Home/WeeklySpendingSection.tsx b/frontend/components/Home/WeeklySpendingSection.tsx index 90099f8..b101ec7 100644 --- a/frontend/components/Home/WeeklySpendingSection.tsx +++ b/frontend/components/Home/WeeklySpendingSection.tsx @@ -55,8 +55,14 @@ export const WeeklySpendingSection: React.FC = ({ setPeriod(val)} + value={period} + onValueChange={setPeriod} + options={[ + { label: "1D", value: "1D" }, + { label: "1W", value: "1W" }, + { label: "1M", value: "1M" }, + { label: "1Y", value: "1Y" }, + ]} /> { + label: string; + value: T; +} -interface SegmentedControlProps { - onValueChange?: (_value: Period) => void; // eslint-disable-line no-unused-vars - defaultValue?: Period; +interface SegmentedControlProps { + options: SegmentedControlOption[]; + value: T; + // eslint-disable-next-line no-unused-vars + onValueChange: (value: T) => void; } -export const SegmentedControl: React.FC = ({ +export function SegmentedControl({ + options, + value, onValueChange, - defaultValue = "1M", -}) => { - const [active, setActive] = useState(defaultValue); - - const handlePress = (period: Period) => { - setActive(period); - onValueChange?.(period); - }; - +}: SegmentedControlProps) { return ( - {PERIODS.map((period) => { - const isActive = active === period; + {options.map((option) => { + const isActive = option.value === value; return ( handlePress(period)} + onPress={() => onValueChange(option.value)} > - {period} + {option.label} ); })} ); -}; +} From 8829e95858a39862c213488a1794c7f81219f747 Mon Sep 17 00:00:00 2001 From: ryansoe Date: Thu, 23 Apr 2026 14:12:28 -0700 Subject: [PATCH 2/4] fix: correctly show total spending over time period --- frontend/app/(tabs)/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/(tabs)/index.tsx b/frontend/app/(tabs)/index.tsx index 666636a..821d6dd 100644 --- a/frontend/app/(tabs)/index.tsx +++ b/frontend/app/(tabs)/index.tsx @@ -213,7 +213,7 @@ export default function Home() { data={lineData} width={chartCardWidth} height={260} - total={total} + total={lineData.reduce((sum, d) => sum + d.total, 0)} /> )} {chartType === "bar" && ( @@ -221,7 +221,7 @@ export default function Home() { data={barData} width={chartCardWidth} height={260} - total={total} + total={barData.reduce((sum, d) => sum + d.value, 0)} /> )} From eaa39bfd8b0867ce1655b46d59d6221aeff0bdce Mon Sep 17 00:00:00 2001 From: ryansoe Date: Thu, 23 Apr 2026 14:38:55 -0700 Subject: [PATCH 3/4] fix: font fore line and bar chart --- frontend/components/Graphs/BarChart.tsx | 4 ++-- frontend/components/Graphs/LineChart.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/components/Graphs/BarChart.tsx b/frontend/components/Graphs/BarChart.tsx index aa0b3aa..eec9fff 100644 --- a/frontend/components/Graphs/BarChart.tsx +++ b/frontend/components/Graphs/BarChart.tsx @@ -55,7 +55,7 @@ export default function BarChart({ fontSize={11} fill={textColor} textAnchor="middle" - fontFamily="Inter" + fontFamily="Inter, Helvetica, Arial, sans-serif" fontWeight="600" > ${item.value.toFixed(0)} @@ -77,7 +77,7 @@ export default function BarChart({ fontSize={11} fill={mutedColor} textAnchor="middle" - fontFamily="Inter" + fontFamily="Inter, Helvetica, Arial, sans-serif" fontWeight="600" > {formatMonth(item.name)} diff --git a/frontend/components/Graphs/LineChart.tsx b/frontend/components/Graphs/LineChart.tsx index 611d2d7..cb2d8cc 100644 --- a/frontend/components/Graphs/LineChart.tsx +++ b/frontend/components/Graphs/LineChart.tsx @@ -88,7 +88,7 @@ export default function LineChart(props: { fontSize={10} fill={mutedColor} textAnchor="end" - fontFamily="Inter" + fontFamily="Inter, Helvetica, Arial, sans-serif" fontWeight="600" > ${value.toFixed(0)} @@ -114,7 +114,7 @@ export default function LineChart(props: { fontSize={10} fill={mutedColor} textAnchor="middle" - fontFamily="Inter" + fontFamily="Inter, Helvetica, Arial, sans-serif" fontWeight="600" > {date.toLocaleDateString("en-US", { From e365fb83ceaaa9ccc09fac6d20d719ba69a20447 Mon Sep 17 00:00:00 2001 From: ryansoe Date: Wed, 29 Apr 2026 15:26:34 -0700 Subject: [PATCH 4/4] feat: history chart switcher and edit goals --- frontend/app/(tabs)/History.tsx | 228 +++++++++++++++------ frontend/components/GoalsList/GoalsRow.tsx | 18 +- 2 files changed, 176 insertions(+), 70 deletions(-) diff --git a/frontend/app/(tabs)/History.tsx b/frontend/app/(tabs)/History.tsx index f131012..f93d5c6 100644 --- a/frontend/app/(tabs)/History.tsx +++ b/frontend/app/(tabs)/History.tsx @@ -1,6 +1,7 @@ import { View, StyleSheet, Text, TouchableOpacity } from "react-native"; import { Picker } from "@react-native-picker/picker"; import { useCallback, useState } from "react"; +import { XStack, YStack } from "tamagui"; import BudgetChart from "@/components/HistoryBudget/BudgetChart"; import FullTransactionHistory from "@/components/TransactionHistory/FullTransactionHistory"; import { useFocusEffect } from "@react-navigation/native"; @@ -8,8 +9,41 @@ import { BACKEND_PORT } from "@env"; import { useAuth } from "@/context/authContext"; import { ScrollView } from "react-native-gesture-handler"; import CustomLineChart from "@/components/Graphs/LineChart"; +import CustomBarChart from "@/components/Graphs/BarChart"; +import CustomPieChart from "@/components/Graphs/PieChart"; +import { SegmentedControl } from "@/components/primitives/SegmentedControl"; +import { AppText } from "@/components/primitives/AppText"; import { useWindowDimensions } from "react-native"; +type ChartType = "pie" | "line" | "bar"; +type Range = "1M" | "3M" | "6M" | "1Y"; + +const RANGE_CONFIG: Record< + Range, + { period: "daily" | "weekly"; months: number } +> = { + "1M": { period: "daily", months: 1 }, + "3M": { period: "weekly", months: 3 }, + "6M": { period: "weekly", months: 6 }, + "1Y": { period: "weekly", months: 12 }, +}; + +const categoryColors = new Map([ + ["Food", "#b8b8ff"], + ["Shopping", "#fff3b0"], + ["Transportation", "#588157"], + ["Subscriptions", "#ff9b85"], + ["Other", "#2b2d42"], +]); + +interface Category { + id: number; + category_name: string; + category_expense: string; + max_category_budget: string; + user_id: number; +} + // Page for showing full Expense History along with the user's budget and how much they spent compared to their budget export default function History() { // Sorting State @@ -23,23 +57,23 @@ export default function History() { // Filter State const [filterType, setFilterType] = useState("none"); // "none", "month", "category" const [selectedMonth, setSelectedMonth] = useState( - new Date().toISOString().substring(0, 7), + new Date().toISOString().substring(0, 7) ); // YYYY-MM format const [selectedCategory, setSelectedCategory] = useState("Food"); const [showFilterOptions, setShowFilterOptions] = useState(false); - const [lineChartData, setLineChartData] = useState([]); - const [selectedTimeRange, setSelectedTimeRange] = useState("3months"); + + // Chart switcher state + const [chartType, setChartType] = useState("pie"); + const [range, setRange] = useState("3M"); + const [lineData, setLineData] = useState<{ date: string; total: number }[]>( + [] + ); + const [barData, setBarData] = useState<{ name: string; value: number }[]>([]); + const [chartCategories, setChartCategories] = useState([]); const screenWidth = useWindowDimensions().width; - const chartWidth = screenWidth * 0.75; - - // Time range configuration - const timeRangeConfig = { - "1month": { period: "daily", months: 1, label: "1 Month" }, - "3months": { period: "weekly", months: 3, label: "3 Months" }, - "6months": { period: "weekly", months: 6, label: "6 Months" }, - "1year": { period: "weekly", months: 12, label: "1 Year" }, - }; + // page paddingHorizontal 20 * 2 + card padding 15 * 2 = 70 + const chartCardWidth = screenWidth * 0.9 - 30; // Get unique categories from transactions const getUniqueCategories = () => { @@ -69,7 +103,7 @@ export default function History() { Accept: "application/json", "Content-Type": "application/json", }, - }, + } ) .then((res) => res.json()) .then((data) => { @@ -91,28 +125,60 @@ export default function History() { .catch((error) => { console.error("API Error:", error); }); - const config = - timeRangeConfig[selectedTimeRange as keyof typeof timeRangeConfig]; - fetch( - `http://localhost:${BACKEND_PORT}/transactions/spendingTrend/${userId}?period=${config.period}&months=${config.months}`, - { - method: "GET", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - }, - ) + + fetch(`http://localhost:${BACKEND_PORT}/users/category/${userId}`, { + method: "GET", + }) .then((res) => res.json()) .then((data) => { - setLineChartData(data); + setChartCategories(data); }) .catch((error) => { console.error("API Error:", error); }); - }, [selectedTimeRange]), + + if (chartType === "line") { + const { period, months } = RANGE_CONFIG[range]; + fetch( + `http://localhost:${BACKEND_PORT}/transactions/spendingTrend/${userId}?period=${period}&months=${months}`, + { method: "GET" } + ) + .then((res) => res.json()) + .then((data) => setLineData(data)) + .catch((error) => { + console.error("API Error:", error); + }); + } + + if (chartType === "bar") { + const { months } = RANGE_CONFIG[range]; + fetch( + `http://localhost:${BACKEND_PORT}/transactions/monthly/${userId}`, + { method: "GET" } + ) + .then((res) => res.json()) + .then((data: { month: string; total: number | string }[]) => { + const mapped = data.map((d) => ({ + name: d.month, + value: parseFloat(String(d.total)), + })); + setBarData(mapped.slice(-months)); + }) + .catch((error) => { + console.error("API Error:", error); + }); + } + }, [chartType, range]) ); + const pieData = chartCategories.map((category) => ({ + value: parseFloat(category.category_expense), + color: categoryColors.get(category.category_name) || "#cccccc", + name: category.category_name, + id: category.id, + })); + const pieTotal = pieData.reduce((sum, d) => sum + d.value, 0); + // Toggle filter options visibility const toggleFilterOptions = () => { setShowFilterOptions(!showFilterOptions); @@ -175,8 +241,8 @@ export default function History() { const months = [ ...new Set( AllTransactions.map((trans) => - new Date(trans.date).toISOString().substring(0, 7), - ), + new Date(trans.date).toISOString().substring(0, 7) + ) ), ] .sort() @@ -197,30 +263,74 @@ export default function History() { /> - - Spending Trend + + Total Spending - {/* */} - + + + + {chartType !== "pie" && ( + + )} - - Time Range: - setSelectedTimeRange(itemValue)} - style={styles.timeRangePicker} - > - - - - - - + + {chartType === "pie" && ( + + )} + {chartType === "line" && ( + sum + d.total, 0)} + /> + )} + {chartType === "bar" && ( + sum + d.value, 0)} + /> + )} + + + {chartType === "pie" && ( + + {pieData.map((category) => ( + + + {category.name} + + ))} + + )} + @@ -401,24 +511,6 @@ const styles = StyleSheet.create({ alignItems: "center", marginVertical: 10, }, - timeRangePickerContainer: { - flexDirection: "row", - alignItems: "center", - marginTop: 15, - width: "100%", - justifyContent: "center", - }, - timeRangeLabel: { - fontSize: 16, - fontWeight: "600", - marginRight: 10, - }, - timeRangePicker: { - height: 50, - width: 150, - backgroundColor: "#E6E6E6", - borderRadius: 5, - }, filterSortContainer: { flexDirection: "row", justifyContent: "space-between", diff --git a/frontend/components/GoalsList/GoalsRow.tsx b/frontend/components/GoalsList/GoalsRow.tsx index 81ee4c5..e99b8ea 100644 --- a/frontend/components/GoalsList/GoalsRow.tsx +++ b/frontend/components/GoalsList/GoalsRow.tsx @@ -48,7 +48,7 @@ export default function GoalsRow(props: any) { setModalVisible(true)} - style={{ height: "100%", width: "80%" }} + style={{ height: "100%", width: "70%" }} > {props.title} {props.date} @@ -66,7 +66,16 @@ export default function GoalsRow(props: any) { ) : null} - + + setModalVisible(true)} + hitSlop={8} + accessibilityLabel="Edit goal" + > + + + +