diff --git a/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts b/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts index 4e3fdf12..1a6dfd34 100644 --- a/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts @@ -243,6 +243,8 @@ export async function listInventoryMatchableOrders( orders: Array<{ workOrderId: number workOrderNo: string + platformOrderId?: string + createdAt?: string productName: string status: string rewardAmountFen: number @@ -293,6 +295,8 @@ export async function listInventoryMatchableOrders( return { workOrderId: Number(order.id), workOrderNo: order.work_order_no || '', + platformOrderId: order.platform_order_id || '', + createdAt: order.created_at || '', productName: order.product_name || '', status: String(order.status || ''), rewardAmountFen: Number(order.reward_amount || 0), diff --git a/apps/backend/src/services/worker-platform/consignment-manual-match.test.ts b/apps/backend/src/services/worker-platform/consignment-manual-match.test.ts index b0ba5379..7c232ec2 100644 --- a/apps/backend/src/services/worker-platform/consignment-manual-match.test.ts +++ b/apps/backend/src/services/worker-platform/consignment-manual-match.test.ts @@ -103,3 +103,20 @@ test('手动匹配的两个读取接口只读,不改状态', async () => { assert.ok(routes.includes('listWorkOrderInventoryMatchOptions(')) assert.ok(routes.includes('listInventoryMatchableOrders(')) }) + +test('库存侧「手动匹配订单」候选列表必须包含创建时间与平台单号', async () => { + const { readFile } = await import('node:fs/promises') + const src = await readFile( + new URL('./consignment-fulfillment-service.ts', import.meta.url), + 'utf8', + ) + const fnBody = src.slice(src.indexOf('export async function listInventoryMatchableOrders')) + assert.ok( + fnBody.includes('createdAt: order.created_at'), + 'listInventoryMatchableOrders 必须透传 order.created_at', + ) + assert.ok( + fnBody.includes('platformOrderId: order.platform_order_id'), + 'listInventoryMatchableOrders 必须透传 order.platform_order_id', + ) +}) diff --git a/apps/frontend/src/components/admin/ConsignmentManualMatchModal.tsx b/apps/frontend/src/components/admin/ConsignmentManualMatchModal.tsx index af26a340..fc20f456 100644 --- a/apps/frontend/src/components/admin/ConsignmentManualMatchModal.tsx +++ b/apps/frontend/src/components/admin/ConsignmentManualMatchModal.tsx @@ -28,6 +28,7 @@ import { resolveManualMatchRequirement, type ManualMatchType, } from '@/utils/consignment-manual-match' +import { formatDateTime } from '@/utils/date-time' import { notifyVoucherConsume } from '@/utils/voucher-consume' const { Text, Paragraph } = Typography @@ -35,8 +36,10 @@ const { Text, Paragraph } = Typography type Selected = { workOrderId: number | string workOrderLabel: string + workOrderCreatedAt?: string inventoryId: number | string inventoryLabel: string + inventoryInboundAt?: string matchType: ManualMatchType conflicts: string[] /** 供货打手(用于回显供货来源) */ @@ -48,9 +51,9 @@ export type ConsignmentManualMatchModalProps = { open: boolean onClose: () => void /** 订单侧入口:从订单出发选现货 */ - workOrder?: { workOrderId: number | string; label: string } | null + workOrder?: { workOrderId: number | string; label: string; createdAt?: string } | null /** 库存侧入口:从现货出发选订单 */ - inventory?: { inventoryId: number | string; label: string } | null + inventory?: { inventoryId: number | string; label: string; inboundAt?: string } | null onMatched?: () => void } @@ -71,6 +74,7 @@ export default function ConsignmentManualMatchModal({ const [options, setOptions] = useState([]) const [orders, setOrders] = useState([]) const [inventoryLabel, setInventoryLabel] = useState('') + const [inventoryInboundAt, setInventoryInboundAt] = useState('') const [inventoryWorker, setInventoryWorker] = useState<{ workerId: number | null workerName: string @@ -89,6 +93,7 @@ export default function ConsignmentManualMatchModal({ setConfirmedPartial(false) setConfirmedMismatch(false) setMatchNote('') + setInventoryInboundAt('') } const load = async () => { @@ -106,6 +111,7 @@ export default function ConsignmentManualMatchModal({ ? `${res.data.inventory.itemName}(${res.data.inventory.sourceSubmissionNo || res.data.inventory.inventoryNo})` : inventory.label, ) + setInventoryInboundAt(res.data?.inventory?.inboundAt || inventory.inboundAt || '') setInventoryWorker({ workerId: res.data?.inventory?.workerId ?? null, workerName: res.data?.inventory?.workerName || '', @@ -251,7 +257,9 @@ export default function ConsignmentManualMatchModal({ description={ mode === 'order' ? '下面是该订单的全部在库现货(含规格不一致的),核对差异后直接内部履约。' - : `当前现货:${inventoryLabel || inventory?.label || ''}。下面是可匹配的订单列表,核对后直接内部履约。` + : `当前现货:${inventoryLabel || inventory?.label || ''}${ + inventoryInboundAt ? `(审核入库:${formatDateTime(inventoryInboundAt)})` : '' + }。下面是可匹配的订单列表,核对后直接内部履约。` } /> @@ -312,6 +320,15 @@ export default function ConsignmentManualMatchModal({ : ''}{' '} · 货源 {option.inventory.workerName || '未关联打手'} · 结算价 ¥ {(Number(option.inventory.costPriceFen || 0) / 100).toFixed(2)} + {option.inventory.inboundAt || option.inventory.createdAt ? ( + <> + {' '} + · 审核入库{' '} + {formatDateTime( + option.inventory.inboundAt || option.inventory.createdAt, + )} + + ) : null} {option.conflicts.length > 0 ? ( @@ -330,8 +347,11 @@ export default function ConsignmentManualMatchModal({ setSelected({ workOrderId: workOrder?.workOrderId || 0, workOrderLabel: workOrder?.label || '', + workOrderCreatedAt: workOrder?.createdAt, inventoryId: option.inventory.id, inventoryLabel: option.inventory.itemName, + inventoryInboundAt: + option.inventory.inboundAt || option.inventory.createdAt, matchType: option.matchType, conflicts: option.conflicts, workerId: option.inventory.workerId, @@ -390,6 +410,9 @@ export default function ConsignmentManualMatchModal({ ? `${order.requiredSpec.diamondLevel}钻` : ''}{' '} · 报酬 ¥{(order.rewardAmountFen / 100).toFixed(2)} + {order.createdAt ? ( + <> · 创建时间 {formatDateTime(order.createdAt)} + ) : null} {order.conflicts.length > 0 ? ( @@ -408,8 +431,10 @@ export default function ConsignmentManualMatchModal({ setSelected({ workOrderId: order.workOrderId, workOrderLabel: `${order.workOrderNo || `#${order.workOrderId}`} · ${order.productName}`, + workOrderCreatedAt: order.createdAt, inventoryId: inventory?.inventoryId || 0, inventoryLabel, + inventoryInboundAt, matchType: order.matchType, conflicts: order.conflicts, workerId: inventoryWorker?.workerId ?? null, @@ -433,8 +458,26 @@ export default function ConsignmentManualMatchModal({ {selected ? (
- {selected.workOrderLabel || '—'} - {selected.inventoryLabel || '—'} + + + {selected.workOrderLabel || '—'} + {selected.workOrderCreatedAt ? ( + + (创建于 {formatDateTime(selected.workOrderCreatedAt)}) + + ) : null} + + + + + {selected.inventoryLabel || '—'} + {selected.inventoryInboundAt ? ( + + (审核入库于 {formatDateTime(selected.inventoryInboundAt)}) + + ) : null} + + {selected.workerId ? ( diff --git a/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx b/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx index 582bffd6..c43d3a70 100644 --- a/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx @@ -502,6 +502,24 @@ export default function ConsignmentInventoryPanel() { ) }, }, + { + title: '通过审核入库时间', + key: 'inboundAt', + width: 160, + render: (_, record) => { + const timeStr = formatDateTime(record.inboundAt || record.createdAt) + return ( +
+ {timeStr} + {record.consumedAt ? ( +
+ 出库: {formatDateTime(record.consumedAt)} +
+ ) : null} +
+ ) + }, + }, { title: '允许匹配', key: 'allowMatching', @@ -1032,7 +1050,19 @@ export default function ConsignmentInventoryPanel() { {viewingItem.inboundBatch || '-'} - {viewingItem.inboundAt || '-'} + + {formatDateTime(viewingItem.inboundAt || viewingItem.createdAt)} + + {viewingItem.consumedAt ? ( + + {formatDateTime(viewingItem.consumedAt)} + + ) : null} + {viewingItem.sourceType === 'worker_portal' && viewingItem.createdAt ? ( + + {formatDateTime(viewingItem.createdAt)} + + ) : null} {viewingItem.sellerContact || '-'} @@ -1225,6 +1255,7 @@ export default function ConsignmentInventoryPanel() { ? { inventoryId: manualMatchItem.id, label: manualMatchItem.itemName, + inboundAt: manualMatchItem.inboundAt || manualMatchItem.createdAt, } : null } diff --git a/apps/frontend/src/pages/admin/panels/ConsignmentSubmissionsPanel.tsx b/apps/frontend/src/pages/admin/panels/ConsignmentSubmissionsPanel.tsx index bcf0c6c6..bd33790d 100644 --- a/apps/frontend/src/pages/admin/panels/ConsignmentSubmissionsPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ConsignmentSubmissionsPanel.tsx @@ -327,8 +327,17 @@ export default function ConsignmentSubmissionsPanel() { { title: '提报时间', dataIndex: 'createdAt', - width: 150, - render: (v) => {v}, + width: 160, + render: (v, record) => ( +
+ {formatDateTime(v)} + {record.reviewedAt ? ( +
+ 审核: {formatDateTime(record.reviewedAt)} +
+ ) : null} +
+ ), }, { title: '操作', @@ -921,6 +930,11 @@ export default function ConsignmentSubmissionsPanel() { {viewingSubmission.reviewerName || '-'} + {viewingSubmission.reviewedAt ? ( + + {formatDateTime(viewingSubmission.reviewedAt)} + + ) : null} {viewingSubmission.reviewNote || '-'} diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index b97ae37f..0b479ab7 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -3426,6 +3426,7 @@ export default function WorkOrdersPanel() { ) : null} {inv.shopName ? 店铺: {inv.shopName} : null} {inv.inboundBatch ? 批次: {inv.inboundBatch} : null} + 入库时间: {formatAdminDateTime(inv.inboundAt || inv.createdAt)}
@@ -3489,6 +3490,7 @@ export default function WorkOrdersPanel() { ? { workOrderId: manualMatchOrder.workOrderId, label: `#${manualMatchOrder.workOrderId} ${manualMatchOrder.productName || ''}`, + createdAt: manualMatchOrder.createdAt, } : null } diff --git a/apps/frontend/src/types/consignment.ts b/apps/frontend/src/types/consignment.ts index 7825f348..6778862e 100644 --- a/apps/frontend/src/types/consignment.ts +++ b/apps/frontend/src/types/consignment.ts @@ -191,6 +191,8 @@ export type WorkOrderInventoryMatchOptions = { export type ConsignmentMatchableOrder = { workOrderId: number workOrderNo: string + platformOrderId?: string + createdAt?: string productName: string status: string rewardAmountFen: number diff --git a/apps/frontend/tests/consignment-manual-match-mount.test.ts b/apps/frontend/tests/consignment-manual-match-mount.test.ts index c6a63ca3..4fbfbff4 100644 --- a/apps/frontend/tests/consignment-manual-match-mount.test.ts +++ b/apps/frontend/tests/consignment-manual-match-mount.test.ts @@ -114,4 +114,45 @@ test('发布拦截弹窗:只走内部现货履约,文案停在待加好友 assert.ok(source.includes('进入待加好友'), '拦截弹窗必须写明工单进入待加好友') assert.ok(!source.includes('自动进冷静期'), '拦截弹窗不得再写自动进冷静期') assert.ok(!source.includes('进入赠送冷静期'), '成功提示不得再写成进入赠送冷静期') + assert.ok(source.includes('入库时间:'), '拦截弹窗现货展示必须包含入库时间') +}) + +test('寄售仓库库存面板:必须包含「通过审核入库时间」列,且详情弹窗正确展示时间', () => { + const source = readFileSync( + new URL('../src/pages/admin/panels/ConsignmentInventoryPanel.tsx', import.meta.url).pathname, + 'utf8', + ) + assert.ok(source.includes("title: '通过审核入库时间'"), '库存表格必须有「通过审核入库时间」列') + assert.ok( + source.includes('formatDateTime(record.inboundAt || record.createdAt)'), + '表格必须格式化入库时间', + ) + assert.ok(source.includes('label="通过审核入库时间"'), '详情弹窗必须包含「通过审核入库时间」项') + assert.ok( + source.includes('inboundAt: manualMatchItem.inboundAt || manualMatchItem.createdAt'), + '打开手动匹配弹窗时必须透传现货的入库时间', + ) +}) + +test('手动匹配弹窗:候选订单必须展示创建时间,现货展示审核入库时间', () => { + const source = readFileSync( + new URL('../src/components/admin/ConsignmentManualMatchModal.tsx', import.meta.url).pathname, + 'utf8', + ) + assert.ok( + source.includes('创建时间 {formatDateTime(order.createdAt)}'), + '候选订单卡片必须展示工单创建时间', + ) + assert.ok( + source.includes('审核入库:${formatDateTime(inventoryInboundAt)}'), + '弹窗现货提示栏必须展示审核入库时间', + ) + assert.ok( + source.includes('审核入库于 {formatDateTime(selected.inventoryInboundAt)}'), + '已选现货概要必须展示审核入库时间', + ) + assert.ok( + source.includes('创建于 {formatDateTime(selected.workOrderCreatedAt)}'), + '已选订单概要必须展示创建时间', + ) })