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.
158 lines
7.2 KiB
TypeScript
158 lines
7.2 KiB
TypeScript
import { useState } from 'react'
|
|
import { formatINR } from '@sims/domain'
|
|
import {
|
|
Badge, Button, ConfirmDialog, DataTable, EmptyState, ErrorState, Notice, PageHeader,
|
|
Skeleton, StatCard, Stats, Toolbar, useToast,
|
|
} from '@sims/ui'
|
|
import { commitApexImport, getImportStatus, stageApexCsv } from '../api'
|
|
import { useData } from './Clients'
|
|
|
|
/**
|
|
* APEX cutover importer (D18 WS-B; owner-only). Stage the two CSV exports →
|
|
* every row carries its own problem list → verify the totals → Commit, which the
|
|
* server refuses while ANY problem remains. Re-stage as many times as needed;
|
|
* after the final commit APEX goes read-only (no double entry, spec §4).
|
|
*/
|
|
export function ImportApex() {
|
|
const toast = useToast()
|
|
const status = useData(getImportStatus, [])
|
|
const [clientsCsv, setClientsCsv] = useState<string | undefined>()
|
|
const [invoicesCsv, setInvoicesCsv] = useState<string | undefined>()
|
|
const [busy, setBusy] = useState(false)
|
|
const [confirming, setConfirming] = useState(false)
|
|
const [done, setDone] = useState<string | undefined>()
|
|
|
|
const readFile = (set: (t: string) => void) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0]
|
|
if (file === undefined) return
|
|
const reader = new FileReader()
|
|
reader.onload = () => set(String(reader.result ?? ''))
|
|
reader.onerror = () => toast.err(`Could not read ${file.name}`)
|
|
reader.readAsText(file)
|
|
}
|
|
|
|
const stage = () => {
|
|
if (busy) return
|
|
if (clientsCsv === undefined && invoicesCsv === undefined) { toast.err('Pick at least one CSV first'); return }
|
|
setBusy(true); setDone(undefined)
|
|
stageApexCsv({
|
|
...(clientsCsv !== undefined ? { clientsCsv } : {}),
|
|
...(invoicesCsv !== undefined ? { invoicesCsv } : {}),
|
|
})
|
|
.then(() => { toast.ok('Staged — review the report below'); status.reload() })
|
|
.catch((e: Error) => toast.err(e.message))
|
|
.finally(() => setBusy(false))
|
|
}
|
|
|
|
const commit = () => {
|
|
setBusy(true)
|
|
commitApexImport()
|
|
.then((r) => {
|
|
setDone(`Committed: ${r.clients} clients, ${r.invoices} invoices`
|
|
+ (r.seeded !== null ? ` · INVOICE series seeded past ${r.seeded.fy} #${r.seeded.lastSeq}` : ''))
|
|
toast.ok('Import committed — APEX is now read-only reference')
|
|
status.reload()
|
|
})
|
|
.catch((e: Error) => toast.err(e.message))
|
|
.finally(() => { setBusy(false); setConfirming(false) })
|
|
}
|
|
|
|
const s = status.data
|
|
const problems = (s?.report.clients.problems ?? 0) + (s?.report.invoices.problems ?? 0)
|
|
const staged = (s?.report.clients.staged ?? 0) + (s?.report.invoices.staged ?? 0)
|
|
|
|
return (
|
|
<div className="wf-page">
|
|
<PageHeader
|
|
title="APEX import"
|
|
desc="The one-time 300-client cutover: stage the CSVs, clear every problem, verify, commit. Trial-run freely — only Commit writes real data."
|
|
/>
|
|
|
|
<section>
|
|
<h2>1 · Stage the CSV exports</h2>
|
|
<Toolbar>
|
|
<label className="wf" style={{ display: 'inline-flex', gap: 8, alignItems: 'center' }}>
|
|
clients.csv
|
|
<input type="file" accept=".csv,text/csv" onChange={readFile(setClientsCsv)} />
|
|
{clientsCsv !== undefined && <Badge tone="ok">loaded</Badge>}
|
|
</label>
|
|
<label className="wf" style={{ display: 'inline-flex', gap: 8, alignItems: 'center' }}>
|
|
invoices.csv
|
|
<input type="file" accept=".csv,text/csv" onChange={readFile(setInvoicesCsv)} />
|
|
{invoicesCsv !== undefined && <Badge tone="ok">loaded</Badge>}
|
|
</label>
|
|
<Button tone="primary" onClick={stage}>{busy ? 'Staging…' : 'Stage'}</Button>
|
|
</Toolbar>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>2 · Verification report</h2>
|
|
{status.error !== undefined ? <ErrorState message={status.error} onRetry={status.reload} />
|
|
: s === undefined ? <Skeleton rows={3} />
|
|
: staged === 0 ? <EmptyState>Nothing staged yet.</EmptyState> : (
|
|
<>
|
|
<Stats>
|
|
<StatCard label="Clients staged" value={String(s.report.clients.staged)}
|
|
{...(s.report.clients.problems > 0 ? { hint: `${s.report.clients.problems} with problems`, hintTone: 'err' as const } : { hint: 'clean' })} />
|
|
<StatCard label="Invoices staged" value={String(s.report.invoices.staged)}
|
|
{...(s.report.invoices.problems > 0 ? { hint: `${s.report.invoices.problems} with problems`, hintTone: 'err' as const } : { hint: 'clean' })} />
|
|
<StatCard label="Invoice value" value={formatINR(s.report.invoices.totalPaise)} hint="cross-check vs APEX" />
|
|
</Stats>
|
|
<p style={{ fontSize: 12, opacity: 0.75 }}>
|
|
First clients: {s.report.samples.firstClients.join(' · ') || '—'}<br />
|
|
Last invoices: {s.report.samples.lastInvoices.join(' · ') || '—'}
|
|
</p>
|
|
{s.problemClients.length > 0 && (
|
|
<>
|
|
<h3>Client rows with problems (first 50 of {s.report.clients.problems})</h3>
|
|
<DataTable
|
|
columns={[{ key: 'row', label: 'Row' }, { key: 'code', label: 'Code', mono: true }, { key: 'name', label: 'Name' }, { key: 'problems', label: 'Problems' }]}
|
|
rows={s.problemClients.map((p) => ({
|
|
row: String(p.row_no), code: p.code ?? '—', name: p.name ?? '—',
|
|
problems: (JSON.parse(p.problems) as string[]).join('; '),
|
|
}))}
|
|
/>
|
|
</>
|
|
)}
|
|
{s.problemInvoices.length > 0 && (
|
|
<>
|
|
<h3>Invoice rows with problems (first 50 of {s.report.invoices.problems})</h3>
|
|
<DataTable
|
|
columns={[{ key: 'row', label: 'Row' }, { key: 'client', label: 'Client code', mono: true }, { key: 'docNo', label: 'Doc no', mono: true }, { key: 'problems', label: 'Problems' }]}
|
|
rows={s.problemInvoices.map((p) => ({
|
|
row: String(p.row_no), client: p.client_code ?? '—', docNo: p.doc_no ?? '—',
|
|
problems: (JSON.parse(p.problems) as string[]).join('; '),
|
|
}))}
|
|
/>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
</section>
|
|
|
|
<section>
|
|
<h2>3 · Commit</h2>
|
|
{problems > 0 && <Notice tone="err">{problems} staged row(s) still carry problems — fix the CSVs and re-stage. Commit stays locked.</Notice>}
|
|
{done !== undefined && <Notice tone="ok">{done}</Notice>}
|
|
<Button
|
|
tone="primary"
|
|
onClick={() => { if (staged > 0 && problems === 0 && !busy) setConfirming(true) }}
|
|
>
|
|
{busy ? 'Working…' : `Commit import${staged > 0 ? ` (${staged} rows)` : ''}`}
|
|
</Button>
|
|
{confirming && (
|
|
<ConfirmDialog
|
|
open
|
|
onClose={() => setConfirming(false)}
|
|
onConfirm={commit}
|
|
title="Commit the APEX import"
|
|
body="This writes the staged clients and invoices for real and seeds the INVOICE series past the legacy numbers. After the final cutover, APEX goes read-only — no double entry. Commit?"
|
|
confirmLabel="Commit import"
|
|
tone="danger"
|
|
/>
|
|
)}
|
|
</section>
|
|
</div>
|
|
)
|
|
}
|