Add source toggle in create/edit dialog, API types, and list/detail display. Co-authored-by: Cursor <cursoragent@cursor.com>
432 lines
14 KiB
TypeScript
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'
|
|
}
|