342 lines
11 KiB
TypeScript
342 lines
11 KiB
TypeScript
import type { QcWorkOrder } from '@/api/qc-work-order'
|
|
import { fetchMyQcTaskList, type QcTask } from '@/api/qc-task'
|
|
import {
|
|
groupInboundQcWorkOrders,
|
|
matchesTemporaryQcSourceFilter
|
|
} from '@/utils/qc-work-order-groups'
|
|
import type { QcWorkOrderTabKey } from '@/utils/qc-work-order-tabs'
|
|
import {
|
|
resolveTabKeyFromWorkOrder,
|
|
tabUsesInfusionTimeFilter,
|
|
getQcWorkOrderTab,
|
|
toWorkOrderListQuery,
|
|
isAllQcWorkOrderTab
|
|
} from '@/utils/qc-work-order-tabs'
|
|
import {
|
|
matchesAllTabTriggerSource,
|
|
toAllTabListQuery,
|
|
workOrderMatchesCategory
|
|
} from '@/utils/qc-work-order-category'
|
|
import { matchesWorkOrderStatusFilter } from '@/utils/status-tag-registry'
|
|
import { compareLabStorageThenCreateTimeDesc } from '@/utils/qc-inbound-lab-storage-sort'
|
|
|
|
export type QcTaskMineFilters = {
|
|
keyword?: string
|
|
taskType?: string
|
|
taskStatus?: string
|
|
tabKey: QcWorkOrderTabKey
|
|
}
|
|
|
|
export type MineWorkOrderSearchFilters = {
|
|
keyword?: string
|
|
workOrderStatus?: string | string[]
|
|
triggerSource?: string
|
|
qcCategory?: string
|
|
createTimeStart?: string
|
|
createTimeEnd?: string
|
|
infusionTimeStart?: string
|
|
infusionTimeEnd?: string
|
|
tabKey: QcWorkOrderTabKey
|
|
sampleTypeName?: string
|
|
}
|
|
|
|
export type MineWorkOrderEntry = {
|
|
workOrderId: string
|
|
workOrder: QcWorkOrder
|
|
tabKey: QcWorkOrderTabKey
|
|
tasks: QcTask[]
|
|
}
|
|
|
|
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: QcWorkOrder,
|
|
keyword: string,
|
|
tabKey: QcWorkOrderTabKey
|
|
): boolean {
|
|
const kw = keyword.trim().toLowerCase()
|
|
if (!kw) return true
|
|
const tab = getQcWorkOrderTab(tabKey)!
|
|
const fields =
|
|
tabKey === 'temporary'
|
|
? [row.workOrderNo, row.productionWorkOrderNo]
|
|
: tabKey === 'outbound_basic_experiment'
|
|
? [
|
|
row.workOrderNo,
|
|
row.applicationNo,
|
|
row.projectTitle,
|
|
row.customerName,
|
|
row.applicationName
|
|
]
|
|
: tab.group === 'outbound'
|
|
? [row.workOrderNo, row.applicationNo, row.applicationName, row.patientName]
|
|
: [
|
|
row.workOrderNo,
|
|
row.protocolNo,
|
|
row.sampleCode,
|
|
row.labStorageCode,
|
|
row.motherName,
|
|
row.newbornName
|
|
]
|
|
return fields.some((f) => (f || '').toLowerCase().includes(kw))
|
|
}
|
|
|
|
export function filterMineEntriesByWorkOrderSearch(
|
|
entries: MineWorkOrderEntry[],
|
|
filters: MineWorkOrderSearchFilters
|
|
): MineWorkOrderEntry[] {
|
|
const tabKey = filters.tabKey
|
|
const tab = getQcWorkOrderTab(tabKey)!
|
|
return entries.filter((entry) => {
|
|
const row = entry.workOrder
|
|
if (!isAllQcWorkOrderTab(tabKey) && resolveTabKeyFromWorkOrder(row) !== tabKey) {
|
|
return false
|
|
}
|
|
if (isAllQcWorkOrderTab(tabKey) && !workOrderMatchesCategory(row, filters.qcCategory || '')) {
|
|
return false
|
|
}
|
|
if (!matchesWorkOrderStatusFilter(row.workOrderStatus, filters.workOrderStatus)) return false
|
|
if (filters.triggerSource) {
|
|
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
|
|
}
|
|
}
|
|
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
|
|
}
|
|
} else if (!dateInRange(row.infusionTime, filters.infusionTimeStart, filters.infusionTimeEnd)) {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
export async function fetchAllMineTasks(filters: QcTaskMineFilters): Promise<QcTask[]> {
|
|
const tab = getQcWorkOrderTab(filters.tabKey)!
|
|
const listQuery = isAllQcWorkOrderTab(filters.tabKey)
|
|
? toAllTabListQuery('')
|
|
: toWorkOrderListQuery(tab)
|
|
const pageSize = MINE_TASK_FETCH_PAGE_SIZE
|
|
let page = 1
|
|
const all: QcTask[] = []
|
|
let total = 0
|
|
while (true) {
|
|
const res = await fetchMyQcTaskList({
|
|
page,
|
|
pageSize,
|
|
keyword: filters.keyword || undefined,
|
|
taskType: filters.taskType || undefined,
|
|
taskStatus: filters.taskStatus || undefined,
|
|
qcBizType: listQuery.workOrderType,
|
|
receptionSource: listQuery.receptionSource,
|
|
bizType: listQuery.bizType
|
|
})
|
|
const list = res.list || []
|
|
all.push(...list)
|
|
total = res.total || 0
|
|
if (!list.length || all.length >= total) break
|
|
page += 1
|
|
}
|
|
return all
|
|
}
|
|
|
|
export async function enrichMineEntriesWithWorkOrders(
|
|
entries: MineWorkOrderEntry[]
|
|
): Promise<MineWorkOrderEntry[]> {
|
|
// 工单摘要已由 /qc-tasks/mine 接口批量 enrich,无需再逐条请求详情
|
|
return entries
|
|
}
|
|
|
|
export function paginateMineEntries<T>(items: T[], page: number, pageSize: number) {
|
|
const safePage = Math.max(1, page)
|
|
const safeSize = Math.max(1, pageSize)
|
|
const start = (safePage - 1) * safeSize
|
|
return {
|
|
total: items.length,
|
|
items: items.slice(start, start + safeSize)
|
|
}
|
|
}
|
|
|
|
export function sortMineEntriesForInboundLabStorage(
|
|
entries: MineWorkOrderEntry[]
|
|
): MineWorkOrderEntry[] {
|
|
return [...entries].sort((a, b) =>
|
|
compareLabStorageThenCreateTimeDesc(
|
|
a.workOrder.labStorageCode,
|
|
a.workOrder.createTime,
|
|
b.workOrder.labStorageCode,
|
|
b.workOrder.createTime
|
|
)
|
|
)
|
|
}
|
|
|
|
export function buildMineTableRows(pageEntries: MineWorkOrderEntry[]): {
|
|
rows: QcWorkOrder[]
|
|
presetTasksByWorkOrderId: Record<string, QcTask[]>
|
|
} {
|
|
return {
|
|
rows: pageEntries.map((e) => e.workOrder),
|
|
presetTasksByWorkOrderId: buildPresetTasksMap(pageEntries)
|
|
}
|
|
}
|
|
|
|
export function resolveWorkOrderTabKey(task: QcTask): QcWorkOrderTabKey {
|
|
const woType = task.qcBizType || task.orderSource || 'inbound'
|
|
if (woType === 'outbound') {
|
|
return resolveTabKeyFromWorkOrder({
|
|
workOrderType: 'outbound',
|
|
bizType: task.receptionSource
|
|
})
|
|
}
|
|
if (woType === 'temporary') {
|
|
return 'temporary'
|
|
}
|
|
return resolveTabKeyFromWorkOrder({
|
|
workOrderType: 'inbound',
|
|
receptionSource: task.receptionSource
|
|
})
|
|
}
|
|
|
|
export function buildWorkOrderFromTasks(tasks: QcTask[]): QcWorkOrder {
|
|
const t = tasks[0]
|
|
const tabKey = resolveWorkOrderTabKey(t)
|
|
const woType = getQcWorkOrderTab(tabKey)?.workOrderType || 'inbound'
|
|
return {
|
|
id: t.workOrderId,
|
|
workOrderNo: t.workOrderNo || '—',
|
|
workOrderType: woType,
|
|
workOrderStatus: t.workOrderStatus || '',
|
|
receptionId: t.receptionId || '',
|
|
protocolSampleId: t.protocolSampleId || '',
|
|
collectionProtocolId: t.collectionProtocolId || '',
|
|
protocolNo: t.protocolNo || '',
|
|
sampleCode: t.sampleCode || '',
|
|
labStorageCode: t.labStorageCode,
|
|
receptionSource: t.receptionSource,
|
|
receptionSourceLabel: t.receptionSourceLabel,
|
|
qcBizType: t.qcBizType || woType,
|
|
qcBizTypeLabel: t.qcBizTypeLabel,
|
|
cellProductId: t.cellProductId,
|
|
cellProductCode: t.cellProductCode,
|
|
cellProductName: t.cellProductName,
|
|
cellProductTypeName: t.cellProductTypeName,
|
|
cellProductPassage: t.cellProductPassage,
|
|
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(),
|
|
receptionTime: t.receptionTime,
|
|
handlerName: t.handlerName,
|
|
hasLab: t.hasLab ?? tasks.some((x) => x.taskType === 'sysz'),
|
|
hasThirdParty: t.hasThirdParty ?? tasks.some((x) => x.taskType === 'dsfjc'),
|
|
createTime: t.workOrderCreateTime || t.createTime || '',
|
|
bizType: woType === 'outbound' ? t.bizType || t.receptionSource : undefined,
|
|
bizTypeLabel: woType === 'outbound' ? t.bizTypeLabel || t.receptionSourceLabel : undefined,
|
|
applicationNo: t.applicationNo,
|
|
applicationName: t.applicationName,
|
|
projectTitle: t.projectTitle,
|
|
customerName: t.customerName,
|
|
channelName: t.channelName,
|
|
patientName: t.patientName,
|
|
motherName: t.motherName,
|
|
newbornName: t.newbornName,
|
|
sortNo: t.sortNo,
|
|
totalCourseCount: t.totalCourseCount,
|
|
infusionTime: t.infusionTime,
|
|
products: t.products,
|
|
cellType: t.cellType,
|
|
specText: t.specText,
|
|
triggerSource: t.triggerSource,
|
|
triggerSourceLabel: t.triggerSourceLabel,
|
|
productionWorkOrderId: t.productionWorkOrderId,
|
|
productionWorkOrderNo: t.productionWorkOrderNo,
|
|
protocolStatus: t.protocolStatus,
|
|
protocolStatusLabel: t.protocolStatusLabel,
|
|
paymentSummary: t.paymentSummary,
|
|
feeSummary: t.feeSummary,
|
|
protocolSiblingSamples: t.protocolSiblingSamples,
|
|
remark: t.workOrderRemark || t.remark,
|
|
unfinishedBadgeCount: t.unfinishedBadgeCount
|
|
}
|
|
}
|
|
|
|
/** 按工单聚合任务,保持 API 返回顺序 */
|
|
export function groupMineTasksByWorkOrder(tasks: QcTask[]): MineWorkOrderEntry[] {
|
|
const map = new Map<string, QcTask[]>()
|
|
const order: string[] = []
|
|
for (const row of tasks) {
|
|
const key = row.workOrderId || row.workOrderNo || row.id
|
|
if (!map.has(key)) {
|
|
map.set(key, [])
|
|
order.push(key)
|
|
}
|
|
map.get(key)!.push(row)
|
|
}
|
|
return order.map((workOrderId) => {
|
|
const groupTasks = map.get(workOrderId)!
|
|
return {
|
|
workOrderId,
|
|
workOrder: buildWorkOrderFromTasks(groupTasks),
|
|
tabKey: resolveWorkOrderTabKey(groupTasks[0]),
|
|
tasks: groupTasks
|
|
}
|
|
})
|
|
}
|
|
|
|
export function buildPresetTasksMap(entries: MineWorkOrderEntry[]): Record<string, QcTask[]> {
|
|
const map: Record<string, QcTask[]> = {}
|
|
for (const entry of entries) {
|
|
map[entry.workOrder.id] = entry.tasks
|
|
}
|
|
return map
|
|
}
|
|
|
|
export type MineCardListModel = {
|
|
inboundGroups: ReturnType<typeof groupInboundQcWorkOrders>
|
|
standaloneEntries: MineWorkOrderEntry[]
|
|
presetTasksByWorkOrderId: Record<string, QcTask[]>
|
|
hasItems: boolean
|
|
}
|
|
|
|
/** 入库走 SampleGroup,出库/临时走 SingleCard standalone */
|
|
export function buildMineCardListModel(
|
|
tasks: QcTask[],
|
|
tabKey: QcWorkOrderTabKey
|
|
): MineCardListModel {
|
|
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 standaloneEntries = useInboundGroups ? [] : entries
|
|
return {
|
|
inboundGroups,
|
|
standaloneEntries,
|
|
presetTasksByWorkOrderId,
|
|
hasItems: entries.length > 0
|
|
}
|
|
}
|