feat: add appointment calendar and cross-customer booking
This commit is contained in:
@@ -50,10 +50,10 @@ const smartDispatchOpen = ref(false)
|
||||
const appointmentLoading = ref(false)
|
||||
const appointmentMode = ref<'create' | 'edit'>('create')
|
||||
const editingAppointmentNo = ref('')
|
||||
const editingCustomerId = ref('')
|
||||
const appointmentSelectedCount = ref(0)
|
||||
const appointmentFbaWarehouse = ref('—')
|
||||
const appointmentForm = reactive({ appointmentNo: '', appointmentAt: '', expectedLoadingAt: '', appointmentStatus: 'SUCCESS' as 'PENDING' | 'SUCCESS' | 'FAILED' | 'CANCELLED', appointmentRemark: '' })
|
||||
const appointmentVersion = ref(0)
|
||||
const appointmentForm = reactive({ appointmentEndAt: '', appointmentNo: '', appointmentAt: '', expectedLoadingAt: '', appointmentStatus: 'SUCCESS' as 'PENDING' | 'SUCCESS' | 'FAILED' | 'CANCELLED', appointmentRemark: '' })
|
||||
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 +61,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.customerId === row.customerId && anchor.fbaWarehouse === row.fbaWarehouse)
|
||||
return !anchor || anchor.fbaWarehouse === row.fbaWarehouse
|
||||
}
|
||||
|
||||
const columns: DataTableColumn[] = [
|
||||
@@ -117,7 +117,6 @@ 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 (new Set(selectedRows.value.map((row) => row.customerId)).size !== 1) return '所选入库单必须属于同一客户'
|
||||
if (new Set(selectedRows.value.map((row) => row.fbaWarehouse)).size !== 1) return '所选入库单必须属于同一 FBA 仓库'
|
||||
return ''
|
||||
}
|
||||
@@ -125,9 +124,10 @@ function selectionError() {
|
||||
function openAppointment() {
|
||||
const error = selectionError()
|
||||
if (error) { ElMessage.warning(error); return }
|
||||
appointmentMode.value = 'create'; editingAppointmentNo.value = ''; editingCustomerId.value = ''
|
||||
appointmentMode.value = 'create'; editingAppointmentNo.value = ''
|
||||
appointmentSelectedCount.value = selectedRows.value.length; appointmentFbaWarehouse.value = selectedRows.value[0]?.fbaWarehouse || '—'
|
||||
Object.assign(appointmentForm, { appointmentNo: '', appointmentAt: '', expectedLoadingAt: '', appointmentStatus: 'SUCCESS', appointmentRemark: '' })
|
||||
appointmentVersion.value = 0
|
||||
Object.assign(appointmentForm, { appointmentEndAt: '', appointmentNo: '', appointmentAt: '', expectedLoadingAt: '', appointmentStatus: 'SUCCESS', appointmentRemark: '' })
|
||||
mutationError.value = null
|
||||
appointmentOpen.value = true
|
||||
}
|
||||
@@ -139,13 +139,14 @@ function openSmartDispatch() {
|
||||
async function openAppointmentEdit(source: object) {
|
||||
const row = source as InboundOrderListItem
|
||||
if (!row.appointmentNo || row.appointmentNo === '—') return
|
||||
appointmentMode.value = 'edit'; editingAppointmentNo.value = row.appointmentNo; editingCustomerId.value = row.customerId
|
||||
appointmentMode.value = 'edit'; editingAppointmentNo.value = row.appointmentNo
|
||||
appointmentLoading.value = true; mutationError.value = null; appointmentOpen.value = true
|
||||
try {
|
||||
const detail = await getFbaAppointment(row.customerId, row.appointmentNo)
|
||||
const detail = await getFbaAppointment(row.appointmentNo)
|
||||
appointmentSelectedCount.value = detail.selectedCount
|
||||
appointmentFbaWarehouse.value = detail.fbaWarehouseCode || row.fbaWarehouse
|
||||
Object.assign(appointmentForm, { appointmentNo: detail.appointmentNo, appointmentAt: detail.appointmentAt, expectedLoadingAt: detail.expectedLoadingAt, appointmentStatus: detail.appointmentStatus, appointmentRemark: detail.appointmentRemark || '' })
|
||||
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 || '' })
|
||||
} catch (caught) {
|
||||
appointmentOpen.value = false
|
||||
mutationError.value = getApiErrorDetails(caught, 'FBA 预约详情加载失败,请重试')
|
||||
@@ -157,9 +158,9 @@ async function submitAppointment() {
|
||||
if (!appointmentForm.appointmentNo.trim() || !appointmentForm.appointmentAt || !appointmentForm.expectedLoadingAt) { ElMessage.warning('请填写预约号、预约送仓时间和预计装车时间'); return }
|
||||
saving.value = true; mutationError.value = null
|
||||
try {
|
||||
const common = { appointmentAt: appointmentForm.appointmentAt, expectedLoadingAt: appointmentForm.expectedLoadingAt, appointmentStatus: appointmentForm.appointmentStatus, appointmentRemark: appointmentForm.appointmentRemark.trim() || undefined }
|
||||
if (appointmentMode.value === 'edit') await updateFbaAppointment(editingAppointmentNo.value, { customerId: editingCustomerId.value, ...common })
|
||||
else await createFbaAppointment({ customerId: selectedRows.value[0].customerId, lineIds: selectedRows.value.map((row) => row.lineId), appointmentNo: appointmentForm.appointmentNo.trim(), ...common })
|
||||
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 })
|
||||
appointmentOpen.value = false; selectedRows.value = []; ElMessage.success('FBA 预约保存成功'); await load()
|
||||
} catch (caught) { mutationError.value = getApiErrorDetails(caught, 'FBA 预约保存失败,请重试') }
|
||||
finally { saving.value = false }
|
||||
@@ -210,6 +211,7 @@ onMounted(() => { void load() })
|
||||
<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>
|
||||
|
||||
@@ -166,7 +166,7 @@ async function reviewPlan(plan: Record<string, unknown>) {
|
||||
}
|
||||
async function cancelPlan(plan: Record<string, unknown>) {
|
||||
try {
|
||||
await ElMessageBox.confirm('取消后释放本计划锁定的来源库存,是否继续?', '取消合箱计划', { type: 'warning' });
|
||||
await ElMessageBox.confirm('取消后释放本计划锁定的来源库存,是否继续?', '取消合箱计划', { type: 'warning', customClass: 'admin-density-confirm' });
|
||||
await cancelReturnPackingPlan(String(plan.planNo)); ElMessage.success('计划已取消,库存已释放'); await load();
|
||||
} catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(error instanceof Error ? error.message : '取消失败'); }
|
||||
}
|
||||
@@ -567,7 +567,7 @@ const tableColumns = computed(() =>
|
||||
>
|
||||
<template #filters
|
||||
><el-tabs
|
||||
class="density-element-tabs"
|
||||
class="density-element-tabs density-status-tabs"
|
||||
v-if="mode === 'inventory'"
|
||||
v-model="inventoryTab"
|
||||
@tab-change="changeInventoryTab"
|
||||
@@ -731,7 +731,7 @@ const tableColumns = computed(() =>
|
||||
}}</template>
|
||||
</DataTable>
|
||||
<AppDialog
|
||||
class="packing-dialog"
|
||||
class="packing-dialog admin-density-dialog"
|
||||
v-model="packingDialogOpen"
|
||||
title="创建退货合箱计划"
|
||||
width="min(1380px, calc(100vw - 32px))"
|
||||
@@ -1000,11 +1000,11 @@ const tableColumns = computed(() =>
|
||||
padding: 7px 10px;
|
||||
border-bottom: 1px solid var(--cc-border);
|
||||
}
|
||||
.packing-sku-list-head { position: sticky; top: 0; z-index: 1; background: var(--cc-surface-subtle, #f8fafc); color: var(--cc-muted); font-size: 12px; }
|
||||
.packing-sku-list-head { position: sticky; top: 0; z-index: 1; background: var(--cc-surface-subtle, #f8fafc); color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||||
.packing-sku-row:last-of-type { border-bottom: 0; }
|
||||
.packing-sku-row > span {
|
||||
color: var(--cc-muted);
|
||||
font-size: 12px;
|
||||
font-size: var(--admin-font-caption);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.packing-sku-row :deep(.el-input-number) {
|
||||
@@ -1014,7 +1014,7 @@ const tableColumns = computed(() =>
|
||||
padding: 0 7px;
|
||||
}
|
||||
.packing-sku-row :deep(.el-input__inner) {
|
||||
font-size: 12px;
|
||||
font-size: var(--admin-font-caption);
|
||||
text-align: center;
|
||||
}
|
||||
.packing-box-list {
|
||||
@@ -1041,7 +1041,7 @@ const tableColumns = computed(() =>
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
color: var(--cc-muted);
|
||||
font-size: 12px;
|
||||
font-size: var(--admin-font-caption);
|
||||
background: var(--cc-surface-subtle, #f8fafc);
|
||||
border-bottom: 1px solid var(--cc-border);
|
||||
}
|
||||
@@ -1055,20 +1055,20 @@ const tableColumns = computed(() =>
|
||||
.packing-box-list-row > span,
|
||||
.packing-box-items {
|
||||
color: var(--cc-muted);
|
||||
font-size: 12px;
|
||||
font-size: var(--admin-font-caption);
|
||||
}
|
||||
.packing-box-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.return-nos-count { color: var(--cc-muted); font-size: 12px; white-space: nowrap; }
|
||||
.return-nos-count { color: var(--cc-muted); font-size: var(--admin-font-caption); white-space: nowrap; }
|
||||
.packing-form :deep(.el-alert) {
|
||||
margin-top: 2px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.packing-form :deep(.el-alert__title) {
|
||||
font-size: 12px;
|
||||
font-size: var(--admin-font-caption);
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.packing-box-list { overflow-x: auto; }
|
||||
|
||||
@@ -26,6 +26,7 @@ describe('overseas warehouse first-batch prototype contract', () => {
|
||||
const expected = new Map([
|
||||
['LastMileOrdersView.vue', 'overseas-last-mile-orders-query'],
|
||||
['InboundOrdersView.vue', 'overseas-inbound-orders-query'],
|
||||
['CargoDiscardsView.vue', 'overseas-cargo-discards-query'],
|
||||
['DispatchesView.vue', 'overseas-dispatches-query'],
|
||||
['DeliveryOrdersView.vue', 'overseas-delivery-orders-query'],
|
||||
['CustomerVisitsView.vue', 'overseas-customer-visits-query'],
|
||||
@@ -211,7 +212,8 @@ describe('overseas warehouse first-batch prototype contract', () => {
|
||||
expect(value).toContain('createFbaAppointment')
|
||||
expect(value).toContain('预计装车时间')
|
||||
expect(value).toContain('所选入库单必须属于同一 FBA 仓库')
|
||||
expect(value).toContain('anchor.customerId === row.customerId')
|
||||
expect(value).not.toContain('anchor.customerId === row.customerId')
|
||||
expect(value).toContain('anchor.fbaWarehouse === row.fbaWarehouse')
|
||||
expect(value).toContain('<CalendarCheck2 :size="16" />预约')
|
||||
expect(value).toContain('<Truck :size="16" />智能排车')
|
||||
expect(value).toContain('type="primary" @click="openSmartDispatch"')
|
||||
|
||||
Reference in New Issue
Block a user