feat(appointments): simplify planning and preserve source warehouse intent
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
| 柜级计划进度/详情 | `GET /platform/v1/last-leg/orders/container-summaries`;`GET /container-summaries/{sourceContainerBookingId}` | summary:`sourceContainerBookingId,containerNo?,totalOrderCount,inboundCodeCount,missingInboundCodeCount,planCount,plannedInboundCodeCount,pendingPlanCount,plannedArrivalAt?,allPlansReady,blockingReason?`;list query:`page,size,containerNo?,createdFrom?,createdTo?`;detail:`summary,orders[]`,订单与计划 ID 均为字符串 | 独立“柜子”Tab 消费服务端全柜分页,默认按计划到仓升序且 null 最后;详情抽屉显示柜内全部订单、入仓码和独立计划状态;同一摘要也供按柜生成选择框,不从订单分页本地拼柜 |
|
||||
| 入库列表 | `GET /platform/v1/last-leg/inbound-plans` | query:`page,size,planNo,inboundCode,status,warehouse,fbaWarehouseCode,appointmentStatus,dispatchStatus,createdFrom,createdTo`;平台跨客户;每行返回计划级 `assigned/mergeAllowed/urgent` | `...:inbound-order:list`;`lineId/planId/orderId/customerId` 均为字符串;合托展示为可选提示,不作为完成门禁 |
|
||||
| 入库详情 | `GET /platform/v1/last-leg/inbound-plans/{planNo}` | query:显式 `customerId`;path 编码;`allocations[]` 直接返回实际 `palletNo`、`palletOwnerPlanNo`、`palletOwnedByCurrentPlan` | 同 list;缺客户范围时页面明确报错;`sourceOrderId` 字符串化;跨计划合托按分配事实展示真实托盘码、所属计划和本单箱数,不把外部托盘计入当前计划自有托盘数 |
|
||||
| FBA 预约新增 | `POST /platform/v1/last-leg/inbound-plans/fba-appointments/batch` | body:`customerId,lineIds:string[],appointmentNo,appointmentAt,expectedLoadingAt,appointmentStatus,appointmentRemark?` | `...:inbound-order:fba-appointment`;只允许同客户、同 FBA 仓勾选 |
|
||||
| FBA 预约新增 | `POST /platform/v1/last-leg/inbound-plans/fba-appointments/batch` | body:`lineIds:string[],details,idempotencyKey`;details区分计划日期/时段、确认号/时段及状态 | `...:inbound-order:fba-appointment`;跨客户同美国起运仓、同FBA仓;服务端将选择明细转换成原稳定货源并生成系统单号,不输入主单客户或预计装车时间 |
|
||||
| FBA 预约详情/编辑 | `GET /fba-appointments/{appointmentNo}?customerId=...`;`PUT` 同 path | PUT body:`customerId,appointmentAt,expectedLoadingAt,appointmentStatus,appointmentRemark?` | GET 同 list;PUT 同 appointment;失败保留弹框和错误元数据 |
|
||||
| 排车列表/详情 | `GET /platform/v1/last-leg/dispatches`;`GET /dispatches/{dispatchNo}` | list query:`page,size,dispatchNo,vehiclePlate,status,fbaAppointmentNo`;不传客户 | `...:inbound-order:list`;Controller 受 `lastleg.dispatch-query-api.enabled` 控制 |
|
||||
| 排车候选 | `GET /platform/v1/last-leg/dispatches/candidates` | query:`planIds?`(逗号分隔长 ID)、`orderNo?`,`fbaWarehouseCode?`,`sourceStage?`,`draftToken?`;退回候选返回`sourceType=RETURN_RECEIPT,returnReceiptId,returnReceiptNo`,箱数为实际剩余可拣量 | 同 inbound list;已实收阶段包含真实退回来源,展示业务凭据号;草稿恢复失败保留选择并允许重试 |
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('inbound order overseas API', () => {
|
||||
vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: detail })
|
||||
|
||||
await getFbaAppointment('APT/1')
|
||||
await updateFbaAppointment('APT/1', { version: detail.version, appointmentAt: detail.appointmentAt, expectedLoadingAt: detail.expectedLoadingAt, appointmentStatus: 'SUCCESS' })
|
||||
await updateFbaAppointment('APT/1', { version: detail.version, idempotencyKey: 'update-key', details: {appointmentAt: detail.appointmentAt, confirmationNo: 'EXT-1', appointmentStatus: 'SUCCESS'} })
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/fba-appointments/APT%2F1')
|
||||
expect(http.put).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/fba-appointments/APT%2F1', expect.not.objectContaining({ customerId: expect.anything() }))
|
||||
@@ -50,11 +50,9 @@ describe('inbound order overseas API', () => {
|
||||
it('preserves selected line ids as strings when creating an appointment', async () => {
|
||||
vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', appointmentNo: 'APT-1', appointmentStatus: 'SUCCESS', selectedCount: 1, fbaWarehouseCode: 'ONT8' } })
|
||||
const payload = {
|
||||
lineIds: ['190000000000000099'],
|
||||
appointmentNo: 'APT-1',
|
||||
appointmentAt: '2026-08-20T10:30:00',
|
||||
expectedLoadingAt: '2026-08-20T09:00:00',
|
||||
appointmentStatus: 'SUCCESS' as const,
|
||||
originWarehouseId: '100', fbaWarehouseCode: 'ONT8', lineIds: ['190000000000000099'],
|
||||
idempotencyKey: 'create-key',
|
||||
details: { plannedDate: '2026-08-20', appointmentStatus: 'PENDING' as const },
|
||||
}
|
||||
await createFbaAppointment(payload)
|
||||
expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/fba-appointments/batch', payload)
|
||||
|
||||
@@ -53,31 +53,48 @@ export interface InboundOrderSummaryDto {
|
||||
version?: number | null
|
||||
}
|
||||
|
||||
export interface BatchFbaAppointmentPayload {
|
||||
lineIds: string[]
|
||||
appointmentNo: string
|
||||
appointmentAt: string
|
||||
export interface FbaAppointmentDetailsPayload {
|
||||
plannedDate?: string | null
|
||||
plannedAt?: string | null
|
||||
plannedEndAt?: string | null
|
||||
confirmationNo?: string | null
|
||||
appointmentAt?: string | null
|
||||
appointmentEndAt?: string | null
|
||||
expectedLoadingAt: string
|
||||
appointmentStatus: 'PENDING' | 'SUCCESS' | 'FAILED' | 'CANCELLED'
|
||||
appointmentRemark?: string
|
||||
appointmentRemark?: string | null
|
||||
startOffset?: string | null
|
||||
endOffset?: string | null
|
||||
}
|
||||
export interface BatchFbaAppointmentPayload {
|
||||
originWarehouseId: string
|
||||
fbaWarehouseCode: string
|
||||
lineIds: string[]
|
||||
details: FbaAppointmentDetailsPayload
|
||||
idempotencyKey: string
|
||||
}
|
||||
|
||||
export interface FbaAppointmentDto {
|
||||
version: number
|
||||
id: string
|
||||
appointmentNo: string
|
||||
appointmentAt: string
|
||||
confirmationNo?: string | null
|
||||
plannedDate?: string | null
|
||||
plannedAt?: string | null
|
||||
plannedEndAt?: string | null
|
||||
appointmentAt?: string | null
|
||||
appointmentEndAt?: string | null
|
||||
expectedLoadingAt: string
|
||||
timezone?: string | null
|
||||
appointmentStartUtc?: string | null
|
||||
appointmentEndUtc?: string | null
|
||||
originWarehouseId: string
|
||||
appointmentStatus: string
|
||||
appointmentRemark?: string | null
|
||||
selectedCount: number
|
||||
fbaWarehouseCode: string
|
||||
items: Array<{ customerId: string; id: string; planId: string; lineId: string; orderId: string; inboundCode: string; fbaWarehouseCode: string }>
|
||||
items: Array<{ customerId: string; id: string; sourceKey: string; sourceVersion?: string | null; orderId?: string | null; inboundCode?: string | null; fbaWarehouseCode: string; packageCount?: number | null }>
|
||||
}
|
||||
|
||||
export type UpdateFbaAppointmentPayload = Omit<BatchFbaAppointmentPayload, 'lineIds' | 'appointmentNo'> & { version: number }
|
||||
export interface UpdateFbaAppointmentPayload { details: FbaAppointmentDetailsPayload; version: number; idempotencyKey: string }
|
||||
|
||||
export async function listFbaAppointmentCalendar(query: { from: string; to: string }, signal?: AbortSignal) {
|
||||
const response = await http.get<ApiEnvelope<FbaAppointmentDto[]>, ApiEnvelope<FbaAppointmentDto[]>>(
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { appointmentFormError, appointmentPayload, createAppointmentForm } from './appointmentForm'
|
||||
|
||||
it('allows an undated arrangement without inventing confirmation or loading facts', () => {
|
||||
const form = createAppointmentForm()
|
||||
expect(appointmentFormError(form)).toBeNull()
|
||||
expect(appointmentPayload(form)).toMatchObject({ plannedDate: null, plannedAt: null, appointmentAt: null, confirmationNo: null, appointmentStatus: 'PENDING' })
|
||||
expect(appointmentPayload(form)).not.toHaveProperty('expectedLoadingAt')
|
||||
})
|
||||
it('retains an already confirmed repeated local time using its stored UTC instant', () => {
|
||||
const form = createAppointmentForm({ id: '1', version: 0, appointmentNo: 'AP-1', confirmationNo: 'EXT-1', originWarehouseId: '100', fbaWarehouseCode: 'ONT8', appointmentStatus: 'SUCCESS', selectedCount: 1, items: [], appointmentAt: '2026-11-01T01:30:00', appointmentStartUtc: '2026-11-01T09:30:00Z', timezone: 'America/Los_Angeles' })
|
||||
expect(form.startOffset).toBe('-08:00')
|
||||
expect(appointmentPayload(form).endOffset).toBeNull()
|
||||
expect(appointmentPayload(form).appointmentAt).toBe('2026-11-01T01:30:00')
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { FbaAppointmentDetailsPayload, FbaAppointmentDto } from './api/inboundOrders'
|
||||
|
||||
export interface AppointmentForm {
|
||||
plannedDate: string; plannedAt: string; plannedEndAt: string; confirmationNo: string
|
||||
appointmentAt: string; appointmentEndAt: string; appointmentStatus: FbaAppointmentDetailsPayload['appointmentStatus']
|
||||
appointmentRemark: string; startOffset: string; endOffset: string
|
||||
}
|
||||
export function createAppointmentForm(detail?: FbaAppointmentDto): AppointmentForm {
|
||||
return { plannedDate: detail?.plannedDate || '', plannedAt: detail?.plannedAt || '', plannedEndAt: detail?.plannedEndAt || '', confirmationNo: detail?.confirmationNo || '',
|
||||
appointmentAt: detail?.appointmentAt || '', appointmentEndAt: detail?.appointmentEndAt || '', appointmentStatus: (detail?.appointmentStatus || 'PENDING') as AppointmentForm['appointmentStatus'],
|
||||
appointmentRemark: detail?.appointmentRemark || '', startOffset: confirmedOffset(detail?.appointmentAt, detail?.appointmentStartUtc), endOffset: confirmedOffset(detail?.appointmentEndAt, detail?.appointmentEndUtc) }
|
||||
}
|
||||
function confirmedOffset(local?: string | null, utc?: string | null): string {
|
||||
if (!local || !utc) return ''
|
||||
const minutes = (Date.parse(`${local}Z`) - Date.parse(utc)) / 60000
|
||||
if (!Number.isInteger(minutes) || Math.abs(minutes) > 18 * 60) return ''
|
||||
return `${minutes < 0 ? '-' : '+'}${String(Math.floor(Math.abs(minutes) / 60)).padStart(2, '0')}:${String(Math.abs(minutes) % 60).padStart(2, '0')}`
|
||||
}
|
||||
export function appointmentPayload(form: AppointmentForm): FbaAppointmentDetailsPayload {
|
||||
return { plannedDate: form.plannedDate || null, plannedAt: form.plannedAt || null, plannedEndAt: form.plannedEndAt || null, confirmationNo: form.confirmationNo.trim() || null,
|
||||
appointmentAt: form.appointmentAt || null, appointmentEndAt: form.appointmentEndAt || null, appointmentStatus: form.appointmentStatus, appointmentRemark: form.appointmentRemark.trim() || null,
|
||||
startOffset: form.startOffset.trim() || null, endOffset: form.endOffset.trim() || null }
|
||||
}
|
||||
export function appointmentFormError(form: AppointmentForm): string | null {
|
||||
if (form.appointmentStatus === 'SUCCESS' && (!form.confirmationNo.trim() || !form.appointmentAt)) return '登记对方确认时,请填写外部约号与确认时间'
|
||||
if (['FAILED', 'CANCELLED'].includes(form.appointmentStatus) && !form.appointmentRemark.trim()) return '请填写取消或未约成的原因'
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import type { AppointmentForm } from '../appointmentForm'
|
||||
defineProps<{ model: AppointmentForm; appointmentNo?: string; timezone?: string | null; disabled?: boolean }>()
|
||||
</script>
|
||||
<template>
|
||||
<p>{{ appointmentNo ? `系统单号:${appointmentNo}` : '系统单号将在保存后生成' }} · {{ timezone || '目的仓时区待核实' }}</p>
|
||||
<el-form label-position="top" class="appointment-fields density-form" :disabled="disabled">
|
||||
<el-form-item label="计划送仓日期"><el-date-picker v-model="model.plannedDate" type="date" value-format="YYYY-MM-DD" placeholder="日期未定可留空" /></el-form-item>
|
||||
<el-form-item label="本次登记"><el-select v-model="model.appointmentStatus"><el-option label="先保存安排,等待对方确认" value="PENDING" /><el-option label="已取得对方正式确认" value="SUCCESS" /><el-option v-if="appointmentNo" label="未约成" value="FAILED" /><el-option v-if="appointmentNo" label="取消安排" value="CANCELLED" /></el-select></el-form-item>
|
||||
<template v-if="model.appointmentStatus === 'SUCCESS'">
|
||||
<el-form-item label="外部约号" required><el-input v-model="model.confirmationNo" maxlength="128" placeholder="填写收货方提供的确认号" /></el-form-item>
|
||||
<el-form-item label="对方确认开始时间" required><el-date-picker v-model="model.appointmentAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="目的仓当地时间" @change="model.startOffset = ''" /></el-form-item>
|
||||
<el-form-item label="对方确认结束时间"><el-date-picker v-model="model.appointmentEndAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="未明确可留空" @change="model.endOffset = ''" /></el-form-item>
|
||||
</template>
|
||||
<el-form-item :label="['FAILED','CANCELLED'].includes(model.appointmentStatus) ? '原因' : '备注'" :required="['FAILED','CANCELLED'].includes(model.appointmentStatus)"><el-input v-model="model.appointmentRemark" maxlength="1000" placeholder="有需交接的事项时填写" /></el-form-item>
|
||||
<details class="appointment-fields__extra"><summary>补充计划时段与夏令时信息</summary>
|
||||
<el-form-item label="计划开始时段"><el-date-picker v-model="model.plannedAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="只知道日期时不必填写" /></el-form-item>
|
||||
<el-form-item label="计划结束时段"><el-date-picker v-model="model.plannedEndAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item>
|
||||
<template v-if="model.appointmentStatus === 'SUCCESS'"><p>当地时钟回拨、同一时间出现两次时,需按对方确认明确时差。</p>
|
||||
<el-form-item label="确认开始时差"><el-input v-model="model.startOffset" placeholder="仅重叠时刻需填,如 -07:00" /></el-form-item>
|
||||
<el-form-item label="确认结束时差"><el-input v-model="model.endOffset" placeholder="仅重叠时刻需填,如 -08:00" /></el-form-item>
|
||||
</template>
|
||||
</details>
|
||||
</el-form>
|
||||
</template>
|
||||
<style scoped>
|
||||
.appointment-fields { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:0 var(--admin-space-6); }
|
||||
.appointment-fields :deep(.el-select), .appointment-fields :deep(.el-date-editor) { width:100%; }
|
||||
.appointment-fields__extra { grid-column:1 / -1; }
|
||||
</style>
|
||||
@@ -2,5 +2,7 @@ export {
|
||||
createFbaAppointment, getFbaAppointment, listFbaAppointmentCalendar,
|
||||
listInboundOrders, updateFbaAppointment,
|
||||
} from './api/inboundOrders'
|
||||
export type { FbaAppointmentDto, BatchFbaAppointmentPayload, InboundOrderSummaryDto } from './api/inboundOrders'
|
||||
export type { FbaAppointmentDto, BatchFbaAppointmentPayload, UpdateFbaAppointmentPayload, FbaAppointmentDetailsPayload, InboundOrderSummaryDto } from './api/inboundOrders'
|
||||
export { default as FbaAppointmentFields } from './components/FbaAppointmentFields.vue'
|
||||
export { createAppointmentForm, appointmentPayload, appointmentFormError } from './appointmentForm'
|
||||
export { overseasWarehousePermissions } from './permissions'
|
||||
|
||||
@@ -15,6 +15,8 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import SmartDispatchDialog from '../components/SmartDispatchDialog.vue'
|
||||
import FbaAppointmentFields from '../components/FbaAppointmentFields.vue'
|
||||
import { createAppointmentForm, appointmentPayload, appointmentFormError } from '../appointmentForm'
|
||||
import { createFbaAppointment, getFbaAppointment, listInboundOrders, updateFbaAppointment, updateInboundPlan, type InboundOrderSummaryDto } from '../api/inboundOrders'
|
||||
import { listReceivingOverseasWarehouses, type ReceivingOverseasWarehouseDto } from '../api/pallets'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
@@ -53,7 +55,9 @@ const editingAppointmentNo = ref('')
|
||||
const appointmentSelectedCount = ref(0)
|
||||
const appointmentFbaWarehouse = ref('—')
|
||||
const appointmentVersion = ref(0)
|
||||
const appointmentForm = reactive({ appointmentEndAt: '', appointmentNo: '', appointmentAt: '', expectedLoadingAt: '', appointmentStatus: 'SUCCESS' as 'PENDING' | 'SUCCESS' | 'FAILED' | 'CANCELLED', appointmentRemark: '' })
|
||||
const appointmentForm = reactive(createAppointmentForm())
|
||||
const appointmentTimezone = ref<string | null>(null)
|
||||
const pendingAppointment = ref<{appointmentNo: string | null; version: number; originWarehouseId: string; fbaWarehouseCode: string; lineIds: string[]; details: ReturnType<typeof appointmentPayload>; idempotencyKey: string} | null>(null)
|
||||
const appointmentTitle = computed(() => appointmentMode.value === 'edit' ? '编辑预约' : '预约')
|
||||
const planEditOpen = ref(false); const planEditLoading = ref(false); const planEditSaving = ref(false); const planEditTarget = ref<InboundOrderListItem | null>(null); const planEditForm = reactive({ warehouseId: '', warehouseCode: '', warehouseName: '', version: 0 }); const warehouses = ref<ReceivingOverseasWarehouseDto[]>([])
|
||||
|
||||
@@ -61,7 +65,7 @@ function selectableRow(source: object) {
|
||||
const row = source as InboundOrderListItem
|
||||
if (!row.fbaWarehouse || row.fbaWarehouse === '—') return false
|
||||
const anchor = selectedRows.value[0]
|
||||
return !anchor || anchor.fbaWarehouse === row.fbaWarehouse
|
||||
return !anchor || (anchor.fbaWarehouse === row.fbaWarehouse && anchor.originWarehouseId === row.originWarehouseId)
|
||||
}
|
||||
|
||||
const columns: DataTableColumn[] = [
|
||||
@@ -102,7 +106,7 @@ function appointmentStatusLabel(value: unknown) { return !value ? '未预约' :
|
||||
function toListItem(dto: InboundOrderSummaryDto): InboundOrderListItem {
|
||||
return {
|
||||
lineId: String(dto.lineId), planId: String(dto.planId), customerId: String(dto.customerId), orderId: String(dto.orderId),
|
||||
warehouseId: text(dto.warehouseId) === '—' ? '' : String(dto.warehouseId), destinationWarehouseCode: text(dto.warehouseCode), destinationWarehouseName: text(dto.warehouseName),
|
||||
originWarehouseId: String(dto.overseasWarehouseId || ''), warehouseId: text(dto.warehouseId) === '—' ? '' : String(dto.warehouseId), destinationWarehouseCode: text(dto.warehouseCode), destinationWarehouseName: text(dto.warehouseName),
|
||||
orderNo: text(dto.planNo), inboundCode: text(dto.inboundCode), lxCode: text(dto.lxCode), type: typeLabel(dto.orderType), warehouse: receivingWarehouse(dto.overseasWarehouseCode, dto.overseasWarehouseName),
|
||||
fbaWarehouse: text(dto.fbaWarehouseCode), owner: text(dto.customerName), cartons: number(dto.packageCount), pickedCartons: number(dto.pickedPackageCount),
|
||||
grossWeight: number(dto.grossWeightKg), netWeight: number(dto.netWeightKg), volume: number(dto.volumeCbm), pallets: number(dto.palletCount),
|
||||
@@ -117,6 +121,7 @@ function onSelectionChange(selection: object[]) { selectedRows.value = selection
|
||||
function selectionError() {
|
||||
if (!selectedRows.value.length) return '请至少选择 1 条入库单'
|
||||
if (selectedRows.value.some((row) => !row.fbaWarehouse || row.fbaWarehouse === '—')) return '所选入库单缺少 FBA 仓库信息'
|
||||
if (selectedRows.value.some(row => !row.originWarehouseId) || new Set(selectedRows.value.map(row => row.originWarehouseId)).size !== 1) return '所选入库单必须属于同一接仓点'
|
||||
if (new Set(selectedRows.value.map((row) => row.fbaWarehouse)).size !== 1) return '所选入库单必须属于同一 FBA 仓库'
|
||||
return ''
|
||||
}
|
||||
@@ -127,7 +132,7 @@ function openAppointment() {
|
||||
appointmentMode.value = 'create'; editingAppointmentNo.value = ''
|
||||
appointmentSelectedCount.value = selectedRows.value.length; appointmentFbaWarehouse.value = selectedRows.value[0]?.fbaWarehouse || '—'
|
||||
appointmentVersion.value = 0
|
||||
Object.assign(appointmentForm, { appointmentEndAt: '', appointmentNo: '', appointmentAt: '', expectedLoadingAt: '', appointmentStatus: 'SUCCESS', appointmentRemark: '' })
|
||||
Object.assign(appointmentForm, createAppointmentForm()); appointmentTimezone.value = null; pendingAppointment.value = null
|
||||
mutationError.value = null
|
||||
appointmentOpen.value = true
|
||||
}
|
||||
@@ -146,7 +151,7 @@ async function openAppointmentEdit(source: object) {
|
||||
appointmentSelectedCount.value = detail.selectedCount
|
||||
appointmentFbaWarehouse.value = detail.fbaWarehouseCode || row.fbaWarehouse
|
||||
appointmentVersion.value = detail.version
|
||||
Object.assign(appointmentForm, { appointmentEndAt: detail.appointmentEndAt || '', appointmentNo: detail.appointmentNo, appointmentAt: detail.appointmentAt, expectedLoadingAt: detail.expectedLoadingAt, appointmentStatus: detail.appointmentStatus, appointmentRemark: detail.appointmentRemark || '' })
|
||||
Object.assign(appointmentForm, createAppointmentForm(detail)); appointmentTimezone.value = detail.timezone || null; pendingAppointment.value = null
|
||||
} catch (caught) {
|
||||
appointmentOpen.value = false
|
||||
mutationError.value = getApiErrorDetails(caught, 'FBA 预约详情加载失败,请重试')
|
||||
@@ -155,14 +160,17 @@ async function openAppointmentEdit(source: object) {
|
||||
|
||||
async function submitAppointment() {
|
||||
if (appointmentMode.value === 'create') { const error = selectionError(); if (error) { ElMessage.warning(error); return } }
|
||||
if (!appointmentForm.appointmentNo.trim() || !appointmentForm.appointmentAt || !appointmentForm.expectedLoadingAt) { ElMessage.warning('请填写预约号、预约送仓时间和预计装车时间'); return }
|
||||
const validation = appointmentFormError(appointmentForm)
|
||||
if (!pendingAppointment.value && validation) { ElMessage.warning(validation); return }
|
||||
saving.value = true; mutationError.value = null
|
||||
try {
|
||||
const common = { appointmentEndAt: appointmentForm.appointmentEndAt || null, appointmentAt: appointmentForm.appointmentAt, expectedLoadingAt: appointmentForm.expectedLoadingAt, appointmentStatus: appointmentForm.appointmentStatus, appointmentRemark: appointmentForm.appointmentRemark.trim() || undefined }
|
||||
if (appointmentMode.value === 'edit') await updateFbaAppointment(editingAppointmentNo.value, { ...common, version: appointmentVersion.value })
|
||||
else await createFbaAppointment({ lineIds: selectedRows.value.map((row) => row.lineId), appointmentNo: appointmentForm.appointmentNo.trim(), ...common })
|
||||
pendingAppointment.value ||= {appointmentNo: appointmentMode.value === 'edit' ? editingAppointmentNo.value : null, version: appointmentVersion.value, originWarehouseId: selectedRows.value[0]?.originWarehouseId || '', fbaWarehouseCode: selectedRows.value[0]?.fbaWarehouse || '', lineIds: selectedRows.value.map(row => row.lineId), details: appointmentPayload(appointmentForm), idempotencyKey: crypto.randomUUID()}
|
||||
const payload = pendingAppointment.value
|
||||
if (payload.appointmentNo) await updateFbaAppointment(payload.appointmentNo, { details: payload.details, version: payload.version, idempotencyKey: payload.idempotencyKey })
|
||||
else await createFbaAppointment({ originWarehouseId: payload.originWarehouseId, fbaWarehouseCode: payload.fbaWarehouseCode, lineIds: payload.lineIds, details: payload.details, idempotencyKey: payload.idempotencyKey })
|
||||
pendingAppointment.value = null
|
||||
appointmentOpen.value = false; selectedRows.value = []; ElMessage.success('FBA 预约保存成功'); await load()
|
||||
} catch (caught) { mutationError.value = getApiErrorDetails(caught, 'FBA 预约保存失败,请重试') }
|
||||
} catch (caught) { mutationError.value = getApiErrorDetails(caught, 'FBA 预约保存失败,请重试'); if (['400','403','404','409','A0400','A0403','A0404','A0409'].includes(mutationError.value.code || '')) pendingAppointment.value = null }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
@@ -205,17 +213,11 @@ onMounted(() => { void load() })
|
||||
<template #pagination-left>共 {{ total }} 条入库单 · 已选 {{ selectedRows.length }} 条</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout>
|
||||
<AppDialog class="admin-density-dialog" v-model="appointmentOpen" :title="appointmentTitle" width="min(1080px, calc(100vw - 48px))" confirm-text="保存预约" :confirm-loading="saving" @confirm="submitAppointment">
|
||||
<AppDialog class="admin-density-dialog" v-model="appointmentOpen" :title="appointmentTitle" width="min(1080px, calc(100vw - 48px))" :confirm-text="pendingAppointment ? '重试本次保存' : '保存预约安排'" :confirm-loading="saving" @confirm="submitAppointment">
|
||||
<div v-loading="appointmentLoading">
|
||||
<div class="appointment-summary">已选择 {{ appointmentSelectedCount }} 条,FBA仓库:{{ appointmentFbaWarehouse }}</div>
|
||||
<el-form label-position="top" class="appointment-form density-form">
|
||||
<el-form-item label="预约号" required><el-input v-model="appointmentForm.appointmentNo" maxlength="64" :readonly="appointmentMode === 'edit'" placeholder="请输入预约号" /></el-form-item>
|
||||
<el-form-item label="预约送仓时间" required><el-date-picker v-model="appointmentForm.appointmentAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预约送仓时间" /></el-form-item>
|
||||
<el-form-item label="预约结束时间"><el-date-picker v-model="appointmentForm.appointmentEndAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="未明确可留空" /></el-form-item>
|
||||
<el-form-item label="预计装车时间" required><el-date-picker v-model="appointmentForm.expectedLoadingAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计装车时间" /></el-form-item>
|
||||
<el-form-item label="预约状态" required><el-select v-model="appointmentForm.appointmentStatus" placeholder="请选择预约状态"><el-option label="预约中" value="PENDING" /><el-option label="预约成功" value="SUCCESS" /><el-option label="预约失败" value="FAILED" /><el-option label="已取消" value="CANCELLED" /></el-select></el-form-item>
|
||||
<el-form-item label="预约备注"><el-input v-model="appointmentForm.appointmentRemark" maxlength="1000" placeholder="请输入预约备注" /></el-form-item>
|
||||
</el-form>
|
||||
<FbaAppointmentFields :model="appointmentForm" :appointment-no="editingAppointmentNo || undefined" :timezone="appointmentTimezone" :disabled="saving || !!pendingAppointment" />
|
||||
|
||||
</div>
|
||||
</AppDialog>
|
||||
<SmartDispatchDialog v-model="smartDispatchOpen" :orders="selectedRows" @created="selectedRows = []; load()" />
|
||||
|
||||
@@ -210,7 +210,8 @@ describe('overseas warehouse first-batch prototype contract', () => {
|
||||
expect(value).toContain("key: 'selection', type: 'selection'")
|
||||
expect(value).toContain('@selection-change="onSelectionChange"')
|
||||
expect(value).toContain('createFbaAppointment')
|
||||
expect(value).toContain('预计装车时间')
|
||||
expect(value).toContain('FbaAppointmentFields')
|
||||
expect(value).not.toContain('appointmentForm.expectedLoadingAt')
|
||||
expect(value).toContain('所选入库单必须属于同一 FBA 仓库')
|
||||
expect(value).not.toContain('anchor.customerId === row.customerId')
|
||||
expect(value).toContain('anchor.fbaWarehouse === row.fbaWarehouse')
|
||||
|
||||
@@ -66,6 +66,7 @@ export interface LastMileOrderListItem {
|
||||
}
|
||||
|
||||
export interface InboundOrderListItem {
|
||||
originWarehouseId: string
|
||||
lineId: string
|
||||
planId: string
|
||||
customerId: string
|
||||
|
||||
Reference in New Issue
Block a user