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
65 changes: 65 additions & 0 deletions backend/src/controllers/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,68 @@ export const deleteTransaction: RequestHandler = async (req, res) => {
res.status(500).json({ error: `Internal server error: ${error}` });
}
};

//get spending trend per category for multi-line chart comparison
export const multiLineTransaction: RequestHandler = async (req, res) => {
try {
const { user_id } = req.params;
const periodParam = req.query.period as string;
const monthsParam = req.query.months as string;

const period = periodParam || "weekly";
const months = monthsParam ? parseInt(monthsParam) : 3;

// Validate input
if (!user_id) {
return res.status(400).json({ error: "Missing user_id" });
}
if (period !== "weekly" && period !== "daily") {
return res.status(400).json({ error: "Period must be 'weekly' or 'daily'" });
}
if (months <= 0 || isNaN(months)) {
return res.status(400).json({ error: "Months must be a positive number" });
}

// Build query
let getMultiLineTrend: string;

if (period === "weekly") {
getMultiLineTrend = `
SELECT
date_trunc('week', date)::date as date,
category_name,
COALESCE(SUM(amount), 0) as total
FROM transactions
WHERE user_id = $1
AND date >= NOW() - INTERVAL '1 month' * $2
GROUP BY date_trunc('week', date), category_name
ORDER BY date ASC, category_name ASC;
`;
} else {
getMultiLineTrend = `
SELECT
date::date as date,
category_name,
COALESCE(SUM(amount), 0) as total
FROM transactions
WHERE user_id = $1
AND date >= NOW() - INTERVAL '1 month' * $2
GROUP BY date::date, category_name
ORDER BY date ASC, category_name ASC;
`;
}

const result = await client.query(getMultiLineTrend, [user_id, months]);

const formattedRows = result.rows.map((row) => ({
date: new Date(row.date).toISOString().split("T")[0], // Format as YYYY-MM-DD
category: row.category_name,
total: parseFloat(row.total),
}));

res.status(200).json(formattedRows);
} catch (error) {
console.error("Error fetching multi-line category trends:", error);
res.status(500).json({ error: `Internal server error: ${error}` });
}
};
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,
multiLineTransaction,
} 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("/multiTrend/:user_id", multiLineTransaction);

export default router;
97 changes: 94 additions & 3 deletions frontend/app/(tabs)/History.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { View, StyleSheet, Text, TouchableOpacity } from "react-native";
import {
View,
StyleSheet,
Text,
TouchableOpacity,
useWindowDimensions,
} from "react-native";
import { Picker } from "@react-native-picker/picker";
import { useCallback, useState } from "react";
import BudgetChart from "@/components/HistoryBudget/BudgetChart";
import FullTransactionHistory from "@/components/TransactionHistory/FullTransactionHistory";
import { StackRouter, useFocusEffect } from "@react-navigation/native";
import { useFocusEffect } from "@react-navigation/native";
import { BACKEND_PORT } from "@env";
import { useAuth } from "@/context/authContext";
import { ScrollView } from "react-native-gesture-handler";
import MultiLineChart from "@/components/Graphs/MultiLineChart";

// 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() {
Expand All @@ -25,6 +32,20 @@ export default function History() {
); // YYYY-MM format
const [selectedCategory, setSelectedCategory] = useState<string>("Food");
const [showFilterOptions, setShowFilterOptions] = useState(false);
const [multiLineChartData, setMultiLineChartData] = useState<
{ date: string; category: string; total: number }[]
>([]);
const [selectedTimeRange, setSelectedTimeRange] = useState("3months");
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" },
};

// Get unique categories from transactions
const getUniqueCategories = () => {
Expand Down Expand Up @@ -76,7 +97,27 @@ 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/multiTrend/${userId}?period=${config.period}&months=${config.months}`,
{
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
},
)
.then((res) => res.json())
.then((data) => {
setMultiLineChartData(data);
})
.catch((error) => {
console.error("API Error:", error);
});
}, [selectedTimeRange]),
);

// Toggle filter options visibility
Expand Down Expand Up @@ -162,6 +203,30 @@ export default function History() {
Budget={budget}
/>

<View style={styles.graphContainer}>
<Text style={{ fontSize: 20, fontWeight: "600" }}>
Category Trends Comparison
</Text>
<MultiLineChart
data={multiLineChartData}
width={chartWidth}
height={300}
/>
<View style={styles.timeRangePickerContainer}>
<Text style={styles.timeRangeLabel}>Time Range:</Text>
<Picker
selectedValue={selectedTimeRange}
onValueChange={(itemValue) => setSelectedTimeRange(itemValue)}
style={styles.timeRangePicker}
>
<Picker.Item label="1 Month" value="1month" />
<Picker.Item label="3 Months" value="3months" />
<Picker.Item label="6 Months" value="6months" />
<Picker.Item label="1 Year" value="1year" />
</Picker>
</View>
</View>

<View style={styles.filterSortContainer}>
{/* Sorting Controls */}
<View style={styles.sortingSection}>
Expand Down Expand Up @@ -424,4 +489,30 @@ const styles = StyleSheet.create({
activeFilterText: {
fontWeight: "bold",
},
graphContainer: {
backgroundColor: "#FFFFFF",
padding: 15,
borderRadius: 10,
width: "90%",
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,
},
});
Loading
Loading