From aa462f3eda902aafacfa6c8a55e4fd868b73fa9e Mon Sep 17 00:00:00 2001 From: Stephen Schier Date: Wed, 4 Jun 2025 20:58:02 -0500 Subject: [PATCH 1/5] added total cost calculator per trip --- client/src/components/FolderCard.tsx | 5 +++++ client/styles/expenseManager.css | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/client/src/components/FolderCard.tsx b/client/src/components/FolderCard.tsx index 3059467..4d257e1 100644 --- a/client/src/components/FolderCard.tsx +++ b/client/src/components/FolderCard.tsx @@ -21,6 +21,8 @@ interface FolderCardProps { } const FolderCard: React.FC = ({ folder }) => { + const totalCost = folder.expenses.reduce((sum, expense) => sum + expense.amount, 0); + return (

{folder.title}

@@ -39,6 +41,9 @@ const FolderCard: React.FC = ({ folder }) => { ))} )} +
+ Total: ${totalCost.toFixed(2)} +
); diff --git a/client/styles/expenseManager.css b/client/styles/expenseManager.css index e1d3ea2..c57e988 100644 --- a/client/styles/expenseManager.css +++ b/client/styles/expenseManager.css @@ -87,4 +87,11 @@ .folder-error { color: red; +} + +.folder-total { + margin-top: 0.75rem; + font-size: 1.1rem; + font-weight: bold; + color: hotpink; } \ No newline at end of file From b2251b73fbbd5909501d0dda5d6da512551e75cf Mon Sep 17 00:00:00 2001 From: Stephen Schier Date: Wed, 4 Jun 2025 21:39:59 -0500 Subject: [PATCH 2/5] added Delete expense functionality --- client/src/components/FolderCard.tsx | 38 ++++++++++++++++++++++++-- client/src/graphql/expenseMutations.ts | 17 +++++++++++- client/styles/expenseManager.css | 9 ++++++ server/src/schemas/resolvers.ts | 19 +++++++++++++ server/src/schemas/typeDefs.ts | 4 ++- 5 files changed, 82 insertions(+), 5 deletions(-) diff --git a/client/src/components/FolderCard.tsx b/client/src/components/FolderCard.tsx index 4d257e1..d6e425a 100644 --- a/client/src/components/FolderCard.tsx +++ b/client/src/components/FolderCard.tsx @@ -1,6 +1,10 @@ -import React from "react"; +import React, { useState } from "react"; +import { useMutation } from "@apollo/client"; import ExpenseForm from "./ExpenseForm"; +import { DELETE_EXPENSE } from "../graphql/expenseMutations"; import "../../styles/expenseManager.css"; +import { FaTrash } from "react-icons/fa"; + interface Expense { @@ -21,6 +25,26 @@ interface FolderCardProps { } const FolderCard: React.FC = ({ folder }) => { + const [localExpenses, setLocalExpenses] = useState(folder.expenses); + + const [deleteExpense] = useMutation(DELETE_EXPENSE, { + onCompleted: (data) => { + setLocalExpenses(data.deleteExpenseFromFolder.expenses); + }, + onError: (error) => { + console.error("Failed to delete expense:", error.message); + }, + }); + + const handleDelete = (index: number) => { + deleteExpense({ + variables: { + folderId: folder._id, + expenseIndex: index, + }, + }); + }; + const totalCost = folder.expenses.reduce((sum, expense) => sum + expense.amount, 0); return ( @@ -29,14 +53,22 @@ const FolderCard: React.FC = ({ folder }) => {

Created on: {new Date(Number(folder.createdAt)).toLocaleDateString()}

- {folder.expenses.length === 0 ? ( + + {localExpenses.length === 0 ? (

No expenses added yet.

) : (
    - {folder.expenses.map((expense, idx) => ( + {localExpenses.map((expense, idx) => (
  • {expense.category}: ${expense.amount.toFixed(2)} {expense.description && ` - ${expense.description}`} +
  • ))}
diff --git a/client/src/graphql/expenseMutations.ts b/client/src/graphql/expenseMutations.ts index 5c87237..a5ab3ac 100644 --- a/client/src/graphql/expenseMutations.ts +++ b/client/src/graphql/expenseMutations.ts @@ -32,4 +32,19 @@ export const ADD_EXPENSE = gql ` } } } -`; \ No newline at end of file +`; + +export const DELETE_EXPENSE = gql` + mutation DeleteExpenseFromFolder($folderId: ID!, $expenseIndex: Int!) { + deleteExpenseFromFolder(folderId: $folderId, expenseIndex: $expenseIndex) { + _id + title + createdAt + expenses { + category + description + amount + } + } + } +`; diff --git a/client/styles/expenseManager.css b/client/styles/expenseManager.css index c57e988..e0a4925 100644 --- a/client/styles/expenseManager.css +++ b/client/styles/expenseManager.css @@ -94,4 +94,13 @@ font-size: 1.1rem; font-weight: bold; color: hotpink; +} + +.delete-expense-btn { + margin-left: 0.5rem; + background: none; + border: none; + color: red; + cursor: pointer; + font-size: 1rem; } \ No newline at end of file diff --git a/server/src/schemas/resolvers.ts b/server/src/schemas/resolvers.ts index 3d27c14..a4a68df 100644 --- a/server/src/schemas/resolvers.ts +++ b/server/src/schemas/resolvers.ts @@ -320,6 +320,25 @@ export const resolvers = { return updatedFolder; }, + + deleteExpenseFromFolder: async ( + _:any, + { folderId, expenseIndex }:any, + context: any + ) => { + if (!context.user) throw new Error("Not authenticated"); + + const folder = await ExpenseFolder.findOne({ _id: folderId, userId: context.user_id }); + + if (!folder) { + throw new Error("Folder not found"); + } + + folder.expenses.splice(expenseIndex, 1); + await folder.save(); + + return folder; + } }, }; diff --git a/server/src/schemas/typeDefs.ts b/server/src/schemas/typeDefs.ts index 02c6dc3..a6ac0a2 100644 --- a/server/src/schemas/typeDefs.ts +++ b/server/src/schemas/typeDefs.ts @@ -69,7 +69,7 @@ const typeDefs = gql` _id: ID! title: String! createdAt: String - expenses: [Expense] + expenses: [Expense!]! } type Mutation { @@ -120,6 +120,8 @@ const typeDefs = gql` amount: Float! description: String ): ExpenseFolder + + deleteExpenseFromFolder(folderId: ID!, expenseIndex: Int!): ExpenseFolder } `; From c31bbbed59b190bde3917995553f283cfe380d53 Mon Sep 17 00:00:00 2001 From: Stephen Schier Date: Thu, 5 Jun 2025 21:03:31 -0500 Subject: [PATCH 3/5] fixing delete functionality, and adjusting adding expense --- client/src/components/ExpenseManager.tsx | 4 +--- client/src/components/FolderCard.tsx | 19 +++++++++++++++---- client/src/main.tsx | 4 ++-- client/styles/expenseManager.css | 10 ++-------- server/src/schemas/resolvers.ts | 8 ++++++-- 5 files changed, 26 insertions(+), 19 deletions(-) diff --git a/client/src/components/ExpenseManager.tsx b/client/src/components/ExpenseManager.tsx index e5f502e..46dcd8a 100644 --- a/client/src/components/ExpenseManager.tsx +++ b/client/src/components/ExpenseManager.tsx @@ -51,9 +51,7 @@ const ExpenseManager: React.FC = () => {
{data.getMyExpenseFolders.map((folder: any) => ( -
- -
+ ))}
diff --git a/client/src/components/FolderCard.tsx b/client/src/components/FolderCard.tsx index d6e425a..364e953 100644 --- a/client/src/components/FolderCard.tsx +++ b/client/src/components/FolderCard.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { useMutation } from "@apollo/client"; import ExpenseForm from "./ExpenseForm"; import { DELETE_EXPENSE } from "../graphql/expenseMutations"; @@ -25,18 +25,29 @@ interface FolderCardProps { } const FolderCard: React.FC = ({ folder }) => { - const [localExpenses, setLocalExpenses] = useState(folder.expenses); + const [localExpenses, setLocalExpenses] = useState(folder.expenses); + + useEffect(() => { + setLocalExpenses(folder.expenses); + }, [folder.expenses]); + const [deleteExpense] = useMutation(DELETE_EXPENSE, { onCompleted: (data) => { setLocalExpenses(data.deleteExpenseFromFolder.expenses); }, onError: (error) => { - console.error("Failed to delete expense:", error.message); + console.error("Failed to delete expense:", error); }, }); const handleDelete = (index: number) => { + console.log("Deleting expense for index", index); + console.log("folderId:", folder._id); + console.log("Variables", { + folderId: folder._id, + expenseIndex: index, + }); deleteExpense({ variables: { folderId: folder._id, @@ -45,7 +56,7 @@ const FolderCard: React.FC = ({ folder }) => { }); }; - const totalCost = folder.expenses.reduce((sum, expense) => sum + expense.amount, 0); + const totalCost = localExpenses.reduce((sum, expense) => sum + expense.amount, 0); return (
diff --git a/client/src/main.tsx b/client/src/main.tsx index e2b19f0..06ee518 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -16,10 +16,10 @@ import { AuthProvider } from "./context/AuthContext"; // Setup HTTP connection to GraphQL const httpLink = createHttpLink({ // TODO: USE THIS FOR LOCAL TESTING: - // uri: "http://localhost:3001/graphql", + uri: "http://localhost:3001/graphql", // TODO: USE THIS FOR DEPLOY - uri: "https://miletracker-wokk.onrender.com/graphql", + //uri: "https://miletracker-wokk.onrender.com/graphql", }); // Attach token to headers diff --git a/client/styles/expenseManager.css b/client/styles/expenseManager.css index e0a4925..2153d88 100644 --- a/client/styles/expenseManager.css +++ b/client/styles/expenseManager.css @@ -53,15 +53,9 @@ } .folder-grid { - display: flex; - flex-wrap: wrap; + display: grid; gap: 1rem; - justify-content: space-between; -} - -.folder-wrapper { - flex: 0 1 calc(33.333% - 1rem); - display: flex; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); } .folder-card { diff --git a/server/src/schemas/resolvers.ts b/server/src/schemas/resolvers.ts index a4a68df..3cacb08 100644 --- a/server/src/schemas/resolvers.ts +++ b/server/src/schemas/resolvers.ts @@ -328,12 +328,16 @@ export const resolvers = { ) => { if (!context.user) throw new Error("Not authenticated"); - const folder = await ExpenseFolder.findOne({ _id: folderId, userId: context.user_id }); + const folder = await ExpenseFolder.findOne({ _id: folderId, userId: context.user._id }); if (!folder) { throw new Error("Folder not found"); } - + + if (expenseIndex < 0 || expenseIndex >= folder.expenses.length) { + throw new Error("Invalid expense index"); + } + folder.expenses.splice(expenseIndex, 1); await folder.save(); From da3eaf8bc9251d41b25c5a6484024969c8a0046b Mon Sep 17 00:00:00 2001 From: Stephen Schier Date: Thu, 5 Jun 2025 21:20:38 -0500 Subject: [PATCH 4/5] backend for update expense functionality --- client/src/graphql/expenseMutations.ts | 27 +++++++++++++++++++++++ server/src/schemas/resolvers.ts | 30 ++++++++++++++++++++++++-- server/src/schemas/typeDefs.ts | 8 +++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/client/src/graphql/expenseMutations.ts b/client/src/graphql/expenseMutations.ts index a5ab3ac..9a13de2 100644 --- a/client/src/graphql/expenseMutations.ts +++ b/client/src/graphql/expenseMutations.ts @@ -34,6 +34,33 @@ export const ADD_EXPENSE = gql ` } `; +export const UPDATE_EXPENSE = gql` + mutation UpdateExpenseInFolder( + $folderId: ID! + $expenseIndex: Int! + $category: String! + $amount: Float! + $description: String + ) { + updateExpenseInFolder( + folderId: $folderId + expenseIndex: $expenseIndex + category: $category + amount: $amount + description: $description + ) { + _id + title + createdAt + expenses { + category + description + amount + } + } + } +`; + export const DELETE_EXPENSE = gql` mutation DeleteExpenseFromFolder($folderId: ID!, $expenseIndex: Int!) { deleteExpenseFromFolder(folderId: $folderId, expenseIndex: $expenseIndex) { diff --git a/server/src/schemas/resolvers.ts b/server/src/schemas/resolvers.ts index 3cacb08..584b386 100644 --- a/server/src/schemas/resolvers.ts +++ b/server/src/schemas/resolvers.ts @@ -337,12 +337,38 @@ export const resolvers = { if (expenseIndex < 0 || expenseIndex >= folder.expenses.length) { throw new Error("Invalid expense index"); } - + folder.expenses.splice(expenseIndex, 1); await folder.save(); return folder; - } + }, + + updateExpenseInFolder: async ( + _: any, + { folderId, expenseIndex, category, amount, description }: any, + context: any + ) => { + if (!context.user) throw new AuthenticationError("Not authenticated"); + + const folder = await ExpenseFolder.findOne({ + _id: folderId, + userId: context.user._id, + }); + + if (!folder) throw new Error("Folder not found"); + if (expenseIndex < 0 || expenseIndex >= folder.expenses.length) { + throw new Error("Invalid expense index"); + } + + const expense = folder.expenses[expenseIndex]; + expense.category = category; + expense.amount = amount; + expense.description = description; + + await folder.save(); + return folder; + }, }, }; diff --git a/server/src/schemas/typeDefs.ts b/server/src/schemas/typeDefs.ts index a6ac0a2..c4efd6b 100644 --- a/server/src/schemas/typeDefs.ts +++ b/server/src/schemas/typeDefs.ts @@ -122,6 +122,14 @@ const typeDefs = gql` ): ExpenseFolder deleteExpenseFromFolder(folderId: ID!, expenseIndex: Int!): ExpenseFolder + + updateExpenseInFolder( + folderId: ID! + expenseIndex: Int! + category: String! + amount: Float! + description: String + ): ExpenseFolder } `; From 3ef94c737da0d3c186e72a991cb5f6e339ea40b5 Mon Sep 17 00:00:00 2001 From: Stephen Schier Date: Fri, 6 Jun 2025 15:18:21 -0500 Subject: [PATCH 5/5] added update expense functionality to expense folders --- client/src/components/FolderCard.tsx | 180 ++++++++++++++++++--------- server/src/schemas/resolvers.ts | 6 +- 2 files changed, 127 insertions(+), 59 deletions(-) diff --git a/client/src/components/FolderCard.tsx b/client/src/components/FolderCard.tsx index 364e953..d156ee2 100644 --- a/client/src/components/FolderCard.tsx +++ b/client/src/components/FolderCard.tsx @@ -1,95 +1,163 @@ import React, { useState, useEffect } from "react"; import { useMutation } from "@apollo/client"; import ExpenseForm from "./ExpenseForm"; -import { DELETE_EXPENSE } from "../graphql/expenseMutations"; +import { DELETE_EXPENSE, UPDATE_EXPENSE } from "../graphql/expenseMutations"; +import { FaTrash, FaEdit, FaSave, FaTimes } from "react-icons/fa"; import "../../styles/expenseManager.css"; -import { FaTrash } from "react-icons/fa"; - - interface Expense { - category: string; - description?: string; - amount: number; + category: string; + description?: string; + amount: number; } interface Folder { - _id: string; - title: string; - createdAt: string; - expenses: Expense[]; + _id: string; + title: string; + createdAt: string; + expenses: Expense[]; } interface FolderCardProps { - folder: Folder; + folder: Folder; } const FolderCard: React.FC = ({ folder }) => { const [localExpenses, setLocalExpenses] = useState(folder.expenses); - useEffect(() => { - setLocalExpenses(folder.expenses); - }, [folder.expenses]); - + const [editingIndex, setEditingIndex] = useState(null); + const [editValues, setEditValues] = useState({ + category: "", + amount: 0, + description: "", + }); const [deleteExpense] = useMutation(DELETE_EXPENSE, { onCompleted: (data) => { setLocalExpenses(data.deleteExpenseFromFolder.expenses); }, onError: (error) => { - console.error("Failed to delete expense:", error); + console.error("Failed to delete expense:", error.message); }, }); + const [updateExpense] = useMutation(UPDATE_EXPENSE, { + onCompleted: (data) => { + setLocalExpenses(data.updateExpenseInFolder.expenses); + setEditingIndex(null); + }, + onError: (error) => { + console.error("Failed to update expense:", error.message); + }, + }); + + useEffect(() => { + setLocalExpenses(folder.expenses); + }, [folder.expenses]); + const handleDelete = (index: number) => { - console.log("Deleting expense for index", index); - console.log("folderId:", folder._id); - console.log("Variables", { + deleteExpense({ + variables: { folderId: folder._id, expenseIndex: index, + }, }); - deleteExpense({ + }; + + const startEditing = (index: number) => { + const expense = localExpenses[index]; + setEditingIndex(index); + setEditValues({ ...expense }); + }; + + const cancelEditing = () => { + setEditingIndex(null); + setEditValues({ category: "", amount: 0, description: "" }); + }; + + const handleUpdate = () => { + if (editingIndex === null) return; + + updateExpense({ variables: { folderId: folder._id, - expenseIndex: index, + expenseIndex: editingIndex, + category: editValues.category, + amount: editValues.amount, + description: editValues.description, }, }); }; - const totalCost = localExpenses.reduce((sum, expense) => sum + expense.amount, 0); - - return ( -
-

{folder.title}

-

- Created on: {new Date(Number(folder.createdAt)).toLocaleDateString()} -

- - {localExpenses.length === 0 ? ( -

No expenses added yet.

- ) : ( -
    - {localExpenses.map((expense, idx) => ( -
  • - {expense.category}: ${expense.amount.toFixed(2)} - {expense.description && ` - ${expense.description}`} - -
  • - ))} -
- )} -
- Total: ${totalCost.toFixed(2)} -
- -
- ); + const totalCost = localExpenses.reduce((sum, expense) => sum + expense.amount, 0); + + return ( +
+

{folder.title}

+

+ Created on: {new Date(Number(folder.createdAt)).toLocaleDateString()} +

+ + {localExpenses.length === 0 ? ( +

No expenses added yet.

+ ) : ( +
    + {localExpenses.map((expense, idx) => ( +
  • + {editingIndex === idx ? ( + <> + + setEditValues({ ...editValues, amount: parseFloat(e.target.value) })} + /> + setEditValues({ ...editValues, description: e.target.value })} + /> + + + + ) : ( + <> + {expense.category}: ${expense.amount.toFixed(2)} + {expense.description && ` - ${expense.description}`} + + + + )} +
  • + ))} +
+ )} + +
+ Total: ${totalCost.toFixed(2)} +
+ + +
+ ); }; export default FolderCard; \ No newline at end of file diff --git a/server/src/schemas/resolvers.ts b/server/src/schemas/resolvers.ts index 584b386..ff69977 100644 --- a/server/src/schemas/resolvers.ts +++ b/server/src/schemas/resolvers.ts @@ -362,9 +362,9 @@ export const resolvers = { } const expense = folder.expenses[expenseIndex]; - expense.category = category; - expense.amount = amount; - expense.description = description; + if (category !== undefined) expense.category = category; + if (amount !== undefined) expense.amount = amount; + if (description !== undefined) expense.description = description; await folder.save(); return folder;