Compare commits
11
Commits
fd3b5b0405
..
1.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90630489df | ||
|
|
2d55aa8488 | ||
|
|
1f23cbc033 | ||
|
|
e9ad1b45e0 | ||
|
|
64e164887b | ||
|
|
4b8fb1119b | ||
|
|
0230c77a2a | ||
|
|
f0306a6dd8 | ||
|
|
35c439dba4 | ||
|
|
3a082a57ff | ||
|
|
c016a4aaaf |
@@ -19,7 +19,6 @@ class AdminController extends Controller
|
||||
function staticAdminData(){
|
||||
return [
|
||||
'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)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Services;
|
||||
use App\DTOs\MissingLivery;
|
||||
use App\Models\IgnoredMissingLivery;
|
||||
use App\Models\UserFlight;
|
||||
use Illuminate\Support\Enumerable;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
@@ -46,36 +45,4 @@ class AdminService
|
||||
->sortBy('airline_name')
|
||||
->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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ COPY . .
|
||||
|
||||
RUN composer install --no-dev --optimize-autoloader
|
||||
|
||||
ARG APP_VERSION=Dev
|
||||
ENV VITE_APP_VERSION=$APP_VE
|
||||
LABEL org.opencontainers.image.version=$APP_VERSION
|
||||
|
||||
RUN npm install && npm run build && rm -f public/hot
|
||||
|
||||
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
"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",
|
||||
"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 --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": {
|
||||
"@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">
|
||||
<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" />
|
||||
<InlineBadge v-else variant="crew">Crew</InlineBadge>
|
||||
</CrewTooltip>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{
|
||||
title: string,
|
||||
title?: string,
|
||||
wide?: boolean,
|
||||
blurb?: string
|
||||
}>(), { wide: false })
|
||||
|
||||
@@ -4,12 +4,14 @@ import { SharedProps } from "@/Types/types"
|
||||
|
||||
const page = usePage<SharedProps>().props
|
||||
const year = new Date().getFullYear()
|
||||
const version = __APP_VERSION__
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="glass">
|
||||
<span class="footer-copyright">
|
||||
© {{ year }} FlightsGoneBy
|
||||
<span class="footer-version">v{{ version }}</span>
|
||||
</span>
|
||||
|
||||
<span class="footer-links">
|
||||
@@ -73,4 +75,9 @@ footer {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-version {
|
||||
opacity: 0.8;
|
||||
margin-left: 0.4em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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 ?? [])
|
||||
|
||||
|
||||
watch(() => form.departure_date, (newVal) => {
|
||||
if (!newVal || form.arrival_date) return
|
||||
const dep = new Date(newVal)
|
||||
const arr = new Date(dep.getTime() + 60 * 60 * 1000)
|
||||
// ── 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.
|
||||
|
||||
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) => {
|
||||
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)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -249,27 +365,109 @@ const arrivalMax = computed(() => getArrivalBound(form, 3))
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- ── Departure + Arrival datetime ──────────────────────── -->
|
||||
<!-- ── Departure date + time ──────────────────────────────── -->
|
||||
<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-model="form.departure_date"
|
||||
label="Departure Date & Time"
|
||||
type="datetime-local"
|
||||
v-bind="menuProps"
|
||||
:model-value="formatDateDisplay(departureDatePart)"
|
||||
label="Departure Date"
|
||||
readonly
|
||||
clearable
|
||||
prepend-inner-icon="mdi-calendar"
|
||||
:disabled="!lookupComplete"
|
||||
: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 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-model="form.arrival_date"
|
||||
label="Arrival Date & Time"
|
||||
type="datetime-local"
|
||||
v-bind="menuProps"
|
||||
:model-value="formatTimeDisplay(departureTimePart)"
|
||||
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"
|
||||
:error-messages="submitForm.errors.arrival_date"
|
||||
:min="arrivalMin"
|
||||
:max="arrivalMax"
|
||||
@click:clear="clearArrivalDate"
|
||||
/>
|
||||
</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-row>
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import {Head} from "@inertiajs/vue3";
|
||||
defineProps<{
|
||||
title: string;
|
||||
missingLiveryCount: number;
|
||||
missingAirlineCount: number;
|
||||
|
||||
}>()
|
||||
</script>
|
||||
@@ -16,7 +15,7 @@ defineProps<{
|
||||
<MainLayout>
|
||||
<Head :title="title" />
|
||||
<div class="admin-container">
|
||||
<AdminSidebar :missingAirlineCount="missingAirlineCount" :missingLiveryCount="missingLiveryCount" />
|
||||
<AdminSidebar :missingLiveryCount="missingLiveryCount" />
|
||||
<div class="admin-content">
|
||||
<GlassBox class="admin-page" :title="title">
|
||||
<slot />
|
||||
|
||||
@@ -3,7 +3,6 @@ import {Link} from "@inertiajs/vue3";
|
||||
import {number} from "echarts";
|
||||
defineProps<{
|
||||
missingLiveryCount: number;
|
||||
missingAirlineCount: number;
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -19,11 +18,6 @@ defineProps<{
|
||||
Reconcile Missing Liveries
|
||||
<v-chip size="x-small" color="error" class="ml-1">{{ missingLiveryCount }}</v-chip>
|
||||
</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>
|
||||
</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>
|
||||
Vendored
+1
-9
@@ -303,7 +303,7 @@ export interface Flight {
|
||||
airline: Airline | null
|
||||
aircraft: Aircraft | null
|
||||
seat_type: SeatType | null
|
||||
crew_type: CrewType | null
|
||||
crew_type?: CrewType | null
|
||||
flight_reason: FlightReason | null
|
||||
flight_class: FlightClass | null
|
||||
duration_display: string
|
||||
@@ -326,14 +326,6 @@ export interface MissingLivery {
|
||||
clipboard_text: string;
|
||||
}
|
||||
|
||||
export interface MissingAirline{
|
||||
code: string;
|
||||
flights: {
|
||||
flight_number: string;
|
||||
link: string;
|
||||
}[]
|
||||
}
|
||||
|
||||
declare module '@inertiajs/vue3' {
|
||||
interface PageProps extends SharedProps {}
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -2,3 +2,4 @@
|
||||
/// <reference types="vite-plugin-pwa/client" />
|
||||
|
||||
declare module '*.css'
|
||||
declare const __APP_VERSION__: string
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<link rel="icon" type="image/x-icon" href="{{ asset('img/favicons/favicon.png') }}">
|
||||
<!-- 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 href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
<!-- Apple Touch Icons -->
|
||||
|
||||
@@ -40,8 +40,6 @@ use Inertia\Inertia;
|
||||
Route::get('/', [AdminController::class, 'dashboard'])->name('dashboard');
|
||||
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::get('/missing-airlines', [AdminController::class, 'reconcileMissingAirlines'])->name('reconcile-missing-airlines')->middleware('permission:reconcile_missing_liveries');
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,22 @@ import vue from '@vitejs/plugin-vue';
|
||||
import path from 'path';
|
||||
import vuetify from "vite-plugin-vuetify";
|
||||
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({
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(process.env.VITE_APP_VERSION ?? 'Dev'),
|
||||
},
|
||||
plugins: [
|
||||
laravel({
|
||||
input: 'resources/js/app.ts',
|
||||
|
||||
Reference in New Issue
Block a user