fix: show overnight appointments on each covered calendar day

This commit is contained in:
2026-10-10 12:40:52 +08:00
parent e3038edd38
commit 0ca12de2f4
4 changed files with 32 additions and 5 deletions
@@ -17,6 +17,19 @@ describe('appointment calendar interactions', () => {
expect(wrapper.get('[aria-label="当天安排"]').text()).toContain('当天暂无到港或送仓预约')
})
it('shows an overnight slot on both days without fetching again and excludes midnight ends', async () => {
const wrapper = render()
await wrapper.setProps({ events: [
{ key: 'overnight', type: 'delivery', date: '2026-09-23', endAt: '2026-09-24T01:00:00', time: '23:00–01:00', title: '跨日送仓', fields: [] },
{ key: 'midnight', type: 'delivery', date: '2026-09-23', endAt: '2026-09-24T00:00:00', time: '23:00–00:00', title: '午夜结束', fields: [] },
] })
expect(wrapper.find('[aria-label="2026-09-23,2 项安排"]').exists()).toBe(true)
expect(wrapper.find('[aria-label="2026-09-24,1 项安排"]').exists()).toBe(true)
expect(wrapper.get('[aria-label="当天安排"]').text()).toContain('跨日送仓')
expect(wrapper.get('[aria-label="当天安排"]').text()).not.toContain('午夜结束')
expect(wrapper.emitted('range')).toBeUndefined()
})
it('requests exactly one range for a view switch and navigates seven days', async () => {
const wrapper = render()
await wrapper.get('[aria-label="预约视图"]').findAll('button')[1]!.trigger('click')
@@ -2,11 +2,12 @@
import '@/styles/adminDensity.css'
import { computed, ref } from 'vue'
import AppointmentInboundCodes from './AppointmentInboundCodes.vue'
import { addDays, calendarDates, calendarRange, dateKey, moveCalendar, type CalendarMode } from './calendar'
import { addDays, calendarDates, calendarRange, dateKey, moveCalendar, occursOnDate, type CalendarMode } from './calendar'
export interface CalendarEvent {
key: string
date: string
endAt?: string
time: string
type: 'arrival' | 'delivery' | 'pickup' | 'trip'
title: string
@@ -38,9 +39,12 @@ const dates = computed(() => calendarDates(selectedDate.value, mode.value))
const grouped = computed(() => {
const result = new Map<string, CalendarEvent[]>()
for (const event of props.events) {
const group = result.get(event.date) || []
group.push(event)
result.set(event.date, group)
for (const date of dates.value) {
if (!occursOnDate(event.date, event.endAt, date)) continue
const group = result.get(date) || []
group.push(event)
result.set(date, group)
}
}
for (const group of result.values()) group.sort((a, b) => a.time.localeCompare(b.time) || a.key.localeCompare(b.key))
return result
@@ -132,12 +132,14 @@ const events = computed<CalendarEvent[]>(() => [...arrivals.value.map(arrival =>
],
})), ...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' ? '已确认送仓' : '计划送仓'}`,
endAt: (appointment.appointmentAt ? appointment.appointmentEndAt : appointment.plannedEndAt) || undefined,
time: (appointment.appointmentAt || appointment.plannedAt || '').slice(11, 16) + ((appointment.appointmentAt ? appointment.appointmentEndAt : appointment.plannedEndAt) ? `–${(appointment.appointmentAt ? appointment.appointmentEndAt : appointment.plannedEndAt)!.slice(11, 16)}` : ''), title: `${appointment.fbaWarehouseCode} ${appointment.appointmentStatus === 'SUCCESS' ? '已确认送仓' : '计划送仓'}`,
fields: [
{ label: '预约仓库', value: appointment.fbaWarehouseCode },
{ label: '系统单号', value: appointment.appointmentNo },
{ label: '外部约号', value: appointment.confirmationNo },
{ label: '地点时区', value: appointment.timezone || '待核实' },
{ label: '完整时段', value: `${appointment.appointmentAt || appointment.plannedAt || appointment.plannedDate || '待定'}${(appointment.appointmentAt ? appointment.appointmentEndAt : appointment.plannedEndAt) ? ` — ${appointment.appointmentAt ? appointment.appointmentEndAt : appointment.plannedEndAt}` : ''}` },
{ label: '预约状态', value: statusLabels[appointment.appointmentStatus] || '—' },
{ label: '货物信息', value: `${appointment.selectedCount} 票 / ${new Set(appointment.items.map(item => item.customerId)).size} 个客户` },
{ label: '预约备注', value: appointment.appointmentRemark },
@@ -1,5 +1,13 @@
export type CalendarMode = 'month' | 'week'
/** End times are exclusive; a slot ending at midnight does not occupy the next day. */
export function occursOnDate(startDate: string, endAt: string | undefined, date: string): boolean {
if (date === startDate) return true
if (date < startDate || !endAt) return false
const endDate = endAt.slice(0, 10)
return date < endDate || (date === endDate && /[1-9]/.test(endAt.slice(11)))
}
/** Calendar dates are warehouse-local dates, never UTC conversions. */
export function dateKey(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`