提交
This commit is contained in:
1 parent
973176d508
commit
cdaa0ac273
327 files changed
+32861
-25627
No files matched your search
@@ -146,3 +146,28 @@ export function productionStepAssigneeText(row: Record<string, any>): string {
|
||||
export function productionStepPlannedStartText(row: Record<string, any>): string {
|
||||
return formatPlannedStartDate(row.plannedStartTime)
|
||||
}
|
||||
|
||||
/** 中文日期展示:2026-07-01 → 2026年7月1日 */
|
||||
export function formatChineseDateYmd(ymd: string): string {
|
||||
const matched = String(ymd || '').trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/)
|
||||
if (!matched) return ymd
|
||||
return `${matched[1]}年${Number(matched[2])}月${Number(matched[3])}日`
|
||||
}
|
||||
|
||||
/** 汇总工单下全部生产任务工艺步骤的计划开始时间区间 */
|
||||
export function computeWorkOrderPlannedStartTimeRange(
|
||||
tasks: Record<string, any>[] | undefined
|
||||
): string {
|
||||
const dates: string[] = []
|
||||
for (const task of tasks || []) {
|
||||
for (const step of Array.isArray(task.steps) ? task.steps : []) {
|
||||
const date = formatPlannedStartDate(step.plannedStartTime)
|
||||
if (date) dates.push(date)
|
||||
}
|
||||
}
|
||||
if (!dates.length) return ''
|
||||
dates.sort()
|
||||
const min = dates[0]
|
||||
const max = dates[dates.length - 1]
|
||||
return `${formatChineseDateYmd(min)} ~ ${formatChineseDateYmd(max)}`
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export async function refreshProductionMenuBadges() {
|
||||
.catch(() =>
|
||||
badgeStore.setBadge(PRODUCTION_MENU_CODE_WORK_ORDER, {
|
||||
overdueIncomplete: 0,
|
||||
unassigned: 0
|
||||
futureIncomplete: 0
|
||||
})
|
||||
)
|
||||
)
|
||||
@@ -87,7 +87,7 @@ export async function refreshProductionMenuBadges() {
|
||||
.catch(() =>
|
||||
badgeStore.setBadge(PRODUCTION_MENU_CODE_MY_TASK, {
|
||||
overdueIncomplete: 0,
|
||||
unassigned: 0
|
||||
futureIncomplete: 0
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,33 +4,30 @@ import { fetchMyProductionTasks } from '@/api/production-task'
|
||||
import type { ProductionWorkOrderTabKey } from '@/utils/production-work-order-tabs'
|
||||
import {
|
||||
resolveTabKeyFromWorkOrder,
|
||||
tabUsesInfusionTimeFilter,
|
||||
getProductionWorkOrderTab,
|
||||
toWorkOrderListQuery,
|
||||
isAllProductionWorkOrderTab
|
||||
} from '@/utils/production-work-order-tabs'
|
||||
import {
|
||||
toAllTabListQuery,
|
||||
workOrderMatchesCategory
|
||||
} from '@/utils/production-work-order-category'
|
||||
import { toAllTabListQuery, workOrderMatchesCategory } from '@/utils/production-work-order-category'
|
||||
import { matchesWorkOrderStatusFilter } from '@/utils/status-tag-registry'
|
||||
import { computeWorkOrderPlannedStartTimeRange } from '@/utils/production-labels'
|
||||
|
||||
export type ProductionTaskMineFilters = {
|
||||
keyword?: string
|
||||
taskStatus?: string
|
||||
productionCategory?: string
|
||||
tabKey: ProductionWorkOrderTabKey
|
||||
excludeVoidProtocol?: boolean
|
||||
plannedStartTimeStart?: string
|
||||
plannedStartTimeEnd?: string
|
||||
}
|
||||
|
||||
export type MineWorkOrderSearchFilters = {
|
||||
keyword?: string
|
||||
workOrderStatus?: string | string[]
|
||||
productionCategory?: string
|
||||
createTimeStart?: string
|
||||
createTimeEnd?: string
|
||||
infusionTimeStart?: string
|
||||
infusionTimeEnd?: string
|
||||
tabKey: ProductionWorkOrderTabKey
|
||||
excludeVoidProtocol?: boolean
|
||||
}
|
||||
|
||||
export type ProductionTaskRow = Record<string, any>
|
||||
@@ -44,14 +41,6 @@ export type MineWorkOrderEntry = {
|
||||
|
||||
const MINE_TASK_FETCH_PAGE_SIZE = 100
|
||||
|
||||
function dateInRange(value: string | undefined, start?: string, end?: string): boolean {
|
||||
const v = (value || '').trim().slice(0, 10)
|
||||
if (!v) return !start && !end
|
||||
if (start && v < start) return false
|
||||
if (end && v > end) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function keywordMatchesWorkOrder(
|
||||
row: ProductionWorkOrderItem,
|
||||
keyword: string,
|
||||
@@ -64,7 +53,13 @@ function keywordMatchesWorkOrder(
|
||||
tabKey === 'temporary'
|
||||
? [row.workOrderNo]
|
||||
: tabKey === 'outbound_basic_experiment'
|
||||
? [row.workOrderNo, row.applicationNo, row.projectTitle, row.customerName, row.applicationName]
|
||||
? [
|
||||
row.workOrderNo,
|
||||
row.applicationNo,
|
||||
row.projectTitle,
|
||||
row.customerName,
|
||||
row.applicationName
|
||||
]
|
||||
: tab.group === 'outbound'
|
||||
? [row.workOrderNo, row.applicationNo, row.applicationName, row.patientName]
|
||||
: [row.workOrderNo, row.sampleCode, row.labStorageCode, row.motherName, row.newbornName]
|
||||
@@ -137,7 +132,9 @@ export function groupMineTasksByWorkOrder(tasks: ProductionTaskRow[]): MineWorkO
|
||||
})
|
||||
}
|
||||
|
||||
export function buildPresetTasksMap(entries: MineWorkOrderEntry[]): Record<string, ProductionTaskRow[]> {
|
||||
export function buildPresetTasksMap(
|
||||
entries: MineWorkOrderEntry[]
|
||||
): Record<string, ProductionTaskRow[]> {
|
||||
const map: Record<string, ProductionTaskRow[]> = {}
|
||||
for (const entry of entries) {
|
||||
map[entry.workOrder.id] = entry.tasks
|
||||
@@ -167,7 +164,10 @@ async function fetchMineTasksByScope(
|
||||
workOrderType: listQuery.workOrderType,
|
||||
receptionSource: listQuery.receptionSource,
|
||||
bizType: listQuery.bizType,
|
||||
includeStepSummary: true
|
||||
includeStepSummary: true,
|
||||
excludeVoidProtocol: filters.excludeVoidProtocol,
|
||||
plannedStartTimeStart: filters.plannedStartTimeStart,
|
||||
plannedStartTimeEnd: filters.plannedStartTimeEnd
|
||||
})
|
||||
const list = res.list || []
|
||||
all.push(...list)
|
||||
@@ -229,7 +229,6 @@ export function filterMineEntriesByWorkOrderSearch(
|
||||
filters: MineWorkOrderSearchFilters
|
||||
): MineWorkOrderEntry[] {
|
||||
const tabKey = filters.tabKey
|
||||
const tab = getProductionWorkOrderTab(tabKey)!
|
||||
return entries.filter((entry) => {
|
||||
const row = entry.workOrder
|
||||
if (!isAllProductionWorkOrderTab(tabKey) && resolveTabKeyFromWorkOrder(row) !== tabKey) {
|
||||
@@ -247,13 +246,6 @@ export function filterMineEntriesByWorkOrderSearch(
|
||||
? resolveTabKeyFromWorkOrder(row)
|
||||
: tabKey
|
||||
if (!keywordMatchesWorkOrder(row, filters.keyword || '', keywordTabKey)) return false
|
||||
if (isAllProductionWorkOrderTab(tabKey) || !tabUsesInfusionTimeFilter(tab)) {
|
||||
if (!dateInRange(row.createTime, filters.createTimeStart, filters.createTimeEnd)) {
|
||||
return false
|
||||
}
|
||||
} else if (!dateInRange(row.infusionTime, filters.infusionTimeStart, filters.infusionTimeEnd)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -273,7 +265,10 @@ export function buildMineTableRows(pageEntries: MineWorkOrderEntry[]): {
|
||||
presetTasksByWorkOrderId: Record<string, ProductionTaskRow[]>
|
||||
} {
|
||||
return {
|
||||
rows: pageEntries.map((e) => e.workOrder),
|
||||
rows: pageEntries.map((e) => ({
|
||||
...e.workOrder,
|
||||
plannedStartTimeRange: computeWorkOrderPlannedStartTimeRange(e.tasks)
|
||||
})),
|
||||
presetTasksByWorkOrderId: buildPresetTasksMap(pageEntries)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatDisplayOrDash, formatDateOnly } from '@/utils/datetime-display'
|
||||
import { formatDisplayOrDash } from '@/utils/datetime-display'
|
||||
import type { ProductionWorkOrderItem } from '@/api/production-work-order'
|
||||
import { receptionSourceLabel } from '@/utils/qc-labels'
|
||||
import {
|
||||
@@ -21,6 +21,8 @@ export type CardMetaField = {
|
||||
getTagType?: (row: ProductionWorkOrderItem) => TagType | undefined
|
||||
visible?: (row: ProductionWorkOrderItem) => boolean
|
||||
wide?: boolean
|
||||
/** 跨列数(如 2 表示占两列,宽于默认但不独占一行) */
|
||||
colSpan?: number
|
||||
}
|
||||
|
||||
export type ProductionWorkOrderCardMetaConfig = {
|
||||
@@ -50,6 +52,12 @@ function formatOutboundScheduleTotalCount(row: ProductionWorkOrderItem) {
|
||||
return `共${Number(n)}次`
|
||||
}
|
||||
|
||||
const plannedStartTimeRangeField: CardMetaField = {
|
||||
label: '计划开始时间',
|
||||
colSpan: 3,
|
||||
getValue: (row) => formatDisplayOrDash(row.plannedStartTimeRange)
|
||||
}
|
||||
|
||||
function protocolStatusLabelForTab(
|
||||
row: ProductionWorkOrderItem,
|
||||
tabKey: ProductionWorkOrderTabKey
|
||||
@@ -92,7 +100,7 @@ function buildInboundMetaFields(tabKey: ProductionWorkOrderTabKey): CardMetaFiel
|
||||
{ label: '样本类型', getValue: (row) => row.sampleTypeName || '—' },
|
||||
{ label: '接收经手人', getValue: (row) => row.handlerName || '—' },
|
||||
{ label: '接收时间', getValue: (row) => formatDisplayOrDash(row.receptionTime) },
|
||||
{ label: '创建时间', getValue: (row) => formatDisplayOrDash(row.createTime) },
|
||||
plannedStartTimeRangeField,
|
||||
{ label: '工单号', getValue: (row) => row.workOrderNo || '—' },
|
||||
{ label: '样本编码', getValue: (row) => row.sampleCode || '—' },
|
||||
{ label: '实验室入库编码', getValue: (row) => row.labStorageCode || '—' },
|
||||
@@ -114,7 +122,7 @@ const outboundPilotClinicalFields: CardMetaField[] = [
|
||||
{ label: '第几次', getValue: formatOutboundScheduleSortNo },
|
||||
{ label: '总次数', getValue: formatOutboundScheduleTotalCount },
|
||||
{ label: '输注时间', getValue: (row) => formatDisplayOrDash(row.infusionTime) },
|
||||
{ label: '创建时间', getValue: (row) => formatDisplayOrDash(row.createTime) },
|
||||
plannedStartTimeRangeField,
|
||||
{
|
||||
label: '备注',
|
||||
getValue: (row) => row.remark || '—',
|
||||
@@ -140,7 +148,7 @@ const outboundBasicExperimentFields: CardMetaField[] = [
|
||||
},
|
||||
visible: (row) => row.feeSummary?.saleAmount != null && row.feeSummary?.saleAmount !== ''
|
||||
},
|
||||
{ label: '创建时间', getValue: (row) => formatDisplayOrDash(row.createTime) },
|
||||
plannedStartTimeRangeField,
|
||||
{
|
||||
label: '备注',
|
||||
getValue: (row) => row.remark || '—',
|
||||
@@ -150,8 +158,8 @@ const outboundBasicExperimentFields: CardMetaField[] = [
|
||||
]
|
||||
|
||||
const temporaryFields: CardMetaField[] = [
|
||||
{ label: '备注', getValue: (row) => row.remark || '—' },
|
||||
{ label: '创建时间', getValue: (row) => formatDisplayOrDash(row.createTime) }
|
||||
{ label: '工单号', getValue: (row) => row.workOrderNo || '—' },
|
||||
plannedStartTimeRangeField
|
||||
]
|
||||
|
||||
const CARD_META_BY_TAB: Record<ProductionWorkOrderTabKey, ProductionWorkOrderCardMetaConfig> = {
|
||||
|
||||
@@ -39,15 +39,10 @@ function stepAssigneeUserIds(step: Record<string, any>): string[] {
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function isStepUnassigned(step: Record<string, any>): boolean {
|
||||
return stepAssigneeUserIds(step).length === 0
|
||||
}
|
||||
|
||||
/** 与后端 tab-step-counts 一致:计划开始日期已到期且步骤未完成 */
|
||||
function isStepOverdueIncomplete(step: Record<string, any>): boolean {
|
||||
function isStepFutureIncomplete(step: Record<string, any>): boolean {
|
||||
const planned = (step.plannedStartTime || '').trim().slice(0, 10)
|
||||
if (!planned) return false
|
||||
return planned <= todayYmd()
|
||||
return planned > todayYmd()
|
||||
}
|
||||
|
||||
/** 与后端 mine-tab-step-counts 任务过滤一致 */
|
||||
@@ -62,14 +57,21 @@ function isMineQualifyingTask(task: Record<string, any>, userId: string): boolea
|
||||
return false
|
||||
}
|
||||
|
||||
/** 单张生产工单卡片:工艺步骤到期未完成 / 未安排负责人数(与 Tab 角标语义一致) */
|
||||
/** 与后端 tab-step-counts 一致:计划开始日期已到期且步骤未完成 */
|
||||
function isStepOverdueIncomplete(step: Record<string, any>): boolean {
|
||||
const planned = (step.plannedStartTime || '').trim().slice(0, 10)
|
||||
if (!planned) return false
|
||||
return planned <= todayYmd()
|
||||
}
|
||||
|
||||
/** 单张生产工单卡片:工艺步骤到期未完成 / 计划开始在今日之后未完成(与 Tab 角标语义一致) */
|
||||
export function sumProductionWorkOrderStepBadgeCount(
|
||||
workOrder: ProductionWorkOrderItem,
|
||||
tasks: Record<string, any>[] | undefined,
|
||||
scope: ProductionWorkOrderStepBadgeScope,
|
||||
currentUserId?: string
|
||||
): ProductionWorkOrderTabStepCount {
|
||||
const result: ProductionWorkOrderTabStepCount = { overdueIncomplete: 0, unassigned: 0 }
|
||||
const result: ProductionWorkOrderTabStepCount = { overdueIncomplete: 0, futureIncomplete: 0 }
|
||||
if (!isActiveWorkOrder(workOrder)) return result
|
||||
|
||||
const uid = String(currentUserId || '').trim()
|
||||
@@ -80,7 +82,7 @@ export function sumProductionWorkOrderStepBadgeCount(
|
||||
for (const step of Array.isArray(task.steps) ? task.steps : []) {
|
||||
if (!isActiveStep(step)) continue
|
||||
if (isStepOverdueIncomplete(step)) result.overdueIncomplete += 1
|
||||
if (isStepUnassigned(step)) result.unassigned += 1
|
||||
if (isStepFutureIncomplete(step)) result.futureIncomplete += 1
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -95,13 +97,13 @@ export function isProductionWorkOrderStepBadgeVisible(
|
||||
export function productionWorkOrderStepBadgeTitle(count: ProductionWorkOrderTabStepCount): string {
|
||||
if (isProductionWorkOrderTabStepBadgeZero(count)) return ''
|
||||
const overdue = count.overdueIncomplete || 0
|
||||
const unassigned = count.unassigned || 0
|
||||
const future = count.futureIncomplete || 0
|
||||
const parts: string[] = []
|
||||
if (overdue > 0) {
|
||||
parts.push(`${overdue} 个工艺步骤已到期未完成`)
|
||||
}
|
||||
if (unassigned > 0) {
|
||||
parts.push(`${unassigned} 个工艺步骤未安排负责人`)
|
||||
if (future > 0) {
|
||||
parts.push(`${future} 个工艺步骤计划开始在今日之后`)
|
||||
}
|
||||
return parts.join(',')
|
||||
}
|
||||
@@ -82,7 +82,9 @@ export const PRODUCTION_WORK_ORDER_TABS: ProductionWorkOrderTabConfig[] = [
|
||||
]
|
||||
|
||||
/** 生产工单 / 我的生产任务 Tab 栏展示项(不含 hidden) */
|
||||
export const PRODUCTION_WORK_ORDER_VISIBLE_TABS = PRODUCTION_WORK_ORDER_TABS.filter((t) => !t.hidden)
|
||||
export const PRODUCTION_WORK_ORDER_VISIBLE_TABS = PRODUCTION_WORK_ORDER_TABS.filter(
|
||||
(t) => !t.hidden
|
||||
)
|
||||
|
||||
export const DEFAULT_PRODUCTION_WORK_ORDER_TAB: ProductionWorkOrderTabKey = 'all'
|
||||
|
||||
@@ -133,10 +135,6 @@ export function resolveTabKeyFromWorkOrder(row: {
|
||||
: 'inbound_cell_storage'
|
||||
}
|
||||
|
||||
export function tabUsesInfusionTimeFilter(tab: ProductionWorkOrderTabConfig): boolean {
|
||||
return tab.group === 'outbound'
|
||||
}
|
||||
|
||||
export function keywordPlaceholderForTab(tab: ProductionWorkOrderTabConfig): string {
|
||||
if (tab.key === 'all') return '工单号 / 样本编码 / 申请单号 / 产品名称等'
|
||||
if (tab.key === 'temporary') return '工单号'
|
||||
@@ -147,25 +145,6 @@ export function keywordPlaceholderForTab(tab: ProductionWorkOrderTabConfig): str
|
||||
return '工单号/样本编码/入库编码'
|
||||
}
|
||||
|
||||
export function filterTimeLabelForTab(tab: ProductionWorkOrderTabConfig): string {
|
||||
if (tab.key === 'all') return '创建时间'
|
||||
if (tab.key === 'outbound_basic_experiment') return '交付时间'
|
||||
if (tab.group === 'outbound') return '输注时间'
|
||||
return '创建时间'
|
||||
}
|
||||
|
||||
export function filterTimeHintForTab(tab: ProductionWorkOrderTabConfig): string {
|
||||
if (tab.key === 'all') {
|
||||
return '出库工单按输注/交付时间筛选请进入对应分类 Tab。'
|
||||
}
|
||||
if (tab.group === 'outbound') {
|
||||
return tab.key === 'outbound_basic_experiment'
|
||||
? '支持按交付时间筛选。'
|
||||
: '支持按输注时间筛选。'
|
||||
}
|
||||
return '支持按创建时间筛选。'
|
||||
}
|
||||
|
||||
export function workOrderMatchesTab(
|
||||
row: ProductionWorkOrderItem,
|
||||
tabKey: ProductionWorkOrderTabKey
|
||||
@@ -185,3 +164,9 @@ export function resolveEffectiveRowTabKey(
|
||||
): ProductionWorkOrderTabKey {
|
||||
return isAllProductionWorkOrderTab(listTabKey) ? resolveTabKeyFromWorkOrder(row) : listTabKey
|
||||
}
|
||||
|
||||
export function tabSupportsInactiveProtocolFilter(tabKey: ProductionWorkOrderTabKey): boolean {
|
||||
return (
|
||||
tabKey === 'all' || tabKey === 'inbound_cell_storage' || tabKey === 'inbound_research_donation'
|
||||
)
|
||||
}
|
||||
@@ -36,6 +36,7 @@ export type MineWorkOrderSearchFilters = {
|
||||
infusionTimeStart?: string
|
||||
infusionTimeEnd?: string
|
||||
tabKey: QcWorkOrderTabKey
|
||||
sampleTypeName?: string
|
||||
}
|
||||
|
||||
export type MineWorkOrderEntry = {
|
||||
@@ -113,6 +114,8 @@ export function filterMineEntriesByWorkOrderSearch(
|
||||
}
|
||||
const keywordTabKey = isAllQcWorkOrderTab(tabKey) ? resolveTabKeyFromWorkOrder(row) : tabKey
|
||||
if (!keywordMatchesWorkOrder(row, filters.keyword || '', keywordTabKey)) return false
|
||||
const sampleTypeName = (filters.sampleTypeName || '').trim()
|
||||
if (sampleTypeName && (row.sampleTypeName || '').trim() !== sampleTypeName) return false
|
||||
if (isAllQcWorkOrderTab(tabKey) || !tabUsesInfusionTimeFilter(tab)) {
|
||||
if (!dateInRange(row.createTime, filters.createTimeStart, filters.createTimeEnd)) {
|
||||
return false
|
||||
@@ -224,8 +227,9 @@ export function buildWorkOrderFromTasks(tasks: QcTask[]): QcWorkOrder {
|
||||
cellProductSpecName: t.cellProductSpecName,
|
||||
sampleTypeName: t.sampleTypeName,
|
||||
isSpecial: tasks.some((x) => x.isSpecial) || !!t.isSpecial,
|
||||
specialRemark:
|
||||
String(tasks.find((x) => x.isSpecial)?.specialRemark || t.specialRemark || '').trim(),
|
||||
specialRemark: String(
|
||||
tasks.find((x) => x.isSpecial)?.specialRemark || t.specialRemark || ''
|
||||
).trim(),
|
||||
receptionTime: t.receptionTime,
|
||||
handlerName: t.handlerName,
|
||||
hasLab: t.hasLab ?? tasks.some((x) => x.taskType === 'sysz'),
|
||||
@@ -255,6 +259,7 @@ export function buildWorkOrderFromTasks(tasks: QcTask[]): QcWorkOrder {
|
||||
protocolStatusLabel: t.protocolStatusLabel,
|
||||
paymentSummary: t.paymentSummary,
|
||||
feeSummary: t.feeSummary,
|
||||
protocolSiblingSamples: t.protocolSiblingSamples,
|
||||
remark: t.workOrderRemark || t.remark,
|
||||
unfinishedBadgeCount: t.unfinishedBadgeCount
|
||||
}
|
||||
|
||||
@@ -133,7 +133,12 @@ const inboundGroupedFields: CardMetaField[] = [
|
||||
getValue: (row) => row.productionWorkOrderNo || '—',
|
||||
visible: (row) => (row.triggerSource || 'reception') === 'production'
|
||||
},
|
||||
{ label: '实验室入库编码', getValue: (row) => row.labStorageCode || '—' },
|
||||
{ label: '样本编码', getValue: (row) => row.sampleCode || '—', visible: isInboundUserInfoRow },
|
||||
{
|
||||
label: '样本类型',
|
||||
getValue: (row) => row.sampleTypeName || '—',
|
||||
visible: isInboundUserInfoRow
|
||||
},
|
||||
...commonTailFields
|
||||
]
|
||||
|
||||
@@ -390,16 +395,18 @@ export function buildGroupContextSubtitle(row: QcWorkOrder, tabKey: QcWorkOrderT
|
||||
return parts.length ? parts.join(' · ') : '—'
|
||||
}
|
||||
|
||||
/** 入库分组列表 · 工单卡片主标题(按触发来源) */
|
||||
/** 入库分组列表 · 工单卡片主标题(统一展示实验室入库编码) */
|
||||
export function resolveInboundWorkOrderCardTitle(row: QcWorkOrder): string {
|
||||
const ts = (row.triggerSource || 'reception').trim()
|
||||
if (ts === 'production') {
|
||||
return String(row.cellProductCode || '').trim() || '—'
|
||||
return String(row.labStorageCode || '').trim() || '—'
|
||||
}
|
||||
|
||||
/** 入库样本组 · 协议组头 / 样本 Tab / 组头主标题 */
|
||||
export function resolveInboundSampleGroupCardTitle(workOrders: QcWorkOrder[]): string {
|
||||
for (const row of workOrders) {
|
||||
const code = String(row.labStorageCode || '').trim()
|
||||
if (code) return code
|
||||
}
|
||||
if (ts === 'reception') {
|
||||
return String(row.labStorageCode || '').trim() || '—'
|
||||
}
|
||||
return String(row.workOrderNo || '').trim() || '—'
|
||||
return '—'
|
||||
}
|
||||
|
||||
export function cardMetaUsesInboundGroups(tabKey: QcWorkOrderTabKey): boolean {
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import type { QcProtocolSiblingSample, QcWorkOrder } from '@/api/qc-work-order'
|
||||
import {
|
||||
groupInboundQcWorkOrders,
|
||||
resolveInboundProtocolGroupKey,
|
||||
resolveGateWorkOrderForSampleGroup,
|
||||
type QcWorkOrderSampleGroupModel
|
||||
} from '@/utils/qc-work-order-groups'
|
||||
import { resolveInboundSampleGroupCardTitle } from '@/utils/qc-work-order-card-meta'
|
||||
|
||||
export type QcInboundProtocolSiblingLink = {
|
||||
anchorId: string
|
||||
labStorageCode: string
|
||||
sampleTypeName: string
|
||||
}
|
||||
|
||||
export type QcInboundSampleListItem = {
|
||||
group: QcWorkOrderSampleGroupModel
|
||||
cardAnchorId: string
|
||||
protocolSiblingLinks: QcInboundProtocolSiblingLink[]
|
||||
showProtocolPaymentAtGroup: boolean
|
||||
}
|
||||
|
||||
export function buildInboundSampleCardAnchorId(groupKey: string) {
|
||||
const safe = String(groupKey || 'sample')
|
||||
.trim()
|
||||
.replace(/[^a-zA-Z0-9_-]+/g, '_')
|
||||
return `qc-inbound-sample-${safe}`
|
||||
}
|
||||
|
||||
function sampleTypeNameForGroup(group: QcWorkOrderSampleGroupModel) {
|
||||
const gate = resolveGateWorkOrderForSampleGroup(group.workOrders)
|
||||
return (gate?.sampleTypeName || group.workOrders[0]?.sampleTypeName || '').trim()
|
||||
}
|
||||
|
||||
function protocolSortTime(group: QcWorkOrderSampleGroupModel): string {
|
||||
let max = ''
|
||||
for (const wo of group.workOrders) {
|
||||
const t = String(wo.createTime || '')
|
||||
if (t > max) max = t
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
function resolveProtocolSiblingSamplesForGroup(
|
||||
group: QcWorkOrderSampleGroupModel
|
||||
): QcProtocolSiblingSample[] {
|
||||
for (const row of group.workOrders) {
|
||||
const samples = row.protocolSiblingSamples
|
||||
if (samples?.length) return samples
|
||||
}
|
||||
const gate = resolveGateWorkOrderForSampleGroup(group.workOrders)
|
||||
return gate?.protocolSiblingSamples || []
|
||||
}
|
||||
|
||||
export function buildProtocolSiblingLinksFromSamples(
|
||||
group: QcWorkOrderSampleGroupModel,
|
||||
samples: QcProtocolSiblingSample[]
|
||||
): QcInboundProtocolSiblingLink[] {
|
||||
const sorted = [...samples].sort((a, b) =>
|
||||
(a.sampleCode || '').localeCompare(b.sampleCode || '')
|
||||
)
|
||||
return sorted.map((sample) => {
|
||||
const sampleCode = (sample.sampleCode || '').trim() || group.groupKey
|
||||
const labStorageCode = (sample.labStorageCode || '').trim()
|
||||
return {
|
||||
anchorId: buildInboundSampleCardAnchorId(sampleCode),
|
||||
labStorageCode: labStorageCode || '—',
|
||||
sampleTypeName: (sample.sampleTypeName || '').trim()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function buildLinksFromVisibleGroups(
|
||||
protocolSampleGroups: QcWorkOrderSampleGroupModel[]
|
||||
): QcInboundProtocolSiblingLink[] {
|
||||
const sorted = [...protocolSampleGroups].sort((a, b) =>
|
||||
(a.sampleCode || '').localeCompare(b.sampleCode || '')
|
||||
)
|
||||
return sorted.map((group) => ({
|
||||
anchorId: buildInboundSampleCardAnchorId(group.groupKey),
|
||||
labStorageCode: resolveInboundSampleGroupCardTitle(group.workOrders),
|
||||
sampleTypeName: sampleTypeNameForGroup(group)
|
||||
}))
|
||||
}
|
||||
|
||||
/** 样本组标题栏「同协议样本」胶囊(优先 API enrich,回退当前页多样本互链) */
|
||||
export function resolveProtocolSiblingLinksForGroup(
|
||||
group: QcWorkOrderSampleGroupModel
|
||||
): QcInboundProtocolSiblingLink[] {
|
||||
const apiSamples = resolveProtocolSiblingSamplesForGroup(group)
|
||||
if (apiSamples.length > 1) {
|
||||
return buildProtocolSiblingLinksFromSamples(group, apiSamples)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/** 入库 Tab:按样本独立卡片,并生成同协议互链与协议费用展示位 */
|
||||
export function buildInboundSampleListItems(rows: QcWorkOrder[]): QcInboundSampleListItem[] {
|
||||
const groups = groupInboundQcWorkOrders(rows)
|
||||
const protocolGroups = new Map<string, QcWorkOrderSampleGroupModel[]>()
|
||||
|
||||
for (const group of groups) {
|
||||
const head = group.workOrders[0]
|
||||
if (!head) continue
|
||||
const protocolKey = resolveInboundProtocolGroupKey(head)
|
||||
const list = protocolGroups.get(protocolKey)
|
||||
if (list) list.push(group)
|
||||
else protocolGroups.set(protocolKey, [group])
|
||||
}
|
||||
|
||||
const siblingLinksByGroupKey = new Map<string, QcInboundProtocolSiblingLink[]>()
|
||||
const protocolPaymentHostGroupKey = new Set<string>()
|
||||
|
||||
for (const [, protocolSampleGroups] of protocolGroups) {
|
||||
const sorted = [...protocolSampleGroups].sort((a, b) =>
|
||||
(a.sampleCode || '').localeCompare(b.sampleCode || '')
|
||||
)
|
||||
let links: QcInboundProtocolSiblingLink[] = []
|
||||
for (const group of sorted) {
|
||||
const apiSamples = resolveProtocolSiblingSamplesForGroup(group)
|
||||
if (apiSamples.length > 1) {
|
||||
links = buildProtocolSiblingLinksFromSamples(group, apiSamples)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (links.length <= 1 && sorted.length > 1) {
|
||||
links = buildLinksFromVisibleGroups(sorted)
|
||||
}
|
||||
if (links.length > 1) {
|
||||
for (const group of sorted) {
|
||||
siblingLinksByGroupKey.set(group.groupKey, links)
|
||||
}
|
||||
}
|
||||
const paymentHost = sorted.find((group) =>
|
||||
group.workOrders.some((wo) => wo.receptionSource === 'cell_storage' && !!wo.paymentSummary)
|
||||
)
|
||||
if (paymentHost) {
|
||||
protocolPaymentHostGroupKey.add(paymentHost.groupKey)
|
||||
}
|
||||
}
|
||||
|
||||
const sortedGroups = [...groups].sort((a, b) => {
|
||||
const headA = a.workOrders[0]
|
||||
const headB = b.workOrders[0]
|
||||
if (!headA || !headB) return 0
|
||||
const protocolA = resolveInboundProtocolGroupKey(headA)
|
||||
const protocolB = resolveInboundProtocolGroupKey(headB)
|
||||
if (protocolA !== protocolB) {
|
||||
return protocolSortTime(b).localeCompare(protocolSortTime(a))
|
||||
}
|
||||
return (a.sampleCode || '').localeCompare(b.sampleCode || '')
|
||||
})
|
||||
|
||||
return sortedGroups.map((group) => ({
|
||||
group,
|
||||
cardAnchorId: buildInboundSampleCardAnchorId(group.groupKey),
|
||||
protocolSiblingLinks: siblingLinksByGroupKey.get(group.groupKey) || [],
|
||||
showProtocolPaymentAtGroup: protocolPaymentHostGroupKey.has(group.groupKey)
|
||||
}))
|
||||
}
|
||||
|
||||
export function scrollToInboundSampleCard(anchorId: string) {
|
||||
const el = document.getElementById(anchorId)
|
||||
if (!el) return
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
el.classList.add('qc-sample-row--highlight')
|
||||
window.setTimeout(() => el.classList.remove('qc-sample-row--highlight'), 1600)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { QcWorkOrder } from '@/api/qc-work-order'
|
||||
|
||||
const INBOUND_SAMPLE_SOURCES = new Set(['cell_storage', 'research_donation'])
|
||||
|
||||
function isInboundSampleWorkOrder(row: QcWorkOrder) {
|
||||
if ((row.workOrderType || 'inbound') !== 'inbound') return false
|
||||
return INBOUND_SAMPLE_SOURCES.has((row.receptionSource || '').trim())
|
||||
}
|
||||
|
||||
/** 入库样本组勾选/导出:按 receptionSource + sampleCode 聚合 sibling 工单 */
|
||||
export function resolveInboundSampleSelectionKey(row: QcWorkOrder): string {
|
||||
const sampleCode = (row.sampleCode || '').trim()
|
||||
if (!sampleCode) return row.id
|
||||
const source = (row.receptionSource || 'cell_storage').trim()
|
||||
return `${source}:${sampleCode}`
|
||||
}
|
||||
|
||||
export function collectInboundSampleSiblingIds(
|
||||
workOrderId: string,
|
||||
rows: QcWorkOrder[]
|
||||
): string[] {
|
||||
const row = rows.find((item) => item.id === workOrderId)
|
||||
if (!row || !isInboundSampleWorkOrder(row)) return [workOrderId]
|
||||
|
||||
const key = resolveInboundSampleSelectionKey(row)
|
||||
const siblingIds = rows
|
||||
.filter((item) => isInboundSampleWorkOrder(item) && resolveInboundSampleSelectionKey(item) === key)
|
||||
.map((item) => item.id)
|
||||
|
||||
return siblingIds.length ? siblingIds : [workOrderId]
|
||||
}
|
||||
|
||||
export function expandInboundWorkOrderIdsForExport(
|
||||
selectedIds: string[],
|
||||
rows: QcWorkOrder[]
|
||||
): string[] {
|
||||
const expanded = new Set<string>()
|
||||
for (const id of selectedIds) {
|
||||
for (const siblingId of collectInboundSampleSiblingIds(id, rows)) {
|
||||
expanded.add(siblingId)
|
||||
}
|
||||
}
|
||||
return [...expanded]
|
||||
}
|
||||
@@ -28,7 +28,8 @@ export const QC_WORK_ORDER_TABS: QcWorkOrderTabConfig[] = [
|
||||
key: 'all',
|
||||
label: '所有',
|
||||
group: 'all',
|
||||
listLayout: 'standalone'
|
||||
listLayout: 'standalone',
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
key: 'inbound_cell_storage',
|
||||
@@ -83,7 +84,7 @@ export const QC_WORK_ORDER_TABS: QcWorkOrderTabConfig[] = [
|
||||
/** 质检工单 / 我的质检任务 Tab 栏展示项(不含 hidden) */
|
||||
export const QC_WORK_ORDER_VISIBLE_TABS = QC_WORK_ORDER_TABS.filter((t) => !t.hidden)
|
||||
|
||||
export const DEFAULT_QC_WORK_ORDER_TAB: QcWorkOrderTabKey = 'all'
|
||||
export const DEFAULT_QC_WORK_ORDER_TAB: QcWorkOrderTabKey = 'inbound_cell_storage'
|
||||
|
||||
export function getQcWorkOrderTab(key: string): QcWorkOrderTabConfig | undefined {
|
||||
return QC_WORK_ORDER_TABS.find((t) => t.key === key)
|
||||
@@ -159,16 +160,6 @@ export function filterTimeLabelForTab(tab: QcWorkOrderTabConfig): string {
|
||||
return '创建时间'
|
||||
}
|
||||
|
||||
export function filterTimeHintForTab(tab: QcWorkOrderTabConfig): string {
|
||||
if (tab.key === 'all') {
|
||||
return '出库工单按输注/交付时间筛选请进入对应分类 Tab。'
|
||||
}
|
||||
if (tab.group === 'outbound') {
|
||||
return tab.key === 'outbound_basic_experiment' ? '支持按交付时间筛选。' : '支持按输注时间筛选。'
|
||||
}
|
||||
return '支持按创建时间筛选。'
|
||||
}
|
||||
|
||||
export function workOrderMatchesTab(row: QcWorkOrder, tabKey: QcWorkOrderTabKey): boolean {
|
||||
if (tabKey === 'all') return true
|
||||
return resolveTabKeyFromWorkOrder(row) === tabKey
|
||||
@@ -178,6 +169,10 @@ export function isAllQcWorkOrderTab(tabKey: QcWorkOrderTabKey): boolean {
|
||||
return tabKey === 'all'
|
||||
}
|
||||
|
||||
export function isInboundSampleQcTab(tabKey: QcWorkOrderTabKey): boolean {
|
||||
return tabKey === 'inbound_cell_storage' || tabKey === 'inbound_research_donation'
|
||||
}
|
||||
|
||||
/** 列表 Tab 为「所有」时,按工单解析其分类 Tab Key */
|
||||
export function resolveEffectiveRowTabKey(
|
||||
listTabKey: QcWorkOrderTabKey,
|
||||
|
||||
@@ -40,6 +40,14 @@ export const protocolStatus: StatusRegistry = {
|
||||
sdfq: { label: '属地废弃', tagType: 'danger' },
|
||||
zky: { label: '转科研', tagType: 'primary' }
|
||||
}
|
||||
|
||||
/** 协议/捐赠单无效态(与样本接收、生产工单列表过滤口径一致) */
|
||||
export const INACTIVE_PROTOCOL_STATUSES = ['yzf', 'sdfq'] as const
|
||||
|
||||
export function isInactiveProtocolStatus(status?: string): boolean {
|
||||
const code = (status || '').trim()
|
||||
return (INACTIVE_PROTOCOL_STATUSES as readonly string[]).includes(code)
|
||||
}
|
||||
export const protocolStatusLabel = createLabelGetter(protocolStatus)
|
||||
export const protocolStatusTagType = createTagTypeGetter(protocolStatus)
|
||||
|
||||
@@ -415,8 +423,12 @@ export const productionMaterialUsageSourceType: StatusRegistry = {
|
||||
prep_consume: { label: '配制消耗', tagType: 'info' },
|
||||
prep_in: { label: '配制入库', tagType: 'success' }
|
||||
}
|
||||
export const productionMaterialUsageSourceTypeLabel = createLabelGetter(productionMaterialUsageSourceType)
|
||||
export const productionMaterialUsageSourceTypeTagType = createTagTypeGetter(productionMaterialUsageSourceType)
|
||||
export const productionMaterialUsageSourceTypeLabel = createLabelGetter(
|
||||
productionMaterialUsageSourceType
|
||||
)
|
||||
export const productionMaterialUsageSourceTypeTagType = createTagTypeGetter(
|
||||
productionMaterialUsageSourceType
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// 入库放行审核域
|
||||
|
||||
Reference in new issue
Block a user