import type { Column } from './components'; import type { ConfirmState } from './ConfirmDialog'; import type { Item } from './shared'; import { useState } from 'react'; import { Filter, MoreHorizontal, Package, Plus, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { EmptyState, ErrorState, Pagination, SearchBox, SectionShell, SimpleTable } from './components'; import { ConfirmDialog } from './ConfirmDialog'; import { formatDate, formatMoney, parseMoneyMinor } from './format'; import { PAGE_SIZE } from './shared'; import { useListFilters } from './useInvoicesSection'; import { useCurrency, useLookups, useResourceList, useResourceMutations } from './useInvoiceShelfData'; // Items — the reusable line-item catalogue. The shortest screen here, and the only list with no detail pane: // an item is four fields, so clicking one opens the editor directly rather than a preview of itself. type Props = { onEdit: (id: number | 'new') => void }; export const ItemsListView = ({ onEdit }: Props) => { const { filters, patch, setPage, clearFilters } = useListFilters(); const currency = useCurrency(); const { units } = useLookups(); const [showFilters, setShowFilters] = useState(false); const [priceInput, setPriceInput] = useState(''); const [selection, setSelection] = useState>(new Set()); const [confirm, setConfirm] = useState(null); const { rows, meta, isLoading, error } = useResourceList('items', { page: filters.page, limit: PAGE_SIZE, search: filters.search || undefined, unit_id: filters.customerId || undefined, // Upstream matches price EXACTLY, in minor units — it is a lookup, not a range. `1000` finds items // priced at 10.00 and nothing else. price: filters.status || undefined, orderByField: 'created_at', orderBy: 'desc', }); const { remove, removeMany } = useResourceMutations('items'); const toggleRow = (id: number) => setSelection((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); const allChecked = rows.length > 0 && rows.every((r) => selection.has(r.id)); const columns: Column[] = [ { key: 'check', label: '', width: '36px', render: (row) => ( e.stopPropagation()} className="flex items-center"> toggleRow(row.id)} /> ), }, { key: 'name', label: 'Name', width: 'minmax(150px, 2fr)', render: (row) => (
{row.name}
{row.description &&
{row.description}
}
), }, { key: 'unit', label: 'Unit', width: 'minmax(80px, 0.8fr)', render: (row) => row.unit?.name ?? '—', }, { key: 'price', label: 'Price', width: 'minmax(100px, 1fr)', align: 'right', render: (row) => formatMoney(row.price, currency), }, { key: 'created_at', label: 'Added', width: 'minmax(90px, 0.9fr)', render: (row) => formatDate(null, row.formatted_created_at), }, ]; const rowMenu = (row: Item) => ( onEdit(row.id)}>Edit setConfirm({ title: `Delete ${row.name}?`, description: 'Lines already written from this item stay as they are — an invoice keeps its own copy of the name and price.', confirmLabel: 'Delete', destructive: true, onConfirm: () => remove.mutate(row.id), }) } > Delete ); if (error) return ; const applyPrice = () => { const trimmed = priceInput.trim(); patch({ status: trimmed ? String(parseMoneyMinor(trimmed, currency)) : null }); }; const toolbar = ( <> setSelection(allChecked ? new Set() : new Set(rows.map((r) => r.id)))} /> patch({ q: v })} placeholder="Item name…" />
{selection.size > 0 && ( )}
); return ( <> }> {showFilters && (
{filters.isFiltered && ( )}
)} r.id} onRowClick={(r) => onEdit(r.id)} rowMenu={rowMenu} empty={ isLoading ? ( ) : ( ) } />
setConfirm(null)} /> ); };