From c016a4aaaf014445e78685584831935e726777b9 Mon Sep 17 00:00:00 2001 From: josh Date: Sun, 12 Jul 2026 00:56:23 +1000 Subject: [PATCH 1/3] Add Date and Time Picker Separate --- resources/js/Pages/AddFlight.vue | 250 +++++++++++++++++++++++++++---- 1 file changed, 224 insertions(+), 26 deletions(-) diff --git a/resources/js/Pages/AddFlight.vue b/resources/js/Pages/AddFlight.vue index d913c54..fa106fc 100644 --- a/resources/js/Pages/AddFlight.vue +++ b/resources/js/Pages/AddFlight.vue @@ -7,6 +7,7 @@ import AircraftSearchBox from '@/Components/FlightsGoneBy/AircraftSearchBox.vue' import AirportSearchBox from '@/Components/FlightsGoneBy/AirportSearchBox.vue' import type {SeatType, FlightReason, FlightClass, CrewType} from '@/Types/types' import { ref, watch, computed } from 'vue' +import { VTimePicker, VDatePicker } from 'vuetify/components' defineOptions({ layout: MainLayout }) @@ -91,6 +92,20 @@ async function lookupFlight() { if (!form.aircraft) form.aircraft = data.aircraft_options[0] } + // Populate scheduled times if the API returned them and the user + // hasn't already entered a time themselves. Suppress the +1hr + // auto-fill guess while doing this, since we have real data. + suppressArrivalAutoFill = true + const scheduledDep = normalizeTimeString(data.scheduled_departure_time) + if (scheduledDep && !departureTimePart.value) { + departureTimePart.value = scheduledDep + } + const scheduledArr = normalizeTimeString(data.scheduled_arrival_time) + if (scheduledArr && !arrivalTimePart.value) { + arrivalTimePart.value = scheduledArr + } + suppressArrivalAutoFill = false + lookupKey.value++ } catch (e) { lookupError.value = String(e) @@ -188,19 +203,117 @@ const fromOptionsData = ref<{ value: number; title: string; country_code: st const toOptionsData = ref<{ value: number; title: string; country_code: string }[]>(props.flight?.to_options ?? []) const aircraftOptionsData = ref<{ value: number; title: string }[]>(props.flight?.aircraft_options ?? []) +// ── Split date/time pickers ─────────────────────────────────────────────────── +// form.departure_date / form.arrival_date stay as the single source of truth +// (YYYY-MM-DDTHH:mm strings) that the rest of the component and submit() rely +// on. These refs + watchers just give the template two separate pickers while +// keeping those combined strings in sync — no server-side changes needed. -watch(() => form.departure_date, (newVal) => { - if (!newVal || form.arrival_date) return - const dep = new Date(newVal) +const pad = (n: number) => String(n).padStart(2, '0') + +function parseDateTime(dt: string | null | undefined) { + if (!dt) return { date: null as Date | null, time: null as string | null } + const d = new Date(dt) + if (isNaN(d.getTime())) return { date: null as Date | null, time: null as string | null } + return { + date: new Date(d.getFullYear(), d.getMonth(), d.getDate()), + time: `${pad(d.getHours())}:${pad(d.getMinutes())}`, + } +} + +function combineDateTime(date: Date | null, time: string | null): string { + if (!date) return '' + const [hh, mm] = (time ?? '00:00').split(':') + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${(hh ?? '00').padStart(2, '0')}:${(mm ?? '00').padStart(2, '0')}` +} + +const formatDateDisplay = (d: Date | null) => + d ? d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) : '' + +// Time is stored internally as a 24hr "HH:mm" string (VTimePicker's model +// value is always 24hr regardless of the `format` display prop). This just +// formats it for display in the read-only text fields. +function formatTimeDisplay(t: string | null): string { + if (!t) return '' + const [hStr, mStr] = t.split(':') + let h = parseInt(hStr, 10) + if (isNaN(h)) return '' + const ampm = h >= 12 ? 'PM' : 'AM' + h = h % 12 + if (h === 0) h = 12 + return `${h}:${mStr} ${ampm}` +} + +function clearDepartureDate() { departureDatePart.value = null } +function clearDepartureTime() { departureTimePart.value = null } +function clearArrivalDate() { arrivalDatePart.value = null } +function clearArrivalTime() { arrivalTimePart.value = null } + +// Normalizes whatever shape the API sends a scheduled time in ("14:30", +// "14:30:00", or a full datetime string) down to the internal "HH:mm" format. +function normalizeTimeString(t: string | null | undefined): string | null { + if (!t) return null + const match = t.match(/(\d{1,2}):(\d{2})/) + if (match) { + return `${match[1].padStart(2, '0')}:${match[2]}` + } + const d = new Date(t) + if (!isNaN(d.getTime())) { + return `${pad(d.getHours())}:${pad(d.getMinutes())}` + } + return null +} + +const depInit = parseDateTime(form.departure_date) +const arrInit = parseDateTime(form.arrival_date) + +const departureDatePart = ref(depInit.date) +const departureTimePart = ref(depInit.time) +const arrivalDatePart = ref(arrInit.date) +const arrivalTimePart = ref(arrInit.time) + +const depDateMenu = ref(false) +const depTimeMenu = ref(false) +const arrDateMenu = ref(false) +const arrTimeMenu = ref(false) + +// A short guard so programmatic updates (e.g. auto-fill of arrival) don't +// fight with the "only auto-fill once" behaviour below. +let suppressArrivalAutoFill = false + +// Keep form.departure_date in sync whenever either split piece changes +watch([departureDatePart, departureTimePart], () => { + form.departure_date = combineDateTime(departureDatePart.value, departureTimePart.value) +}) + +// Keep form.arrival_date in sync whenever either split piece changes +watch([arrivalDatePart, arrivalTimePart], () => { + if (suppressArrivalAutoFill) return + form.arrival_date = combineDateTime(arrivalDatePart.value, arrivalTimePart.value) +}) + +// Auto-fill arrival (+1hr) the first time a departure TIME is chosen (and a +// departure date is already set) and arrival is still empty. Picking the +// departure date alone no longer triggers this — only setting the time does. +watch(departureTimePart, (newTime) => { + if (suppressArrivalAutoFill) return + if (!newTime || !departureDatePart.value || arrivalDatePart.value) return + + const dep = new Date(departureDatePart.value) + const [h, m] = newTime.split(':').map(Number) + dep.setHours(h, m) const arr = new Date(dep.getTime() + 60 * 60 * 1000) - const pad = (n: number) => String(n).padStart(2, '0') - form.arrival_date = `${arr.getFullYear()}-${pad(arr.getMonth() + 1)}-${pad(arr.getDate())}T${pad(arr.getHours())}:${pad(arr.getMinutes())}` -}) + suppressArrivalAutoFill = true + arrivalDatePart.value = new Date(arr.getFullYear(), arr.getMonth(), arr.getDate()) + arrivalTimePart.value = `${pad(arr.getHours())}:${pad(arr.getMinutes())}` + suppressArrivalAutoFill = false + + form.arrival_date = combineDateTime(arrivalDatePart.value, arrivalTimePart.value) +}, { flush: 'sync' }) const getArrivalBound = (form: any, offsetDays: number) => { if (!form.departure_date) return undefined - const pad = (n: number) => String(n).padStart(2, '0') const d = new Date(form.departure_date) d.setDate(d.getDate() + offsetDays) return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}` @@ -208,6 +321,9 @@ const getArrivalBound = (form: any, offsetDays: number) => { const arrivalMin = computed(() => getArrivalBound(form, -2)) const arrivalMax = computed(() => getArrivalBound(form, 3)) + +const arrivalMinDate = computed(() => arrivalMin.value ? new Date(arrivalMin.value) : undefined) +const arrivalMaxDate = computed(() => arrivalMax.value ? new Date(arrivalMax.value) : undefined)