import type { QcWorkOrder } from '@/api/qc-work-order' import { getQcWorkOrderTab, resolveTabKeyFromWorkOrder } from '@/utils/qc-work-order-tabs' export type QcWorkOrderSampleGroupModel = { groupKey: string /** 入库:样本编码;出库:申请单号等分组主标题 */ sampleCode: string groupTitle?: string workOrders: QcWorkOrder[] } function triggerSortWeight(row: QcWorkOrder) { const ts = row.triggerSource || 'reception' if (ts === 'production') return 1 return 0 } function sortWorkOrdersInGroup(rows: QcWorkOrder[]) { return [...rows].sort((a, b) => { const byTrigger = triggerSortWeight(a) - triggerSortWeight(b) if (byTrigger !== 0) return byTrigger return String(a.createTime || '').localeCompare(String(b.createTime || '')) }) } function sortWorkOrdersInInboundGroup(rows: QcWorkOrder[]) { return [...rows].sort((a, b) => String(b.createTime || '').localeCompare(String(a.createTime || '')) ) } /** 入库 Tab:按样本编码分组(前端分组,分页仍按工单条数) */ export function groupInboundQcWorkOrders(rows: QcWorkOrder[]): QcWorkOrderSampleGroupModel[] { const map = new Map() for (const row of rows) { const key = (row.sampleCode || '').trim() || row.id const list = map.get(key) if (list) list.push(row) else map.set(key, [row]) } return Array.from(map.entries()).map(([groupKey, workOrders]) => { const sorted = sortWorkOrdersInInboundGroup(workOrders) return { groupKey, sampleCode: sorted[0]?.sampleCode || '', workOrders: sorted } }) } /** 出库 Tab:按输注计划分组 */ export function groupOutboundQcWorkOrders(rows: QcWorkOrder[]): QcWorkOrderSampleGroupModel[] { const map = new Map() for (const row of rows) { const key = (row.infusionScheduleId || '').trim() || row.id const list = map.get(key) if (list) list.push(row) else map.set(key, [row]) } return Array.from(map.entries()).map(([groupKey, workOrders]) => { const sorted = sortWorkOrdersInGroup(workOrders) const head = sorted[0] const groupTitle = (head?.applicationNo || head?.workOrderNo || groupKey).trim() return { groupKey, sampleCode: groupTitle, groupTitle, workOrders: sorted } }) } export function triggerSourceTabLabel(row: QcWorkOrder) { if (row.triggerSourceLabel) return row.triggerSourceLabel const ts = row.triggerSource || 'reception' if (ts === 'production') return '生产过程' if (ts === 'schedule') return '计划质检' return '样本接收' } export function isGateTriggerSource(source?: string | null) { const ts = (source || 'reception').trim() return ts === 'reception' || ts === 'schedule' } /** 临时质检:是否由生产工单派发 */ export function isTemporaryQcFromProduction(row: QcWorkOrder) { return (row.triggerSource || '') === 'production' || !!(row.productionWorkOrderId || '').trim() } export function temporaryQcSourceLabel(row: QcWorkOrder) { return isTemporaryQcFromProduction(row) ? '生产派发' : '手动创建' } export function temporaryQcSourceTagType(row: QcWorkOrder): 'warning' | 'info' { return isTemporaryQcFromProduction(row) ? 'warning' : 'info' } export function matchesTemporaryQcSourceFilter(row: QcWorkOrder, filterValue?: string) { if (!filterValue) return true if (filterValue === 'production') return isTemporaryQcFromProduction(row) if (filterValue === 'manual') return !isTemporaryQcFromProduction(row) return (row.triggerSource || '') === filterValue } export function resolveInboundProtocolGroupKey(row: QcWorkOrder): string { const id = (row.collectionProtocolId || '').trim() if (id) return id const no = (row.protocolNo || '').trim() if (no) return `protocol-no:${no}` return row.id } export function resolveInboundSampleGroupKey(row: QcWorkOrder): string { return (row.sampleCode || '').trim() || row.id } /** 样本组内用于组头/Tab 状态的 gate 工单(接收优先,否则首条) */ export function resolveGateWorkOrderForSampleGroup( workOrders: QcWorkOrder[] ): QcWorkOrder | undefined { if (!workOrders.length) return undefined return workOrders.find((w) => isGateTriggerSource(w.triggerSource)) || workOrders[0] } export type QcWorkOrderProtocolGroupModel = { groupKey: string protocolId: string protocolNo: string receptionSource: 'cell_storage' | 'research_donation' sampleGroups: QcWorkOrderSampleGroupModel[] workOrders: QcWorkOrder[] } function protocolGroupSortTime(group: QcWorkOrderProtocolGroupModel): string { let max = '' for (const wo of group.workOrders) { const t = String(wo.createTime || '') if (t > max) max = t } return max } /** 入库 Tab:按协议单分组,组内再按样本分组 */ export function groupInboundQcWorkOrdersByProtocol( rows: QcWorkOrder[] ): QcWorkOrderProtocolGroupModel[] { const map = new Map() for (const row of rows) { const key = resolveInboundProtocolGroupKey(row) const list = map.get(key) if (list) list.push(row) else map.set(key, [row]) } const groups = Array.from(map.entries()).map(([groupKey, workOrders]) => { const sampleGroups = groupInboundQcWorkOrders(workOrders) const head = workOrders[0] const source = (head?.receptionSource || 'cell_storage') === 'research_donation' ? 'research_donation' : 'cell_storage' return { groupKey, protocolId: (head?.collectionProtocolId || '').trim(), protocolNo: (head?.protocolNo || '').trim(), receptionSource: source as 'cell_storage' | 'research_donation', sampleGroups, workOrders } }) return groups.sort((a, b) => protocolGroupSortTime(b).localeCompare(protocolGroupSortTime(a))) } /** 协议组进度:已配置样本数 / 样本总数(按 gate 工单 workOrderStatus !== dpz) */ export function countConfiguredSamplesInProtocolGroup(group: QcWorkOrderProtocolGroupModel): { configured: number total: number } { const total = group.sampleGroups.length let configured = 0 for (const sg of group.sampleGroups) { const gate = resolveGateWorkOrderForSampleGroup(sg.workOrders) if (gate && gate.workOrderStatus !== 'dpz') configured += 1 } return { configured, total } } export type QcWorkOrderAllTabListItem = | { kind: 'sample-group'; group: QcWorkOrderSampleGroupModel } | { kind: 'standalone'; row: QcWorkOrder } type AllTabPendingGroup = { layout: 'inbound-grouped' | 'outbound-grouped' workOrders: QcWorkOrder[] firstIndex: number } /** 「所有」Tab:按行类型选用分组/单列布局,同页内合并同样本/同输注计划 */ export function buildAllTabListItems(rows: QcWorkOrder[]): QcWorkOrderAllTabListItem[] { const pendingInbound = new Map() const pendingOutbound = new Map() const slots: { firstIndex: number; item: QcWorkOrderAllTabListItem }[] = [] rows.forEach((row, index) => { const rowTabKey = resolveTabKeyFromWorkOrder(row) const layout = getQcWorkOrderTab(rowTabKey)?.listLayout if (layout === 'inbound-grouped') { const key = (row.sampleCode || '').trim() || row.id const existing = pendingInbound.get(key) if (existing) { existing.workOrders.push(row) } else { pendingInbound.set(key, { layout: 'inbound-grouped', workOrders: [row], firstIndex: index }) } return } if (layout === 'outbound-grouped') { const key = (row.infusionScheduleId || '').trim() || row.id const existing = pendingOutbound.get(key) if (existing) { existing.workOrders.push(row) } else { pendingOutbound.set(key, { layout: 'outbound-grouped', workOrders: [row], firstIndex: index }) } return } slots.push({ firstIndex: index, item: { kind: 'standalone', row } }) }) for (const pending of pendingInbound.values()) { const [group] = groupInboundQcWorkOrders(pending.workOrders) if (group) { slots.push({ firstIndex: pending.firstIndex, item: { kind: 'sample-group', group } }) } } for (const pending of pendingOutbound.values()) { const [group] = groupOutboundQcWorkOrders(pending.workOrders) if (group) { slots.push({ firstIndex: pending.firstIndex, item: { kind: 'sample-group', group } }) } } return slots.sort((a, b) => a.firstIndex - b.firstIndex).map((slot) => slot.item) }