diff --git a/app/Enums/FlightNotificationType.php b/app/Enums/FlightNotificationType.php index 7f935ed..58e3cf1 100644 --- a/app/Enums/FlightNotificationType.php +++ b/app/Enums/FlightNotificationType.php @@ -28,7 +28,7 @@ enum FlightNotificationType: string { return match ($this) { self::Booked => "{$user->name} booked a new flight", - self::Logged => "{$user->name} logged a new flight", + self::Logged => "{$user->name} logged a historic flight", self::Cancelled => "{$user->name} cancelled a flight", self::Departed => "{$user->name} is taking off!", self::Arrived => "{$user->name} has landed!", diff --git a/app/Models/User.php b/app/Models/User.php index ebd7748..ef310c7 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -153,7 +153,26 @@ class User extends Authenticatable implements MustVerifyEmail ->toJson(); }); - $collection = collect(json_decode($json)); + $dateFormat = auth()->user()?->resolved_settings['date_format'] ?? 'j M Y'; + $timeFormat = auth()->user()?->resolved_settings['time_format'] === '24hr' ? 'Hi' : 'g:iA'; + + $collection = collect(json_decode($json)) + ->map(function ($flight) use ($dateFormat, $timeFormat) { + $departure = Carbon::parse($flight->departure_date) + ->setTimezone($flight->departure_airport->timezone); + + $arrival = Carbon::parse($flight->arrival_date) + ->setTimezone($flight->arrival_airport->timezone); + + $flight->departure_date_display = $departure->format($dateFormat); + $flight->departure_time_display = $departure->format($timeFormat); + + $flight->arrival_date_display = $arrival->format($dateFormat); + $flight->arrival_time_display = $arrival->format($timeFormat); + + return $flight; + }); + $today = now('UTC')->toDateString(); return match ($filter) { diff --git a/app/Settings/SettingsRegistry.php b/app/Settings/SettingsRegistry.php index 8720fc0..f6120a1 100644 --- a/app/Settings/SettingsRegistry.php +++ b/app/Settings/SettingsRegistry.php @@ -54,6 +54,19 @@ class SettingsRegistry ['value' => '24hr', 'label' => '24-Hour'], ], ], + [ + 'key' => 'date_format', + 'type' => 'select', + 'label' => 'Date Format', + 'category' => 'Localization', + 'default' => 'j M Y', + 'options' => [ + ['value' => 'j M Y', 'label' => '20 Feb 2015'], + ['value' => 'Y-m-d', 'label' => '2015-02-20'], + ['value' => 'd/m/Y', 'label' => '20/02/2015'], + ['value' => 'm/d/Y', 'label' => '02/20/2015'], + ], + ], [ 'key' => 'default_login_page', 'type' => 'select', diff --git a/resources/js/Components/FlightsGoneBy/DepartureBoardTableRow.vue b/resources/js/Components/FlightsGoneBy/DepartureBoardTableRow.vue index c5e1bb6..504f4c5 100644 --- a/resources/js/Components/FlightsGoneBy/DepartureBoardTableRow.vue +++ b/resources/js/Components/FlightsGoneBy/DepartureBoardTableRow.vue @@ -12,7 +12,6 @@ import { usePage } from "@inertiajs/vue3"; import { User } from "@/Types/types"; import Mono from "@/Components/FlightsGoneBy/Mono.vue"; import DayDifference from "@/Components/FlightsGoneBy/DayDifference.vue"; -import Time from "@/Components/FlightsGoneBy/Time.vue"; const props = defineProps<{ flight: Flight @@ -99,12 +98,12 @@ const showColumn = (column: string) => props.columnsToShow.includes(column) - + {{flight.departure_time_display}} - + {{flight.arrival_time_display}} diff --git a/resources/js/Components/FlightsGoneBy/Time.vue b/resources/js/Components/FlightsGoneBy/Time.vue deleted file mode 100644 index e176007..0000000 --- a/resources/js/Components/FlightsGoneBy/Time.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - - - diff --git a/resources/js/Pages/AddFlight.vue b/resources/js/Pages/AddFlight.vue index fda1745..146ce90 100644 --- a/resources/js/Pages/AddFlight.vue +++ b/resources/js/Pages/AddFlight.vue @@ -41,95 +41,9 @@ const isEdit = !!props.flight // ── User format preferences ───────────────────────────────────────────────── const timeFormat = computed(() => page.auth?.user?.resolved_settings?.time_format ?? 'ampm') -// day.js/date-fns-style tokens, e.g. 'D MMM YYYY' -> "7 Dec 2026". Not user-configurable yet. -const dateFormat = computed(() => 'DD-MM-YYYY') - -// ── Date token formatting/parsing helpers (D, DD, M, MM, MMM, MMMM, YY, YYYY) ── const pad = (n: number) => String(n).padStart(2, '0') -const MONTHS_SHORT = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'] -const MONTHS_LONG = ['January','February','March','April','May','June','July','August','September','October','November','December'] - -// Formats a Date according to a token format string. Tokens matched longest-first -// so e.g. "YYYY" isn't partially consumed by a lone "Y"-style rule. -function formatDateForFormat(d: Date | null, format: string): string { - if (!d) return '' - return format.replace(/YYYY|YY|MMMM|MMM|MM|M|DD|D/g, (token) => { - switch (token) { - case 'YYYY': return String(d.getFullYear()) - case 'YY': return String(d.getFullYear()).slice(-2) - case 'MMMM': return MONTHS_LONG[d.getMonth()] - case 'MMM': return MONTHS_SHORT[d.getMonth()] - case 'MM': return pad(d.getMonth() + 1) - case 'M': return String(d.getMonth() + 1) - case 'DD': return pad(d.getDate()) - case 'D': return String(d.getDate()) - default: return token - } - }) -} - -// Extracts the ordered list of D/M/Y tokens from a format string, e.g. -// "D MMM YYYY" -> ['D', 'MMM', 'YYYY'] -function getFormatTokens(format: string): string[] { - return format.match(/YYYY|YY|MMMM|MMM|MM|M|DD|D/g) ?? ['D', 'MMM', 'YYYY'] -} - -// Parses user-typed text into a Date, following the token order of `format`. -// Splits on any run of non-alphanumeric characters, so "/", "-", ".", and -// spaces are all accepted as separators regardless of what the format uses. -// Month tokens (MMM/MMMM) match against month names case-insensitively. -function parseDateInputStr(input: string, format: string): Date | null { - const trimmed = input.trim() - if (!trimmed) return null - - const parts = trimmed.split(/[^a-zA-Z0-9]+/).filter(Boolean) - const tokens = getFormatTokens(format) - if (parts.length !== tokens.length) return null - - let day: number | null = null - let month: number | null = null // 0-indexed - let year: number | null = null - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i] - const part = parts[i] - - if (token === 'D' || token === 'DD') { - const val = parseInt(part, 10) - if (isNaN(val)) return null - day = val - } else if (token === 'M' || token === 'MM') { - const val = parseInt(part, 10) - if (isNaN(val)) return null - month = val - 1 - } else if (token === 'MMM' || token === 'MMMM') { - const lower = part.toLowerCase() - const idx = MONTHS_LONG.findIndex(m => m.toLowerCase().startsWith(lower) || lower.startsWith(m.slice(0, 3).toLowerCase())) - if (idx === -1) return null - month = idx - } else if (token === 'YYYY') { - const val = parseInt(part, 10) - if (isNaN(val)) return null - year = val - } else if (token === 'YY') { - const val = parseInt(part, 10) - if (isNaN(val)) return null - year = val < 100 ? 2000 + val : val - } - } - - if (day === null || month === null || year === null) return null - - const date = new Date(year, month, day) - // Reject dates that overflowed (e.g. day 31 in a 30-day month) - if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { - return null - } - return date -} - // ── Flight number lookup ────────────────────────────────────────────────────── const flightNumber = ref(props.flight?.flight_number ?? '') @@ -397,47 +311,36 @@ 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) -// ── Editable, format-aware date text inputs ───────────────────────────────── +// ── Native date inputs ─────────────────────────────────────────────────────── // departureDatePart / arrivalDatePart stay the source of truth as Date -// objects. These input refs are what the text-fields actually bind to, so -// the user can type freely; parsing only commits on blur/enter. +// objects. These computed get/set pairs are what the native +// elements bind to — the browser handles locale display, typing, and +// validation itself, so no custom format-aware parsing is needed here. +// The Vuetify v-date-picker is still available via a separate calendar icon +// for users who prefer to pick from a calendar rather than type. -const departureDateInput = ref(formatDateForFormat(departureDatePart.value, dateFormat.value)) -const arrivalDateInput = ref(formatDateForFormat(arrivalDatePart.value, dateFormat.value)) - -watch([departureDatePart, dateFormat], () => { - departureDateInput.value = formatDateForFormat(departureDatePart.value, dateFormat.value) -}) -watch([arrivalDatePart, dateFormat], () => { - arrivalDateInput.value = formatDateForFormat(arrivalDatePart.value, dateFormat.value) -}) - -function commitDepartureDateInput() { - if (!departureDateInput.value.trim()) { - departureDatePart.value = null - return - } - const parsed = parseDateInputStr(departureDateInput.value, dateFormat.value) - if (parsed) { - departureDatePart.value = parsed - } else { - departureDateInput.value = formatDateForFormat(departureDatePart.value, dateFormat.value) - } +function toIsoDateString(d: Date | null): string { + if (!d) return '' + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` } -function commitArrivalDateInput() { - if (!arrivalDateInput.value.trim()) { - arrivalDatePart.value = null - return - } - const parsed = parseDateInputStr(arrivalDateInput.value, dateFormat.value) - if (parsed) { - arrivalDatePart.value = parsed - } else { - arrivalDateInput.value = formatDateForFormat(arrivalDatePart.value, dateFormat.value) - } +function fromIsoDateString(value: string): Date | null { + if (!value) return null + const [y, m, d] = value.split('-').map(Number) + if (!y || !m || !d) return null + return new Date(y, m - 1, d) } +const departureDateValue = computed({ + get: () => toIsoDateString(departureDatePart.value), + set: (value: string) => { departureDatePart.value = fromIsoDateString(value) }, +}) + +const arrivalDateValue = computed({ + get: () => toIsoDateString(arrivalDatePart.value), + set: (value: string) => { arrivalDatePart.value = fromIsoDateString(value) }, +}) + // ── Editable, format-aware time text fields ──────────────────────────────── // departureTimePart / arrivalTimePart stay the source of truth as internal // "HH:mm" (24hr) strings. These input refs are what the text-fields actually @@ -569,28 +472,30 @@ function commitArrivalTimeInput() { - -