Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions client/src/components/ExpenseManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ const ExpenseManager: React.FC = () => {

<div className="folder-grid">
{data.getMyExpenseFolders.map((folder: any) => (
<div key={folder._id} className="folder-wrapper">
<FolderCard folder={folder} />
</div>
<FolderCard key={folder._id} folder={folder} />
))}
</div>
</div>
Expand Down
178 changes: 147 additions & 31 deletions client/src/components/FolderCard.tsx
Original file line number Diff line number Diff line change
@@ -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<FolderCardProps> = ({ folder }) => {
return (
<div className="folder-card">
<h3>{folder.title}</h3>
<p className="folder-created">
Created on: {new Date(Number(folder.createdAt)).toLocaleDateString()}
</p>
{folder.expenses.length === 0 ? (
<p>No expenses added yet.</p>
) : (
<ul className="folder-expense-list">
{folder.expenses.map((expense, idx) => (
<li key={idx}>
<strong>{expense.category}</strong>: ${expense.amount.toFixed(2)}
{expense.description && ` - ${expense.description}`}
</li>
))}
</ul>
)}
<ExpenseForm folderId={folder._id} />
</div>
);
const [localExpenses, setLocalExpenses] = useState<Expense[]>(folder.expenses);

const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [editValues, setEditValues] = useState<Expense>({
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 (
<div className="folder-card">
<h3>{folder.title}</h3>
<p className="folder-created">
Created on: {new Date(Number(folder.createdAt)).toLocaleDateString()}
</p>

{localExpenses.length === 0 ? (
<p>No expenses added yet.</p>
) : (
<ul className="folder-expense-list">
{localExpenses.map((expense, idx) => (
<li key={idx}>
{editingIndex === idx ? (
<>
<select
value={editValues.category}
onChange={(e) => setEditValues({ ...editValues, category: e.target.value })}
>
<option value="Airfare">Airfare</option>
<option value="Food">Food</option>
<option value="Hotel">Hotel</option>
<option value="Vehicle">Vehicle</option>
<option value="Miscellaneous">Miscellaneous</option>
</select>
<input
type="number"
value={editValues.amount}
onChange={(e) => setEditValues({ ...editValues, amount: parseFloat(e.target.value) })}
/>
<input
type="text"
placeholder="Description"
value={editValues.description || ""}
onChange={(e) => setEditValues({ ...editValues, description: e.target.value })}
/>
<button onClick={handleUpdate} title="Save">
<FaSave />
</button>
<button onClick={cancelEditing} title="Cancel">
<FaTimes />
</button>
</>
) : (
<>
<strong>{expense.category}</strong>: ${expense.amount.toFixed(2)}
{expense.description && ` - ${expense.description}`}
<button onClick={() => startEditing(idx)} title="Edit">
<FaEdit />
</button>
<button onClick={() => handleDelete(idx)} title="Delete" className="delete-expense-btn">
<FaTrash size={14} color="red" />
</button>
</>
)}
</li>
))}
</ul>
)}

<div className="folder-total">
<strong>Total:</strong> ${totalCost.toFixed(2)}
</div>

<ExpenseForm folderId={folder._id} />
</div>
);
};

export default FolderCard;
44 changes: 43 additions & 1 deletion client/src/graphql/expenseMutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,46 @@ 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) {
_id
title
createdAt
expenses {
category
description
amount
}
}
}
`;
4 changes: 2 additions & 2 deletions client/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 18 additions & 8 deletions client/styles/expenseManager.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down
49 changes: 49 additions & 0 deletions server/src/schemas/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
},
};

Expand Down
12 changes: 11 additions & 1 deletion server/src/schemas/typeDefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const typeDefs = gql`
_id: ID!
title: String!
createdAt: String
expenses: [Expense]
expenses: [Expense!]!
}

type Mutation {
Expand Down Expand Up @@ -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
}
`;

Expand Down