提交
This commit is contained in:
1 parent
427bcb1551
commit
31b04575e2
247 files changed
+39284
-20367
No files matched your search
@@ -0,0 +1,238 @@
|
||||
import type { ProductionWorkOrderItem } from '@/api/production-work-order'
|
||||
import { fetchProductionWorkOrderDetail } from '@/api/production-work-order'
|
||||
import { fetchMyProductionTasks } from '@/api/production-task'
|
||||
|
||||
export type ProductionWorkOrderTypeTab = 'inbound' | 'temporary' | 'outbound'
|
||||
|
||||
export type ProductionTaskMineFilters = {
|
||||
keyword?: string
|
||||
taskStatus?: string
|
||||
workOrderType?: ProductionWorkOrderTypeTab
|
||||
}
|
||||
|
||||
export type MineWorkOrderSearchFilters = {
|
||||
keyword?: string
|
||||
workOrderStatus?: string
|
||||
bizType?: string
|
||||
createTimeStart?: string
|
||||
createTimeEnd?: string
|
||||
infusionTimeStart?: string
|
||||
infusionTimeEnd?: string
|
||||
activeTypeTab: ProductionWorkOrderTypeTab
|
||||
}
|
||||
|
||||
export type ProductionTaskRow = Record<string, any>
|
||||
|
||||
export type MineWorkOrderEntry = {
|
||||
workOrderId: string
|
||||
workOrder: ProductionWorkOrderItem
|
||||
activeTypeTab: ProductionWorkOrderTypeTab
|
||||
tasks: ProductionTaskRow[]
|
||||
}
|
||||
|
||||
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,
|
||||
tab: ProductionWorkOrderTypeTab
|
||||
): boolean {
|
||||
const kw = keyword.trim().toLowerCase()
|
||||
if (!kw) return true
|
||||
const fields =
|
||||
tab === 'temporary'
|
||||
? [row.workOrderNo]
|
||||
: tab === 'outbound'
|
||||
? [row.workOrderNo, row.applicationNo, row.applicationName]
|
||||
: [row.workOrderNo, row.sampleCode, row.labStorageCode]
|
||||
return fields.some((f) => (f || '').toLowerCase().includes(kw))
|
||||
}
|
||||
|
||||
export function resolveWorkOrderTypeTab(task: ProductionTaskRow): ProductionWorkOrderTypeTab {
|
||||
const t = String(task.workOrderType || task.orderSource || 'inbound').trim()
|
||||
if (t === 'outbound' || t === 'temporary' || t === 'inbound') return t
|
||||
return 'inbound'
|
||||
}
|
||||
|
||||
export function buildWorkOrderFromTasks(tasks: ProductionTaskRow[]): ProductionWorkOrderItem {
|
||||
const t = tasks[0]
|
||||
const woType = resolveWorkOrderTypeTab(t)
|
||||
return {
|
||||
id: t.workOrderId,
|
||||
workOrderNo: t.workOrderNo || '—',
|
||||
workOrderType: woType,
|
||||
workOrderStatus: t.workOrderStatus || '',
|
||||
protocolNo: t.protocolNo || '',
|
||||
sampleCode: t.sampleCode || '',
|
||||
receptionSource: t.receptionSource,
|
||||
receptionSourceLabel: t.receptionSourceLabel,
|
||||
protocolStatus: t.protocolStatus,
|
||||
bizType: woType === 'outbound' ? t.bizType : undefined,
|
||||
bizTypeLabel: woType === 'outbound' ? t.bizTypeLabel : undefined,
|
||||
applicationNo: t.applicationNo,
|
||||
applicationName: t.applicationName,
|
||||
channelName: t.channelName,
|
||||
patientName: t.patientName,
|
||||
sortNo: t.sortNo,
|
||||
totalCourseCount: t.totalCourseCount,
|
||||
infusionTime: t.infusionTime,
|
||||
sampleTypeName: t.sampleTypeName,
|
||||
handlerName: t.handlerName,
|
||||
receptionTime: t.receptionTime,
|
||||
labStorageCode: t.labStorageCode,
|
||||
remark: t.workOrderRemark || t.remark,
|
||||
createTime: t.workOrderCreateTime || t.createTime || ''
|
||||
}
|
||||
}
|
||||
|
||||
export function groupMineTasksByWorkOrder(tasks: ProductionTaskRow[]): MineWorkOrderEntry[] {
|
||||
const map = new Map<string, ProductionTaskRow[]>()
|
||||
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),
|
||||
activeTypeTab: resolveWorkOrderTypeTab(groupTasks[0]),
|
||||
tasks: groupTasks
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function buildPresetTasksMap(entries: MineWorkOrderEntry[]): Record<string, ProductionTaskRow[]> {
|
||||
const map: Record<string, ProductionTaskRow[]> = {}
|
||||
for (const entry of entries) {
|
||||
map[entry.workOrder.id] = entry.tasks
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
async function fetchMineTasksByScope(
|
||||
scope: 'current' | 'history',
|
||||
filters: ProductionTaskMineFilters
|
||||
): Promise<ProductionTaskRow[]> {
|
||||
const pageSize = MINE_TASK_FETCH_PAGE_SIZE
|
||||
let page = 1
|
||||
const all: ProductionTaskRow[] = []
|
||||
let total = 0
|
||||
while (true) {
|
||||
const res = await fetchMyProductionTasks({
|
||||
scope,
|
||||
page,
|
||||
pageSize,
|
||||
keyword: filters.keyword || undefined,
|
||||
taskStatus: filters.taskStatus || undefined,
|
||||
workOrderType: filters.workOrderType || undefined
|
||||
})
|
||||
const list = res.list || []
|
||||
all.push(...list)
|
||||
total = res.total || 0
|
||||
if (!list.length || all.length >= total) break
|
||||
page += 1
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
/** 合并当前负责人任务与曾转发任务(方案 C:单列表) */
|
||||
export async function fetchAllMineProductionTasks(
|
||||
filters: ProductionTaskMineFilters
|
||||
): Promise<ProductionTaskRow[]> {
|
||||
const [current, history] = await Promise.all([
|
||||
fetchMineTasksByScope('current', filters),
|
||||
fetchMineTasksByScope('history', filters)
|
||||
])
|
||||
const map = new Map<string, ProductionTaskRow>()
|
||||
for (const row of [...current, ...history]) {
|
||||
if (row.id) map.set(row.id, row)
|
||||
}
|
||||
return [...map.values()]
|
||||
}
|
||||
|
||||
export async function enrichMineEntriesWithWorkOrders(
|
||||
entries: MineWorkOrderEntry[]
|
||||
): Promise<MineWorkOrderEntry[]> {
|
||||
const cache = new Map<string, ProductionWorkOrderItem>()
|
||||
const uniqueIds = [...new Set(entries.map((entry) => entry.workOrderId))]
|
||||
await Promise.all(
|
||||
uniqueIds.map(async (id) => {
|
||||
try {
|
||||
const detail = await fetchProductionWorkOrderDetail(id)
|
||||
cache.set(id, detail)
|
||||
} catch {
|
||||
// 保留任务聚合的工单摘要
|
||||
}
|
||||
})
|
||||
)
|
||||
return entries.map((entry) => {
|
||||
const detail = cache.get(entry.workOrderId)
|
||||
if (!detail) return entry
|
||||
return {
|
||||
...entry,
|
||||
workOrder: {
|
||||
...entry.workOrder,
|
||||
...detail,
|
||||
paymentSummary: detail.paymentSummary ?? entry.workOrder.paymentSummary,
|
||||
feeSummary: detail.feeSummary ?? entry.workOrder.feeSummary
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function filterMineEntriesByWorkOrderSearch(
|
||||
entries: MineWorkOrderEntry[],
|
||||
filters: MineWorkOrderSearchFilters
|
||||
): MineWorkOrderEntry[] {
|
||||
const tab = filters.activeTypeTab
|
||||
return entries.filter((entry) => {
|
||||
const row = entry.workOrder
|
||||
if (row.workOrderType && row.workOrderType !== tab) return false
|
||||
if (entry.activeTypeTab !== tab) return false
|
||||
if (filters.workOrderStatus && row.workOrderStatus !== filters.workOrderStatus) return false
|
||||
if (tab === 'outbound' && filters.bizType && row.bizType !== filters.bizType) return false
|
||||
if (!keywordMatchesWorkOrder(row, filters.keyword || '', tab)) return false
|
||||
if (tab === 'outbound') {
|
||||
if (!dateInRange(row.infusionTime, filters.infusionTimeStart, filters.infusionTimeEnd)) {
|
||||
return false
|
||||
}
|
||||
} else if (!dateInRange(row.createTime, filters.createTimeStart, filters.createTimeEnd)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
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 buildMineTableRows(pageEntries: MineWorkOrderEntry[]): {
|
||||
rows: ProductionWorkOrderItem[]
|
||||
presetTasksByWorkOrderId: Record<string, ProductionTaskRow[]>
|
||||
} {
|
||||
return {
|
||||
rows: pageEntries.map((e) => e.workOrder),
|
||||
presetTasksByWorkOrderId: buildPresetTasksMap(pageEntries)
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,9 @@
|
||||
|
||||
import {
|
||||
calcFqInstallmentDueDate,
|
||||
calcInstallmentDueAmountsByRatios,
|
||||
calcUnpaidAmountsByRatios,
|
||||
DEFAULT_INSTALLMENT_ADVANCE_DAYS,
|
||||
finalizeInstallmentRatios,
|
||||
defaultInstallmentAmounts,
|
||||
normalizeInstallmentAmounts,
|
||||
sortFqPaymentDetails
|
||||
} from '@/utils/protocol-payment-installment'
|
||||
import {
|
||||
@@ -156,22 +155,14 @@ export function evaluateFqParamsChange(
|
||||
|
||||
function buildFqAmounts(
|
||||
contractAmount: number | null,
|
||||
ratios: number[],
|
||||
installmentAmounts: number[],
|
||||
paidRows: DetailLike[],
|
||||
count: number
|
||||
): number[] {
|
||||
const amounts = normalizeInstallmentAmounts(installmentAmounts, contractAmount, count)
|
||||
if (paidRows.length === 0) {
|
||||
return calcInstallmentDueAmountsByRatios(contractAmount, ratios)
|
||||
return amounts
|
||||
}
|
||||
const remaining = calcRemainingAmount(contractAmount, paidRows)
|
||||
const unpaidIndices = Array.from({ length: count }, (_, i) => i).filter(
|
||||
(i) => !paidRows.some((d) => Number(d.installmentNo) === i + 1)
|
||||
)
|
||||
const unpaidAmounts = calcUnpaidAmountsByRatios(remaining, ratios, unpaidIndices)
|
||||
const amounts = Array(count).fill(0)
|
||||
unpaidIndices.forEach((idx, j) => {
|
||||
amounts[idx] = unpaidAmounts[j] ?? 0
|
||||
})
|
||||
paidRows.forEach((d) => {
|
||||
const no = Number(d.installmentNo)
|
||||
if (no >= 1 && no <= count) amounts[no - 1] = Number(d.dueAmount) || 0
|
||||
@@ -189,7 +180,11 @@ export function applyFqParamsChange(
|
||||
const interval = Number(params.installmentIntervalMonths) || 0
|
||||
const startDate = params.installmentStartDate || ''
|
||||
const advanceDays = Number(params.installmentAdvanceDays ?? DEFAULT_INSTALLMENT_ADVANCE_DAYS)
|
||||
const ratios = finalizeInstallmentRatios(params.installmentRatios || [])
|
||||
const installmentAmounts = normalizeInstallmentAmounts(
|
||||
params.installmentRatios || [],
|
||||
contractAmount,
|
||||
count
|
||||
)
|
||||
if (!count || !interval || !startDate) return details
|
||||
|
||||
const paidRows = getPaidInstallmentRows(details).map((d) => ({
|
||||
@@ -197,7 +192,7 @@ export function applyFqParamsChange(
|
||||
_payModeLocked: true
|
||||
}))
|
||||
const remainingCount = count - paidRows.length
|
||||
const amounts = buildFqAmounts(contractAmount, ratios, paidRows, count)
|
||||
const amounts = buildFqAmounts(contractAmount, installmentAmounts, paidRows, count)
|
||||
|
||||
const result: DetailLike[] = [...paidRows]
|
||||
|
||||
@@ -225,10 +220,14 @@ export function generateInitialFqDetails(
|
||||
const interval = Number(params.installmentIntervalMonths) || 0
|
||||
const startDate = params.installmentStartDate || ''
|
||||
const advanceDays = Number(params.installmentAdvanceDays ?? DEFAULT_INSTALLMENT_ADVANCE_DAYS)
|
||||
const ratios = finalizeInstallmentRatios(params.installmentRatios || [])
|
||||
const installmentAmounts = normalizeInstallmentAmounts(
|
||||
params.installmentRatios || [],
|
||||
contractAmount,
|
||||
count
|
||||
)
|
||||
if (!count || !interval || !startDate) return []
|
||||
|
||||
const amounts = calcInstallmentDueAmountsByRatios(contractAmount, ratios)
|
||||
const amounts = installmentAmounts
|
||||
const result: DetailLike[] = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
result.push(
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
|
||||
import {
|
||||
calcFqInstallmentDueDate,
|
||||
calcInstallmentDueAmountsByRatios,
|
||||
DEFAULT_INSTALLMENT_ADVANCE_DAYS,
|
||||
defaultInstallmentRatios,
|
||||
finalizeInstallmentRatios
|
||||
defaultInstallmentAmounts
|
||||
} from '@/utils/protocol-payment-installment'
|
||||
|
||||
export type PayMode = 'dj' | 'fq'
|
||||
@@ -174,10 +172,10 @@ export function applyPayModeSwitch(
|
||||
const interval = Number(ctx.installmentIntervalMonths) || 6
|
||||
const startDate = ctx.installmentStartDate || ''
|
||||
const advanceDays = Number(ctx.installmentAdvanceDays ?? DEFAULT_INSTALLMENT_ADVANCE_DAYS)
|
||||
const ratios = finalizeInstallmentRatios(
|
||||
ctx.installmentRatios?.length ? ctx.installmentRatios : defaultInstallmentRatios(count)
|
||||
)
|
||||
const amounts = calcInstallmentDueAmountsByRatios(ctx.contractAmount, ratios)
|
||||
const amounts =
|
||||
ctx.installmentRatios?.length && sumPaidAmount(rows) === 0
|
||||
? ctx.installmentRatios.map((v) => Number(v || 0))
|
||||
: defaultInstallmentAmounts(ctx.contractAmount, count)
|
||||
const result: DetailLike[] = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
result.push(
|
||||
|
||||
@@ -43,6 +43,114 @@ export function isInstallmentRatiosValid(ratios: number[]): boolean {
|
||||
return Math.abs(total - 100) < 0.01
|
||||
}
|
||||
|
||||
/** 历史数据:installmentRatios 存的是百分比(合计约 100) */
|
||||
export function isLegacyInstallmentRatios(
|
||||
values: number[],
|
||||
contractAmount?: number | null
|
||||
): boolean {
|
||||
if (!values.length) return false
|
||||
const nums = values.map((v) => Number(v || 0))
|
||||
const sum = nums.reduce((s, v) => s + v, 0)
|
||||
const max = Math.max(...nums)
|
||||
if (contractAmount != null && Math.abs(sum - Number(contractAmount)) < 0.02) {
|
||||
return false
|
||||
}
|
||||
return max <= 100 && Math.abs(sum - 100) < 0.05
|
||||
}
|
||||
|
||||
/** 将存储值规范为各期金额(元);兼容旧比例数据 */
|
||||
export function normalizeInstallmentAmounts(
|
||||
values: number[],
|
||||
contractAmount: number | null | undefined,
|
||||
count: number
|
||||
): number[] {
|
||||
if (!values.length) {
|
||||
return defaultInstallmentAmounts(contractAmount, count)
|
||||
}
|
||||
if (isLegacyInstallmentRatios(values, contractAmount)) {
|
||||
const sized =
|
||||
values.length === count ? values : resizeInstallmentRatios(values, count)
|
||||
return calcInstallmentDueAmountsByRatios(contractAmount, finalizeInstallmentRatios(sized))
|
||||
}
|
||||
const amounts = values.map((v) => Number(v || 0))
|
||||
if (amounts.length === count) return amounts
|
||||
return resizeInstallmentAmounts(amounts, count, contractAmount)
|
||||
}
|
||||
|
||||
export function defaultInstallmentAmounts(
|
||||
contractAmount: number | null | undefined,
|
||||
count: number
|
||||
): number[] {
|
||||
return calcInstallmentDueAmounts(contractAmount, count)
|
||||
}
|
||||
|
||||
export function resizeInstallmentAmounts(
|
||||
oldAmounts: number[],
|
||||
newCount: number,
|
||||
contractAmount: number | null | undefined
|
||||
): number[] {
|
||||
if (newCount <= 0) return []
|
||||
const head = oldAmounts.map((v) => Number(v || 0))
|
||||
if (newCount > head.length) {
|
||||
const existingSum = sumInstallmentAmounts(head)
|
||||
const remaining =
|
||||
contractAmount != null
|
||||
? Math.max(0, Math.round((Number(contractAmount) - existingSum) * 100) / 100)
|
||||
: 0
|
||||
const extraCount = newCount - head.length
|
||||
const extra =
|
||||
remaining > 0 && contractAmount != null
|
||||
? defaultInstallmentAmounts(remaining, extraCount)
|
||||
: Array(extraCount).fill(0)
|
||||
return [...head, ...extra]
|
||||
}
|
||||
if (newCount < head.length) {
|
||||
return head.slice(0, newCount)
|
||||
}
|
||||
return head
|
||||
}
|
||||
|
||||
export function isInstallmentAmountsValid(
|
||||
amounts: number[],
|
||||
contractAmount: number | null | undefined
|
||||
): boolean {
|
||||
if (!amounts.length || contractAmount == null) return false
|
||||
if (amounts.some((v) => Number(v) < 0)) return false
|
||||
const sum = amounts.reduce((s, v) => s + Number(v || 0), 0)
|
||||
return Math.abs(sum - Number(contractAmount)) < 0.01
|
||||
}
|
||||
|
||||
export function sumInstallmentAmounts(amounts: number[]): number {
|
||||
const total = amounts.reduce((s, v) => s + Number(v || 0), 0)
|
||||
return Math.round(total * 100) / 100
|
||||
}
|
||||
|
||||
/** 合同金额变更时,按原各期占比等比缩放金额 */
|
||||
export function scaleInstallmentAmountsToContract(
|
||||
amounts: number[],
|
||||
contractAmount: number | null | undefined
|
||||
): number[] {
|
||||
if (!amounts.length || contractAmount == null) return amounts
|
||||
const oldSum = sumInstallmentAmounts(amounts)
|
||||
if (oldSum <= 0) {
|
||||
return defaultInstallmentAmounts(contractAmount, amounts.length)
|
||||
}
|
||||
const target = Number(contractAmount)
|
||||
const scaled: number[] = []
|
||||
let sumCents = 0
|
||||
const totalCents = Math.round(target * 100)
|
||||
for (let i = 0; i < amounts.length; i++) {
|
||||
if (i === amounts.length - 1) {
|
||||
scaled.push((totalCents - sumCents) / 100)
|
||||
} else {
|
||||
const cents = Math.floor((totalCents * Number(amounts[i] || 0)) / oldSum)
|
||||
scaled.push(cents / 100)
|
||||
sumCents += cents
|
||||
}
|
||||
}
|
||||
return scaled
|
||||
}
|
||||
|
||||
export function addMonthsYmd(dateStr: string, months: number): string {
|
||||
const compact = String(dateStr || '').replace(/-/g, '')
|
||||
if (compact.length !== 8) return ''
|
||||
@@ -175,29 +283,23 @@ type DetailLike = Record<string, any>
|
||||
|
||||
function resolveFqAmounts(payment: PaymentLike, count: number, details?: DetailLike[]): number[] {
|
||||
const contract = payment?.contractAmount != null ? Number(payment.contractAmount) : null
|
||||
const ratios = Array.isArray(payment?.installmentRatios)
|
||||
const stored = Array.isArray(payment?.installmentRatios)
|
||||
? (payment!.installmentRatios as number[])
|
||||
: []
|
||||
if (ratios.length === count && contract != null) {
|
||||
if (stored.length === count && contract != null) {
|
||||
const baseAmounts = isLegacyInstallmentRatios(stored, contract)
|
||||
? calcInstallmentDueAmountsByRatios(contract, finalizeInstallmentRatios(stored))
|
||||
: stored.map((v) => Number(v || 0))
|
||||
const paidRows = (details || []).filter((d) => String(d.payStatus) === 'yjf')
|
||||
if (paidRows.length > 0) {
|
||||
const paidSum = paidRows.reduce((s, d) => s + (Number(d.dueAmount) || 0), 0)
|
||||
const remaining = Math.round((contract - paidSum) * 100) / 100
|
||||
const unpaidIndices = Array.from({ length: count }, (_, i) => i).filter(
|
||||
(i) => !paidRows.some((d) => Number(d.installmentNo) === i + 1)
|
||||
)
|
||||
const unpaidAmounts = calcUnpaidAmountsByRatios(remaining, ratios, unpaidIndices)
|
||||
const amounts = Array(count).fill(0)
|
||||
unpaidIndices.forEach((idx, j) => {
|
||||
amounts[idx] = unpaidAmounts[j] ?? 0
|
||||
})
|
||||
const amounts = [...baseAmounts]
|
||||
paidRows.forEach((d) => {
|
||||
const no = Number(d.installmentNo)
|
||||
if (no >= 1 && no <= count) amounts[no - 1] = Number(d.dueAmount) || 0
|
||||
})
|
||||
return amounts
|
||||
}
|
||||
return calcInstallmentDueAmountsByRatios(contract, ratios)
|
||||
return baseAmounts
|
||||
}
|
||||
return calcInstallmentDueAmounts(contract, count)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
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 } from '@/utils/qc-work-order-groups'
|
||||
|
||||
export type QcWorkOrderTypeTab = 'inbound' | 'temporary' | 'outbound' | 'all'
|
||||
|
||||
export type QcTaskMineFilters = {
|
||||
keyword?: string
|
||||
taskType?: string
|
||||
taskStatus?: string
|
||||
qcBizType?: string
|
||||
}
|
||||
|
||||
export type MineWorkOrderSearchFilters = {
|
||||
keyword?: string
|
||||
workOrderStatus?: string
|
||||
bizType?: string
|
||||
triggerSource?: string
|
||||
createTimeStart?: string
|
||||
createTimeEnd?: string
|
||||
infusionTimeStart?: string
|
||||
infusionTimeEnd?: string
|
||||
activeTypeTab: QcWorkOrderTypeTab
|
||||
}
|
||||
|
||||
export type MineWorkOrderEntry = {
|
||||
workOrderId: string
|
||||
workOrder: QcWorkOrder
|
||||
activeTypeTab: QcWorkOrderTypeTab
|
||||
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, tab: QcWorkOrderTypeTab): boolean {
|
||||
const kw = keyword.trim().toLowerCase()
|
||||
if (!kw) return true
|
||||
const fields =
|
||||
tab === 'temporary'
|
||||
? [row.workOrderNo]
|
||||
: tab === 'outbound'
|
||||
? [row.workOrderNo, row.applicationNo, row.applicationName]
|
||||
: [row.workOrderNo, row.sampleCode, row.labStorageCode]
|
||||
return fields.some((f) => (f || '').toLowerCase().includes(kw))
|
||||
}
|
||||
|
||||
export function filterMineEntriesByWorkOrderSearch(
|
||||
entries: MineWorkOrderEntry[],
|
||||
filters: MineWorkOrderSearchFilters
|
||||
): MineWorkOrderEntry[] {
|
||||
const tab = filters.activeTypeTab
|
||||
return entries.filter((entry) => {
|
||||
const row = entry.workOrder
|
||||
if (filters.workOrderStatus && row.workOrderStatus !== filters.workOrderStatus) return false
|
||||
if (tab === 'inbound' && filters.triggerSource && row.triggerSource !== filters.triggerSource) {
|
||||
return false
|
||||
}
|
||||
if (tab === 'outbound' && filters.bizType && row.bizType !== filters.bizType) return false
|
||||
if (!keywordMatchesWorkOrder(row, filters.keyword || '', tab)) return false
|
||||
if (tab === 'outbound') {
|
||||
if (!dateInRange(row.infusionTime, filters.infusionTimeStart, filters.infusionTimeEnd)) {
|
||||
return false
|
||||
}
|
||||
} else if (!dateInRange(row.createTime, filters.createTimeStart, filters.createTimeEnd)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchAllMineTasks(filters: QcTaskMineFilters): Promise<QcTask[]> {
|
||||
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: filters.qcBizType || undefined
|
||||
})
|
||||
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[]> {
|
||||
const cache = new Map<string, QcWorkOrder>()
|
||||
const uniqueIds = [...new Set(entries.map((entry) => entry.workOrderId))]
|
||||
await Promise.all(
|
||||
uniqueIds.map(async (id) => {
|
||||
try {
|
||||
const detail = await fetchQcWorkOrderDetail(id)
|
||||
cache.set(id, detail)
|
||||
} catch {
|
||||
// 保留任务聚合的工单摘要
|
||||
}
|
||||
})
|
||||
)
|
||||
return entries.map((entry) => {
|
||||
const detail = cache.get(entry.workOrderId)
|
||||
if (!detail) return entry
|
||||
return {
|
||||
...entry,
|
||||
workOrder: {
|
||||
...detail,
|
||||
hasLab: detail.hasLab ?? entry.workOrder.hasLab,
|
||||
hasThirdParty: detail.hasThirdParty ?? entry.workOrder.hasThirdParty
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 buildMineTableRows(pageEntries: MineWorkOrderEntry[]): {
|
||||
rows: QcWorkOrder[]
|
||||
presetTasksByWorkOrderId: Record<string, QcTask[]>
|
||||
} {
|
||||
return {
|
||||
rows: pageEntries.map((e) => e.workOrder),
|
||||
presetTasksByWorkOrderId: buildPresetTasksMap(pageEntries)
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveWorkOrderTypeTab(task: QcTask): QcWorkOrderTypeTab {
|
||||
const t = (task.qcBizType || task.orderSource || 'inbound').trim()
|
||||
if (t === 'outbound' || t === 'temporary' || t === 'inbound') return t
|
||||
return 'inbound'
|
||||
}
|
||||
|
||||
export function buildWorkOrderFromTasks(tasks: QcTask[]): QcWorkOrder {
|
||||
const t = tasks[0]
|
||||
const woType = resolveWorkOrderTypeTab(t)
|
||||
return {
|
||||
id: t.workOrderId,
|
||||
workOrderNo: t.workOrderNo || '—',
|
||||
workOrderType: woType,
|
||||
workOrderStatus: t.workOrderStatus || '',
|
||||
receptionId: '',
|
||||
protocolSampleId: '',
|
||||
collectionProtocolId: '',
|
||||
protocolNo: t.protocolNo || '',
|
||||
sampleCode: t.sampleCode || '',
|
||||
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,
|
||||
receptionTime: t.receptionTime,
|
||||
handlerName: t.handlerName,
|
||||
hasLab: tasks.some((x) => x.taskType === 'sysz'),
|
||||
hasThirdParty: tasks.some((x) => x.taskType === 'dsfjc'),
|
||||
createTime: t.createTime || '',
|
||||
bizType: woType === 'outbound' ? t.receptionSource : undefined,
|
||||
bizTypeLabel: woType === 'outbound' ? t.receptionSourceLabel : undefined,
|
||||
protocolStatus: t.protocolStatus,
|
||||
protocolStatusLabel: t.protocolStatusLabel
|
||||
}
|
||||
}
|
||||
|
||||
/** 按工单聚合任务,保持 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),
|
||||
activeTypeTab: resolveWorkOrderTypeTab(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[]): MineCardListModel {
|
||||
const entries = groupMineTasksByWorkOrder(tasks)
|
||||
const presetTasksByWorkOrderId = buildPresetTasksMap(entries)
|
||||
const inboundEntries = entries.filter((e) => e.activeTypeTab === 'inbound')
|
||||
const standaloneEntries = entries.filter((e) => e.activeTypeTab !== 'inbound')
|
||||
const inboundGroups = groupInboundQcWorkOrders(inboundEntries.map((e) => e.workOrder))
|
||||
return {
|
||||
inboundGroups,
|
||||
standaloneEntries,
|
||||
presetTasksByWorkOrderId,
|
||||
hasItems: entries.length > 0
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
* 储物格下拉选项:设施树 DFS(order_num)+ 盒内格位按行/列升序
|
||||
*/
|
||||
import { buildTreePathLabelFromFlat } from '@/utils/tree-path'
|
||||
import { fetchStorageSlotSelectOptions } from '@/api/admin'
|
||||
|
||||
export type StorageSlotSelectOption = { id: string; name: string }
|
||||
|
||||
@@ -219,6 +220,49 @@ export async function loadStorageSlotSelectOptions(
|
||||
return options
|
||||
}
|
||||
|
||||
export async function remoteSearchStorageSlotSelectOptions(
|
||||
query: string,
|
||||
opts?: LoadStorageSlotSelectOptions
|
||||
): Promise<StorageSlotSelectOption[]> {
|
||||
const includeSlotIds = (opts?.includeSlotIds || [])
|
||||
.map((id) => String(id).trim())
|
||||
.filter(Boolean)
|
||||
.join(',')
|
||||
try {
|
||||
const res = await fetchStorageSlotSelectOptions({
|
||||
keyword: query?.trim() || undefined,
|
||||
availableOnly: opts?.availableOnly ? true : undefined,
|
||||
includeSlotIds: includeSlotIds || undefined,
|
||||
page: 1,
|
||||
pageSize: 100
|
||||
})
|
||||
return (res?.list || []).map((item) => ({
|
||||
id: String(item.id || ''),
|
||||
name: String(item.name || item.fullSlotCode || item.slotCode || item.id || '')
|
||||
}))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureStorageSlotsInOptions(
|
||||
options: StorageSlotSelectOption[],
|
||||
stubs: Array<{ id?: string; name?: string }>
|
||||
): StorageSlotSelectOption[] {
|
||||
const next = [...options]
|
||||
const seen = new Set(next.map((o) => o.id))
|
||||
for (const stub of stubs) {
|
||||
const id = String(stub.id || '').trim()
|
||||
if (!id || seen.has(id)) continue
|
||||
seen.add(id)
|
||||
next.unshift({
|
||||
id,
|
||||
name: String(stub.name || id)
|
||||
})
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
/** 校验表单内储物格不可重复(一格一物) */
|
||||
export function validateUniqueStorageSlotIds(
|
||||
slotIds: Array<string | undefined | null>,
|
||||
|
||||
Reference in new issue
Block a user