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:
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
|
||||
}
|
||||
Reference in new issue
Block a user