Prace na sprave letu

This commit is contained in:
Michal Kváček
2023-06-07 08:56:52 +02:00
parent 25bf93589e
commit 4d9950e362
15 changed files with 267 additions and 154 deletions
+5 -2
View File
@@ -1,19 +1,20 @@
<script setup> <script setup>
import {isRequestPending} from "src/composables/form/props"; import {isRequestPending} from "src/composables/form/props";
import {useSlots} from "vue";
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Object, type: Object,
required: true required: true
}, },
resetAfterSubmit: { resetAfterSubmit: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
}, },
isRequestPending, isRequestPending,
saveButton: { saveButton: {
type: Object, type: Object,
required: false, required: false,
@@ -37,7 +38,9 @@ const saveForm = () => {
<template> <template>
<q-form @submit.prevent="saveForm" ref="form"> <q-form @submit.prevent="saveForm" ref="form">
<slot/> <slot name="default"/>
<slot name="saveButton" />
<q-btn type="submit" color="primary" :icon="props.saveButton.icon" :loading="props.isRequestPending"> <q-btn type="submit" color="primary" :icon="props.saveButton.icon" :loading="props.isRequestPending">
{{ props.saveButton.label }} {{ props.saveButton.label }}
@@ -0,0 +1,3 @@
<template>
skeleton
</template>
+145 -109
View File
@@ -2,7 +2,7 @@
import LocationInput from "components/inputs/LocationInput.vue"; import LocationInput from "components/inputs/LocationInput.vue";
import TabsStepper from "components/TabsStepper.vue"; import TabsStepper from "components/TabsStepper.vue";
import FormWrapper from "components/FormWrapper.vue"; import FormWrapper from "components/FormWrapper.vue";
import {computed, reactive, ref} from "vue"; import {computed, reactive, ref, watch} from "vue";
import {isRequired} from "src/composables/form/rules"; import {isRequired} from "src/composables/form/rules";
import DatetimePicker from "components/inputs/DatetimePicker.vue"; import DatetimePicker from "components/inputs/DatetimePicker.vue";
@@ -23,18 +23,20 @@ const props = defineProps({
) )
const formData = reactive({ const formData = reactive({
aircraftId: null, aircraftId: 43,
takeoffAirportId: null, takeoffAirportId: 3,
landingAirportId: null, landingAirportId: 3,
takeoffDatetime: null, takeoffDatetime: new Date("2023-06-05T09:55:01"),
landingDatetime: null, landingDatetime: new Date("2023-06-05T10:41:51"),
photos: [],
// turnPoints: [], // turnPoints: [],
copilotId: null, copilotId: null,
solo: true, solo: true,
withInstructor: false, withInstructor: false,
durationPic: 0, durationPic: 0,
// flightDurationMinutes: 0, durationTotal: null,
landings: 1, landings: 1,
name: "", name: "",
@@ -46,7 +48,16 @@ const rules = {
landingAirport: [], landingAirport: [],
} }
const formSteps = ["aircraft", "basic", "copilot", "track", "photos", "notes"] const recalculateDuration = () => {
const diff = formData.landingDatetime - formData.takeoffDatetime;
formData.durationTotal = diff / 1000 / 60;
}
watch(() => formData.takeoffDatetime, () => recalculateDuration())
watch(() => formData.landingDatetime, () => recalculateDuration())
const formSteps = ["basic", "copilot", "track", "photos", "notes"]
const formStep = ref(formSteps[0]); const formStep = ref(formSteps[0]);
const basicInfoFilled = computed(() => { const basicInfoFilled = computed(() => {
return ( return (
@@ -59,119 +70,144 @@ const basicInfoFilled = computed(() => {
<template> <template>
<FormWrapper v-model="formData"> <FormWrapper v-model="formData">
{{ formData }} <template #default>
<q-tabs <pre>{{ formData }}</pre>
v-model="formStep"
outside-arrows
mobile-arrows
left-icon="chevron_left"
right-icon="chevron_right"
active-color="primary"
class="q-mb-lg"
>
<q-tab name="aircraft" icon="flight" label="Aircraft"/>
<q-tab name="basic" icon="flight_takeoff" label="Basic"/>
<q-tab name="copilot" icon="person_add" label="Copilot"/>
<q-tab name="track" icon="route" label="Route"/>
<q-tab name="photos" icon="add_a_photo" label="Photos"/>
<q-tab name="notes" icon="edit_note" label="Notes"/>
</q-tabs>
<q-tab-panels v-model="formStep" animated> <q-tabs
<q-tab-panel name="aircraft"> v-model="formStep"
<q-select outside-arrows
v-model="formData.aircraftId" mobile-arrows
option-label="name" left-icon="chevron_left"
option-value="id" right-icon="chevron_right"
emit-value active-color="primary"
map-options class="q-mb-lg"
:rules="rules.aircraft" >
:options="aircrafts" <q-tab name="basic" icon="flight_takeoff" label="Základní informace"/>
label="Letadlo" <q-tab name="copilot" icon="person_add" label="Spolucestující"/>
> <q-tab name="track" icon="route" label="Trasa"/>
<template #prepend> <q-tab name="photos" icon="add_a_photo" label="Fotky"/>
<q-icon name="flight"></q-icon> <q-tab name="notes" icon="edit_note" label="Poznámky"/>
</q-tabs>
<q-tab-panels v-model="formStep" animated>
<q-tab-panel name="basic">
<div class="row">
<div class="col">
<q-select
v-model="formData.aircraftId"
option-label="name"
option-value="id"
emit-value
map-options
:rules="rules.aircraft"
:options="aircrafts"
label="Letadlo"
>
<template #prepend>
<q-icon name="flight"></q-icon>
</template>
</q-select>
</div>
</div>
<template v-if="formData.aircraftId">
<div class="row">
<div class="col">
<h3>Vzlet</h3>
<DatetimePicker v-model="formData.takeoffDatetime" label="Datum a čas vzletu"/>
<LocationInput label="Letiště vzletu" :rules="rules.takeoffAirport" :locations="airports" v-model="formData.takeoffAirportId" prepend-icon="flight_takeoff"/>
</div>
</div>
<div class="row" v-if="!!formData.takeoffDatetime && !!formData.takeoffAirportId">
<div class="col">
<h3>Přistání</h3>
<LocationInput label="Letiště přistání" :rules="rules.landingAirport" :locations="airports" v-model="formData.landingAirportId" prepend-icon="flight_land"/>
<DatetimePicker v-model="formData.landingDatetime" label="Datum a čas přistání"/>
<div class="row">
<div class="col">
<q-input v-model="formData.landings" label="Počet přistání" type="number" min="1"/>
</div>
<div class="col">
<q-input v-model="formData.durationTotal" readonly min=0 label="Délka letu (v minutách)" type="number"/>
</div>
</div>
</div>
</div>
<TabsStepper v-model="formStep" next-step="track" v-if="formData.landingAirportId"/>
</template> </template>
</q-select> </q-tab-panel>
<TabsStepper v-model="formStep" next-step="basic"/> <q-tab-panel name="copilot">
</q-tab-panel> <q-btn-toggle spread no-caps v-model="formData.solo" toggle-color="primary" :options="[{label: 'Solo flight', value: true}, {label: 'Flight with someone', value: false}]"/>
<q-tab-panel name="basic">
<!-- <q-input type="date" v-model="formData.date" label="Date"/>-->
<DatetimePicker v-model="formData.takeoffDatetime" label="Datum a čas vzletu" /> <div v-if="!formData.solo">
<q-input type="text" v-model="formData.secondPilot" label="Second pilot"/>
<q-checkbox v-model="formData.instructor" v-if="formData.secondPilot !== ''" label="Instructor"/>
<!-- <q-input v-model="formData.picTime" label="PIC time"/>-->
</div>
<LocationInput label="Take-off location" :rules="rules.takeoffAirport" :locations="airports" v-model="formData.takeoffAirportId" prepend-icon="flight_takeoff"/> <TabsStepper v-model="formStep" previous-step="basic" next-step="track"/>
<LocationInput label="Landing location" :rules="rules.landingAirport" :locations="airports" v-model="formData.landingAirportId" prepend-icon="flight_land"/> </q-tab-panel>
<q-input type="datetime-local" v-model="formData.landingDatetime" label="Landing date &amp; time"/>
<q-input v-model="formData.landings" label="Landings" type="number" min="1"/>
<q-input v-model="formData.flightDurationMinutes" min=0 label="Flight duration (minutes)" type="number"/>
<TabsStepper v-model="formStep" previous-step="aircraft" next-step="track"/> <q-tab-panel name="track">
</q-tab-panel> <!-- <template v-for="i in turnPointsCount" :key="`${i}-${turnPointsCount}`">-->
<!-- <LocationInput label="Turn point" :locations="locations" v-model="formData.turnPoints[i-1]">-->
<!-- <template #append v-if="i > 1">-->
<!-- <q-btn @click="removeTurnPoint(i - 1)">-</q-btn>-->
<!-- </template>-->
<!-- </LocationInput>-->
<!-- </template>-->
<!-- <q-btn @click="turnPointsCount++">-->
<!-- Add turn point-->
<!-- </q-btn>-->
<q-tab-panel name="track"> <q-uploader
<!-- <template v-for="i in turnPointsCount" :key="`${i}-${turnPointsCount}`">--> label="Upload GPX track"
<!-- <LocationInput label="Turn point" :locations="locations" v-model="formData.turnPoints[i-1]">--> url="http://localhost:4444/upload"
<!-- <template #append v-if="i > 1">--> class="q-mx-sm"
<!-- <q-btn @click="removeTurnPoint(i - 1)">-</q-btn>--> style="width: 100%"
<!-- </template>--> auto-upload
<!-- </LocationInput>--> color="secondary"
<!-- </template>--> />
<!-- <q-btn @click="turnPointsCount++">-->
<!-- Add turn point-->
<!-- </q-btn>-->
<q-uploader <TabsStepper v-model="formStep" previous-step="copilot" next-step="photos"/>
label="Upload GPX track" </q-tab-panel>
url="http://localhost:4444/upload"
class="q-mx-sm"
style="width: 100%"
auto-upload
color="secondary"
/>
<TabsStepper v-model="formStep" previous-step="basic" next-step="copilot"/>
</q-tab-panel>
<q-tab-panel name="copilot">
<q-btn-toggle spread no-caps v-model="formData.solo" toggle-color="primary" :options="[{label: 'Solo flight', value: true}, {label: 'Flight with someone', value: false}]"/>
<div v-if="!formData.solo"> <q-tab-panel name="photos">
<q-input type="text" v-model="formData.secondPilot" label="Second pilot"/> <q-file
<q-checkbox v-model="formData.instructor" v-if="formData.secondPilot !== ''" label="Instructor"/> label="Upload photos"
<q-input v-model="formData.picTime" label="PIC time"/> multiple
style="width: 100%;"
color="secondary"
/>
<TabsStepper v-model="formStep" previous-step="copilot" next-step="notes"/>
</q-tab-panel>
<q-tab-panel name="notes">
<q-input type="text" v-model="formData.name" label="Nazev"/>
<q-editor v-model="formData.description" height="60vh" label="Poznámky"/>
<TabsStepper v-model="formStep" previous-step="photos"/>
</q-tab-panel>
</q-tab-panels>
</template>
<template #saveButton>
<q-page-sticky expand position="bottom" :offset="[10, 10]" v-if="basicInfoFilled">
<div>
<q-btn type="submit" size="lg" class="full-width" color="primary" icon="save" label="Save flight"/>
</div> </div>
</q-page-sticky>
<TabsStepper v-model="formStep" previous-step="track" next-step="photos"/> </template>
</q-tab-panel>
<q-tab-panel name="photos">
<q-uploader
label="Upload photos"
url="http://localhost:4444/upload"
multiple
auto-upload
style="width: 100%;"
color="secondary"
/>
<TabsStepper v-model="formStep" previous-step="copilot" next-step="notes"/>
</q-tab-panel>
<q-tab-panel name="notes">
<q-input type="text" v-model="formData.name" label="Nazev" />
<q-editor v-model="formData.description" height="60vh" label="Poznámky"/>
<TabsStepper v-model="formStep" previous-step="photos"/>
</q-tab-panel>
</q-tab-panels>
<q-page-sticky expand position="bottom" :offset="[10, 10]" v-if="basicInfoFilled">
<div>
<q-btn type="submit" size="lg" class="full-width" color="primary" icon="save" label="Save flight"/>
</div>
</q-page-sticky>
</FormWrapper> </FormWrapper>
</template> </template>
+3 -3
View File
@@ -1,5 +1,5 @@
<script setup> <script setup>
import {minutesToHumanReadable} from "src/composables/timeUtils"; import {formatDate, minutesToHumanReadable} from "src/composables/datetimeUtils";
import {computed} from "vue"; import {computed} from "vue";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
@@ -47,10 +47,10 @@ const totalFlightTime = computed(() => {
{{ flight.takeoffAirport.name }} - {{ flight.landingAirport.name }} {{ flight.takeoffAirport.name }} - {{ flight.landingAirport.name }}
</router-link> </router-link>
</div> </div>
<div class="text-subtitle2">{{ minutesToHumanReadable(flight.flightDurationMinutes) }}</div> <div class="text-subtitle2">{{ minutesToHumanReadable(flight.durationMinCalculated) }}</div>
<div class="text-secondary"> <div class="text-secondary">
<q-icon name="today"/> <q-icon name="today"/>
{{ flight.date }} {{ formatDate(flight.takeoffDatetime) }}
</div> </div>
<div class="text-secondary"> <div class="text-secondary">
<q-icon name="plane"/> <q-icon name="plane"/>
+59 -10
View File
@@ -1,24 +1,73 @@
<script setup> <script setup>
import {onMounted, ref, watch} from "vue"; import {computed, onMounted, ref, watch} from "vue";
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: String, type: Date,
required: true },
dateMask: {
type: String, Boolean,
required: false,
default: "YYYY-MM-DD"
},
timeMask: {
type: String, Boolean,
required: false,
default: "HH:mm"
} }
}) })
const emit = defineEmits(['update:modelValue']);
let model = ref(""); let dateModel = ref("");
let timeModel = ref("");
onMounted(() => { onMounted(() => {
model.value = props.modelValue; let date = "";
let time = "";
if (props.modelValue) {
date = props.dateMask.replace("YYYY", props.modelValue.getFullYear())
.replace("MM", ('0' + props.modelValue.getMonth()).slice(-2))
.replace("DD", ('0' + props.modelValue.getDate()).slice(-2));
time = props.timeMask.replace("HH", ('0' + props.modelValue.getHours()).slice(-2))
.replace("mm", ('0' + props.modelValue.getMinutes()).slice(-2))
}
if (time === '') {
time = "00:00";
}
dateModel.value = date;
timeModel.value = time;
}); });
watch(model, (newVal) => { const model = computed(() => dateModel.value + " " + timeModel.value);
emit("update:modelValue", newVal);
}) const emit = defineEmits(['update:modelValue']);
const emitModelUpdate = () => {
const now = new Date();
const val = new Date(model.value);
val.setHours(val.getHours() + now.getTimezoneOffset());
emit("update:modelValue", val);
}
watch([dateModel, timeModel], emitModelUpdate)
</script> </script>
<template> <template>
<q-input type="datetime-local" v-model="model" v-bind="$attrs"/> <q-input v-model="model" readonly>
<template #append>
<q-icon name="event">
<q-popup-proxy cover>
<div class="row">
<div class="col" v-if="dateMask">
<q-date v-model="dateModel" :mask="dateMask"/>
</div>
<div class="col" v-if="timeMask">
<q-time v-model="timeModel" format24h :mask="timeMask"/>
</div>
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</template> </template>
-2
View File
@@ -39,8 +39,6 @@ watch(selectedLocationModel, (newVal) => {
// TODO: rozdelit emit na dva modely - jeden na emit existujici lokality, druhy na emit nove vytvarene // TODO: rozdelit emit na dva modely - jeden na emit existujici lokality, druhy na emit nove vytvarene
if (!newVal) return; if (!newVal) return;
console.log(newVal);
let value, eventName; let value, eventName;
if (newVal instanceof String) { if (newVal instanceof String) {
eventName = "update:modelValue"; eventName = "update:modelValue";
@@ -3,3 +3,10 @@ export const minutesToHumanReadable = (totalMinutes) => {
const minutes = totalMinutes % 60; const minutes = totalMinutes % 60;
return `${hours}h ${minutes}min` return `${hours}h ${minutes}min`
} }
export const formatDate = (date) => {
if (typeof date === 'string') {
date = new Date(date);
}
return date.getDate()+". "+date.getMonth()+". "+date.getFullYear();
}
+4
View File
@@ -4,8 +4,12 @@ export const FRAGMENT_FLIGHT_FULL_INFO = gql`
fragment flightFullInfo on Flight { fragment flightFullInfo on Flight {
id id
name name
description
takeoffDatetime takeoffDatetime
landingDatetime landingDatetime
durationMinCalculated
takeoffAirport { takeoffAirport {
id id
+10 -1
View File
@@ -7,7 +7,16 @@ query FlightsList {
...flightFullInfo ...flightFullInfo
} }
} }
${FRAGMENT_FLIGHT_FULL_INFO} ${FRAGMENT_FLIGHT_FULL_INFO}
`;
export const FLIGHT_DETAIL = gql`
query FlightDetail($id: Int!) {
flight(id: $id) {
...flightFullInfo
}
}
${FRAGMENT_FLIGHT_FULL_INFO}
`; `;
@@ -10,7 +10,7 @@ const route = useRoute();
const router = useRouter(); const router = useRouter();
const aircraftId = computed(() => parseInt(route.params.id)); const aircraftId = computed(() => parseInt(route.params.id));
let loading = ref(false); const loading = ref(false);
let aircraft = reactive({}); let aircraft = reactive({});
onMounted(() => { onMounted(() => {
loading.value = true; loading.value = true;
+26 -19
View File
@@ -1,34 +1,41 @@
<script setup> <script setup>
import {useFlightsStore} from "stores/Flights"; import {useFlightsStore} from "stores/Flights";
import {useRoute} from "vue-router"; import {useRoute} from "vue-router";
import {computed, onMounted} from "vue"; import {onMounted, reactive, ref} from "vue";
import {minutesToHumanReadable} from "src/composables/timeUtils"; import {formatDate, minutesToHumanReadable} from "src/composables/datetimeUtils";
import FlightDetailSkeleton from "components/flights/FlightDetailSkeleton.vue";
import AddFAB from "components/AddFAB.vue";
const flightsStore = useFlightsStore();
const route = useRoute(); const route = useRoute();
const loading = ref(false)
let flight = reactive({});
onMounted(() => { onMounted(() => {
flightsStore.fetchDetail(route.params.id); loading.value = true;
useFlightsStore().fetchDetail(parseInt(route.params.id))
.then(respFlight => flight = respFlight)
.finally(() => loading.value = false);
}) })
const flight = computed(() => flightsStore.getDetail(route.params.id) || {});
</script> </script>
<template> <template>
<h1>{{flight.takeOffLocation}} - {{flight.landingLocation}}</h1> <FlightDetailSkeleton v-if="loading || Object.keys(flight).length === 0"/>
<q-icon name="calendar">{{flight.date}}</q-icon> <template v-else>
<q-icon name="weather"> <h1>{{ flight.name }}</h1>
Clouds: few <h2>{{ flight.takeoffAirport.name }} - {{ flight.landingAirport.name }}</h2>
Wind: 220/12kt
Temp: 12°C
</q-icon>
Track: {{ flight.turnPoints }} <article v-html="flight.description" />
{{ minutesToHumanReadable(flight.flightDurationMinutes) }} <q-icon name="calendar"/>
{{ formatDate(flight.takeoffDatetime) }}
{{ minutesToHumanReadable(flight.durationMinCalculated) }}
<q-page-sticky location="bottom-right" :offset="[30, 30]"> <!-- <q-icon name="weather"/>-->
<q-fab color="primary" icon="edit" label="Edit" @click="$router.push({name: 'flightEdit', params:{id: flight.id}})"/> <!-- Clouds: few-->
</q-page-sticky> <!-- Wind: 220/12kt-->
<!-- Temp: 12°C-->
<AddFAB :router-link="{name: 'flightEdit', params:{id: flight.id}}" label="Upravit"/>
</template>
</template> </template>
@@ -4,7 +4,7 @@ import {useRoute} from "vue-router";
import {useFlightsStore} from "stores/Flights"; import {useFlightsStore} from "stores/Flights";
import {computed, onMounted} from "vue"; import {computed, onMounted} from "vue";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import {minutesToHumanReadable} from "src/composables/timeUtils"; import {minutesToHumanReadable} from "src/composables/datetimeUtils";
const secondPilotStore = useSecondPilotsStore(); const secondPilotStore = useSecondPilotsStore();
const flightsStore = useFlightsStore(); const flightsStore = useFlightsStore();
-1
View File
@@ -9,7 +9,6 @@ const password = ref("");
const router = useRouter(); const router = useRouter();
const logUser = () => { const logUser = () => {
useUserStore().login({email: email.value, password: password.value}).then(() => { useUserStore().login({email: email.value, password: password.value}).then(() => {
console.log("tu ok");
router.push({name: "myFlights"}); router.push({name: "myFlights"});
}); });
}; };
+1 -3
View File
@@ -17,9 +17,7 @@ export const useAircraftsStore = defineStore('Aircrafts', {
fetchDetail(id) { fetchDetail(id) {
return apolloWrapper return apolloWrapper
.query(AIRCRAFT_DETAIL, {id}) .query(AIRCRAFT_DETAIL, {id})
.then((result) => { .then((result) => result.data.aircraft);
return result.data.aircraft;
});
}, },
create(formData) { create(formData) {
delete formData['id']; delete formData['id'];
+2 -2
View File
@@ -1,6 +1,6 @@
import {defineStore} from 'pinia' import {defineStore} from 'pinia'
import {apolloWrapper} from "src/graphql/apollo" import {apolloWrapper} from "src/graphql/apollo"
import {FLIGHTS_LIST} from "src/graphql/queries/flights"; import {FLIGHT_DETAIL, FLIGHTS_LIST} from "src/graphql/queries/flights";
import {CREATE_FLIGHT} from "src/graphql/mutations/flight"; import {CREATE_FLIGHT} from "src/graphql/mutations/flight";
export const useFlightsStore = defineStore('Flights', { export const useFlightsStore = defineStore('Flights', {
@@ -24,7 +24,7 @@ export const useFlightsStore = defineStore('Flights', {
}) })
}, },
fetchDetail(id) { fetchDetail(id) {
this.items.push({id: id, takeOffLocation: "LKLT", landingLocation: "LKLT", date: "2023-02-01", aircraft: {id: 1, name: "OK-AUR28"}, flightDurationMinutes: 76}) return apolloWrapper.query(FLIGHT_DETAIL, {id}).then(result => result.data.flight);
}, },
fetch() { fetch() {
return apolloWrapper return apolloWrapper