Added Imported Flights Table
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
const props = defineProps<{
|
||||
prefilledOptions: { value: number, title: string }[]
|
||||
errorMessages?: string[] | string
|
||||
}>()
|
||||
|
||||
const model = defineModel<{ value: number, title: string } | null>()
|
||||
|
||||
const airlineOptions = ref(props.prefilledOptions ?? [])
|
||||
const autocompleteRef = ref<any>(null)
|
||||
|
||||
const onFocus = () => {
|
||||
nextTick(() => {
|
||||
const input = autocompleteRef.value?.$el?.querySelector('input')
|
||||
input?.select()
|
||||
})
|
||||
}
|
||||
|
||||
const searchAirlines = async (query: string) => {
|
||||
if (!query || query.length < 2) {
|
||||
airlineOptions.value = props.prefilledOptions ?? []
|
||||
return
|
||||
}
|
||||
|
||||
if (query === model.value?.title) return
|
||||
|
||||
const { data } = await axios.get('/airlines/search', { params: { q: query } })
|
||||
airlineOptions.value = data
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-autocomplete
|
||||
ref="autocompleteRef"
|
||||
v-model="model"
|
||||
label="Airline"
|
||||
:items="airlineOptions"
|
||||
:error-messages="errorMessages"
|
||||
item-title="title"
|
||||
@update:search="searchAirlines"
|
||||
@focus="onFocus"
|
||||
hint="Showing closest matches to the imported value"
|
||||
placeholder="SriLankan Airlines"
|
||||
clearable
|
||||
hide-no-data
|
||||
return-object
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<img
|
||||
v-if="model"
|
||||
style="padding: 0.25em"
|
||||
width="40"
|
||||
height="40"
|
||||
:src="`http://api.flightsgoneby.test:8000/airlines/logos/tail/id/${model.value}`"
|
||||
/>
|
||||
</template>
|
||||
<template #item="{ item, props: itemProps }">
|
||||
<v-list-item v-bind="itemProps">
|
||||
<template #prepend>
|
||||
<img style="padding:0.25em" width="40" height="40" :src="`http://api.flightsgoneby.test:8000/airlines/logos/tail/id/${item.value}`" />
|
||||
</template>
|
||||
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</template>
|
||||
@@ -11,12 +11,9 @@
|
||||
<style scoped>
|
||||
.glass-box {
|
||||
width: 50%;
|
||||
height: 50dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 50dvh;
|
||||
gap:1em;
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,24 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import MainLayout from "@/Layouts/MainLayout.vue";
|
||||
import GlassBox from "@/Components/FlightsGoneBy/GlassBox.vue";
|
||||
import {Head} from "@inertiajs/vue3";
|
||||
import { Head } from "@inertiajs/vue3";
|
||||
import { ref } from "vue";
|
||||
import {VFileInput} from "vuetify/components";
|
||||
import {getCsrfToken} from "@/utils/helpers";
|
||||
|
||||
defineOptions({
|
||||
layout: MainLayout
|
||||
})
|
||||
|
||||
defineOptions({ layout: MainLayout });
|
||||
|
||||
const status = ref<"idle" | "uploading" | "success" | "error">("idle");
|
||||
const importedCount = ref(0);
|
||||
const errors = ref<string[]>([]);
|
||||
const fileInput = ref<InstanceType<typeof VFileInput> | null>(null);
|
||||
const selectedFile = ref<File | null>(null);
|
||||
|
||||
async function onFileChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
const actualFile = Array.isArray(file) ? file[0] : file;
|
||||
if (!actualFile) return;
|
||||
if (!file) return;
|
||||
|
||||
status.value = "uploading";
|
||||
errors.value = [];
|
||||
|
||||
const form = new FormData();
|
||||
form.append("csv", file);
|
||||
|
||||
try {
|
||||
const response = await fetch(route("flights.import.store"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": getCsrfToken(),
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: form,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
errors.value = data.errors ?? [data.message ?? "Import failed."];
|
||||
status.value = "error";
|
||||
} else {
|
||||
importedCount.value = data.imported;
|
||||
status.value = "success";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Fetch error:", e);
|
||||
errors.value = [String(e)];
|
||||
status.value = "error";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Import" />
|
||||
<GlassBox>
|
||||
<GlassBox style="display: flex;flex-direction: column;align-items: center;justify-content: center;">
|
||||
<h2>Import Your Flights</h2>
|
||||
<p>
|
||||
Import a CSV export from MyFlightRadar24. You will then be guided to reconcile any data mismatches.
|
||||
<p v-if="status !== 'success'">
|
||||
Import a CSV export from MyFlightRadar24. You will then be guided
|
||||
to reconcile any data mismatches.
|
||||
</p>
|
||||
<v-file-input style="width:100%; flex:0" label="Select CSV File" accept=".csv" />
|
||||
<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=""
|
||||
v-if="status !== 'uploading' && status !== 'success'"
|
||||
label="Select CSV File"
|
||||
accept=".csv"
|
||||
@change="onFileChange"
|
||||
ref="fileInput"
|
||||
v-model="selectedFile"
|
||||
/>
|
||||
|
||||
<div v-else-if="status === 'uploading'" class="d-flex align-center ga-3 mt-2">
|
||||
<v-progress-circular indeterminate color="primary" />
|
||||
<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>
|
||||
</GlassBox>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h2{
|
||||
font-size: 2rem;
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
import MainLayout from "@/Layouts/MainLayout.vue";
|
||||
import GlassBox from "@/Components/FlightsGoneBy/GlassBox.vue";
|
||||
import {Head, useForm} from "@inertiajs/vue3";
|
||||
import AirlineSearchBox from "@/Components/FlightsGoneBy/AirlineSearchBox.vue";
|
||||
|
||||
defineOptions({ layout: MainLayout });
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
flight: {
|
||||
flight_reasons: { value: number, title: string }[]
|
||||
flight_classes:{ value: number, title: string }[]
|
||||
seat_types: { value: number, title: string }[]
|
||||
seat_type: number,
|
||||
'flight_class': number,
|
||||
'flight_reason': number,
|
||||
flight_number: string
|
||||
date: string
|
||||
registration: string
|
||||
seat_number: string
|
||||
note: string
|
||||
dep_time: string
|
||||
arr_time: string
|
||||
duration: string
|
||||
airline_options: { value: number, title: string }[]
|
||||
}
|
||||
}>()
|
||||
const flight = props.flight;
|
||||
|
||||
const form = useForm({
|
||||
date: flight.date,
|
||||
flight_number: flight.flight_number,
|
||||
from: '',
|
||||
to: '',
|
||||
dep_time: flight.dep_time,
|
||||
arr_time: flight.arr_time,
|
||||
duration: flight.duration,
|
||||
airline_id: flight.airline_options[0] ?? null,
|
||||
aircraft: '',
|
||||
registration: flight.registration,
|
||||
seat_number: flight.seat_number,
|
||||
seat_type: flight.seat_types[flight.seat_type],
|
||||
flight_class: flight.flight_classes[flight.flight_class],
|
||||
flight_reason: flight.flight_reasons[flight.flight_reason],
|
||||
note: flight.note,
|
||||
|
||||
});
|
||||
|
||||
function submit() {
|
||||
form.post(route('reconcile.store'));
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Reconcile"></Head>
|
||||
<GlassBox>
|
||||
<h2>Reconcile {{flight.flight_number.length > 1 ? 'Flight ' + flight.flight_number: 'This Flight'}}</h2>
|
||||
<p>Review and correct your imported flight details before they're saved.</p>
|
||||
|
||||
<v-form style="width: 100%">
|
||||
<v-container>
|
||||
|
||||
<!-- Date / Flight Number / Registration -->
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field type="date" v-model="form.date" label="Date" placeholder="03-18-11" :error-messages="form.errors.date" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field v-model="form.flight_number" label="Flight Number" placeholder="UL227" :error-messages="form.errors.flight_number" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field v-model="form.registration" label="Registration" placeholder="4R-ALX" :error-messages="form.errors.registration" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- From / To -->
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-autocomplete label="From" :items="[]" placeholder="Dubai International (DXB)" :error-messages="form.errors.from" hide-no-data clearable />
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-autocomplete label="To" :items="[]" placeholder="Kuwait (KWI)" :error-messages="form.errors.to" hide-no-data clearable />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Dep time / Arr time / Duration -->
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field v-model="form.dep_time" type="time" label="Departure Time" placeholder="09:30" :error-messages="form.errors.dep_time" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field v-model="form.arr_time" type="time" label="Arrival Time" placeholder="11:45" :error-messages="form.errors.arr_time" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field v-model="form.duration" label="Duration" placeholder="1:23" :error-messages="form.errors.duration" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Airline / Aircraft -->
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<AirlineSearchBox
|
||||
v-model="form.airline_id"
|
||||
:prefilled-options="flight.airline_options"
|
||||
:error-messages="form.errors.airline_id"
|
||||
/>
|
||||
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-autocomplete label="Aircraft" :items="[]" placeholder="Airbus A330" :error-messages="form.errors.aircraft" hide-no-data clearable />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Seat Number / Seat Type / Flight Class / Flight Reason -->
|
||||
<v-row>
|
||||
<v-col cols="12" md="3">
|
||||
<v-text-field v-model="form.seat_number" label="Seat Number" placeholder="22A" :error-messages="form.errors.seat_number" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-select v-model="form.seat_type" label="Seat Type" :items="flight.seat_types" :error-messages="form.errors.seat_type" clearable />
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-select v-model="form.flight_class" label="Class" :items="flight.flight_classes" :error-messages="form.errors.flight_class" clearable />
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-select v-model="form.flight_reason" label="Reason" :items="flight.flight_reasons" :error-messages="form.errors.flight_reason" clearable />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Note -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea v-model="form.note" label="Note" placeholder="Any additional notes…" :error-messages="form.errors.note" rows="3" auto-grow />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Submit -->
|
||||
<v-row>
|
||||
<v-col cols="12" class="d-flex justify-end">
|
||||
<v-btn size="large" :loading="form.processing" :disabled="form.processing" @click="submit">
|
||||
Save Flight
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
</v-form>
|
||||
</GlassBox>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
||||
import { ZiggyVue } from '../../vendor/tightenco/ziggy';
|
||||
import { createApp, h, DefineComponent } from 'vue';
|
||||
import vuetify from './plugins/vuetify';
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'vuetify/styles'
|
||||
import { createVuetify } from 'vuetify'
|
||||
|
||||
|
||||
export default createVuetify({
|
||||
theme: {
|
||||
defaultTheme: 'dark',
|
||||
},
|
||||
icons: {
|
||||
defaultSet: 'mdi',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function getCsrfToken(): string {
|
||||
return (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content ?? '';
|
||||
}
|
||||
Reference in New Issue
Block a user