Files
zkxw/web-admin/src/utils/qc-work-order-groups.ts
T
2026-08-18 14:40:15 +08:00

249 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}