Added timezones

This commit is contained in:
2026-04-05 12:38:59 +10:00
parent 509efbe821
commit bf34c20d85
3 changed files with 77 additions and 11 deletions
@@ -46,7 +46,7 @@ class PopulateAirportTimezones extends Command
$airport->update(['timezone' => $zoneName]); $airport->update(['timezone' => $zoneName]);
$this->info("{$airport->name}{$zoneName}"); $this->info("{$airport->name}{$zoneName}");
} else { } else {
$this->warn("{$airport->name} — giving up after {$attempts} attempts"); $this->warn("{$airport->name} — giving up after {$attempts} attempts".$response->body());
} }
} }
}); });
@@ -148,7 +148,7 @@ class FlightImportController extends Controller
$date = null; $date = null;
if ($flightToReconcile->date) { if ($flightToReconcile->date) {
$date = Carbon::createFromFormat('m-d-y', $flightToReconcile->date)->format('Y-m-d'); $date = Carbon::createFromFormat('Y-m-d', $flightToReconcile->date)->format('Y-m-d');
} }
@@ -192,7 +192,7 @@ class FlightImportController extends Controller
'to_id' => 'required|integer|exists:airports,id', 'to_id' => 'required|integer|exists:airports,id',
'dep_time' => 'nullable|date_format:H:i', 'dep_time' => 'nullable|date_format:H:i',
'arr_time' => 'nullable|date_format:H:i', 'arr_time' => 'nullable|date_format:H:i',
'duration' => 'nullable|date_format:H:i', 'duration' => 'required|date_format:H:i',
'airline_id' => 'nullable|integer|exists:airlines,id', 'airline_id' => 'nullable|integer|exists:airlines,id',
'aircraft_id' => 'nullable|integer|exists:aircraft,id', 'aircraft_id' => 'nullable|integer|exists:aircraft,id',
'registration' => 'nullable|string', 'registration' => 'nullable|string',
@@ -207,10 +207,68 @@ class FlightImportController extends Controller
'dep_time.date_format' => 'Departure time must be in HH:MM format, i.e: 01:25', 'dep_time.date_format' => 'Departure time must be in HH:MM format, i.e: 01:25',
'arr_time.date_format' => 'Arrival time must be in HH:MM format, i.e: 12:25', 'arr_time.date_format' => 'Arrival time must be in HH:MM format, i.e: 12:25',
'duration.date_format' => 'Must be in HH:MM format, e.g: 03:37', 'duration.date_format' => 'Must be in HH:MM format, e.g: 03:37',
'duration.required' => 'A duration is required to be able to accurately calculate the arrival date',
]); ]);
} }
private function validateCsvFormat(string $path): ?string
{
$handle = fopen($path, 'r');
// Must have empty first line
$firstLine = fgetcsv($handle);
if (!empty(array_filter($firstLine))) {
fclose($handle);
return 'CSV must match the MyFlightRadar24 export format.';
}
// Validate headers
$expectedHeaders = [
'date', 'flight_number', 'from', 'to', 'dep_time', 'arr_time',
'duration', 'airline', 'aircraft', 'registration', 'seat_number',
'seat_type', 'flight_class', 'flight_reason', 'note',
'dep_id', 'arr_id', 'airline_id', 'aircraft_id'
];
$headers = array_map(
fn($h) => strtolower(trim(str_replace([' ', '"'], ['_', ''], $h))),
fgetcsv($handle)
);
if ($headers !== $expectedHeaders) {
fclose($handle);
return 'CSV headers do not match the expected format.';
}
// Validate data rows
$row = 1;
while (($data = fgetcsv($handle)) !== false) {
$row++;
if (count($data) !== count($expectedHeaders)) {
fclose($handle);
return "Row {$row} has the wrong number of columns.";
}
$combined = array_combine($expectedHeaders, $data);
if (empty($combined['date']) || empty($combined['from']) || empty($combined['to'])) {
fclose($handle);
return "Row {$row} is missing a required field (date, from, or to).";
}
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $combined['date'])) {
fclose($handle);
return "Row {$row} has an invalid date format. Expected YYYY-MM-DD, got '{$combined['date']}'.";
}
}
fclose($handle);
return null;
}
public function store(Request $request) public function store(Request $request)
{ {
try { try {
@@ -219,6 +277,12 @@ class FlightImportController extends Controller
]); ]);
$path = $request->file('csv')->getRealPath(); $path = $request->file('csv')->getRealPath();
$validationError = $this->validateCsvFormat($path);
if ($validationError) {
return response()->json(['message' => $validationError], 422);
}
$handle = fopen($path, 'r'); $handle = fopen($path, 'r');
fgetcsv($handle); fgetcsv($handle);
+10 -8
View File
@@ -2,7 +2,7 @@
import MainLayout from "@/Layouts/MainLayout.vue"; import MainLayout from "@/Layouts/MainLayout.vue";
import GlassBox from "@/Components/FlightsGoneBy/GlassBox.vue"; import GlassBox from "@/Components/FlightsGoneBy/GlassBox.vue";
import { Head } from "@inertiajs/vue3"; import { Head } from "@inertiajs/vue3";
import { ref } from "vue"; import { ref, computed} from "vue";
import {VFileInput} from "vuetify/components"; import {VFileInput} from "vuetify/components";
import {getCsrfToken} from "@/utils/helpers"; import {getCsrfToken} from "@/utils/helpers";
@@ -15,6 +15,13 @@ const errors = ref<string[]>([]);
const fileInput = ref<InstanceType<typeof VFileInput> | null>(null); const fileInput = ref<InstanceType<typeof VFileInput> | null>(null);
const selectedFile = ref<File | null>(null); const selectedFile = ref<File | null>(null);
const blurb = computed(() => {
if (status.value === 'success') {
return `Successfully imported ${importedCount} flight${importedCount.value !== 1 ? 's' : ''}. You will just need to reconcile some mismatched airlines and airports.`;
}
return 'Import a CSV export from MyFlightRadar24. You will then be guided to reconcile any data mismatches.';
});
async function onFileChange(e: Event) { async function onFileChange(e: Event) {
const input = e.target as HTMLInputElement; const input = e.target as HTMLInputElement;
const file = input.files?.[0]; const file = input.files?.[0];
@@ -57,8 +64,7 @@ async function onFileChange(e: Event) {
<template> <template>
<Head title="Import" /> <Head title="Import" />
<GlassBox style="display: flex;flex-direction: column;align-items: center;justify-content: center;"> <GlassBox title="Import Your Flights">
<h2>Import Your Flights</h2>
<p v-if="status !== 'success'"> <p v-if="status !== 'success'">
Import a CSV export from MyFlightRadar24. You will then be guided Import a CSV export from MyFlightRadar24. You will then be guided
to reconcile any data mismatches. to reconcile any data mismatches.
@@ -66,7 +72,6 @@ async function onFileChange(e: Event) {
<p v-else-if="status === 'success'" type="success" > <p v-else-if="status === 'success'" type="success" >
Successfully imported {{ importedCount }} flight{{ importedCount !== 1 ? 's' : '' }}. You will just need to reconcile some mismatched airlines and airports. Successfully imported {{ importedCount }} flight{{ importedCount !== 1 ? 's' : '' }}. You will just need to reconcile some mismatched airlines and airports.
</p> </p>
<div style="flex:0;width: 100%;display:flex;flex-direction:column;align-items: center;justify-content: center;gap:2em;">
<v-file-input <v-file-input
style="flex:0;width:100%" style="flex:0;width:100%"
prepend-icon="" prepend-icon=""
@@ -83,14 +88,11 @@ async function onFileChange(e: Event) {
<span>Importing your flights</span> <span>Importing your flights</span>
</div> </div>
<v-alert closable v-if="status === 'error'" type="error"> <v-alert closable v-if="status === 'error'" type="error">
<div v-for="(err, i) in errors" :key="i">{{ err }}</div> <div v-for="(err, i) in errors" :key="i">{{ err }}</div>
</v-alert> </v-alert>
<v-btn :href="route('reconcile')" v-if="status === 'success'" variant="tonal" size="x-large" block >Reconcile Your Data</v-btn> <v-btn :href="route('reconcile')" v-if="status === 'success'" size="x-large" block >Reconcile Your Data</v-btn>
</div>
</GlassBox> </GlassBox>
</template> </template>
<style scoped> <style scoped>