|
|
import { useState } from 'react'
|
|
|
import { useNavigate } from 'react-router-dom'
|
|
|
import { formatINR } from '@sims/domain'
|
|
|
import { Badge, DataTable, EmptyState, ErrorState, FilterChips, PageHeader, Skeleton, Toolbar } from '@sims/ui'
|
|
|
import { getDocuments, DOC_STATUSES, type DocStatus, type DocType } from '../api'
|
|
|
import { useData, DOC_TONE } from './Clients'
|
|
|
|
|
|
const PAGE_SIZE = 50
|
|
|
|
|
|
const TYPE_LABEL: Record<DocType, string> = {
|
|
|
QUOTATION: 'Quotation', PROFORMA: 'Proforma', INVOICE: 'Invoice',
|
|
|
CREDIT_NOTE: 'Credit note', RECEIPT: 'Receipt',
|
|
|
}
|
|
|
const TYPE_CHIPS: { key: string; label: string }[] = [
|
|
|
{ key: 'all', label: 'All' },
|
|
|
{ key: 'QUOTATION', label: 'Quotations' },
|
|
|
{ key: 'PROFORMA', label: 'Proformas' },
|
|
|
{ key: 'INVOICE', label: 'Invoices' },
|
|
|
{ key: 'CREDIT_NOTE', label: 'Credit notes' },
|
|
|
{ key: 'RECEIPT', label: 'Receipts' },
|
|
|
]
|
|
|
|
|
|
/**
|
|
|
* Documents register (go-live cluster WS-D): every document across all clients,
|
|
|
* newest first, server-paginated with an honest total (rule 8). Type chips and
|
|
|
* the status filter compose server-side; a row opens the document view.
|
|
|
*/
|
|
|
export function Documents() {
|
|
|
const nav = useNavigate()
|
|
|
const [type, setType] = useState('all')
|
|
|
const [status, setStatus] = useState('')
|
|
|
const [page, setPage] = useState(1)
|
|
|
|
|
|
const list = useData(
|
|
|
() => getDocuments({
|
|
|
...(type !== 'all' ? { type: type as DocType } : {}),
|
|
|
...(status !== '' ? { status: status as DocStatus } : {}),
|
|
|
page, pageSize: PAGE_SIZE,
|
|
|
}),
|
|
|
[type, status, page],
|
|
|
)
|
|
|
|
|
|
const data = list.data
|
|
|
const docs = data?.documents
|
|
|
const totalPages = data !== undefined ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1
|
|
|
const from = data !== undefined && data.total > 0 ? (data.page - 1) * data.pageSize + 1 : 0
|
|
|
const to = data !== undefined ? Math.min(data.page * data.pageSize, data.total) : 0
|
|
|
const why = [
|
|
|
type !== 'all' ? ` of type ${TYPE_LABEL[type as DocType].toLowerCase()}` : '',
|
|
|
status !== '' ? ` with status ${status}` : '',
|
|
|
].filter((x) => x !== '').join('')
|
|
|
|
|
|
return (
|
|
|
<div className="wf-page">
|
|
|
<PageHeader
|
|
|
title="Documents"
|
|
|
desc="Every quotation, proforma, invoice, credit note and receipt — newest first. Click a row to open it."
|
|
|
/>
|
|
|
<Toolbar>
|
|
|
<FilterChips
|
|
|
chips={TYPE_CHIPS}
|
|
|
active={type}
|
|
|
onChange={(k) => { setType(k); setPage(1) }}
|
|
|
/>
|
|
|
<select
|
|
|
className="wf" style={{ maxWidth: 180 }} value={status} aria-label="Status"
|
|
|
onChange={(e) => { setStatus(e.target.value); setPage(1) }}
|
|
|
>
|
|
|
<option value="">Status: any</option>
|
|
|
{DOC_STATUSES.map((s) => <option key={s} value={s}>{s}</option>)}
|
|
|
</select>
|
|
|
<span style={{ flex: 1 }} />
|
|
|
{data !== undefined && <Badge>{data.total} total</Badge>}
|
|
|
</Toolbar>
|
|
|
{list.error !== undefined && <ErrorState message={list.error} onRetry={list.reload} />}
|
|
|
{list.error !== undefined ? null
|
|
|
: docs === undefined ? <Skeleton rows={6} />
|
|
|
: docs.length === 0 ? <EmptyState>No documents{why !== '' ? why : ' yet'}.</EmptyState> : (
|
|
|
<>
|
|
|
<DataTable
|
|
|
columns={[
|
|
|
{ key: 'no', label: 'No.', mono: true },
|
|
|
{ key: 'type', label: 'Type' },
|
|
|
{ key: 'client', label: 'Client' },
|
|
|
{ key: 'date', label: 'Date' },
|
|
|
{ key: 'status', label: 'Status' },
|
|
|
{ key: 'amount', label: 'Amount', numeric: true },
|
|
|
]}
|
|
|
onRowClick={(_row, i) => nav(`/documents/${docs[i]!.id}`)}
|
|
|
rows={docs.map((d) => ({
|
|
|
no: d.docNo ?? '— draft —',
|
|
|
type: TYPE_LABEL[d.docType],
|
|
|
client: d.clientName,
|
|
|
date: d.docDate,
|
|
|
status: <Badge tone={DOC_TONE[d.status]}>{d.status}</Badge>,
|
|
|
amount: formatINR(d.payablePaise),
|
|
|
}))}
|
|
|
/>
|
|
|
<Toolbar>
|
|
|
<Badge>Showing {from}–{to} of {data!.total}</Badge>
|
|
|
<span style={{ flex: 1 }} />
|
|
|
<button
|
|
|
type="button" className="wf" disabled={data!.page <= 1}
|
|
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
|
|
>
|
|
|
‹ Prev
|
|
|
</button>
|
|
|
<span style={{ fontSize: 12, opacity: 0.7 }}>Page {data!.page} / {totalPages}</span>
|
|
|
<button
|
|
|
type="button" className="wf" disabled={data!.page >= totalPages}
|
|
|
onClick={() => setPage((p) => p + 1)}
|
|
|
>
|
|
|
Next ›
|
|
|
</button>
|
|
|
</Toolbar>
|
|
|
</>
|
|
|
)}
|
|
|
</div>
|
|
|
)
|
|
|
}
|