You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/apps/hq-web/src/pages/Documents.tsx

121 lines
4.5 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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>
)
}