From e00f84227a41fa042bf1b4390692c19a3b0b7c05 Mon Sep 17 00:00:00 2001 From: Allison67 Date: Fri, 1 Dec 2023 14:24:21 -0500 Subject: [PATCH 01/60] Update Home.jsx --- front-end/src/components/Home.jsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/front-end/src/components/Home.jsx b/front-end/src/components/Home.jsx index 171865c..212fc47 100644 --- a/front-end/src/components/Home.jsx +++ b/front-end/src/components/Home.jsx @@ -239,9 +239,6 @@ const Home = ({ isDarkMode }) => { ))} - {/* - View All - */}

Friends Summary

From fae4273001cb0de3b41757a0ad7a9442a625f492 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Fri, 1 Dec 2023 14:52:57 -0500 Subject: [PATCH 02/60] Refine settlement save When settleTo === settleFrom, set status to true --- back-end/routes/addExpenseRoute.js | 37 +++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/back-end/routes/addExpenseRoute.js b/back-end/routes/addExpenseRoute.js index b195a77..b3264e8 100644 --- a/back-end/routes/addExpenseRoute.js +++ b/back-end/routes/addExpenseRoute.js @@ -29,18 +29,33 @@ router.post( let splitDetailsWithSettlements = []; // Create Settlement objects for (const split of req.body.peopleSplit) { - if (split.user !== req.body.paidBy) { - let newSettlement = new Settlement({ - status: false, - amount: split.amount, - settleTo: req.body.paidBy, - settleFrom: split.user, - event: req.body.event, - }); - console.log(newSettlement); - await newSettlement.save(); - splitDetailsWithSettlements.push({ user: split.user, settlement: newSettlement._id }); + const settleTo = req.body.paidBy; + const settleFrom = split.user; + let newSettlement; + // Debugging: Log the values and types + console.log(`settleTo: ${settleTo}, Type: ${typeof settleTo}`); + console.log(`settleFrom: ${settleFrom}, Type: ${typeof settleFrom}`); + + if (settleTo.toString() === settleFrom._id.toString()) { + newSettlement = new Settlement({ + status: true, + amount: split.amount, + settleTo: settleTo, + settleFrom: settleFrom, + event: req.body.event, + }); + } else { + newSettlement = new Settlement({ + status: false, + amount: split.amount, + settleTo: settleTo, + settleFrom: settleFrom, + event: req.body.event, + }); } + console.log(newSettlement); + await newSettlement.save(); + splitDetailsWithSettlements.push({ user: split.user, settlement: newSettlement._id }); } // Create a new Expense object From 08dd07027a115747dda58e77595b807c56a382f2 Mon Sep 17 00:00:00 2001 From: Allison67 Date: Fri, 1 Dec 2023 15:51:59 -0500 Subject: [PATCH 03/60] change the expense amount specific to each user --- back-end/routes/eventRoute.js | 8 +++- front-end/src/components/Event.js | 59 +++++++++++++++++++++++----- front-end/src/components/Expense.jsx | 2 +- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/back-end/routes/eventRoute.js b/back-end/routes/eventRoute.js index 42e3746..84d990d 100644 --- a/back-end/routes/eventRoute.js +++ b/back-end/routes/eventRoute.js @@ -7,9 +7,13 @@ router.get("/:eventId", async (req, res) => { const eventId = req.params.eventId; const event = await Event.findById(eventId).populate({ path: "expenses", - model: "Expense" + model: "Expense", + populate: { + path: "splitDetails.settlement", + model: "Settlement", + }, }); - + if (!event) { return res.status(404).json({ message: "Event not found" }); } diff --git a/front-end/src/components/Event.js b/front-end/src/components/Event.js index c17bc40..159aeaf 100644 --- a/front-end/src/components/Event.js +++ b/front-end/src/components/Event.js @@ -3,9 +3,11 @@ import React, { useState, useEffect } from "react"; import "../styles/Event.css"; import { Link, useParams } from "react-router-dom"; import Navbar from "./Navbar"; +import { jwtDecode } from "jwt-decode"; const Event = (props) => { const [data, setData] = useState([]); + const [userExpenses, setUserExpenses] = useState([]); const isDarkMode = props.isDarkMode; const { eventId } = useParams(); console.log("Event ID:", eventId); // check the eventID received @@ -46,6 +48,35 @@ const Event = (props) => { }; }, [isDarkMode]); + const processUserExpenses = (expenses, userId) => { + const processedExpenses = expenses.map((expense) => { + const isParticipant = expense.splitDetails.find( + (detail) => detail.user === userId + ); + let settlement; + + if (isParticipant) { + // User is a participant, find the settlement from splitDetails + const userSplitDetail = expense.splitDetails.find( + (detail) => detail.user === userId + ); + settlement = userSplitDetail + ? userSplitDetail.settlement + : { amount: 0 }; // Add other fields as needed + } else { + // User is not a participant, create a settlement object with amount 0 + settlement = { amount: 0 }; // Add other fields as needed + } + + return { + expense: expense, + settlement: settlement, + }; + }); + + setUserExpenses(processedExpenses); + }; + useEffect(() => { // fetch some mock data about expense console.log("fetching the event"); @@ -95,6 +126,14 @@ const Event = (props) => { fetchEvent(); }, []); + useEffect(() => { + const token = localStorage.getItem("token"); + const currentUser = jwtDecode(token); + if (data.expenses && currentUser) { + processUserExpenses(data.expenses, currentUser.id); + } + }, [data]); + return (
{" "} @@ -118,25 +157,27 @@ const Event = (props) => {
- {data.expenses && - data.expenses.map((item) => ( -
-
{reformatDate(item.date)}
+ {userExpenses && + userExpenses.map((item) => ( +
+
{reformatDate(item.expense.date)}
- -
{item.name}
+ +
{item.expense.name}
-
${item.totalAmount}
+
${item.settlement.amount}
- +
))}
- {/* */} + + {" "} + {/* */} Add Expense
diff --git a/front-end/src/components/Expense.jsx b/front-end/src/components/Expense.jsx index 7f32bd9..42b109e 100644 --- a/front-end/src/components/Expense.jsx +++ b/front-end/src/components/Expense.jsx @@ -48,7 +48,7 @@ function Expense({ isDarkMode }) { return (

- {expensesData.event ? expensesData.event.name : 'Loading...'} | + {expensesData.event ? expensesData.event.name : 'Loading...'}| {expensesData.name ? expensesData.name : 'Loading...'}

From 2cd2b77eddd742b7232feb32035991bb3fe6a789 Mon Sep 17 00:00:00 2001 From: Allison67 Date: Fri, 1 Dec 2023 16:13:20 -0500 Subject: [PATCH 04/60] Update Event.js --- front-end/src/components/Event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front-end/src/components/Event.js b/front-end/src/components/Event.js index 159aeaf..789cf86 100644 --- a/front-end/src/components/Event.js +++ b/front-end/src/components/Event.js @@ -65,7 +65,7 @@ const Event = (props) => { : { amount: 0 }; // Add other fields as needed } else { // User is not a participant, create a settlement object with amount 0 - settlement = { amount: 0 }; // Add other fields as needed + settlement = { amount: 0 }; } return { From b67969328bfdb6a956c941fa208043d0c090cea6 Mon Sep 17 00:00:00 2001 From: Allison67 Date: Fri, 1 Dec 2023 16:31:51 -0500 Subject: [PATCH 05/60] add another settlement route --- back-end/routes/addExpenseRoute.js | 42 ++++++++++++++++-------------- back-end/routes/settlementRoute.js | 19 ++++++++++++++ 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/back-end/routes/addExpenseRoute.js b/back-end/routes/addExpenseRoute.js index b3264e8..68ee984 100644 --- a/back-end/routes/addExpenseRoute.js +++ b/back-end/routes/addExpenseRoute.js @@ -25,37 +25,39 @@ router.post( console.log("validation passed"); try { - let splitDetailsWithSettlements = []; // Create Settlement objects for (const split of req.body.peopleSplit) { - const settleTo = req.body.paidBy; - const settleFrom = split.user; - let newSettlement; + const settleTo = req.body.paidBy; + const settleFrom = split.user; + let newSettlement; // Debugging: Log the values and types console.log(`settleTo: ${settleTo}, Type: ${typeof settleTo}`); console.log(`settleFrom: ${settleFrom}, Type: ${typeof settleFrom}`); if (settleTo.toString() === settleFrom._id.toString()) { - newSettlement = new Settlement({ - status: true, - amount: split.amount, - settleTo: settleTo, - settleFrom: settleFrom, - event: req.body.event, - }); + newSettlement = new Settlement({ + status: true, + amount: split.amount, + settleTo: settleTo, + settleFrom: settleFrom, + event: req.body.event, + }); } else { - newSettlement = new Settlement({ - status: false, - amount: split.amount, - settleTo: settleTo, - settleFrom: settleFrom, - event: req.body.event, - }); + newSettlement = new Settlement({ + status: false, + amount: split.amount, + settleTo: settleTo, + settleFrom: settleFrom, + event: req.body.event, + }); } console.log(newSettlement); await newSettlement.save(); - splitDetailsWithSettlements.push({ user: split.user, settlement: newSettlement._id }); + splitDetailsWithSettlements.push({ + user: split.user, + settlement: newSettlement._id, + }); } // Create a new Expense object @@ -99,4 +101,4 @@ router.post( } ); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/back-end/routes/settlementRoute.js b/back-end/routes/settlementRoute.js index e0e6e4c..7acf71e 100644 --- a/back-end/routes/settlementRoute.js +++ b/back-end/routes/settlementRoute.js @@ -19,4 +19,23 @@ router.get("/from/:userId", async (req, res) => { } }); +router.get("/from/:userId1/to/:userId2", async (req, res) => { + try { + const userId1 = req.params.userId1; + const userId2 = req.params.userId2; + + const settlements = await Settlement.find({ + settleFrom: userId1, + settleTo: userId2, + }); + + res.status(200).json(settlements); + } catch (error) { + console.error("Error fetching settlements:", error); + res + .status(500) + .json({ message: "Error fetching settlements", error: error }); + } +}); + module.exports = router; From e821deefbbb8b1065c08a556b7333a0cef9115fe Mon Sep 17 00:00:00 2001 From: Allison67 Date: Fri, 1 Dec 2023 16:51:48 -0500 Subject: [PATCH 06/60] Update Home.jsx --- front-end/src/components/Home.jsx | 35 ++++++++++++++----------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/front-end/src/components/Home.jsx b/front-end/src/components/Home.jsx index 212fc47..02fc6a1 100644 --- a/front-end/src/components/Home.jsx +++ b/front-end/src/components/Home.jsx @@ -128,9 +128,6 @@ const Home = ({ isDarkMode }) => { const token = getTokenFromLocalStorage(); const currentUser = decodeToken(token); - let totalSpending = 0; - let expenses = []; - if (currentUser) { console.log("Current User:", currentUser); @@ -204,6 +201,22 @@ const Home = ({ isDarkMode }) => {

Welcome, {data.userName}

+
+

Expenses Summary

+ {/*

${calculateTotalSpending(data.expenses || [])}

*/} +
    + {data.expenses.map((event) => ( +
  • +
    +

    {event.event.name}

    +

    + ${event.amount.toFixed(2)} +

    +
    +
  • + ))} +
+

Events Summary

    @@ -224,22 +237,6 @@ const Home = ({ isDarkMode }) => { View All
-
-

Expenses Summary

- {/*

${calculateTotalSpending(data.expenses || [])}

*/} -
    - {data.expenses.map((event) => ( -
  • -
    -

    {event.event.name}

    -

    - ${event.amount.toFixed(2)} -

    -
    -
  • - ))} -
-

Friends Summary

    From 4e8b9a1c0007ce92235a496d2b9dd2fab77bca44 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Fri, 1 Dec 2023 17:25:35 -0500 Subject: [PATCH 07/60] Update FriendsPage.jsx --- front-end/src/components/FriendsPage.jsx | 34 +++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/front-end/src/components/FriendsPage.jsx b/front-end/src/components/FriendsPage.jsx index a3deca1..4eec9db 100644 --- a/front-end/src/components/FriendsPage.jsx +++ b/front-end/src/components/FriendsPage.jsx @@ -9,8 +9,6 @@ function FriendsPage({ isDarkMode }) { const [userData, setUserData] = useState(null); const [showModal, setShowModal] = useState(false); - const backupData = {"id":1,"name":"Bryn","email":"btaylot0@booking.com","phone":"850-479-2094","avatar":"https://robohash.org/utetquibusdam.png?size=50x50\u0026set=set1","friends":[{"id":5,"name":"Jdavie","email":"jzecchinii0@yahoo.co.jp","phone":"967-156-0272","balance":"$57.06"},{"id":2,"name":"Emmie","email":"esworder1@xinhuanet.com","phone":"832-141-0597","balance":"$60.04"}]}; - // useEffect for dark mode useEffect(() => { if (isDarkMode) { @@ -33,16 +31,46 @@ function FriendsPage({ isDarkMode }) { const userId = currentUser.id; const result = await axios.get(`http://localhost:3001/friends/${userId}`); setUserData(result.data); + console.log(result.data); } catch (err) { console.error(err); - setUserData(backupData); } } fetchData(); }, []); + const fetchSettlementsForFriends = async () => { + if (!userData || !userData.friends) return; + + const settlements = await Promise.all(userData.friends.map(async (friend) => { + try { + const fromUserToFriend = await axios.get(`http://localhost:3001/settlement/from/${userData._id}/to/${friend._id}`); + const fromFriendToUser = await axios.get(`http://localhost:3001/settlement/from/${friend._id}/to/${userData._id}`); + + return { + friendId: friend, + fromUserToFriend: fromUserToFriend.data, + fromFriendToUser: fromFriendToUser.data, + }; + } catch (error) { + console.error('Error fetching settlements:', error); + return null; + } + })); + + console.log(settlements); + }; + + useEffect(() => { + fetchSettlementsForFriends(); + }, [userData]); + + console.log('typeof userdata is', typeof userData); + if (!userData) return
    Loading...
    ; + + // const totalBalance = userData.friends && userData.friends.length ? userData.friends.reduce((acc, friend) => acc + parseFloat(friend.balance.replace('$', '')), 0) : 0; return ( From e2dd5ad022fc3c10e9a58c3d10d6a726f8fecbd4 Mon Sep 17 00:00:00 2001 From: Allison67 Date: Fri, 1 Dec 2023 17:33:15 -0500 Subject: [PATCH 08/60] updated expense summary --- front-end/src/components/Home.jsx | 115 +++++++++++++++++++++--------- 1 file changed, 80 insertions(+), 35 deletions(-) diff --git a/front-end/src/components/Home.jsx b/front-end/src/components/Home.jsx index 02fc6a1..2e78e48 100644 --- a/front-end/src/components/Home.jsx +++ b/front-end/src/components/Home.jsx @@ -13,6 +13,7 @@ const Home = ({ isDarkMode }) => { friends: [], events: [], }); + const [expenseSummary, setExpenseSummary] = useState([]); /* backupData from Expenses, Events, Friends.jsx */ const backupData = { @@ -93,15 +94,46 @@ const Home = ({ isDarkMode }) => { }; function reformatDate(dateStr) { - const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + const months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; const date = new Date(dateStr); - + const monthName = months[date.getMonth()]; const day = date.getDate(); const year = date.getFullYear(); return `${monthName} ${day} ${year}`; -} + } + + function calculateExpenseSummary(expenses) { + const summary = expenses.reduce((acc, expense) => { + // If the event is already in the accumulator, add to its totalAmount + if (acc[expense.event._id]) { + acc[expense.event._id].totalAmount += expense.amount; + } else { + // Otherwise, create a new entry in the accumulator + acc[expense.event._id] = { + event: expense.event, + totalAmount: expense.amount, + }; + } + return acc; + }, {}); + // Convert the summary object back into an array + return Object.values(summary); + } useEffect(() => { const getTokenFromLocalStorage = () => { @@ -131,32 +163,38 @@ const Home = ({ isDarkMode }) => { if (currentUser) { console.log("Current User:", currentUser); - const expenseRes = await axios.get(`http://localhost:3001/settlement/from/${currentUser.id}`); - console.log("Home Response:", expenseRes.data); + const expenseRes = await axios.get( + `http://localhost:3001/settlement/from/${currentUser.id}` + ); + console.log("Response:", expenseRes.data); const newTotalSpending = calculateTotalSpending(expenseRes.data); - const newExpenses = expenseRes.data.map(settlement => { + const newExpenses = expenseRes.data.map((settlement) => { return { - event: settlement.event, - amount: settlement.amount + event: settlement.event, + amount: settlement.amount, }; - }); + }); // Fetch events data - const eventsRes = await axios.get(`http://localhost:3001/events/for/${currentUser.id}`); + const eventsRes = await axios.get( + `http://localhost:3001/events/for/${currentUser.id}` + ); const events = eventsRes.data.events || []; // Fetch friends data - const friendsRes = await axios.get(`http://localhost:3001/friends/${currentUser.id}`); + const friendsRes = await axios.get( + `http://localhost:3001/friends/${currentUser.id}` + ); const friends = friendsRes.data.friends || []; const userName = currentUser.username || ""; - setData({ - userName, - totalSpending: newTotalSpending, - expenses: newExpenses, - friends, - events + setData({ + userName, + totalSpending: newTotalSpending, + expenses: newExpenses, + friends, + events, }); } else { console.error("No valid user found."); @@ -170,10 +208,15 @@ const Home = ({ isDarkMode }) => { fetchData(); }, []); + useEffect(() => { + const newExpenseSummary = calculateExpenseSummary(data.expenses); + setExpenseSummary(newExpenseSummary); + }, [data.expenses]); + function calculateTotalSpending(settlements) { let total = 0; - settlements.forEach(settlement => { - total += settlement.amount; + settlements.forEach((settlement) => { + total += settlement.amount; }); return total; } @@ -205,12 +248,12 @@ const Home = ({ isDarkMode }) => {

    Expenses Summary

    {/*

    ${calculateTotalSpending(data.expenses || [])}

    */}
      - {data.expenses.map((event) => ( -
    • + {expenseSummary.map((item) => ( +
    • -

      {event.event.name}

      +

      {item.event.name}

      - ${event.amount.toFixed(2)} + ${item.totalAmount.toFixed(2)}

    • @@ -222,12 +265,14 @@ const Home = ({ isDarkMode }) => {
        {eventsPending.length > 0 ? ( eventsPending.map((event) => ( -
      • -
        -

        {event.name}

        -

        {reformatDate(event.date)}

        -
        -
      • +
      • +
        +

        {event.name}

        +

        + {reformatDate(event.date)} +

        +
        +
      • )) ) : (
        No Events Added Yet.
        @@ -242,12 +287,12 @@ const Home = ({ isDarkMode }) => {
          {friendsPendingPayment.length > 0 ? ( friendsPendingPayment.map((friend) => ( -
        • -
          -

          {friend.username}

          -

          {friend.balance}

          -
          -
        • +
        • +
          +

          {friend.username}

          +

          {friend.balance}

          +
          +
        • )) ) : (
          No Friends Added Yet.
          From 05964278e237d0be1b78ea21a6e7ad7ad73cf034 Mon Sep 17 00:00:00 2001 From: elaineZhang67 <144281338+elaineZhang67@users.noreply.github.com> Date: Sat, 2 Dec 2023 02:24:28 -0500 Subject: [PATCH 09/60] Add Status Settlement Function --- back-end/app.js | 2 ++ back-end/package.json | 2 +- back-end/routes/expenseStatusRoute.js | 47 +++++++++++++++++++++++++++ front-end/src/components/Expense.jsx | 24 +++++++++++++- 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 back-end/routes/expenseStatusRoute.js diff --git a/back-end/app.js b/back-end/app.js index b37f939..6f4ee29 100644 --- a/back-end/app.js +++ b/back-end/app.js @@ -21,6 +21,7 @@ const logoutRoute = require("./routes/logoutRoute"); const searchFriendRoute = require("./routes/searchFriendRoute"); const expenseRoute = require("./routes/expenseRoute"); const settlementRoute = require("./routes/settlementRoute"); +const expenseStatusRoute = require("./routes/expenseStatusRoute"); // connect to the database // console.log(`Conneting to MongoDB at ${process.env.MONGODB_URI}`) @@ -59,6 +60,7 @@ app.use("/logout", logoutRoute); app.use("/searchFriend", searchFriendRoute); app.use("/expense", expenseRoute); app.use("/settlement", settlementRoute); +app.use("/expenseStatus", expenseStatusRoute); // export the express app we created to make it available to other modules module.exports = app; diff --git a/back-end/package.json b/back-end/package.json index 4a7f137..a9dd232 100644 --- a/back-end/package.json +++ b/back-end/package.json @@ -27,6 +27,6 @@ "chai": "^4.3.10", "chai-http": "^4.4.0", "mocha": "^10.2.0", - "nodemon": "^3.0.1" + "nodemon": "^3.0.2" } } diff --git a/back-end/routes/expenseStatusRoute.js b/back-end/routes/expenseStatusRoute.js new file mode 100644 index 0000000..1a1ad0a --- /dev/null +++ b/back-end/routes/expenseStatusRoute.js @@ -0,0 +1,47 @@ +const express = require("express"); +const router = express.Router(); +const { body, validationResult } = require("express-validator"); +const {Settlement} = require("../models/Settlement.js") + +router.post( + "/:settlementId", + + async (req, res) => { + + /* + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + */ + + try { + const settlementId = req.params.settlementId; + const status = req.body.status; + console.log("__________________________") + console.log(status); + + const updatedSettlement = await Settlement.findByIdAndUpdate( + settlementId, + { status }, + { new: true } + ); + + if(!updatedSettlement){ + return res.status(404).json({ message: "Settlement not found" }); + } + // Send a success response + res.status(200).json({ + message: "Settlements updated successfully", + data: updatedSettlement, + }); + } catch (error) { + // Handle any errors that occur during saving to database + console.error(error); + res.status(500).json({ status: "Error", message: error.message }); + } + } + ); + + +module.exports = router; \ No newline at end of file diff --git a/front-end/src/components/Expense.jsx b/front-end/src/components/Expense.jsx index 7f32bd9..6293ace 100644 --- a/front-end/src/components/Expense.jsx +++ b/front-end/src/components/Expense.jsx @@ -22,6 +22,16 @@ function Expense({ isDarkMode }) { } }; + const settleExpenses = async(settlementId, newStatus) => { + try{ + const response = await axios.post(`http://localhost:3001/expenseStatus/${settlementId}`, {status: newStatus}); + await console.log('Settlements updated:', response.data); + } catch (error) { + console.error('Error updating settlements:', error); + console.log(error.response.data); + } + } + useEffect(() => { // Toggle the 'body-dark-mode' class on the body element if (isDarkMode) { @@ -45,6 +55,12 @@ function Expense({ isDarkMode }) { navigate(-1); }; + const handleSettlementChange = (e, settlementId, newStatus) => { + if(e.target.checked){ + settleExpenses(settlementId, newStatus) + } + }; + return (

          @@ -63,7 +79,13 @@ function Expense({ isDarkMode }) {
          {split.user.username} 0 ? 'positive' : 'negative'}>{split.settlement.amount} -
          +
          + handleSettlementChange(e, split.settlement._id, true)} + /> +
          ))}

          From ffd42af34f3850b01bc2fa86437f649161082ef7 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Sat, 2 Dec 2023 16:29:29 -0500 Subject: [PATCH 10/60] Update FriendsPage.jsx Display balance --- front-end/src/components/FriendsPage.jsx | 66 +++++++++++++++--------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/front-end/src/components/FriendsPage.jsx b/front-end/src/components/FriendsPage.jsx index 4eec9db..9a040a1 100644 --- a/front-end/src/components/FriendsPage.jsx +++ b/front-end/src/components/FriendsPage.jsx @@ -39,33 +39,53 @@ function FriendsPage({ isDarkMode }) { fetchData(); }, []); + const [settlements, setSettlements] = useState([]); const fetchSettlementsForFriends = async () => { if (!userData || !userData.friends) return; - - const settlements = await Promise.all(userData.friends.map(async (friend) => { + + let settlements = []; + for (const friend of userData.friends) { try { const fromUserToFriend = await axios.get(`http://localhost:3001/settlement/from/${userData._id}/to/${friend._id}`); const fromFriendToUser = await axios.get(`http://localhost:3001/settlement/from/${friend._id}/to/${userData._id}`); - - return { - friendId: friend, + + settlements.push({ + friend: friend, fromUserToFriend: fromUserToFriend.data, fromFriendToUser: fromFriendToUser.data, - }; + }); } catch (error) { - console.error('Error fetching settlements:', error); - return null; + console.error('Error fetching settlements for friend:', friend._id, error); + // Optionally, you can push a default value or skip the friend + settlements.push({ friend: friend, fromUserToFriend: [], fromFriendToUser: [] }); } - })); - - console.log(settlements); + } + + console.log(settlements); + setSettlements(settlements); }; useEffect(() => { fetchSettlementsForFriends(); }, [userData]); - console.log('typeof userdata is', typeof userData); + const calculateBalances = (items) => { + return items.map(item => { + const balance = item.fromUserToFriend.reduce((acc, settlement) => acc - settlement.amount, 0) + + item.fromFriendToUser.reduce((acc, settlement) => acc + settlement.amount, 0); + return { + ...item.friend, + balance: balance + }; + }); + }; + + const totalBalance = settlements.reduce((acc, item) => { + return acc + item.fromUserToFriend.reduce((sum, settlement) => sum - settlement.amount, 0) + + item.fromFriendToUser.reduce((sum, settlement) => sum + settlement.amount, 0); + }, 0); + + if (!userData) return
          Loading...
          ; @@ -80,31 +100,29 @@ function FriendsPage({ isDarkMode }) { User Avatar
          Total balance
          - {/*
          - {totalBalance < 0 && ( +
          + {totalBalance < 0 ? (
          You owe ${Math.abs(totalBalance).toFixed(2)}
          - )} - {totalBalance > 0 && ( + ) : totalBalance > 0 ? (
          You are owed ${totalBalance.toFixed(2)}
          - )} - {totalBalance === 0 && ( + ) : (
          All Balances are Settled!
          )} -
          */} +
    - {userData.friends.map((friend) => ( -
  • + {calculateBalances(settlements).map((friend) => ( +
  • {`${friend.username}'s {friend.username} - {/* - {friend.balance} - */} + + ${friend.balance.toFixed(2)} +
  • ))}
From 2c5eb12970bd613f60964e07fc9a0896abf62703 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Sat, 2 Dec 2023 17:02:23 -0500 Subject: [PATCH 11/60] Update FriendsPage Add checkbox for clear balance (functionality is not yet applied) --- front-end/src/components/FriendsPage.jsx | 331 +++++++++++++---------- front-end/src/styles/FriendsPage.css | 4 + 2 files changed, 196 insertions(+), 139 deletions(-) diff --git a/front-end/src/components/FriendsPage.jsx b/front-end/src/components/FriendsPage.jsx index 9a040a1..2fbca55 100644 --- a/front-end/src/components/FriendsPage.jsx +++ b/front-end/src/components/FriendsPage.jsx @@ -1,149 +1,202 @@ -import axios from 'axios'; -import React, { useState, useEffect } from 'react'; -import '../styles/FriendsPage.css'; -import AddFriendModal from './AddFriendModal'; +import axios from "axios"; +import React, { useState, useEffect } from "react"; +import "../styles/FriendsPage.css"; +import AddFriendModal from "./AddFriendModal"; import Navbar from "./Navbar"; -import { jwtDecode } from 'jwt-decode'; +import { jwtDecode } from "jwt-decode"; function FriendsPage({ isDarkMode }) { - const [userData, setUserData] = useState(null); - const [showModal, setShowModal] = useState(false); - - // useEffect for dark mode - useEffect(() => { - if (isDarkMode) { - document.body.classList.add('body-dark-mode'); - } else { - document.body.classList.remove('body-dark-mode'); - } - // Cleanup function to remove dark mode class - return () => { - document.body.classList.remove('body-dark-mode'); - }; - }, [isDarkMode]); // Depend on isDarkMode prop - - - useEffect(() => { - const fetchData = async () => { - try { - const token = localStorage.getItem("token"); - const currentUser = jwtDecode(token); - const userId = currentUser.id; - const result = await axios.get(`http://localhost:3001/friends/${userId}`); - setUserData(result.data); - console.log(result.data); - } catch (err) { - console.error(err); - } - } - fetchData(); - }, []); - - const [settlements, setSettlements] = useState([]); - const fetchSettlementsForFriends = async () => { - if (!userData || !userData.friends) return; - - let settlements = []; - for (const friend of userData.friends) { - try { - const fromUserToFriend = await axios.get(`http://localhost:3001/settlement/from/${userData._id}/to/${friend._id}`); - const fromFriendToUser = await axios.get(`http://localhost:3001/settlement/from/${friend._id}/to/${userData._id}`); - - settlements.push({ - friend: friend, - fromUserToFriend: fromUserToFriend.data, - fromFriendToUser: fromFriendToUser.data, - }); - } catch (error) { - console.error('Error fetching settlements for friend:', friend._id, error); - // Optionally, you can push a default value or skip the friend - settlements.push({ friend: friend, fromUserToFriend: [], fromFriendToUser: [] }); - } - } - - console.log(settlements); - setSettlements(settlements); + const [userData, setUserData] = useState(null); + const [showModal, setShowModal] = useState(false); + + // useEffect for dark mode + useEffect(() => { + if (isDarkMode) { + document.body.classList.add("body-dark-mode"); + } else { + document.body.classList.remove("body-dark-mode"); + } + // Cleanup function to remove dark mode class + return () => { + document.body.classList.remove("body-dark-mode"); }; - - useEffect(() => { - fetchSettlementsForFriends(); - }, [userData]); - - const calculateBalances = (items) => { - return items.map(item => { - const balance = item.fromUserToFriend.reduce((acc, settlement) => acc - settlement.amount, 0) - + item.fromFriendToUser.reduce((acc, settlement) => acc + settlement.amount, 0); - return { - ...item.friend, - balance: balance - }; - }); + }, [isDarkMode]); // Depend on isDarkMode prop + + useEffect(() => { + const fetchData = async () => { + try { + const token = localStorage.getItem("token"); + const currentUser = jwtDecode(token); + const userId = currentUser.id; + const result = await axios.get( + `http://localhost:3001/friends/${userId}` + ); + setUserData(result.data); + console.log(result.data); + } catch (err) { + console.error(err); + } }; - - const totalBalance = settlements.reduce((acc, item) => { - return acc + item.fromUserToFriend.reduce((sum, settlement) => sum - settlement.amount, 0) - + item.fromFriendToUser.reduce((sum, settlement) => sum + settlement.amount, 0); - }, 0); - - - - if (!userData) return
Loading...
; - - - - // const totalBalance = userData.friends && userData.friends.length ? userData.friends.reduce((acc, friend) => acc + parseFloat(friend.balance.replace('$', '')), 0) : 0; - + fetchData(); + }, []); + + const [settlements, setSettlements] = useState([]); + const fetchSettlementsForFriends = async () => { + if (!userData || !userData.friends) return; + + let settlements = []; + for (const friend of userData.friends) { + try { + const fromUserToFriend = await axios.get( + `http://localhost:3001/settlement/from/${userData._id}/to/${friend._id}` + ); + const fromFriendToUser = await axios.get( + `http://localhost:3001/settlement/from/${friend._id}/to/${userData._id}` + ); + + settlements.push({ + friend: friend, + fromUserToFriend: fromUserToFriend.data, + fromFriendToUser: fromFriendToUser.data, + }); + } catch (error) { + console.error( + "Error fetching settlements for friend:", + friend._id, + error + ); + // Optionally, you can push a default value or skip the friend + settlements.push({ + friend: friend, + fromUserToFriend: [], + fromFriendToUser: [], + }); + } + } + + console.log(settlements); + setSettlements(settlements); + }; + + useEffect(() => { + fetchSettlementsForFriends(); + }, [userData]); + + const calculateBalances = (items) => { + return items.map((item) => { + const balance = + item.fromUserToFriend.reduce( + (acc, settlement) => acc - settlement.amount, + 0 + ) + + item.fromFriendToUser.reduce( + (acc, settlement) => acc + settlement.amount, + 0 + ); + + const settlementIds = [ + ...item.fromUserToFriend.map((settlement) => settlement._id), + ...item.fromFriendToUser.map((settlement) => settlement._id), + ]; + + return { + ...item.friend, + balance: balance, + settlementIds: settlementIds, + }; + }); + }; + + const totalBalance = settlements.reduce((acc, item) => { return ( -
-

Friends

-
- User Avatar -
-
Total balance
-
- {totalBalance < 0 ? ( -
You owe ${Math.abs(totalBalance).toFixed(2)}
- ) : totalBalance > 0 ? ( -
You are owed ${totalBalance.toFixed(2)}
- ) : ( -
All Balances are Settled!
- )} -
-
-
- -
-
    - {calculateBalances(settlements).map((friend) => ( -
  • - - {`${friend.username}'s - {friend.username} - - - ${friend.balance.toFixed(2)} - -
  • - ))} -
-
- -
- -
- -
- - {showModal && ( - {setShowModal(false); window.location.reload();}} /> + acc + + item.fromUserToFriend.reduce( + (sum, settlement) => sum - settlement.amount, + 0 + ) + + item.fromFriendToUser.reduce( + (sum, settlement) => sum + settlement.amount, + 0 + ) + ); + }, 0); + + if (!userData) return
Loading...
; + + // const totalBalance = userData.friends && userData.friends.length ? userData.friends.reduce((acc, friend) => acc + parseFloat(friend.balance.replace('$', '')), 0) : 0; + + return ( +
+

Friends

+
+ User Avatar +
+
Total balance
+
+ {totalBalance < 0 ? ( +
You owe ${Math.abs(totalBalance).toFixed(2)}
+ ) : totalBalance > 0 ? ( +
You are owed ${totalBalance.toFixed(2)}
+ ) : ( +
All Balances are Settled!
)} - - - +
- ); +
+ +
+
    + {calculateBalances(settlements).map((friend) => ( +
  • + + {`${friend.username}'s + {friend.username} + +
    + + ${friend.balance.toFixed(2)} + +
    + +
    +
    +
  • + ))} +
+
+ +
+ +
+ +
+ + {showModal && ( + { + setShowModal(false); + window.location.reload(); + }} + /> + )} + + +
+ ); } -export default FriendsPage; \ No newline at end of file +export default FriendsPage; diff --git a/front-end/src/styles/FriendsPage.css b/front-end/src/styles/FriendsPage.css index 98016bb..01a4b26 100644 --- a/front-end/src/styles/FriendsPage.css +++ b/front-end/src/styles/FriendsPage.css @@ -92,6 +92,10 @@ @apply flex justify-between items-center p-4 border-b border-gray-200 bg-cyan-100; } +.checkbox { + padding-left: 5px; +} + .space-to-scroll { height: 60px; } From 1b8c35659062d25fa890e2d112b0507cd11ef778 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Sat, 2 Dec 2023 17:04:56 -0500 Subject: [PATCH 12/60] Formatting code --- back-end/routes/addExpensePayerRoute.js | 16 +- back-end/routes/addFriendRoute.js | 53 +++--- back-end/routes/friendsPageRoute.js | 11 +- back-end/routes/searchFriendRoute.js | 8 +- front-end/src/components/AddFriendModal.jsx | 194 ++++++++++---------- front-end/src/styles/FriendsPage.css | 102 +++++----- 6 files changed, 198 insertions(+), 186 deletions(-) diff --git a/back-end/routes/addExpensePayerRoute.js b/back-end/routes/addExpensePayerRoute.js index 7c4e494..01ef088 100644 --- a/back-end/routes/addExpensePayerRoute.js +++ b/back-end/routes/addExpensePayerRoute.js @@ -1,21 +1,23 @@ const express = require("express"); const router = express.Router(); -const {User} = require("../models/User.js") +const { User } = require("../models/User.js"); const Event = require("../models/Event.js"); -router.get('/EventMember/:eventId', async (req, res) => { +router.get("/EventMember/:eventId", async (req, res) => { try { //fetch all data const eventId = req.params.eventId; - console.log('eventId:', eventId); - const eventMember = await Event.findById(eventId).populate('participants'); + console.log("eventId:", eventId); + const eventMember = await Event.findById(eventId).populate("participants"); if (!eventMember) { - return res.status(404).json({ message: "Event not found or you don't have access to it" }); + return res + .status(404) + .json({ message: "Event not found or you don't have access to it" }); } res.json(eventMember.participants); - }catch (error) { + } catch (error) { console.error("Error fetching event members:", error); - res.status(500).json({ message: "Server error", error}); + res.status(500).json({ message: "Server error", error }); } }); diff --git a/back-end/routes/addFriendRoute.js b/back-end/routes/addFriendRoute.js index a0765a7..f8e1c71 100644 --- a/back-end/routes/addFriendRoute.js +++ b/back-end/routes/addFriendRoute.js @@ -1,37 +1,36 @@ -const express = require('express'); +const express = require("express"); const router = express.Router(); const { User } = require("../models/User.js"); -router.post('/', async (req, res) => { - const { currentUserId, friendUserId } = req.body; +router.post("/", async (req, res) => { + const { currentUserId, friendUserId } = req.body; - try { - const currentUser = await User.findById(currentUserId); - const friendUser = await User.findById(friendUserId); + try { + const currentUser = await User.findById(currentUserId); + const friendUser = await User.findById(friendUserId); - if (!currentUser || !friendUser) { - return res.status(404).send("One or both users not found"); - } - - if (!currentUser.friends.includes(friendUserId)) { - currentUser.friends.push(friendUserId); - await currentUser.save(); - console.log(`Added ${friendUserId} to ${currentUser.id}'s friends list.`); - } else { - return res.status(200).send("Already friends"); - } + if (!currentUser || !friendUser) { + return res.status(404).send("One or both users not found"); + } - if (!friendUser.friends.includes(currentUserId)) { - friendUser.friends.push(currentUserId); - await friendUser.save(); - } + if (!currentUser.friends.includes(friendUserId)) { + currentUser.friends.push(friendUserId); + await currentUser.save(); + console.log(`Added ${friendUserId} to ${currentUser.id}'s friends list.`); + } else { + return res.status(200).send("Already friends"); + } - res.status(200).send("Friends added successfully"); - - } catch (error) { - console.error("Error in addFriend route:", error); - res.status(500).send("Error adding friend"); + if (!friendUser.friends.includes(currentUserId)) { + friendUser.friends.push(currentUserId); + await friendUser.save(); } + + res.status(200).send("Friends added successfully"); + } catch (error) { + console.error("Error in addFriend route:", error); + res.status(500).send("Error adding friend"); + } }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/back-end/routes/friendsPageRoute.js b/back-end/routes/friendsPageRoute.js index 2a1cef2..5f13638 100644 --- a/back-end/routes/friendsPageRoute.js +++ b/back-end/routes/friendsPageRoute.js @@ -1,20 +1,19 @@ -const express = require('express'); +const express = require("express"); const router = express.Router(); const { User } = require("../models/User.js"); -router.get('/:userId', async (req, res) => { +router.get("/:userId", async (req, res) => { try { const userId = req.params.userId; - const user = await User.findById(userId).populate('friends'); + const user = await User.findById(userId).populate("friends"); if (!user) { - return res.status(404).json({ error: 'User not found' }); + return res.status(404).json({ error: "User not found" }); } res.json(user); - } catch (error) { console.error(error); - res.status(500).json({ error: 'Internal Server Error' }); + res.status(500).json({ error: "Internal Server Error" }); } }); diff --git a/back-end/routes/searchFriendRoute.js b/back-end/routes/searchFriendRoute.js index 9e8cfcc..9140235 100644 --- a/back-end/routes/searchFriendRoute.js +++ b/back-end/routes/searchFriendRoute.js @@ -2,10 +2,12 @@ const express = require("express"); const router = express.Router(); const { User } = require("../models/User.js"); -router.get('/', async (req, res) => { +router.get("/", async (req, res) => { const { username } = req.query; try { - const userData = await User.findOne({ username: username }).select("username avatar _id"); + const userData = await User.findOne({ username: username }).select( + "username avatar _id" + ); if (!userData) { return res.status(404).send("User not found"); } @@ -15,4 +17,4 @@ router.get('/', async (req, res) => { } }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/front-end/src/components/AddFriendModal.jsx b/front-end/src/components/AddFriendModal.jsx index 791260c..b5efd0d 100644 --- a/front-end/src/components/AddFriendModal.jsx +++ b/front-end/src/components/AddFriendModal.jsx @@ -1,103 +1,113 @@ -import React, { useState, useEffect } from 'react'; -import addFriendButton from "../images/plus-button.png"; -import { jwtDecode } from 'jwt-decode'; +import React, { useState, useEffect } from "react"; +import addFriendButton from "../images/plus-button.png"; +import { jwtDecode } from "jwt-decode"; function AddFriendModal({ showModal, onClose }) { - const [userData, setUserData] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); + const [userData, setUserData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); - async function handleSearchClick() { - setUserData(null); - setError(''); - setLoading(true); - const username = document.querySelector('.input-content').value; - try { - const response = await fetch(`http://localhost:3001/searchFriend?username=${username}`); - if (!response.ok) { - if (response.status === 404) { - setError('User not found.'); - } else { - throw new Error(`HTTP error! Status: ${response.status}`); - } - } else { - const data = await response.json(); - setUserData(data); - } - } catch (error) { - console.error("Error fetching data:", error); - setError('Failed to fetch data. Please try again.'); - } finally { - setLoading(false); + async function handleSearchClick() { + setUserData(null); + setError(""); + setLoading(true); + const username = document.querySelector(".input-content").value; + try { + const response = await fetch( + `http://localhost:3001/searchFriend?username=${username}` + ); + if (!response.ok) { + if (response.status === 404) { + setError("User not found."); + } else { + throw new Error(`HTTP error! Status: ${response.status}`); } + } else { + const data = await response.json(); + setUserData(data); + } + } catch (error) { + console.error("Error fetching data:", error); + setError("Failed to fetch data. Please try again."); + } finally { + setLoading(false); } + } - function addFriend(friendUserId) { - const token = localStorage.getItem("token"); - const currentUser = jwtDecode(token); - - if (!currentUser || !currentUser.id) { - console.error("No current user found in local storage."); - return; - } - - if (currentUser.id === friendUserId) { - setError("You cannot add yourself as a friend."); - return; - } - - fetch("http://localhost:3001/addFriends", { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ currentUserId: currentUser.id, friendUserId: friendUserId }), - }) - .then(response => { - if (!response.ok) { - throw new Error('Could not add friend'); - } - return response.text(); - }) - .then(message => { - setError(message); - }) - .catch(error => { - console.error("Error adding friend:", error); - setError("Failed to add friend. Please try again."); - }); + function addFriend(friendUserId) { + const token = localStorage.getItem("token"); + const currentUser = jwtDecode(token); + + if (!currentUser || !currentUser.id) { + console.error("No current user found in local storage."); + return; } + if (currentUser.id === friendUserId) { + setError("You cannot add yourself as a friend."); + return; + } + + fetch("http://localhost:3001/addFriends", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + currentUserId: currentUser.id, + friendUserId: friendUserId, + }), + }) + .then((response) => { + if (!response.ok) { + throw new Error("Could not add friend"); + } + return response.text(); + }) + .then((message) => { + setError(message); + }) + .catch((error) => { + console.error("Error adding friend:", error); + setError("Failed to add friend. Please try again."); + }); + } - return ( -
-
- × -

Add a new friend

- - - {loading &&
Loading...
} - {userData && ( -
- {`avatar`} - {userData.username} - Add friend addFriend(userData._id)} - /> -
- )} - {error && ( -
- {error} -
- )} -
-
- ); + return ( +
+
+ + × + +

Add a new friend

+ + + {loading &&
Loading...
} + {userData && ( +
+ {`avatar`} + {userData.username} + Add friend addFriend(userData._id)} + /> +
+ )} + {error &&
{error}
} +
+
+ ); } -export default AddFriendModal; \ No newline at end of file +export default AddFriendModal; diff --git a/front-end/src/styles/FriendsPage.css b/front-end/src/styles/FriendsPage.css index 01a4b26..4c058f8 100644 --- a/front-end/src/styles/FriendsPage.css +++ b/front-end/src/styles/FriendsPage.css @@ -1,164 +1,164 @@ .friends-page { - @apply bg-white font-mono; + @apply bg-white font-mono; } .page-title { - @apply text-2xl font-bold mb-5 p-2; + @apply text-2xl font-bold mb-5 p-2; } .balance-section { - @apply flex space-x-4 items-center p-4 bg-cyan-100 rounded-lg; + @apply flex space-x-4 items-center p-4 bg-cyan-100 rounded-lg; } .user-avatar { - @apply w-20 h-20 rounded-full border-4 border-cyan-300; + @apply w-20 h-20 rounded-full border-4 border-cyan-300; } .avatar { - @apply rounded-full w-12 h-12 mr-5; + @apply rounded-full w-12 h-12 mr-5; } .balance-title { - @apply font-bold mb-2.5; + @apply font-bold mb-2.5; } .balance-details { - @apply flex flex-col; + @apply flex flex-col; } .friends-list { - @apply h-96 w-full overflow-auto flex flex-col space-y-4 mt-6 bg-cyan-100 bg-opacity-50; + @apply h-96 w-full overflow-auto flex flex-col space-y-4 mt-6 bg-cyan-100 bg-opacity-50; } .friends-list h3 { - @apply mt-5 mb-2.5; + @apply mt-5 mb-2.5; } .friend-item { - @apply flex justify-between items-center p-4 border-b border-gray-200; + @apply flex justify-between items-center p-4 border-b border-gray-200; } .item-name-avatar { - @apply flex items-center; + @apply flex items-center; } .friend-avatar { - @apply rounded-full mr-2.5 w-12 h-12; + @apply rounded-full mr-2.5 w-12 h-12; } .positive-balance { - @apply text-green-500; + @apply text-green-500; } .negative-balance { - @apply text-red-500; + @apply text-red-500; } .add-friends-btn-div { - @apply flex justify-end mt-5; + @apply flex justify-end mt-5; } .add-friends-btn { - @apply w-24 h-16 rounded-full text-center p-1 bg-cyan-100 border-cyan-500 hover:bg-orange-100 active:bg-orange-200; + @apply w-24 h-16 rounded-full text-center p-1 bg-cyan-100 border-cyan-500 hover:bg-orange-100 active:bg-orange-200; } /* Add styling for the pop-up window for add friends*/ .input-content { - @apply w-full p-2.5 rounded-full border border-gray-200 text-lg mb-5; + @apply w-full p-2.5 rounded-full border border-gray-200 text-lg mb-5; } .modal { - @apply flex justify-center items-center fixed inset-0 z-50; + @apply flex justify-center items-center fixed inset-0 z-50; } .modal-content { - @apply bg-cyan-300 rounded-lg w-4/5 p-5 shadow-md; + @apply bg-cyan-300 rounded-lg w-4/5 p-5 shadow-md; } .modal-content h2 { - @apply text-lg leading-6 font-medium text-gray-700 mb-4 mt-0; + @apply text-lg leading-6 font-medium text-gray-700 mb-4 mt-0; } .modal-content button { - @apply w-full p-3 rounded-full border border-slate-100 bg-cyan-100 mb-4; + @apply w-full p-3 rounded-full border border-slate-100 bg-cyan-100 mb-4; } .add-friend-name { - @apply flex-grow text-left px-4; + @apply flex-grow text-left px-4; } .add-friend-item { - @apply flex justify-between items-center p-4 border-b border-gray-200 bg-cyan-100; + @apply flex justify-between items-center p-4 border-b border-gray-200 bg-cyan-100; } .checkbox { - padding-left: 5px; + padding-left: 5px; } .space-to-scroll { - height: 60px; + height: 60px; } /* Dark Mode for Friends Page */ .body-dark-mode { - @apply bg-gray-800; /* add dark mode to the outter most background on the page (area around the container) */ + @apply bg-gray-800; /* add dark mode to the outter most background on the page (area around the container) */ } .dark-mode .friends-page { - @apply bg-gray-800 text-white; - min-height: 100vh; + @apply bg-gray-800 text-white; + min-height: 100vh; } - + .dark-mode .balance-section { - @apply bg-gray-700; + @apply bg-gray-700; } - + .dark-mode .user-avatar { - @apply border-cyan-600; + @apply border-cyan-600; } - + .dark-mode .friends-list { - @apply bg-gray-600 bg-opacity-50; + @apply bg-gray-600 bg-opacity-50; } - + .dark-mode .friend-item { - @apply border-gray-600; + @apply border-gray-600; } - + .dark-mode .add-friends-btn { - @apply bg-gray-500 border-gray-600 hover:bg-gray-600 active:bg-gray-700; + @apply bg-gray-500 border-gray-600 hover:bg-gray-600 active:bg-gray-700; } .dark-mode .add-friends-btn-div { - @apply mb-4; + @apply mb-4; } .dark-mode .space-to-scroll { - @apply bg-gray-700; - height: 0px; + @apply bg-gray-700; + height: 0px; } /* Dark Mode for Add Friends Modal */ .dark-mode .modal-content { - @apply bg-gray-800; + @apply bg-gray-800; } - + .dark-mode .modal-content h2, .dark-mode .add-friend-name, .dark-mode .input-content, .dark-mode .modal-content button { - @apply text-white; + @apply text-white; } - + .dark-mode .input-content { - @apply bg-gray-600 border-gray-500; + @apply bg-gray-600 border-gray-500; } - + .dark-mode .modal-content button { - @apply bg-gray-600 hover:bg-gray-500 active:bg-gray-400; + @apply bg-gray-600 hover:bg-gray-500 active:bg-gray-400; } - + .dark-mode .add-friend-item { - @apply bg-gray-600 border-gray-500; -} \ No newline at end of file + @apply bg-gray-600 border-gray-500; +} From 8e93c3c7ef0ca64b4d13dcad2b3ab0fc454d9262 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Sat, 2 Dec 2023 17:18:03 -0500 Subject: [PATCH 13/60] Formatting --- back-end/routes/addExpenseRoute.js | 3 -- back-end/routes/expenseRoute.js | 63 +++++++++++++++--------------- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/back-end/routes/addExpenseRoute.js b/back-end/routes/addExpenseRoute.js index 68ee984..cddd492 100644 --- a/back-end/routes/addExpenseRoute.js +++ b/back-end/routes/addExpenseRoute.js @@ -31,9 +31,6 @@ router.post( const settleTo = req.body.paidBy; const settleFrom = split.user; let newSettlement; - // Debugging: Log the values and types - console.log(`settleTo: ${settleTo}, Type: ${typeof settleTo}`); - console.log(`settleFrom: ${settleFrom}, Type: ${typeof settleFrom}`); if (settleTo.toString() === settleFrom._id.toString()) { newSettlement = new Settlement({ diff --git a/back-end/routes/expenseRoute.js b/back-end/routes/expenseRoute.js index 377835b..c389f89 100644 --- a/back-end/routes/expenseRoute.js +++ b/back-end/routes/expenseRoute.js @@ -3,37 +3,36 @@ const router = express.Router(); const { Expense } = require("../models/Expense.js"); router.get("/ExpenseDetail/:expenseId", async (req, res) => { - try { - const expenseId = req.params.expenseId; - - const expenseSplit = await Expense.findById(expenseId) - .populate('event') - .populate({ - path: 'splitDetails', - populate: { - path: 'settlement', - model: 'Settlement' - } - }) - .populate({ - path: 'splitDetails', - populate: { - path: 'user', - model: 'User' - } - }) - - if (!expenseSplit) { - return res.status(404).json({ message: "Expense not found" }); - } - // Return the user's events - console.log(expenseSplit) - res.json(expenseSplit) - } catch (error) { - console.error("Error fetching expense details:", error); - res.status(500).json({ message: "Server error" , error}); - } - }); + try { + const expenseId = req.params.expenseId; + + const expenseSplit = await Expense.findById(expenseId) + .populate("event") + .populate({ + path: "splitDetails", + populate: { + path: "settlement", + model: "Settlement", + }, + }) + .populate({ + path: "splitDetails", + populate: { + path: "user", + model: "User", + }, + }); + if (!expenseSplit) { + return res.status(404).json({ message: "Expense not found" }); + } + // Return the user's events + console.log(expenseSplit); + res.json(expenseSplit); + } catch (error) { + console.error("Error fetching expense details:", error); + res.status(500).json({ message: "Server error", error }); + } +}); -module.exports = router; \ No newline at end of file +module.exports = router; From 3a77a4917ab506809a2726634b8a5475f318a735 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Sat, 2 Dec 2023 17:24:24 -0500 Subject: [PATCH 14/60] Update searchFriendRoute.js Make the search not case sensitive --- back-end/routes/searchFriendRoute.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/back-end/routes/searchFriendRoute.js b/back-end/routes/searchFriendRoute.js index 9140235..015b559 100644 --- a/back-end/routes/searchFriendRoute.js +++ b/back-end/routes/searchFriendRoute.js @@ -5,9 +5,10 @@ const { User } = require("../models/User.js"); router.get("/", async (req, res) => { const { username } = req.query; try { - const userData = await User.findOne({ username: username }).select( - "username avatar _id" - ); + const userData = await User.findOne({ + username: new RegExp("^" + username + "$", "i"), + }).select("username avatar _id"); + if (!userData) { return res.status(404).send("User not found"); } From e45c01d2c261d8438aadca80990515d6f7ba6779 Mon Sep 17 00:00:00 2001 From: HedwigO Date: Sun, 3 Dec 2023 12:20:38 -0500 Subject: [PATCH 15/60] add summary of total spending on home page --- front-end/src/components/Home.jsx | 3 +++ front-end/src/styles/Home.css | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/front-end/src/components/Home.jsx b/front-end/src/components/Home.jsx index 2e78e48..b9c99ad 100644 --- a/front-end/src/components/Home.jsx +++ b/front-end/src/components/Home.jsx @@ -242,6 +242,9 @@ const Home = ({ isDarkMode }) => {

Welcome, {data.userName}

+

+ You have spent ${data.totalSpending.toFixed(2)} in total! +

diff --git a/front-end/src/styles/Home.css b/front-end/src/styles/Home.css index be3fa8c..6920972 100644 --- a/front-end/src/styles/Home.css +++ b/front-end/src/styles/Home.css @@ -56,6 +56,10 @@ @apply text-gray-600 flex-1 text-right; } +.total-spent { + @apply text-center font-bold; +} + .view-all { @apply text-center text-sm text-blue-500 mx-auto; } From 2c4a1f160df0b2b618ae5f540d488692ecef5530 Mon Sep 17 00:00:00 2001 From: HedwigO Date: Sun, 3 Dec 2023 12:27:02 -0500 Subject: [PATCH 16/60] adjust dark mode styling for events date --- front-end/src/styles/Events.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/front-end/src/styles/Events.css b/front-end/src/styles/Events.css index 05f78ce..9ac3d7e 100644 --- a/front-end/src/styles/Events.css +++ b/front-end/src/styles/Events.css @@ -92,6 +92,12 @@ min-height: 100vh; } +.dark-mode .Event-date { + @apply bg-gray-600; + @apply text-white; + @apply opacity-100; +} + .dark-mode .add_events_button { @apply bg-gray-500 border-gray-600 hover:bg-gray-600 active:bg-gray-700; } From be74211654fa6e905edadc4cae2bbb07f91a54a9 Mon Sep 17 00:00:00 2001 From: HedwigO Date: Sun, 3 Dec 2023 13:20:24 -0500 Subject: [PATCH 17/60] adjust alignment for SplitModal --- front-end/src/components/SplitModal.js | 21 ++++++++------ front-end/src/styles/SplitModal.css | 38 ++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/front-end/src/components/SplitModal.js b/front-end/src/components/SplitModal.js index 139614a..fd73e0d 100644 --- a/front-end/src/components/SplitModal.js +++ b/front-end/src/components/SplitModal.js @@ -173,23 +173,24 @@ function SplitModal({ )} {participants.map((participant) => (
- {participant.username} + {participant.username} {activeTab === "equally" && ( - - {"$" + (totalAmount / participants.length).toFixed(2)} + + {"$" + (totalAmount / participants.length).toFixed(2)} - )} + )} {activeTab === "percentage" && (
handlePercentageChange(e, participant._id) } /> - % = - + % = + {" " + "$" + calculateAmount( @@ -201,13 +202,17 @@ function SplitModal({
)} {activeTab === "amount" && ( -
- ${" "} +
+
+ $ handleAmountChange(e, participant._id)} />
+
)}
))} diff --git a/front-end/src/styles/SplitModal.css b/front-end/src/styles/SplitModal.css index 62a6336..f43ebd9 100644 --- a/front-end/src/styles/SplitModal.css +++ b/front-end/src/styles/SplitModal.css @@ -23,15 +23,47 @@ } .amountPerPerson { - @apply grid grid-cols-3 p-1 mb-4 rounded-lg; + @apply grid grid-cols-2 gap-4 p-1 mb-4 items-center; +} + +.amountPerPerson span:first-child { + @apply text-left; +} + +.amountPerPerson span:last-child { + @apply text-right; +} + +.amountPerPerson .participant-name { + @apply text-left; +} + +.amountPerPerson .calculated-amount { + @apply text-right; +} + +.amountPerPerson .dollar-sign { + @apply mr-2; } .amountPerPerson input { - @apply w-14 mr-1; + @apply w-full text-right; } .amountPerPerson .percentageContent { - @apply col-span-2; + @apply flex justify-end items-center w-full; +} + +.amountPerPerson .amountInputContainer { + @apply flex items-center; +} + +.amountPerPerson .percentageContent input { + @apply text-right mr-2; +} + +.amountPerPerson .percentageContent span { + @apply whitespace-nowrap; } /* Dark Mode for Split Modal */ From b26aedbdf98257dc92f2a514316e595ac1feb815 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Sun, 3 Dec 2023 14:55:25 -0500 Subject: [PATCH 18/60] Add expense to settlement schema --- back-end/models/Settlement.js | 1 + back-end/routes/addExpenseRoute.js | 70 +++++++++++------------------- 2 files changed, 27 insertions(+), 44 deletions(-) diff --git a/back-end/models/Settlement.js b/back-end/models/Settlement.js index 2e4f242..4fbdb97 100644 --- a/back-end/models/Settlement.js +++ b/back-end/models/Settlement.js @@ -8,6 +8,7 @@ const settlementSchema = new Schema({ settleTo: { type: mongoose.Schema.Types.ObjectId, ref: "User" }, settleFrom: { type: mongoose.Schema.Types.ObjectId, ref: "User" }, event: { type: mongoose.Schema.Types.ObjectId, ref: "Event" }, + expense: { type: mongoose.Schema.Types.ObjectId, ref: "Expense" }, }); // create mongoose Model diff --git a/back-end/routes/addExpenseRoute.js b/back-end/routes/addExpenseRoute.js index b3264e8..0ec53f2 100644 --- a/back-end/routes/addExpenseRoute.js +++ b/back-end/routes/addExpenseRoute.js @@ -25,53 +25,40 @@ router.post( console.log("validation passed"); try { - - let splitDetailsWithSettlements = []; - // Create Settlement objects - for (const split of req.body.peopleSplit) { - const settleTo = req.body.paidBy; - const settleFrom = split.user; - let newSettlement; - // Debugging: Log the values and types - console.log(`settleTo: ${settleTo}, Type: ${typeof settleTo}`); - console.log(`settleFrom: ${settleFrom}, Type: ${typeof settleFrom}`); - - if (settleTo.toString() === settleFrom._id.toString()) { - newSettlement = new Settlement({ - status: true, - amount: split.amount, - settleTo: settleTo, - settleFrom: settleFrom, - event: req.body.event, - }); - } else { - newSettlement = new Settlement({ - status: false, - amount: split.amount, - settleTo: settleTo, - settleFrom: settleFrom, - event: req.body.event, - }); - } - console.log(newSettlement); - await newSettlement.save(); - splitDetailsWithSettlements.push({ user: split.user, settlement: newSettlement._id }); - } - - // Create a new Expense object const newExpense = new Expense({ name: req.body.name, description: req.body.description, totalAmount: req.body.totalAmount, date: new Date(req.body.date), paidBy: req.body.paidBy, - splitDetails: splitDetailsWithSettlements, event: req.body.event, }); - console.log(newExpense); + const savedExpense = await newExpense.save(); + + let splitDetailsWithSettlements = []; + for (const split of req.body.peopleSplit) { + const settleTo = req.body.paidBy; + const settleFrom = split.user; + let newSettlement = new Settlement({ + status: settleTo.toString() === settleFrom._id.toString(), + amount: split.amount, + settleTo: settleTo, + settleFrom: settleFrom, + event: req.body.event, + expense: savedExpense._id, + }); + + await newSettlement.save(); + splitDetailsWithSettlements.push({ + user: split.user, + settlement: newSettlement._id, + }); + } + + savedExpense.splitDetails = splitDetailsWithSettlements; + await savedExpense.save(); - // Save the Expense object to the database const event = await Event.findById(req.body.event); if (!event) { return res @@ -79,24 +66,19 @@ router.post( .json({ status: "Error", message: "Event not found" }); } - // Add the expense ID to the event's expenses list - // ? no "savedExpense" defined, maybe "newExpense"? - const savedExpense = await newExpense.save(); event.expenses.push(savedExpense._id); await event.save(); - // Send a success response res.status(201).json({ status: "Success", message: "Expense added successfully", - data: newExpense, + data: savedExpense, }); } catch (error) { - // Handle any errors that occur during saving to database console.log(error); res.status(500).json({ status: "Error", message: error.message }); } } ); -module.exports = router; \ No newline at end of file +module.exports = router; From b1f4ad528714b990a93ca8b3b4102109013edcf4 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Sun, 3 Dec 2023 18:00:38 -0500 Subject: [PATCH 19/60] Add Friend Detail Page --- back-end/app.js | 2 + back-end/routes/searchUserInfoRoute.js | 21 ++++ back-end/routes/settlementRoute.js | 4 +- front-end/src/App.js | 2 + front-end/src/components/FriendDetailPage.js | 118 +++++++++++++++++++ front-end/src/components/FriendsPage.jsx | 61 +++++----- front-end/src/styles/FriendDetailPage.css | 27 +++++ 7 files changed, 202 insertions(+), 33 deletions(-) create mode 100644 back-end/routes/searchUserInfoRoute.js create mode 100644 front-end/src/components/FriendDetailPage.js create mode 100644 front-end/src/styles/FriendDetailPage.css diff --git a/back-end/app.js b/back-end/app.js index b37f939..528d01c 100644 --- a/back-end/app.js +++ b/back-end/app.js @@ -21,6 +21,7 @@ const logoutRoute = require("./routes/logoutRoute"); const searchFriendRoute = require("./routes/searchFriendRoute"); const expenseRoute = require("./routes/expenseRoute"); const settlementRoute = require("./routes/settlementRoute"); +const searchUserInfoRoute = require("./routes/searchUserInfoRoute"); // connect to the database // console.log(`Conneting to MongoDB at ${process.env.MONGODB_URI}`) @@ -59,6 +60,7 @@ app.use("/logout", logoutRoute); app.use("/searchFriend", searchFriendRoute); app.use("/expense", expenseRoute); app.use("/settlement", settlementRoute); +app.use("/search-user-info", searchUserInfoRoute); // export the express app we created to make it available to other modules module.exports = app; diff --git a/back-end/routes/searchUserInfoRoute.js b/back-end/routes/searchUserInfoRoute.js new file mode 100644 index 0000000..fca7d50 --- /dev/null +++ b/back-end/routes/searchUserInfoRoute.js @@ -0,0 +1,21 @@ +const express = require("express"); +const router = express.Router(); +const { User } = require("../models/User.js"); + +// Route to get a friend's details by their ID +router.get("/:friendId", async (req, res) => { + try { + const friendId = req.params.friendId; + const friend = await User.findById(friendId); + + if (!friend) { + return res.status(404).json({ message: "Friend not found" }); + } + res.json(friend); + } catch (error) { + console.error(error); + res.status(500).json({ message: "Error fetching friend data" }); + } +}); + +module.exports = router; diff --git a/back-end/routes/settlementRoute.js b/back-end/routes/settlementRoute.js index 7acf71e..97b9c32 100644 --- a/back-end/routes/settlementRoute.js +++ b/back-end/routes/settlementRoute.js @@ -27,7 +27,9 @@ router.get("/from/:userId1/to/:userId2", async (req, res) => { const settlements = await Settlement.find({ settleFrom: userId1, settleTo: userId2, - }); + }) + .populate("expense") + .populate("event"); res.status(200).json(settlements); } catch (error) { diff --git a/front-end/src/App.js b/front-end/src/App.js index 719e9b8..f9ac45b 100644 --- a/front-end/src/App.js +++ b/front-end/src/App.js @@ -6,6 +6,7 @@ import "./App.css"; import Login from "./components/Login"; import Home from "./components/Home"; import FriendsPage from "./components/FriendsPage"; +import FriendDetailPage from "./components/FriendDetailPage"; import Events from "./components/Events"; import Expense from "./components/Expense"; import UserInfo from "./components/UserInfo"; @@ -90,6 +91,7 @@ function App() { /> } /> + } />
diff --git a/front-end/src/components/FriendDetailPage.js b/front-end/src/components/FriendDetailPage.js new file mode 100644 index 0000000..11a52ae --- /dev/null +++ b/front-end/src/components/FriendDetailPage.js @@ -0,0 +1,118 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { jwtDecode } from "jwt-decode"; +import { Link, useParams } from "react-router-dom"; +import Navbar from "./Navbar"; +import "../styles/FriendDetailPage.css"; + +function FriendDetailPage() { + const [settlements, setSettlements] = useState({ + fromUserToFriend: [], + fromFriendToUser: [], + }); + const [friend, setFriend] = useState(null); + const { friendId } = useParams(); + const token = localStorage.getItem("token"); + const currentUser = jwtDecode(token); + const userId = currentUser.id; + + useEffect(() => { + const fetchSettlements = async () => { + try { + const friendInfo = await axios.get( + `http://localhost:3001/search-user-info/${friendId}` + ); + const fromUserToFriend = await axios.get( + `http://localhost:3001/settlement/from/${userId}/to/${friendId}` + ); + const fromFriendToUser = await axios.get( + `http://localhost:3001/settlement/from/${friendId}/to/${userId}` + ); + setFriend(friendInfo.data); + setSettlements({ + fromUserToFriend: fromUserToFriend.data, + fromFriendToUser: fromFriendToUser.data, + }); + } catch (error) { + console.error("Error fetching friend and settlements:", error); + } + }; + + fetchSettlements(); + }, [friendId, userId]); + + const renderSettlements = (settlementList, isFromUser) => { + return settlementList.map((settlement, index) => { + const expenseName = settlement.expense?.name || "Unknown"; + return ( +
+ + {settlement.event.name}|{expenseName} + + + {isFromUser ? "-" : "+"}${Math.abs(settlement.amount).toFixed(2)} + + + {settlement.status ? "Settled" : ""} + +
+ ); + }); + }; + + const calculateTotalBalance = () => { + const amountYouOwe = settlements.fromUserToFriend.reduce( + (total, settlement) => total + settlement.amount, + 0 + ); + const amountFriendOwesYou = settlements.fromFriendToUser.reduce( + (total, settlement) => total + settlement.amount, + 0 + ); + return { amountYouOwe, amountFriendOwesYou }; + }; + + const { amountYouOwe, amountFriendOwesYou } = calculateTotalBalance(); + + // Check if friend is not null before rendering + if (!friend) { + return
Loading...
; + } + + return ( +
+
+

+ Friends|{friend.username} +

+
+ +
+ Friend Avatar +
+
Balance Overview
+
+
+ You owe: ${amountYouOwe.toFixed(2)} +
+
+ {friend.username} owes: ${amountFriendOwesYou.toFixed(2)} +
+
+
+
+ +
+

Amount You Owe {friend.username}

+ {renderSettlements(settlements.fromUserToFriend, true)} +
+
+

Amount {friend.username} Owe You

+ {renderSettlements(settlements.fromFriendToUser, false)} +
+ +
+ ); +} + +export default FriendDetailPage; diff --git a/front-end/src/components/FriendsPage.jsx b/front-end/src/components/FriendsPage.jsx index 2fbca55..c785616 100644 --- a/front-end/src/components/FriendsPage.jsx +++ b/front-end/src/components/FriendsPage.jsx @@ -1,5 +1,6 @@ import axios from "axios"; import React, { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; import "../styles/FriendsPage.css"; import AddFriendModal from "./AddFriendModal"; import Navbar from "./Navbar"; @@ -32,7 +33,6 @@ function FriendsPage({ isDarkMode }) { `http://localhost:3001/friends/${userId}` ); setUserData(result.data); - console.log(result.data); } catch (err) { console.error(err); } @@ -65,7 +65,6 @@ function FriendsPage({ isDarkMode }) { friend._id, error ); - // Optionally, you can push a default value or skip the friend settlements.push({ friend: friend, fromUserToFriend: [], @@ -73,8 +72,6 @@ function FriendsPage({ isDarkMode }) { }); } } - - console.log(settlements); setSettlements(settlements); }; @@ -86,17 +83,23 @@ function FriendsPage({ isDarkMode }) { return items.map((item) => { const balance = item.fromUserToFriend.reduce( - (acc, settlement) => acc - settlement.amount, + (acc, settlement) => + acc - (settlement.status === false ? settlement.amount : 0), 0 ) + item.fromFriendToUser.reduce( - (acc, settlement) => acc + settlement.amount, + (acc, settlement) => + acc + (settlement.status === false ? settlement.amount : 0), 0 ); const settlementIds = [ - ...item.fromUserToFriend.map((settlement) => settlement._id), - ...item.fromFriendToUser.map((settlement) => settlement._id), + ...item.fromUserToFriend + .filter((settlement) => settlement.status === false) + .map((settlement) => settlement._id), + ...item.fromFriendToUser + .filter((settlement) => settlement.status === false) + .map((settlement) => settlement._id), ]; return { @@ -123,8 +126,6 @@ function FriendsPage({ isDarkMode }) { if (!userData) return
Loading...
; - // const totalBalance = userData.friends && userData.friends.length ? userData.friends.reduce((acc, friend) => acc + parseFloat(friend.balance.replace('$', '')), 0) : 0; - return (

Friends

@@ -148,29 +149,25 @@ function FriendsPage({ isDarkMode }) {
    {calculateBalances(settlements).map((friend) => (
  • - - {`${friend.username}'s - {friend.username} - -
    - - ${friend.balance.toFixed(2)} - -
    - + + {`${friend.username}'s -
    -
    + {friend.username} + + + + {friend.balance === 0 + ? "Settled" + : `$${friend.balance.toFixed(2)}`} +
  • ))}
diff --git a/front-end/src/styles/FriendDetailPage.css b/front-end/src/styles/FriendDetailPage.css new file mode 100644 index 0000000..ee4fe67 --- /dev/null +++ b/front-end/src/styles/FriendDetailPage.css @@ -0,0 +1,27 @@ +header { + @apply text-2xl font-bold; +} + +.user-avatar { + @apply w-20 h-20 rounded-full border-4 border-cyan-300; +} + +.balance { + @apply flex space-x-4 items-center p-4 rounded-lg; +} + +.settlements-section { + @apply h-64 w-full overflow-auto flex flex-col space-y-4 mt-6 bg-cyan-100 bg-opacity-50; +} + +.settlements-section h3 { + @apply text-lg font-bold; +} + +.settlements-section .settlement-label { + @apply text-base font-bold; +} + +.settlements-section .settlement-item { + @apply flex justify-between items-center p-4 border-b border-gray-200; +} From 8f0523423ba80e35bc6e8d14144be125ba60ad1f Mon Sep 17 00:00:00 2001 From: elaineZhang67 <144281338+elaineZhang67@users.noreply.github.com> Date: Sun, 3 Dec 2023 18:16:13 -0500 Subject: [PATCH 20/60] mplement the expenseSettle function --- back-end/routes/expenseRoute.js | 1 + back-end/routes/expenseStatusRoute.js | 1 - front-end/src/components/Expense.jsx | 23 ++++++++++++++--------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/back-end/routes/expenseRoute.js b/back-end/routes/expenseRoute.js index 377835b..82f3fb2 100644 --- a/back-end/routes/expenseRoute.js +++ b/back-end/routes/expenseRoute.js @@ -26,6 +26,7 @@ router.get("/ExpenseDetail/:expenseId", async (req, res) => { if (!expenseSplit) { return res.status(404).json({ message: "Expense not found" }); } + // Return the user's events console.log(expenseSplit) res.json(expenseSplit) diff --git a/back-end/routes/expenseStatusRoute.js b/back-end/routes/expenseStatusRoute.js index 1a1ad0a..d9f8997 100644 --- a/back-end/routes/expenseStatusRoute.js +++ b/back-end/routes/expenseStatusRoute.js @@ -18,7 +18,6 @@ router.post( try { const settlementId = req.params.settlementId; const status = req.body.status; - console.log("__________________________") console.log(status); const updatedSettlement = await Settlement.findByIdAndUpdate( diff --git a/front-end/src/components/Expense.jsx b/front-end/src/components/Expense.jsx index 25d0d86..7c3e6a1 100644 --- a/front-end/src/components/Expense.jsx +++ b/front-end/src/components/Expense.jsx @@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react'; import '../styles/Expense.css'; import axios from "axios"; import Navbar from "./Navbar"; -import { Link, useNavigate, useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; function Expense({ isDarkMode }) { @@ -15,7 +15,7 @@ function Expense({ isDarkMode }) { const fetchData = async () => { try { const response = await axios.get(`http://localhost:3001/expense/ExpenseDetail/${expenseId}`); - console.log(response.data); + console.log("Fetched Data:", response.data); // Debug setExpensesData(response.data); }catch(error){ console.error("There was an error fetching the data:", error); @@ -25,7 +25,7 @@ function Expense({ isDarkMode }) { const settleExpenses = async(settlementId, newStatus) => { try{ const response = await axios.post(`http://localhost:3001/expenseStatus/${settlementId}`, {status: newStatus}); - await console.log('Settlements updated:', response.data); + console.log('Settlements updated:', response.data); } catch (error) { console.error('Error updating settlements:', error); console.log(error.response.data); @@ -50,15 +50,16 @@ function Expense({ isDarkMode }) { fetchData(); }, []); + {/* navigates to the previous page */} const handleTitleClick = () => { navigate(-1); }; - const handleSettlementChange = (e, settlementId, newStatus) => { - if(e.target.checked){ - settleExpenses(settlementId, newStatus) - } + const handleSettlementChange = async (e, settlementId) => { + const newStatus = e.target.checked; + await settleExpenses(settlementId, newStatus); + fetchData(); }; return ( @@ -75,7 +76,9 @@ function Expense({ isDarkMode }) { {expensesData && (
- {expensesData.splitDetails && expensesData.splitDetails.map(split => ( + {expensesData.splitDetails && expensesData.splitDetails + .sort((a, b) => a.settlement.status === b.settlement.status ? 0 : a.settlement.status ? 1 : -1) + .map(split => (
{split.user.username} 0 ? 'positive' : 'negative'}>{split.settlement.amount} @@ -83,7 +86,9 @@ function Expense({ isDarkMode }) { handleSettlementChange(e, split.settlement._id, true)} + checked = {split.settlement.isChecked} + onChange={(e) => handleSettlementChange(e, split.settlement._id)} + defaultChecked={split.settlement.status} />
From 5dc54ace8c82b6fdb1a7fc26760934f98a6e5751 Mon Sep 17 00:00:00 2001 From: HedwigO Date: Sun, 3 Dec 2023 19:38:51 -0500 Subject: [PATCH 21/60] add dark mode for FriendDetail page --- front-end/src/App.js | 4 +++- front-end/src/components/FriendDetailPage.js | 15 ++++++++++++++- front-end/src/styles/FriendDetailPage.css | 20 ++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/front-end/src/App.js b/front-end/src/App.js index f9ac45b..a8d302a 100644 --- a/front-end/src/App.js +++ b/front-end/src/App.js @@ -91,7 +91,9 @@ function App() { /> } /> - } /> + } />
diff --git a/front-end/src/components/FriendDetailPage.js b/front-end/src/components/FriendDetailPage.js index 11a52ae..3f287c0 100644 --- a/front-end/src/components/FriendDetailPage.js +++ b/front-end/src/components/FriendDetailPage.js @@ -5,7 +5,7 @@ import { Link, useParams } from "react-router-dom"; import Navbar from "./Navbar"; import "../styles/FriendDetailPage.css"; -function FriendDetailPage() { +function FriendDetailPage({ isDarkMode }) { const [settlements, setSettlements] = useState({ fromUserToFriend: [], fromFriendToUser: [], @@ -41,6 +41,19 @@ function FriendDetailPage() { fetchSettlements(); }, [friendId, userId]); + /* useEffect for controlling DarkMode of the margin around the page */ + useEffect(() => { + if (isDarkMode) { + document.body.classList.add("body-dark-mode"); + } else { + document.body.classList.remove("body-dark-mode"); + } + // if not in dark mode, remove this effect + return () => { + document.body.classList.remove("body-dark-mode"); + }; + }, [isDarkMode]); + const renderSettlements = (settlementList, isFromUser) => { return settlementList.map((settlement, index) => { const expenseName = settlement.expense?.name || "Unknown"; diff --git a/front-end/src/styles/FriendDetailPage.css b/front-end/src/styles/FriendDetailPage.css index ee4fe67..15f7e36 100644 --- a/front-end/src/styles/FriendDetailPage.css +++ b/front-end/src/styles/FriendDetailPage.css @@ -25,3 +25,23 @@ header { .settlements-section .settlement-item { @apply flex justify-between items-center p-4 border-b border-gray-200; } + +/* Dark Mode for Friend Detail Page */ +.dark-mode #friend-detail-page { + @apply bg-gray-800 text-white; /* add dark mode to the outter most background on the page (area around the container) */ +} + +.dark-mode .balance { + @apply bg-gray-700; +} + +.dark-mode .user-avatar { + @apply border-cyan-600; +} + +.dark-mode .settlements-section { + @apply bg-gray-700 mb-4; +} + + + From 32414d3a7ca0d8fb3d3175c3e7cef786b470032d Mon Sep 17 00:00:00 2001 From: joyc7 Date: Sun, 3 Dec 2023 20:42:35 -0500 Subject: [PATCH 22/60] Refine Friend details page --- front-end/src/components/FriendDetailPage.js | 30 ++++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/front-end/src/components/FriendDetailPage.js b/front-end/src/components/FriendDetailPage.js index 11a52ae..e424390 100644 --- a/front-end/src/components/FriendDetailPage.js +++ b/front-end/src/components/FriendDetailPage.js @@ -62,13 +62,19 @@ function FriendDetailPage() { const calculateTotalBalance = () => { const amountYouOwe = settlements.fromUserToFriend.reduce( - (total, settlement) => total + settlement.amount, + (total, settlement) => { + return settlement.status ? total : total + settlement.amount; + }, 0 ); + const amountFriendOwesYou = settlements.fromFriendToUser.reduce( - (total, settlement) => total + settlement.amount, + (total, settlement) => { + return settlement.status ? total : total + settlement.amount; + }, 0 ); + return { amountYouOwe, amountFriendOwesYou }; }; @@ -92,12 +98,18 @@ function FriendDetailPage() {
Balance Overview
-
- You owe: ${amountYouOwe.toFixed(2)} -
-
- {friend.username} owes: ${amountFriendOwesYou.toFixed(2)} -
+ {amountYouOwe === 0 && amountFriendOwesYou === 0 ? ( +
Your balances are settled!
+ ) : ( + <> +
+ You owe: ${amountYouOwe.toFixed(2)} +
+
+ {friend.username} owes: ${amountFriendOwesYou.toFixed(2)} +
+ + )}
@@ -107,7 +119,7 @@ function FriendDetailPage() { {renderSettlements(settlements.fromUserToFriend, true)}
-

Amount {friend.username} Owe You

+

Amount {friend.username} Owes You

{renderSettlements(settlements.fromFriendToUser, false)}
From 34d78d7416e497cee5b70bf2b43fcc87e7e6e5e9 Mon Sep 17 00:00:00 2001 From: elaineZhang67 <144281338+elaineZhang67@users.noreply.github.com> Date: Sun, 3 Dec 2023 22:29:33 -0500 Subject: [PATCH 23/60] modify the logic of split and expense balance --- front-end/src/components/Event.js | 36 ++++++++++++++++++++-------- front-end/src/components/Expense.jsx | 30 ++++++++++++++++++++--- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/front-end/src/components/Event.js b/front-end/src/components/Event.js index 789cf86..47bac36 100644 --- a/front-end/src/components/Event.js +++ b/front-end/src/components/Event.js @@ -10,13 +10,14 @@ const Event = (props) => { const [userExpenses, setUserExpenses] = useState([]); const isDarkMode = props.isDarkMode; const { eventId } = useParams(); + const [balance, setBalance] = useState(0); console.log("Event ID:", eventId); // check the eventID received function reformatDate(dateStr) { const months = [ "Jan", "Feb", - "Mar", + "Mar", "Apr", "May", "Jun", @@ -53,28 +54,44 @@ const Event = (props) => { const isParticipant = expense.splitDetails.find( (detail) => detail.user === userId ); - let settlement; + //let settlement; + let userBalance = 0; if (isParticipant) { // User is a participant, find the settlement from splitDetails + /* const userSplitDetail = expense.splitDetails.find( (detail) => detail.user === userId ); settlement = userSplitDetail ? userSplitDetail.settlement : { amount: 0 }; // Add other fields as needed + */ + if(expense.paidBy === userId){ + expense.splitDetails.forEach(split =>{ + if(!split.settlement.status){ + userBalance += split.settlement.amount; + } + }) + } else { + //user is not the one who paid, find what the user owe to the person who paid + const paidBy = expense.splitDetails.find(split => split.user === expense.paidBy); + if(paidBy){ + userBalance = paidBy.settlement.status ? 0 : -paidBy.settlement.amount; + } + } } else { // User is not a participant, create a settlement object with amount 0 - settlement = { amount: 0 }; + userBalance = 0 ; } return { expense: expense, - settlement: settlement, + settlement: userBalance, }; }); - setUserExpenses(processedExpenses); + return processedExpenses; }; useEffect(() => { @@ -129,8 +146,10 @@ const Event = (props) => { useEffect(() => { const token = localStorage.getItem("token"); const currentUser = jwtDecode(token); + //const currentUserId = currentUser.id; if (data.expenses && currentUser) { - processUserExpenses(data.expenses, currentUser.id); + const processedExpenses = processUserExpenses(data.expenses, currentUser.id); + setUserExpenses(processedExpenses) } }, [data]); @@ -166,10 +185,7 @@ const Event = (props) => {
{item.expense.name}
-
${item.settlement.amount}
-
- -
+
${item.settlement}
))} diff --git a/front-end/src/components/Expense.jsx b/front-end/src/components/Expense.jsx index 7c3e6a1..20dcf8f 100644 --- a/front-end/src/components/Expense.jsx +++ b/front-end/src/components/Expense.jsx @@ -5,10 +5,11 @@ import '../styles/Expense.css'; import axios from "axios"; import Navbar from "./Navbar"; import { useNavigate, useParams } from "react-router-dom"; - +import { jwtDecode } from "jwt-decode"; function Expense({ isDarkMode }) { const [expensesData, setExpensesData] = useState([]); + const [filteredData, setFilteredData] = useState([]); const navigate = useNavigate(); const { expenseId } = useParams(); @@ -16,7 +17,9 @@ function Expense({ isDarkMode }) { try { const response = await axios.get(`http://localhost:3001/expense/ExpenseDetail/${expenseId}`); console.log("Fetched Data:", response.data); // Debug + const processedData = processExpenses(response.data, currentuserId); setExpensesData(response.data); + setFilteredData(processedData); }catch(error){ console.error("There was an error fetching the data:", error); } @@ -50,6 +53,27 @@ function Expense({ isDarkMode }) { fetchData(); }, []); + const token = localStorage.getItem("token") + const currentUser = jwtDecode(token); + const currentuserId = currentUser.id + + const processExpenses = (expensesData, userId) =>{ + const isParticipant = expensesData.splitDetails.some(split => split.user._id === userId); + + if (!isParticipant) { + // If not a participant, return an empty array or another appropriate value + return []; + } + + let filteredExpenses; + + if(expensesData.paidBy === userId){ + filteredExpenses = expensesData.splitDetails.filter(split => split.user._id !== userId) + }else{ + filteredExpenses = expensesData.splitDetails.filter(split => split.user._id === userId && expensesData.paidBy !== userId); + } + return filteredExpenses; + } {/* navigates to the previous page */} const handleTitleClick = () => { @@ -76,11 +100,11 @@ function Expense({ isDarkMode }) { {expensesData && (
- {expensesData.splitDetails && expensesData.splitDetails + {filteredData && filteredData .sort((a, b) => a.settlement.status === b.settlement.status ? 0 : a.settlement.status ? 1 : -1) .map(split => (
- {split.user.username} + {split.user?.username || 'Unknown User'} 0 ? 'positive' : 'negative'}>{split.settlement.amount}
Date: Sun, 3 Dec 2023 22:35:36 -0500 Subject: [PATCH 24/60] modify the logic --- front-end/src/components/Event.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/front-end/src/components/Event.js b/front-end/src/components/Event.js index 47bac36..5c6a9bf 100644 --- a/front-end/src/components/Event.js +++ b/front-end/src/components/Event.js @@ -74,10 +74,11 @@ const Event = (props) => { } }) } else { + const user = expense.splitDetails.find(split => split.user) //user is not the one who paid, find what the user owe to the person who paid - const paidBy = expense.splitDetails.find(split => split.user === expense.paidBy); - if(paidBy){ - userBalance = paidBy.settlement.status ? 0 : -paidBy.settlement.amount; + //const paidBy = expense.splitDetails.find(split => split.user === expense.paidBy); + if(user){ + userBalance = user.settlement.status ? 0 : -user.settlement.amount; } } } else { From 1ccc5ba7e1f3cdc3a9363c921ff988e0165ef4b7 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Sun, 3 Dec 2023 23:21:30 -0500 Subject: [PATCH 25/60] Change display of Balance section --- front-end/src/components/FriendsPage.jsx | 33 ++++++++++-------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/front-end/src/components/FriendsPage.jsx b/front-end/src/components/FriendsPage.jsx index c785616..9a4b1bb 100644 --- a/front-end/src/components/FriendsPage.jsx +++ b/front-end/src/components/FriendsPage.jsx @@ -110,19 +110,16 @@ function FriendsPage({ isDarkMode }) { }); }; - const totalBalance = settlements.reduce((acc, item) => { - return ( - acc + - item.fromUserToFriend.reduce( - (sum, settlement) => sum - settlement.amount, - 0 - ) + - item.fromFriendToUser.reduce( - (sum, settlement) => sum + settlement.amount, - 0 - ) - ); - }, 0); + let totalOwed = 0; + let totalOwing = 0; + const balances = calculateBalances(settlements); + balances.forEach((friend) => { + if (friend.balance < 0) { + totalOwed += Math.abs(friend.balance); + } else { + totalOwing += friend.balance; + } + }); if (!userData) return
Loading...
; @@ -134,12 +131,10 @@ function FriendsPage({ isDarkMode }) {
Total balance
- {totalBalance < 0 ? ( -
You owe ${Math.abs(totalBalance).toFixed(2)}
- ) : totalBalance > 0 ? ( -
You are owed ${totalBalance.toFixed(2)}
- ) : ( -
All Balances are Settled!
+ {totalOwed > 0 &&
You owe ${totalOwed.toFixed(2)}
} + {totalOwing > 0 &&
You are owed ${totalOwing.toFixed(2)}
} + {totalOwed === 0 && totalOwing === 0 && ( +
All Balances are Settled!
)}
From 10078c53e355d57d6e7613cb3007a3d30ee32956 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Sun, 3 Dec 2023 23:39:59 -0500 Subject: [PATCH 26/60] Debug --- back-end/app.js | 1 + back-end/routes/expenseStatusRoute.js | 44 +++++++++++++-------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/back-end/app.js b/back-end/app.js index b50f39e..7120798 100644 --- a/back-end/app.js +++ b/back-end/app.js @@ -62,6 +62,7 @@ app.use("/searchFriend", searchFriendRoute); app.use("/expense", expenseRoute); app.use("/settlement", settlementRoute); app.use("/search-user-info", searchUserInfoRoute); +app.use("/expenseStatus", expenseStatusRoute); // export the express app we created to make it available to other modules module.exports = app; diff --git a/back-end/routes/expenseStatusRoute.js b/back-end/routes/expenseStatusRoute.js index d9f8997..bf8697f 100644 --- a/back-end/routes/expenseStatusRoute.js +++ b/back-end/routes/expenseStatusRoute.js @@ -1,13 +1,12 @@ const express = require("express"); const router = express.Router(); const { body, validationResult } = require("express-validator"); -const {Settlement} = require("../models/Settlement.js") +const { Settlement } = require("../models/Settlement.js"); router.post( - "/:settlementId", + "/:settlementId", - async (req, res) => { - + async (req, res) => { /* const errors = validationResult(req); if (!errors.isEmpty()) { @@ -15,10 +14,10 @@ router.post( } */ - try { - const settlementId = req.params.settlementId; - const status = req.body.status; - console.log(status); + try { + const settlementId = req.params.settlementId; + const status = req.body.status; + console.log(status); const updatedSettlement = await Settlement.findByIdAndUpdate( settlementId, @@ -26,21 +25,22 @@ router.post( { new: true } ); - if(!updatedSettlement){ + console.log(updatedSettlement); + + if (!updatedSettlement) { return res.status(404).json({ message: "Settlement not found" }); } - // Send a success response - res.status(200).json({ - message: "Settlements updated successfully", - data: updatedSettlement, - }); - } catch (error) { - // Handle any errors that occur during saving to database - console.error(error); - res.status(500).json({ status: "Error", message: error.message }); - } + // Send a success response + res.status(200).json({ + message: "Settlements updated successfully", + data: updatedSettlement, + }); + } catch (error) { + // Handle any errors that occur during saving to database + console.error(error); + res.status(500).json({ status: "Error", message: error.message }); } - ); - + } +); -module.exports = router; \ No newline at end of file +module.exports = router; From e3aa24a1fc056372093698c9ca86f0e4df209f7d Mon Sep 17 00:00:00 2001 From: elaineZhang67 <144281338+elaineZhang67@users.noreply.github.com> Date: Sun, 3 Dec 2023 23:55:29 -0500 Subject: [PATCH 27/60] Update Event.js --- front-end/src/components/Event.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/front-end/src/components/Event.js b/front-end/src/components/Event.js index 5c6a9bf..42ce8fd 100644 --- a/front-end/src/components/Event.js +++ b/front-end/src/components/Event.js @@ -10,7 +10,7 @@ const Event = (props) => { const [userExpenses, setUserExpenses] = useState([]); const isDarkMode = props.isDarkMode; const { eventId } = useParams(); - const [balance, setBalance] = useState(0); + //const [balance, setBalance] = useState(0); console.log("Event ID:", eventId); // check the eventID received function reformatDate(dateStr) { @@ -166,7 +166,7 @@ const Event = (props) => {
-

{data.description}

+

{data.description}

@@ -207,4 +207,4 @@ const Event = (props) => { ); }; -export default Event; +export default Event; \ No newline at end of file From d1b1dbbc28bbd23f5b35f762462168161b10ab09 Mon Sep 17 00:00:00 2001 From: elaineZhang67 <144281338+elaineZhang67@users.noreply.github.com> Date: Mon, 4 Dec 2023 01:01:29 -0500 Subject: [PATCH 28/60] styling --- front-end/src/components/Event.js | 4 ++-- front-end/src/components/Expense.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/front-end/src/components/Event.js b/front-end/src/components/Event.js index 42ce8fd..8d9b7d7 100644 --- a/front-end/src/components/Event.js +++ b/front-end/src/components/Event.js @@ -66,7 +66,7 @@ const Event = (props) => { settlement = userSplitDetail ? userSplitDetail.settlement : { amount: 0 }; // Add other fields as needed - */ + */ if(expense.paidBy === userId){ expense.splitDetails.forEach(split =>{ if(!split.settlement.status){ @@ -186,7 +186,7 @@ const Event = (props) => {
{item.expense.name}
-
${item.settlement}
+
${item.settlement.toFixed(2)}
))} diff --git a/front-end/src/components/Expense.jsx b/front-end/src/components/Expense.jsx index 20dcf8f..340d916 100644 --- a/front-end/src/components/Expense.jsx +++ b/front-end/src/components/Expense.jsx @@ -105,7 +105,7 @@ function Expense({ isDarkMode }) { .map(split => (
{split.user?.username || 'Unknown User'} - 0 ? 'positive' : 'negative'}>{split.settlement.amount} + 0 ? 'positive' : 'negative'}>{split.settlement.amount.toFixed(2)}
Date: Mon, 4 Dec 2023 01:31:13 -0500 Subject: [PATCH 29/60] debug --- front-end/src/components/Event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front-end/src/components/Event.js b/front-end/src/components/Event.js index 8d9b7d7..b186e3a 100644 --- a/front-end/src/components/Event.js +++ b/front-end/src/components/Event.js @@ -74,7 +74,7 @@ const Event = (props) => { } }) } else { - const user = expense.splitDetails.find(split => split.user) + const user = expense.splitDetails.find(split => split.user === userId) //user is not the one who paid, find what the user owe to the person who paid //const paidBy = expense.splitDetails.find(split => split.user === expense.paidBy); if(user){ From 57c592b1de362c915e0d2c9a61b450e8c883e61c Mon Sep 17 00:00:00 2001 From: elaineZhang67 <144281338+elaineZhang67@users.noreply.github.com> Date: Mon, 4 Dec 2023 02:10:14 -0500 Subject: [PATCH 30/60] allow the paidBy person to be displayed when the currentUser is not the paidby --- back-end/routes/expenseRoute.js | 1 + front-end/src/components/Expense.jsx | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/back-end/routes/expenseRoute.js b/back-end/routes/expenseRoute.js index c389f89..9ea75a1 100644 --- a/back-end/routes/expenseRoute.js +++ b/back-end/routes/expenseRoute.js @@ -8,6 +8,7 @@ router.get("/ExpenseDetail/:expenseId", async (req, res) => { const expenseSplit = await Expense.findById(expenseId) .populate("event") + .populate('paidBy', 'username') .populate({ path: "splitDetails", populate: { diff --git a/front-end/src/components/Expense.jsx b/front-end/src/components/Expense.jsx index 340d916..61a7aae 100644 --- a/front-end/src/components/Expense.jsx +++ b/front-end/src/components/Expense.jsx @@ -20,6 +20,7 @@ function Expense({ isDarkMode }) { const processedData = processExpenses(response.data, currentuserId); setExpensesData(response.data); setFilteredData(processedData); + console.log(":", processedData) }catch(error){ console.error("There was an error fetching the data:", error); } @@ -65,12 +66,12 @@ function Expense({ isDarkMode }) { return []; } - let filteredExpenses; + let filteredExpenses =[]; - if(expensesData.paidBy === userId){ - filteredExpenses = expensesData.splitDetails.filter(split => split.user._id !== userId) + if(expensesData.paidBy._id === userId){ + filteredExpenses = expensesData.splitDetails.filter(split => split.user._id !== userId).map(split => ({ ...split, displayName: split.user.username })); }else{ - filteredExpenses = expensesData.splitDetails.filter(split => split.user._id === userId && expensesData.paidBy !== userId); + filteredExpenses = expensesData.splitDetails.filter(split => split.user._id === userId && expensesData.paidBy !== userId).map(split => ({ ...split, displayName: expensesData.paidBy.username }));; } return filteredExpenses; } @@ -104,7 +105,7 @@ function Expense({ isDarkMode }) { .sort((a, b) => a.settlement.status === b.settlement.status ? 0 : a.settlement.status ? 1 : -1) .map(split => (
- {split.user?.username || 'Unknown User'} + {split.displayName} 0 ? 'positive' : 'negative'}>{split.settlement.amount.toFixed(2)}
Date: Mon, 4 Dec 2023 08:26:49 -0500 Subject: [PATCH 31/60] Add checkbox for settle settlements --- front-end/src/components/FriendDetailPage.js | 53 ++++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/front-end/src/components/FriendDetailPage.js b/front-end/src/components/FriendDetailPage.js index aad6081..614362d 100644 --- a/front-end/src/components/FriendDetailPage.js +++ b/front-end/src/components/FriendDetailPage.js @@ -54,8 +54,48 @@ function FriendDetailPage({ isDarkMode }) { }; }, [isDarkMode]); + const settleExpenses = async (settlementId, newStatus) => { + try { + const response = await axios.post( + `http://localhost:3001/expenseStatus/${settlementId}`, + { status: newStatus } + ); + console.log("Settlements updated:", response.data); + } catch (error) { + console.error("Error updating settlements:", error); + console.log(error.response.data); + } + }; + + const handleSettlementChange = async (e, settlementId) => { + const newStatus = e.target.checked; + await settleExpenses(settlementId, newStatus); + setSettlements((prevSettlements) => { + const updateSettlements = (settlements) => { + return settlements.map((settlement) => { + if (settlement._id === settlementId) { + return { ...settlement, status: newStatus }; + } + return settlement; + }); + }; + return { + fromUserToFriend: updateSettlements(prevSettlements.fromUserToFriend), + fromFriendToUser: updateSettlements(prevSettlements.fromFriendToUser), + }; + }); + }; + const renderSettlements = (settlementList, isFromUser) => { - return settlementList.map((settlement, index) => { + // Sort the settlements: unchecked items first, then checked items + const sortedSettlements = [...settlementList].sort((a, b) => { + if (a.status === b.status) { + return 0; // Keep original order if both have the same status + } + return a.status ? 1 : -1; // Move checked (true) items to the end + }); + + return sortedSettlements.map((settlement, index) => { const expenseName = settlement.expense?.name || "Unknown"; return (
@@ -65,9 +105,14 @@ function FriendDetailPage({ isDarkMode }) { {isFromUser ? "-" : "+"}${Math.abs(settlement.amount).toFixed(2)} - - {settlement.status ? "Settled" : ""} - +
+ handleSettlementChange(e, settlement._id)} + /> +
); }); From 6c07c33bb4fcef3d4c4cda97d06c225049341b44 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Mon, 4 Dec 2023 09:56:03 -0500 Subject: [PATCH 32/60] Update for settle all functionality Settle all is implemented as a button. If clicked, all settlement with the user will be set as settled. This action cannot undo. --- back-end/routes/expenseStatusRoute.js | 65 ++++++++++---------- front-end/src/components/FriendDetailPage.js | 57 ++++++++++++++--- front-end/src/styles/FriendDetailPage.css | 14 ++++- 3 files changed, 91 insertions(+), 45 deletions(-) diff --git a/back-end/routes/expenseStatusRoute.js b/back-end/routes/expenseStatusRoute.js index bf8697f..dbcd11b 100644 --- a/back-end/routes/expenseStatusRoute.js +++ b/back-end/routes/expenseStatusRoute.js @@ -3,44 +3,45 @@ const router = express.Router(); const { body, validationResult } = require("express-validator"); const { Settlement } = require("../models/Settlement.js"); -router.post( - "/:settlementId", - - async (req, res) => { - /* - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - */ - - try { - const settlementId = req.params.settlementId; - const status = req.body.status; - console.log(status); +router.post("/:settlementId", async (req, res) => { + try { + const status = req.body.status; + const settlementId = req.params.settlementId; - const updatedSettlement = await Settlement.findByIdAndUpdate( - settlementId, - { status }, - { new: true } + if (settlementId === "all") { + const settlementIds = req.body.settlementIds; + const result = await Settlement.updateMany( + { _id: { $in: settlementIds } }, + { status } ); - console.log(updatedSettlement); - - if (!updatedSettlement) { - return res.status(404).json({ message: "Settlement not found" }); + if (result.nModified === 0) { + return res.status(404).json({ message: "No settlements updated" }); } - // Send a success response - res.status(200).json({ - message: "Settlements updated successfully", - data: updatedSettlement, + + return res.status(200).json({ + message: `${result.nModified} settlements updated successfully`, }); - } catch (error) { - // Handle any errors that occur during saving to database - console.error(error); - res.status(500).json({ status: "Error", message: error.message }); } + + const updatedSettlement = await Settlement.findByIdAndUpdate( + settlementId, + { status }, + { new: true } + ); + + if (!updatedSettlement) { + return res.status(404).json({ message: "Settlement not found" }); + } + + res.status(200).json({ + message: "Settlement updated successfully", + data: updatedSettlement, + }); + } catch (error) { + console.error(error); + res.status(500).json({ status: "Error", message: error.message }); } -); +}); module.exports = router; diff --git a/front-end/src/components/FriendDetailPage.js b/front-end/src/components/FriendDetailPage.js index 614362d..933f12a 100644 --- a/front-end/src/components/FriendDetailPage.js +++ b/front-end/src/components/FriendDetailPage.js @@ -87,12 +87,11 @@ function FriendDetailPage({ isDarkMode }) { }; const renderSettlements = (settlementList, isFromUser) => { - // Sort the settlements: unchecked items first, then checked items const sortedSettlements = [...settlementList].sort((a, b) => { if (a.status === b.status) { - return 0; // Keep original order if both have the same status + return 0; } - return a.status ? 1 : -1; // Move checked (true) items to the end + return a.status ? 1 : -1; }); return sortedSettlements.map((settlement, index) => { @@ -118,25 +117,56 @@ function FriendDetailPage({ isDarkMode }) { }); }; - const calculateTotalBalance = () => { - const amountYouOwe = settlements.fromUserToFriend.reduce( + const [amountYouOwe, setAmountYouOwe] = useState(0); + const [amountFriendOwesYou, setAmountFriendOwesYou] = useState(0); + + useEffect(() => { + const calculatedAmountYouOwe = settlements.fromUserToFriend.reduce( (total, settlement) => { - return settlement.status ? total : total + settlement.amount; + return !settlement.status ? total + settlement.amount : total; }, 0 ); - const amountFriendOwesYou = settlements.fromFriendToUser.reduce( + const calculatedAmountFriendOwesYou = settlements.fromFriendToUser.reduce( (total, settlement) => { - return settlement.status ? total : total + settlement.amount; + return !settlement.status ? total + settlement.amount : total; }, 0 ); - return { amountYouOwe, amountFriendOwesYou }; + setAmountYouOwe(calculatedAmountYouOwe); + setAmountFriendOwesYou(calculatedAmountFriendOwesYou); + }, [settlements]); + + const handleSettleAllClick = async () => { + const updateSettlements = (settlements) => + settlements.map((settlement) => ({ ...settlement, status: true })); + + setSettlements({ + fromUserToFriend: updateSettlements(settlements.fromUserToFriend), + fromFriendToUser: updateSettlements(settlements.fromFriendToUser), + }); + + await updateSettlementsInBackend(true); }; - const { amountYouOwe, amountFriendOwesYou } = calculateTotalBalance(); + const updateSettlementsInBackend = async (newStatus) => { + const settlementIds = [ + ...settlements.fromUserToFriend.map((s) => s._id), + ...settlements.fromFriendToUser.map((s) => s._id), + ]; + + try { + await axios.post(`http://localhost:3001/expenseStatus/all`, { + settlementIds, + status: newStatus, + }); + console.log("All settlements updated"); + } catch (error) { + console.error("Error updating all settlements:", error); + } + }; // Check if friend is not null before rendering if (!friend) { @@ -172,6 +202,12 @@ function FriendDetailPage({ isDarkMode }) {
+
+ +
+

Amount You Owe {friend.username}

{renderSettlements(settlements.fromUserToFriend, true)} @@ -180,6 +216,7 @@ function FriendDetailPage({ isDarkMode }) {

Amount {friend.username} Owes You

{renderSettlements(settlements.fromFriendToUser, false)}
+
); diff --git a/front-end/src/styles/FriendDetailPage.css b/front-end/src/styles/FriendDetailPage.css index 15f7e36..15e1089 100644 --- a/front-end/src/styles/FriendDetailPage.css +++ b/front-end/src/styles/FriendDetailPage.css @@ -26,6 +26,17 @@ header { @apply flex justify-between items-center p-4 border-b border-gray-200; } +.settle-all-section { + text-align: right; + padding-right: 20px; +} + +#settle-all-btn { + @apply bg-cyan-100 font-bold py-2 px-4 rounded shadow-md; + @apply focus:outline-none; + @apply transition-colors duration-300 ease-in-out; +} + /* Dark Mode for Friend Detail Page */ .dark-mode #friend-detail-page { @apply bg-gray-800 text-white; /* add dark mode to the outter most background on the page (area around the container) */ @@ -42,6 +53,3 @@ header { .dark-mode .settlements-section { @apply bg-gray-700 mb-4; } - - - From 7bf8045fb043a6f1fe15e7b06e752e0ede752e10 Mon Sep 17 00:00:00 2001 From: Laura Zhao <97476561+HedwigO@users.noreply.github.com> Date: Mon, 4 Dec 2023 11:33:29 -0500 Subject: [PATCH 33/60] Update and rename readme.txt to README.md --- .github/README.md | 31 +++++++++++++++++++++++++++++++ .github/readme.txt | 2 -- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 .github/README.md delete mode 100644 .github/readme.txt diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..7768631 --- /dev/null +++ b/.github/README.md @@ -0,0 +1,31 @@ +# The Group Bill Splitting App + +## Description of Project + +The Group Bill Splitting App is designed to streamline the process of splitting bills among groups of people. In today's social and financial landscape, individuals often find themselves sharing expenses in various settings, such as dining out, traveling, or living together. This application aims to provide an efficient and user-friendly solution for managing shared expenses, ensuring that everyone pays their fair share effortlessly. + +### Product Vision Statement + +In a world where coming together and sharing moments are core to our daily lives - whether it's traveling abroad, dining out with friends, going on work trips with colleagues, or handling routine household bills with family - dealing with shared financial responsibilities should be simple and straightforward. Our vision is to introduce a unified platform where handling and dividing shared expenses is as easy as a single click. We aim for an app that makes group financial transactions clear-cut while prioritizing clarity, adaptability, and user comfort. The Group Bill Splitting App is designed to enable users to easily track, manage, and clear their shared expenses, fostering trust and enhancing the pleasure of shared moments without the monetary hassle. + +## Core Team Members + +- [Allison Ji](https://github.com/Allison67) +- [Joy Chen](https://github.com/joyc7) +- [Cindy Liang](https://github.com/cindyliang01) +- [Laura Zhao](https://github.com/HedwigO) +- [Elaine Zhang](https://github.com/elaineZhang67) + +## Contribution + +Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to this project. + +## Instructions for Building and Testing + +To be updated + +## Additional Links + +- [User Experience Design](UX-DESIGN.md) + +## Notes diff --git a/.github/readme.txt b/.github/readme.txt deleted file mode 100644 index 16908de..0000000 --- a/.github/readme.txt +++ /dev/null @@ -1,2 +0,0 @@ -The files in this directory contain configuration settings for continuous integration using GitHub Actions (https://docs.github.com/en/actions). -Do not modify the given files, although you are welcome to add additional files as needed. From 028fbbc7316ebdbee2feded5bb660df4c574ff68 Mon Sep 17 00:00:00 2001 From: elaineZhang67 <144281338+elaineZhang67@users.noreply.github.com> Date: Mon, 4 Dec 2023 11:49:22 -0500 Subject: [PATCH 34/60] Update eventsRoute.js --- back-end/routes/eventsRoute.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/back-end/routes/eventsRoute.js b/back-end/routes/eventsRoute.js index 2d62da6..324bc85 100644 --- a/back-end/routes/eventsRoute.js +++ b/back-end/routes/eventsRoute.js @@ -92,9 +92,18 @@ router.get("/for/:userId", async (req, res) => { const userId = req.params.userId; // Fetch the user and populate the events - const userWithEvents = await User.findById(userId).populate({ + const userWithEvents = await User.findById(userId) + .populate({ path: "events", model: "Event", + populate:{ + path: "expenses", + model: "Expense", + populate: { + path: "splitDetails.settlement", + model: "Settlement", + } + } }); if (!userWithEvents) { From 320fff8b784e5f78bc5ae989180af0f56d89b676 Mon Sep 17 00:00:00 2001 From: HedwigO Date: Mon, 4 Dec 2023 13:49:58 -0500 Subject: [PATCH 35/60] link reset pw in account page to reset pw page --- front-end/src/components/UserInfo.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/front-end/src/components/UserInfo.jsx b/front-end/src/components/UserInfo.jsx index 9cf88fa..d59431f 100644 --- a/front-end/src/components/UserInfo.jsx +++ b/front-end/src/components/UserInfo.jsx @@ -19,6 +19,10 @@ function UserInfo({ isDarkMode, toggleDarkMode }) { setMessage(""); }; + const redirectToForgotPassword = () => { + navigate('/forgot-password'); + }; + const handleLogout = () => { localStorage.removeItem("token"); navigate("/"); @@ -106,7 +110,7 @@ function UserInfo({ isDarkMode, toggleDarkMode }) { -
  • Password
  • +
  • Reset Password
  • From 427c09ae4286682a6c56a9b17c49bbec83418019 Mon Sep 17 00:00:00 2001 From: HedwigO Date: Mon, 4 Dec 2023 13:55:13 -0500 Subject: [PATCH 36/60] change naming for forgot pw to reset pw --- front-end/src/components/ForgotPassword.jsx | 4 ++-- front-end/src/components/Login.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/front-end/src/components/ForgotPassword.jsx b/front-end/src/components/ForgotPassword.jsx index 7cba671..9488da3 100644 --- a/front-end/src/components/ForgotPassword.jsx +++ b/front-end/src/components/ForgotPassword.jsx @@ -52,7 +52,7 @@ const ForgotPassword = () => { return (
    -

    Forgot Password

    +

    Reset Password

    From 36836aec01b7ac7deb299628e1eda82fd301d1e0 Mon Sep 17 00:00:00 2001 From: elaineZhang67 <144281338+elaineZhang67@users.noreply.github.com> Date: Mon, 4 Dec 2023 16:33:30 -0500 Subject: [PATCH 37/60] change the styling of balance shown --- front-end/src/components/Event.js | 3 ++- front-end/src/components/Expense.jsx | 7 +++---- front-end/src/styles/Event.css | 8 ++++++-- front-end/src/styles/Expense.css | 8 ++++++++ 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/front-end/src/components/Event.js b/front-end/src/components/Event.js index b186e3a..567b86d 100644 --- a/front-end/src/components/Event.js +++ b/front-end/src/components/Event.js @@ -186,7 +186,8 @@ const Event = (props) => {
    {item.expense.name}
    -
    ${item.settlement.toFixed(2)}
    +
    {item.settlement.toFixed(2) === "0.00" ? + Settled : `$${item.settlement.toFixed(2)}`}
    ))} diff --git a/front-end/src/components/Expense.jsx b/front-end/src/components/Expense.jsx index 61a7aae..15c31d9 100644 --- a/front-end/src/components/Expense.jsx +++ b/front-end/src/components/Expense.jsx @@ -20,7 +20,6 @@ function Expense({ isDarkMode }) { const processedData = processExpenses(response.data, currentuserId); setExpensesData(response.data); setFilteredData(processedData); - console.log(":", processedData) }catch(error){ console.error("There was an error fetching the data:", error); } @@ -69,9 +68,9 @@ function Expense({ isDarkMode }) { let filteredExpenses =[]; if(expensesData.paidBy._id === userId){ - filteredExpenses = expensesData.splitDetails.filter(split => split.user._id !== userId).map(split => ({ ...split, displayName: split.user.username })); + filteredExpenses = expensesData.splitDetails.filter(split => split.user._id !== userId).map(split => ({ ...split, displayName: split.user.username, amount: split.settlement.amount})); }else{ - filteredExpenses = expensesData.splitDetails.filter(split => split.user._id === userId && expensesData.paidBy !== userId).map(split => ({ ...split, displayName: expensesData.paidBy.username }));; + filteredExpenses = expensesData.splitDetails.filter(split => split.user._id === userId && expensesData.paidBy !== userId).map(split => ({ ...split, displayName: expensesData.paidBy.username, amount: -split.settlement.amount}));; } return filteredExpenses; } @@ -106,7 +105,7 @@ function Expense({ isDarkMode }) { .map(split => (
    {split.displayName} - 0 ? 'positive' : 'negative'}>{split.settlement.amount.toFixed(2)} + 0 ? 'positive' : 'negative'}>{split.amount.toFixed(2)}
    Date: Mon, 4 Dec 2023 19:37:03 -0500 Subject: [PATCH 38/60] display summary of event and expense --- front-end/src/components/Event.js | 50 +++-- front-end/src/components/Expense.jsx | 282 ++++++++++++++++----------- front-end/src/styles/Event.css | 6 +- 3 files changed, 194 insertions(+), 144 deletions(-) diff --git a/front-end/src/components/Event.js b/front-end/src/components/Event.js index b186e3a..a298c72 100644 --- a/front-end/src/components/Event.js +++ b/front-end/src/components/Event.js @@ -17,7 +17,7 @@ const Event = (props) => { const months = [ "Jan", "Feb", - "Mar", + "Mar", "Apr", "May", "Jun", @@ -58,32 +58,24 @@ const Event = (props) => { let userBalance = 0; if (isParticipant) { - // User is a participant, find the settlement from splitDetails - /* - const userSplitDetail = expense.splitDetails.find( - (detail) => detail.user === userId - ); - settlement = userSplitDetail - ? userSplitDetail.settlement - : { amount: 0 }; // Add other fields as needed - */ - if(expense.paidBy === userId){ - expense.splitDetails.forEach(split =>{ - if(!split.settlement.status){ + if (expense.paidBy === userId) { + expense.splitDetails.forEach((split) => { + if (!split.settlement.status) { userBalance += split.settlement.amount; } - }) + }); } else { - const user = expense.splitDetails.find(split => split.user === userId) //user is not the one who paid, find what the user owe to the person who paid - //const paidBy = expense.splitDetails.find(split => split.user === expense.paidBy); - if(user){ + const user = expense.splitDetails.find( + (split) => split.user === userId + ); + if (user) { userBalance = user.settlement.status ? 0 : -user.settlement.amount; } } } else { // User is not a participant, create a settlement object with amount 0 - userBalance = 0 ; + userBalance = 0; } return { @@ -149,8 +141,11 @@ const Event = (props) => { const currentUser = jwtDecode(token); //const currentUserId = currentUser.id; if (data.expenses && currentUser) { - const processedExpenses = processUserExpenses(data.expenses, currentUser.id); - setUserExpenses(processedExpenses) + const processedExpenses = processUserExpenses( + data.expenses, + currentUser.id + ); + setUserExpenses(processedExpenses); } }, [data]); @@ -166,14 +161,15 @@ const Event = (props) => {
    -

    {data.description}

    +

    {data.description}

    -
    - {/* to see the remaining unsettled balance */} - - {/* to see the history of all bill/transaction */} - +
    +
    • Date: {reformatDate(data.date)}
    +
    + • Total{" "} + {data.participants ? data.participants.length : "Loading..."} people +
    @@ -207,4 +203,4 @@ const Event = (props) => { ); }; -export default Event; \ No newline at end of file +export default Event; diff --git a/front-end/src/components/Expense.jsx b/front-end/src/components/Expense.jsx index 340d916..7bff5fe 100644 --- a/front-end/src/components/Expense.jsx +++ b/front-end/src/components/Expense.jsx @@ -1,128 +1,186 @@ /* Expense.jsx - components of Expense Page */ -import React, { useState, useEffect } from 'react'; -import '../styles/Expense.css'; +import React, { useState, useEffect } from "react"; +import "../styles/Expense.css"; import axios from "axios"; import Navbar from "./Navbar"; -import { useNavigate, useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { jwtDecode } from "jwt-decode"; function Expense({ isDarkMode }) { - const [expensesData, setExpensesData] = useState([]); - const [filteredData, setFilteredData] = useState([]); - const navigate = useNavigate(); - const { expenseId } = useParams(); - - const fetchData = async () => { - try { - const response = await axios.get(`http://localhost:3001/expense/ExpenseDetail/${expenseId}`); - console.log("Fetched Data:", response.data); // Debug - const processedData = processExpenses(response.data, currentuserId); - setExpensesData(response.data); - setFilteredData(processedData); - }catch(error){ - console.error("There was an error fetching the data:", error); - } - }; + const [expensesData, setExpensesData] = useState([]); + const [filteredData, setFilteredData] = useState([]); + const navigate = useNavigate(); + const { expenseId } = useParams(); + + function reformatDate(dateStr) { + const months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + const date = new Date(dateStr); + + const monthName = months[date.getMonth()]; + const day = date.getDate(); - const settleExpenses = async(settlementId, newStatus) => { - try{ - const response = await axios.post(`http://localhost:3001/expenseStatus/${settlementId}`, {status: newStatus}); - console.log('Settlements updated:', response.data); - } catch (error) { - console.error('Error updating settlements:', error); - console.log(error.response.data); - } + return `${monthName} ${day}`; + } + + const fetchData = async () => { + try { + const response = await axios.get( + `http://localhost:3001/expense/ExpenseDetail/${expenseId}` + ); + console.log("Fetched Data:", response.data); // Debug + const processedData = processExpenses(response.data, currentuserId); + setExpensesData(response.data); + setFilteredData(processedData); + } catch (error) { + console.error("There was an error fetching the data:", error); } + }; - useEffect(() => { - // Toggle the 'body-dark-mode' class on the body element - if (isDarkMode) { - document.body.classList.add('body-dark-mode'); - } else { - document.body.classList.remove('body-dark-mode'); - } - - // Clean up function to remove the class when the component unmounts or when dark mode is turned off - return () => { - document.body.classList.remove('body-dark-mode'); - }; - }, [isDarkMode]); - - useEffect(() => { - fetchData(); - }, []); - - const token = localStorage.getItem("token") - const currentUser = jwtDecode(token); - const currentuserId = currentUser.id - - const processExpenses = (expensesData, userId) =>{ - const isParticipant = expensesData.splitDetails.some(split => split.user._id === userId); - - if (!isParticipant) { - // If not a participant, return an empty array or another appropriate value - return []; - } - - let filteredExpenses; - - if(expensesData.paidBy === userId){ - filteredExpenses = expensesData.splitDetails.filter(split => split.user._id !== userId) - }else{ - filteredExpenses = expensesData.splitDetails.filter(split => split.user._id === userId && expensesData.paidBy !== userId); - } - return filteredExpenses; + const settleExpenses = async (settlementId, newStatus) => { + try { + const response = await axios.post( + `http://localhost:3001/expenseStatus/${settlementId}`, + { status: newStatus } + ); + console.log("Settlements updated:", response.data); + } catch (error) { + console.error("Error updating settlements:", error); + console.log(error.response.data); } - - {/* navigates to the previous page */} - const handleTitleClick = () => { - navigate(-1); - }; - - const handleSettlementChange = async (e, settlementId) => { - const newStatus = e.target.checked; - await settleExpenses(settlementId, newStatus); - fetchData(); + }; + + useEffect(() => { + // Toggle the 'body-dark-mode' class on the body element + if (isDarkMode) { + document.body.classList.add("body-dark-mode"); + } else { + document.body.classList.remove("body-dark-mode"); + } + + // Clean up function to remove the class when the component unmounts or when dark mode is turned off + return () => { + document.body.classList.remove("body-dark-mode"); }; + }, [isDarkMode]); - return ( -
    -

    - {expensesData.event ? expensesData.event.name : 'Loading...'}| - {expensesData.name ? expensesData.name : 'Loading...'} -

    -
    - - -
    -
    - - {expensesData && ( -
    - {filteredData && filteredData - .sort((a, b) => a.settlement.status === b.settlement.status ? 0 : a.settlement.status ? 1 : -1) - .map(split => ( -
    - {split.user?.username || 'Unknown User'} - 0 ? 'positive' : 'negative'}>{split.settlement.amount.toFixed(2)} -
    - handleSettlementChange(e, split.settlement._id)} - defaultChecked={split.settlement.status} - /> -
    -
    - ))} -
    - )} -
    - -
    + useEffect(() => { + fetchData(); + }, []); + + const token = localStorage.getItem("token"); + const currentUser = jwtDecode(token); + const currentuserId = currentUser.id; + + const processExpenses = (expensesData, userId) => { + const isParticipant = expensesData.splitDetails.some( + (split) => split.user._id === userId ); + + if (!isParticipant) { + // If not a participant, return an empty array or another appropriate value + return []; + } + + let filteredExpenses; + + if (expensesData.paidBy === userId) { + filteredExpenses = expensesData.splitDetails.filter( + (split) => split.user._id !== userId + ); + } else { + filteredExpenses = expensesData.splitDetails.filter( + (split) => split.user._id === userId && expensesData.paidBy !== userId + ); + } + return filteredExpenses; + }; + + { + /* navigates to the previous page */ + } + const handleTitleClick = () => { + navigate(-1); + }; + + const handleSettlementChange = async (e, settlementId) => { + const newStatus = e.target.checked; + await settleExpenses(settlementId, newStatus); + fetchData(); + }; + + return ( +
    +

    + {expensesData.event ? expensesData.event.name : "Loading..."}| + {expensesData.name ? expensesData.name : "Loading..."} +

    +
    +
    • Date: {reformatDate(expensesData.date)}
    +
    + • Total{" "} + {expensesData.splitDetails + ? expensesData.splitDetails.length + : "Loading..."}{" "} + people +
    +
    +
    + {expensesData && ( +
    + {filteredData && + filteredData + .sort((a, b) => + a.settlement.status === b.settlement.status + ? 0 + : a.settlement.status + ? 1 + : -1 + ) + .map((split) => ( +
    + {split.user?.username || "Unknown User"} + 0 + ? "positive" + : "negative" + } + > + {split.settlement.amount.toFixed(2)} + +
    + + handleSettlementChange(e, split.settlement._id) + } + defaultChecked={split.settlement.status} + /> +
    +
    + ))} +
    + )} +
    + +
    + ); } -export default Expense; \ No newline at end of file +export default Expense; diff --git a/front-end/src/styles/Event.css b/front-end/src/styles/Event.css index d1a7585..7f5dbaa 100644 --- a/front-end/src/styles/Event.css +++ b/front-end/src/styles/Event.css @@ -6,14 +6,10 @@ header { @apply h-20 w-full text-center p-4 mt-3 mb-6 rounded-lg; } -.operations { +.summary { @apply flex space-x-4 place-content-center mb-6; } -.operations button { - @apply w-24 h-8 rounded-full justify-items-center bg-cyan-50 hover:bg-orange-100 active:bg-orange-200; -} - .expenses { @apply overflow-auto mb-4 p-2 rounded-lg bg-cyan-100 bg-opacity-50; } From a0d53cdc0a8a3307176bd4507eaad9d1f5fcab34 Mon Sep 17 00:00:00 2001 From: Allison67 Date: Mon, 4 Dec 2023 19:52:33 -0500 Subject: [PATCH 39/60] Update Event.js --- front-end/src/components/Event.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/front-end/src/components/Event.js b/front-end/src/components/Event.js index a298c72..9d50e7c 100644 --- a/front-end/src/components/Event.js +++ b/front-end/src/components/Event.js @@ -10,8 +10,6 @@ const Event = (props) => { const [userExpenses, setUserExpenses] = useState([]); const isDarkMode = props.isDarkMode; const { eventId } = useParams(); - //const [balance, setBalance] = useState(0); - console.log("Event ID:", eventId); // check the eventID received function reformatDate(dateStr) { const months = [ From 2b5e4b62e84ec809414cb1e28d5e81fefadd9b30 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Mon, 4 Dec 2023 20:36:00 -0500 Subject: [PATCH 40/60] Update friend summary in home page --- front-end/src/components/Home.jsx | 194 ++++++++++++++++-------------- 1 file changed, 102 insertions(+), 92 deletions(-) diff --git a/front-end/src/components/Home.jsx b/front-end/src/components/Home.jsx index b9c99ad..a718b78 100644 --- a/front-end/src/components/Home.jsx +++ b/front-end/src/components/Home.jsx @@ -15,84 +15,6 @@ const Home = ({ isDarkMode }) => { }); const [expenseSummary, setExpenseSummary] = useState([]); - /* backupData from Expenses, Events, Friends.jsx */ - const backupData = { - userName: "Bryn", - totalSpending: 0, - expenses: [ - { - id: 1, - name: "Lunch", - amount: 358, - creator: "Jane", - date: "06/16/2023", - }, - { - id: 2, - name: "Flights to LA", - amount: 261, - creator: "Tom", - date: "01/21/2023", - }, - { - id: 3, - name: "Hotels", - amount: 170, - creator: "David", - date: "08/02/2023", - }, - ], - events: [ - { - id: 1, - EventName: "Cooro", - Date: "06/16/2023", - balance: "$358.00", - description: "Lunch at local restaurant", - members: [{ names: "Jane" }, { names: "John" }], - }, - { - id: 2, - EventName: "Kobe", - Date: "01/21/2023", - balance: "$262.00", - description: "Flight tickets for LA trip", - members: [{ names: "Tom" }, { names: "Lucy" }], - }, - { - id: 3, - EventName: "Cuiji", - Date: "08/02/2023", - balance: "$170.00", - description: "Accommodation expenses", - members: [{ names: "David" }, { names: "Sarah" }], - }, - ], - friends: [ - { - id: 2, - name: "Jdavie", - email: "jzecchinii0@yahoo.co.jp", - phone: "967-156-0272", - balance: "$57.06", - }, - { - id: 4, - name: "Emmie", - email: "esworder1@xinhuanet.com", - phone: "832-141-0597", - balance: "$60.04", - }, - { - id: 5, - name: "Jason", - email: "jsathep@pehisbd.com", - phone: "212-121-0437", - balance: "$70.41", - }, - ], - }; - function reformatDate(dateStr) { const months = [ "Jan", @@ -201,7 +123,6 @@ const Home = ({ isDarkMode }) => { } } catch (error) { console.error("Error fetching data:", error); - setData(backupData); // set to backup data in case of error } }; @@ -221,6 +142,95 @@ const Home = ({ isDarkMode }) => { return total; } + /* For Friends summary */ + const [userData, setUserData] = useState(null); + useEffect(() => { + const fetchData = async () => { + try { + const token = localStorage.getItem("token"); + const currentUser = jwtDecode(token); + const userId = currentUser.id; + const result = await axios.get( + `http://localhost:3001/friends/${userId}` + ); + setUserData(result.data); + } catch (err) { + console.error(err); + } + }; + fetchData(); + }, []); + + const [settlements, setSettlements] = useState([]); + const fetchSettlementsForFriends = async () => { + if (!userData || !userData.friends) return; + + let settlements = []; + for (const friend of userData.friends) { + try { + const fromUserToFriend = await axios.get( + `http://localhost:3001/settlement/from/${userData._id}/to/${friend._id}` + ); + const fromFriendToUser = await axios.get( + `http://localhost:3001/settlement/from/${friend._id}/to/${userData._id}` + ); + + settlements.push({ + friend: friend, + fromUserToFriend: fromUserToFriend.data, + fromFriendToUser: fromFriendToUser.data, + }); + } catch (error) { + console.error( + "Error fetching settlements for friend:", + friend._id, + error + ); + settlements.push({ + friend: friend, + fromUserToFriend: [], + fromFriendToUser: [], + }); + } + } + setSettlements(settlements); + }; + + useEffect(() => { + fetchSettlementsForFriends(); + }, [userData]); + + const calculateBalances = (items) => { + return items.map((item) => { + const balance = + item.fromUserToFriend.reduce( + (acc, settlement) => + acc - (settlement.status === false ? settlement.amount : 0), + 0 + ) + + item.fromFriendToUser.reduce( + (acc, settlement) => + acc + (settlement.status === false ? settlement.amount : 0), + 0 + ); + + const settlementIds = [ + ...item.fromUserToFriend + .filter((settlement) => settlement.status === false) + .map((settlement) => settlement._id), + ...item.fromFriendToUser + .filter((settlement) => settlement.status === false) + .map((settlement) => settlement._id), + ]; + + return { + ...item.friend, + balance: balance, + settlementIds: settlementIds, + }; + }); + }; + /* useEffect for controlling DarkMode of the margin around the page */ useEffect(() => { if (isDarkMode) { @@ -244,7 +254,7 @@ const Home = ({ isDarkMode }) => {

    Welcome, {data.userName}

    You have spent ${data.totalSpending.toFixed(2)} in total! -

    +

    @@ -288,18 +298,18 @@ const Home = ({ isDarkMode }) => {

    Friends Summary

      - {friendsPendingPayment.length > 0 ? ( - friendsPendingPayment.map((friend) => ( -
    • -
      -

      {friend.username}

      -

      {friend.balance}

      -
      -
    • - )) - ) : ( -
      No Friends Added Yet.
      - )} + {calculateBalances(settlements).map((friend) => ( +
    • +
      +

      {friend.username}

      +

      + {friend.balance === 0 + ? "Settled" + : `$${friend.balance.toFixed(2)}`} +

      +
      +
    • + ))}
    View All From b4cf8f5297528c220499c3c4ed6d619851281ff3 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Mon, 4 Dec 2023 20:49:47 -0500 Subject: [PATCH 41/60] Update Home.css Update the style so the page content could be displayed properly --- front-end/src/styles/Home.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/front-end/src/styles/Home.css b/front-end/src/styles/Home.css index 6920972..8655b63 100644 --- a/front-end/src/styles/Home.css +++ b/front-end/src/styles/Home.css @@ -7,12 +7,11 @@ } .dashboard { - @apply flex flex-col justify-center items-center; + @apply flex flex-col justify-center items-center; } .box { @apply p-4 rounded-md bg-cyan-100 my-2 flex flex-col cursor-pointer justify-center; - @apply overflow-auto; width: 100%; height: 170px; /* if the sections exceed the page, reduce this height */ } @@ -26,7 +25,7 @@ } .expenses-list-container { - @apply overflow-auto h-48; + @apply overflow-auto h-48; } @media (min-width: 768px) { @@ -45,6 +44,7 @@ .home-list { @apply rounded-lg mt-1; + @apply overflow-auto; } .home-expense-text, @@ -98,7 +98,7 @@ } .dark-mode .box { - @apply bg-gray-700; + @apply bg-gray-700; } .dark-mode .box h2, @@ -109,4 +109,4 @@ .dark-mode .greeting h1 { @apply text-left; -} \ No newline at end of file +} From 10d1d486b1a6fe8836be020f8d8b65501b7b57e8 Mon Sep 17 00:00:00 2001 From: cindyliang01 Date: Mon, 4 Dec 2023 21:24:39 -0500 Subject: [PATCH 42/60] Alert when invalid email or username are used --- front-end/src/components/ForgotPassword.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/front-end/src/components/ForgotPassword.jsx b/front-end/src/components/ForgotPassword.jsx index 9488da3..d006bd9 100644 --- a/front-end/src/components/ForgotPassword.jsx +++ b/front-end/src/components/ForgotPassword.jsx @@ -44,7 +44,7 @@ const ForgotPassword = () => { navigate("/", { state: { showAlert: true } }); } } catch (error) { - setErrorMessage("something went wrong with forgot password!"); + setErrorMessage("Invalid email or username, please try again!"); } }; @@ -53,6 +53,8 @@ const ForgotPassword = () => {

    Reset Password

    + {errorMessage ?

    {errorMessage}

    : ""} +
    - - {expensesData && ( + {expensesData && (
    - {filteredData && filteredData - .sort((a, b) => a.settlement.status === b.settlement.status ? 0 : a.settlement.status ? 1 : -1) - .map(split => ( + {filteredData && + filteredData + .sort((a, b) => + a.settlement.status === b.settlement.status + ? 0 + : a.settlement.status + ? 1 + : -1 + ) + .map((split) => (
    - {split.displayName} - 0 ? 'positive' : 'negative'}>{split.settlement.amount.toFixed(2)} -
    - handleSettlementChange(e, split.settlement._id)} - defaultChecked={split.settlement.status} - /> -
    + {split.displayName} + 0 + ? "positive" + : "negative" + } + > + {split.settlement.amount.toFixed(2)} + +
    + + handleSettlementChange(e, split.settlement._id) + } + defaultChecked={split.settlement.status} + /> +
    - ))} + ))}
    - )} -
    + )} +
    ); From 48765134713875468e83a700a081d44d282cce32 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Mon, 4 Dec 2023 23:46:33 -0500 Subject: [PATCH 44/60] Update AddExpense.js Add select all button --- front-end/src/components/AddExpense.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/front-end/src/components/AddExpense.js b/front-end/src/components/AddExpense.js index 8863634..3678c8d 100644 --- a/front-end/src/components/AddExpense.js +++ b/front-end/src/components/AddExpense.js @@ -314,6 +314,15 @@ const AddExpense = (props) => { setAmountError("Please enter a valid amount."); } }; + /* Click Handler Function for button Select All */ + const handleSelectAll = () => { + setSelectedPeople([...selectedPeople, ...availablePeople]); + setAvailablePeople([]); + setValidationMessages((prevMessages) => ({ + ...prevMessages, + selectedPeople: "", + })); + }; return (
    @@ -414,6 +423,7 @@ const AddExpense = (props) => { ))} +
    From 5745f618b4d224bec47134e49d8d5fa48126309e Mon Sep 17 00:00:00 2001 From: elaineZhang67 <144281338+elaineZhang67@users.noreply.github.com> Date: Mon, 4 Dec 2023 23:47:26 -0500 Subject: [PATCH 45/60] add the total balance section and search function --- back-end/routes/settlementRoute.js | 15 ++- front-end/src/components/Events.jsx | 187 ++++++++++------------------ 2 files changed, 80 insertions(+), 122 deletions(-) diff --git a/back-end/routes/settlementRoute.js b/back-end/routes/settlementRoute.js index 97b9c32..dfef9d4 100644 --- a/back-end/routes/settlementRoute.js +++ b/back-end/routes/settlementRoute.js @@ -1,17 +1,30 @@ const express = require("express"); const router = express.Router(); const { Settlement } = require("../models/Settlement.js"); +const { Expense } = require("../models/Expense.js") // Route to get settlements for a specific user as 'settleFrom' router.get("/from/:userId", async (req, res) => { try { const userId = req.params.userId; - const settlements = await Settlement.find({ settleFrom: userId }) + const settlements = await Settlement.find({ + $or: [{ settleFrom: userId }, { settleTo: userId }] + }) .populate("settleTo") + .populate("settleFrom") + .populate({ + path: "expense", + model: "Expense", + populate: { + path: "paidBy", + model: "User" + } + }) .populate("event"); res.status(200).json(settlements); + console.log(settlements) } catch (error) { res .status(500) diff --git a/front-end/src/components/Events.jsx b/front-end/src/components/Events.jsx index 9b17c2a..b19a258 100644 --- a/front-end/src/components/Events.jsx +++ b/front-end/src/components/Events.jsx @@ -10,9 +10,13 @@ import { jwtDecode } from "jwt-decode"; function Events({ isDarkMode }) { const[eventData, setEventData] = useState([]) const[addEvent, setaddEvent] = useState(false) - const[showFilter, setShowFilter] = useState(false); - const[selectedFilter, setSelectedFilter] = useState('all'); - const[filteredEvents, setFilteredEvents] = useState([]); + const [settlements, setSettlements] = useState([]); + const [amountOwed, setAmountOwed] = useState(0); + const [amountOwedBy, setAmountOwedBy] = useState(0); + const [searchTerm, setSearchTerm] = useState(""); + //const[showFilter, setShowFilter] = useState(false); + //const[selectedFilter, setSelectedFilter] = useState('all'); + //const[filteredEvents, setFilteredEvents] = useState([]); function reformatDate(dateStr) { const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; @@ -39,13 +43,14 @@ function Events({ isDarkMode }) { }; }, [isDarkMode]); + const token = localStorage.getItem("token"); + const decode = jwtDecode(token); + useEffect(()=>{ //fetch mock data about a user's events list async function dataFetch(){ try{ - const token = localStorage.getItem("token"); - const decode = jwtDecode(token); - if (!decode || !decode.id) { + if (!decode.id) { console.error("No current user found in local storage."); return; } else { @@ -63,79 +68,57 @@ function Events({ isDarkMode }) { } dataFetch(); },[]); + + useEffect(() => { + console.log(decode.id) + const fetchSettlements = async () => { + try { + const response = await axios.get(`http://localhost:3001/settlement/from/${decode.id}`); + console.log("Settlements:", response.data); + setSettlements(response.data); + // Process and use the fetched settlements here + } catch (error) { + console.error("Error fetching settlements:", error.response); + } + }; + + fetchSettlements(); + + }, []); - /* - let clearedEvents = []; - let otherEvents = []; - if (eventData.events && eventData.events.length){ - eventData.events.forEach(event => { - const eventBalance = parseFloat(event.balance.replace('$', '')) - if(eventBalance === 0){ - clearedEvents.push(event); - }else{ - otherEvents.push(event); + function calculateAmounts(expenses, currentUserId) { + let amountOwed = 0; // Amount the current user owes to others + let amountOwedBy = 0; // Amount owed to the current user by others + + expenses.forEach(expense => { + if (expense.settleTo._id !== currentUserId && !expense.status) { + // If the current user is not the one who paid and the status is false (unsettled) + amountOwed += expense.amount; + } + if (expense.settleTo._id === currentUserId && !expense.status) { + // If the current user is the one who paid and the status is false (unsettled) + amountOwedBy += expense.amount; } }); + + return { amountOwed, amountOwedBy }; } useEffect(() => { - let filtered = []; - if (eventData.events) { - switch (selectedFilter) { - case 'owe': - filtered = eventData.events.filter(event => parseFloat(event.balance.replace('$', '')) < 0); - break; - case 'owed': - filtered = eventData.events.filter(event => parseFloat(event.balance.replace('$', '')) > 0); - break; - case 'cleared': - filtered = eventData.events.filter(event => parseFloat(event.balance.replace('$', '')) === 0); - break; - case 'all': - default: - filtered = eventData.events; - break; - } + if (settlements.length > 0) { + const { amountOwed, amountOwedBy } = calculateAmounts(settlements, decode.id); + setAmountOwed(amountOwed); + setAmountOwedBy(amountOwedBy); + // Now you can use amountOwed and amountOwedBy in your component + console.log(`Amount Owed: ${amountOwed}, Amount Owed By: ${amountOwedBy}`); } - setFilteredEvents(filtered); - }, [selectedFilter, eventData.events]); - - const handleFilterChange = (newFilter) => { - setSelectedFilter(newFilter.target.value); - setShowFilter(false); // Hide filter options - }; + }, [settlements, decode.id]); - const handleDropdownClick = (event) =>{ - event.stopPropagation(); - } - - const totalOwed = eventData && eventData.events && eventData.events.length - ? eventData.events.reduce((acc, event) => { - const balance = parseFloat(event.balance.replace('$', '')); - return balance < 0 ? acc + balance : acc; - }, 0) - : 0; - - const totalOwedToYou = eventData && eventData.events && eventData.events.length - ? eventData.events.reduce((acc, event) => { - const balance = parseFloat(event.balance.replace('$', '')); - return balance > 0 ? acc + balance : acc; - }, 0) - : 0; - - //const totalBalance = eventData && eventData.events && eventData.events.length ? eventData.events.reduce((acc, event) => acc + parseFloat(event.balance.replace('$', '')), 0): 0; - - let sortedEvents = []; - if (eventData.events && eventData.events.length) { - sortedEvents = eventData.events.sort((a, b) => new Date(b.date) - new Date(a.date)); - } - - */ - function EventClick(eventId){ console.log(`Event ${eventId} was clicked`) } - + + const filteredEvents = eventData.events.filter(event => event.name.toLowerCase().includes(searchTerm.toLowerCase())); return(
    @@ -144,74 +127,36 @@ function Events({ isDarkMode }) { Add Events -{/* total balance Section + Filter Icon -
    User's Avatar
    -
    Total Balance
    - {totalOwed < 0 && ( -
    You owe ${Math.abs(totalOwed).toFixed(2)}
    + { ( +
    You owe ${Math.abs(amountOwed).toFixed(2)}
    )} - {totalOwedToYou > 0 && ( -
    You are owed ${totalOwedToYou.toFixed(2)}
    + {( +
    You are owed ${amountOwedBy.toFixed(2)}
    )} - {totalOwed === 0 && totalOwedToYou === 0 && ( + {amountOwed === 0 && amountOwedBy === 0 && (
    All Balances are Settled!
    )}
    -
    setShowFilter(!showFilter)}> - EventsList - {showFilter && ( - - )} -
    - - */} - -{/* -
    -
      - {filteredEvents.map(event =>( -
    • -
      - {reformatDate(event.date)} -
      -
      - {event.name} -
      - - - -
    • - ))} -
    -
    - - */} + setSearchTerm(e.target.value)} + className="mt-4 search-input" + />
      - {eventData.events && eventData.events.length > 0 ? (eventData.events.map(event =>( + {eventData.events && eventData.events.length > 0 ? (eventData.events + .filter(event => event.name.toLowerCase().includes(searchTerm.toLowerCase())) + .map(event => (
    • {reformatDate(event.date)} From c969a7f659169e79f6fe54fb24e5ad7df7aa439d Mon Sep 17 00:00:00 2001 From: elaineZhang67 <144281338+elaineZhang67@users.noreply.github.com> Date: Mon, 4 Dec 2023 23:55:10 -0500 Subject: [PATCH 46/60] Update Events.jsx --- front-end/src/components/Events.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/front-end/src/components/Events.jsx b/front-end/src/components/Events.jsx index b19a258..8df6caf 100644 --- a/front-end/src/components/Events.jsx +++ b/front-end/src/components/Events.jsx @@ -118,8 +118,6 @@ function Events({ isDarkMode }) { console.log(`Event ${eventId} was clicked`) } - const filteredEvents = eventData.events.filter(event => event.name.toLowerCase().includes(searchTerm.toLowerCase())); - return(

      Events

      From 2f0baf24e1094c2b4a68664850a62abb93c4c643 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Tue, 5 Dec 2023 00:15:48 -0500 Subject: [PATCH 47/60] Debug --- front-end/src/components/AddExpense.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/front-end/src/components/AddExpense.js b/front-end/src/components/AddExpense.js index 3678c8d..c26ee33 100644 --- a/front-end/src/components/AddExpense.js +++ b/front-end/src/components/AddExpense.js @@ -423,7 +423,9 @@ const AddExpense = (props) => { ))} - +
      From 5339db61af8266e4ef4a4791fc893fa31664db54 Mon Sep 17 00:00:00 2001 From: elaineZhang67 <144281338+elaineZhang67@users.noreply.github.com> Date: Tue, 5 Dec 2023 00:53:11 -0500 Subject: [PATCH 48/60] change the styling --- front-end/src/components/Events.jsx | 3 ++- front-end/src/styles/Events.css | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/front-end/src/components/Events.jsx b/front-end/src/components/Events.jsx index 8df6caf..d728b23 100644 --- a/front-end/src/components/Events.jsx +++ b/front-end/src/components/Events.jsx @@ -128,6 +128,7 @@ function Events({ isDarkMode }) {
      User's Avatar
      +
      Total Balance
      { (
      You owe ${Math.abs(amountOwed).toFixed(2)}
      @@ -149,7 +150,7 @@ function Events({ isDarkMode }) { onChange={(e) => setSearchTerm(e.target.value)} className="mt-4 search-input" /> - +
        {eventData.events && eventData.events.length > 0 ? (eventData.events diff --git a/front-end/src/styles/Events.css b/front-end/src/styles/Events.css index 9ac3d7e..a3717d4 100644 --- a/front-end/src/styles/Events.css +++ b/front-end/src/styles/Events.css @@ -9,7 +9,7 @@ .add_events_button { @apply w-32 h-10 rounded-lg bg-cyan-100 hover:bg-orange-100 active:bg-orange-200; - @apply absolute top-2.5 right-2.5 p-1 px-2.5 border-none rounded-sm cursor-pointer; + @apply absolute mt-4 mr-3 top-5 right-5 p-1 px-2.5 border-none rounded-lg cursor-pointer; } .add-event-content { From 9f1e0807de410730d3bb490abca758f38ec86d04 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Tue, 5 Dec 2023 01:00:47 -0500 Subject: [PATCH 49/60] Update AddExpense.js --- front-end/src/components/AddExpense.js | 30 +++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/front-end/src/components/AddExpense.js b/front-end/src/components/AddExpense.js index c26ee33..a85830c 100644 --- a/front-end/src/components/AddExpense.js +++ b/front-end/src/components/AddExpense.js @@ -101,6 +101,23 @@ const AddExpense = (props) => { const handleAmountsChange = (amounts) => { setIndividualAmounts(amounts); + + // Check all amounts are valid now + let allAmountsValid = true; + for (const amount of Object.values(amounts)) { + if (typeof amount !== "number" || isNaN(amount) || amount <= 0) { + allAmountsValid = false; + break; + } + } + + // Clear the error message if all amounts are valid + if (allAmountsValid) { + setValidationMessages((prevMessages) => ({ + ...prevMessages, + individualAmounts: "", + })); + } }; const handleAddExpense = async () => { @@ -148,11 +165,14 @@ const AddExpense = (props) => { const amountNumber = parseFloat(formData.amount); + let allAmountsValid = true; + const peopleSplit = Array.isArray(selectedPeople) ? selectedPeople.map((person) => { const amount = individualAmounts[person._id]; - if (typeof amount !== "number" || isNaN(amount)) { - invalidAmounts = true; + if (typeof amount !== "number" || isNaN(amount) || amount <= 0) { + invalidAmounts = true; // Found an invalid amount + allAmountsValid = false; // Set the flag to false as we found an invalid amount } return { user: person, @@ -164,9 +184,13 @@ const AddExpense = (props) => { if (invalidAmounts) { newValidationMessages.individualAmounts = "Invalid amount(s) in split details"; - isValid = false; + } else { + // If all amounts are valid, clear the error message + newValidationMessages.individualAmounts = ""; } + isValid = !invalidAmounts; + setValidationMessages(newValidationMessages); if (!isValid) { From fbc6a7d839d4ceae155f991b3475581bbeae5917 Mon Sep 17 00:00:00 2001 From: cindyliang01 Date: Tue, 5 Dec 2023 10:15:28 -0500 Subject: [PATCH 50/60] Protect pages --- front-end/src/components/Events.jsx | 395 +++++++++++++---------- front-end/src/components/FriendsPage.jsx | 23 ++ front-end/src/components/Home.jsx | 41 ++- front-end/src/components/Navbar.jsx | 8 +- front-end/src/styles/Events.css | 99 +++--- front-end/src/styles/FriendsPage.css | 4 + front-end/src/styles/Home.css | 4 + 7 files changed, 355 insertions(+), 219 deletions(-) diff --git a/front-end/src/components/Events.jsx b/front-end/src/components/Events.jsx index d728b23..7810322 100644 --- a/front-end/src/components/Events.jsx +++ b/front-end/src/components/Events.jsx @@ -1,188 +1,255 @@ import React, { useState, useEffect } from "react"; -import '../styles/Events.css'; +import "../styles/Events.css"; import Navbar from "./Navbar"; import { Link } from "react-router-dom"; import axios from "axios"; import AddEvent from "./AddEvent"; -import EventsFilter from "../images/filter.png"; +import EventsFilter from "../images/filter.png"; import { jwtDecode } from "jwt-decode"; +import { useNavigate } from "react-router-dom"; function Events({ isDarkMode }) { - const[eventData, setEventData] = useState([]) - const[addEvent, setaddEvent] = useState(false) - const [settlements, setSettlements] = useState([]); - const [amountOwed, setAmountOwed] = useState(0); - const [amountOwedBy, setAmountOwedBy] = useState(0); - const [searchTerm, setSearchTerm] = useState(""); - //const[showFilter, setShowFilter] = useState(false); - //const[selectedFilter, setSelectedFilter] = useState('all'); - //const[filteredEvents, setFilteredEvents] = useState([]); - - function reformatDate(dateStr) { - const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - const date = new Date(dateStr); - - const monthName = months[date.getMonth()]; - const day = date.getDate(); - const year = date.getFullYear(); - - return `${monthName} ${day} ${year}`; + const [eventData, setEventData] = useState([]); + const [addEvent, setaddEvent] = useState(false); + const [settlements, setSettlements] = useState([]); + const [amountOwed, setAmountOwed] = useState(0); + const [amountOwedBy, setAmountOwedBy] = useState(0); + const [searchTerm, setSearchTerm] = useState(""); + + //const[showFilter, setShowFilter] = useState(false); + //const[selectedFilter, setSelectedFilter] = useState('all'); + //const[filteredEvents, setFilteredEvents] = useState([]); + + const [isLoggedIn, setIsLoggedIn] = useState(true); + const navigate = useNavigate(); + const [decode, setDecode] = useState(0); // Declare decode at the top level + + const handleButtonClick = () => { + navigate("/"); + }; + + function reformatDate(dateStr) { + const months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + const date = new Date(dateStr); + + const monthName = months[date.getMonth()]; + const day = date.getDate(); + const year = date.getFullYear(); + + return `${monthName} ${day} ${year}`; + } + + // Toggle the 'body-dark-mode' class on the body element + useEffect(() => { + if (isDarkMode) { + document.body.classList.add("body-dark-mode"); + } else { + document.body.classList.remove("body-dark-mode"); } - // Toggle the 'body-dark-mode' class on the body element - useEffect(() => { - if (isDarkMode) { - document.body.classList.add('body-dark-mode'); - } else { - document.body.classList.remove('body-dark-mode'); - } - - // Clean up function to remove the class when the component unmounts or when dark mode is turned off - return () => { - document.body.classList.remove('body-dark-mode'); - }; - }, [isDarkMode]); - - const token = localStorage.getItem("token"); - const decode = jwtDecode(token); - - useEffect(()=>{ - //fetch mock data about a user's events list - async function dataFetch(){ - try{ - if (!decode.id) { - console.error("No current user found in local storage."); - return; - } else { - console.log(decode.id); - } - //requesting data from the mock API endpoint - const response = await axios.get(`http://localhost:3001/events/for/${decode.id}`); - console.log(response) - //return the data - setEventData(response.data) - - }catch(error){ - console.error("There was an error fetching the data:", error); - } + // Clean up function to remove the class when the component unmounts or when dark mode is turned off + return () => { + document.body.classList.remove("body-dark-mode"); + }; + }, [isDarkMode]); + + useEffect(() => { + async function check() { + try { + const token = localStorage.getItem("token"); + if (!token) { + console.error("No token found"); + console.error("Please log in to view pages"); + setIsLoggedIn(false); + return; } - dataFetch(); - },[]); - - useEffect(() => { - console.log(decode.id) - const fetchSettlements = async () => { - try { - const response = await axios.get(`http://localhost:3001/settlement/from/${decode.id}`); - console.log("Settlements:", response.data); - setSettlements(response.data); - // Process and use the fetched settlements here - } catch (error) { - console.error("Error fetching settlements:", error.response); - } - }; - - fetchSettlements(); - - }, []); - - function calculateAmounts(expenses, currentUserId) { - let amountOwed = 0; // Amount the current user owes to others - let amountOwedBy = 0; // Amount owed to the current user by others - - expenses.forEach(expense => { - if (expense.settleTo._id !== currentUserId && !expense.status) { - // If the current user is not the one who paid and the status is false (unsettled) - amountOwed += expense.amount; - } - if (expense.settleTo._id === currentUserId && !expense.status) { - // If the current user is the one who paid and the status is false (unsettled) - amountOwedBy += expense.amount; - } - }); - - return { amountOwed, amountOwedBy }; + const decodedToken = jwtDecode(token); + setDecode(decodedToken); + } catch (error) { + console.error("There was an error fetching the data:", error); + } } + check(); + }, []); - useEffect(() => { - if (settlements.length > 0) { - const { amountOwed, amountOwedBy } = calculateAmounts(settlements, decode.id); - setAmountOwed(amountOwed); - setAmountOwedBy(amountOwedBy); - // Now you can use amountOwed and amountOwedBy in your component - console.log(`Amount Owed: ${amountOwed}, Amount Owed By: ${amountOwedBy}`); + useEffect(() => { + //fetch mock data about a user's events list + async function dataFetch() { + try { + if (!decode.id) { + console.error("No current user found in local storage."); + return; + } else { + console.log(decode.id); } - }, [settlements, decode.id]); + //requesting data from the mock API endpoint + const response = await axios.get( + `http://localhost:3001/events/for/${decode.id}` + ); + console.log(response); + //return the data + setEventData(response.data); + } catch (error) { + console.error("There was an error fetching the data:", error); + } + } + dataFetch(); + }, []); - function EventClick(eventId){ - console.log(`Event ${eventId} was clicked`) + useEffect(() => { + console.log(decode.id); + const fetchSettlements = async () => { + try { + const response = await axios.get( + `http://localhost:3001/settlement/from/${decode.id}` + ); + console.log("Settlements:", response.data); + setSettlements(response.data); + // Process and use the fetched settlements here + } catch (error) { + console.error("Error fetching settlements:", error.response); + } + }; + + fetchSettlements(); + }, []); + + function calculateAmounts(expenses, currentUserId) { + let amountOwed = 0; // Amount the current user owes to others + let amountOwedBy = 0; // Amount owed to the current user by others + + expenses.forEach((expense) => { + if (expense.settleTo._id !== currentUserId && !expense.status) { + // If the current user is not the one who paid and the status is false (unsettled) + amountOwed += expense.amount; + } + if (expense.settleTo._id === currentUserId && !expense.status) { + // If the current user is the one who paid and the status is false (unsettled) + amountOwedBy += expense.amount; + } + }); + + return { amountOwed, amountOwedBy }; + } + + useEffect(() => { + if (settlements.length > 0) { + const { amountOwed, amountOwedBy } = calculateAmounts( + settlements, + decode.id + ); + setAmountOwed(amountOwed); + setAmountOwedBy(amountOwedBy); + // Now you can use amountOwed and amountOwedBy in your component + console.log( + `Amount Owed: ${amountOwed}, Amount Owed By: ${amountOwedBy}` + ); } + }, [settlements, decode.id]); - return( -
        -

        Events

        - - -
        - User's Avatar -
        -
        Total Balance
        -
        - { ( -
        You owe ${Math.abs(amountOwed).toFixed(2)}
        - )} - {( -
        You are owed ${amountOwedBy.toFixed(2)}
        - )} - {amountOwed === 0 && amountOwedBy === 0 && ( -
        All Balances are Settled!
        - )} -
        -
        -
        + function EventClick(eventId) { + console.log(`Event ${eventId} was clicked`); + } - setSearchTerm(e.target.value)} - className="mt-4 search-input" - /> - -
        -
          - {eventData.events && eventData.events.length > 0 ? (eventData.events - .filter(event => event.name.toLowerCase().includes(searchTerm.toLowerCase())) - .map(event => ( -
        • -
          - {reformatDate(event.date)} -
          -
          - {event.name} -
          - - - -
        • - )) - ) : ( -
          Please add your first event!
          - )} -
        -
        + if (!isLoggedIn) + return ( +
        +
        Please log in to view pages!
        + +
        + ); + return ( +
        +

        Events

        + - {addEvent && ( - {setaddEvent(false); window.location.reload();}} /> +
        + User's Avatar +
        +
        Total Balance
        +
        + {
        You owe ${Math.abs(amountOwed).toFixed(2)}
        } + {
        You are owed ${amountOwedBy.toFixed(2)}
        } + {amountOwed === 0 && amountOwedBy === 0 && ( +
        All Balances are Settled!
        )} -
        -
        - +
        - ) +
        + + setSearchTerm(e.target.value)} + className="mt-4 search-input" + /> + +
        +
          + {eventData.events && eventData.events.length > 0 ? ( + eventData.events + .filter((event) => + event.name.toLowerCase().includes(searchTerm.toLowerCase()) + ) + .map((event) => ( +
        • +
          {reformatDate(event.date)}
          +
          + {event.name} +
          + + + +
        • + )) + ) : ( +
          + Please add your first event! +
          + )} +
        +
        + + {addEvent && ( + { + setaddEvent(false); + window.location.reload(); + }} + /> + )} +
        +
        + +
        +
        + ); } -export default Events \ No newline at end of file +export default Events; diff --git a/front-end/src/components/FriendsPage.jsx b/front-end/src/components/FriendsPage.jsx index 9a4b1bb..b4c6672 100644 --- a/front-end/src/components/FriendsPage.jsx +++ b/front-end/src/components/FriendsPage.jsx @@ -5,11 +5,19 @@ import "../styles/FriendsPage.css"; import AddFriendModal from "./AddFriendModal"; import Navbar from "./Navbar"; import { jwtDecode } from "jwt-decode"; +import { useNavigate } from "react-router-dom"; function FriendsPage({ isDarkMode }) { const [userData, setUserData] = useState(null); const [showModal, setShowModal] = useState(false); + const [isLoggedIn, setIsLoggedIn] = useState(true); + const navigate = useNavigate(); + + const handleButtonClick = () => { + navigate("/"); + }; + // useEffect for dark mode useEffect(() => { if (isDarkMode) { @@ -27,6 +35,12 @@ function FriendsPage({ isDarkMode }) { const fetchData = async () => { try { const token = localStorage.getItem("token"); + if (!token) { + console.error("No token found"); + console.error("Plese login in view pages"); + setIsLoggedIn(false); + return; + } const currentUser = jwtDecode(token); const userId = currentUser.id; const result = await axios.get( @@ -121,6 +135,15 @@ function FriendsPage({ isDarkMode }) { } }); + if (!isLoggedIn) + return ( +
        +
        Please log in to view pages!
        + +
        + ); if (!userData) return
        Loading...
        ; return ( diff --git a/front-end/src/components/Home.jsx b/front-end/src/components/Home.jsx index a718b78..38d32ed 100644 --- a/front-end/src/components/Home.jsx +++ b/front-end/src/components/Home.jsx @@ -4,8 +4,16 @@ import Navbar from "./Navbar"; import axios from "axios"; import { Link } from "react-router-dom"; import { jwtDecode } from "jwt-decode"; +import { useNavigate } from "react-router-dom"; const Home = ({ isDarkMode }) => { + const [isLoggedIn, setIsLoggedIn] = useState(true); + const navigate = useNavigate(); + + const handleButtonClick = () => { + navigate("/"); + }; + const [data, setData] = useState({ userName: "", totalSpending: 0, @@ -58,10 +66,10 @@ const Home = ({ isDarkMode }) => { } useEffect(() => { - const getTokenFromLocalStorage = () => { - const token = localStorage.getItem("token"); - return token; - }; + // const getTokenFromLocalStorage = () => { + // const token = localStorage.getItem("token"); + // return token; + // }; const decodeToken = (token) => { try { @@ -79,7 +87,14 @@ const Home = ({ isDarkMode }) => { const fetchData = async () => { try { - const token = getTokenFromLocalStorage(); + // const token = getTokenFromLocalStorage(); + const token = localStorage.getItem("token"); + if (!token) { + console.error("No token found"); + console.error("Plese login in view pages"); + setIsLoggedIn(false); + return; + } const currentUser = decodeToken(token); if (currentUser) { @@ -148,6 +163,12 @@ const Home = ({ isDarkMode }) => { const fetchData = async () => { try { const token = localStorage.getItem("token"); + if (!token) { + console.error("No token found"); + console.error("Plese login in view pages"); + setIsLoggedIn(false); + return; + } const currentUser = jwtDecode(token); const userId = currentUser.id; const result = await axios.get( @@ -248,6 +269,16 @@ const Home = ({ isDarkMode }) => { const friendsPendingPayment = data.friends ? data.friends.slice(0, 3) : []; const eventsPending = data.events ? data.events.slice(0, 3) : []; + if (!isLoggedIn) + return ( +
        +
        Please log in to view pages!
        + +
        + ); + return (
        diff --git a/front-end/src/components/Navbar.jsx b/front-end/src/components/Navbar.jsx index 8f1bb9a..7c25570 100644 --- a/front-end/src/components/Navbar.jsx +++ b/front-end/src/components/Navbar.jsx @@ -14,14 +14,18 @@ const Navbar = ({ isDarkMode }) => { // extract userId for link to User Page const token = localStorage.getItem("token"); + if (!token) { + console.error("No token found"); + console.error("Plese login in view pages"); + return null; + } const decoded = jwtDecode(token); const userId = decoded.id; - //changed up the events route and removed contact page return ( -
        + setSearchTerm(e.target.value)} + className="mt-4 search-input" + /> +
          - {calculateBalances(settlements).map((friend) => ( -
        • - - - {`${friend.username}'s - {friend.username} - - - - {friend.balance === 0 - ? "Settled" - : `$${friend.balance.toFixed(2)}`} - -
        • - ))} + {filteredSettlements.map((settlement) => { + // Calculate balance for each friend + const balance = + settlement.fromUserToFriend.reduce( + (acc, transaction) => + acc - (transaction.status === false ? transaction.amount : 0), + 0 + ) + + settlement.fromFriendToUser.reduce( + (acc, transaction) => + acc + (transaction.status === false ? transaction.amount : 0), + 0 + ); + + return ( +
        • + + + {`${settlement.friend.username}'s + {settlement.friend.username} + + + + {balance === 0 ? "Settled" : `$${balance.toFixed(2)}`} + +
        • + ); + })}
        diff --git a/front-end/src/components/Home.jsx b/front-end/src/components/Home.jsx index a718b78..41e288c 100644 --- a/front-end/src/components/Home.jsx +++ b/front-end/src/components/Home.jsx @@ -245,7 +245,6 @@ const Home = ({ isDarkMode }) => { }, [isDarkMode]); /* since spaces are limited, only display 3 event expenses/friends, the user can access the rest by clicking "view more" */ - const friendsPendingPayment = data.friends ? data.friends.slice(0, 3) : []; const eventsPending = data.events ? data.events.slice(0, 3) : []; return ( From 505ade79c9ad68d7361a7937aceac1fe20204020 Mon Sep 17 00:00:00 2001 From: joyc7 Date: Tue, 5 Dec 2023 14:49:26 -0500 Subject: [PATCH 55/60] User-info page in the navBar(remove user id in url)) --- front-end/src/App.js | 14 ++- front-end/src/components/Navbar.jsx | 11 +- front-end/src/components/UserInfo.jsx | 149 +++++++++++++------------- 3 files changed, 85 insertions(+), 89 deletions(-) diff --git a/front-end/src/App.js b/front-end/src/App.js index a8d302a..503b150 100644 --- a/front-end/src/App.js +++ b/front-end/src/App.js @@ -74,7 +74,7 @@ function App() { } /> } /> - } /> + + } + />
        diff --git a/front-end/src/components/Navbar.jsx b/front-end/src/components/Navbar.jsx index 8f1bb9a..8ed5802 100644 --- a/front-end/src/components/Navbar.jsx +++ b/front-end/src/components/Navbar.jsx @@ -12,16 +12,9 @@ const Navbar = ({ isDarkMode }) => { const location = useLocation(); const pathName = location.pathname; - // extract userId for link to User Page - const token = localStorage.getItem("token"); - const decoded = jwtDecode(token); - const userId = decoded.id; - - //changed up the events route and removed contact page - return ( -