From 5deefcbfb3d618eafcc528082c09d058a1e04aa6 Mon Sep 17 00:00:00 2001 From: josh Date: Mon, 20 Apr 2026 13:36:58 +1000 Subject: [PATCH] Add more airlines and fix edit bugs --- .../FlightsGoneBy/BoardingPasses.vue | 12 +- .../Charts/ChartTypes/DonutChart.vue | 102 ++++++ .../ScrollingHorizontalBarChart.vue | 172 +++++++++ .../Charts/ChartTypes/VerticalBarChart.vue | 103 ++++++ .../FlightsGoneBy/Charts/ContinentsChart.vue | 129 +------ .../FlightsGoneBy/Charts/CountriesChart.vue | 271 +++----------- .../FlightsGoneBy/Charts/FlightClassChart.vue | 93 +---- .../Charts/FlightReasonsChart.vue | 114 +----- .../FlightsGoneBy/Charts/FlightTypeChart.vue | 97 +---- .../Charts/FlightsPerDayChart.vue | 103 +----- .../Charts/FlightsPerMonthChart.vue | 103 +----- .../Charts/FlightsPerYearChart.vue | 112 +----- .../FlightsGoneBy/Charts/SeatTypeChart.vue | 94 +---- .../FlightsGoneBy/Charts/TopAirlinesChart.vue | 237 +++---------- .../FlightsGoneBy/Charts/TopAirportsChart.vue | 274 +++------------ .../FlightsGoneBy/DepartureBoard.vue | 17 +- .../Components/FlightsGoneBy/FlightCharts.vue | 88 ++--- .../{FlightFillter.vue => FlightFilter.vue} | 0 .../js/Components/FlightsGoneBy/FlightMap.vue | 3 +- .../FlightsGoneBy/FlightMapAndCharts.vue | 104 ++---- resources/js/Composables/useFlightStats.ts | 332 ++++++++++++++++++ resources/js/Pages/UserProfile.vue | 79 ++++- 22 files changed, 1109 insertions(+), 1530 deletions(-) create mode 100644 resources/js/Components/FlightsGoneBy/Charts/ChartTypes/DonutChart.vue create mode 100644 resources/js/Components/FlightsGoneBy/Charts/ChartTypes/ScrollingHorizontalBarChart.vue create mode 100644 resources/js/Components/FlightsGoneBy/Charts/ChartTypes/VerticalBarChart.vue rename resources/js/Components/FlightsGoneBy/{FlightFillter.vue => FlightFilter.vue} (100%) create mode 100644 resources/js/Composables/useFlightStats.ts diff --git a/resources/js/Components/FlightsGoneBy/BoardingPasses.vue b/resources/js/Components/FlightsGoneBy/BoardingPasses.vue index 3d83ce4..2c9835d 100644 --- a/resources/js/Components/FlightsGoneBy/BoardingPasses.vue +++ b/resources/js/Components/FlightsGoneBy/BoardingPasses.vue @@ -2,24 +2,20 @@ import { computed } from "vue"; import { Flight } from "@/Types/types"; import BoardingPass from "@/Components/FlightsGoneBy/BoardingPass.vue"; +import { FlightStats } from "@/Composables/useFlightStats"; const props = defineProps<{ - flights: Flight[] + flightStats: FlightStats canEdit: boolean }>() -const today = new Date() -today.setHours(0, 0, 0, 0) - const upcomingFlights = computed(() => - props.flights - .filter(f => new Date(f.departure_date) >= today) + [...props.flightStats.upcomingFlights.value] .sort((a, b) => new Date(a.departure_date).getTime() - new Date(b.departure_date).getTime()) ) const departedFlights = computed(() => - props.flights - .filter(f => new Date(f.departure_date) < today) + [...props.flightStats.pastFlights.value] .sort((a, b) => new Date(b.departure_date).getTime() - new Date(a.departure_date).getTime()) ) diff --git a/resources/js/Components/FlightsGoneBy/Charts/ChartTypes/DonutChart.vue b/resources/js/Components/FlightsGoneBy/Charts/ChartTypes/DonutChart.vue new file mode 100644 index 0000000..94727f1 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Charts/ChartTypes/DonutChart.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/resources/js/Components/FlightsGoneBy/Charts/ChartTypes/ScrollingHorizontalBarChart.vue b/resources/js/Components/FlightsGoneBy/Charts/ChartTypes/ScrollingHorizontalBarChart.vue new file mode 100644 index 0000000..b5d7361 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Charts/ChartTypes/ScrollingHorizontalBarChart.vue @@ -0,0 +1,172 @@ + + + + + diff --git a/resources/js/Components/FlightsGoneBy/Charts/ChartTypes/VerticalBarChart.vue b/resources/js/Components/FlightsGoneBy/Charts/ChartTypes/VerticalBarChart.vue new file mode 100644 index 0000000..9ca61ef --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Charts/ChartTypes/VerticalBarChart.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/resources/js/Components/FlightsGoneBy/Charts/ContinentsChart.vue b/resources/js/Components/FlightsGoneBy/Charts/ContinentsChart.vue index 8c63cbf..9047660 100644 --- a/resources/js/Components/FlightsGoneBy/Charts/ContinentsChart.vue +++ b/resources/js/Components/FlightsGoneBy/Charts/ContinentsChart.vue @@ -1,125 +1,18 @@ - - - + diff --git a/resources/js/Components/FlightsGoneBy/Charts/CountriesChart.vue b/resources/js/Components/FlightsGoneBy/Charts/CountriesChart.vue index 467d80d..66b6958 100644 --- a/resources/js/Components/FlightsGoneBy/Charts/CountriesChart.vue +++ b/resources/js/Components/FlightsGoneBy/Charts/CountriesChart.vue @@ -1,225 +1,62 @@ + + - - - - diff --git a/resources/js/Components/FlightsGoneBy/Charts/FlightClassChart.vue b/resources/js/Components/FlightsGoneBy/Charts/FlightClassChart.vue index b26ff50..2e4a4ae 100644 --- a/resources/js/Components/FlightsGoneBy/Charts/FlightClassChart.vue +++ b/resources/js/Components/FlightsGoneBy/Charts/FlightClassChart.vue @@ -1,89 +1,18 @@ - - - + diff --git a/resources/js/Components/FlightsGoneBy/Charts/FlightReasonsChart.vue b/resources/js/Components/FlightsGoneBy/Charts/FlightReasonsChart.vue index 0a749d5..6242889 100644 --- a/resources/js/Components/FlightsGoneBy/Charts/FlightReasonsChart.vue +++ b/resources/js/Components/FlightsGoneBy/Charts/FlightReasonsChart.vue @@ -1,110 +1,18 @@ - - - + diff --git a/resources/js/Components/FlightsGoneBy/Charts/FlightTypeChart.vue b/resources/js/Components/FlightsGoneBy/Charts/FlightTypeChart.vue index 214e093..92c885c 100644 --- a/resources/js/Components/FlightsGoneBy/Charts/FlightTypeChart.vue +++ b/resources/js/Components/FlightsGoneBy/Charts/FlightTypeChart.vue @@ -1,91 +1,18 @@ - - - + diff --git a/resources/js/Components/FlightsGoneBy/Charts/FlightsPerDayChart.vue b/resources/js/Components/FlightsGoneBy/Charts/FlightsPerDayChart.vue index 9d7fd2b..3589a55 100644 --- a/resources/js/Components/FlightsGoneBy/Charts/FlightsPerDayChart.vue +++ b/resources/js/Components/FlightsGoneBy/Charts/FlightsPerDayChart.vue @@ -1,99 +1,22 @@ - - + diff --git a/resources/js/Components/FlightsGoneBy/Charts/FlightsPerMonthChart.vue b/resources/js/Components/FlightsGoneBy/Charts/FlightsPerMonthChart.vue index 0589762..dc1775a 100644 --- a/resources/js/Components/FlightsGoneBy/Charts/FlightsPerMonthChart.vue +++ b/resources/js/Components/FlightsGoneBy/Charts/FlightsPerMonthChart.vue @@ -1,95 +1,22 @@ + - - diff --git a/resources/js/Components/FlightsGoneBy/Charts/FlightsPerYearChart.vue b/resources/js/Components/FlightsGoneBy/Charts/FlightsPerYearChart.vue index 2239732..3e6e990 100644 --- a/resources/js/Components/FlightsGoneBy/Charts/FlightsPerYearChart.vue +++ b/resources/js/Components/FlightsGoneBy/Charts/FlightsPerYearChart.vue @@ -1,109 +1,21 @@ - - + diff --git a/resources/js/Components/FlightsGoneBy/Charts/TopAirlinesChart.vue b/resources/js/Components/FlightsGoneBy/Charts/TopAirlinesChart.vue index a99b55f..016bd7a 100644 --- a/resources/js/Components/FlightsGoneBy/Charts/TopAirlinesChart.vue +++ b/resources/js/Components/FlightsGoneBy/Charts/TopAirlinesChart.vue @@ -1,48 +1,11 @@ - - - + diff --git a/resources/js/Components/FlightsGoneBy/Charts/TopAirportsChart.vue b/resources/js/Components/FlightsGoneBy/Charts/TopAirportsChart.vue index f7f402b..dc6cad8 100644 --- a/resources/js/Components/FlightsGoneBy/Charts/TopAirportsChart.vue +++ b/resources/js/Components/FlightsGoneBy/Charts/TopAirportsChart.vue @@ -1,17 +1,48 @@ + + - - + diff --git a/resources/js/Composables/useFlightStats.ts b/resources/js/Composables/useFlightStats.ts new file mode 100644 index 0000000..af2b346 --- /dev/null +++ b/resources/js/Composables/useFlightStats.ts @@ -0,0 +1,332 @@ +// composables/useFlightStats.ts +import {computed, ComputedRef, Ref} from 'vue' +import type {Airport, Flight} from '@/Types/types' + +const flightYear = (f: Flight): number => + Number(new Intl.DateTimeFormat('en', { + timeZone: f.departure_airport.timezone, + year: 'numeric', + }).format(new Date(f.departure_date))) + +const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] +const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] + + +const dayIndex = (f: Flight): number => { + const localDay = new Intl.DateTimeFormat('en', { + timeZone: f.departure_airport.timezone, + weekday: 'long', + }).format(new Date(f.departure_date)) + const idx = DAYS.indexOf(localDay) + return idx === -1 ? 0 : idx +} + +export function getFlightsPerDay(flights: Flight[], upcomingFlights: Flight[]) { + const countByDay = (list: Flight[]) => + DAYS.map((_, i) => list.filter(f => dayIndex(f) === i).length) + + return { + days: DAYS, + series: [ + { name: 'Flown', data: countByDay(flights) }, + { name: 'Upcoming', data: countByDay(upcomingFlights) }, + ], + } +} + +const monthIndex = (f: Flight): number => + Number(new Intl.DateTimeFormat('en', { + timeZone: f.departure_airport.timezone, + month: 'numeric', + }).format(new Date(f.departure_date))) - 1 + +export function getFlightsPerMonth(flights: Flight[], upcomingFlights: Flight[]) { + const countByMonth = (list: Flight[]) => + MONTHS.map((_, i) => list.filter(f => monthIndex(f) === i).length) + + return { + months: MONTHS, + series: [ + { name: 'Flown', data: countByMonth(flights) }, + { name: 'Upcoming', data: countByMonth(upcomingFlights) }, + ], + } +} + +export function getFlightsPerYear(flights: Flight[], upcomingFlights: Flight[]) { + const allFlights = [...flights, ...upcomingFlights] + + const allYears = new Set() + allFlights.forEach(f => allYears.add(flightYear(f))) + const sorted = [...allYears].sort((a, b) => a - b) + + let min = sorted[0] + let max = sorted[sorted.length - 1] + while (max - min + 1 < 5) { + min-- + if (max - min + 1 < 5) max++ + } + + const years = Array.from({ length: max - min + 1 }, (_, i) => min + i) + const countByYear = (list: Flight[]) => + years.map(year => list.filter(f => flightYear(f) === year).length) + + return { + years, + series: [ + { name: 'Flown', data: countByYear(flights) }, + { name: 'Upcoming', data: countByYear(upcomingFlights) }, + ], + } +} + +function groupByName(flights: Flight[], accessor: (f: Flight) => string) { + const counts = new Map() + flights.forEach(f => { + const key = accessor(f) ?? 'Unknown' + counts.set(key, (counts.get(key) ?? 0) + 1) + }) + const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]) + return { + labels: sorted.map(([name]) => name), + series: sorted.map(([, count]) => count), + } +} + +export function getFlightReasons(flights: Flight[]) { + return groupByName(flights, f => f.flight_reason?.name ?? 'Unknown') +} + +export function getFlightClasses(flights: Flight[]) { + return groupByName(flights, f => f.flight_class?.name ?? 'Unknown') +} + +export function getSeatTypes(flights: Flight[]) { + return groupByName(flights, f => f.seat_type?.name ?? 'Unknown') +} + +function countCountries(flights: Flight[]) { + const counts = new Map() + flights.forEach(f => { + const dep = f.departure_airport?.region?.country + const arr = f.arrival_airport?.region?.country + + if (dep?.name) { + const existing = counts.get(dep.name) ?? { count: 0, code: dep.code } + existing.count++ + counts.set(dep.name, existing) + } + if (arr?.name && arr.name !== dep?.name) { + const existing = counts.get(arr.name) ?? { count: 0, code: arr.code } + existing.count++ + counts.set(arr.name, existing) + } + }) + return counts +} + +export function getCountries(flights: Flight[], upcomingFlights: Flight[]) { + const past = countCountries(flights) + const upcoming = countCountries(upcomingFlights) + const allCountries = new Set([...past.keys(), ...upcoming.keys()]) + + const sorted = [...allCountries] + .map(name => ({ + name, + code: (past.get(name) ?? upcoming.get(name))!.code, + past: past.get(name)?.count ?? 0, + upcoming: upcoming.get(name)?.count ?? 0, + })) + .sort((a, b) => (b.past + b.upcoming) - (a.past + a.upcoming)) + + return { + countries: sorted, + series: [ + { name: 'Flights', data: sorted.map(s => s.past) }, + { name: 'Upcoming', data: sorted.map(s => s.upcoming) }, + ], + } +} + +export function getContinents(flights: Flight[]) { + const counts = new Map() + flights.forEach(f => { + const continents = new Set() + const dep = f.departure_airport.region?.continent?.name + const arr = f.arrival_airport.region?.continent?.name + if (dep) continents.add(dep) + if (arr) continents.add(arr) + continents.forEach(c => counts.set(c, (counts.get(c) ?? 0) + 1)) + }) + const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]) + return { + labels: sorted.map(([name]) => name), + series: sorted.map(([, count]) => count), + } +} + +function countAirlines(flights: Flight[]) { + const counts = new Map() + flights.forEach(f => { + const airline = f.airline + if (!airline?.name) return + const existing = counts.get(airline.name) ?? { count: 0, id: airline.id } + existing.count++ + counts.set(airline.name, existing) + }) + return counts +} + +export function getTopAirlines(flights: Flight[], upcomingFlights: Flight[]) { + const past = countAirlines(flights) + const upcoming = countAirlines(upcomingFlights) + const allAirlines = new Set([...past.keys(), ...upcoming.keys()]) + + const sorted = [...allAirlines] + .map(name => ({ + name, + id: (past.get(name) ?? upcoming.get(name))!.id, + past: past.get(name)?.count ?? 0, + upcoming: upcoming.get(name)?.count ?? 0, + })) + .sort((a, b) => (b.past + b.upcoming) - (a.past + a.upcoming)) + + return { + airlines: sorted, + series: [ + { name: 'Flights', data: sorted.map(s => s.past) }, + { name: 'Upcoming', data: sorted.map(s => s.upcoming) }, + ], + } +} + +interface AirportItem { + label: string + fullName: string + departures: number + arrivals: number + upcoming: number +} + +function airportLabel(airport: Airport | null | undefined): string | null { + if (!airport) return null + return airport.iata_code ?? airport.icao_code ?? airport.name ?? null +} + +export function getTopAirports(flights: Flight[], upcomingFlights: Flight[]) { + const map = new Map() + const empty = (): AirportItem => ({ departures: 0, arrivals: 0, upcoming: 0, label: '', fullName: '' }) + + flights.forEach(f => { + const depLabel = airportLabel(f.departure_airport) + const arrLabel = airportLabel(f.arrival_airport) + + if (depLabel) { + const e = map.get(depLabel) ?? empty() + e.departures++ + e.label = depLabel + e.fullName = f.departure_airport?.name ?? depLabel + map.set(depLabel, e) + } + if (arrLabel) { + const e = map.get(arrLabel) ?? empty() + e.arrivals++ + e.label = arrLabel + e.fullName = f.arrival_airport?.name ?? arrLabel + map.set(arrLabel, e) + } + }) + + upcomingFlights.forEach(f => { + const depLabel = airportLabel(f.departure_airport) + const arrLabel = airportLabel(f.arrival_airport) + + if (depLabel) { + const e = map.get(depLabel) ?? empty() + e.upcoming++ + e.label = depLabel + e.fullName = f.departure_airport?.name ?? depLabel + map.set(depLabel, e) + } + if (arrLabel) { + const e = map.get(arrLabel) ?? empty() + e.upcoming++ + e.label = arrLabel + e.fullName = f.arrival_airport?.name ?? arrLabel + map.set(arrLabel, e) + } + }) + + const sorted = [...map.values()] + .sort((a, b) => + (b.departures + b.arrivals + b.upcoming) - (a.departures + a.arrivals + a.upcoming) + ) + + return { + airports: sorted, + series: [ + { name: 'Departures', data: sorted.map(s => s.departures) }, + { name: 'Arrivals', data: sorted.map(s => s.arrivals) }, + { name: 'Upcoming', data: sorted.map(s => s.upcoming) }, + ], + } +} + +export function getFlightTypes(flights: Flight[]) { + const counts = { International: 0, Domestic: 0 } + flights.forEach(f => { + const dep = f.departure_airport.region?.country?.id + const arr = f.arrival_airport.region?.country?.id + if (dep && arr) { + dep === arr ? counts.Domestic++ : counts.International++ + } + }) + const sorted = Object.entries(counts).filter(([, count]) => count > 0) + return { + labels: sorted.map(([name]) => name), + series: sorted.map(([, count]) => count), + } +} + +export type FlightStats = ReturnType + +export function useFlightStats(flights: Ref) { + const now = new Date() + + const pastFlights = computed(() => + flights.value.filter(f => new Date(f.departure_date) <= now) + ) + const upcomingFlights = computed(() => + flights.value.filter(f => new Date(f.departure_date) > now) + ) + + const allFlights = computed(() => flights.value) + + const perYear = computed(() => getFlightsPerYear(pastFlights.value, upcomingFlights.value)) + const perMonth = computed(() => getFlightsPerMonth(pastFlights.value, upcomingFlights.value)) + const perDay = computed(() => getFlightsPerDay(pastFlights.value, upcomingFlights.value)) + const reasons = computed(() => getFlightReasons(allFlights.value)) + const classes = computed(() => getFlightClasses(allFlights.value)) + const seatTypes = computed(() => getSeatTypes(allFlights.value)) + const countries = computed(() => getCountries(pastFlights.value, upcomingFlights.value)) + const continents = computed(() => getContinents(allFlights.value)) + const topAirlines = computed(() => getTopAirlines(pastFlights.value, upcomingFlights.value)) + const topAirports = computed(() => getTopAirports(pastFlights.value, upcomingFlights.value)) + const flightTypes = computed(() => getFlightTypes(allFlights.value)) + + return { + pastFlights, + upcomingFlights, + perYear, + perMonth, + perDay, + reasons, + classes, + seatTypes, + countries, + continents, + flightTypes, + topAirlines, + topAirports, + } +} diff --git a/resources/js/Pages/UserProfile.vue b/resources/js/Pages/UserProfile.vue index f882b67..8d16c51 100644 --- a/resources/js/Pages/UserProfile.vue +++ b/resources/js/Pages/UserProfile.vue @@ -1,20 +1,15 @@