mdi-bell-outline
@@ -106,6 +121,16 @@ watch(open, async (isOpen) => {
overflow: hidden;
}
+@media (max-width: 480px) {
+ .notif-menu {
+ position: fixed;
+ top: 4.5rem; /* adjust to sit just below your navbar height */
+ left: 50%;
+ transform: translateX(-50%);
+ width: min(320px, calc(100vw - 1.5rem));
+ }
+}
+
.notif-header {
display: flex;
align-items: center;
diff --git a/resources/js/Components/FlightsGoneBy/Panels/RoutePanel.vue b/resources/js/Components/FlightsGoneBy/Panels/RoutePanel.vue
index 1bf7080..cf044fe 100644
--- a/resources/js/Components/FlightsGoneBy/Panels/RoutePanel.vue
+++ b/resources/js/Components/FlightsGoneBy/Panels/RoutePanel.vue
@@ -4,6 +4,7 @@ import FlightMap from "@/Components/FlightsGoneBy/FlightMap.vue";
import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue";
import DetailRows from "@/Components/FlightsGoneBy/Panels/DetailRows.vue";
import DetailRow from "@/Components/FlightsGoneBy/Panels/DetailRow.vue";
+import PanelLabel from "@/Components/FlightsGoneBy/Panels/PanelLabel.vue";
defineProps<{
flight: Flight
@@ -19,6 +20,10 @@ defineProps<{
+
+ Notes
+ {{flight.note}}
+
diff --git a/resources/js/Components/FlightsGoneBy/Tooltips/MissingEntityLink.vue b/resources/js/Components/FlightsGoneBy/Tooltips/MissingEntityLink.vue
new file mode 100644
index 0000000..4d1705d
--- /dev/null
+++ b/resources/js/Components/FlightsGoneBy/Tooltips/MissingEntityLink.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/Pages/AddFlight.vue b/resources/js/Pages/AddFlight.vue
index 146ce90..8e502d8 100644
--- a/resources/js/Pages/AddFlight.vue
+++ b/resources/js/Pages/AddFlight.vue
@@ -6,8 +6,9 @@ import AirlineSearchBox from '@/Components/FlightsGoneBy/AirlineSearchBox.vue'
import AircraftSearchBox from '@/Components/FlightsGoneBy/AircraftSearchBox.vue'
import AirportSearchBox from '@/Components/FlightsGoneBy/AirportSearchBox.vue'
import type {SeatType, FlightReason, FlightClass, CrewType, SharedProps} from '@/Types/types'
-import { ref, watch, computed } from 'vue'
+import { ref, watch, computed, onMounted, nextTick } from 'vue'
import { VTimePicker, VDatePicker } from 'vuetify/components'
+import MissingEntityLink from "@/Components/FlightsGoneBy/Tooltips/MissingEntityLink.vue";
defineOptions({ layout: MainLayout })
const page = usePage().props
@@ -49,25 +50,13 @@ const pad = (n: number) => String(n).padStart(2, '0')
const flightNumber = ref(props.flight?.flight_number ?? '')
const lookupLoading = ref(false)
const lookupError = ref(null)
-const lookupComplete = ref(true)
-interface LookupResult {
- airline_options: { value: number; title: string, logo_url: string }[]
- from_options: { value: number; title: string; country_code: string }[]
- to_options: { value: number; title: string; country_code: string }[]
- aircraft_options: { value: number; title: string }[]
- scheduled_departure_time: string | null
- scheduled_arrival_time: string | null
-}
-
-const lookupResult = ref(null)
const lookupKey = ref(0)
async function lookupFlight() {
if (!flightNumber.value.trim()) return
lookupLoading.value = true
lookupError.value = null
- lookupResult.value = null
try {
const response = await fetch(`${route('flights.lookup')}?number=${encodeURIComponent(flightNumber.value.trim())}`, {
@@ -78,8 +67,6 @@ async function lookupFlight() {
lookupError.value = data.message ?? 'Lookup failed.'
return
}
- lookupResult.value = data
- lookupComplete.value = true
if (data.airline_options?.length) {
airlineOptionsData.value = data.airline_options
if (!form.airline) form.airline = data.airline_options[0]
@@ -111,6 +98,12 @@ async function lookupFlight() {
}
suppressArrivalAutoFill = false
+ // NOTE: if the lookup API is ever extended to return a day
+ // difference for the arrival (e.g. `data.arrival_day_offset`),
+ // wire it in here: `arrivalOffset.value = data.arrival_day_offset`
+ // (clamped/mapped to 'custom' if outside -1..2). Left out for now
+ // since the API doesn't provide it yet.
+
lookupKey.value++
} catch (e) {
lookupError.value = String(e)
@@ -122,7 +115,6 @@ async function lookupFlight() {
// ── Display form (drives the template) ───────────────────────────────────────
const form = useForm({
- flight_number: props.flight?.flight_number ?? '',
departure_date: props.flight?.departure_date ?? '',
arrival_date: props.flight?.arrival_date ?? '',
from: props.flight?.from_options[0] ?? null as { value: number; title: string; country_code: string } | null,
@@ -163,9 +155,9 @@ const submitForm = useForm({
crew_type_id: null as number | null,
note: '' as string | null,
auto_update: false,
+ add_more: false,
})
-
const departureIsFuture = computed(() => {
if (!form.departure_date) return false
return new Date(form.departure_date) > new Date()
@@ -177,7 +169,7 @@ watch(departureIsFuture, (isFuture) => {
}, { immediate: true })
-function submit() {
+function submit(addMore = false) {
submitForm.flight_number = flightNumber.value
submitForm.departure_date = form.departure_date
submitForm.arrival_date = form.arrival_date
@@ -193,6 +185,7 @@ function submit() {
submitForm.crew_type_id = form.crew_type?.id ?? null
submitForm.note = form.note
submitForm.auto_update = form.auto_update
+ submitForm.add_more = addMore
if (isEdit) {
submitForm.put(route('flights.update', { flight: props.flight!.id }))
@@ -230,9 +223,15 @@ function combineDateTime(date: Date | null, time: string | null): string {
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${(hh ?? '00').padStart(2, '0')}:${(mm ?? '00').padStart(2, '0')}`
}
-function clearDepartureDate() { departureDatePart.value = null }
+// Whole-day difference between two Date objects (time-of-day ignored).
+function diffInDays(from: Date | null, to: Date | null): number | null {
+ if (!from || !to) return null
+ const a = new Date(from.getFullYear(), from.getMonth(), from.getDate())
+ const b = new Date(to.getFullYear(), to.getMonth(), to.getDate())
+ return Math.round((b.getTime() - a.getTime()) / 86_400_000)
+}
+
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",
@@ -278,19 +277,88 @@ watch([arrivalDatePart, arrivalTimePart], () => {
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.
+// ── Arrival day offset ───────────────────────────────────────────────────────
+// Rather than making the user pick an independent arrival date, arrival date
+// is normally *derived* from departure date + a small relative offset
+// (-1 / Same day / +1 / +2). "Custom" drops back to a real date picker for
+// the rare case that falls outside that range. This avoids needing airport
+// time zones to work out the arrival date, which a duration-based input
+// would require.
+
+type ArrivalOffset = -1 | 0 | 1 | 2
+
+const arrivalOffsetOptions: { value: ArrivalOffset; label: string }[] = [
+ { value: -1, label: '−1 Day' },
+ { value: 0, label: 'Same Day' },
+ { value: 1, label: '+1 Day' },
+ { value: 2, label: '+2 Days' },
+]
+
+const initialDiff = diffInDays(depInit.date, arrInit.date)
+const initialDiffIsQuickOption = initialDiff !== null && ([-1, 0, 1, 2] as number[]).includes(initialDiff)
+
+// 'offset' (the default): arrival date is derived from departure date + the
+// selected quick offset. 'custom': the user has dropped into a real date
+// picker, e.g. because the actual gap is more than +2 days.
+const arrivalDateMode = ref<'offset' | 'custom'>(
+ initialDiff !== null && !initialDiffIsQuickOption ? 'custom' : 'offset'
+)
+
+const arrivalOffset = ref(
+ initialDiffIsQuickOption ? (initialDiff as ArrivalOffset) : 0
+)
+
+function applyArrivalOffset() {
+ if (suppressArrivalAutoFill) return
+ if (arrivalDateMode.value === 'custom') return
+ if (!departureDatePart.value) {
+ arrivalDatePart.value = null
+ return
+ }
+ const d = new Date(departureDatePart.value)
+ d.setDate(d.getDate() + arrivalOffset.value)
+ arrivalDatePart.value = d
+}
+
+// Re-derive arrival date whenever departure date moves, or the offset is
+// changed by hand.
+watch(departureDatePart, applyArrivalOffset)
+watch(arrivalOffset, applyArrivalOffset)
+
+function useCustomArrivalDate() {
+ arrivalDateMode.value = 'custom'
+}
+
+function useQuickArrivalOffset() {
+ arrivalDateMode.value = 'offset'
+ applyArrivalOffset()
+}
+
+function formatFullDate(d: Date | null): string {
+ if (!d) return ''
+ return d.toLocaleDateString(undefined, { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' })
+}
+
+// Auto-fill arrival TIME (+1hr) the first time a departure time is chosen
+// (and a departure date is already set) and arrival time is still empty.
+// If adding the hour rolls over midnight, bump the offset selector to match
+// (e.g. departs 23:30 -> offset flips to "+1") so the resolved arrival date
+// stays correct without the user having to notice and fix it themselves.
watch(departureTimePart, (newTime) => {
if (suppressArrivalAutoFill) return
- if (!newTime || !departureDatePart.value || arrivalDatePart.value) return
+ if (arrivalDateMode.value === 'custom') return
+ if (!newTime || !departureDatePart.value || arrivalTimePart.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 rawDiff = diffInDays(departureDatePart.value, arr) ?? 0
+ const clampedDiff = Math.min(Math.max(rawDiff, -1), 2) as -1 | 0 | 1 | 2
+
suppressArrivalAutoFill = true
+ arrivalOffset.value = clampedDiff
arrivalDatePart.value = new Date(arr.getFullYear(), arr.getMonth(), arr.getDate())
arrivalTimePart.value = `${pad(arr.getHours())}:${pad(arr.getMinutes())}`
suppressArrivalAutoFill = false
@@ -313,11 +381,31 @@ const arrivalMaxDate = computed(() => arrivalMax.value ? new Date(arrivalMax.val
// ── Native date inputs ───────────────────────────────────────────────────────
// departureDatePart / arrivalDatePart stay the source of truth as Date
-// 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.
+// objects. The elements below are deliberately left
+// UNCONTROLLED from Vue's perspective — no v-model, no :model-value, no
+// reactive :value binding of any kind. That's on purpose: any reactive
+// binding through Vuetify's v-text-field — even one that only commits on
+// blur/change, not every keystroke — still runs through v-text-field's own
+// internal echo of the field, which re-renders the wrapped on every
+// keystroke regardless of what we bind from outside. For type="date"
+// specifically, that internal re-render resets the browser's own
+// per-segment editing state mid-type (day/month/year), which is what was
+// scrambling what you typed. Using a genuinely uncontrolled native
+// (not wrapped in v-text-field at all) sidesteps that entirely.
+//
+// We grab a direct DOM ref to each native input and manage its .value
+// imperatively instead:
+// - once on mount, to show the initial value
+// - whenever departureDatePart/arrivalDatePart changes from elsewhere
+// (the calendar picker, a flight lookup, auto-fill) — but only if the
+// field isn't currently focused, so we never fight with active typing
+// - the native `change` event (fires once a full date has been entered
+// and committed, or cleared) is what flows the typed value back into
+// departureDatePart/arrivalDatePart
+// This keeps the real native, locale-aware date input and picker UI intact.
+//
+// The arrival native date input only exists in the DOM while arrivalOffset
+// is 'custom' (see template); its value is (re)synced whenever it mounts.
function toIsoDateString(d: Date | null): string {
if (!d) return ''
@@ -331,16 +419,42 @@ function fromIsoDateString(value: string): Date | null {
return new Date(y, m - 1, d)
}
-const departureDateValue = computed({
- get: () => toIsoDateString(departureDatePart.value),
- set: (value: string) => { departureDatePart.value = fromIsoDateString(value) },
+const departureDateEl = ref(null)
+const arrivalDateEl = ref(null)
+
+onMounted(() => {
+ if (departureDateEl.value) departureDateEl.value.value = toIsoDateString(departureDatePart.value)
+ if (arrivalDateEl.value) arrivalDateEl.value.value = toIsoDateString(arrivalDatePart.value)
})
-const arrivalDateValue = computed({
- get: () => toIsoDateString(arrivalDatePart.value),
- set: (value: string) => { arrivalDatePart.value = fromIsoDateString(value) },
+watch(departureDatePart, (d) => {
+ const el = departureDateEl.value
+ if (el && document.activeElement !== el) el.value = toIsoDateString(d)
})
+watch(arrivalDatePart, (d) => {
+ const el = arrivalDateEl.value
+ if (el && document.activeElement !== el) el.value = toIsoDateString(d)
+})
+
+// The arrival native input only exists while arrivalDateMode === 'custom',
+// so re-sync its value whenever it (re)mounts into the DOM.
+watch(arrivalDateMode, async (mode) => {
+ if (mode !== 'custom') return
+ await nextTick()
+ if (arrivalDateEl.value) arrivalDateEl.value.value = toIsoDateString(arrivalDatePart.value)
+})
+
+function commitDepartureDateInput(e: Event) {
+ const value = (e.target as HTMLInputElement).value
+ departureDatePart.value = value ? fromIsoDateString(value) : null
+}
+
+function commitArrivalDateInput(e: Event) {
+ const value = (e.target as HTMLInputElement).value
+ arrivalDatePart.value = value ? fromIsoDateString(value) : null
+}
+
// ── 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
@@ -472,30 +586,33 @@ function commitArrivalTimeInput() {
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
+
+
If you can't find the airline you flew with, just leave it blank but make sure to add a valid flight number. We will be notified of flights with missing airlines and add the airline shortly.
+
If you don't know your flight number or a missing airline hasn't been added within 48 hours, please contact support.