feat(appointments): project trip plans by origin time zone
This commit is contained in:
@@ -25,6 +25,7 @@ import AppointmentCargoPanel from './AppointmentCargoPanel.vue'
|
||||
import AppointmentContainerPanel from './AppointmentContainerPanel.vue'
|
||||
import { calendarRange, dateKey } from './calendar'
|
||||
import { deleteAppointment, listArrivalCalendar, listPickupCalendar, type ArrivalCalendarEntry, type PickupCalendarEntry } from './api'
|
||||
import { listTripCalendar, projectTripCalendar, tripIssues, type TripCalendarEntry } from './tripCalendar'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const route = useRoute()
|
||||
@@ -38,6 +39,18 @@ const appointments = ref<FbaAppointmentDto[]>([])
|
||||
const arrivals = ref<ArrivalCalendarEntry[]>([])
|
||||
const pickups = ref<PickupCalendarEntry[]>([])
|
||||
const pickupNotice = ref('')
|
||||
const tripEvents = ref<CalendarEvent[]>([])
|
||||
const unresolvedTrips = ref<TripCalendarEntry[]>([])
|
||||
const tripNotice = ref('')
|
||||
const unresolvedPage = ref(1), unresolvedSize = ref(10)
|
||||
const unresolvedRows = computed(() => unresolvedTrips.value.slice((unresolvedPage.value - 1) * unresolvedSize.value, unresolvedPage.value * unresolvedSize.value))
|
||||
const unresolvedColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', slotName: 'actions', width: 110, fixed: 'left' },
|
||||
{ prop: 'tripNo', label: '车次号', minWidth: 210 },
|
||||
{ prop: 'originWarehouseName', label: '起运仓', minWidth: 170 },
|
||||
{ prop: 'plannedDepartureAt', label: '原计划出发(UTC)', minWidth: 225 },
|
||||
{ key: 'issues', label: '待核实资料', slotName: 'issues', minWidth: 230 },
|
||||
]
|
||||
const range = ref(calendarRange(dateKey(new Date()), 'month'))
|
||||
const loading = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
@@ -129,7 +142,7 @@ const events = computed<CalendarEvent[]>(() => [...arrivals.value.map(arrival =>
|
||||
inboundCodes: appointment.items.map(item => item.inboundCode),
|
||||
canEdit: access.can(permission.inboundOrderAppointment),
|
||||
canDelete: access.can('platform:appointment:delete'),
|
||||
}))])
|
||||
})), ...tripEvents.value])
|
||||
|
||||
async function load(nextRange = range.value) {
|
||||
range.value = nextRange
|
||||
@@ -143,13 +156,17 @@ async function load(nextRange = range.value) {
|
||||
appointments.value = []
|
||||
arrivals.value = []
|
||||
pickups.value = []
|
||||
tripEvents.value = []; unresolvedTrips.value = []; unresolvedPage.value = 1
|
||||
const canReadPickup = access.can('platform:firstleg:terminal-drayage:list')
|
||||
const canReadTrips = access.can('platform:fleet:trip:list')
|
||||
pickupNotice.value = canReadPickup ? '' : '无提柜查看权限,当前日历不包含提柜预约。'
|
||||
tripNotice.value = canReadTrips ? '' : '无车次查看权限,当前日历不包含用车计划。'
|
||||
try {
|
||||
const [result, arrivalResult, pickupResult] = await Promise.allSettled([
|
||||
const [result, arrivalResult, pickupResult, tripResult] = await Promise.allSettled([
|
||||
listFbaAppointmentCalendar(nextRange, calendarController.signal),
|
||||
listArrivalCalendar(nextRange, calendarController.signal),
|
||||
canReadPickup ? listPickupCalendar(nextRange, calendarController.signal) : Promise.resolve(null),
|
||||
canReadTrips ? listTripCalendar(nextRange, calendarController.signal) : Promise.resolve(null),
|
||||
])
|
||||
if (request !== calendarRequest) return
|
||||
if (result.status === 'fulfilled') appointments.value = result.value
|
||||
@@ -160,6 +177,11 @@ async function load(nextRange = range.value) {
|
||||
pickups.value = pickupResult.value.records
|
||||
if (pickupResult.value.truncated || Number(pickupResult.value.unclassifiedCount) > 0) pickupNotice.value = '提柜来源有超出数量上限或目的仓国家待核实的记录,当前统计不完整,请缩小日期范围或完善仓库资料。'
|
||||
} else if (pickupResult.status === 'rejected') sourceErrors.value.push({ title: '提柜预约加载失败', details: getApiErrorDetails(pickupResult.reason, '提柜预约加载失败') })
|
||||
if (tripResult.status === 'fulfilled' && tripResult.value) {
|
||||
const projection = projectTripCalendar(tripResult.value, nextRange)
|
||||
tripEvents.value = projection.events; unresolvedTrips.value = projection.unresolved
|
||||
if (projection.unresolved.length) tripNotice.value = '部分车次无法确定起运地点或当地日期,已单列待核实,未计入每日安排。'
|
||||
} else if (tripResult.status === 'rejected') sourceErrors.value.push({ title: '用车计划加载失败', details: getApiErrorDetails(tripResult.reason, '用车计划加载失败') })
|
||||
} catch (error) {
|
||||
if (request === calendarRequest) loadError.value = getApiErrorDetails(error, '预约日历加载失败')
|
||||
} finally { if (request === calendarRequest) loading.value = false }
|
||||
@@ -353,7 +375,16 @@ onBeforeUnmount(() => { ++unscheduledRequest; ++calendarRequest; ++candidateRequ
|
||||
<LoadErrorState v-if="unscheduledError" title="未定时安排加载失败" :message="unscheduledError.message" @retry="loadUnscheduled" />
|
||||
<details v-if="unscheduledTotal" class="appointment-extra"><summary>未定时安排({{ unscheduledTotal }})</summary><DataTable v-model:page="unscheduledPage" v-model:page-size="unscheduledSize" :data="unscheduled" :columns="unscheduledColumns" :total="unscheduledTotal" row-key="appointmentNo" @update:page="loadUnscheduled" @update:page-size="unscheduledPage = 1; loadUnscheduled()"><template #actions="{ row }"><el-button @click="openEdit({key: row.appointmentNo})">查看 / 继续</el-button></template></DataTable></details>
|
||||
<p v-if="pickupNotice" role="status">{{ pickupNotice }}</p>
|
||||
<AppointmentCalendarPanel :events="events" :loading="loading" :incomplete="sourceErrors.length > 0 || !!pickupNotice" :can-create="access.can(permission.inboundOrderAppointment)" @range="load" @create="openCreate" @edit="openEdit" @delete="remove" @open="openSource" />
|
||||
<p v-if="tripNotice" role="status">{{ tripNotice }}</p>
|
||||
<details v-if="unresolvedTrips.length" class="appointment-extra">
|
||||
<summary>待核实地点的车次({{ unresolvedTrips.length }})</summary>
|
||||
<p>检索窗口内的车次,尚无法确定当地日期。原计划时间保留 UTC,不用浏览器时区换算。</p>
|
||||
<DataTable v-model:page="unresolvedPage" v-model:page-size="unresolvedSize" :data="unresolvedRows" :columns="unresolvedColumns" :total="unresolvedTrips.length" row-key="tripNo" @update:page-size="unresolvedPage = 1">
|
||||
<template #actions="{ row }"><el-button class="density-action" @click="router.push(`/overseas-master-data/trips/${encodeURIComponent(row.tripNo)}`)">查看来源</el-button></template>
|
||||
<template #issues="{ row }">{{ tripIssues(row as TripCalendarEntry) }}</template>
|
||||
</DataTable>
|
||||
</details>
|
||||
<AppointmentCalendarPanel :events="events" :loading="loading" :incomplete="sourceErrors.length > 0 || !!pickupNotice || !!tripNotice" :can-create="access.can(permission.inboundOrderAppointment)" @range="load" @create="openCreate" @edit="openEdit" @delete="remove" @open="openSource" />
|
||||
</div>
|
||||
<AppDialog v-model="dialogOpen" class="admin-density-dialog" :title="editing ? '编辑预约' : '新增预约'" width="min(1080px, calc(100vw - 48px))" :confirm-text="pendingSave ? '重试本次保存' : '保存'" :confirm-loading="saving" :confirm-disabled="detailLoading || !detailReady || (!editing && (!warehouse || !selected.length || candidateLoading || !!candidateError))" @confirm="save">
|
||||
<LoadErrorState v-if="dialogError" title="预约操作失败" :message="dialogError.message" :show-retry="false"><template #details><ApiErrorMeta :details="dialogError" /></template></LoadErrorState>
|
||||
|
||||
Reference in New Issue
Block a user