Files
zkxw/web-admin/src/utils/qc-work-order-card-meta.ts
T
zlwuandCursor ae248961cc feat(web-admin): temporary QC external target code UI
Add source toggle in create/edit dialog, API types, and list/detail display.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 10:41:38 +08:00

432 lines
14 KiB
TypeScript

import { formatDisplayOrDash, formatDateOnly } from '@/utils/datetime-display'
import type { QcWorkOrder, QcProtocolPaymentSummary } from '@/api/qc-work-order'
import {
donationStatusDisplayLabel,
donationStatusTagType,
protocolStatusDisplayLabel,
protocolStatusTagType
} from '@/utils/status-tag-registry'
import type { TagType } from '@/utils/status-tag-registry'
import type { QcWorkOrderTabKey } from '@/utils/qc-work-order-tabs'
import { getQcWorkOrderTab, resolveTabKeyFromWorkOrder } from '@/utils/qc-work-order-tabs'
import { formatMoneyDisplay } from '@/utils/money-format'
import {
countConfiguredSamplesInProtocolGroup,
isTemporaryQcFromProduction,
type QcWorkOrderProtocolGroupModel
} from '@/utils/qc-work-order-groups'
export type CardMetaFieldKind = 'text' | 'tag'
export type CardMetaField = {
label: string
kind?: CardMetaFieldKind
getValue: (row: QcWorkOrder) => string
getTagType?: (row: QcWorkOrder) => TagType | undefined
visible?: (row: QcWorkOrder) => boolean
/** 占满整行 */
wide?: boolean
/** 跨列数(如 2 表示占两列,宽于默认但不独占一行) */
colSpan?: number
}
export type QcWorkOrderCardMetaConfig = {
tabKey: QcWorkOrderTabKey
listLayout: 'inbound-grouped' | 'outbound-grouped' | 'standalone'
showCategoryTag: boolean
showTriggerTag: boolean
showProtocolFeeSummary: boolean
showOutboundFeeSummary: boolean
/** 卡片展示计划质检订单细胞产品 / 生产过程质检细胞产品 */
showCellProducts: boolean
metaFields: CardMetaField[]
buildGroupContextParts: (row: QcWorkOrder) => string[]
allowEditTemporary: boolean
allowEditRemark: boolean
allowDelete: boolean
}
function formatDetectMethods(row: QcWorkOrder) {
const parts: string[] = []
if (row.hasLab) parts.push('实验室')
if (row.hasThirdParty) parts.push('第三方')
return parts.length ? parts.join(' / ') : '—'
}
function formatQcTarget(row: QcWorkOrder) {
if ((row.triggerSource || 'reception') === 'production') {
const parts = [row.cellProductCode, row.cellProductName].filter(Boolean)
return parts.length ? parts.join(' · ') : '—'
}
return '原始样本'
}
function formatOutboundScheduleSortNo(row: QcWorkOrder) {
const n = row.sortNo
if (n == null || n === '' || Number(n) <= 0) return '—'
return `第${Number(n)}次`
}
function formatOutboundScheduleTotalCount(row: QcWorkOrder) {
const n = row.totalCourseCount
if (n == null || n === '' || Number(n) <= 0) return '—'
return `共${Number(n)}次`
}
function formatTempCellProductField(row: QcWorkOrder, value?: string | null) {
if (row.workOrderType !== 'temporary') return '—'
return String(value || '').trim() || '—'
}
function isInboundUserInfoRow(row: QcWorkOrder) {
const source = (row.receptionSource || '').trim()
return source === 'cell_storage' || source === 'research_donation'
}
function protocolStatusLabelBySource(status: string, source: string, voidType?: string): string {
return source === 'research_donation'
? donationStatusDisplayLabel(status, voidType)
: protocolStatusDisplayLabel(status, voidType)
}
function protocolStatusTagTypeBySource(status: string, source: string): TagType {
return source === 'research_donation'
? donationStatusTagType(status)
: protocolStatusTagType(status)
}
const commonTailFields: CardMetaField[] = [
{ label: '检测方式', getValue: formatDetectMethods },
{ label: '创建时间', getValue: (row) => formatDisplayOrDash(row.createTime) },
{
label: '备注',
getValue: (row) => row.remark || '—',
visible: (row) => !!row.remark,
wide: true
}
]
const inboundProtocolStatusField: CardMetaField = {
label: '协议状态',
kind: 'tag',
getValue: (row) =>
protocolStatusLabelBySource(
String(row.protocolStatus || ''),
row.receptionSource || 'cell_storage',
String(row.voidType || '')
),
getTagType: (row) =>
protocolStatusTagTypeBySource(
String(row.protocolStatus || ''),
row.receptionSource || 'cell_storage'
),
visible: isInboundUserInfoRow
}
const inboundGroupedFields: CardMetaField[] = [
{ label: '母亲姓名', getValue: (row) => row.motherName || '—', visible: isInboundUserInfoRow },
{ label: '新生儿姓名', getValue: (row) => row.newbornName || '—', visible: isInboundUserInfoRow },
inboundProtocolStatusField,
{ label: '质检对象', getValue: formatQcTarget, colSpan: 2 },
{
label: '关联生产工单',
getValue: (row) => row.productionWorkOrderNo || '—',
visible: (row) => (row.triggerSource || 'reception') === 'production'
},
{ label: '样本编码', getValue: (row) => row.sampleCode || '—', visible: isInboundUserInfoRow },
{
label: '样本类型',
getValue: (row) => row.sampleTypeName || '—',
visible: isInboundUserInfoRow
},
...commonTailFields
]
const outboundPilotClinicalFields: CardMetaField[] = [
{ label: '申请单号', getValue: (row) => row.applicationNo || '—' },
{ label: '申请单名称', getValue: (row) => row.applicationName || '—' },
{ label: '渠道名称', getValue: (row) => row.channelName || '—' },
{ label: '患者姓名', getValue: (row) => row.patientName || '—' },
{ label: '第几次', getValue: formatOutboundScheduleSortNo },
{ label: '总次数', getValue: formatOutboundScheduleTotalCount },
{ label: '输注时间', getValue: (row) => formatDisplayOrDash(row.infusionTime) },
...commonTailFields
]
const outboundBasicExperimentFields: CardMetaField[] = [
{ label: '申请单号', getValue: (row) => row.applicationNo || '—' },
{ label: '课题名称', getValue: (row) => row.projectTitle || row.applicationName || '—' },
{ label: '客户名称', getValue: (row) => row.customerName || '—' },
{ label: '交付时间', getValue: (row) => formatDisplayOrDash(row.infusionTime) },
{
label: '售卖金额',
getValue: (row) => {
const amount = row.feeSummary?.saleAmount
if (amount == null || amount === '') return '—'
return `¥${formatMoneyDisplay(amount)}`
}
},
...commonTailFields
]
const temporaryFields: CardMetaField[] = [
{
label: '关联生产工单',
getValue: (row) => row.productionWorkOrderNo || '—',
visible: (row) => isTemporaryQcFromProduction(row)
},
{
label: '质检对象',
getValue: (row) => String(row.externalTargetCode || '').trim() || '—',
visible: (row) => !!(row.externalTargetCode || '').trim()
},
{
label: '产品名称',
getValue: (row) => formatTempCellProductField(row, row.cellProductName)
},
{
label: '产品类型',
getValue: (row) => formatTempCellProductField(row, row.cellProductTypeName)
},
...commonTailFields
]
function buildCellStorageGroupContext(row: QcWorkOrder): string[] {
const parts: string[] = []
if (row.motherName) parts.push(`母亲 ${row.motherName}`)
if (row.newbornName) parts.push(`新生儿 ${row.newbornName}`)
if (row.sampleTypeName) parts.push(row.sampleTypeName)
if (row.receptionTime) parts.push(`接收 ${row.receptionTime}`)
if (row.handlerName) parts.push(row.handlerName)
return parts
}
function buildDonationGroupContext(row: QcWorkOrder): string[] {
const parts: string[] = []
if (row.motherName) parts.push(`母亲 ${row.motherName}`)
if (row.newbornName) parts.push(`新生儿 ${row.newbornName}`)
if (row.sampleTypeName) parts.push(row.sampleTypeName)
if (row.receptionTime) parts.push(`接收 ${row.receptionTime}`)
if (row.handlerName) parts.push(row.handlerName)
return parts
}
export function buildProtocolGroupProgressLabel(group: QcWorkOrderProtocolGroupModel): string {
const { configured, total } = countConfiguredSamplesInProtocolGroup(group)
if (total <= 0) return ''
if (configured <= 0) return ''
return `${configured}/${total} 已配置`
}
export function resolveProtocolGroupStatus(row: QcWorkOrder): {
label: string
tagType: TagType
} {
const status = String(row.protocolStatus || '').trim()
const source = row.receptionSource || 'cell_storage'
if (!status) {
return { label: '—', tagType: 'info' }
}
return {
label: protocolStatusLabelBySource(status, source, String(row.voidType || '')),
tagType: protocolStatusTagTypeBySource(status, source)
}
}
/** 协议组级费用摘要:取组内首条有 paymentSummary 的 cell_storage 工单 */
export function resolveProtocolGroupPaymentSummary(
group: QcWorkOrderProtocolGroupModel
): QcProtocolPaymentSummary | null {
if (group.receptionSource !== 'cell_storage') return null
for (const wo of group.workOrders) {
if (wo.paymentSummary) return wo.paymentSummary
}
return null
}
const outboundGroupedFields: CardMetaField[] = [
{ label: '质检对象', getValue: formatQcTarget, colSpan: 2 },
{
label: '关联生产工单',
getValue: (row) => row.productionWorkOrderNo || '—',
visible: (row) => (row.triggerSource || 'reception') === 'production'
},
...commonTailFields
]
function buildOutboundPilotGroupContext(row: QcWorkOrder): string[] {
const parts: string[] = []
if (row.applicationName) parts.push(row.applicationName)
if (row.channelName) parts.push(row.channelName)
if (row.patientName) parts.push(row.patientName)
if (row.infusionTime) parts.push(`输注 ${row.infusionTime}`)
return parts
}
function buildOutboundBasicExperimentGroupContext(row: QcWorkOrder): string[] {
const parts: string[] = []
const title = row.projectTitle || row.applicationName
if (title) parts.push(title)
if (row.customerName) parts.push(row.customerName)
if (row.infusionTime) parts.push(`交付 ${row.infusionTime}`)
return parts
}
const CARD_META_BY_TAB: Record<QcWorkOrderTabKey, QcWorkOrderCardMetaConfig> = {
all: {
tabKey: 'all',
listLayout: 'standalone',
showCategoryTag: true,
showTriggerTag: true,
showProtocolFeeSummary: false,
showOutboundFeeSummary: false,
showCellProducts: false,
metaFields: [],
buildGroupContextParts: () => [],
allowEditTemporary: false,
allowEditRemark: false,
allowDelete: false
},
inbound_cell_storage: {
tabKey: 'inbound_cell_storage',
listLayout: 'inbound-grouped',
showCategoryTag: false,
showTriggerTag: true,
showProtocolFeeSummary: true,
showOutboundFeeSummary: false,
showCellProducts: false,
metaFields: inboundGroupedFields,
buildGroupContextParts: buildCellStorageGroupContext,
allowEditTemporary: false,
allowEditRemark: true,
allowDelete: true
},
inbound_research_donation: {
tabKey: 'inbound_research_donation',
listLayout: 'inbound-grouped',
showCategoryTag: false,
showTriggerTag: true,
showProtocolFeeSummary: false,
showOutboundFeeSummary: false,
showCellProducts: false,
metaFields: inboundGroupedFields,
buildGroupContextParts: buildDonationGroupContext,
allowEditTemporary: false,
allowEditRemark: true,
allowDelete: true
},
outbound_pilot_service: {
tabKey: 'outbound_pilot_service',
listLayout: 'outbound-grouped',
showCategoryTag: false,
showTriggerTag: true,
showProtocolFeeSummary: false,
showOutboundFeeSummary: false,
showCellProducts: true,
metaFields: outboundGroupedFields,
buildGroupContextParts: buildOutboundPilotGroupContext,
allowEditTemporary: false,
allowEditRemark: true,
allowDelete: false
},
outbound_basic_experiment: {
tabKey: 'outbound_basic_experiment',
listLayout: 'outbound-grouped',
showCategoryTag: false,
showTriggerTag: true,
showProtocolFeeSummary: false,
showOutboundFeeSummary: true,
showCellProducts: true,
metaFields: outboundGroupedFields,
buildGroupContextParts: buildOutboundBasicExperimentGroupContext,
allowEditTemporary: false,
allowEditRemark: true,
allowDelete: false
},
outbound_clinical_application: {
tabKey: 'outbound_clinical_application',
listLayout: 'standalone',
showCategoryTag: false,
showTriggerTag: false,
showProtocolFeeSummary: false,
showOutboundFeeSummary: true,
showCellProducts: false,
metaFields: outboundPilotClinicalFields,
buildGroupContextParts: () => [],
allowEditTemporary: false,
allowEditRemark: true,
allowDelete: false
},
temporary: {
tabKey: 'temporary',
listLayout: 'standalone',
showCategoryTag: false,
showTriggerTag: true,
showProtocolFeeSummary: false,
showOutboundFeeSummary: false,
showCellProducts: false,
metaFields: temporaryFields,
buildGroupContextParts: () => [],
allowEditTemporary: true,
allowEditRemark: false,
allowDelete: true
}
}
export function getCardMetaConfig(tabKey: QcWorkOrderTabKey): QcWorkOrderCardMetaConfig {
return CARD_META_BY_TAB[tabKey]
}
export function resolveCardMetaForRow(
row: QcWorkOrder,
tabKey: QcWorkOrderTabKey
): QcWorkOrderCardMetaConfig {
if (tabKey !== 'all') return getCardMetaConfig(tabKey)
const rowTabKey = resolveTabKeyFromWorkOrder(row)
const rowMeta = getCardMetaConfig(rowTabKey)
return {
...rowMeta,
tabKey: 'all',
showCategoryTag: true
}
}
export function buildGroupContextSubtitle(row: QcWorkOrder, tabKey: QcWorkOrderTabKey): string {
const parts = getCardMetaConfig(tabKey).buildGroupContextParts(row)
return parts.length ? parts.join(' · ') : '—'
}
/** 入库分组列表 · 工单卡片主标题(统一展示实验室入库编码) */
export function resolveInboundWorkOrderCardTitle(row: QcWorkOrder): string {
return String(row.labStorageCode || '').trim() || '—'
}
/** 临时质检 · 工单卡片主标题(外部编号优先,否则细胞产品编码) */
export function resolveTemporaryWorkOrderCardTitle(row: QcWorkOrder): string {
const external = String(row.externalTargetCode || '').trim()
if (external) return external
return formatTempCellProductField(row, row.cellProductCode)
}
/** 入库样本组 · 协议组头 / 样本 Tab / 组头主标题 */
export function resolveInboundSampleGroupCardTitle(workOrders: QcWorkOrder[]): string {
for (const row of workOrders) {
const code = String(row.labStorageCode || '').trim()
if (code) return code
}
return '—'
}
export function cardMetaUsesInboundGroups(tabKey: QcWorkOrderTabKey): boolean {
return getQcWorkOrderTab(tabKey)?.listLayout === 'inbound-grouped'
}
export function cardMetaUsesOutboundGroups(tabKey: QcWorkOrderTabKey): boolean {
return getQcWorkOrderTab(tabKey)?.listLayout === 'outbound-grouped'
}
export function cardMetaUsesGroupedList(tabKey: QcWorkOrderTabKey): boolean {
const layout = getQcWorkOrderTab(tabKey)?.listLayout
return layout === 'inbound-grouped' || layout === 'outbound-grouped'
}