feat(appointments): show pickup appointments with source scope warnings
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { resolvePlanningSource, listUnscheduledAppointments, type ResolvedPlanningSource } from './planningApi'
|
||||
import AppointmentFollowupDialog from './AppointmentFollowupDialog.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
@@ -24,10 +24,11 @@ import AppointmentInboundCodes from './AppointmentInboundCodes.vue'
|
||||
import AppointmentCargoPanel from './AppointmentCargoPanel.vue'
|
||||
import AppointmentContainerPanel from './AppointmentContainerPanel.vue'
|
||||
import { calendarRange, dateKey } from './calendar'
|
||||
import { deleteAppointment, listArrivalCalendar, type ArrivalCalendarEntry } from './api'
|
||||
import { deleteAppointment, listArrivalCalendar, listPickupCalendar, type ArrivalCalendarEntry, type PickupCalendarEntry } from './api'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const linkedSource = ref<ResolvedPlanningSource | null>(null)
|
||||
const linkError = ref<ApiErrorDetails | null>(null)
|
||||
const linkLoading = ref(false)
|
||||
@@ -35,6 +36,8 @@ let linkRequest = 0
|
||||
const workView = ref<'calendar' | 'cargo' | 'containers'>('calendar')
|
||||
const appointments = ref<FbaAppointmentDto[]>([])
|
||||
const arrivals = ref<ArrivalCalendarEntry[]>([])
|
||||
const pickups = ref<PickupCalendarEntry[]>([])
|
||||
const pickupNotice = ref('')
|
||||
const range = ref(calendarRange(dateKey(new Date()), 'month'))
|
||||
const loading = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
@@ -99,6 +102,18 @@ const events = computed<CalendarEvent[]>(() => [...arrivals.value.map(arrival =>
|
||||
{ label: '柜次号', value: arrival.bookingNo },
|
||||
{ label: '货物去向', value: arrival.destinationWarehouseCode },
|
||||
],
|
||||
})), ...pickups.value.map(pickup => ({
|
||||
key: `pickup-${pickup.containerBookingId}`, type: 'pickup' as const,
|
||||
date: pickup.pickupAppointmentAt.slice(0, 10), time: pickup.pickupAppointmentAt.slice(11, 16),
|
||||
title: `${pickup.containerNo || pickup.bookingNo || '未关联柜号'} 预约提柜`,
|
||||
sourcePath: `/container-loading/overseas/${encodeURIComponent(pickup.containerBookingId)}`,
|
||||
subtitle: '美国目的仓相关提柜安排',
|
||||
fields: [
|
||||
{ label: '柜次号', value: pickup.bookingNo }, { label: '目的港', value: pickup.destinationPortName },
|
||||
{ label: '目的仓', value: pickup.destinationWarehouseCode }, { label: '车队', value: pickup.fleetSupplierName },
|
||||
{ label: '实际提柜', value: pickup.actualPickupAt || '尚未记录' },
|
||||
{ label: '时间口径', value: '原提柜业务时间,港口时区待核实' },
|
||||
],
|
||||
})), ...appointments.value.map(appointment => ({
|
||||
key: appointment.appointmentNo, type: 'delivery' as const, date: (appointment.appointmentAt || appointment.plannedAt || appointment.plannedDate || '').slice(0, 10),
|
||||
time: (appointment.appointmentAt || appointment.plannedAt || '').slice(11, 16) + (appointment.appointmentEndAt ? `–${appointment.appointmentEndAt.slice(11, 16)}` : ''), title: `${appointment.fbaWarehouseCode} ${appointment.appointmentStatus === 'SUCCESS' ? '已确认送仓' : '计划送仓'}`,
|
||||
@@ -127,21 +142,33 @@ async function load(nextRange = range.value) {
|
||||
errorTitle.value = '预约日历加载失败'
|
||||
appointments.value = []
|
||||
arrivals.value = []
|
||||
pickups.value = []
|
||||
const canReadPickup = access.can('platform:firstleg:terminal-drayage:list')
|
||||
pickupNotice.value = canReadPickup ? '' : '无提柜查看权限,当前日历不包含提柜预约。'
|
||||
try {
|
||||
const [result, arrivalResult] = await Promise.allSettled([
|
||||
const [result, arrivalResult, pickupResult] = await Promise.allSettled([
|
||||
listFbaAppointmentCalendar(nextRange, calendarController.signal),
|
||||
listArrivalCalendar(nextRange, calendarController.signal),
|
||||
canReadPickup ? listPickupCalendar(nextRange, calendarController.signal) : Promise.resolve(null),
|
||||
])
|
||||
if (request !== calendarRequest) return
|
||||
if (result.status === 'fulfilled') appointments.value = result.value
|
||||
else sourceErrors.value.push({ title: '送仓预约加载失败', details: getApiErrorDetails(result.reason, '送仓预约加载失败') })
|
||||
if (arrivalResult.status === 'fulfilled') arrivals.value = arrivalResult.value
|
||||
else sourceErrors.value.push({ title: '船舶到港加载失败', details: getApiErrorDetails(arrivalResult.reason, '船舶到港加载失败') })
|
||||
if (pickupResult.status === 'fulfilled' && pickupResult.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, '提柜预约加载失败') })
|
||||
} catch (error) {
|
||||
if (request === calendarRequest) loadError.value = getApiErrorDetails(error, '预约日历加载失败')
|
||||
} finally { if (request === calendarRequest) loading.value = false }
|
||||
}
|
||||
|
||||
function openSource(event: CalendarEvent) {
|
||||
if (event.sourcePath) void router.push(event.sourcePath)
|
||||
}
|
||||
|
||||
function openCreate(date: string) {
|
||||
++candidateRequest
|
||||
++detailRequest
|
||||
@@ -325,7 +352,8 @@ onBeforeUnmount(() => { ++unscheduledRequest; ++calendarRequest; ++candidateRequ
|
||||
<LoadErrorState v-for="error in sourceErrors" :key="error.title" :title="error.title" :message="error.details.message" :loading="loading" @retry="load()"><template #details><ApiErrorMeta :details="error.details" /></template></LoadErrorState>
|
||||
<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>
|
||||
<AppointmentCalendarPanel :events="events" :loading="loading" :incomplete="sourceErrors.length > 0" :can-create="access.can(permission.inboundOrderAppointment)" @range="load" @create="openCreate" @edit="openEdit" @delete="remove" />
|
||||
<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" />
|
||||
</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