249 lines
8.4 KiB
TypeScript
249 lines
8.4 KiB
TypeScript
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<string, QcWorkOrder[]>()
|
||
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<string, QcWorkOrder[]>()
|
||
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<string, QcWorkOrder[]>()
|
||
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<string, AllTabPendingGroup>()
|
||
const pendingOutbound = new Map<string, AllTabPendingGroup>()
|
||
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)
|
||
}
|