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
23 changes: 22 additions & 1 deletion react-app/src/components/invoices/InvoiceTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Table data-testid="invoice-table">
<TableHeader>
Expand All @@ -24,6 +26,7 @@ function InvoiceTable({ invoices }: InvoiceTableProps) {
<TableHead className="hidden md:table-cell">Status</TableHead>
<TableHead className="hidden lg:table-cell">Due Date</TableHead>
<TableHead className="text-right">Amount</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
Expand All @@ -44,6 +47,24 @@ function InvoiceTable({ invoices }: InvoiceTableProps) {
<TableCell className="text-right font-heading font-medium text-primary">
{formatCurrency(invoice.amount)}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<button
className="rounded-sm border border-border px-2.5 py-1 text-xs font-semibold text-secondary transition-colors hover:border-primary hover:text-primary"
type="button"
onClick={() => onEditInvoice?.(invoice)}
>
Edit
</button>
<button
className="rounded-sm border border-danger-text/30 px-2.5 py-1 text-xs font-semibold text-danger-text transition-colors hover:bg-danger-text hover:text-white"
type="button"
onClick={() => onDeleteInvoice?.(invoice.id)}
>
Delete
</button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
Expand Down
99 changes: 80 additions & 19 deletions react-app/src/hooks/useInvoices.ts
Original file line number Diff line number Diff line change
@@ -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<Invoice[]>(mockInvoices)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [mutationError, setMutationError] = useState<string | null>(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,
}
}
Loading
Loading