Many improvements. Still no reactivity on List

This commit is contained in:
2024-06-02 08:48:14 -04:00
parent 387af2e6ce
commit 9104ccab0f
16 changed files with 395 additions and 289 deletions

View File

@@ -37,13 +37,13 @@ const AppwriteIds = process.env.DEV
? { ? {
databaseId: '65ee1cbf9c2493faf15f', databaseId: '65ee1cbf9c2493faf15f',
collection: { collection: {
boat: '66341910003e287cd71c', boat: 'boat',
reservation: '663f8847000b8f5e29bb', reservation: 'reservation',
skillTags: '66072582a74d94a4bd01', skillTags: 'skillTags',
task: '65ee1cd5b550023fae4f', task: 'task',
taskTags: '65ee21d72d5c8007c34c', taskTags: 'taskTags',
interval: '66361869002883fb4c4b', interval: 'interval',
intervalTemplate: '66361f480007fdd639af', intervalTemplate: 'intervalTemplate',
}, },
function: { function: {
userinfo: 'userinfo', userinfo: 'userinfo',

View File

@@ -166,7 +166,7 @@ watch(reservation, (newReservation) => {
interval: { interval: {
start: newReservation.start, start: newReservation.start,
end: newReservation.end, end: newReservation.end,
boatId: newReservation.resource, resource: newReservation.resource,
}, },
}; };
bookingForm.value = updatedReservation; bookingForm.value = updatedReservation;
@@ -190,7 +190,7 @@ const bookingName = computed(() =>
); );
const boat = computed((): Boat | null => { const boat = computed((): Boat | null => {
const boatId = bookingForm.value.interval?.boatId; const boatId = bookingForm.value.interval?.resource;
console.log('Boat Lookup:', boatId); console.log('Boat Lookup:', boatId);
return boatStore.getBoatById(boatId); return boatStore.getBoatById(boatId);
}); });
@@ -203,18 +203,18 @@ const onReset = () => {
interval: { interval: {
start: reservation.value.start, start: reservation.value.start,
end: reservation.value.end, end: reservation.value.end,
boatId: reservation.value.resource, resource: reservation.value.resource,
}, },
} }
: { ...newForm }; : { ...newForm };
}; };
const onSubmit = () => { const onSubmit = async () => {
const booking = bookingForm.value; const booking = bookingForm.value;
if ( if (
!( !(
booking.interval && booking.interval &&
booking.interval.boatId && booking.interval.resource &&
booking.interval.start && booking.interval.start &&
booking.interval.end && booking.interval.end &&
auth.currentUser auth.currentUser
@@ -224,7 +224,7 @@ const onSubmit = () => {
return; return;
} }
const reservation = <Reservation>{ const reservation = <Reservation>{
resource: booking.interval.boatId, resource: booking.interval.resource,
start: booking.interval.start, start: booking.interval.start,
end: booking.interval.end, end: booking.interval.end,
user: auth.currentUser.$id, user: auth.currentUser.$id,
@@ -232,15 +232,34 @@ const onSubmit = () => {
reason: booking.reason, reason: booking.reason,
comment: booking.comment, comment: booking.comment,
}; };
// TODO: Fix this. It will always look successful const status = $q.notify({
reservationStore.createReservation(reservation); // Probably should pass the notify as a callback to the reservation creation. color: 'secondary',
$q.notify({
color: 'green-4',
textColor: 'white', textColor: 'white',
icon: 'cloud_done', message: 'Submitting Reservation',
message: 'Submitted', spinner: true,
timeout: 3000, closeBtn: 'Dismiss',
position: 'top',
timeout: 0,
group: false,
}); });
try {
const r = await reservationStore.createReservation(reservation);
status({
color: 'positive',
icon: 'cloud_done',
message: `Booking successful: ${
boatStore.getBoatById(r.resource)?.name
} at ${formatDate(r.start)}`,
spinner: false,
});
} catch (e) {
status({
color: 'negative',
icon: 'error',
spinner: false,
message: 'Failed to book!' + e,
});
}
router.go(-1); router.go(-1);
}; };
</script> </script>

View File

@@ -21,7 +21,11 @@
<q-icon :name="link.icon" /> <q-icon :name="link.icon" />
</q-item-section> </q-item-section>
<q-item-section>{{ link.name }}</q-item-section> <q-item-section>
<span :class="link.color ? `text-${link.color}` : ''">
{{ link.name }}
</span>
</q-item-section>
</q-item> </q-item>
<q-list v-if="link.sublinks"> <q-list v-if="link.sublinks">
<div <div
@@ -36,7 +40,11 @@
<q-icon :name="sublink.icon" /> <q-icon :name="sublink.icon" />
</q-item-section> </q-item-section>
<q-item-section>{{ sublink.name }}</q-item-section> <q-item-section>
<span :class="sublink.color ? `text-${sublink.color}` : ''">
{{ sublink.name }}
</span>
</q-item-section>
</q-item> </q-item>
</div> </div>
</q-list> </q-list>

View File

@@ -3,8 +3,7 @@
expand-icon-toggle expand-icon-toggle
draggable="true" draggable="true"
@dragstart="onDragStart($event, template)" @dragstart="onDragStart($event, template)"
v-model="expanded" v-model="expanded">
>
<template v-slot:header> <template v-slot:header>
<q-item-section> <q-item-section>
<q-input <q-input
@@ -12,17 +11,21 @@
:borderless="!edit" :borderless="!edit"
dense dense
v-model="template.name" v-model="template.name"
v-if="edit" v-if="edit" />
/><q-item-label v-if="!edit" class="cursor-pointer">{{ <q-item-label
template.name v-if="!edit"
}}</q-item-label></q-item-section class="cursor-pointer">
> {{ template.name }}
</q-item-label>
</q-item-section>
</template> </template>
<q-card flat> <q-card flat>
<q-card-section horizontal> <q-card-section horizontal>
<q-card-section class="q-pt-xs"> <q-card-section class="q-pt-xs">
<q-list dense> <q-list dense>
<q-item v-for="(item, index) in template.timeTuples" :key="item[0]"> <q-item
v-for="(item, index) in template.timeTuples"
:key="item[0]">
<q-input <q-input
class="q-mx-sm" class="q-mx-sm"
dense dense
@@ -38,8 +41,7 @@
type="time" type="time"
label="End" label="End"
:borderless="!edit" :borderless="!edit"
:readonly="!edit" :readonly="!edit">
>
<template v-slot:after> <template v-slot:after>
<q-btn <q-btn
v-if="edit" v-if="edit"
@@ -47,46 +49,44 @@
dense dense
flat flat
icon="delete" icon="delete"
@click="template.timeTuples.splice(index, 1)" @click="template.timeTuples.splice(index, 1)" />
/> </template></q-input></q-item </template>
></q-list> </q-input>
</q-item>
</q-list>
<q-btn <q-btn
v-if="edit" v-if="edit"
dense dense
color="primary" color="primary"
size="sm" size="sm"
label="Add interval" label="Add interval"
@click="template.timeTuples.push(['00:00', '00:00'])" @click="template.timeTuples.push(['00:00', '00:00'])" />
/></q-card-section> </q-card-section>
<q-card-actions vertical> <q-card-actions vertical>
<q-btn <q-btn
v-if="!edit" v-if="!edit"
color="primary" color="primary"
icon="edit" icon="edit"
label="Edit" label="Edit"
@click="toggleEdit" @click="toggleEdit" />
/>
<q-btn <q-btn
v-if="edit" v-if="edit"
color="primary" color="primary"
icon="save" icon="save"
label="Save" label="Save"
@click="saveTemplate($event, template)" @click="saveTemplate($event, template)" />
/>
<q-btn <q-btn
v-if="edit" v-if="edit"
color="secondary" color="secondary"
icon="cancel" icon="cancel"
label="Cancel" label="Cancel"
@click="revert" @click="revert" />
/>
<q-btn <q-btn
color="negative" color="negative"
icon="delete" icon="delete"
label="Delete" label="Delete"
v-if="template.$id !== ''" v-if="template.$id !== ''"
@click="deleteTemplate($event, template)" @click="deleteTemplate($event, template)" />
/>
</q-card-actions> </q-card-actions>
</q-card-section> </q-card-section>
</q-card> </q-card>
@@ -101,25 +101,29 @@
square square
icon="schedule" icon="schedule"
v-for="item in overlapped" v-for="item in overlapped"
:key="item.start" :key="item.start">
> {{ item.start }}-{{ item.end }}
{{ item.start }}-{{ item.end }}</q-chip </q-chip>
>
</q-card-section> </q-card-section>
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn flat label="OK" color="primary" v-close-popup /> <q-btn
</q-card-actions> </q-card flat
></q-dialog> label="OK"
color="primary"
v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useScheduleStore } from 'src/stores/schedule'; import { useIntervalTemplateStore } from 'src/stores/intervalTemplate';
import { IntervalTemplate } from 'src/stores/schedule.types'; import { IntervalTemplate } from 'src/stores/schedule.types';
import { copyIntervalTemplate, timeTuplesOverlapped } from 'src/utils/schedule'; import { copyIntervalTemplate, timeTuplesOverlapped } from 'src/utils/schedule';
import { ref } from 'vue'; import { ref } from 'vue';
const alert = ref(false); const alert = ref(false);
const overlapped = ref(); const overlapped = ref();
const scheduleStore = useScheduleStore(); const intervalTemplateStore = useIntervalTemplateStore();
const props = defineProps<{ edit?: boolean; modelValue: IntervalTemplate }>(); const props = defineProps<{ edit?: boolean; modelValue: IntervalTemplate }>();
const edit = ref(props.edit); const edit = ref(props.edit);
const expanded = ref(props.edit); const expanded = ref(props.edit);
@@ -141,7 +145,7 @@ const deleteTemplate = (
event: Event, event: Event,
template: IntervalTemplate | undefined template: IntervalTemplate | undefined
) => { ) => {
if (template?.$id) scheduleStore.deleteIntervalTemplate(template.$id); if (template?.$id) intervalTemplateStore.deleteIntervalTemplate(template.$id);
}; };
function onDragStart(e: DragEvent, template: IntervalTemplate) { function onDragStart(e: DragEvent, template: IntervalTemplate) {
@@ -159,9 +163,9 @@ const saveTemplate = (evt: Event, template: IntervalTemplate | undefined) => {
} else { } else {
edit.value = false; edit.value = false;
if (template.$id && template.$id !== 'unsaved') { if (template.$id && template.$id !== 'unsaved') {
scheduleStore.updateIntervalTemplate(template, template.$id); intervalTemplateStore.updateIntervalTemplate(template, template.$id);
} else { } else {
scheduleStore.createIntervalTemplate(template); intervalTemplateStore.createIntervalTemplate(template);
emit('saved'); emit('saved');
} }
} }

View File

@@ -35,7 +35,10 @@
<template #day-body="{ scope }"> <template #day-body="{ scope }">
<div <div
v-for="block in getBoatBlocks(scope)" v-for="block in getAvailableIntervals(
scope.timestamp,
boats[scope.columnIndex]
)"
:key="block.$id"> :key="block.$id">
<div <div
class="timeblock" class="timeblock"
@@ -95,23 +98,24 @@ import CalendarHeaderComponent from './CalendarHeaderComponent.vue';
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useBoatStore } from 'src/stores/boat'; import { useBoatStore } from 'src/stores/boat';
import { useScheduleStore } from 'src/stores/schedule';
import { useAuthStore } from 'src/stores/auth'; import { useAuthStore } from 'src/stores/auth';
import { Interval, Reservation } from 'src/stores/schedule.types'; import { Interval, Reservation } from 'src/stores/schedule.types';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useReservationStore } from 'src/stores/reservation'; import { useReservationStore } from 'src/stores/reservation';
import { useIntervalTemplateStore } from 'src/stores/intervalTemplate';
import { useIntervalStore } from 'src/stores/interval';
const scheduleStore = useScheduleStore(); const intervalTemplateStore = useIntervalTemplateStore();
const reservationStore = useReservationStore(); const reservationStore = useReservationStore();
const { boats } = storeToRefs(useBoatStore()); const { boats } = storeToRefs(useBoatStore());
const selectedBlock = defineModel<Interval | null>(); const selectedBlock = defineModel<Interval | null>();
const selectedDate = ref(today()); const selectedDate = ref(today());
const { getAvailableIntervals } = useIntervalStore();
const calendar = ref<QCalendarDay | null>(null); const calendar = ref<QCalendarDay | null>(null);
onMounted(async () => { onMounted(async () => {
await useBoatStore().fetchBoats(); await useBoatStore().fetchBoats();
await scheduleStore.fetchIntervalTemplates(); await intervalTemplateStore.fetchIntervalTemplates();
}); });
function handleSwipe({ ...event }) { function handleSwipe({ ...event }) {
@@ -194,23 +198,6 @@ function selectBlock(event: MouseEvent, scope: DayBodyScope, block: Interval) {
: (selectedBlock.value = block); : (selectedBlock.value = block);
} }
const boatBlocks = computed((): Record<string, Interval[]> => {
return scheduleStore
.getIntervals(selectedDate.value)
.reduce((result, interval) => {
if (!result[interval.boatId]) result[interval.boatId] = [];
if (
!reservationStore.isResourceTimeOverlapped(
interval.boatId,
new Date(interval.start),
new Date(interval.end)
)
)
result[interval.boatId].push(interval);
return result;
}, <Record<string, Interval[]>>{});
});
const boatReservations = computed((): Record<string, Reservation[]> => { const boatReservations = computed((): Record<string, Reservation[]> => {
return reservationStore return reservationStore
.getReservationsByDate(selectedDate.value) .getReservationsByDate(selectedDate.value)
@@ -220,11 +207,6 @@ const boatReservations = computed((): Record<string, Reservation[]> => {
return result; return result;
}, <Record<string, Reservation[]>>{}); }, <Record<string, Reservation[]>>{});
}); });
function getBoatBlocks(scope: DayBodyScope): Interval[] {
const boat = boats.value[scope.columnIndex];
return boat ? boatBlocks.value[boat.$id] : [];
}
function getBoatReservations(scope: DayBodyScope): Reservation[] { function getBoatReservations(scope: DayBodyScope): Reservation[] {
const boat = boats.value[scope.columnIndex]; const boat = boats.value[scope.columnIndex];
return boat ? boatReservations.value[boat.$id] : []; return boat ? boatReservations.value[boat.$id] : [];

View File

@@ -1,6 +1,6 @@
<template> <template>
<q-page padding> <q-page>
<q-card class="subcontent"> <q-card class="row">
<navigation-bar <navigation-bar
@today="onToday" @today="onToday"
@prev="onPrev" @prev="onPrev"
@@ -13,8 +13,10 @@
resource-label="displayName" resource-label="displayName"
:weekdays="[1, 2, 3, 4, 5, 6, 0]" :weekdays="[1, 2, 3, 4, 5, 6, 0]"
:view="$q.screen.gt.md ? 'week' : 'day'" :view="$q.screen.gt.md ? 'week' : 'day'"
bordered v-touch-swipe.mouse.left.right="handleSwipe"
:max-days="$q.screen.lt.sm ? 3 : 7"
animated animated
style="--calendar-resources-width: 2em"
day-min-height="50px" day-min-height="50px"
@change="onChange" @change="onChange"
@moved="onMoved" @moved="onMoved"
@@ -24,19 +26,26 @@
@click-head-day="onClickHeadDay"> @click-head-day="onClickHeadDay">
<template #day="{ scope }"> <template #day="{ scope }">
<div <div
v-for="event in boatReservationEvents(scope)" v-for="interval in getSortedIntervals(
:key="event.id"> scope.timestamp,
<div scope.resource
v-if="event.start !== undefined" )"
class="booking-event"> :key="interval.$id"
<span class="title q-calendar__ellipsis"> class="row q-pb-xs">
{{ useAuthStore().getUserNameById(event.user) }} @ <q-badge
{{ renderTime(event.start) }} multi-line
<q-tooltip> class="col-12"
{{ renderTime(event.start) + ' - ' + renderTime(event.end) }} :transparent="interval.user != undefined"
</q-tooltip> :color="interval.user ? 'secondary' : 'primary'"
</span> :id="interval.id">
</div> {{
interval.user
? useAuthStore().getUserNameById(interval.user)
: 'Available'
}}
<br />
{{ formatTime(interval.start) }} - {{ formatTime(interval.end) }}
</q-badge>
</div> </div>
</template> </template>
</q-calendar-scheduler> </q-calendar-scheduler>
@@ -46,7 +55,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useReservationStore } from 'src/stores/reservation'; import { useReservationStore } from 'src/stores/reservation';
import { onMounted, ref } from 'vue'; import { ref } from 'vue';
import { useAuthStore } from 'src/stores/auth'; import { useAuthStore } from 'src/stores/auth';
const reservationStore = useReservationStore(); const reservationStore = useReservationStore();
@@ -56,27 +65,31 @@ import { Timestamp } from '@quasar/quasar-ui-qcalendar';
import { Boat, useBoatStore } from 'src/stores/boat'; import { Boat, useBoatStore } from 'src/stores/boat';
import NavigationBar from 'src/components/scheduling/NavigationBar.vue'; import NavigationBar from 'src/components/scheduling/NavigationBar.vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { formatTime } from 'src/utils/schedule';
import { useIntervalStore } from 'src/stores/interval';
import { Interval } from 'src/stores/schedule.types';
const selectedDate = ref(today()); const selectedDate = ref(today());
const boatStore = useBoatStore(); const boatStore = useBoatStore();
const calendar = ref(); const calendar = ref();
const $q = useQuasar(); const $q = useQuasar();
const { getAvailableIntervals } = useIntervalStore();
interface DayScope { // interface DayScope {
timestamp: Timestamp; // timestamp: Timestamp;
columnIndex: number; // columnIndex: number;
resource: object; // resource: object;
resourceIndex: number; // resourceIndex: number;
indentLevel: number; // indentLevel: number;
activeDate: boolean; // activeDate: boolean;
droppable: boolean; // droppable: boolean;
} // }
const renderTime = (dateString: string) => { const getSortedIntervals = (timestamp: Timestamp, boat?: Boat): Interval[] => {
const date = new Date(dateString); return getAvailableIntervals(timestamp, boat)
return date.toLocaleTimeString(); .concat(boatReservationEvents(timestamp, boat))
.sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
}; };
onMounted(() => boatStore.fetchBoats());
// Method declarations // Method declarations
// function slotStyle( // function slotStyle(
@@ -98,7 +111,14 @@ onMounted(() => boatStore.fetchBoats());
// return s; // return s;
// } // }
function boatReservationEvents({ timestamp, resource }: DayScope) { function handleSwipe({ ...event }) {
event.direction === 'right' ? calendar.value?.prev() : calendar.value?.next();
}
function boatReservationEvents(
timestamp: Timestamp,
resource: Boat | undefined
) {
if (!resource) return [];
return reservationStore.getReservationsByDate( return reservationStore.getReservationsByDate(
getDate(timestamp), getDate(timestamp),
(resource as Boat).$id (resource as Boat).$id
@@ -132,25 +152,3 @@ function onNext() {
calendar.value.next(); calendar.value.next();
} }
</script> </script>
<style lang="sass" scoped>
.booking-event
position: absolute
font-size: 12px
justify-content: space-evenly
margin: 0 1px
text-overflow: ellipsis
overflow: hidden
color: white
max-width: 100%
background: #027BE3FF
cursor: pointer
.title
position: relative
display: flex
justify-content: center
align-items: center
height: 100%
</style>

View File

@@ -22,7 +22,7 @@
class="q-pa-none"> class="q-pa-none">
<q-card <q-card
clas="q-ma-md" clas="q-ma-md"
v-if="!reservationStore.futureUserReservations.length"> v-if="!futureUserReservations.length">
<q-card-section> <q-card-section>
<div class="text-h6">You don't have any upcoming bookings!</div> <div class="text-h6">You don't have any upcoming bookings!</div>
<div class="text-h8">Why don't you go make one?</div> <div class="text-h8">Why don't you go make one?</div>
@@ -41,7 +41,7 @@
</q-card> </q-card>
<div v-else> <div v-else>
<div <div
v-for="reservation in reservationStore.futureUserReservations" v-for="reservation in sortedUserReservations"
:key="reservation.$id"> :key="reservation.$id">
<ReservationCardComponent :modelValue="reservation" /> <ReservationCardComponent :modelValue="reservation" />
</div> </div>
@@ -51,7 +51,7 @@
name="past" name="past"
class="q-pa-none"> class="q-pa-none">
<div <div
v-for="reservation in reservationStore.pastUserReservations" v-for="reservation in pastUserReservations"
:key="reservation.$id"> :key="reservation.$id">
<ReservationCardComponent :modelValue="reservation" /> <ReservationCardComponent :modelValue="reservation" />
</div> </div>
@@ -63,7 +63,9 @@ import { useReservationStore } from 'src/stores/reservation';
import ReservationCardComponent from 'src/components/scheduling/ReservationCardComponent.vue'; import ReservationCardComponent from 'src/components/scheduling/ReservationCardComponent.vue';
import { ref } from 'vue'; import { ref } from 'vue';
const reservationStore = useReservationStore(); const { sortedUserReservations, futureUserReservations, pastUserReservations } =
useReservationStore();
const tab = ref('upcoming'); const tab = ref('upcoming');
// const showMarker = ( // const showMarker = (

View File

@@ -1,8 +1,13 @@
<template> <template>
<div class="fit row wrap justify-start items-start content-start"> <div class="fit row wrap justify-start items-start content-start">
<div class="q-pa-md"> <div class="q-pa-md">
<div class="scheduler" style="max-width: 1200px"> <div
<NavigationBar @next="onNext" @today="onToday" @prev="onPrev" /> class="scheduler"
style="max-width: 1200px">
<NavigationBar
@next="onNext"
@today="onToday"
@prev="onPrev" />
<q-calendar-scheduler <q-calendar-scheduler
ref="calendar" ref="calendar"
v-model="selectedDate" v-model="selectedDate"
@@ -18,8 +23,7 @@
:drag-leave-func="onDragLeave" :drag-leave-func="onDragLeave"
:drop-func="onDrop" :drop-func="onDrop"
day-min-height="50px" day-min-height="50px"
cell-width="150px" cell-width="150px">
>
<template #day="{ scope }"> <template #day="{ scope }">
<div <div
v-if="filteredIntervals(scope.timestamp, scope.resource).length" v-if="filteredIntervals(scope.timestamp, scope.resource).length"
@@ -29,15 +33,13 @@
justify-content: space-evenly; justify-content: space-evenly;
align-items: center; align-items: center;
font-size: 12px; font-size: 12px;
" ">
>
<template <template
v-for="block in sortedIntervals( v-for="block in sortedIntervals(
scope.timestamp, scope.timestamp,
scope.resource scope.resource
)" )"
:key="block.id" :key="block.id">
>
<q-chip class="cursor-pointer"> <q-chip class="cursor-pointer">
{{ date.formatDate(block.start, 'HH:mm') }} - {{ date.formatDate(block.start, 'HH:mm') }} -
{{ date.formatDate(block.end, 'HH:mm') }} {{ date.formatDate(block.end, 'HH:mm') }}
@@ -77,46 +79,53 @@
).toISOString()) ).toISOString())
" "
/> />
</q-popup-edit>--> </q-chip </q-popup-edit>-->
><q-btn </q-chip>
<q-btn
size="xs" size="xs"
icon="delete" icon="delete"
round round
@click="deleteBlock(block)" @click="deleteBlock(block)" />
/>
</template> </template>
</div> </div>
</template> </template>
</q-calendar-scheduler> </q-calendar-scheduler>
</div> </div>
</div> </div>
<div class="q-pa-md" style="width: 400px"> <div
<q-list padding bordered class="rounded-borders"> class="q-pa-md"
style="width: 400px">
<q-list
padding
bordered
class="rounded-borders">
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label overline>Availability Templates</q-item-label> <q-item-label overline>Availability Templates</q-item-label>
<q-item-label caption <q-item-label caption>
>Drag and drop a template to a boat / date to create booking Drag and drop a template to a boat / date to create booking
availability</q-item-label availability
> </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn label="Add Template" color="primary" @click="createTemplate" /> <q-btn
label="Add Template"
color="primary"
@click="createTemplate" />
</q-card-actions> </q-card-actions>
<q-item v-if="newTemplate.$id === 'unsaved'" <q-item v-if="newTemplate.$id === 'unsaved'">
><IntervalTemplateComponent <IntervalTemplateComponent
:model-value="newTemplate" :model-value="newTemplate"
:edit="true" :edit="true"
@cancel="resetNewTemplate" @cancel="resetNewTemplate"
@saved="resetNewTemplate" @saved="resetNewTemplate" />
/></q-item> </q-item>
<q-separator spaced /> <q-separator spaced />
<IntervalTemplateComponent <IntervalTemplateComponent
v-for="template in intervalTemplates" v-for="template in intervalTemplates"
:key="template.$id" :key="template.$id"
:model-value="template" :model-value="template" />
/>
</q-list> </q-list>
</div> </div>
</div> </div>
@@ -127,16 +136,23 @@
</q-card-section> </q-card-section>
<q-card-section class="q-pt-none"> <q-card-section class="q-pt-none">
Conflicting times! Please delete overlapped items! Conflicting times! Please delete overlapped items!
<q-chip v-for="item in overlapped" :key="item.index" <q-chip
>{{ boats.find((b) => b.$id === item.boatId)?.name }}: v-for="item in overlapped"
:key="item.index">
{{ boats.find((b) => b.$id === item.boatId)?.name }}:
{{ date.formatDate(item.start, 'hh:mm') }} - {{ date.formatDate(item.start, 'hh:mm') }} -
{{ date.formatDate(item.end, 'hh:mm') }} {{ date.formatDate(item.end, 'hh:mm') }}
</q-chip> </q-chip>
</q-card-section> </q-card-section>
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn flat label="OK" color="primary" v-close-popup /> <q-btn
</q-card-actions> </q-card flat
></q-dialog> label="OK"
color="primary"
v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -146,7 +162,7 @@ import {
today, today,
} from '@quasar/quasar-ui-qcalendar'; } from '@quasar/quasar-ui-qcalendar';
import { Boat, useBoatStore } from 'src/stores/boat'; import { Boat, useBoatStore } from 'src/stores/boat';
import { useScheduleStore } from 'src/stores/schedule'; import { useIntervalStore } from 'src/stores/interval';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import type { import type {
Interval, Interval,
@@ -158,12 +174,14 @@ import IntervalTemplateComponent from 'src/components/scheduling/IntervalTemplat
import NavigationBar from 'src/components/scheduling/NavigationBar.vue'; import NavigationBar from 'src/components/scheduling/NavigationBar.vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { buildInterval, intervalsOverlapped } from 'src/utils/schedule'; import { buildInterval, intervalsOverlapped } from 'src/utils/schedule';
import { useIntervalTemplateStore } from 'src/stores/intervalTemplate';
const selectedDate = ref(today()); const selectedDate = ref(today());
const { fetchBoats } = useBoatStore(); const { fetchBoats } = useBoatStore();
const scheduleStore = useScheduleStore(); const intervalStore = useIntervalStore();
const intervalTemplateStore = useIntervalTemplateStore();
const { boats } = storeToRefs(useBoatStore()); const { boats } = storeToRefs(useBoatStore());
const intervalTemplates = scheduleStore.getIntervalTemplates(); const intervalTemplates = intervalTemplateStore.getIntervalTemplates();
const calendar = ref(); const calendar = ref();
const overlapped = ref(); const overlapped = ref();
const alert = ref(false); const alert = ref(false);
@@ -182,11 +200,11 @@ const newTemplate = ref<IntervalTemplate>({
onMounted(async () => { onMounted(async () => {
await fetchBoats(); await fetchBoats();
await scheduleStore.fetchIntervalTemplates(); await intervalTemplateStore.fetchIntervalTemplates();
}); });
const filteredIntervals = (date: Timestamp, boat: Boat) => { const filteredIntervals = (date: Timestamp, boat: Boat) => {
return scheduleStore.getIntervals(date, boat); return intervalStore.getIntervals(date, boat);
}; };
const sortedIntervals = (date: Timestamp, boat: Boat) => { const sortedIntervals = (date: Timestamp, boat: Boat) => {
@@ -207,11 +225,11 @@ function createTemplate() {
} }
function createIntervals(boat: Boat, templateId: string, date: string) { function createIntervals(boat: Boat, templateId: string, date: string) {
const intervals = intervalsFromTemplate(boat, templateId, date); const intervals = intervalsFromTemplate(boat, templateId, date);
intervals.forEach((interval) => scheduleStore.createInterval(interval)); intervals.forEach((interval) => intervalStore.createInterval(interval));
} }
function getIntervals(date: Timestamp, boat: Boat) { function getIntervals(date: Timestamp, boat: Boat) {
return scheduleStore.getIntervals(date, boat); return intervalStore.getIntervals(date, boat);
} }
function intervalsFromTemplate( function intervalsFromTemplate(
@@ -219,7 +237,7 @@ function intervalsFromTemplate(
templateId: string, templateId: string,
date: string date: string
): Interval[] { ): Interval[] {
const template = scheduleStore const template = intervalTemplateStore
.getIntervalTemplates() .getIntervalTemplates()
.value.find((t) => t.$id === templateId); .value.find((t) => t.$id === templateId);
return template return template
@@ -231,7 +249,7 @@ function intervalsFromTemplate(
function deleteBlock(block: Interval) { function deleteBlock(block: Interval) {
if (block.$id) { if (block.$id) {
scheduleStore.deleteInterval(block.$id); intervalStore.deleteInterval(block.$id);
} }
} }

View File

@@ -1,16 +1,17 @@
<template> <template>
<q-page padding> <q-page padding>
<q-item v-for="link in navlinks" :key="link.name"> <q-item
v-for="link in navlinks"
:key="link.name">
<q-btn <q-btn
:icon="link.icon" :icon="link.icon"
color="primary" :color="link.color ? link.color : 'primary'"
size="1.25em" size="1.25em"
:to="link.to" :to="link.to"
:label="link.name" :label="link.name"
rounded rounded
class="full-width" class="full-width"
align="left" align="left" />
/>
</q-item> </q-item>
</q-page> </q-page>
</template> </template>

View File

@@ -1,6 +1,17 @@
import { useAuthStore } from 'src/stores/auth'; import { useAuthStore } from 'src/stores/auth';
export const links = [ export type Link = {
name: string;
to: string;
icon: string;
front_links?: boolean;
enabled?: boolean;
color?: string;
sublinks?: Link[];
requiredRoles?: string[];
};
export const links = <Link[]>[
{ {
name: 'Home', name: 'Home',
to: '/', to: '/',
@@ -30,7 +41,7 @@ export const links = [
enabled: true, enabled: true,
sublinks: [ sublinks: [
{ {
name: 'List', name: 'My View',
to: '/schedule/list', to: '/schedule/list',
icon: 'list', icon: 'list',
front_links: false, front_links: false,
@@ -44,7 +55,7 @@ export const links = [
enabled: true, enabled: true,
}, },
{ {
name: 'View', name: 'Calendar',
to: '/schedule/view', to: '/schedule/view',
icon: 'calendar_month', icon: 'calendar_month',
front_links: false, front_links: false,
@@ -56,6 +67,7 @@ export const links = [
icon: 'edit_calendar', icon: 'edit_calendar',
front_links: false, front_links: false,
enabled: true, enabled: true,
color: 'accent',
requiredRoles: ['Schedule Admins'], requiredRoles: ['Schedule Admins'],
}, },
], ],

View File

@@ -1,18 +1,18 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { Ref, computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { Boat } from './boat'; import { Boat } from './boat';
import { Timestamp } from '@quasar/quasar-ui-qcalendar'; import { Timestamp } from '@quasar/quasar-ui-qcalendar';
import { IntervalTemplate, Interval, IntervalRecord } from './schedule.types'; import { Interval, IntervalRecord } from './schedule.types';
import { AppwriteIds, databases } from 'src/boot/appwrite'; import { AppwriteIds, databases } from 'src/boot/appwrite';
import { ID, Models, Query } from 'appwrite'; import { ID, Query } from 'appwrite';
import { arrayToTimeTuples } from 'src/utils/schedule'; import { useReservationStore } from './reservation';
export const useScheduleStore = defineStore('schedule', () => { export const useIntervalStore = defineStore('interval', () => {
// TODO: Implement functions to dynamically pull this data. // TODO: Implement functions to dynamically pull this data.
const intervals = ref<Map<string, Interval>>(new Map()); const intervals = ref<Map<string, Interval>>(new Map());
const intervalDates = ref<IntervalRecord>({}); const intervalDates = ref<IntervalRecord>({});
const intervalTemplates = ref<IntervalTemplate[]>([]); const reservationStore = useReservationStore();
const getIntervals = (date: Timestamp | string, boat?: Boat): Interval[] => { const getIntervals = (date: Timestamp | string, boat?: Boat): Interval[] => {
const searchDate = typeof date === 'string' ? date : date.date; const searchDate = typeof date === 'string' ? date : date.date;
@@ -28,12 +28,29 @@ export const useScheduleStore = defineStore('schedule', () => {
const intervalEnd = new Date(interval.end); const intervalEnd = new Date(interval.end);
const isWithinDay = intervalStart < dayEnd && intervalEnd > dayStart; const isWithinDay = intervalStart < dayEnd && intervalEnd > dayStart;
const matchesBoat = boat ? boat.$id === interval.boatId : true; const matchesBoat = boat ? boat.$id === interval.resource : true;
return isWithinDay && matchesBoat; return isWithinDay && matchesBoat;
}); });
}).value; }).value;
}; };
const getAvailableIntervals = (
date: Timestamp | string,
boat?: Boat
): Interval[] => {
return computed(() => {
console.log(boat);
console.log(getIntervals(date, boat));
return getIntervals(date, boat).filter((interval) => {
return !reservationStore.isResourceTimeOverlapped(
interval.resource,
new Date(interval.start),
new Date(interval.end)
);
});
}).value;
};
async function fetchIntervals(dateString: string) { async function fetchIntervals(dateString: string) {
try { try {
const response = await databases.listDocuments( const response = await databases.listDocuments(
@@ -62,31 +79,6 @@ export const useScheduleStore = defineStore('schedule', () => {
} }
} }
const getIntervalTemplates = (): Ref<IntervalTemplate[]> => {
// Should subscribe to get new intervaltemplates when they are created
if (!intervalTemplates.value) fetchIntervalTemplates();
return intervalTemplates;
};
async function fetchIntervalTemplates() {
try {
const response = await databases.listDocuments(
AppwriteIds.databaseId,
AppwriteIds.collection.intervalTemplate
);
intervalTemplates.value = response.documents.map(
(d: Models.Document): IntervalTemplate => {
return {
...d,
timeTuples: arrayToTimeTuples(d.timeTuple),
} as IntervalTemplate;
}
);
} catch (error) {
console.error('Failed to fetch timeblock templates', error);
}
}
const createInterval = async (interval: Interval) => { const createInterval = async (interval: Interval) => {
try { try {
const response = await databases.createDocument( const response = await databases.createDocument(
@@ -131,70 +123,13 @@ export const useScheduleStore = defineStore('schedule', () => {
console.error('Error deleting Interval: ' + e); console.error('Error deleting Interval: ' + e);
} }
}; };
const createIntervalTemplate = async (template: IntervalTemplate) => {
try {
const response = await databases.createDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.intervalTemplate,
ID.unique(),
{ name: template.name, timeTuple: template.timeTuples.flat(2) }
);
intervalTemplates.value.push(response as IntervalTemplate);
} catch (e) {
console.error('Error updating IntervalTemplate: ' + e);
}
};
const deleteIntervalTemplate = async (id: string) => {
try {
await databases.deleteDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.intervalTemplate,
id
);
intervalTemplates.value = intervalTemplates.value.filter(
(template) => template.$id !== id
);
} catch (e) {
console.error('Error deleting IntervalTemplate: ' + e);
}
};
const updateIntervalTemplate = async (
template: IntervalTemplate,
id: string
) => {
try {
const response = await databases.updateDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.intervalTemplate,
id,
{
name: template.name,
timeTuple: template.timeTuples.flat(2),
}
);
intervalTemplates.value = intervalTemplates.value.map((b) =>
b.$id !== id
? b
: ({
...response,
timeTuples: arrayToTimeTuples(response.timeTuple),
} as IntervalTemplate)
);
} catch (e) {
console.error('Error updating IntervalTemplate: ' + e);
}
};
return { return {
getIntervals, getIntervals,
getIntervalTemplates, getAvailableIntervals,
fetchIntervals, fetchIntervals,
fetchIntervalTemplates,
createInterval, createInterval,
updateInterval, updateInterval,
deleteInterval, deleteInterval,
createIntervalTemplate,
deleteIntervalTemplate,
updateIntervalTemplate,
}; };
}); });

View File

@@ -0,0 +1,97 @@
import { Ref, ref } from 'vue';
import { IntervalTemplate } from './schedule.types';
import { defineStore } from 'pinia';
import { AppwriteIds, databases } from 'src/boot/appwrite';
import { ID, Models } from 'appwrite';
import { arrayToTimeTuples } from 'src/utils/schedule';
export const useIntervalTemplateStore = defineStore('intervalTemplate', () => {
const intervalTemplates = ref<IntervalTemplate[]>([]);
const getIntervalTemplates = (): Ref<IntervalTemplate[]> => {
// Should subscribe to get new intervaltemplates when they are created
if (!intervalTemplates.value) fetchIntervalTemplates();
return intervalTemplates;
};
async function fetchIntervalTemplates() {
try {
const response = await databases.listDocuments(
AppwriteIds.databaseId,
AppwriteIds.collection.intervalTemplate
);
intervalTemplates.value = response.documents.map(
(d: Models.Document): IntervalTemplate => {
return {
...d,
timeTuples: arrayToTimeTuples(d.timeTuple),
} as IntervalTemplate;
}
);
} catch (error) {
console.error('Failed to fetch timeblock templates', error);
}
}
const createIntervalTemplate = async (template: IntervalTemplate) => {
try {
const response = await databases.createDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.intervalTemplate,
ID.unique(),
{ name: template.name, timeTuple: template.timeTuples.flat(2) }
);
intervalTemplates.value.push(response as IntervalTemplate);
} catch (e) {
console.error('Error updating IntervalTemplate: ' + e);
}
};
const deleteIntervalTemplate = async (id: string) => {
try {
await databases.deleteDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.intervalTemplate,
id
);
intervalTemplates.value = intervalTemplates.value.filter(
(template) => template.$id !== id
);
} catch (e) {
console.error('Error deleting IntervalTemplate: ' + e);
}
};
const updateIntervalTemplate = async (
template: IntervalTemplate,
id: string
) => {
try {
const response = await databases.updateDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.intervalTemplate,
id,
{
name: template.name,
timeTuple: template.timeTuples.flat(2),
}
);
intervalTemplates.value = intervalTemplates.value.map((b) =>
b.$id !== id
? b
: ({
...response,
timeTuples: arrayToTimeTuples(response.timeTuple),
} as IntervalTemplate)
);
} catch (e) {
console.error('Error updating IntervalTemplate: ' + e);
}
};
return {
getIntervalTemplates,
fetchIntervalTemplates,
createIntervalTemplate,
deleteIntervalTemplate,
updateIntervalTemplate,
};
});

View File

@@ -3,7 +3,7 @@ import type { Reservation } from './schedule.types';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { AppwriteIds, databases } from 'src/boot/appwrite'; import { AppwriteIds, databases } from 'src/boot/appwrite';
import { ID, Query } from 'appwrite'; import { ID, Query } from 'appwrite';
import { date } from 'quasar'; import { date, useQuasar } from 'quasar';
import { Timestamp, parseDate, today } from '@quasar/quasar-ui-qcalendar'; import { Timestamp, parseDate, today } from '@quasar/quasar-ui-qcalendar';
import { LoadingTypes } from 'src/utils/misc'; import { LoadingTypes } from 'src/utils/misc';
import { useAuthStore } from './auth'; import { useAuthStore } from './auth';
@@ -15,6 +15,7 @@ export const useReservationStore = defineStore('reservation', () => {
const userReservations = ref<Map<string, Reservation>>(new Map()); const userReservations = ref<Map<string, Reservation>>(new Map());
// TODO: Come up with a better way of storing reservations by date & reservations for user // TODO: Come up with a better way of storing reservations by date & reservations for user
const authStore = useAuthStore(); const authStore = useAuthStore();
const $q = useQuasar();
// Fetch reservations for a specific date range // Fetch reservations for a specific date range
const fetchReservationsForDateRange = async ( const fetchReservationsForDateRange = async (
@@ -60,7 +61,9 @@ export const useReservationStore = defineStore('reservation', () => {
} }
}; };
const createReservation = async (reservation: Reservation) => { const createReservation = async (
reservation: Reservation
): Promise<Reservation> => {
try { try {
const response = await databases.createDocument( const response = await databases.createDocument(
AppwriteIds.databaseId, AppwriteIds.databaseId,
@@ -70,8 +73,10 @@ export const useReservationStore = defineStore('reservation', () => {
); );
reservations.value.set(response.$id, response as Reservation); reservations.value.set(response.$id, response as Reservation);
console.info('Reservation booked: ', response); console.info('Reservation booked: ', response);
return response as Reservation;
} catch (e) { } catch (e) {
console.error('Error creating Reservation: ' + e); console.error('Error creating Reservation: ' + e);
throw e;
} }
}; };
@@ -88,6 +93,16 @@ export const useReservationStore = defineStore('reservation', () => {
return false; return false;
} }
const status = $q.notify({
color: 'secondary',
textColor: 'white',
message: 'Deleting Reservation',
spinner: true,
closeBtn: 'Dismiss',
position: 'top',
timeout: 0,
group: false,
});
try { try {
await databases.deleteDocument( await databases.deleteDocument(
AppwriteIds.databaseId, AppwriteIds.databaseId,
@@ -97,8 +112,21 @@ export const useReservationStore = defineStore('reservation', () => {
reservations.value.delete(id); reservations.value.delete(id);
userReservations.value.delete(id); userReservations.value.delete(id);
console.info(`Deleted reservation: ${id}`); console.info(`Deleted reservation: ${id}`);
status({
color: 'warning',
message: 'Reservation Deleted',
spinner: false,
icon: 'delete',
timeout: 4000,
});
} catch (e) { } catch (e) {
console.error('Error deleting reservation: ' + e); console.error('Error deleting reservation: ' + e);
status({
color: 'negative',
message: 'Failed to Delete Reservation',
spinner: false,
icon: 'error',
});
} }
}; };
@@ -197,7 +225,7 @@ export const useReservationStore = defineStore('reservation', () => {
} }
}; };
const sortedUserReservations = computed(() => const sortedUserReservations = computed((): Reservation[] =>
[...userReservations.value?.values()].sort( [...userReservations.value?.values()].sort(
(a, b) => new Date(b.start).getTime() - new Date(a.start).getTime() (a, b) => new Date(b.start).getTime() - new Date(a.start).getTime()
) )

View File

@@ -50,7 +50,7 @@ export function getSampleIntervals(): Interval[] {
return template.blocks.map((t: TimeTuple): Interval => { return template.blocks.map((t: TimeTuple): Interval => {
return { return {
$id: 'id' + Math.random().toString(32).slice(2), $id: 'id' + Math.random().toString(32).slice(2),
boatId: b.$id, resource: b.$id,
start: addToDate(tsToday, { day: i }).date + ' ' + t[0], start: addToDate(tsToday, { day: i }).date + ' ' + t[0],
end: addToDate(tsToday, { day: i }).date + ' ' + t[1], end: addToDate(tsToday, { day: i }).date + ' ' + t[1],
}; };

View File

@@ -2,11 +2,8 @@ import { Models } from 'appwrite';
import { LoadingTypes } from 'src/utils/misc'; import { LoadingTypes } from 'src/utils/misc';
export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined; export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined;
export type Reservation = Partial<Models.Document> & { export type Reservation = Interval & {
user: string; user: string;
start: string;
end: string;
resource: string; // Boat ID
status?: StatusTypes; status?: StatusTypes;
reason: string; reason: string;
comment: string; comment: string;
@@ -21,11 +18,11 @@ export type Reservation = Partial<Models.Document> & {
objects in here? */ objects in here? */
export type TimeTuple = [start: string, end: string]; export type TimeTuple = [start: string, end: string];
export type Interval = Partial<Models.Document> & { export type Interval = Partial<Models.Document> & {
boatId: string; resource: string;
start: string; start: string;
end: string; end: string;
selected?: false;
}; };
export type IntervalTemplate = Partial<Models.Document> & { export type IntervalTemplate = Partial<Models.Document> & {

View File

@@ -18,7 +18,7 @@ export function timeTuplesOverlapped(tuples: TimeTuple[]): Interval[] {
return intervalsOverlapped( return intervalsOverlapped(
tuples.map((tuples) => { tuples.map((tuples) => {
return { return {
boatId: '', resource: '',
start: '01/01/2001 ' + tuples[0], start: '01/01/2001 ' + tuples[0],
end: '01/01/2001 ' + tuples[1], end: '01/01/2001 ' + tuples[1],
}; };
@@ -64,7 +64,7 @@ export function buildInterval(
/* When the time zone offset is absent, date-only forms are interpreted /* When the time zone offset is absent, date-only forms are interpreted
as a UTC time and date-time forms are interpreted as local time. */ as a UTC time and date-time forms are interpreted as local time. */
const result = { const result = {
boatId: resource.$id, resource: resource.$id,
start: new Date(blockDate + 'T' + time[0]).toISOString(), start: new Date(blockDate + 'T' + time[0]).toISOString(),
end: new Date(blockDate + 'T' + time[1]).toISOString(), end: new Date(blockDate + 'T' + time[1]).toISOString(),
}; };
@@ -83,3 +83,8 @@ export function formatDate(inputDate: string | undefined): string {
if (!inputDate) return ''; if (!inputDate) return '';
return date.formatDate(new Date(inputDate), 'ddd MMM Do hh:mm A'); return date.formatDate(new Date(inputDate), 'ddd MMM Do hh:mm A');
} }
export function formatTime(inputDate: string | undefined): string {
if (!inputDate) return '';
return date.formatDate(new Date(inputDate), 'hh:mm A');
}