12 Commits
17 changed files with 257 additions and 255 deletions
-11
View File
@@ -19,7 +19,6 @@ class AdminController extends Controller
function staticAdminData(){ function staticAdminData(){
return [ return [
'missingLiveryCount' => $this->adminService->getMissingLiveries()->count(), 'missingLiveryCount' => $this->adminService->getMissingLiveries()->count(),
'missingAirlineCount' => $this->adminService->getMissingAirlines()->count(),
]; ];
} }
@@ -45,16 +44,6 @@ class AdminController extends Controller
]); ]);
} }
function reconcileMissingAirlines(){
$missingAirlines = $this->adminService->getMissingAirlines()->values();
return Inertia::render('Admin/MissingAirlines', [
'title' => $missingAirlines->count() . ' Missing Airlines',
'missingAirlines' => $missingAirlines,
...$this->staticAdminData(),
]);
}
public function ignoreMissingLivery(Request $request) public function ignoreMissingLivery(Request $request)
{ {
$validated = $request->validate([ $validated = $request->validate([
-33
View File
@@ -5,7 +5,6 @@ namespace App\Services;
use App\DTOs\MissingLivery; use App\DTOs\MissingLivery;
use App\Models\IgnoredMissingLivery; use App\Models\IgnoredMissingLivery;
use App\Models\UserFlight; use App\Models\UserFlight;
use Illuminate\Support\Enumerable;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -46,36 +45,4 @@ class AdminService
->sortBy('airline_name') ->sortBy('airline_name')
->values(); ->values();
} }
function getMissingAirlines(): Enumerable|Collection
{
$flights = UserFlight::whereNotNull('flight_number')
->whereNull('airline_id')
->with('user:id,name') // adjust to your actual user relation/columns
->get(['id', 'flight_number', 'user_id']);
return $flights
->map(function ($flight) {
// Extract the 2-character IATA-style prefix (letters and/or digits)
preg_match('/^([A-Za-z0-9]{2})\d+/', trim($flight->flight_number), $matches);
return [
'code' => $matches[1] ?? null,
'flight_number' => $flight->flight_number,
'link' => "/u/{$flight->user->name}/flight/{$flight->id}",
];
})
->filter(fn ($f) => $f['code'] !== null) // drop any that didn't match the pattern
->groupBy('code')
->map(function ($group, $code) {
return [
'code' => $code,
'flights' => $group->map(fn ($f) => [
'flight_number' => $f['flight_number'],
'link' => $f['link'],
])->values()->all(),
];
})
->values();
}
} }
+4
View File
@@ -12,6 +12,10 @@ COPY . .
RUN composer install --no-dev --optimize-autoloader RUN composer install --no-dev --optimize-autoloader
ARG APP_VERSION=Dev
ENV VITE_APP_VERSION=$APP_VERSION
LABEL org.opencontainers.image.version=$APP_VERSION
RUN npm install && npm run build && rm -f public/hot RUN npm install && npm run build && rm -f public/hot
COPY docker/nginx.conf /etc/nginx/nginx.conf COPY docker/nginx.conf /etc/nginx/nginx.conf
+1 -1
View File
@@ -8,7 +8,7 @@
"dev": "vite", "dev": "vite",
"dev:all": "concurrently -s all \"php artisan serve\" \"php artisan reverb:start\" \"php artisan queue:work\" \"npm run dev\" > /dev/null 2>&1", "dev:all": "concurrently -s all \"php artisan serve\" \"php artisan reverb:start\" \"php artisan queue:work\" \"npm run dev\" > /dev/null 2>&1",
"serve:all": "concurrently -s all \"php artisan serve\" \"php artisan reverb:start\" \"php artisan queue:work\" > /dev/null 2>&1", "serve:all": "concurrently -s all \"php artisan serve\" \"php artisan reverb:start\" \"php artisan queue:work\" > /dev/null 2>&1",
"updateVersion": "cross-var docker build -t dredgy/flights-api:$npm_config_tag -t dredgy/flights-api:latest . && cross-var docker push dredgy/flights-api:$npm_config_tag && docker push dredgy/flights-api:latest" "updateVersion": "cross-var docker build --no-cache --build-arg APP_VERSION=$npm_config_tag -t dredgy/flights-api:$npm_config_tag -t dredgy/flights-api:latest . && cross-var docker push dredgy/flights-api:$npm_config_tag && docker push dredgy/flights-api:latest"
}, },
"devDependencies": { "devDependencies": {
"@inertiajs/vue3": "^2.0.0", "@inertiajs/vue3": "^2.0.0",
@@ -1,91 +0,0 @@
<script setup lang="ts">
import { MissingAirline } from "@/Types/types"
defineProps<{
airline: MissingAirline
}>()
</script>
<template>
<div class="missing-airline-card">
<div class="card-header">
<span class="airline-code">{{ airline.code }}</span>
<span class="flight-count">{{ airline.flights.length }} flight{{ airline.flights.length === 1 ? '' : 's' }}</span>
</div>
<ul class="flight-list">
<li v-for="flight in airline.flights" :key="flight.link" class="flight-item">
<a :href="flight.link" class="flight-link" target="_blank" rel="noopener">
{{ flight.flight_number }}
</a>
</li>
</ul>
</div>
</template>
<style scoped>
.missing-airline-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 1rem 1.25rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.card-header {
display: flex;
align-items: baseline;
justify-content: space-between;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
padding-bottom: 0.5rem;
}
.airline-code {
font-family: 'Share Tech Mono', monospace;
font-size: 1.1rem;
letter-spacing: 0.08em;
color: #ffc107;
font-weight: 700;
}
.flight-count {
font-family: 'Share Tech Mono', monospace;
font-size: 0.68rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #778;
}
.flight-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
list-style: none;
padding: 0;
margin: 0;
}
.flight-item {
display: flex;
}
.flight-link {
font-family: 'Share Tech Mono', monospace;
font-size: 0.72rem;
letter-spacing: 0.06em;
color: #9fb3c8;
text-decoration: none;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 4px;
padding: 0.25rem 0.5rem;
transition: color 0.15s ease, border-color 0.15s ease;
}
.flight-link:hover {
color: #ffc107;
border-color: rgba(255, 193, 7, 0.4);
}
</style>
@@ -160,7 +160,7 @@ const showColumn = (column: string) => props.columnsToShow.includes(column)
<td v-if="showColumn('class_seat_combined')" class="v-data-table__td class-badges-cell"> <td v-if="showColumn('class_seat_combined')" class="v-data-table__td class-badges-cell">
<span class="class-cell"> <span class="class-cell">
<CrewTooltip v-if="flight.flight_reason?.name == 'Crew'" :crew-type="flight.crew_type!"> <CrewTooltip v-if="flight.flight_reason?.name == 'Crew' && flight.crew_type" :crew-type="flight.crew_type!">
<FlightClassBadge v-if="flight.flight_class?.internal_name === 'crew'" :flight="flight" /> <FlightClassBadge v-if="flight.flight_class?.internal_name === 'crew'" :flight="flight" />
<InlineBadge v-else variant="crew">Crew</InlineBadge> <InlineBadge v-else variant="crew">Crew</InlineBadge>
</CrewTooltip> </CrewTooltip>
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
withDefaults(defineProps<{ withDefaults(defineProps<{
title: string, title?: string,
wide?: boolean, wide?: boolean,
blurb?: string blurb?: string
}>(), { wide: false }) }>(), { wide: false })
@@ -4,12 +4,15 @@ import { SharedProps } from "@/Types/types"
const page = usePage<SharedProps>().props const page = usePage<SharedProps>().props
const year = new Date().getFullYear() const year = new Date().getFullYear()
const version = __APP_VERSION__
</script> </script>
<template> <template>
<footer class="glass"> <footer class="glass">
<span class="footer-copyright"> <span class="footer-copyright">
&copy; {{ year }} FlightsGoneBy &copy; {{ year }} FlightsGoneBy
<span class="footer-version">v{{ version }}</span>
</span> </span>
<span class="footer-links"> <span class="footer-links">
@@ -73,4 +76,9 @@ footer {
font-size: 0.6rem; font-size: 0.6rem;
} }
} }
.footer-version {
opacity: 0.8;
margin-left: 0.4em;
}
</style> </style>
+216 -18
View File
@@ -7,6 +7,7 @@ import AircraftSearchBox from '@/Components/FlightsGoneBy/AircraftSearchBox.vue'
import AirportSearchBox from '@/Components/FlightsGoneBy/AirportSearchBox.vue' import AirportSearchBox from '@/Components/FlightsGoneBy/AirportSearchBox.vue'
import type {SeatType, FlightReason, FlightClass, CrewType} from '@/Types/types' import type {SeatType, FlightReason, FlightClass, CrewType} from '@/Types/types'
import { ref, watch, computed } from 'vue' import { ref, watch, computed } from 'vue'
import { VTimePicker, VDatePicker } from 'vuetify/components'
defineOptions({ layout: MainLayout }) defineOptions({ layout: MainLayout })
@@ -91,6 +92,20 @@ async function lookupFlight() {
if (!form.aircraft) form.aircraft = data.aircraft_options[0] 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++ lookupKey.value++
} catch (e) { } catch (e) {
lookupError.value = String(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 toOptionsData = ref<{ value: number; title: string; country_code: string }[]>(props.flight?.to_options ?? [])
const aircraftOptionsData = ref<{ value: number; title: string }[]>(props.flight?.aircraft_options ?? []) const aircraftOptionsData = ref<{ value: number; title: string }[]>(props.flight?.aircraft_options ?? [])
// ── Split date/time pickers ───────────────────────────────────────────────────
watch(() => form.departure_date, (newVal) => { // form.departure_date / form.arrival_date stay as the single source of truth
if (!newVal || form.arrival_date) return // (YYYY-MM-DDTHH:mm strings) that the rest of the component and submit() rely
const dep = new Date(newVal) // on. These refs + watchers just give the template two separate pickers while
const arr = new Date(dep.getTime() + 60 * 60 * 1000) // keeping those combined strings in sync — no server-side changes needed.
const pad = (n: number) => String(n).padStart(2, '0') 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())}`
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<Date | null>(depInit.date)
const departureTimePart = ref<string | null>(depInit.time)
const arrivalDatePart = ref<Date | null>(arrInit.date)
const arrivalTimePart = ref<string | null>(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)
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) => { const getArrivalBound = (form: any, offsetDays: number) => {
if (!form.departure_date) return undefined if (!form.departure_date) return undefined
const pad = (n: number) => String(n).padStart(2, '0')
const d = new Date(form.departure_date) const d = new Date(form.departure_date)
d.setDate(d.getDate() + offsetDays) d.setDate(d.getDate() + offsetDays)
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}` 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 arrivalMin = computed(() => getArrivalBound(form, -2))
const arrivalMax = computed(() => getArrivalBound(form, 3)) 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)
</script> </script>
<template> <template>
@@ -249,27 +365,109 @@ const arrivalMax = computed(() => getArrivalBound(form, 3))
</v-col> </v-col>
</v-row> </v-row>
<!-- Departure + Arrival datetime --> <!-- Departure date + time -->
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" sm="6">
<v-menu v-model="depDateMenu" :close-on-content-click="false">
<template #activator="{ props: menuProps }">
<v-text-field <v-text-field
v-model="form.departure_date" v-bind="menuProps"
label="Departure Date & Time" :model-value="formatDateDisplay(departureDatePart)"
type="datetime-local" label="Departure Date"
readonly
clearable
prepend-inner-icon="mdi-calendar"
:disabled="!lookupComplete" :disabled="!lookupComplete"
:error-messages="submitForm.errors.departure_date" :error-messages="submitForm.errors.departure_date"
@click:clear="clearDepartureDate"
/> />
</template>
<v-date-picker
v-model="departureDatePart"
@update:model-value="depDateMenu = false"
/>
</v-menu>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" sm="6">
<v-menu v-model="depTimeMenu" :close-on-content-click="false">
<template #activator="{ props: menuProps }">
<v-text-field <v-text-field
v-model="form.arrival_date" v-bind="menuProps"
label="Arrival Date & Time" :model-value="formatTimeDisplay(departureTimePart)"
type="datetime-local" label="Departure Time"
readonly
clearable
prepend-inner-icon="mdi-clock-outline"
:disabled="!lookupComplete"
@click:clear="clearDepartureTime"
/>
</template>
<v-card>
<v-time-picker
v-model="departureTimePart"
format="ampm"
scrollable
/>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="depTimeMenu = false">OK</v-btn>
</v-card-actions>
</v-card>
</v-menu>
</v-col>
</v-row>
<!-- Arrival date + time -->
<v-row>
<v-col cols="12" sm="6">
<v-menu v-model="arrDateMenu" :close-on-content-click="false">
<template #activator="{ props: menuProps }">
<v-text-field
v-bind="menuProps"
:model-value="formatDateDisplay(arrivalDatePart)"
label="Arrival Date"
readonly
clearable
prepend-inner-icon="mdi-calendar"
:disabled="!lookupComplete" :disabled="!lookupComplete"
:error-messages="submitForm.errors.arrival_date" :error-messages="submitForm.errors.arrival_date"
:min="arrivalMin" @click:clear="clearArrivalDate"
:max="arrivalMax"
/> />
</template>
<v-date-picker
v-model="arrivalDatePart"
:min="arrivalMinDate"
:max="arrivalMaxDate"
@update:model-value="arrDateMenu = false"
/>
</v-menu>
</v-col>
<v-col cols="12" sm="6">
<v-menu v-model="arrTimeMenu" :close-on-content-click="false">
<template #activator="{ props: menuProps }">
<v-text-field
v-bind="menuProps"
:model-value="formatTimeDisplay(arrivalTimePart)"
label="Arrival Time"
readonly
clearable
prepend-inner-icon="mdi-clock-outline"
:disabled="!lookupComplete"
@click:clear="clearArrivalTime"
/>
</template>
<v-card>
<v-time-picker
v-model="arrivalTimePart"
format="ampm"
scrollable
/>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="arrTimeMenu = false">OK</v-btn>
</v-card-actions>
</v-card>
</v-menu>
</v-col> </v-col>
</v-row> </v-row>
+1 -2
View File
@@ -7,7 +7,6 @@ import {Head} from "@inertiajs/vue3";
defineProps<{ defineProps<{
title: string; title: string;
missingLiveryCount: number; missingLiveryCount: number;
missingAirlineCount: number;
}>() }>()
</script> </script>
@@ -16,7 +15,7 @@ defineProps<{
<MainLayout> <MainLayout>
<Head :title="title" /> <Head :title="title" />
<div class="admin-container"> <div class="admin-container">
<AdminSidebar :missingAirlineCount="missingAirlineCount" :missingLiveryCount="missingLiveryCount" /> <AdminSidebar :missingLiveryCount="missingLiveryCount" />
<div class="admin-content"> <div class="admin-content">
<GlassBox class="admin-page" :title="title"> <GlassBox class="admin-page" :title="title">
<slot /> <slot />
@@ -3,7 +3,6 @@ import {Link} from "@inertiajs/vue3";
import {number} from "echarts"; import {number} from "echarts";
defineProps<{ defineProps<{
missingLiveryCount: number; missingLiveryCount: number;
missingAirlineCount: number;
}>() }>()
</script> </script>
@@ -19,11 +18,6 @@ defineProps<{
Reconcile Missing Liveries Reconcile Missing Liveries
<v-chip size="x-small" color="error" class="ml-1">{{ missingLiveryCount }}</v-chip> <v-chip size="x-small" color="error" class="ml-1">{{ missingLiveryCount }}</v-chip>
</Link> </Link>
<Link :href="route('admin.reconcile-missing-airlines')" class="sidebar-link">
<v-icon icon="mdi-airplane-takeoff" size="18" />
Reconcile Missing Airlines
<v-chip size="x-small" color="error" class="ml-1">{{ missingAirlineCount }}</v-chip>
</Link>
</aside> </aside>
</template> </template>
@@ -1,72 +0,0 @@
<script setup lang="ts">
import GlassBox from "@/Components/FlightsGoneBy/GlassBox.vue";
import { Head } from "@inertiajs/vue3";
import { MissingAirline } from "@/Types/types";
import MissingAirlineCard from "@/Components/Admin/MissingAirlineCard.vue";
import AdminLayout from "@/Pages/Admin/AdminLayout.vue";
import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue";
defineOptions({ layout: AdminLayout });
defineProps<{
missingAirlines: MissingAirline[]
}>()
</script>
<template>
<Head title="Missing Airlines" />
<Panel>
<template #header>
<h1 class="panel-title">Missing Airlines</h1>
<span class="panel-subtitle">
{{ missingAirlines.length }} unmatched code{{ missingAirlines.length === 1 ? '' : 's' }}
</span>
</template>
<div v-if="missingAirlines.length === 0" class="empty-state">
No missing airlines 🎉
</div>
<div v-else class="missing-airlines-grid">
<MissingAirlineCard
v-for="airline in missingAirlines"
:key="airline.code"
:airline="airline"
/>
</div>
</Panel>
</template>
<style scoped>
.panel-title {
font-family: 'Share Tech Mono', monospace;
font-size: 1.25rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #e6e9ef;
}
.panel-subtitle {
font-family: 'Share Tech Mono', monospace;
font-size: 0.7rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #778;
}
.missing-airlines-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.empty-state {
font-family: 'Share Tech Mono', monospace;
font-size: 0.85rem;
color: #778;
padding: 2rem 0;
text-align: center;
}
</style>
+1 -9
View File
@@ -303,7 +303,7 @@ export interface Flight {
airline: Airline | null airline: Airline | null
aircraft: Aircraft | null aircraft: Aircraft | null
seat_type: SeatType | null seat_type: SeatType | null
crew_type: CrewType | null crew_type?: CrewType | null
flight_reason: FlightReason | null flight_reason: FlightReason | null
flight_class: FlightClass | null flight_class: FlightClass | null
duration_display: string duration_display: string
@@ -326,14 +326,6 @@ export interface MissingLivery {
clipboard_text: string; clipboard_text: string;
} }
export interface MissingAirline{
code: string;
flights: {
flight_number: string;
link: string;
}[]
}
declare module '@inertiajs/vue3' { declare module '@inertiajs/vue3' {
interface PageProps extends SharedProps {} interface PageProps extends SharedProps {}
} }
+1
View File
@@ -2,3 +2,4 @@
/// <reference types="vite-plugin-pwa/client" /> /// <reference types="vite-plugin-pwa/client" />
declare module '*.css' declare module '*.css'
declare const __APP_VERSION__: string
+1
View File
@@ -9,6 +9,7 @@
<link rel="icon" type="image/x-icon" href="{{ asset('img/favicons/favicon.png') }}"> <link rel="icon" type="image/x-icon" href="{{ asset('img/favicons/favicon.png') }}">
<!-- Fonts --> <!-- Fonts -->
<meta name="description" content="FlightsGoneBy is the ultimate flight diary — log every flight, earn achievements, visualise your travels on an interactive map, and share your stats with friends.">
<link rel="preconnect" href="https://fonts.bunny.net"> <link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" /> <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Apple Touch Icons --> <!-- Apple Touch Icons -->
-2
View File
@@ -40,8 +40,6 @@ use Inertia\Inertia;
Route::get('/', [AdminController::class, 'dashboard'])->name('dashboard'); Route::get('/', [AdminController::class, 'dashboard'])->name('dashboard');
Route::get('/missing-liveries', [AdminController::class, 'reconcileMissingLiveries'])->name('reconcile-missing-liveries')->middleware('permission:reconcile_missing_liveries'); Route::get('/missing-liveries', [AdminController::class, 'reconcileMissingLiveries'])->name('reconcile-missing-liveries')->middleware('permission:reconcile_missing_liveries');
Route::post('/ignore-missing-livery', [AdminController::class, 'ignoreMissingLivery'])->name('ignore-missing-livery'); Route::post('/ignore-missing-livery', [AdminController::class, 'ignoreMissingLivery'])->name('ignore-missing-livery');
Route::get('/missing-airlines', [AdminController::class, 'reconcileMissingAirlines'])->name('reconcile-missing-airlines')->middleware('permission:reconcile_missing_liveries');
}); });
+14
View File
@@ -4,8 +4,22 @@ import vue from '@vitejs/plugin-vue';
import path from 'path'; import path from 'path';
import vuetify from "vite-plugin-vuetify"; import vuetify from "vite-plugin-vuetify";
import { VitePWA } from 'vite-plugin-pwa'; import { VitePWA } from 'vite-plugin-pwa';
import { execSync } from 'child_process'
function getAppVersion(): string {
try {
// Prefer a tag if one points at HEAD, else fall back to short hash
return execSync('git describe --tags --always --dirty').toString().trim()
} catch {
return 'Unknown'
}
}
export default defineConfig({ export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify(process.env.VITE_APP_VERSION ?? 'Dev'),
},
plugins: [ plugins: [
laravel({ laravel({
input: 'resources/js/app.ts', input: 'resources/js/app.ts',