Files
admin-web/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue
T

72 lines
6.0 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { FbaAppointmentFields, createAppointmentForm, appointmentFormError, appointmentPayload, overseasWarehousePermissions, type FbaAppointmentDto } from '@/modules/overseas-warehouse'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { previewPlanningBatch, createSourceAppointment, type PlanningBatchPreview, type SourceAppointmentPayload } from './planningApi'
import type { SourceReference } from './workApi'
import { rejectedCommand, useWorkDraft } from './useWorkDraft'
import { workReason } from './workPresentation'
const props = defineProps<{ sources: SourceReference[]; canWrite: boolean }>()
const emit = defineEmits<{ saved: [appointment: FbaAppointmentDto] }>()
const access = useAccessPreviewStore(), preview = ref<PlanningBatchPreview | null>(null)
const confirmationReady = ref(false)
const appointmentFields = ref<InstanceType<typeof FbaAppointmentFields> | null>(null)
const form = reactive(createAppointmentForm()), baseline = ref(JSON.stringify(form))
const loading = ref(false), saving = ref(false), error = ref<ApiErrorDetails | null>(null), saveError = ref<ApiErrorDetails | null>(null)
const pending = ref<SourceAppointmentPayload | null>(null)
const writable = computed(() => props.canWrite && access.can(overseasWarehousePermissions.inboundOrderAppointment))
const visibleMissingFields = computed(() => preview.value?.missingFields.filter(field => field !== 'READY_WINDOW_UNVERIFIED') || [])
useWorkDraft(computed(() => baseline.value !== JSON.stringify(form)), computed(() => saving.value || !!pending.value))
async function load() {
if (!props.sources.length || props.sources.some(source => !source.customerId || !source.sourceKey || !source.destinationKey)) { error.value = { message: '缺少完整货源、客户或目的仓范围,无法新建预约。' }; return }
loading.value = true; error.value = null
try { preview.value = await previewPlanningBatch(props.sources.map(source => ({ ...source, destinationKey: source.destinationKey! }))) }
catch (cause) { error.value = getApiErrorDetails(cause, '预约货物预览失败') }
finally { loading.value = false }
}
async function save() {
if (!writable.value || loading.value || saving.value || error.value || !preview.value) return
if (!pending.value) {
const invalid = appointmentFormError(form); if (invalid) { ElMessage.warning(invalid); return }
if (form.appointmentStatus === 'SUCCESS' && !confirmationReady.value) { ElMessage.warning('请先在“正式确认前”区域补齐并保存每票货物资料'); return }
if (appointmentFields.value?.canLeave && !await appointmentFields.value.canLeave()) return
if (saving.value || !writable.value) return
pending.value = { originWarehouseId: preview.value.group.originWarehouseId, fbaWarehouseCode: preview.value.destinationWarehouseCode, sources: preview.value.sources.map(source => ({ sourceKey: source.sourceKey, customerId: source.customerId, destinationKey: source.destinationKey })), details: appointmentPayload(form), idempotencyKey: crypto.randomUUID() }
}
saving.value = true; saveError.value = null
try {
const result = await createSourceAppointment(pending.value)
pending.value = null; baseline.value = JSON.stringify(form)
ElMessage.success(`预约安排 ${result.appointmentNo} 已保存`); emit('saved', result)
} catch (cause) {
saveError.value = getApiErrorDetails(cause, '创建结果待确认,请重试本次请求')
if (rejectedCommand(cause)) { pending.value = null; await load() }
} finally { saving.value = false }
}
onMounted(load)
</script>
<template>
<section class="work-form density-dialog-sections" aria-label="新建预约安排">
<LoadErrorState v-if="error" title="预约货物预览失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<p v-if="loading">正在核对本次货物范围…</p>
<template v-if="preview && !error">
<section 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>{{ preview.originWarehouseName || '—' }}</td><th scope="row">目的FBA仓</th><td>{{ preview.destinationWarehouseCode || '—' }}</td></tr>
<tr><th scope="row">票数</th><td>{{ preview.group.sourceCount }}</td><th scope="row">箱数</th><td>{{ preview.group.packages ?? '箱数待核实' }}</td></tr>
<tr><th scope="row">货物业务号</th><td colspan="3"><template v-for="(source, index) in preview.sources" :key="`${source.customerId}:${source.sourceKey}`">{{ index ? '、' : '' }}{{ source.inboundCode || source.bookingNo || '原票业务号未提供' }}</template></td></tr>
</tbody></table>
<p v-for="field in visibleMissingFields" :key="field" role="status" class="density-dialog-note">{{ workReason(field) }}</p>
</section>
<FbaAppointmentFields continuous hide-confirmation-times ref="appointmentFields" :sources="sources.map(source => ({ ...source, destinationKey: source.destinationKey || undefined }))" @confirmation-ready="confirmationReady = $event" :model="form" :timezone="preview.destinationTimezone" :disabled="!writable || loading || saving || !!pending" />
<LoadErrorState v-if="saveError" title="预约尚未创建" :message="saveError.message" :show-retry="false"><template #details><ApiErrorMeta :details="saveError" /></template></LoadErrorState>
<div class="density-actions density-actions--footer"><el-button type="primary" :disabled="!writable || loading" :loading="saving" @click="save">{{ pending ? '重试本次创建' : '保存预约安排' }}</el-button></div>
</template>
</section>
</template>