Added timezones
This commit is contained in:
@@ -46,7 +46,7 @@ class PopulateAirportTimezones extends Command
|
||||
$airport->update(['timezone' => $zoneName]);
|
||||
$this->info("✓ {$airport->name} — {$zoneName}");
|
||||
} 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;
|
||||
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',
|
||||
'dep_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',
|
||||
'aircraft_id' => 'nullable|integer|exists:aircraft,id',
|
||||
'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',
|
||||
'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.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)
|
||||
{
|
||||
try {
|
||||
@@ -219,6 +277,12 @@ class FlightImportController extends Controller
|
||||
]);
|
||||
|
||||
$path = $request->file('csv')->getRealPath();
|
||||
|
||||
$validationError = $this->validateCsvFormat($path);
|
||||
if ($validationError) {
|
||||
return response()->json(['message' => $validationError], 422);
|
||||
}
|
||||
|
||||
$handle = fopen($path, 'r');
|
||||
|
||||
fgetcsv($handle);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import MainLayout from "@/Layouts/MainLayout.vue";
|
||||
import GlassBox from "@/Components/FlightsGoneBy/GlassBox.vue";
|
||||
import { Head } from "@inertiajs/vue3";
|
||||
import { ref } from "vue";
|
||||
import { ref, computed} from "vue";
|
||||
import {VFileInput} from "vuetify/components";
|
||||
import {getCsrfToken} from "@/utils/helpers";
|
||||
|
||||
@@ -15,6 +15,13 @@ const errors = ref<string[]>([]);
|
||||
const fileInput = ref<InstanceType<typeof VFileInput> | 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) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
@@ -57,8 +64,7 @@ async function onFileChange(e: Event) {
|
||||
|
||||
<template>
|
||||
<Head title="Import" />
|
||||
<GlassBox style="display: flex;flex-direction: column;align-items: center;justify-content: center;">
|
||||
<h2>Import Your Flights</h2>
|
||||
<GlassBox title="Import Your Flights">
|
||||
<p v-if="status !== 'success'">
|
||||
Import a CSV export from MyFlightRadar24. You will then be guided
|
||||
to reconcile any data mismatches.
|
||||
@@ -66,7 +72,6 @@ async function onFileChange(e: Event) {
|
||||
<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.
|
||||
</p>
|
||||
<div style="flex:0;width: 100%;display:flex;flex-direction:column;align-items: center;justify-content: center;gap:2em;">
|
||||
<v-file-input
|
||||
style="flex:0;width:100%"
|
||||
prepend-icon=""
|
||||
@@ -83,14 +88,11 @@ async function onFileChange(e: Event) {
|
||||
<span>Importing your flights…</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<v-alert closable v-if="status === 'error'" type="error">
|
||||
<div v-for="(err, i) in errors" :key="i">{{ err }}</div>
|
||||
</v-alert>
|
||||
|
||||
<v-btn :href="route('reconcile')" v-if="status === 'success'" variant="tonal" size="x-large" block >Reconcile Your Data</v-btn>
|
||||
</div>
|
||||
<v-btn :href="route('reconcile')" v-if="status === 'success'" size="x-large" block >Reconcile Your Data</v-btn>
|
||||
</GlassBox>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user