Sprava eventu

This commit is contained in:
Michal Kváček
2023-09-19 06:33:17 +02:00
parent 0c91ff7838
commit 6fbaef4a1e
25 changed files with 533 additions and 30 deletions
+28
View File
@@ -0,0 +1,28 @@
<script setup>
const props = defineProps({
avatars: {
type: Array,
required: true
}
});
</script>
<template>
<div style="height: 80px">
<q-avatar
v-for="(avatar, idx) in avatars"
:key="idx"
class="overlapping"
:style="`left: ${(idx+1) * 20}px`"
>
<img :src="avatar"/>
<!-- <q-icon name="flight" v-else />-->
</q-avatar>
</div>
</template>
<style lang="sass" scoped>
.overlapping
position: absolute
</style>
@@ -3,7 +3,6 @@ import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
</script> </script>
<template> <template>
<q-card class="flight-detail-card"> <q-skeleton type="rect" class="q-my-lg" style="width: 200px; height: 40px" />
<BasicListSkeleton :items-count-min="1" :items-count-max="5"/> <BasicListSkeleton :items-count-min="1" :items-count-max="5"/>
</q-card>
</template> </template>
@@ -1,5 +1,5 @@
<script setup></script> <script setup></script>
<template> <template>
<em>Copilot skeleton</em> <q-skeleton type="QInput" />
</template> </template>
@@ -0,0 +1,9 @@
<script setup>
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
</script>
<template>
<q-card class="flight-detail-card">
<BasicListSkeleton :items-count-min="1" :items-count-max="5"/>
</q-card>
</template>
@@ -0,0 +1,5 @@
<script setup></script>
<template>
<em>Copilot skeleton</em>
</template>
+48
View File
@@ -0,0 +1,48 @@
<script setup>
import {onMounted, reactive} from "vue";
import {setDefaultData} from "src/composables/form/formHandling";
import FormWrapper from "components/FormWrapper.vue";
import {commonProps} from "src/composables/inputs";
import {isRequestPending} from "src/composables/form/props";
import {isRequired} from "src/composables/form/rules";
import RichTextEditor from "components/inputs/RichTextEditor.vue";
const props = defineProps({
event: {
type: Object
},
isRequestPending
})
const formData = reactive({
name: null,
isPublic: false,
description: "",
});
const rules = {
name: [isRequired]
}
onMounted(() => {
if (props.event) {
setDefaultData(formData, props.event);
}
})
</script>
<template>
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
<q-input
v-bind="commonProps"
label="Jméno"
:rules="rules.name"
v-model="formData.name"
/>
<RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..."/>
<q-checkbox v-model="formData.isPublic" label="Veřejný?"/>
</FormWrapper>
</template>
+32
View File
@@ -0,0 +1,32 @@
<script setup>
import AvatarsGroup from "components/AvatarsGroup.vue";
const props = defineProps({
items: {
type: Array,
required: true
}
});
const getCoverPhotos = (item) => item.flights.filter(f => !!f.coverPhoto).map(f => f.coverPhoto.thumbnailUrl);
</script>
<template>
<q-list bordered separator>
<q-item clickable v-ripple v-for="event of items" :key="event" :to="{name: 'eventDetail', params: {id: event.id }}">
<q-item-section>
<q-item-label>
<q-avatar icon="event" color="primary" text-color="white" v-if="event.flights.length === 0"/>
<AvatarsGroup :avatars="getCoverPhotos(event)" v-else/>
{{ event.name }}
</q-item-label>
<q-item-label lines="4" caption>
<article v-html="event.description"/>
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</template>
+42 -7
View File
@@ -1,5 +1,6 @@
<script setup> <script setup>
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue"; import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
import {randomInt} from "src/composables/random";
const props = defineProps({ const props = defineProps({
step: { step: {
@@ -10,27 +11,61 @@ const props = defineProps({
</script> </script>
<template> <template>
<q-card class="" v-if="step !== 'photos'"> <template v-if="step === 'basic'">
<div class="row">
<div class="col">
<q-card class="q-my-sm">
<q-card-section> <q-card-section>
<q-skeleton type="QInput" class="q-my-sm" v-for="i in 3" :key="i"/> <q-skeleton type="QInput" class="q-my-sm" v-for="i in 2" :key="i"/>
</q-card-section> </q-card-section>
</q-card> </q-card>
</div>
</div>
<template v-if="step === 'photos'"> <div class="row">
<div class="col col-12 col-md-6" v-for="i in 2" :key="i">
<q-card class="q-mr-md-sm q-my-sm">
<q-card-section>
<q-skeleton type="QInput" class="q-my-sm" v-for="i in 2" :key="i"/>
</q-card-section>
</q-card>
</div>
</div>
</template>
<q-card v-else-if="step === 'copilot'">
<q-card class="q-mr-md-sm q-my-sm">
<q-card-section>
<q-skeleton type="QInput" class="q-my-sm"/>
</q-card-section>
</q-card>
</q-card>
<template v-else-if="step === 'track'">
<q-card> <q-card>
<q-card-section> <q-card-section>
<q-skeleton type="QInput" class="q-my-sm" v-for="i in 2" :key="i"/>
<q-skeleton type="QInput" class="q-my-sm"/>
</q-card-section> </q-card-section>
</q-card> </q-card>
<BasicListSkeleton :items-count-min="2" :items-count-max="6"/>
</template>
<template v-else-if="step === 'photos'">
<q-card class="q-my-lg">
<q-card-section>
<q-skeleton type="QInput" class="q-my-sm"/>
</q-card-section>
</q-card>
<q-card v-for="i in randomInt(3, 10)" :key="i"> <q-card v-for="i in randomInt(3, 10)" :key="i">
<q-skeleton type="rect"/> <q-skeleton type="rect"/>
</q-card> </q-card>
</template> </template>
<div v-else-if="step === 'notes'">
<q-skeleton type="QInput" class="q-my-sm" v-for="i in 2" :key="i"/>
<q-skeleton type="rect" width="100%" height="500px"/>
</div>
</template> </template>
+21 -11
View File
@@ -1,35 +1,45 @@
<script setup> <script setup>
import {onMounted, ref} from "vue"; import {onMounted, reactive, ref, watch} from "vue";
import FormWrapper from "components/FormWrapper.vue"; import FormWrapper from "components/FormWrapper.vue";
import {setDefaultData} from "src/composables/form/formHandling"; import {setDefaultData} from "src/composables/form/formHandling";
import RichTextEditor from "components/inputs/RichTextEditor.vue"; import RichTextEditor from "components/inputs/RichTextEditor.vue";
import {commonProps} from "src/composables/inputs"; import {commonProps} from "src/composables/inputs";
import ComboboxInput from "components/inputs/ComboboxInput.vue";
import {isRequestPending} from "src/composables/objects/flight";
const props = defineProps({ const props = defineProps({
flight: { flight: {
type: Object, type: Object,
required: false required: false
}, },
events: {
type: Array,
required: false,
}
}); });
const formData = ref({ const formData = reactive({
name: "", name: "",
event: null,
description: "" description: ""
}) })
const isMounted = ref(false); watch(() => props.flight, (newVal) => {
onMounted(() => { if (newVal) {
if (props.flight) setDefaultData(formData, newVal);
setDefaultData(formData.value, props.flight);
isMounted.value = true; if (newVal.event) {
}) formData.event = {id: newVal.event.id, name: newVal.event.name};
}
}
}, {immediate: true});
</script> </script>
<template> <template>
<FormWrapper v-model="formData" v-if="isMounted"> <FormWrapper v-model="formData" :is-request-pending="isRequestPending">
<q-input type="text" v-model="formData.name" label="Jméno letu" v-bind="commonProps" /> <ComboboxInput :options="events" v-if="events" v-model="formData.event" label="Událost" v-bind="commonProps"/>
<RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..." /> <q-input type="text" v-model="formData.name" label="Jméno letu" v-bind="commonProps"/>
<RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..."/>
</FormWrapper> </FormWrapper>
</template> </template>
+4
View File
@@ -110,5 +110,9 @@ const filterFn = (val, update, abort) => {
</q-item-section> </q-item-section>
</q-item> </q-item>
</template> </template>
<template #append>
<slot name="append" />
</template>
</q-select> </q-select>
</template> </template>
+37
View File
@@ -0,0 +1,37 @@
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
import {useEventsStore} from "stores/Events";
import {ref} from "vue";
import {storeToRefs} from "pinia";
import {confirmObject} from "src/composables/dialog";
import {useAppStore} from "stores/App";
export const loading = ref(true);
export const event = ref(null);
export const fetchEvent = (eventId, username) => {
loading.value = true;
return useEventsStore().fetchDetail(parseInt(eventId), username)
.then(respEvent => {
event.value = respEvent;
return respEvent;
})
.finally(() => loading.value = false);
};
export const events = storeToRefs(useEventsStore()).items;
export const fetchEvents = (username = null) => {
loading.value = true;
useEventsStore().fetch(username).finally(() => {
loading.value = false;
});
};
export const handleDeleteEvent = ($q, eventId, onOkCallback = () => {
}) => {
$q.dialog(confirmObject(`Opravdu chceš smazat tento let?`))
.onOk(() => {
useEventsStore().delete(eventId).then(onOkCallback);
});
};
export const isRequestPending = storeToRefs(useEventsStore()).isRequestPending;
+19
View File
@@ -0,0 +1,19 @@
import gql from "graphql-tag";
export const FRAGMENT_EVENT_LIST_INFO = gql`
fragment eventListInfo on Event {
id
name
description
isPublic
}
`;
export const FRAGMENT_EVENT_FULL_INFO = gql`
fragment eventFullInfo on Event {
id
name
description
isPublic
}
`;
+6
View File
@@ -41,6 +41,12 @@ fragment flightFullInfo on Flight {
id id
name name
} }
event {
id
name
}
gpxTrackUrl gpxTrackUrl
gpxTrack { gpxTrack {
coordinates { coordinates {
+29
View File
@@ -0,0 +1,29 @@
import gql from "graphql-tag";
import {FRAGMENT_EVENT_FULL_INFO} from "src/graphql/fragments/event";
export const CREATE_EVENT = gql`
mutation createEvent($input: CreateEventInput!) {
createEvent(input: $input) {
...eventFullInfo
}
}
${FRAGMENT_EVENT_FULL_INFO}
`;
export const EDIT_EVENT = gql`
mutation editEvent($id: Int!, $input: EditEventInput!) {
editEvent(id: $id, input: $input) {
...eventFullInfo
}
}
${FRAGMENT_EVENT_FULL_INFO}
`;
export const DELETE_EVENT = gql`
mutation deleteEvent($id: Int!) {
deleteEvent(id: $id) {
id
}
}
`;
+34
View File
@@ -0,0 +1,34 @@
import gql from "graphql-tag";
import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
import {FRAGMENT_EVENT_FULL_INFO, FRAGMENT_EVENT_LIST_INFO} from "src/graphql/fragments/event";
export const EVENTS_LIST = gql`
query eventsList {
events {
...eventListInfo
flights {
coverPhoto {
thumbnailUrl
}
}
}
}
${FRAGMENT_EVENT_LIST_INFO}
`;
export const EVENT_DETAIL = gql`
query event($id: Int!) {
event(id: $id) {
...eventFullInfo
flights {
...flightListInfo
}
}
}
${FRAGMENT_EVENT_FULL_INFO}
${FRAGMENT_FLIGHT_LIST_INFO}
`;
+11 -2
View File
@@ -103,23 +103,32 @@ const linksList = [
to: {name: "myFlights"} to: {name: "myFlights"}
}, },
{separator: true}, {separator: true},
{
title: 'Události',
icon: 'event',
to: {name: "events"}
},
{ {
title: 'Letadla', title: 'Letadla',
icon: "connecting_airports", icon: "connecting_airports",
to: {name: "aircrafts"} to: {name: "aircrafts"}
}, },
{separator: true},
{ {
title: 'Zajímavá místa', title: 'Zajímavá místa',
icon: "place", icon: "place",
to: {name: "poi"} to: {name: "poi"}
}, },
{separator: true},
{ {
title: 'Spolucestující', title: 'Spolucestující',
icon: "people", icon: "people",
to: {name: "copilots"} to: {name: "copilots"}
}, },
{separator: true},
{
title: 'Organizace',
icon: "groups",
to: {name: "organizations"}
},
] ]
const leftDrawerOpen = ref(false) const leftDrawerOpen = ref(false)
+45
View File
@@ -0,0 +1,45 @@
<script setup>
import {onMounted} from "vue";
import {loading, event, fetchEvent, isRequestPending} from "src/composables/objects/event";
import {useAppStore} from "stores/App";
import {useRoute, useRouter} from "vue-router";
import {useEventsStore} from "stores/Events";
import {successObject} from "src/composables/dialog";
import {useQuasar} from "quasar";
import EventEditSkeleton from "components/event/EventEditSkeleton.vue";
import EventForm from "components/event/EventForm.vue";
const eventId = parseInt(useRoute().params.id);
onMounted(() => {
fetchEvent(eventId).then(() => {
useAppStore().setPageTitle(`${event.value.name} - editace`);
useAppStore().setBreadcrumbs([
{to: {name: "events"}, label: "Události"},
{to: {name: "eventDetail", params: {id: eventId}}, label: event.value.name},
{label: "Editace"}
])
})
});
const $q = useQuasar();
const $router = useRouter();
const handleSave = (formData) => {
useEventsStore().update(eventId, formData).then(() => {
$q.notify(successObject("Událost úspěšně upravena"));
$router.push({name: "eventDetail", params: {id: eventId}});
});
}
</script>
<template>
<EventEditSkeleton v-if="loading"/>
<EventForm
v-else
:event="event"
:is-request-pending="isRequestPending"
@save="handleSave"
/>
</template>
@@ -0,0 +1,38 @@
<script setup>
import {useRoute} from "vue-router";
import {onMounted} from "vue";
import {event, fetchEvent, loading} from "src/composables/objects/event";
import EventDetailSkeleton from "components/event/EventDetailSkeleton.vue";
import {useAppStore} from "stores/App";
import FAB from "components/FAB.vue";
import FlightsList from "components/flights/FlightsList.vue";
const eventId = parseInt(useRoute().params.id);
onMounted(() => {
fetchEvent(eventId).then(() => {
useAppStore().setPageTitle(event.value.name);
useAppStore().setBreadcrumbs([
{to: {name: "events"}, label: "Události"},
{label: event.value.name}
])
});
})
</script>
<template>
<EventDetailSkeleton v-if="loading || !event"/>
<template v-else>
<article v-html="event.description" />
<h3>Lety</h3>
<q-banner v-if="event.flights.length === 0" class="bg-light-blue text-white">
Zatím tu nemáš uložené žádné lety.
</q-banner>
<FlightsList :items="event.flights" v-else/>
<FAB mode="edit" :router-link="{name: 'eventEdit', params: {id: eventId}}"/>
</template>
</template>
+33
View File
@@ -0,0 +1,33 @@
<script setup>
import {onMounted} from "vue";
import {useAppStore} from "stores/App";
import FAB from "components/FAB.vue";
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
import {events, fetchEvents, loading} from "src/composables/objects/event";
import EventsList from "components/event/EventsList.vue";
onMounted(() => {
fetchEvents();
});
useAppStore().setPageTitle("Letecké události")
</script>
<template>
<BasicListSkeleton :items-count-min="3" :items-count-max="8" v-if="loading"/>
<BasicInfoBanner
v-else-if="!loading && events.length === 0"
:route-add="{name: 'addEvent'}"
label-add="Přidat"
>
Nemáš tu uloženou žádnou událost. Události slouží ke sloučení letů z jedné akce/výletu do přehledné skupiny.
</BasicInfoBanner>
<EventsList
v-else
:items="events"
/>
<FAB :router-link="{name: 'addEvent'}" mode="add"/>
</template>
+28
View File
@@ -0,0 +1,28 @@
<script setup>
import {useRouter} from "vue-router";
import {useQuasar} from "quasar";
import {successObject} from "src/composables/dialog";
import {useEventsStore} from "stores/Events";
import {useAppStore} from "stores/App";
import {isRequestPending} from "src/composables/objects/event";
import EventForm from "components/event/EventForm.vue";
const $q = useQuasar();
const router = useRouter();
const handleSave = (formData) => {
useEventsStore().create(formData).then(() => {
$q.notify(successObject("Událost úspěšně vytvořena"));
router.push({name: "events"});
});
};
useAppStore().setPageTitle("Nová událost");
useAppStore().setBreadcrumbs([
{to: {name: "events"}, label: "Události"},
{label: "Nová"}
]);
</script>
<template>
<EventForm @save="handleSave" :is-request-pending="isRequestPending"/>
</template>
+5 -3
View File
@@ -25,6 +25,7 @@ import BatchPhotoFormDialog from "components/photos/BatchPhotoFormDialog.vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import {editBatchPhotos, handleBatchPhotoEdit, openBatchEdit, openBatchPhotoForm} from "src/composables/photos/batchEdit"; import {editBatchPhotos, handleBatchPhotoEdit, openBatchEdit, openBatchPhotoForm} from "src/composables/photos/batchEdit";
import {editPhoto, handlePhotoUpdate, openEdit, openPhotoEdit} from "src/composables/photos/simpleEdit"; import {editPhoto, handlePhotoUpdate, openEdit, openPhotoEdit} from "src/composables/photos/simpleEdit";
import {fetchEvents, events} from "src/composables/objects/event";
const $q = useQuasar(); const $q = useQuasar();
const $route = useRoute(); const $route = useRoute();
@@ -35,6 +36,7 @@ const locations = storeToRefs(usePointsOfInterestStore()).items;
const airports = storeToRefs(useAirportsStore()).items; const airports = storeToRefs(useAirportsStore()).items;
const copilots = storeToRefs(useCopilotsStore()).items; const copilots = storeToRefs(useCopilotsStore()).items;
const formStep = ref(""); const formStep = ref("");
watch(() => $route.params.step, (val) => formStep.value = val); watch(() => $route.params.step, (val) => formStep.value = val);
@@ -46,6 +48,8 @@ onMounted(() => {
useAirportsStore().fetch(); useAirportsStore().fetch();
useCopilotsStore().fetch(); useCopilotsStore().fetch();
fetchEvents();
fetchFlight(flightId).then(() => { fetchFlight(flightId).then(() => {
const flightNameFormatted = flightName(flight.value); const flightNameFormatted = flightName(flight.value);
@@ -98,8 +102,6 @@ const deletePhoto = (id) => {
flight.value.photos.splice(_findPhotoIndex(data.id), 1); flight.value.photos.splice(_findPhotoIndex(data.id), 1);
}); });
} }
</script> </script>
<template> <template>
@@ -164,7 +166,7 @@ const deletePhoto = (id) => {
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="notes"> <q-tab-panel name="notes">
<FlightFormNotes @save="handleFlightEdit" :flight="flight"/> <FlightFormNotes @save="handleFlightEdit" :events="events" :flight="flight"/>
</q-tab-panel> </q-tab-panel>
</q-tab-panels> </q-tab-panels>
@@ -16,7 +16,6 @@ import {storeToRefs} from "pinia";
const $q = useQuasar(); const $q = useQuasar();
const $router = useRouter(); const $router = useRouter();
const flightId = parseInt(useRoute().params.id); const flightId = parseInt(useRoute().params.id);
const loggedUser = storeToRefs(useUserStore()).user; const loggedUser = storeToRefs(useUserStore()).user;
onMounted(() => { onMounted(() => {
@@ -0,0 +1,7 @@
<script setup>
</script>
<template>
organizace
</template>
+6
View File
@@ -40,6 +40,12 @@ const routes = [
{path: 'aircrafts/new', name: "addAircraft", component: () => import('pages/profile/aircraft/NewAircraftsPage.vue')}, {path: 'aircrafts/new', name: "addAircraft", component: () => import('pages/profile/aircraft/NewAircraftsPage.vue')},
{path: 'aircrafts/:id', name: "aircraftDetail", component: () => import('pages/profile/aircraft/AircraftDetailPage.vue')}, {path: 'aircrafts/:id', name: "aircraftDetail", component: () => import('pages/profile/aircraft/AircraftDetailPage.vue')},
{path: 'aircrafts/:id/edit', name: "aircraftEdit", component: () => import('pages/profile/aircraft/EditAircraftPage.vue')}, {path: 'aircrafts/:id/edit', name: "aircraftEdit", component: () => import('pages/profile/aircraft/EditAircraftPage.vue')},
{path: 'events', name: "events", component: () => import('pages/profile/event/EventsPage.vue')},
{path: 'events/add', name: "addEvent", component: () => import('pages/profile/event/NewEventPage.vue')},
{path: 'events/:id', name: "eventDetail", component: () => import('pages/profile/event/EventDetailPage.vue')},
{path: 'events/:id/edit', name: "eventEdit", component: () => import('pages/profile/event/EditEventPage.vue')},
{path: 'organizations', name: "organizations", component: () => import('pages/profile/organization/OrganizationsPage.vue')},
] ]
}, },
+41
View File
@@ -0,0 +1,41 @@
import {defineStore} from 'pinia'
import {apolloWrapper} from "src/graphql/apollo";
import {CREATE_EVENT, EDIT_EVENT} from "src/graphql/mutations/event";
import {updateInItems} from "stores/helpers";
import {EVENT_DETAIL, EVENTS_LIST} from "src/graphql/queries/event";
export const useEventsStore = defineStore('SecondPilots', {
state: () => ({
items: [],
isMutationRunning: false,
}),
actions: {
fetchDetail(eventId) {
return apolloWrapper.query(EVENT_DETAIL, {id: eventId}).then(({data}) => data.event);
},
fetch() {
return apolloWrapper.query(EVENTS_LIST).then(result => {
this.items = result.data.events;
return result.data.events;
});
},
create(formData) {
delete formData['id'];
this.isMutationRunning = true;
return apolloWrapper.mutate(CREATE_EVENT, {input: formData}).then(({data}) => {
const event = data.createEvent;
this.items.push(event);
return event;
}).finally(() => this.isMutationRunning = false);
},
update(id, formData) {
return apolloWrapper.mutate(EDIT_EVENT, {id: id, input: formData}).then(({data}) => {
const event = data.editEvent;
updateInItems(this.items, "id", event);
return event;
}).finally(() => this.isMutationRunning = false);
},
}
})