完善预约确认与排车表单并统一计费和筛选展示
This commit is contained in:
@@ -3,11 +3,11 @@ import { appointmentFormError, appointmentPayload, createAppointmentForm } from
|
||||
|
||||
it('requires an existing external confirmation for a new platform registration', () => {
|
||||
const form = createAppointmentForm()
|
||||
expect(appointmentFormError(form)).toContain('外部约号与确认时间')
|
||||
expect(appointmentFormError(form)).toContain('外部约号')
|
||||
expect(appointmentPayload(form)).toMatchObject({ plannedDate: null, plannedAt: null, appointmentAt: null, confirmationNo: null, appointmentStatus: 'SUCCESS' })
|
||||
form.confirmationNo = 'EXT-CONFIRMED'
|
||||
form.appointmentAt = '2026-10-25T09:30:00'
|
||||
expect(appointmentFormError(form)).toBeNull()
|
||||
expect(appointmentPayload(form)).toMatchObject({ appointmentAt: null, appointmentEndAt: null, startOffset: null, endOffset: null })
|
||||
expect(appointmentPayload(form)).not.toHaveProperty('expectedLoadingAt')
|
||||
})
|
||||
it('retains an already confirmed repeated local time using its stored UTC instant', () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ export function appointmentPayload(form: AppointmentForm): FbaAppointmentDetails
|
||||
}
|
||||
export function appointmentFormError(form: AppointmentForm): string | null {
|
||||
if (form.appointmentStatus === 'CANCELLED') return '请在预约列表使用取消预约操作,并填写实际取消原因'
|
||||
if (form.appointmentStatus === 'SUCCESS' && (!form.confirmationNo.trim() || !form.appointmentAt)) return '登记对方确认时,请填写外部约号与确认时间'
|
||||
if (form.appointmentStatus === 'SUCCESS' && !form.confirmationNo.trim()) return '登记对方确认时,请填写外部约号'
|
||||
if (form.appointmentStatus === 'FAILED' && !form.appointmentRemark.trim()) return '请填写未约成的原因'
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -38,13 +38,16 @@ describe('delivery document drafts', () => {
|
||||
it('loads once and stages replacement and deletion until a full versioned save', async () => {
|
||||
const wrapper = mountDialog(); await flushPromises()
|
||||
expect(getDeliveryDocuments).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.text()).toContain('1/3 已上传')
|
||||
expect(wrapper.find('[aria-label="派送资料摘要"]').exists()).toBe(false)
|
||||
expect(wrapper.findAll('.document-file')).toHaveLength(1)
|
||||
expect(wrapper.findAll('.document-files th').map(cell => cell.text())).toEqual(['操作', '文件名称'])
|
||||
await choose(wrapper.find('[data-category="BOL"] .document-file input[type="file"]'), new File(['next'], 'new.pdf'))
|
||||
expect(saveDeliveryDocuments).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).not.toContain('original.pdf')
|
||||
expect(wrapper.text()).toContain('new.pdf')
|
||||
expect(wrapper.get('.document-file-pending').text()).toBe('待保存')
|
||||
await wrapper.find('[data-category="BOL"] .document-file').findAll('button').find(button => button.text() === '删除')!.trigger('click')
|
||||
expect(wrapper.text()).toContain('0/3 已上传')
|
||||
expect(wrapper.findAll('.document-file')).toHaveLength(0)
|
||||
await wrapper.find('.save').trigger('click'); await flushPromises()
|
||||
expect(saveDeliveryDocuments).toHaveBeenCalledWith('DL/1', '100', { expectedVersion: 9, idempotencyKey: expect.any(String), bolNo: 'BOL-1', documents: deliveryDocumentCategories.map(item => ({ category: item.category, fileIds: [] })) })
|
||||
expect(wrapper.emitted('saved')).toHaveLength(1)
|
||||
@@ -64,7 +67,7 @@ describe('delivery document drafts', () => {
|
||||
expect(dialog.findAll('[data-category]')).toHaveLength(3)
|
||||
await choose(dialog.find('[data-category="RECEIPT"] header input[type="file"]'), new File(['pod'], 'pod.pdf'))
|
||||
await choose(dialog.find('[data-category="RECEIPT"] header input[type="file"]'), new File(['courier'], 'courier.pdf'))
|
||||
expect(dialog.text()).toContain('2/3 已上传')
|
||||
expect(dialog.findAll('[data-category="RECEIPT"] .document-file')).toHaveLength(2)
|
||||
await choose(dialog.find('[data-category="BOL"] .document-file input[type="file"]'), new File(['signed'], 'signed-bol.pdf'))
|
||||
await dialog.find('[data-category="RECEIPT"] .document-file').findAll('button').find(button => button.text() === '删除')!.trigger('click')
|
||||
expect(dialog.text()).not.toContain('pod.pdf')
|
||||
@@ -80,7 +83,8 @@ describe('delivery document drafts', () => {
|
||||
vi.mocked(getDeliveryDocuments).mockResolvedValue(persisted)
|
||||
await wrapper.setProps({ modelValue: false }); await wrapper.setProps({ modelValue: true }); await flushPromises()
|
||||
const reopened = body.find('[role="dialog"]')
|
||||
expect(reopened.text()).toContain('2/3 已上传')
|
||||
expect(reopened.findAll('.document-file')).toHaveLength(2)
|
||||
expect(reopened.find('.document-file-pending').exists()).toBe(false)
|
||||
expect(reopened.text()).toContain('signed-bol.pdf')
|
||||
expect(reopened.text()).toContain('courier.pdf')
|
||||
expect(reopened.text()).not.toContain('original.pdf')
|
||||
@@ -90,12 +94,13 @@ describe('delivery document drafts', () => {
|
||||
it('discards unsaved uploads on cancel and reloads persisted categories on reopening', async () => {
|
||||
const wrapper = mountDialog(); await flushPromises()
|
||||
await choose(wrapper.find('[data-category="RECEIPT"] input[type="file"]'), new File(['proof'], 'new.pdf'))
|
||||
expect(wrapper.text()).toContain('2/3 已上传')
|
||||
expect(wrapper.findAll('.document-file')).toHaveLength(2)
|
||||
await wrapper.find('.close').trigger('click')
|
||||
await wrapper.setProps({ modelValue: false }); await wrapper.setProps({ modelValue: true }); await flushPromises()
|
||||
expect(saveDeliveryDocuments).not.toHaveBeenCalled()
|
||||
expect(wrapper.emitted('saved')).toBeUndefined()
|
||||
expect(wrapper.text()).toContain('1/3 已上传')
|
||||
expect(wrapper.findAll('.document-file')).toHaveLength(1)
|
||||
expect(wrapper.find('.document-file-pending').exists()).toBe(false)
|
||||
expect(wrapper.text()).not.toContain('new.pdf')
|
||||
wrapper.unmount()
|
||||
})
|
||||
@@ -184,7 +189,7 @@ describe('delivery document drafts', () => {
|
||||
await wrapper.setProps({ modelValue: true, deliveryNo: 'DL/2', customerId: '200' }); await flushPromises()
|
||||
fail(new Error('old upload failed')); await flushPromises()
|
||||
expect(wrapper.text()).not.toContain('old upload failed')
|
||||
expect(wrapper.text()).toContain('1/3 已上传')
|
||||
expect(wrapper.findAll('.document-file')).toHaveLength(1)
|
||||
expect(wrapper.find('[data-category="RECEIPT"] input[type="file"]').attributes('disabled')).toBeUndefined()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
@@ -28,7 +28,6 @@ const error = ref<ApiErrorDetails | null>(null)
|
||||
const conflict = ref(false)
|
||||
const canEdit = computed(() => access.can(permission.deliveryDocuments) && Boolean(snapshot.value?.allowedActions.includes('MANAGE_DOCUMENTS')))
|
||||
const busy = computed(() => saving.value || uploading.value)
|
||||
const uploadedCount = computed(() => visibleGroups.value.filter(item => item.files.length > 0).length)
|
||||
const visibleGroups = computed(() => draft.value.filter(item => !props.categories || props.categories.includes(item.category)))
|
||||
const dirty = computed(() => Boolean(snapshot.value) && (bolNo.value !== (snapshot.value?.bolNo || '') || draft.value.some(group => JSON.stringify(group.files.map(file => file.fileId)) !== JSON.stringify(snapshot.value?.categories.find(item => item.category === group.category)?.files.map(file => file.fileId) || []))))
|
||||
defineExpose({ busy, dirty, canEdit, loading, conflict, save })
|
||||
@@ -118,16 +117,21 @@ async function save() {
|
||||
<component :is="embedded ? 'div' : AppDialog" class="admin-density-dialog admin-workbench is-dialog" v-model="open" :title="embedded ? undefined : '派送单所需资料 · ' + (deliveryNo || '—')" width="min(1040px, calc(100vw - 40px))" fullscreenable confirm-text="保存资料" cancel-text="关闭" :loading="loading" :show-confirm="canEdit" :confirm-loading="busy || loading" :confirm-disabled="conflict || loading" @confirm="save">
|
||||
<div class="density-dialog-sections">
|
||||
<LoadErrorState v-if="error" compact title="派送资料操作失败" :message="error.message" :loading="busy || loading" :show-retry="!snapshot || conflict" @retry="reload"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<section v-if="!embedded" class="density-dialog-section" data-tone="summary" aria-label="派送资料摘要"><h3 class="density-dialog-section__title">资料核对</h3><table class="density-table"><tbody><tr><th scope="row">派送单号</th><td>{{ deliveryNo || '—' }}</td><th scope="row">资料类别</th><td>{{ snapshot ? uploadedCount + '/' + visibleGroups.length + ' 已上传' : loading ? '正在读取…' : '资料未返回' }}</td></tr></tbody></table><p v-if="snapshot" class="density-dialog-note">{{ canEdit ? '上传、替换和删除将在保存资料后生效;关闭将放弃未保存修改。' : '当前账号仅可查看已保存资料。' }}</p></section>
|
||||
<el-skeleton v-if="loading && !snapshot" :rows="6" animated />
|
||||
<template v-if="snapshot">
|
||||
<section v-if="!categories || categories.includes('BOL')" class="density-dialog-section" data-tone="work" aria-label="BOL资料登记"><h3 class="density-dialog-section__title">BOL资料</h3><el-form class="density-form" label-position="left" label-width="80px" :disabled="busy || conflict || loading"><table class="density-table density-table--editable"><tbody><tr><td><el-form-item label="BOL号"><el-input v-if="canEdit" v-model="bolNo" :disabled="busy || conflict || loading" /><span v-else>{{ bolNo || '—' }}</span></el-form-item></td></tr></tbody></table></el-form></section>
|
||||
<section v-for="group in visibleGroups" :key="group.category" class="document-group density-dialog-section" data-tone="source" :data-category="group.category">
|
||||
<header><h3 class="density-dialog-section__title">{{ group.label }}</h3><el-tag :type="group.files.length ? 'success' : 'info'">{{ group.files.length ? '已上传' : '待上传' }}</el-tag><span>{{ group.files.length }}/20</span><el-upload v-if="canEdit" :key="uploadKey" :auto-upload="false" :show-file-list="false" :disabled="busy || conflict || group.files.length >= 20" :on-change="(file: UploadFile) => upload(group.category, file)"><el-button class="density-action" data-tone="primary" :disabled="busy || conflict || group.files.length >= 20">上传</el-button></el-upload></header>
|
||||
<section v-for="group in visibleGroups" :key="group.category" class="document-group density-dialog-section" :data-category="group.category" :aria-label="group.label">
|
||||
<header>
|
||||
<h3 class="density-dialog-section__title">{{ group.label }}<small>{{ group.files.length }} 个文件</small></h3>
|
||||
<el-upload v-if="canEdit" :key="uploadKey" class="document-group-upload" :auto-upload="false" :show-file-list="false" :disabled="busy || conflict || group.files.length >= 20" :on-change="(file: UploadFile) => upload(group.category, file)"><el-button class="density-action" data-tone="primary" :disabled="busy || conflict || group.files.length >= 20">上传</el-button></el-upload>
|
||||
</header>
|
||||
<el-form v-if="group.category === 'BOL'" class="density-form document-bol-form" label-position="left" label-width="80px" :disabled="busy || conflict || loading">
|
||||
<el-form-item label="BOL号"><el-input v-if="canEdit" v-model="bolNo" :disabled="busy || conflict || loading" /><span v-else>{{ bolNo || '—' }}</span></el-form-item>
|
||||
</el-form>
|
||||
<p v-if="!group.files.length" class="density-dialog-note">尚未上传文件</p>
|
||||
<table v-else class="density-table document-files"><thead><tr><th class="document-file-actions">操作</th><th>文件名称</th><th class="document-file-state">保存状态</th><th class="document-file-time">上传时间</th></tr></thead><tbody>
|
||||
<table v-else class="density-table document-files"><thead><tr><th scope="col" class="document-file-actions">操作</th><th scope="col">文件名称</th></tr></thead><tbody>
|
||||
<tr v-for="file in group.files" :key="file.fileId" class="document-file">
|
||||
<td><div class="density-actions"><el-button link :aria-label="'下载 ' + file.fileName" :disabled="downloading || !isSaved(group.category, file.fileId)" @click="download(group.category, file)">下载</el-button><template v-if="canEdit"><el-upload :key="uploadKey" :auto-upload="false" :show-file-list="false" :disabled="busy || conflict" :on-change="(uploadFile: UploadFile) => upload(group.category, uploadFile, file.fileId)"><el-button class="density-action" data-tone="primary" :disabled="busy || conflict">替换</el-button></el-upload><el-button class="density-action" data-tone="danger" :disabled="busy || conflict" @click="remove(group.category, file.fileId)">删除</el-button></template></div></td>
|
||||
<td><LineClampText :content="file.fileName" /></td><td>{{ isSaved(group.category, file.fileId) ? '已保存' : '待保存' }}</td><td>{{ file.uploadedAt?.replace('T', ' ') || '—' }}</td>
|
||||
<td><div class="density-actions row-actions admin-text-actions"><el-button class="density-action" data-tone="neutral" :aria-label="'下载 ' + file.fileName" :disabled="downloading || !isSaved(group.category, file.fileId)" @click="download(group.category, file)">下载</el-button><template v-if="canEdit"><el-upload :key="uploadKey" :auto-upload="false" :show-file-list="false" :disabled="busy || conflict" :on-change="(uploadFile: UploadFile) => upload(group.category, uploadFile, file.fileId)"><el-button class="density-action" data-tone="primary" :disabled="busy || conflict">替换</el-button></el-upload><el-button class="density-action" data-tone="danger" :disabled="busy || conflict" @click="remove(group.category, file.fileId)">删除</el-button></template></div></td>
|
||||
<td><div class="document-file-name"><LineClampText :content="file.fileName" :lines="2" class="document-file-label" /><el-tag v-if="!isSaved(group.category, file.fileId)" class="document-file-pending" type="warning" size="small">待保存</el-tag></div></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
@@ -138,10 +142,12 @@ async function save() {
|
||||
|
||||
<style scoped>
|
||||
.document-group header { display: flex; align-items: center; flex-wrap: wrap; gap: var(--admin-space-3); margin-bottom: var(--admin-space-3); }
|
||||
.document-group header h3 { margin: 0; }
|
||||
.document-group header > :last-child { margin-left: auto; }
|
||||
.document-group header h3 { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--admin-space-3); margin: 0; }
|
||||
.document-group-upload { margin-left: auto; }
|
||||
.document-bol-form { max-width: 460px; margin-bottom: var(--admin-space-4); }
|
||||
.document-files { table-layout: fixed; }
|
||||
.document-file-actions { width: 220px; }
|
||||
.document-file-time { width: 170px; }
|
||||
.document-file-state { width: 90px; }
|
||||
.document-file-actions { width: 180px; }
|
||||
.document-file-name { display: flex; align-items: center; gap: var(--admin-space-3); }
|
||||
.document-file-label { flex: 1; min-width: 0; }
|
||||
.document-file-pending { flex-shrink: 0; }
|
||||
</style>
|
||||
|
||||
+13
@@ -39,6 +39,19 @@ it('links both pallets to the confirmed appointment on the original dispatch wit
|
||||
batchAllocations: [expect.objectContaining({ planPalletId: '21', packageCount: 10, recordVersion: '0' }), expect.objectContaining({ planPalletId: '22', packageCount: 10, recordVersion: '0' })] }))
|
||||
expect(wrapper.emitted('saved')).toHaveLength(1); wrapper.unmount()
|
||||
})
|
||||
it('links a formal confirmation without inventing a missing receiving time', async () => {
|
||||
const value = facts()
|
||||
value.items.forEach(item => { item.sources[0]!.options[0]!.appointmentAt = null })
|
||||
vi.mocked(api.getDispatchAppointmentAllocations).mockResolvedValue(value)
|
||||
const wrapper = await open()
|
||||
expect(wrapper.text()).toContain('CONF-9'); expect(wrapper.text()).toContain('确认时间未填写')
|
||||
const dialog = wrapper.findComponent(AppDialog)
|
||||
expect(dialog.props('confirmDisabled')).toBe(false)
|
||||
dialog.vm.$emit('confirm'); await flushPromises()
|
||||
expect(api.saveDispatchAppointmentAllocations).toHaveBeenCalledTimes(1)
|
||||
expect(vi.mocked(api.saveDispatchAppointmentAllocations).mock.calls[0]![1].batchAllocations).toHaveLength(2)
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('counts a shared source quota once across pallets and rejects incomplete allocation', async () => {
|
||||
const value = facts(); value.items.forEach(item => { item.sources[0]!.options[0]!.availablePackages = 15 })
|
||||
vi.mocked(api.getDispatchAppointmentAllocations).mockResolvedValue(value)
|
||||
|
||||
@@ -97,8 +97,8 @@ onBeforeUnmount(() => { generation++ })
|
||||
<p class="density-dialog-note">按本车每托、每票货物核对已确认的正式预约及箱数。保存后保留原排车,重新提交两级审核。</p>
|
||||
<div class="appointment-table"><table class="density-table density-table--editable"><thead><tr><th>客户/托盘</th><th>订单号/入仓码</th><th>目的仓</th><th>本票箱数</th><th>正式预约/当地送达时间</th><th>本票可排箱数</th><th>分配箱数</th></tr></thead><tbody>
|
||||
<template v-for="row in rows" :key="row.key">
|
||||
<tr v-for="choice in row.choices" :key="choice.batchKey"><td>{{ row.item.customerName || '—' }}<br>{{ row.item.palletNo }}</td><td>{{ row.source.orderNo || '—' }}<br>{{ row.source.inboundCode || '—' }}</td><td>{{ row.source.destinationKey.slice(4) }}</td><td>{{ row.source.packageCount ?? '待核实' }}</td><td>{{ choice.batchNo || '—' }}<span v-if="choice.unavailable">(已失效)</span><br>{{ choice.confirmationNo || '—' }}<br>{{ choice.appointmentAt?.replace('T', ' ') }}<template v-if="choice.appointmentEndAt"> ~ {{ choice.appointmentEndAt.replace('T', ' ') }}</template><br>{{ choice.timezone }}</td><td>{{ choice.availablePackages }}<span v-if="!choice.unavailable">(同票共用)</span></td><td><el-input-number v-model="choice.packages" :min="0" :precision="0" :controls="false" :disabled="loading || saving || !permitted" :aria-label="`${row.item.palletNo} ${choice.batchNo} 分配箱数`" /></td></tr>
|
||||
<tr v-if="!row.choices.length"><td>{{ row.item.customerName || '—' }}<br>{{ row.item.palletNo }}</td><td>{{ row.source.orderNo || '—' }}<br>{{ row.source.inboundCode || '—' }}</td><td>{{ row.source.destinationKey.slice(4) }}</td><td>{{ row.source.packageCount ?? '待核实' }}</td><td colspan="3">{{ row.source.blockReasons.join(';') || '暂无已确认的正式预约,请先在预约列表确认送仓时间' }}</td></tr>
|
||||
<tr v-for="choice in row.choices" :key="choice.batchKey"><td>{{ row.item.customerName || '—' }}<br>{{ row.item.palletNo }}</td><td>{{ row.source.orderNo || '—' }}<br>{{ row.source.inboundCode || '—' }}</td><td>{{ row.source.destinationKey.slice(4) }}</td><td>{{ row.source.packageCount ?? '待核实' }}</td><td>{{ choice.batchNo || '—' }}<span v-if="choice.unavailable">(已失效)</span><br>{{ choice.confirmationNo || '—' }}<br>{{ choice.appointmentAt?.replace('T', ' ') || '确认时间未填写' }}<template v-if="choice.appointmentEndAt"> ~ {{ choice.appointmentEndAt.replace('T', ' ') }}</template><br>{{ choice.timezone }}</td><td>{{ choice.availablePackages }}<span v-if="!choice.unavailable">(同票共用)</span></td><td><el-input-number v-model="choice.packages" :min="0" :precision="0" :controls="false" :disabled="loading || saving || !permitted" :aria-label="`${row.item.palletNo} ${choice.batchNo} 分配箱数`" /></td></tr>
|
||||
<tr v-if="!row.choices.length"><td>{{ row.item.customerName || '—' }}<br>{{ row.item.palletNo }}</td><td>{{ row.source.orderNo || '—' }}<br>{{ row.source.inboundCode || '—' }}</td><td>{{ row.source.destinationKey.slice(4) }}</td><td>{{ row.source.packageCount ?? '待核实' }}</td><td colspan="3">{{ row.source.blockReasons.join(';') || '暂无已确认的正式预约,请先在预约列表登记外部约号及覆盖货物' }}</td></tr>
|
||||
</template><tr v-if="!loading && !rows.length"><td colspan="7">本车暂无可关联正式预约的FBA货物</td></tr>
|
||||
</tbody></table></div>
|
||||
<el-alert v-if="factsReady && permitted && validation" :title="validation" type="warning" :closable="false" show-icon />
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { expect, it } from 'vitest'
|
||||
import { createAppointmentForm } from '../appointmentForm'
|
||||
import FbaAppointmentFields from './FbaAppointmentFields.vue'
|
||||
|
||||
it('removes confirmation time and offset inputs only for the new appointment flow while retaining planned dates', async () => {
|
||||
const wrapper = mount(FbaAppointmentFields, {
|
||||
props: { model: createAppointmentForm(), continuous: true, hideConfirmationTimes: true, timezone: 'America/New_York' },
|
||||
global: { stubs: {
|
||||
ElForm: { template: '<form><slot /></form>' },
|
||||
ElFormItem: { props: ['label'], template: '<label>{{ label }}<slot /></label>' },
|
||||
ElInput: { template: '<input />' },
|
||||
ElDatePicker: { template: '<input data-picker />' },
|
||||
InlineHelp: true,
|
||||
} },
|
||||
})
|
||||
expect(wrapper.text()).toContain('外部约号')
|
||||
expect(wrapper.text()).toContain('计划送仓日期')
|
||||
expect(wrapper.text()).toContain('补充计划时段')
|
||||
expect(wrapper.findAll('[data-picker]')).toHaveLength(3)
|
||||
expect(wrapper.text()).not.toMatch(/对方确认开始时间|对方确认结束时间|确认开始时差|确认结束时差|夏令时/)
|
||||
await wrapper.setProps({ hideConfirmationTimes: false })
|
||||
expect(wrapper.text()).toContain('对方确认开始时间')
|
||||
expect(wrapper.findAll('[data-picker]')).toHaveLength(5)
|
||||
wrapper.unmount()
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import InlineHelp from '@/shared/components/InlineHelp.vue'
|
||||
import { AppointmentConfirmationCargo, type PlanningSourceLookup } from '@/modules/appointment-calendar'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
import type { AppointmentForm } from '../appointmentForm'
|
||||
const props = defineProps<{ model: AppointmentForm; appointmentNo?: string; timezone?: string | null; disabled?: boolean; continuous?: boolean; sources?: PlanningSourceLookup[] }>()
|
||||
const props = defineProps<{ model: AppointmentForm; appointmentNo?: string; timezone?: string | null; disabled?: boolean; continuous?: boolean; hideConfirmationTimes?: boolean; sources?: PlanningSourceLookup[] }>()
|
||||
const emit = defineEmits<{ confirmationReady: [ready: boolean] }>()
|
||||
const cargo = ref<InstanceType<typeof AppointmentConfirmationCargo> | null>(null)
|
||||
watchEffect(() => {
|
||||
@@ -13,7 +13,7 @@ watchEffect(() => {
|
||||
defineExpose({ canLeave: () => cargo.value?.canLeave() ?? Promise.resolve(true) })
|
||||
</script>
|
||||
<template>
|
||||
<p v-if="model.appointmentStatus === 'SUCCESS' && timezone && /^(UTC|GMT|Etc\/(GMT|UTC)|[+-])/.test(timezone)" role="alert">目的仓当前只有固定时差,尚未设置地区时区。请到“仓库管理”编辑本次目的 FBA 仓,按实际地址设置地区时区(如 America/New_York),再重新打开预约;固定时差无法判断夏令时。</p>
|
||||
<p v-if="!hideConfirmationTimes && model.appointmentStatus === 'SUCCESS' && timezone && /^(UTC|GMT|Etc\/(GMT|UTC)|[+-])/.test(timezone)" role="alert">目的仓当前只有固定时差,尚未设置地区时区。请到“仓库管理”编辑本次目的 FBA 仓,按实际地址设置地区时区(如 America/New_York),再重新打开预约;固定时差无法判断夏令时。</p>
|
||||
<AppointmentConfirmationCargo v-if="sources?.length" ref="cargo" :sources="sources" :active="model.appointmentStatus === 'SUCCESS'" :disabled="disabled" @ready="emit('confirmationReady', $event)" />
|
||||
<div v-if="continuous" class="density-dialog-sections">
|
||||
<section class="density-dialog-section" aria-label="预约来源信息"><h3 class="density-dialog-section__title">预约信息</h3><table class="density-table"><tbody><tr><th scope="row">系统单号</th><td>{{ appointmentNo || '保存后生成' }}</td><th scope="row">目的仓时区</th><td>{{ timezone || '待核实' }}</td></tr></tbody></table></section>
|
||||
@@ -22,13 +22,13 @@ defineExpose({ canLeave: () => cargo.value?.canLeave() ?? Promise.resolve(true)
|
||||
<tr><td><el-form-item label="计划送仓日期"><el-date-picker v-model="model.plannedDate" type="date" value-format="YYYY-MM-DD" placeholder="日期未定可留空" /></el-form-item></td><td><el-form-item label="预约状态"><el-input :model-value="({ SUCCESS: '已确认', CANCELLED: '已取消', FAILED: '未约成', PENDING: '待确认' })[model.appointmentStatus]" readonly /></el-form-item></td></tr>
|
||||
<template v-if="model.appointmentStatus === 'SUCCESS'">
|
||||
<tr><td colspan="2"><el-form-item label="外部约号" required><el-input v-model="model.confirmationNo" maxlength="128" placeholder="填写收货方提供的确认号" /></el-form-item></td></tr>
|
||||
<tr><td><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></td><td><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></td></tr>
|
||||
<tr v-if="!hideConfirmationTimes"><td><el-form-item label="对方确认开始时间"><el-date-picker v-model="model.appointmentAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="目的仓当地时间" @change="model.startOffset = ''" /></el-form-item></td><td><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></td></tr>
|
||||
</template>
|
||||
<tr><td colspan="2"><el-form-item :label="model.appointmentStatus === 'FAILED' ? '原因' : '备注'" :required="model.appointmentStatus === 'FAILED'"><el-input v-model="model.appointmentRemark" aria-label="预约备注" maxlength="1000" /></el-form-item></td></tr>
|
||||
</tbody></table>
|
||||
<details><summary>补充计划时段与夏令时信息</summary><table class="density-table density-table--editable appointment-editor-table"><tbody>
|
||||
<details><summary>{{ hideConfirmationTimes ? '补充计划时段' : '补充计划时段与夏令时信息' }}</summary><table class="density-table density-table--editable appointment-editor-table"><tbody>
|
||||
<tr><td><el-form-item label="计划开始时段"><el-date-picker v-model="model.plannedAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="只知道日期时不必填写" /></el-form-item></td><td><el-form-item label="计划结束时段"><el-date-picker v-model="model.plannedEndAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item></td></tr>
|
||||
<template v-if="model.appointmentStatus === 'SUCCESS'"><tr><td colspan="2"><span class="density-dialog-note">当地时钟回拨、同一时间出现两次时,需按对方确认明确时差。</span><InlineHelp title="夏令时填写说明">仅重叠时刻需填写对方确认的时差;修改确认时间后须重新核对时差。</InlineHelp></td></tr><tr><td><el-form-item label="确认开始时差"><el-input v-model="model.startOffset" placeholder="仅重叠时刻需填,如 -07:00" /></el-form-item></td><td><el-form-item label="确认结束时差"><el-input v-model="model.endOffset" placeholder="仅重叠时刻需填,如 -08:00" /></el-form-item></td></tr></template>
|
||||
<template v-if="!hideConfirmationTimes && model.appointmentStatus === 'SUCCESS'"><tr><td colspan="2"><span class="density-dialog-note">当地时钟回拨、同一时间出现两次时,需按对方确认明确时差。</span><InlineHelp title="夏令时填写说明">仅重叠时刻需填写对方确认的时差;修改确认时间后须重新核对时差。</InlineHelp></td></tr><tr><td><el-form-item label="确认开始时差"><el-input v-model="model.startOffset" placeholder="仅重叠时刻需填,如 -07:00" /></el-form-item></td><td><el-form-item label="确认结束时差"><el-input v-model="model.endOffset" placeholder="仅重叠时刻需填,如 -08:00" /></el-form-item></td></tr></template>
|
||||
</tbody></table></details></el-form>
|
||||
</section>
|
||||
</div>
|
||||
@@ -39,15 +39,15 @@ defineExpose({ canLeave: () => cargo.value?.canLeave() ?? Promise.resolve(true)
|
||||
<el-form-item label="预约状态"><el-input :model-value="({ SUCCESS: '已确认', CANCELLED: '已取消', FAILED: '未约成', PENDING: '待确认' })[model.appointmentStatus]" readonly /></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>
|
||||
<el-form-item v-if="!hideConfirmationTimes" label="对方确认开始时间"><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 v-if="!hideConfirmationTimes" 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="model.appointmentStatus === 'FAILED' ? '原因' : '备注'" :required="model.appointmentStatus === 'FAILED'"><el-input v-model="model.appointmentRemark" aria-label="预约备注" maxlength="1000" /></el-form-item>
|
||||
<details class="appointment-fields__extra"><summary>补充计划时段与夏令时信息</summary>
|
||||
<details class="appointment-fields__extra"><summary>{{ hideConfirmationTimes ? '补充计划时段' : '补充计划时段与夏令时信息' }}</summary>
|
||||
<div class="appointment-fields__extra-grid">
|
||||
<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>
|
||||
<template v-if="!hideConfirmationTimes && 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>
|
||||
|
||||
@@ -58,7 +58,7 @@ export interface DispatchItems { dispatchNo: string; recordVersion: Version; sta
|
||||
export interface DispatchRouteStop { index: number; name: string; address: string | null; destinationKey: string | null }
|
||||
export interface DispatchRoute { dispatchNo: string; recordVersion: Version; stops: DispatchRouteStop[]; edit: Gate }
|
||||
export interface DispatchRouteCommand extends VersionCommand { destinationKeys: string[]; reason: string }
|
||||
export interface AppointmentAllocationOption { batchKey: string; batchNo: string; recordVersion: Version; availablePackages: number; confirmationNo: string | null; appointmentAt: string; appointmentEndAt: string | null; timezone: string }
|
||||
export interface AppointmentAllocationOption { batchKey: string; batchNo: string; recordVersion: Version; availablePackages: number; confirmationNo: string | null; appointmentAt: string | null; appointmentEndAt: string | null; timezone: string }
|
||||
export interface AppointmentAllocationSource { sourceKey: string; orderNo: string | null; inboundCode: string | null; destinationKey: string; packageCount: number | null; options: AppointmentAllocationOption[]; blockReasons: string[] }
|
||||
export interface AppointmentAllocationItem { dispatchItemId: string; planPalletId: string; customerId: string; customerName: string | null; palletNo: string; sources: AppointmentAllocationSource[]; allocations: DispatchBatchAllocation[] }
|
||||
export interface AppointmentAllocations { dispatchNo: string; recordVersion: Version; items: AppointmentAllocationItem[]; edit: Gate }
|
||||
|
||||
@@ -139,19 +139,19 @@ describe('native delivery operation dialogs', () => {
|
||||
it('keeps readonly document facts plain and only exposes saved downloads', async () => {
|
||||
api.documents.mockResolvedValueOnce({ ...documents(), allowedActions: [] })
|
||||
const { dialog } = await openDocuments()
|
||||
expect(dialog.props('showConfirm')).toBe(false); expect(dialog.text()).toContain('当前账号仅可查看已保存资料')
|
||||
expect(dialog.props('showConfirm')).toBe(false)
|
||||
expect(dialog.findAll('input')).toHaveLength(0); expect(dialog.text()).toContain('BOL-1')
|
||||
expect(dialog.findAll('[data-category]')).toHaveLength(3)
|
||||
expect(dialog.findAll('button').some(button => button.text() === '上传')).toBe(false)
|
||||
await dialog.get('[aria-label="下载 完整文件名称.pdf"]').trigger('click'); await flushPromises(); expect(api.download).toHaveBeenCalledOnce()
|
||||
})
|
||||
it('shows an unavailable summary after a document read failure and retries within scope', async () => {
|
||||
it('shows the document read failure and retries within scope', async () => {
|
||||
api.documents.mockRejectedValueOnce(new ApiResponseError('读取失败', 'A0503', 503, null, 'document-read-request'))
|
||||
const { dialog } = await openDocuments()
|
||||
expect(dialog.text()).toContain('document-read-request'); expect(dialog.text()).toContain('资料未返回')
|
||||
expect(dialog.text()).not.toContain('正在读取…'); expect(dialog.props('showConfirm')).toBe(false)
|
||||
expect(dialog.text()).toContain('document-read-request'); expect(dialog.findComponent(LoadErrorState).props('message')).toBe('读取失败')
|
||||
expect(dialog.findAll('[data-category]')).toHaveLength(0); expect(dialog.props('showConfirm')).toBe(false)
|
||||
dialog.findComponent(LoadErrorState).vm.$emit('retry'); await flushPromises()
|
||||
expect(api.documents).toHaveBeenLastCalledWith('DEL/1', 'C1'); expect(dialog.text()).toContain('1/3 已上传')
|
||||
expect(api.documents).toHaveBeenLastCalledWith('DEL/1', 'C1'); expect(dialog.findAll('.document-file')).toHaveLength(1)
|
||||
})
|
||||
it('retains conflicting document drafts until confirmed reload', async () => {
|
||||
const confirmation = vi.spyOn(ElMessageBox, 'confirm').mockRejectedValueOnce('cancel')
|
||||
|
||||
Reference in New Issue
Block a user