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
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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`
8. Open a Pull Request

## 📊 Project Structure

Expand Down
145 changes: 73 additions & 72 deletions backend/src/db/database.sql
Original file line number Diff line number Diff line change
@@ -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();
11 changes: 5 additions & 6 deletions backend/src/googleAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -32,7 +33,9 @@ passport.use(
}

// Generate a random username (for new users)
const randomUsername = `user_${Math.floor(Math.random() * 1000000)}`;
console.log("Google Profile:", profile);
//const randomUsername = `user_${Math.floor(Math.random() * 1000000)}`;
const username = profile.displayName; // ADDED REAL USERNAME FROM GOOGLE PROFILE second merge request
const photos = profile.photos;

try {
Expand All @@ -51,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,
randomUsername,
profilePicture,
]);
const insertResult = await db.query(insertUserQuery, [email, username, profilePicture]);

profile.id = insertResult.rows[0].id;
return done(null, profile);
Expand Down
78 changes: 67 additions & 11 deletions frontend/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
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 { 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";
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.
*/
Expand All @@ -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<Category[]>([]);
const { userId } = useAuth();
const [username, setUsername] = useState("");
Expand All @@ -33,6 +38,9 @@ export default function Home() {
["Others", "#2b2d42"], //black
]);

// only two graph types for now: pie and area
const [graphType, setGraphType] = useState<GraphType>("pie");

useFocusEffect(
useCallback(() => {
fetch(
Expand All @@ -49,8 +57,9 @@ export default function Home() {
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);
Expand All @@ -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);
Expand All @@ -76,6 +87,7 @@ export default function Home() {
return res.json();
})
.then((data) => {
console.log("category data", data);
setCategories(data);
setTotal(
data.reduce(
Expand All @@ -91,12 +103,13 @@ export default function Home() {
}, [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 (
<>
Expand All @@ -105,13 +118,38 @@ export default function Home() {
<View style={styles.homeContainer}>
<Text style={styles.Title}>Hello {username}</Text>
<View style={styles.graphContainer}>
<Text style={{ fontSize: 20, fontWeight: "600" }}>
Total Spending
</Text>
<View style={styles.graphHeaderRow}>
<Text style={{ fontSize: 20, fontWeight: "600" }}>
Total Spending
</Text>
<View style={styles.pickerWrapper}>
<Picker
selectedValue={graphType}
onValueChange={(itemValue: GraphType) =>
setGraphType(itemValue)
}
style={styles.graphTypePicker}
dropdownIconColor="#00629B"
>
<Picker.Item label="Pie" value="pie" />
<Picker.Item label="Area" value="area" />
</Picker>
</View>
</View>
{/* <View style={styles.graph}></View> */}
<CustomPieChart data={pieData} size={250} total={total} />
{graphType === "pie" && (
<CustomPieChart data={categoryData} size={250} total={total} />
)}
{graphType === "area" && (
<AreaChart
categories={categories}
totalBudget={totalBudget}
transactions={allTransactions}
/>
)}

<View style={styles.legendContainer}>
{pieData.map((category) => {
{categoryData.map((category) => {
return (
<View key={category.id} style={styles.legendItem}>
<View
Expand Down Expand Up @@ -158,16 +196,16 @@ const styles = StyleSheet.create({
paddingHorizontal: 10,
},
graphContainer: {
height: 500,
width: "100%",
backgroundColor: "white",
borderRadius: 15,
padding: 20,
flexDirection: "column",
justifyContent: "space-between",
gap: 30,
justifyContent: "flex-start",
gap: 20,
shadowRadius: 12,
shadowOpacity: 0.4,
alignSelf: "flex-start",
},
graph: {
width: "100%",
Expand Down Expand Up @@ -198,4 +236,22 @@ const styles = StyleSheet.create({
fontSize: 16,
color: "black",
},
graphHeaderRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
},
pickerWrapper: {
borderRadius: 8,
overflow: "hidden",
borderWidth: 1,
borderColor: "#D0D0D0",
backgroundColor: "#FFFFFF",
},
graphTypePicker: {
width: 110,
height: 45,
backgroundColor: "#FFFFFF",
},
});
Loading
Loading