Added timezones
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user