105 lines
11 KiB
Vue
105 lines
11 KiB
Vue
<script setup lang="ts">
|
||
import '@/styles/adminWorkbench.css'
|
||
import { computed, onMounted, onBeforeUnmount, provide, ref, watch } from 'vue'
|
||
import { onBeforeRouteLeave } from 'vue-router'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import { type DispatchBatchReference, type FbaAppointmentDto } from '@/modules/overseas-warehouse'
|
||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||
import { getAppointmentBatch, type FbaAppointmentBatchContext } from './batchApi'
|
||
import { getReceivingContext, type ReceivingContext } from './receivingApi'
|
||
import WorkReceivingPanel from './WorkReceivingPanel.vue'
|
||
import AppointmentBatchForm from './AppointmentBatchForm.vue'
|
||
import WorkAppointmentForm from './WorkAppointmentForm.vue'
|
||
import WorkBatchContinuation from './WorkBatchContinuation.vue'
|
||
import type { TripPreviousDispatch } from '@/modules/overseas-master-data'
|
||
import type { LeaveGuard } from './useWorkDraft'
|
||
const props = defineProps<{ dispatches: Array<{ dispatchNo: string; customerId: string; packageCount: number; appointmentBatches: DispatchBatchReference[] }>; previousDispatches: TripPreviousDispatch[]; historyUnavailableReason: string | null; canWrite: boolean }>()
|
||
const emit = defineEmits<{ updated: []; work: [key: string]; external: [path: string] }>()
|
||
type Card = { key: string; batchKey: string; batchNo: string | null; customerId: string; current: boolean; dispatchNos: string[]; historyNos: string[]; sources: DispatchBatchReference['sources']; batch: FbaAppointmentBatchContext | null; receiving: ReceivingContext | null; error: ApiErrorDetails | null }
|
||
const access = useAccessPreviewStore(), cards = ref<Card[]>([]), loading = ref(false)
|
||
const readable = computed(() => ['platform:appointment:list', 'platform:overseas-warehouse:dispatch:list'].every(permission => access.can(permission)))
|
||
const selected = ref<{ batchKey: string; customerId: string } | null>(null), mode = ref<'receiving' | 'batch' | 'appointment' | 'continue'>('receiving'), appointmentNo = ref('')
|
||
const guards = new Set<LeaveGuard>()
|
||
provide('appointment-work-leave-guard', (guard: LeaveGuard) => { guards.add(guard); return () => guards.delete(guard) })
|
||
async function canLeave() { for (const guard of guards) if (!await guard()) return false; return true }
|
||
defineExpose({ canLeave })
|
||
onBeforeRouteLeave(canLeave)
|
||
let generation = 0
|
||
async function load() {
|
||
if (!readable.value) return
|
||
const current = ++generation; loading.value = true
|
||
const batches = new Map<string, Promise<FbaAppointmentBatchContext>>(), receiving = new Map<string, Promise<ReceivingContext>>()
|
||
const references = new Map<string, Card>()
|
||
const add = (binding: { dispatchNo: string; customerId: string; appointmentBatches: DispatchBatchReference[] }, historical: boolean) => {
|
||
for (const reference of binding.appointmentBatches) {
|
||
const key = JSON.stringify([reference.batchKey, reference.customerId])
|
||
let card = references.get(key)
|
||
if (!card) { card = { key, batchKey: reference.batchKey, batchNo: reference.batchNo, customerId: reference.customerId, current: false, dispatchNos: [], historyNos: [], sources: [], batch: null, receiving: null, error: null }; references.set(key, card) }
|
||
if (reference.customerId !== binding.customerId || !reference.batchKey || !reference.sources.length) card.error = { message: '排车引用的客户或来源范围不完整,请核对原排车单。' }
|
||
card.current ||= !historical
|
||
const numbers = historical ? card.historyNos : card.dispatchNos
|
||
if (!numbers.includes(binding.dispatchNo)) numbers.push(binding.dispatchNo)
|
||
for (const source of reference.sources) if (!card.sources.some(existing => existing.sourceKey === source.sourceKey && existing.destinationKey === source.destinationKey)) card.sources.push(source)
|
||
}
|
||
}
|
||
props.dispatches.forEach(binding => add(binding, false))
|
||
props.previousDispatches.forEach(binding => add(binding, true))
|
||
const loaded = await Promise.all([...references.values()].map(async card => {
|
||
if (card.error) return card
|
||
try {
|
||
if (!batches.has(card.batchKey)) batches.set(card.batchKey, getAppointmentBatch(card.batchKey))
|
||
if (!receiving.has(card.batchKey)) receiving.set(card.batchKey, getReceivingContext(card.batchKey))
|
||
const [batch, scope] = await Promise.all([batches.get(card.batchKey)!, receiving.get(card.batchKey)!])
|
||
if (batch.appointment.batchKey !== card.batchKey || scope.batch.batchKey !== card.batchKey) throw new Error('批次范围不匹配')
|
||
card.batch = batch; card.receiving = scope
|
||
} catch (cause) { card.error = getApiErrorDetails(cause, '本客户预约与去向读取失败') }
|
||
return card
|
||
}))
|
||
if (current === generation) { cards.value = loaded; loading.value = false }
|
||
}
|
||
function customerName(card: Card) { return card.receiving?.agreements.flatMap(row => row.members).find(member => member.customerId === card.customerId)?.customerName || '—' }
|
||
function destinations(card: Card) {
|
||
if (!card.batch) return []
|
||
const original = new Set(card.batch.originalSourceRefs.filter(item => item.customerId === card.customerId && card.sources.some(source => source.sourceKey === item.sourceKey && source.destinationKey === item.destinationKey)).map(item => JSON.stringify([item.sourceKey, item.destinationKey])))
|
||
return [...new Map(card.batch.currentBatches.map(batch => [batch.batchKey, batch])).values()].map(batch => ({ batch, items: batch.items.filter(item => item.customerId === card.customerId && original.has(JSON.stringify([item.sourceKey, item.destinationKey])) && (item.packageCount == null || item.packageCount > 0)) })).filter(row => row.items.length)
|
||
}
|
||
async function open(batchKey: string, customerId: string, next: 'receiving' | 'batch' | 'continue') { if (await canLeave()) { selected.value = { batchKey, customerId }; mode.value = next } }
|
||
async function openAppointment(number: string) { if (await canLeave()) { appointmentNo.value = number; mode.value = 'appointment' } }
|
||
async function close(done: () => void) { if (await canLeave()) { done(); selected.value = null } }
|
||
function saved(appointment: FbaAppointmentDto) { if (appointment.batchKey && selected.value) { selected.value = { ...selected.value, batchKey: appointment.batchKey }; mode.value = 'continue' }; updated() }
|
||
function updated() { emit('updated'); void load() }
|
||
watch(() => JSON.stringify([props.dispatches, props.previousDispatches]), () => { void load() })
|
||
onMounted(load)
|
||
onBeforeUnmount(() => { ++generation })
|
||
</script>
|
||
<template>
|
||
<section aria-label="本车客户预约与后送去向">
|
||
<p v-if="!readable">需要预约及排车列表权限才能读取客户预约范围。</p>
|
||
<p v-if="loading">正在核对本车客户的预约与当前去向…</p>
|
||
<el-button :loading="loading" @click="load">刷新客户预约与去向</el-button>
|
||
<p v-if="historyUnavailableReason" role="alert">历史预约引用读取不完整:{{ historyUnavailableReason }}。当前车次仍可查看,请重试读取后再核对历史去向。</p><p v-for="binding in dispatches.filter(row => !row.appointmentBatches.length)" :key="binding.dispatchNo" role="status">{{ binding.dispatchNo }}:本排车单没有预约批次引用,请从原排车单核对业务来源。</p><article v-for="card in cards" :key="card.key" class="trip-customer-card">
|
||
<h4>{{ customerName(card) }} · {{ card.batchNo || '批次业务号未提供' }}</h4><p v-if="card.current">当前关联排车单:{{ card.dispatchNos.join('、') }}</p><p v-if="card.historyNos.length">历史关联排车单:{{ card.historyNos.join('、') }} · 仅供追溯,不代表当前装载或可排数量。</p>
|
||
<LoadErrorState v-if="card.error" title="本客户范围不可用" :message="card.error.message" @retry="load" />
|
||
<template v-if="card.batch && card.receiving">
|
||
<p v-for="pending in card.receiving.pendingChanges.filter(row => row.intent.customerId === card.customerId)" :key="pending.changeKey" role="status">待仓库回复:{{ pending.intent.reason }}。原安排保留,请继续处理。</p>
|
||
<el-button v-if="card.current" :disabled="loading || !canWrite" @click="open(card.batch.appointment.batchKey!, card.customerId, 'receiving')">调整本客户先送 / 后送</el-button>
|
||
<el-button v-if="card.current" :disabled="loading" @click="open(card.batch.appointment.batchKey!, card.customerId, 'batch')">查看批次与完整去向</el-button>
|
||
<div v-for="destination in destinations(card)" :key="destination.batch.batchKey"><b>{{ destination.batch.appointmentNo }}</b><p>{{ destination.batch.plannedDate || '日期未定' }}</p><p v-for="item in destination.items" :key="item.id">{{ item.inboundCode || '—' }} · {{ item.packageCount == null ? '箱数待核实' : `${item.packageCount} 箱` }}</p><el-button :disabled="loading" @click="open(destination.batch.batchKey!, card.customerId, 'continue')">继续本批当前工作</el-button></div>
|
||
<p v-if="!destinations(card).length">原客户来源范围当前不可匹配,请核对完整批次去向;此处不代表已完成或没有后送需求。</p>
|
||
</template>
|
||
</article>
|
||
<p v-if="readable && !loading && !cards.length">当前没有可读取的预约批次引用。</p>
|
||
<AppDialog :model-value="!!selected" title="客户预约与后送安排" width="min(1120px, calc(100vw - 48px))" class="admin-density-dialog admin-density admin-workbench is-dialog" fullscreenable append-to-body :show-footer="false" :before-close="close" :close-on-click-modal="true" :close-on-press-escape="true" destroy-on-close>
|
||
<template v-if="selected">
|
||
<WorkReceivingPanel v-if="mode === 'receiving'" :key="`${selected.batchKey}:${selected.customerId}`" :batch-key="selected.batchKey" :initial-customer-id="selected.customerId" :can-write="canWrite" @updated="updated" @appointment="openAppointment" @batch="key => open(key, selected!.customerId, 'batch')" @external="path => emit('external', path)" />
|
||
<AppointmentBatchForm v-else-if="mode === 'batch'" :key="selected.batchKey" :batch-key="selected.batchKey" @updated="updated" @appointment="openAppointment" />
|
||
<WorkAppointmentForm v-else-if="mode === 'appointment'" :key="appointmentNo" :appointment-no="appointmentNo" :cancel="false" :can-write="canWrite" @saved="saved" @batch="key => open(key, selected!.customerId, 'batch')" @receiving="key => open(key, selected!.customerId, 'receiving')" />
|
||
<WorkBatchContinuation v-else :key="`${selected.batchKey}:${selected.customerId}`" :batch-key="selected.batchKey" :customer-id="selected.customerId" @open="key => emit('work', key)" />
|
||
</template>
|
||
</AppDialog>
|
||
</section>
|
||
</template>
|
||
<style scoped>.trip-customer-card { padding-block: var(--admin-space-4); border-bottom: 1px solid var(--workbench-border); }</style>
|