From a3ddde45c0e7d0df64d8d2f02ddf018fa3104789 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/lib/expense.tsx | 2 +- app/ui/components/expense-form-dialog.tsx | 83 +++++++++++++++++++++++ app/ui/expenses/table.tsx | 34 +++++----- 4 files changed, 136 insertions(+), 26 deletions(-) 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/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)} /> +
+ )} />