This commit is contained in:
zlwu committed 2026-07-06 16:59:53 +08:00
1 parent 973176d508
commit cdaa0ac273
327 files changed
+32861 -25627

No files matched your search

+25
View File
@@ -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)}`
}
+2 -2
View File
@@ -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'
)
}
+7 -2
View File
@@ -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
}
+16 -9
View File
@@ -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]
}
+7 -12
View File
@@ -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,
+14 -2
View File
@@ -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
)
// ══════════════════════════════════════════════════════════════════════════════
// 入库放行审核域