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 @@
-
-
-
- {{ formattedTime }}
-
-
-
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() {
-
-
-
+
+
+
+
+
+
+
+
-
-
+
@@ -626,29 +531,32 @@ function commitArrivalTimeInput() {
-
-
-
+
+
+
+
+
+
+
+
-
-
+
@@ -860,4 +768,8 @@ function commitArrivalTimeInput() {
diff --git a/resources/js/Pages/ReconcileFlight.vue b/resources/js/Pages/ReconcileFlight.vue
index a3608ac..7a39b5c 100644
--- a/resources/js/Pages/ReconcileFlight.vue
+++ b/resources/js/Pages/ReconcileFlight.vue
@@ -191,7 +191,6 @@ watch(() => form.flight_reason, (val) => console.log('flight_reason changed:', v
- {{console.log(form.flight_reason)}}