提交
This commit is contained in:
1 parent
0b50eff3d8
commit
ee364e24aa
323 files changed
+57731
-35766
No files matched your search
@@ -47,3 +47,36 @@ export function productionTaskTypeForWorkOrder(workOrderType?: string | null): s
|
||||
if (workOrderType === 'outbound') return 'outbound'
|
||||
return 'cell_storage'
|
||||
}
|
||||
|
||||
/** 工艺步骤状态 dzx=待执行 jxz=进行中 ywc=已完成 */
|
||||
export function productionStepStatusTag(status?: string | null): { label: string; type: TagType } {
|
||||
const map: Record<string, { label: string; type: TagType }> = {
|
||||
dzx: { label: '待执行', type: 'info' },
|
||||
jxz: { label: '进行中', type: 'warning' },
|
||||
ywc: { label: '已完成', type: 'success' }
|
||||
}
|
||||
const s = (status || '').trim()
|
||||
return map[s] || { label: s || '—', type: 'info' }
|
||||
}
|
||||
|
||||
export function extractProductionTaskSteps(detail: Record<string, any> | null | undefined) {
|
||||
if (!detail || typeof detail !== 'object') return []
|
||||
const steps = detail.steps ?? detail.taskSteps ?? detail.stepList
|
||||
return Array.isArray(steps) ? steps : []
|
||||
}
|
||||
|
||||
/** 计划开始时间:展示为 YYYY-MM-DD */
|
||||
export function formatPlannedStartDate(raw?: string | null): string {
|
||||
const s = String(raw || '').trim()
|
||||
if (!s) return ''
|
||||
const datePart = s.slice(0, 10)
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(datePart)) return datePart
|
||||
const matched = s.match(/^(\d{4}-\d{2}-\d{2})/)
|
||||
return matched ? matched[1] : ''
|
||||
}
|
||||
|
||||
/** 计划开始时间:表单绑定为 YYYY-MM-DD 或 null */
|
||||
export function normalizePlannedStartDate(raw?: string | null): string | null {
|
||||
const formatted = formatPlannedStartDate(raw)
|
||||
return formatted || null
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
fetchMineProductionWorkOrderTabStepCounts,
|
||||
fetchProductionWorkOrderTabStepCounts,
|
||||
sumProductionWorkOrderTabStepCounts
|
||||
} from '@/api/production-work-order'
|
||||
import {
|
||||
PRODUCTION_MENU_CODE_MY_TASK,
|
||||
PRODUCTION_MENU_CODE_WORK_ORDER,
|
||||
useProductionMenuBadgeStore
|
||||
} from '@/store/modules/production-menu-badge'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
import { PRODUCTION_WORK_ORDER_VISIBLE_TABS } from '@/utils/production-work-order-tabs'
|
||||
|
||||
const VISIBLE_TAB_KEYS = PRODUCTION_WORK_ORDER_VISIBLE_TABS.map((tab) => tab.key)
|
||||
|
||||
/** 解析菜单项对应的生产角标 menuCode(兼容后端菜单未带 menuCode) */
|
||||
export function resolveProductionMenuBadgeCode(item: AppRouteRecord): string | undefined {
|
||||
const code = (item.meta.menuCode || '').trim()
|
||||
if (code === PRODUCTION_MENU_CODE_WORK_ORDER || code === PRODUCTION_MENU_CODE_MY_TASK) {
|
||||
return code
|
||||
}
|
||||
const name = String(item.name || '')
|
||||
if (name === 'ProductionWorkOrder') return PRODUCTION_MENU_CODE_WORK_ORDER
|
||||
if (name === 'ProductionMyProductionTask') return PRODUCTION_MENU_CODE_MY_TASK
|
||||
const path = String(item.path || '')
|
||||
if (path.endsWith('/production-work-order') || path === 'production-work-order') {
|
||||
return PRODUCTION_MENU_CODE_WORK_ORDER
|
||||
}
|
||||
if (path.endsWith('/my-production-task') || path === 'my-production-task') {
|
||||
return PRODUCTION_MENU_CODE_MY_TASK
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 从当前侧栏菜单树收集需展示角标的生产菜单 code */
|
||||
export function collectVisibleProductionMenuBadgeCodes(menuList: AppRouteRecord[]): string[] {
|
||||
const codes = new Set<string>()
|
||||
const walk = (items: AppRouteRecord[]) => {
|
||||
for (const item of items) {
|
||||
const code = resolveProductionMenuBadgeCode(item)
|
||||
if (code) codes.add(code)
|
||||
if (item.children?.length) walk(item.children)
|
||||
}
|
||||
}
|
||||
walk(menuList || [])
|
||||
return [...codes]
|
||||
}
|
||||
|
||||
/** 刷新侧栏「生产工单 / 我的生产任务」Tab 角标汇总 */
|
||||
export async function refreshProductionMenuBadges() {
|
||||
const menuStore = useMenuStore()
|
||||
const badgeStore = useProductionMenuBadgeStore()
|
||||
const visibleCodes = collectVisibleProductionMenuBadgeCodes(menuStore.menuList)
|
||||
|
||||
const tasks: Promise<void>[] = []
|
||||
|
||||
if (visibleCodes.includes(PRODUCTION_MENU_CODE_WORK_ORDER)) {
|
||||
tasks.push(
|
||||
fetchProductionWorkOrderTabStepCounts()
|
||||
.then((data) =>
|
||||
badgeStore.setBadge(
|
||||
PRODUCTION_MENU_CODE_WORK_ORDER,
|
||||
sumProductionWorkOrderTabStepCounts(data, VISIBLE_TAB_KEYS)
|
||||
)
|
||||
)
|
||||
.catch(() =>
|
||||
badgeStore.setBadge(PRODUCTION_MENU_CODE_WORK_ORDER, {
|
||||
overdueIncomplete: 0,
|
||||
unassigned: 0
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (visibleCodes.includes(PRODUCTION_MENU_CODE_MY_TASK)) {
|
||||
tasks.push(
|
||||
fetchMineProductionWorkOrderTabStepCounts()
|
||||
.then((data) =>
|
||||
badgeStore.setBadge(
|
||||
PRODUCTION_MENU_CODE_MY_TASK,
|
||||
sumProductionWorkOrderTabStepCounts(data, VISIBLE_TAB_KEYS)
|
||||
)
|
||||
)
|
||||
.catch(() =>
|
||||
badgeStore.setBadge(PRODUCTION_MENU_CODE_MY_TASK, {
|
||||
overdueIncomplete: 0,
|
||||
unassigned: 0
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (!tasks.length) return
|
||||
|
||||
await Promise.all(tasks)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import request from '@/utils/http'
|
||||
import type { QcTask } from '@/api/qc-task'
|
||||
|
||||
export type QcInspectionBatch = {
|
||||
id: string
|
||||
workOrderId?: string
|
||||
batchNo: string
|
||||
batchName: string
|
||||
orderNum?: number
|
||||
assigneeUserIds: string[]
|
||||
assigneeUserNames?: string
|
||||
hasLab?: boolean
|
||||
hasThirdParty?: boolean
|
||||
labItemIds?: string[]
|
||||
thirdPartyItemIds?: string[]
|
||||
taskCount?: number
|
||||
completedTaskCount?: number
|
||||
tasks?: QcTask[]
|
||||
}
|
||||
|
||||
export type QcInspectionBatchConfigureItem = {
|
||||
id?: string
|
||||
batchName: string
|
||||
assigneeUserIds: string[]
|
||||
hasLab: boolean
|
||||
hasThirdParty: boolean
|
||||
labItemIds: string[]
|
||||
thirdPartyItemIds: string[]
|
||||
}
|
||||
|
||||
export function fetchQcInspectionBatches(workOrderId: string) {
|
||||
return request.get<QcInspectionBatch[]>({
|
||||
url: `/web/admin/qc-work-orders/${workOrderId}/inspection-batches`
|
||||
})
|
||||
}
|
||||
|
||||
/** 优先使用详情内嵌批次,否则独立 GET */
|
||||
export async function loadQcInspectionBatches(
|
||||
workOrderId: string,
|
||||
embedded?: QcInspectionBatch[] | null
|
||||
): Promise<QcInspectionBatch[]> {
|
||||
if (embedded?.length) return embedded
|
||||
if (!workOrderId) return []
|
||||
try {
|
||||
return (await fetchQcInspectionBatches(workOrderId)) || []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
type QcBatchTaskLike = {
|
||||
id?: string
|
||||
batchId?: string
|
||||
taskStatus?: string
|
||||
assigneeUserNames?: string
|
||||
}
|
||||
|
||||
export type QcBatchTaskSection<T extends QcBatchTaskLike = QcBatchTaskLike> = {
|
||||
key: string
|
||||
batchId: string
|
||||
batchNo: string
|
||||
batchName: string
|
||||
assigneeUserNames: string
|
||||
taskCount: number
|
||||
completedTaskCount: number
|
||||
tasks: T[]
|
||||
}
|
||||
|
||||
export function batchTasksPanelSlot(batchKey: string) {
|
||||
return `batch-tasks-${batchKey}`
|
||||
}
|
||||
|
||||
export function buildBatchSectionsFromBatches<T extends QcBatchTaskLike>(
|
||||
batches: QcInspectionBatch[],
|
||||
fallbackTasks?: T[]
|
||||
): QcBatchTaskSection<T>[] {
|
||||
if (batches.length) {
|
||||
return batches.map((b) => ({
|
||||
key: b.id || b.batchNo,
|
||||
batchId: b.id,
|
||||
batchNo: b.batchNo,
|
||||
batchName: b.batchName || b.batchNo || '检测批次',
|
||||
assigneeUserNames: b.assigneeUserNames || '',
|
||||
taskCount: b.taskCount ?? (b.tasks?.length || 0),
|
||||
completedTaskCount: b.completedTaskCount ?? 0,
|
||||
tasks: (b.tasks || []) as T[]
|
||||
}))
|
||||
}
|
||||
if (fallbackTasks?.length) {
|
||||
return buildBatchSectionsFromTasks(fallbackTasks)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function buildBatchSectionsFromTasks<T extends QcBatchTaskLike>(
|
||||
tasks: T[]
|
||||
): QcBatchTaskSection<T>[] {
|
||||
const map = new Map<string, T[]>()
|
||||
const order: string[] = []
|
||||
for (const t of tasks) {
|
||||
const bid = (t.batchId || '').trim() || '__legacy__'
|
||||
if (!map.has(bid)) {
|
||||
map.set(bid, [])
|
||||
order.push(bid)
|
||||
}
|
||||
map.get(bid)!.push(t)
|
||||
}
|
||||
return order.map((bid) => {
|
||||
const group = map.get(bid)!
|
||||
const completed = group.filter((t) => t.taskStatus === 'ywc').length
|
||||
return {
|
||||
key: bid,
|
||||
batchId: bid === '__legacy__' ? '' : bid,
|
||||
batchNo: '',
|
||||
batchName: bid === '__legacy__' ? '历史任务' : '检测批次',
|
||||
assigneeUserNames: group[0]?.assigneeUserNames || '',
|
||||
taskCount: group.length,
|
||||
completedTaskCount: completed,
|
||||
tasks: group
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function resolveBatchSections<T extends QcBatchTaskLike>(
|
||||
batches: QcInspectionBatch[] | undefined | null,
|
||||
tasks: T[] | undefined | null
|
||||
): QcBatchTaskSection<T>[] {
|
||||
const fromBatches = buildBatchSectionsFromBatches(batches || [], tasks || undefined)
|
||||
if (fromBatches.length) return fromBatches
|
||||
if (tasks?.length) return buildBatchSectionsFromTasks(tasks)
|
||||
return []
|
||||
}
|
||||
|
||||
export function createEmptyInspectionBatch(): QcInspectionBatchConfigureItem {
|
||||
const today = new Date()
|
||||
const y = today.getFullYear()
|
||||
const m = String(today.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(today.getDate()).padStart(2, '0')
|
||||
return {
|
||||
batchName: `${y}${m}${d}检测`,
|
||||
assigneeUserIds: [],
|
||||
hasLab: false,
|
||||
hasThirdParty: false,
|
||||
labItemIds: [],
|
||||
thirdPartyItemIds: []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { QcItemParam } from '@/api/qc-item-param'
|
||||
|
||||
/** 解析参数 options 字段(JSON 数组字符串) */
|
||||
export function parseQcParamOptions(options?: string | null): string[] {
|
||||
if (!options) return []
|
||||
try {
|
||||
const arr = JSON.parse(options)
|
||||
return Array.isArray(arr)
|
||||
? arr.map((x) => String(x ?? '').trim()).filter(Boolean)
|
||||
: []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** 逗号分隔文本 → 选项列表(支持中英文逗号) */
|
||||
export function parseQcParamOptionText(text?: string | null): string[] {
|
||||
if (!text?.trim()) return []
|
||||
const seen = new Set<string>()
|
||||
const list: string[] = []
|
||||
for (const part of text.split(/[,,]/)) {
|
||||
const val = part.trim()
|
||||
if (!val || seen.has(val)) continue
|
||||
seen.add(val)
|
||||
list.push(val)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/** 选项列表 → 逗号分隔展示文本(默认项排在最前) */
|
||||
export function formatQcParamOptionText(options?: string | null, defaultValue?: string | null): string {
|
||||
const list = parseQcParamOptions(options)
|
||||
if (!list.length) return ''
|
||||
const dv = (defaultValue || '').trim()
|
||||
if (dv && list.includes(dv) && list[0] !== dv) {
|
||||
return [dv, ...list.filter((x) => x !== dv)].join(',')
|
||||
}
|
||||
return list.join(',')
|
||||
}
|
||||
|
||||
/** 表单加载:补充单选选项编辑态 */
|
||||
export function hydrateQcItemParamsForForm(params: QcItemParam[]): QcItemParam[] {
|
||||
return (params || []).map((p) => {
|
||||
if (p.paramType !== 'select') return { ...p }
|
||||
return {
|
||||
...p,
|
||||
_optionText: formatQcParamOptionText(p.options, p.defaultValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 类型切换时清理或初始化单选字段 */
|
||||
export function onQcItemParamTypeChange(param: QcItemParam) {
|
||||
if (param.paramType === 'select') {
|
||||
if (param._optionText === undefined) {
|
||||
param._optionText = ''
|
||||
}
|
||||
return
|
||||
}
|
||||
param.options = undefined
|
||||
param.defaultValue = undefined
|
||||
param._optionText = undefined
|
||||
}
|
||||
|
||||
/** 提交前序列化并校验 */
|
||||
export function serializeQcItemParamsForSave(params: QcItemParam[]): QcItemParam[] {
|
||||
return (params || []).map((p, idx) => {
|
||||
const { _optionText, ...rest } = p
|
||||
const base: QcItemParam = { ...rest, orderNum: p.orderNum ?? idx }
|
||||
if (p.paramType !== 'select') {
|
||||
base.options = undefined
|
||||
base.defaultValue = undefined
|
||||
return base
|
||||
}
|
||||
const list =
|
||||
_optionText !== undefined
|
||||
? parseQcParamOptionText(_optionText)
|
||||
: parseQcParamOptions(p.options)
|
||||
return {
|
||||
...base,
|
||||
options: list.length ? JSON.stringify(list) : undefined,
|
||||
defaultValue: list[0] || undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** @returns 错误文案;通过时返回 null */
|
||||
export function validateQcItemParamsBeforeSave(params: QcItemParam[]): string | null {
|
||||
const serialized = serializeQcItemParamsForSave(params)
|
||||
for (let i = 0; i < serialized.length; i++) {
|
||||
const p = serialized[i]
|
||||
const label = p.paramName?.trim() || `第 ${i + 1} 个参数`
|
||||
if (p.paramType === 'select') {
|
||||
const opts = parseQcParamOptions(p.options)
|
||||
if (!opts.length) {
|
||||
return `「${label}」为单选类型,请至少配置一个选项`
|
||||
}
|
||||
if (new Set(opts).size !== opts.length) {
|
||||
return `「${label}」的选项不能重复`
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
fetchMineQcWorkOrderTabUnfinishedCounts,
|
||||
fetchQcWorkOrderTabUnfinishedCounts
|
||||
} from '@/api/qc-work-order'
|
||||
import {
|
||||
QC_MENU_CODE_MY_TASK,
|
||||
QC_MENU_CODE_WORK_ORDER,
|
||||
useQcMenuBadgeStore
|
||||
} from '@/store/modules/qc-menu-badge'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
|
||||
/** 解析菜单项对应的质检角标 menuCode(兼容后端菜单未带 menuCode) */
|
||||
export function resolveQcMenuBadgeCode(item: AppRouteRecord): string | undefined {
|
||||
const code = (item.meta.menuCode || '').trim()
|
||||
if (code === QC_MENU_CODE_WORK_ORDER || code === QC_MENU_CODE_MY_TASK) {
|
||||
return code
|
||||
}
|
||||
const name = String(item.name || '')
|
||||
if (name === 'QualityInspectionQcWorkOrder') return QC_MENU_CODE_WORK_ORDER
|
||||
if (name === 'QualityInspectionMyQcTask') return QC_MENU_CODE_MY_TASK
|
||||
const path = String(item.path || '')
|
||||
if (path.endsWith('/qc-work-order') || path === 'qc-work-order') {
|
||||
return QC_MENU_CODE_WORK_ORDER
|
||||
}
|
||||
if (path.endsWith('/my-qc-task') || path === 'my-qc-task') {
|
||||
return QC_MENU_CODE_MY_TASK
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 从当前侧栏菜单树收集需展示角标的质检菜单 code */
|
||||
export function collectVisibleQcMenuBadgeCodes(menuList: AppRouteRecord[]): string[] {
|
||||
const codes = new Set<string>()
|
||||
const walk = (items: AppRouteRecord[]) => {
|
||||
for (const item of items) {
|
||||
const code = resolveQcMenuBadgeCode(item)
|
||||
if (code) codes.add(code)
|
||||
if (item.children?.length) walk(item.children)
|
||||
}
|
||||
}
|
||||
walk(menuList || [])
|
||||
return [...codes]
|
||||
}
|
||||
|
||||
/** 刷新侧栏「质检工单 / 我的质检任务」未完成数角标 */
|
||||
export async function refreshQcMenuBadges() {
|
||||
const menuStore = useMenuStore()
|
||||
const badgeStore = useQcMenuBadgeStore()
|
||||
const visibleCodes = collectVisibleQcMenuBadgeCodes(menuStore.menuList)
|
||||
|
||||
const tasks: Promise<void>[] = []
|
||||
|
||||
if (visibleCodes.includes(QC_MENU_CODE_WORK_ORDER)) {
|
||||
tasks.push(
|
||||
fetchQcWorkOrderTabUnfinishedCounts()
|
||||
.then((data) => badgeStore.setBadge(QC_MENU_CODE_WORK_ORDER, data?.all ?? 0))
|
||||
.catch(() => badgeStore.setBadge(QC_MENU_CODE_WORK_ORDER, 0))
|
||||
)
|
||||
}
|
||||
|
||||
if (visibleCodes.includes(QC_MENU_CODE_MY_TASK)) {
|
||||
tasks.push(
|
||||
fetchMineQcWorkOrderTabUnfinishedCounts()
|
||||
.then((data) => badgeStore.setBadge(QC_MENU_CODE_MY_TASK, data?.all ?? 0))
|
||||
.catch(() => badgeStore.setBadge(QC_MENU_CODE_MY_TASK, 0))
|
||||
)
|
||||
}
|
||||
|
||||
if (!tasks.length) return
|
||||
|
||||
await Promise.all(tasks)
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
import type { QcWorkOrder } from '@/api/qc-work-order'
|
||||
import { fetchQcWorkOrderDetail } from '@/api/qc-work-order'
|
||||
import { fetchMyQcTaskList, type QcTask } from '@/api/qc-task'
|
||||
import { groupInboundQcWorkOrders, matchesTemporaryQcSourceFilter } from '@/utils/qc-work-order-groups'
|
||||
import {
|
||||
groupInboundQcWorkOrders,
|
||||
matchesTemporaryQcSourceFilter
|
||||
} from '@/utils/qc-work-order-groups'
|
||||
import type { QcWorkOrderTabKey } from '@/utils/qc-work-order-tabs'
|
||||
import {
|
||||
resolveTabKeyFromWorkOrder,
|
||||
tabUsesInfusionTimeFilter,
|
||||
getQcWorkOrderTab,
|
||||
toWorkOrderListQuery
|
||||
toWorkOrderListQuery,
|
||||
isAllQcWorkOrderTab
|
||||
} from '@/utils/qc-work-order-tabs'
|
||||
import {
|
||||
matchesAllTabTriggerSource,
|
||||
toAllTabListQuery,
|
||||
workOrderMatchesCategory
|
||||
} from '@/utils/qc-work-order-category'
|
||||
|
||||
export type QcTaskMineFilters = {
|
||||
keyword?: string
|
||||
@@ -21,6 +30,7 @@ export type MineWorkOrderSearchFilters = {
|
||||
keyword?: string
|
||||
workOrderStatus?: string
|
||||
triggerSource?: string
|
||||
qcCategory?: string
|
||||
createTimeStart?: string
|
||||
createTimeEnd?: string
|
||||
infusionTimeStart?: string
|
||||
@@ -45,7 +55,11 @@ function dateInRange(value: string | undefined, start?: string, end?: string): b
|
||||
return true
|
||||
}
|
||||
|
||||
function keywordMatchesWorkOrder(row: QcWorkOrder, keyword: string, tabKey: QcWorkOrderTabKey): boolean {
|
||||
function keywordMatchesWorkOrder(
|
||||
row: QcWorkOrder,
|
||||
keyword: string,
|
||||
tabKey: QcWorkOrderTabKey
|
||||
): boolean {
|
||||
const kw = keyword.trim().toLowerCase()
|
||||
if (!kw) return true
|
||||
const tab = getQcWorkOrderTab(tabKey)!
|
||||
@@ -53,10 +67,16 @@ function keywordMatchesWorkOrder(row: QcWorkOrder, keyword: string, tabKey: QcWo
|
||||
tabKey === 'temporary'
|
||||
? [row.workOrderNo, row.productionWorkOrderNo]
|
||||
: 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.workOrderNo, row.protocolNo, row.sampleCode, row.labStorageCode]
|
||||
return fields.some((f) => (f || '').toLowerCase().includes(kw))
|
||||
}
|
||||
|
||||
@@ -68,21 +88,29 @@ export function filterMineEntriesByWorkOrderSearch(
|
||||
const tab = getQcWorkOrderTab(tabKey)!
|
||||
return entries.filter((entry) => {
|
||||
const row = entry.workOrder
|
||||
if (resolveTabKeyFromWorkOrder(row) !== tabKey) return false
|
||||
if (!isAllQcWorkOrderTab(tabKey) && resolveTabKeyFromWorkOrder(row) !== tabKey) {
|
||||
return false
|
||||
}
|
||||
if (isAllQcWorkOrderTab(tabKey) && !workOrderMatchesCategory(row, filters.qcCategory || '')) {
|
||||
return false
|
||||
}
|
||||
if (filters.workOrderStatus && row.workOrderStatus !== filters.workOrderStatus) return false
|
||||
if (filters.triggerSource) {
|
||||
if (tabKey === 'temporary') {
|
||||
if (isAllQcWorkOrderTab(tabKey)) {
|
||||
if (!matchesAllTabTriggerSource(row, filters.triggerSource)) return false
|
||||
} else if (tabKey === 'temporary') {
|
||||
if (!matchesTemporaryQcSourceFilter(row, filters.triggerSource)) return false
|
||||
} else if (tab.listLayout !== 'standalone' && row.triggerSource !== filters.triggerSource) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (!keywordMatchesWorkOrder(row, filters.keyword || '', tabKey)) return false
|
||||
if (tabUsesInfusionTimeFilter(tab)) {
|
||||
if (!dateInRange(row.infusionTime, filters.infusionTimeStart, filters.infusionTimeEnd)) {
|
||||
const keywordTabKey = isAllQcWorkOrderTab(tabKey) ? resolveTabKeyFromWorkOrder(row) : tabKey
|
||||
if (!keywordMatchesWorkOrder(row, filters.keyword || '', keywordTabKey)) return false
|
||||
if (isAllQcWorkOrderTab(tabKey) || !tabUsesInfusionTimeFilter(tab)) {
|
||||
if (!dateInRange(row.createTime, filters.createTimeStart, filters.createTimeEnd)) {
|
||||
return false
|
||||
}
|
||||
} else if (!dateInRange(row.createTime, filters.createTimeStart, filters.createTimeEnd)) {
|
||||
} else if (!dateInRange(row.infusionTime, filters.infusionTimeStart, filters.infusionTimeEnd)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -91,7 +119,9 @@ export function filterMineEntriesByWorkOrderSearch(
|
||||
|
||||
export async function fetchAllMineTasks(filters: QcTaskMineFilters): Promise<QcTask[]> {
|
||||
const tab = getQcWorkOrderTab(filters.tabKey)!
|
||||
const listQuery = toWorkOrderListQuery(tab)
|
||||
const listQuery = isAllQcWorkOrderTab(filters.tabKey)
|
||||
? toAllTabListQuery('')
|
||||
: toWorkOrderListQuery(tab)
|
||||
const pageSize = MINE_TASK_FETCH_PAGE_SIZE
|
||||
let page = 1
|
||||
const all: QcTask[] = []
|
||||
@@ -103,7 +133,7 @@ export async function fetchAllMineTasks(filters: QcTaskMineFilters): Promise<QcT
|
||||
keyword: filters.keyword || undefined,
|
||||
taskType: filters.taskType || undefined,
|
||||
taskStatus: filters.taskStatus || undefined,
|
||||
qcBizType: tab.workOrderType,
|
||||
qcBizType: listQuery.workOrderType,
|
||||
receptionSource: listQuery.receptionSource,
|
||||
bizType: listQuery.bizType
|
||||
})
|
||||
@@ -262,11 +292,15 @@ export function buildMineCardListModel(
|
||||
tasks: QcTask[],
|
||||
tabKey: QcWorkOrderTabKey
|
||||
): MineCardListModel {
|
||||
const entries = groupMineTasksByWorkOrder(tasks).filter((e) => e.tabKey === tabKey)
|
||||
const entries = groupMineTasksByWorkOrder(tasks).filter((e) =>
|
||||
isAllQcWorkOrderTab(tabKey) ? true : e.tabKey === tabKey
|
||||
)
|
||||
const presetTasksByWorkOrderId = buildPresetTasksMap(entries)
|
||||
const tab = getQcWorkOrderTab(tabKey)!
|
||||
const useInboundGroups = tab.listLayout === 'inbound-grouped'
|
||||
const inboundGroups = useInboundGroups ? groupInboundQcWorkOrders(entries.map((e) => e.workOrder)) : []
|
||||
const inboundGroups = useInboundGroups
|
||||
? groupInboundQcWorkOrders(entries.map((e) => e.workOrder))
|
||||
: []
|
||||
const standaloneEntries = useInboundGroups ? [] : entries
|
||||
return {
|
||||
inboundGroups,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QcWorkOrder } from '@/api/qc-work-order'
|
||||
import type { QcWorkOrder, QcProtocolPaymentSummary } from '@/api/qc-work-order'
|
||||
import {
|
||||
donationStatusLabel,
|
||||
donationStatusTagType,
|
||||
@@ -7,10 +7,12 @@ import {
|
||||
} from '@/utils/status-tag-registry'
|
||||
import type { TagType } from '@/utils/status-tag-registry'
|
||||
import type { QcWorkOrderTabKey } from '@/utils/qc-work-order-tabs'
|
||||
import { getQcWorkOrderTab } from '@/utils/qc-work-order-tabs'
|
||||
import { getQcWorkOrderTab, resolveTabKeyFromWorkOrder } from '@/utils/qc-work-order-tabs'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
import {
|
||||
isTemporaryQcFromProduction
|
||||
countConfiguredSamplesInProtocolGroup,
|
||||
isTemporaryQcFromProduction,
|
||||
type QcWorkOrderProtocolGroupModel
|
||||
} from '@/utils/qc-work-order-groups'
|
||||
|
||||
export type CardMetaFieldKind = 'text' | 'tag'
|
||||
@@ -30,6 +32,7 @@ export type CardMetaField = {
|
||||
export type QcWorkOrderCardMetaConfig = {
|
||||
tabKey: QcWorkOrderTabKey
|
||||
listLayout: 'inbound-grouped' | 'outbound-grouped' | 'standalone'
|
||||
showCategoryTag: boolean
|
||||
showTriggerTag: boolean
|
||||
showProtocolFeeSummary: boolean
|
||||
showOutboundFeeSummary: boolean
|
||||
@@ -79,7 +82,9 @@ function protocolStatusLabelBySource(status: string, source: string): string {
|
||||
}
|
||||
|
||||
function protocolStatusTagTypeBySource(status: string, source: string): TagType {
|
||||
return source === 'research_donation' ? donationStatusTagType(status) : protocolStatusTagType(status)
|
||||
return source === 'research_donation'
|
||||
? donationStatusTagType(status)
|
||||
: protocolStatusTagType(status)
|
||||
}
|
||||
|
||||
const commonTailFields: CardMetaField[] = [
|
||||
@@ -176,6 +181,36 @@ function buildDonationGroupContext(row: QcWorkOrder): string[] {
|
||||
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 || '')
|
||||
const source = row.receptionSource || 'cell_storage'
|
||||
return {
|
||||
label: protocolStatusLabelBySource(status, source),
|
||||
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 },
|
||||
{
|
||||
@@ -205,9 +240,24 @@ function buildOutboundBasicExperimentGroupContext(row: QcWorkOrder): string[] {
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -221,6 +271,7 @@ const CARD_META_BY_TAB: Record<QcWorkOrderTabKey, QcWorkOrderCardMetaConfig> = {
|
||||
inbound_research_donation: {
|
||||
tabKey: 'inbound_research_donation',
|
||||
listLayout: 'inbound-grouped',
|
||||
showCategoryTag: false,
|
||||
showTriggerTag: true,
|
||||
showProtocolFeeSummary: false,
|
||||
showOutboundFeeSummary: false,
|
||||
@@ -234,6 +285,7 @@ const CARD_META_BY_TAB: Record<QcWorkOrderTabKey, QcWorkOrderCardMetaConfig> = {
|
||||
outbound_pilot_service: {
|
||||
tabKey: 'outbound_pilot_service',
|
||||
listLayout: 'outbound-grouped',
|
||||
showCategoryTag: false,
|
||||
showTriggerTag: true,
|
||||
showProtocolFeeSummary: false,
|
||||
showOutboundFeeSummary: false,
|
||||
@@ -247,6 +299,7 @@ const CARD_META_BY_TAB: Record<QcWorkOrderTabKey, QcWorkOrderCardMetaConfig> = {
|
||||
outbound_basic_experiment: {
|
||||
tabKey: 'outbound_basic_experiment',
|
||||
listLayout: 'outbound-grouped',
|
||||
showCategoryTag: false,
|
||||
showTriggerTag: true,
|
||||
showProtocolFeeSummary: false,
|
||||
showOutboundFeeSummary: true,
|
||||
@@ -260,6 +313,7 @@ const CARD_META_BY_TAB: Record<QcWorkOrderTabKey, QcWorkOrderCardMetaConfig> = {
|
||||
outbound_clinical_application: {
|
||||
tabKey: 'outbound_clinical_application',
|
||||
listLayout: 'standalone',
|
||||
showCategoryTag: false,
|
||||
showTriggerTag: false,
|
||||
showProtocolFeeSummary: false,
|
||||
showOutboundFeeSummary: true,
|
||||
@@ -273,6 +327,7 @@ const CARD_META_BY_TAB: Record<QcWorkOrderTabKey, QcWorkOrderCardMetaConfig> = {
|
||||
temporary: {
|
||||
tabKey: 'temporary',
|
||||
listLayout: 'standalone',
|
||||
showCategoryTag: false,
|
||||
showTriggerTag: true,
|
||||
showProtocolFeeSummary: false,
|
||||
showOutboundFeeSummary: false,
|
||||
@@ -289,11 +344,37 @@ export function getCardMetaConfig(tabKey: QcWorkOrderTabKey): QcWorkOrderCardMet
|
||||
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 {
|
||||
const ts = (row.triggerSource || 'reception').trim()
|
||||
if (ts === 'production') {
|
||||
return String(row.cellProductCode || '').trim() || '—'
|
||||
}
|
||||
if (ts === 'reception') {
|
||||
return String(row.labStorageCode || '').trim() || '—'
|
||||
}
|
||||
return String(row.workOrderNo || '').trim() || '—'
|
||||
}
|
||||
|
||||
export function cardMetaUsesInboundGroups(tabKey: QcWorkOrderTabKey): boolean {
|
||||
return getQcWorkOrderTab(tabKey)?.listLayout === 'inbound-grouped'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import type { QcWorkOrder } from '@/api/qc-work-order'
|
||||
import { isTemporaryQcFromProduction } from '@/utils/qc-work-order-groups'
|
||||
import type { TagType } from '@/utils/status-tag-registry'
|
||||
import {
|
||||
resolveTabKeyFromWorkOrder,
|
||||
type QcWorkOrderTabKey,
|
||||
getQcWorkOrderTab,
|
||||
QC_WORK_ORDER_VISIBLE_TABS
|
||||
} from '@/utils/qc-work-order-tabs'
|
||||
|
||||
/** 「所有」Tab 内质检类型子筛选(不含 all / hidden clinical) */
|
||||
export type QcWorkOrderCategoryKey = Exclude<
|
||||
QcWorkOrderTabKey,
|
||||
'all' | 'outbound_clinical_application'
|
||||
>
|
||||
|
||||
export const QC_WORK_ORDER_CATEGORY_OPTIONS: { key: QcWorkOrderCategoryKey; label: string }[] =
|
||||
QC_WORK_ORDER_VISIBLE_TABS.filter((t) => t.key !== 'all').map((t) => ({
|
||||
key: t.key as QcWorkOrderCategoryKey,
|
||||
label: t.label.replace(/质检$/, '')
|
||||
}))
|
||||
|
||||
const CATEGORY_LABELS: Record<QcWorkOrderCategoryKey, string> = {
|
||||
inbound_cell_storage: '细胞存储',
|
||||
inbound_research_donation: '科研捐赠',
|
||||
outbound_pilot_service: '中试服务',
|
||||
outbound_basic_experiment: '基础实验',
|
||||
temporary: '临时'
|
||||
}
|
||||
|
||||
const CATEGORY_TAG_TYPES: Record<QcWorkOrderCategoryKey, TagType> = {
|
||||
inbound_cell_storage: 'primary',
|
||||
inbound_research_donation: 'primary',
|
||||
outbound_pilot_service: 'info',
|
||||
outbound_basic_experiment: 'info',
|
||||
temporary: 'warning'
|
||||
}
|
||||
|
||||
export function resolveWorkOrderCategoryKey(row: {
|
||||
workOrderType?: string
|
||||
receptionSource?: string
|
||||
bizType?: string
|
||||
}): QcWorkOrderCategoryKey {
|
||||
const tabKey = resolveTabKeyFromWorkOrder(row)
|
||||
if (tabKey === 'outbound_clinical_application') return 'outbound_pilot_service'
|
||||
return tabKey as QcWorkOrderCategoryKey
|
||||
}
|
||||
|
||||
export function qcWorkOrderCategoryLabel(row: QcWorkOrder): string {
|
||||
return CATEGORY_LABELS[resolveWorkOrderCategoryKey(row)] || '—'
|
||||
}
|
||||
|
||||
export function qcWorkOrderCategoryTagType(row: QcWorkOrder): TagType {
|
||||
return CATEGORY_TAG_TYPES[resolveWorkOrderCategoryKey(row)] || 'info'
|
||||
}
|
||||
|
||||
/** 「所有」Tab 工单来源筛选项 value(含临时专用值) */
|
||||
export const QC_ALL_TAB_TRIGGER_SOURCE_OPTIONS = [
|
||||
{ label: '样本接收', value: 'reception' },
|
||||
{ label: '计划质检', value: 'schedule' },
|
||||
{ label: '生产过程', value: 'production' },
|
||||
{ label: '生产派发', value: 'temp_production' },
|
||||
{ label: '手动创建', value: 'temp_manual' }
|
||||
] as const
|
||||
|
||||
export type QcAllTabTriggerSource = (typeof QC_ALL_TAB_TRIGGER_SOURCE_OPTIONS)[number]['value']
|
||||
|
||||
export function toAllTabListQuery(categoryKey: string) {
|
||||
const key = (categoryKey || '').trim()
|
||||
if (!key) {
|
||||
return {
|
||||
workOrderType: undefined as string | undefined,
|
||||
receptionSource: undefined as string | undefined,
|
||||
bizType: undefined as string | undefined
|
||||
}
|
||||
}
|
||||
const tab = getQcWorkOrderTab(key)
|
||||
if (!tab || tab.key === 'all') {
|
||||
return {
|
||||
workOrderType: undefined,
|
||||
receptionSource: undefined,
|
||||
bizType: undefined
|
||||
}
|
||||
}
|
||||
return {
|
||||
workOrderType: tab.workOrderType,
|
||||
receptionSource: tab.receptionSource,
|
||||
bizType: tab.bizType
|
||||
}
|
||||
}
|
||||
|
||||
export function workOrderMatchesCategory(row: QcWorkOrder, categoryKey: string): boolean {
|
||||
const key = (categoryKey || '').trim()
|
||||
if (!key) return true
|
||||
return resolveWorkOrderCategoryKey(row) === key
|
||||
}
|
||||
|
||||
export function matchesAllTabTriggerSource(row: QcWorkOrder, value?: string): boolean {
|
||||
const filterValue = (value || '').trim()
|
||||
if (!filterValue) return true
|
||||
const wt = row.workOrderType || 'inbound'
|
||||
const ts = row.triggerSource || 'reception'
|
||||
if (filterValue === 'reception' || filterValue === 'schedule') {
|
||||
return ts === filterValue
|
||||
}
|
||||
if (filterValue === 'production') {
|
||||
return wt !== 'temporary' && ts === 'production'
|
||||
}
|
||||
if (filterValue === 'temp_production') {
|
||||
return wt === 'temporary' && isTemporaryQcFromProduction(row)
|
||||
}
|
||||
if (filterValue === 'temp_manual') {
|
||||
return wt === 'temporary' && !isTemporaryQcFromProduction(row)
|
||||
}
|
||||
return ts === filterValue
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { QcWorkOrder } from '@/api/qc-work-order'
|
||||
import { getQcWorkOrderTab, resolveTabKeyFromWorkOrder } from '@/utils/qc-work-order-tabs'
|
||||
|
||||
export type QcWorkOrderSampleGroupModel = {
|
||||
groupKey: string
|
||||
@@ -95,3 +96,147 @@ export function matchesTemporaryQcSourceFilter(row: QcWorkOrder, filterValue?: s
|
||||
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)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { QcWorkOrder } from '@/api/qc-work-order'
|
||||
|
||||
export type QcWorkOrderTabKey =
|
||||
| 'all'
|
||||
| 'inbound_cell_storage'
|
||||
| 'inbound_research_donation'
|
||||
| 'outbound_pilot_service'
|
||||
@@ -8,13 +9,13 @@ export type QcWorkOrderTabKey =
|
||||
| 'outbound_clinical_application'
|
||||
| 'temporary'
|
||||
|
||||
export type QcWorkOrderTabGroup = 'inbound' | 'outbound' | 'temporary'
|
||||
export type QcWorkOrderTabGroup = 'all' | 'inbound' | 'outbound' | 'temporary'
|
||||
|
||||
export type QcWorkOrderTabConfig = {
|
||||
key: QcWorkOrderTabKey
|
||||
label: string
|
||||
group: QcWorkOrderTabGroup
|
||||
workOrderType: 'inbound' | 'outbound' | 'temporary'
|
||||
workOrderType?: 'inbound' | 'outbound' | 'temporary'
|
||||
receptionSource?: 'cell_storage' | 'research_donation'
|
||||
bizType?: 'pilot_service' | 'basic_experiment' | 'clinical_application'
|
||||
listLayout: 'inbound-grouped' | 'outbound-grouped' | 'standalone'
|
||||
@@ -23,6 +24,12 @@ export type QcWorkOrderTabConfig = {
|
||||
}
|
||||
|
||||
export const QC_WORK_ORDER_TABS: QcWorkOrderTabConfig[] = [
|
||||
{
|
||||
key: 'all',
|
||||
label: '所有',
|
||||
group: 'all',
|
||||
listLayout: 'standalone'
|
||||
},
|
||||
{
|
||||
key: 'inbound_cell_storage',
|
||||
label: '细胞存储质检',
|
||||
@@ -76,7 +83,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 = 'inbound_cell_storage'
|
||||
export const DEFAULT_QC_WORK_ORDER_TAB: QcWorkOrderTabKey = 'all'
|
||||
|
||||
export function getQcWorkOrderTab(key: string): QcWorkOrderTabConfig | undefined {
|
||||
return QC_WORK_ORDER_TABS.find((t) => t.key === key)
|
||||
@@ -93,6 +100,13 @@ export function parseQcWorkOrderTabKey(raw?: string | null): QcWorkOrderTabKey {
|
||||
}
|
||||
|
||||
export function toWorkOrderListQuery(tab: QcWorkOrderTabConfig) {
|
||||
if (tab.key === 'all') {
|
||||
return {
|
||||
workOrderType: undefined as string | undefined,
|
||||
receptionSource: undefined as string | undefined,
|
||||
bizType: undefined as string | undefined
|
||||
}
|
||||
}
|
||||
return {
|
||||
workOrderType: tab.workOrderType,
|
||||
receptionSource: tab.receptionSource,
|
||||
@@ -123,29 +137,51 @@ export function tabUsesInfusionTimeFilter(tab: QcWorkOrderTabConfig): boolean {
|
||||
}
|
||||
|
||||
export function keywordPlaceholderForTab(tab: QcWorkOrderTabConfig): string {
|
||||
if (tab.key === 'all') return '工单号 / 样本编码 / 申请单号 / 产品名称等'
|
||||
if (tab.key === 'temporary') return '工单号'
|
||||
if (tab.key === 'outbound_basic_experiment') {
|
||||
return '工单号/申请单号/课题名称/客户'
|
||||
}
|
||||
if (tab.group === 'outbound') return '工单号/申请单号/申请单名称/患者'
|
||||
if (tab.key === 'inbound_research_donation') {
|
||||
return '工单号/捐赠单号/样本编码/入库编码'
|
||||
}
|
||||
if (tab.group === 'inbound') {
|
||||
return '工单号/协议号/样本编码/入库编码'
|
||||
}
|
||||
return '工单号/样本编码/入库编码'
|
||||
}
|
||||
|
||||
export function filterTimeLabelForTab(tab: QcWorkOrderTabConfig): string {
|
||||
if (tab.key === 'all') return '创建时间'
|
||||
if (tab.key === 'outbound_basic_experiment') return '交付时间'
|
||||
if (tab.group === 'outbound') return '输注时间'
|
||||
return '创建时间'
|
||||
}
|
||||
|
||||
export function filterTimeHintForTab(tab: QcWorkOrderTabConfig): string {
|
||||
if (tab.key === 'all') {
|
||||
return '出库工单按输注/交付时间筛选请进入对应分类 Tab。'
|
||||
}
|
||||
if (tab.group === 'outbound') {
|
||||
return tab.key === 'outbound_basic_experiment'
|
||||
? '支持按交付时间筛选。'
|
||||
: '支持按输注时间筛选。'
|
||||
return tab.key === 'outbound_basic_experiment' ? '支持按交付时间筛选。' : '支持按输注时间筛选。'
|
||||
}
|
||||
return '支持按创建时间筛选。'
|
||||
}
|
||||
|
||||
export function workOrderMatchesTab(row: QcWorkOrder, tabKey: QcWorkOrderTabKey): boolean {
|
||||
if (tabKey === 'all') return true
|
||||
return resolveTabKeyFromWorkOrder(row) === tabKey
|
||||
}
|
||||
|
||||
export function isAllQcWorkOrderTab(tabKey: QcWorkOrderTabKey): boolean {
|
||||
return tabKey === 'all'
|
||||
}
|
||||
|
||||
/** 列表 Tab 为「所有」时,按工单解析其分类 Tab Key */
|
||||
export function resolveEffectiveRowTabKey(
|
||||
listTabKey: QcWorkOrderTabKey,
|
||||
row: QcWorkOrder
|
||||
): QcWorkOrderTabKey {
|
||||
return isAllQcWorkOrderTab(listTabKey) ? resolveTabKeyFromWorkOrder(row) : listTabKey
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
export type SampleReceptionSourceType = 'cell_storage' | 'research_donation'
|
||||
|
||||
const SAMPLE_TYPE_PERINATAL_HSC = '围产期造血干细胞'
|
||||
const SAMPLE_TYPE_UC_MSC = '脐带间充质干细胞'
|
||||
|
||||
function normalizeSampleTypeName(name: string): string {
|
||||
return String(name || '').trim()
|
||||
}
|
||||
|
||||
function normalizeSourceType(source: string): SampleReceptionSourceType {
|
||||
return source === 'research_donation' ? 'research_donation' : 'cell_storage'
|
||||
}
|
||||
|
||||
function formatYm(d: Date): string {
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}${pad(d.getMonth() + 1)}`
|
||||
}
|
||||
|
||||
function formatYear(d: Date): string {
|
||||
return String(d.getFullYear())
|
||||
}
|
||||
|
||||
/** 从接收时间字符串解析参考日期;无效时回退为当前时刻 */
|
||||
export function parseLabStorageReferenceDate(receptionTime: string): Date {
|
||||
const s = String(receptionTime || '').trim()
|
||||
const dateStr = s.length >= 10 ? s.slice(0, 10) : ''
|
||||
if (!dateStr || dateStr[4] !== '-' || dateStr[7] !== '-') {
|
||||
return new Date()
|
||||
}
|
||||
const [y, m, d] = dateStr.split('-').map((v) => Number(v))
|
||||
if (!y || !m || !d) return new Date()
|
||||
return new Date(y, m - 1, d)
|
||||
}
|
||||
|
||||
/**
|
||||
* 按来源与样本类型预制实验室入库编码;不匹配规则时返回空字符串。
|
||||
*/
|
||||
export function buildPrefilledLabStorageCode(
|
||||
sourceType: string,
|
||||
sampleTypeName: string,
|
||||
referenceDate: Date = new Date()
|
||||
): string {
|
||||
const source = normalizeSourceType(sourceType)
|
||||
const typeName = normalizeSampleTypeName(sampleTypeName)
|
||||
|
||||
if (source === 'research_donation') {
|
||||
if (typeName === SAMPLE_TYPE_PERINATAL_HSC) {
|
||||
return `UBMC${formatYm(referenceDate)}`
|
||||
}
|
||||
if (typeName === SAMPLE_TYPE_UC_MSC) {
|
||||
return `UC${formatYear(referenceDate)}`
|
||||
}
|
||||
}
|
||||
|
||||
if (source === 'cell_storage') {
|
||||
if (typeName === SAMPLE_TYPE_UC_MSC) {
|
||||
return 'BC'
|
||||
}
|
||||
if (typeName === SAMPLE_TYPE_PERINATAL_HSC) {
|
||||
return 'B'
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
Reference in new issue
Block a user