feat(hq-web): document view record header + composer line cards

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 5 days ago
parent 08bb25abfc
commit 94019980bb

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom' import { Link, useNavigate, useParams } from 'react-router-dom'
import { formatINR } from '@sims/domain' import { formatINR } from '@sims/domain'
import { Badge, Button, DataTable, EmptyState, Notice, PageHeader, Toolbar } from '@sims/ui' import { Badge, Button, DataTable, EmptyState, ErrorState, Notice, RecordHeader, Skeleton, Toolbar } from '@sims/ui'
import { import {
createShare, documentAction, downloadDocumentPdf, fetchPdfBlob, getClient, getDocumentFull, createShare, documentAction, downloadDocumentPdf, fetchPdfBlob, getClient, getDocumentFull,
revokeShare, type Client, type Doc, type Share, revokeShare, type Client, type Doc, type Share,
@ -56,8 +56,8 @@ export function DocumentView() {
} }
}, [id, doc?.docNo, doc?.status]) }, [id, doc?.docNo, doc?.status])
if (full.error !== undefined) return <Notice tone="err">{full.error}</Notice> if (full.error !== undefined) return <div className="wf-page"><ErrorState message={full.error} onRetry={full.reload} /></div>
if (doc === undefined || full.data === undefined) return <EmptyState>Loading</EmptyState> if (doc === undefined || full.data === undefined) return <div className="wf-page"><Skeleton rows={6} /></div>
/** POST an action; convert/credit-note navigate to the new document. */ /** POST an action; convert/credit-note navigate to the new document. */
const act = (action: string, body?: Record<string, unknown>, navigateToResult = false) => { const act = (action: string, body?: Record<string, unknown>, navigateToResult = false) => {
@ -87,11 +87,23 @@ export function DocumentView() {
return ( return (
<div className="wf-page"> <div className="wf-page">
<PageHeader <div className="wf-crumbs">
title={`${TITLE[doc.docType]} ${doc.docNo ?? '(draft)'}`} <Link to="/clients">Clients</Link><span>/</span>
badge={doc.fy} {client.data !== undefined ? <Link to={`/clients/${doc.clientId}`}>{client.data.name}</Link> : <span></span>}
desc={`${client.data?.name ?? '…'} · ${doc.docDate} · Payable ${formatINR(doc.payablePaise)}`} <span>/</span><span className="mono">{doc.docNo ?? 'draft'}</span>
actions={<Badge tone={DOC_TONE[doc.status]}>{doc.status}</Badge>} </div>
<RecordHeader
name={TITLE[doc.docType]}
avatarSeed={client.data?.name ?? doc.clientId}
status={<Badge tone={DOC_TONE[doc.status]}>{doc.status}</Badge>}
meta={
<>
<span className="mono">{doc.docNo ?? '(draft)'}</span>
<span>· {doc.fy}</span>
<span>· {doc.docDate}</span>
<span>· Payable <strong className="num">{formatINR(doc.payablePaise)}</strong></span>
</>
}
/> />
<Toolbar> <Toolbar>
@ -160,6 +172,8 @@ export function DocumentView() {
/> />
)} )}
<div className="dash-grid">
<section>
<h3 style={{ marginTop: 20 }}>Timeline</h3> <h3 style={{ marginTop: 20 }}>Timeline</h3>
{full.data.events.length === 0 ? <EmptyState>No events.</EmptyState> : ( {full.data.events.length === 0 ? <EmptyState>No events.</EmptyState> : (
<DataTable <DataTable
@ -173,7 +187,9 @@ export function DocumentView() {
}))} }))}
/> />
)} )}
</section>
<section>
<h3 style={{ marginTop: 20 }}>Email log</h3> <h3 style={{ marginTop: 20 }}>Email log</h3>
{full.data.emails.length === 0 ? <EmptyState>Not emailed yet.</EmptyState> : ( {full.data.emails.length === 0 ? <EmptyState>Not emailed yet.</EmptyState> : (
<DataTable <DataTable
@ -190,6 +206,8 @@ export function DocumentView() {
}))} }))}
/> />
)} )}
</section>
</div>
</div> </div>
) )
} }

@ -169,7 +169,7 @@ export function NewDocument() {
<div style={{ position: 'relative', maxWidth: 340 }}> <div style={{ position: 'relative', maxWidth: 340 }}>
<input className="wf" placeholder="Type to search clients…" value={q} autoFocus onChange={(e) => setQ(e.target.value)} /> <input className="wf" placeholder="Type to search clients…" value={q} autoFocus onChange={(e) => setQ(e.target.value)} />
{hits.length > 0 && ( {hits.length > 0 && (
<div style={{ position: 'absolute', zIndex: 5, top: '100%', left: 0, right: 0, background: 'var(--bg-raised)', border: '1px solid var(--border)', borderRadius: 8 }}> <div style={{ position: 'absolute', zIndex: 5, top: '100%', left: 0, right: 0, background: 'var(--bg-raised)', border: '1px solid var(--border)', borderRadius: 'var(--radius)', boxShadow: 'var(--shadow-lg)' }}>
{hits.slice(0, 8).map((c) => ( {hits.slice(0, 8).map((c) => (
<button key={c.id} type="button" className="wf" style={{ display: 'block', width: '100%', textAlign: 'left', border: 'none' }} <button key={c.id} type="button" className="wf" style={{ display: 'block', width: '100%', textAlign: 'left', border: 'none' }}
onClick={() => { setClient(c); setHits([]); commit() }}> onClick={() => { setClient(c); setHits([]); commit() }}>
@ -200,7 +200,7 @@ export function NewDocument() {
const editions = editionsOf(prices) const editions = editionsOf(prices)
const priceGap = warnings.some((w) => mod !== undefined && w.includes(mod.code)) const priceGap = warnings.some((w) => mod !== undefined && w.includes(mod.code))
return ( return (
<div key={i} style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'flex-end', marginBottom: 8 }}> <div key={i} className="wf-card" style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'flex-end' }}>
<Field label="Module"> <Field label="Module">
<select className="wf" value={l.moduleId} <select className="wf" value={l.moduleId}
onChange={(e) => { onChange={(e) => {

Loading…
Cancel
Save