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 3059467..d156ee2 100644
--- a/client/src/components/FolderCard.tsx
+++ b/client/src/components/FolderCard.tsx
@@ -1,47 +1,163 @@
-import React from "react";
+import React, { useState, useEffect } from "react";
+import { useMutation } from "@apollo/client";
import ExpenseForm from "./ExpenseForm";
+import { DELETE_EXPENSE, UPDATE_EXPENSE } from "../graphql/expenseMutations";
+import { FaTrash, FaEdit, FaSave, FaTimes } from "react-icons/fa";
import "../../styles/expenseManager.css";
-
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 }) => {
- return (
-
-
{folder.title}
-
- Created on: {new Date(Number(folder.createdAt)).toLocaleDateString()}
-
- {folder.expenses.length === 0 ? (
-
No expenses added yet.
- ) : (
-
- {folder.expenses.map((expense, idx) => (
- -
- {expense.category}: ${expense.amount.toFixed(2)}
- {expense.description && ` - ${expense.description}`}
-
- ))}
-
- )}
-
-
- );
+ const [localExpenses, setLocalExpenses] = useState(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.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) => {
+ deleteExpense({
+ variables: {
+ folderId: folder._id,
+ expenseIndex: index,
+ },
+ });
+ };
+
+ 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: 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.
+ ) : (
+
+ )}
+
+
+ Total: ${totalCost.toFixed(2)}
+
+
+
+
+ );
};
export default FolderCard;
\ No newline at end of file
diff --git a/client/src/graphql/expenseMutations.ts b/client/src/graphql/expenseMutations.ts
index 5c87237..9a13de2 100644
--- a/client/src/graphql/expenseMutations.ts
+++ b/client/src/graphql/expenseMutations.ts
@@ -32,4 +32,46 @@ export const ADD_EXPENSE = gql `
}
}
}
-`;
\ No newline at end of file
+`;
+
+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) {
+ _id
+ title
+ createdAt
+ expenses {
+ category
+ description
+ amount
+ }
+ }
+ }
+`;
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 a6feb49..39709f3 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 {
@@ -88,6 +82,22 @@
.folder-error {
color: red;
}
+
+.folder-total {
+ margin-top: 0.75rem;
+ 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;
+}
/* RESPONSIVE IMPROVEMENTS */
@media (max-width: 768px) {
diff --git a/server/src/schemas/resolvers.ts b/server/src/schemas/resolvers.ts
index 3d27c14..ff69977 100644
--- a/server/src/schemas/resolvers.ts
+++ b/server/src/schemas/resolvers.ts
@@ -320,6 +320,55 @@ 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");
+ }
+
+ 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];
+ if (category !== undefined) expense.category = category;
+ if (amount !== undefined) expense.amount = amount;
+ if (description !== undefined) expense.description = description;
+
+ await folder.save();
+ return folder;
+ },
},
};
diff --git a/server/src/schemas/typeDefs.ts b/server/src/schemas/typeDefs.ts
index 02c6dc3..c4efd6b 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,16 @@ const typeDefs = gql`
amount: Float!
description: String
): ExpenseFolder
+
+ deleteExpenseFromFolder(folderId: ID!, expenseIndex: Int!): ExpenseFolder
+
+ updateExpenseInFolder(
+ folderId: ID!
+ expenseIndex: Int!
+ category: String!
+ amount: Float!
+ description: String
+ ): ExpenseFolder
}
`;