feat(web-admin): add protocol-audit sample-row flatten helpers.

Support Excel-aligned list mode for cell-storage protocol audit.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-09 09:56:42 +08:00
1 parent 4904508d9f
commit 0da75fb855
2 files changed
+233

No files matched your search

@@ -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'])
})
})
@@ -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<string, unknown> | null {
return v && typeof v === 'object' ? (v as Record<string, unknown>) : 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<string, unknown>[] {
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<string>()
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
}