From 251f799691e82543e04a9a366fb9921fe721cea9 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Mon, 9 Dec 2024 19:18:57 +0100 Subject: [PATCH] Expenses CRUD operations --- app/[slug]/dashboard/expenses/page.tsx | 43 ++++++++++-- app/api/expenses.tsx | 40 ----------- app/lib/expense.tsx | 2 +- app/ui/components/expense-form-dialog.tsx | 83 +++++++++++++++++++++++ app/ui/expenses/table.tsx | 34 +++++----- 5 files changed, 136 insertions(+), 66 deletions(-) delete mode 100644 app/api/expenses.tsx create mode 100644 app/ui/components/expense-form-dialog.tsx diff --git a/app/[slug]/dashboard/expenses/page.tsx b/app/[slug]/dashboard/expenses/page.tsx index 8b24064..dfa7bbf 100644 --- a/app/[slug]/dashboard/expenses/page.tsx +++ b/app/[slug]/dashboard/expenses/page.tsx @@ -1,15 +1,44 @@ /* Copyright (C) 2024 Manuel Bustillo*/ -import { lusitana } from '@/app/ui/fonts'; +'use client' + +import { AbstractApi } from '@/app/api/abstract-api'; +import { Expense, ExpenseSerializer } from '@/app/lib/expense'; +import { classNames } from '@/app/ui/components/button'; +import ExpenseFormDialog from '@/app/ui/components/expense-form-dialog'; import ExpensesTable from '@/app/ui/expenses/table'; - -export default function Page () { +import SkeletonTable from '@/app/ui/guests/skeleton-row'; +import { Suspense, useEffect, useState } from 'react'; + +export default function Page() { + const refreshExpenses = () => { + new AbstractApi().getAll(new ExpenseSerializer(), (expenses: Expense[]) => { + setExpenses(expenses); + }); + } + + const [expenses, setExpenses] = useState([]); + const [expenseBeingEdited, setExpenseBeingEdited] = useState(undefined); + useEffect(() => { refreshExpenses() }, []); + return (
-
-

Expenses

-

Summary

- +
+ + { refreshExpenses(); setExpenseBeingEdited(undefined) }} + expense={expenseBeingEdited} + visible={expenseBeingEdited !== undefined} + onHide={() => { setExpenseBeingEdited(undefined) }} + /> + }> + setExpenseBeingEdited(expense)} + /> +
); diff --git a/app/api/expenses.tsx b/app/api/expenses.tsx deleted file mode 100644 index b3929bf..0000000 --- a/app/api/expenses.tsx +++ /dev/null @@ -1,40 +0,0 @@ -// /* Copyright (C) 2024 Manuel Bustillo*/ - -// import { Expense } from '@/app/lib/definitions'; -// import { getCsrfToken, getSlug } from '@/app/lib/utils'; - -// export function loadExpenses(onLoad?: (expenses: Expense[]) => void) { -// fetch(`/api/${getSlug()}/expenses`) -// .then((response) => response.json()) -// .then((data) => { -// onLoad && onLoad(data.map((record: any) => { -// return ({ -// id: record.id, -// name: record.name, -// amount: record.amount, -// pricingType: record.pricing_type -// }); -// })); -// }, (error) => { -// return []; -// }); -// } - -// export function updateExpense(expense: Expense) { -// fetch(`/api/${getSlug()}/expenses/${expense.id}`, -// { -// method: 'PUT', -// body: JSON.stringify({ -// expense: { -// name: expense.name, -// amount: expense.amount, -// pricing_type: expense.pricingType, -// } -// }), -// headers: { -// 'Content-Type': 'application/json', -// 'X-CSRF-TOKEN': getCsrfToken(), -// } -// }) -// .catch((error) => console.error(error)); -// } \ No newline at end of file diff --git a/app/lib/expense.tsx b/app/lib/expense.tsx index a402623..2345d54 100644 --- a/app/lib/expense.tsx +++ b/app/lib/expense.tsx @@ -1,7 +1,7 @@ import { Serializable } from "../api/abstract-api"; import { Entity } from "./definitions"; -export const pricingTypes = ['fixed', 'per person'] as const; +export const pricingTypes = ['fixed', 'per_person'] as const; export type PricingType = typeof pricingTypes[number]; export class Expense implements Entity { diff --git a/app/ui/components/expense-form-dialog.tsx b/app/ui/components/expense-form-dialog.tsx new file mode 100644 index 0000000..3c6747f --- /dev/null +++ b/app/ui/components/expense-form-dialog.tsx @@ -0,0 +1,83 @@ +/* Copyright (C) 2024 Manuel Bustillo*/ + +'use client'; + +import { AbstractApi } from '@/app/api/abstract-api'; +import { Expense, ExpenseSerializer, PricingType, pricingTypes } from '@/app/lib/expense'; +import { capitalize } from '@/app/lib/utils'; +import { classNames } from '@/app/ui/components/button'; +import { Dialog } from 'primereact/dialog'; +import { Dropdown } from 'primereact/dropdown'; +import { FloatLabel } from 'primereact/floatlabel'; +import { InputText } from 'primereact/inputtext'; +import { useState } from 'react'; + +export default function ExpenseFormDialog({ onCreate, onHide, expense, visible }: { + onCreate?: () => void, + onHide: () => void, + expense?: Expense, + visible: boolean, +}) { + + const [name, setName] = useState(expense?.name || ''); + const [amount, setAmount] = useState(expense?.amount || 0); + const [pricingType, setPricingType] = useState(expense?.pricingType || 'fixed'); + + const api = new AbstractApi(); + const serializer = new ExpenseSerializer(); + + function resetForm() { + setName(''); + setAmount(0); + setPricingType('fixed'); + } + + function submitGroup() { + if (expense?.id !== undefined) { + expense.name = name; + expense.amount = amount; + expense.pricingType = pricingType; + + api.update(serializer, expense, () => { + resetForm(); + onCreate && onCreate(); + }); + } else { + + api.create(serializer, new Expense(undefined, name, amount, pricingType), () => { + resetForm(); + onCreate && onCreate(); + }); + } + } + + return ( + <> + + +
+ + setName(e.target.value)} /> + + + + setAmount(parseFloat(e.target.value))} /> + + + + setPricingType(e.target.value)} options={ + pricingTypes.map((type) => { + return { label: capitalize(type), value: type }; + }) + } /> + + + + +
+
+ + ); +} \ No newline at end of file diff --git a/app/ui/expenses/table.tsx b/app/ui/expenses/table.tsx index 0d68005..28543d6 100644 --- a/app/ui/expenses/table.tsx +++ b/app/ui/expenses/table.tsx @@ -2,45 +2,43 @@ 'use client' -import { useState } from "react"; -import InlineTextField from "../components/form/inlineTextField"; -import TableOfContents from "../components/table-of-contents"; import { AbstractApi } from '@/app/api/abstract-api'; import { Expense, ExpenseSerializer } from '@/app/lib/expense'; - -export default function ExpensesTable() { - const [expenses, setExpenses] = useState>([]); - const [expensesLoaded, setExpensesLoaded] = useState(false); +import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; +import TableOfContents from "../components/table-of-contents"; - function refreshExpenses() { - new AbstractApi().getAll(new ExpenseSerializer(), (expenses: Expense[]) => { - setExpenses(expenses); - setExpensesLoaded(true); - }); - } - - !expensesLoaded && refreshExpenses(); +export default function ExpensesTable({ expenses, onUpdate, onEdit }: { + expenses: Expense[], + onUpdate: () => void, + onEdit: (expense: Expense) => void, +}) { const api = new AbstractApi(); const serializer = new ExpenseSerializer(); return ( ( - { expense.name = value; updateExpense(expense) }} /> + {expense.name} - { expense.amount = parseFloat(value); updateExpense(expense) }} /> + {expense.amount} {expense.pricingType} + +
+ { api.destroy(serializer, expense, onUpdate) }} /> + onEdit(expense)} /> +
+ )} />