Files
FlightsAPI/resources/js/Components/FlightsGoneBy/ProfileMap.vue
T
2026-04-11 20:49:01 +10:00

269 lines
11 KiB
Vue

<template>
<FlightMap :page="page.props" :flights="mappedFlights" class="profile-map__map" />
<div class="profile-map__toolbar">
<v-select
v-model="selectedYears"
:items="availableYears"
label="Year"
multiple
clearable
hide-details
density="compact"
variant="outlined"
>
<template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text">
{{ item }}<span v-if="index < Math.min(selectedYears.length, 2) - 1">,&nbsp;</span>
</span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedYears.length - 2 }}</span>
</template>
</v-select>
<v-select
v-model="selectedAirlines"
:items="availableAirlines"
item-title="name"
item-value="id"
label="Airline"
multiple
clearable
hide-details
density="compact"
variant="outlined"
>
<template #item="{ item, props: itemProps }">
<v-list-item v-bind="itemProps">
<template #prepend="{ isSelected }">
<v-checkbox-btn :model-value="isSelected" tabindex="-1" />
</template>
<template #title>
<img
:src="airlineLogoUrl((item as any).id)"
width="32"
height="32"
style="object-fit: contain; margin-right: 8px; vertical-align: middle;"
alt=""
/>
{{ (item as any).name }}
</template>
</v-list-item>
</template>
<template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).name }}<span v-if="index < Math.min(selectedAirlines.length, 2) - 1">,&nbsp;</span>
</span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedAirlines.length - 2 }}</span>
</template>
</v-select>
<v-select
v-model="selectedCountries"
:items="availableCountries"
item-title="name"
item-value="code"
label="Country"
multiple
clearable
hide-details
density="compact"
variant="outlined"
>
<template #item="{ item, props: itemProps }">
<v-list-item v-bind="itemProps">
<template #prepend="{ isSelected }">
<v-checkbox-btn :model-value="isSelected" tabindex="-1" />
</template>
<template #title>
<span :class="countryFlagClass((item as any).code)" style="margin-right: 8px; font-size: 1.1em;" />
{{ (item as any).name }}
</template>
</v-list-item>
</template>
<template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text">
<span :class="countryFlagClass((item as any).code)" style="margin-right: 4px;" />
{{ (item as any).name }}<span v-if="index < Math.min(selectedCountries.length, 2) - 1">,&nbsp;</span>
</span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedCountries.length - 2 }}</span>
</template>
</v-select>
<v-select
v-model="selectedContinents"
:items="availableContinents"
item-title="name"
item-value="code"
label="Continent"
multiple
clearable
hide-details
density="compact"
variant="outlined"
>
<template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).name }}<span v-if="index < Math.min(selectedContinents.length, 2) - 1">,&nbsp;</span>
</span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedContinents.length - 2 }}</span>
</template>
</v-select>
<v-select
v-model="selectedFlightClasses"
:items="availableFlightClasses"
item-title="name"
item-value="id"
label="Flight class"
multiple
clearable
hide-details
density="compact"
variant="outlined"
>
<template #selection="{ item, index }">
<span v-if="index < 2" class="v-select__selection-text">
{{ (item as any).name }}<span v-if="index < Math.min(selectedFlightClasses.length, 2) - 1">,&nbsp;</span>
</span>
<span v-if="index === 2" class="text-caption text-medium-emphasis">+{{ selectedFlightClasses.length - 2 }}</span>
</template>
</v-select>
</div>
<FlightStatsBar :flights="pastFlights" :upcoming-flights="upcomingFlights" />
<FlightCharts :flights="pastFlights" :upcoming-flights="upcomingFlights" />
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { usePage } from '@inertiajs/vue3'
import FlightMap from '@/Components/FlightsGoneBy/FlightMap.vue'
import type { Flight, SharedProps } from '@/Types/types'
import FlightStatsBar from '@/Components/FlightsGoneBy/FlightStatsBar.vue'
import FlightCharts from '@/Components/FlightsGoneBy/FlightCharts.vue'
const props = defineProps<{
flights: Flight[]
}>()
const page = usePage<SharedProps>()
const now = new Date()
const selectedYears = ref<number[]>([])
const selectedAirlines = ref<number[]>([])
const selectedCountries = ref<string[]>([])
const selectedContinents = ref<string[]>([])
const selectedFlightClasses = ref<number[]>([])
// ── Helpers ───────────────────────────────────────────────────────────────────
const airlineLogoUrl = (id: number) =>
`${page.props.logo_api_url}/airlines/logos/tail/id/${id}`
const countryFlagClass = (code: string) =>
`fi fi-${code.toLowerCase()}`
// ── Available filter options ──────────────────────────────────────────────────
const availableYears = computed(() => {
const years = new Set<number>()
props.flights.forEach(f => years.add(new Date(f.departure_date).getFullYear()))
return [...years].sort((a, b) => b - a)
})
const availableAirlines = computed((): { id: number; name: string }[] => {
const map = new Map<number, { id: number; name: string }>()
props.flights.forEach(f => {
if (f.airline?.id && f.airline?.name) {
map.set(f.airline.id, { id: f.airline.id, name: f.airline.name })
}
})
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name))
})
const availableCountries = computed((): { code: string; name: string }[] => {
const map = new Map<string, { code: string; name: string }>()
props.flights.forEach(f => {
const dep = f.departure_airport.region?.country
const arr = f.arrival_airport.region?.country
if (dep) map.set(dep.code, { code: dep.code, name: dep.name })
if (arr) map.set(arr.code, { code: arr.code, name: arr.name })
})
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name))
})
const availableContinents = computed((): { code: string; name: string }[] => {
const map = new Map<string, { code: string; name: string }>()
props.flights.forEach(f => {
const dep = f.departure_airport.region?.continent
const arr = f.arrival_airport.region?.continent
if (dep) map.set(dep.code, { code: dep.code, name: dep.name })
if (arr) map.set(arr.code, { code: arr.code, name: arr.name })
})
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name))
})
const availableFlightClasses = computed((): { id: number; name: string }[] => {
const map = new Map<number, { id: number; name: string }>()
props.flights.forEach(f => {
if (f.flight_class?.id && f.flight_class?.name) {
map.set(f.flight_class.id, { id: f.flight_class.id, name: f.flight_class.name })
}
})
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name))
})
// ── Filter helper ─────────────────────────────────────────────────────────────
function matchesFilters(f: Flight, date: Date): boolean {
if (selectedYears.value.length && !selectedYears.value.includes(date.getFullYear())) return false
if (selectedAirlines.value.length && !selectedAirlines.value.includes(f.airline?.id ?? -1)) return false
if (selectedCountries.value.length) {
const depCode = f.departure_airport.region?.country?.code
const arrCode = f.arrival_airport.region?.country?.code
if (!selectedCountries.value.includes(depCode ?? '') && !selectedCountries.value.includes(arrCode ?? '')) return false
}
if (selectedContinents.value.length) {
const depCode = f.departure_airport.region?.continent?.code
const arrCode = f.arrival_airport.region?.continent?.code
if (!selectedContinents.value.includes(depCode ?? '') && !selectedContinents.value.includes(arrCode ?? '')) return false
}
if (selectedFlightClasses.value.length && !selectedFlightClasses.value.includes(f.flight_class?.id ?? -1)) return false
return true
}
// ── Filtered flights ──────────────────────────────────────────────────────────
const pastFlights = computed(() =>
props.flights.filter(f => {
const date = new Date(f.departure_date)
return date <= now && matchesFilters(f, date)
})
)
const upcomingFlights = computed(() =>
props.flights.filter(f => {
const date = new Date(f.departure_date)
return date > now && matchesFilters(f, date)
})
)
const mappedFlights = computed(() => [
...pastFlights.value,
...upcomingFlights.value,
])
</script>
<style scoped>
.profile-map__toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 12px;
}
.profile-map__toolbar .v-select {
flex: 1 1 160px;
}
</style>