From e5a17339294acf8a1c4ce7dcdd36184e650fad3c Mon Sep 17 00:00:00 2001 From: Juan Gabriel Kairupan Date: Fri, 12 Jun 2026 21:23:46 +0700 Subject: [PATCH] crud input added --- .../src/components/invoices/InvoiceTable.tsx | 23 +- react-app/src/hooks/useInvoices.ts | 99 +++++++-- react-app/src/pages/Invoices.tsx | 197 +++++++++++++++++- react-app/src/services/invoices.ts | 19 +- react-app/src/types/invoice.ts | 4 + 5 files changed, 317 insertions(+), 25 deletions(-) diff --git a/react-app/src/components/invoices/InvoiceTable.tsx b/react-app/src/components/invoices/InvoiceTable.tsx index b618aa0..664d6a4 100644 --- a/react-app/src/components/invoices/InvoiceTable.tsx +++ b/react-app/src/components/invoices/InvoiceTable.tsx @@ -12,9 +12,11 @@ import { type InvoiceTableProps = { invoices: Invoice[] + onDeleteInvoice?: (id: number) => void + onEditInvoice?: (invoice: Invoice) => void } -function InvoiceTable({ invoices }: InvoiceTableProps) { +function InvoiceTable({ invoices, onDeleteInvoice, onEditInvoice }: InvoiceTableProps) { return ( @@ -24,6 +26,7 @@ function InvoiceTable({ invoices }: InvoiceTableProps) { Status Due Date Amount + Actions @@ -44,6 +47,24 @@ function InvoiceTable({ invoices }: InvoiceTableProps) { {formatCurrency(invoice.amount)} + +
+ + +
+
))}
diff --git a/react-app/src/hooks/useInvoices.ts b/react-app/src/hooks/useInvoices.ts index 5747580..4a0f8c4 100644 --- a/react-app/src/hooks/useInvoices.ts +++ b/react-app/src/hooks/useInvoices.ts @@ -1,33 +1,94 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { mockInvoices } from '../data/invoices' -import { fetchInvoices } from '../services/invoices' -import type { Invoice } from '../types/invoice' +import { + createInvoice, + deleteInvoice, + fetchInvoices, + updateInvoice, +} from '../services/invoices' +import type { CreateInvoiceInput, Invoice, UpdateInvoiceInput } from '../types/invoice' export function useInvoices() { const [invoices, setInvoices] = useState(mockInvoices) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) + const [mutationError, setMutationError] = useState(null) + + const loadInvoices = useCallback(async () => { + setIsLoading(true) + try { + const data = await fetchInvoices() + setInvoices(data) + setError(null) + } catch { + setError('Unable to load invoice data.') + } finally { + setIsLoading(false) + } + }, []) useEffect(() => { - let isMounted = true - - const load = async () => { - try { - const data = await fetchInvoices() - if (isMounted) setInvoices(data) - } catch { - if (isMounted) setError('Unable to load invoice data.') - } finally { - if (isMounted) setIsLoading(false) - } + loadInvoices() + }, [loadInvoices]) + + const addInvoice = async (invoice: CreateInvoiceInput) => { + const optimisticInvoice = { + ...invoice, + id: Math.max(...invoices.map((current) => current.id), 0) + 1, + } + + setInvoices((current) => [...current, optimisticInvoice]) + setMutationError(null) + + try { + const savedInvoice = await createInvoice(invoice) + setInvoices((current) => + current.map((existing) => + existing.id === optimisticInvoice.id ? savedInvoice : existing, + ), + ) + } catch { + setMutationError('API unavailable. Invoice was added locally for this session.') + } + } + + const editInvoice = async (id: number, invoice: UpdateInvoiceInput) => { + setInvoices((current) => + current.map((existing) => + existing.id === id ? { ...existing, ...invoice } : existing, + ), + ) + setMutationError(null) + + try { + const savedInvoice = await updateInvoice(id, invoice) + setInvoices((current) => + current.map((existing) => (existing.id === id ? savedInvoice : existing)), + ) + } catch { + setMutationError('API unavailable. Invoice changes were kept locally for this session.') } + } - load() + const removeInvoice = async (id: number) => { + setInvoices((current) => current.filter((invoice) => invoice.id !== id)) + setMutationError(null) - return () => { - isMounted = false + try { + await deleteInvoice(id) + } catch { + setMutationError('API unavailable. Invoice was deleted locally for this session.') } - }, []) // fetchInvoices is a stable module-level import; effect runs once on mount + } - return { invoices, isLoading, error } + return { + invoices, + isLoading, + error, + mutationError, + refreshInvoices: loadInvoices, + addInvoice, + editInvoice, + removeInvoice, + } } diff --git a/react-app/src/pages/Invoices.tsx b/react-app/src/pages/Invoices.tsx index 8ec484c..c2eea65 100644 --- a/react-app/src/pages/Invoices.tsx +++ b/react-app/src/pages/Invoices.tsx @@ -1,19 +1,62 @@ import { MagnifyingGlass } from '@phosphor-icons/react' +import type { FormEvent } from 'react' +import { useState } from 'react' import InvoiceTable from '../components/invoices/InvoiceTable' import PageHeader from '../components/PageHeader' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card' import { useInvoices } from '../hooks/useInvoices' import { useInvoiceFilters } from '../hooks/useInvoiceFilters' -import type { Invoice } from '../types/invoice' +import type { CreateInvoiceInput, Invoice, InvoiceStatus } from '../types/invoice' + +type InvoiceFormState = { + amount: string + client: string + dueDate: string + status: InvoiceStatus +} type InvoicesContentProps = { error: string | null isLoading: boolean invoices: Invoice[] filteredInvoices: Invoice[] + onDeleteInvoice: (id: number) => void + onEditInvoice: (invoice: Invoice) => void } -function InvoicesContent({ error, isLoading, invoices, filteredInvoices }: InvoicesContentProps) { +const emptyForm: InvoiceFormState = { + amount: '', + client: '', + dueDate: '', + status: 'pending', +} + +function toFormState(invoice: Invoice): InvoiceFormState { + return { + amount: String(invoice.amount), + client: invoice.client, + dueDate: invoice.dueDate, + status: invoice.status, + } +} + +function toInvoiceInput(form: InvoiceFormState): CreateInvoiceInput { + return { + amount: Number(form.amount), + client: form.client.trim(), + dueDate: form.dueDate, + status: form.status, + } +} + +function InvoicesContent({ + error, + isLoading, + invoices, + filteredInvoices, + onDeleteInvoice, + onEditInvoice, +}: InvoicesContentProps) { if (error && invoices.length === 0) { return

{error}

} @@ -23,20 +66,164 @@ function InvoicesContent({ error, isLoading, invoices, filteredInvoices }: Invoi if (filteredInvoices.length === 0) { return

No invoices match your filters.

} - return + return ( + + ) } function Invoices() { - const { invoices, isLoading, error } = useInvoices() + const { invoices, isLoading, error, mutationError, addInvoice, editInvoice, removeInvoice } = + useInvoices() const { searchValue, setSearchValue, statusFilter, setStatusFilter, filteredInvoices } = useInvoiceFilters(invoices) + const [form, setForm] = useState(emptyForm) + const [editingInvoice, setEditingInvoice] = useState(null) const paidCount = invoices.filter((inv) => inv.status === 'paid').length const overdueCount = invoices.filter((inv) => inv.status === 'overdue').length + const handleSubmit = async (event: FormEvent) => { + event.preventDefault() + const invoiceInput = toInvoiceInput(form) + + if (editingInvoice) { + await editInvoice(editingInvoice.id, invoiceInput) + setEditingInvoice(null) + } else { + await addInvoice(invoiceInput) + } + + setForm(emptyForm) + } + + const handleDelete = (id: number) => { + if (editingInvoice?.id === id) { + setEditingInvoice(null) + setForm(emptyForm) + } + removeInvoice(id) + } + + const handleEdit = (invoice: Invoice) => { + setEditingInvoice(invoice) + setForm(toFormState(invoice)) + } + + const handleCancelEdit = () => { + setEditingInvoice(null) + setForm(emptyForm) + } + return (
+ + + {editingInvoice ? `Edit invoice #${editingInvoice.id}` : 'Add invoice'} + + Enter client, amount, status, and due date for invoice tracking. + + + +
+
+ + + setForm((current) => ({ ...current, client: event.target.value })) + } + placeholder="Client name" + /> +
+
+ + + setForm((current) => ({ ...current, amount: event.target.value })) + } + placeholder="0.00" + /> +
+
+ + + setForm((current) => ({ ...current, dueDate: event.target.value })) + } + /> +
+
+ + +
+
+ + {editingInvoice ? ( + + ) : null} + {mutationError ? ( +

+ {mutationError} +

+ ) : null} +
+ +
+
@@ -96,6 +283,8 @@ function Invoices() { isLoading={isLoading} invoices={invoices} filteredInvoices={filteredInvoices} + onDeleteInvoice={handleDelete} + onEditInvoice={handleEdit} /> diff --git a/react-app/src/services/invoices.ts b/react-app/src/services/invoices.ts index 7df3553..15329ae 100644 --- a/react-app/src/services/invoices.ts +++ b/react-app/src/services/invoices.ts @@ -1,5 +1,5 @@ import apiClient from './http' -import type { Invoice } from '../types/invoice' +import type { CreateInvoiceInput, Invoice, UpdateInvoiceInput } from '../types/invoice' export const fetchInvoices = async (): Promise => { const response = await apiClient.get('/invoices') @@ -10,3 +10,20 @@ export const fetchInvoiceById = async (id: number): Promise => { const response = await apiClient.get(`/invoices/${id}`) return response.data } + +export const createInvoice = async (invoice: CreateInvoiceInput): Promise => { + const response = await apiClient.post('/invoices', invoice) + return response.data +} + +export const updateInvoice = async ( + id: number, + invoice: UpdateInvoiceInput, +): Promise => { + const response = await apiClient.put(`/invoices/${id}`, invoice) + return response.data +} + +export const deleteInvoice = async (id: number): Promise => { + await apiClient.delete(`/invoices/${id}`) +} diff --git a/react-app/src/types/invoice.ts b/react-app/src/types/invoice.ts index bd98d5e..1e84508 100644 --- a/react-app/src/types/invoice.ts +++ b/react-app/src/types/invoice.ts @@ -7,3 +7,7 @@ export type Invoice = { status: InvoiceStatus dueDate: string } + +export type CreateInvoiceInput = Omit + +export type UpdateInvoiceInput = Partial