diff --git a/web-admin/src/utils/protocol-audit-flatten.test.ts b/web-admin/src/utils/protocol-audit-flatten.test.ts new file mode 100644 index 00000000..5dbe0706 --- /dev/null +++ b/web-admin/src/utils/protocol-audit-flatten.test.ts @@ -0,0 +1,98 @@ +// web-admin/src/utils/protocol-audit-flatten.test.ts +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { + flattenProtocolAuditRows, + extractCollectDate, + uniqueProtocolsFromFlatRows, + firstPayTimeLines +} from './protocol-audit-flatten' + +describe('extractCollectDate', () => { + it('takes YYYY-MM-DD from datetime', () => { + assert.equal(extractCollectDate('2025-09-15 08:51:00'), '2025-09-15') + assert.equal(extractCollectDate('2025-09-15T08:51:00'), '2025-09-15') + }) + it('empty → empty', () => { + assert.equal(extractCollectDate(''), '') + assert.equal(extractCollectDate(undefined), '') + }) +}) + +describe('flattenProtocolAuditRows', () => { + it('one protocol two samples → two rows, both co-stored', () => { + const rows = flattenProtocolAuditRows([ + { + id: 'p1', + protocolNo: 'ZKZX1', + motherName: '郭梦朵', + sampleCollectTime: '2025-09-15 08:51:00', + sampleItems: [ + { sampleTypeName: '脐带血', sampleCode: 'A1', labStorageCode: 'B0000004' }, + { sampleTypeName: '胎盘', sampleCode: 'A2', labStorageCode: 'B0000004A' } + ] + } + ]) + assert.equal(rows.length, 2) + assert.equal(rows[0].isCoStored, true) + assert.equal(rows[1].isCoStored, true) + assert.equal(rows[0].collectDate, '2025-09-15') + assert.equal(rows[0].labStorageCode, 'B0000004') + assert.equal(rows[1].labStorageCode, 'B0000004A') + assert.equal(rows[0].protocolId, 'p1') + }) + + it('zero samples → one placeholder row, not co-stored', () => { + const rows = flattenProtocolAuditRows([{ id: 'p2', protocolNo: 'ZKZX2', sampleItems: [] }]) + assert.equal(rows.length, 1) + assert.equal(rows[0].isCoStored, false) + assert.equal(rows[0].labStorageCode, '') + }) + + it('one sample → not co-stored', () => { + const rows = flattenProtocolAuditRows([ + { + id: 'p3', + sampleItems: [{ sampleTypeName: '脐带血', sampleCode: 'C1', labStorageCode: 'B7' }] + } + ]) + assert.equal(rows.length, 1) + assert.equal(rows[0].isCoStored, false) + }) +}) + +describe('uniqueProtocolsFromFlatRows', () => { + it('dedupes by protocolId keeping first protocol ref', () => { + const flat = flattenProtocolAuditRows([ + { + id: 'p1', + motherName: 'A', + sampleItems: [ + { sampleCode: '1', labStorageCode: 'B1' }, + { sampleCode: '2', labStorageCode: 'B2' } + ] + } + ]) + const uniq = uniqueProtocolsFromFlatRows(flat) + assert.equal(uniq.length, 1) + assert.equal(String(uniq[0].id), 'p1') + }) +}) + +describe('firstPayTimeLines', () => { + it('prefers transferTime then dueDate; stacks multiple', () => { + const lines = firstPayTimeLines({ + paymentDetails: [ + { installmentNo: 1, transferTime: '2025-09-15 09:20:00', installmentDueDate: '2025-09-01' }, + { installmentNo: 2, transferTime: '2025-10-15 10:05:00' } + ] + }) + assert.deepEqual(lines, ['2025-09-15 09:20:00', '2025-10-15 10:05:00']) + }) + it('falls back to dueDate when no transferTime', () => { + const lines = firstPayTimeLines({ + paymentDetails: [{ installmentNo: 1, installmentDueDate: '2025-09-01' }] + }) + assert.deepEqual(lines, ['2025-09-01']) + }) +}) diff --git a/web-admin/src/utils/protocol-audit-flatten.ts b/web-admin/src/utils/protocol-audit-flatten.ts new file mode 100644 index 00000000..1cd3a996 --- /dev/null +++ b/web-admin/src/utils/protocol-audit-flatten.ts @@ -0,0 +1,135 @@ +import type { CollectionProtocolRow } from '../api/collection-protocol' +import type { QcProtocolPaymentSummary } from '../api/qc-work-order' + +export type ProtocolAuditSampleTransport = { + transportType?: string + destSiteName?: string + expressCompanyName?: string + waybillNo?: string + carrierName?: string + carrierPhone?: string + etaTime?: string +} + +export type ProtocolAuditFlatRow = { + rowKey: string + protocolId: string + protocol: CollectionProtocolRow + sampleCount: number + sampleIndex: number + isCoStored: boolean + collectDate: string + sampleCollectTime: string + motherName: string + protocolNo: string + regionName: string + hospitalName: string + salesUserName: string + regionManagerUserName: string + sampleTypeName: string + sampleCode: string + labStorageCode: string + transportType: string + transportLabel: string + paymentSummary: QcProtocolPaymentSummary | null +} + +function asRecord(v: unknown): Record | null { + return v && typeof v === 'object' ? (v as Record) : null +} + +function str(v: unknown): string { + return v == null ? '' : String(v).trim() +} + +export function extractCollectDate(raw?: string | null): string { + const s = str(raw) + if (!s) return '' + const m = s.match(/^(\d{4}-\d{2}-\d{2})/) + return m ? m[1] : '' +} + +function readSampleItems(protocol: CollectionProtocolRow): Record[] { + const raw = protocol.sampleItems + if (Array.isArray(raw) && raw.length) { + return raw.map((x) => asRecord(x) || {}) + } + return [] +} + +export function flattenProtocolAuditRows( + list: CollectionProtocolRow[] +): ProtocolAuditFlatRow[] { + const out: ProtocolAuditFlatRow[] = [] + for (const protocol of list || []) { + const protocolId = str(protocol.id) + const items = readSampleItems(protocol) + const sampleCount = items.length + const isCoStored = sampleCount >= 2 + const slots = sampleCount > 0 ? items : [null] + slots.forEach((item, sampleIndex) => { + const rec = item || {} + const transport = asRecord(rec.transport) + const transportType = str(transport?.transportType) + out.push({ + rowKey: `${protocolId}|${str(rec.sampleCode) || sampleIndex}`, + protocolId, + protocol, + sampleCount, + sampleIndex, + isCoStored, + collectDate: extractCollectDate(str(protocol.sampleCollectTime)), + sampleCollectTime: str(protocol.sampleCollectTime), + motherName: str(protocol.motherName), + protocolNo: str(protocol.protocolNo), + regionName: str(protocol.regionName), + hospitalName: str(protocol.hospitalName), + salesUserName: str(protocol.salesUserName), + regionManagerUserName: str(protocol.regionManagerUserName), + sampleTypeName: str(rec.sampleTypeName), + sampleCode: str(rec.sampleCode), + labStorageCode: str(rec.labStorageCode), + transportType, + transportLabel: '', + paymentSummary: + protocol.paymentSummary && typeof protocol.paymentSummary === 'object' + ? (protocol.paymentSummary as QcProtocolPaymentSummary) + : null + }) + }) + } + return out +} + +export function uniqueProtocolsFromFlatRows( + rows: ProtocolAuditFlatRow[] +): CollectionProtocolRow[] { + const seen = new Set() + const out: CollectionProtocolRow[] = [] + for (const r of rows) { + if (!r.protocolId || seen.has(r.protocolId)) continue + seen.add(r.protocolId) + out.push(r.protocol) + } + return out +} + +export function firstPayTimeLines( + summary: QcProtocolPaymentSummary | null | undefined +): string[] { + const details = summary?.paymentDetails + if (!Array.isArray(details) || !details.length) return [] + const sorted = [...details].sort((a, b) => { + const na = a.installmentNo == null ? 10_000 : Number(a.installmentNo) + const nb = b.installmentNo == null ? 10_000 : Number(b.installmentNo) + return na - nb + }) + const lines: string[] = [] + for (const d of sorted) { + const t = str((d as { transferTime?: string }).transferTime) + const due = str(d.installmentDueDate) + const pick = t || due + if (pick) lines.push(pick) + } + return lines +}