Compare commits

2 Commits

Author SHA1 Message Date
76b0498a18 Booking
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 1m57s
2024-05-18 10:07:09 -04:00
d6339815aa Navigation Tweaks 2024-05-18 08:49:56 -04:00
8 changed files with 187 additions and 106 deletions

View File

@@ -9,13 +9,28 @@
<q-scroll-area class="fit"> <q-scroll-area class="fit">
<q-list padding class="menu-list"> <q-list padding class="menu-list">
<template v-for="link in enabledLinks" :key="link.name"> <template v-for="link in enabledLinks" :key="link.name">
<q-item clickable v-ripple :to="link.to"> <!-- TODO: Template this to be DRY --><q-item
clickable
v-ripple
:to="link.to"
>
<q-item-section avatar> <q-item-section avatar>
<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> {{ link.name }} </q-item-section>
</q-item> </q-item>
<q-list v-if="link.sublinks">
<div v-for="sublink in link.sublinks" :key="sublink.name">
<q-item clickable v-ripple :to="sublink.to" class="q-ml-md">
<q-item-section avatar>
<q-icon :name="sublink.icon" />
</q-item-section>
<q-item-section> {{ sublink.name }} </q-item-section>
</q-item>
</div></q-list
>
</template> </template>
<q-item clickable v-ripple @click="logout()"> <q-item clickable v-ripple @click="logout()">
<q-item-section avatar><q-icon name="logout" /></q-item-section <q-item-section avatar><q-icon name="logout" /></q-item-section

View File

@@ -7,11 +7,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import BoatPreviewComponent from 'src/components/boat/BoatPreviewComponent.vue'; import BoatPreviewComponent from 'src/components/boat/BoatPreviewComponent.vue';
import { ref } from 'vue'; import { onMounted } from 'vue';
import { useBoatStore } from 'src/stores/boat'; import { useBoatStore } from 'src/stores/boat';
import ToolbarComponent from 'src/components/ToolbarComponent.vue'; import ToolbarComponent from 'src/components/ToolbarComponent.vue';
import { storeToRefs } from 'pinia';
const boatStore = useBoatStore(); const boatStore = useBoatStore();
boatStore.fetchBoats(); const { boats } = storeToRefs(boatStore);
const boats = ref(useBoatStore().boats);
onMounted(() => boatStore.fetchBoats());
</script> </script>

View File

@@ -24,7 +24,7 @@
Use the calendar to pick a date. Select an available boat and Use the calendar to pick a date. Select an available boat and
timeslot below. timeslot below.
</q-banner> </q-banner>
<BoatScheduleTableComponent v-model="timeblock" /> <BoatScheduleTableComponent v-model="interval" />
<q-banner <q-banner
rounded rounded
@@ -45,7 +45,7 @@
</li> </li>
</ol> </ol>
</q-banner> </q-banner>
<q-card-section> <!-- <q-card-section>
<q-btn <q-btn
color="primary" color="primary"
class="full-width" class="full-width"
@@ -53,9 +53,9 @@
icon-right="keyboard_arrow_down" icon-right="keyboard_arrow_down"
label="Next: Crew & Passengers" label="Next: Crew & Passengers"
@click="resourceView = false" @click="resourceView = false"
/></q-card-section> /></q-card-section> -->
</q-expansion-item> </q-expansion-item>
<q-expansion-item <!-- <q-expansion-item
expand-separator expand-separator
icon="people" icon="people"
label="Crew and Passengers" label="Crew and Passengers"
@@ -87,7 +87,7 @@
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-separator /> <q-separator />
</q-expansion-item> </q-expansion-item> -->
<q-item-section> <q-item-section>
<q-btn label="Submit" type="submit" color="primary" /> <q-btn label="Submit" type="submit" color="primary" />
@@ -100,10 +100,12 @@
import { ref, computed, watch } from 'vue'; import { ref, computed, watch } from 'vue';
import { useAuthStore } from 'src/stores/auth'; import { useAuthStore } from 'src/stores/auth';
import { Boat, useBoatStore } from 'src/stores/boat'; import { Boat, useBoatStore } from 'src/stores/boat';
import { date } from 'quasar'; import { date, useQuasar } from 'quasar';
import { Interval } from 'src/stores/schedule.types'; import { Interval, Reservation } from 'src/stores/schedule.types';
import BoatScheduleTableComponent from 'src/components/scheduling/boat/BoatScheduleTableComponent.vue'; import BoatScheduleTableComponent from 'src/components/scheduling/boat/BoatScheduleTableComponent.vue';
import { getNewId } from 'src/utils/misc'; import { getNewId } from 'src/utils/misc';
import { useRouter } from 'vue-router';
import { useReservationStore } from 'src/stores/reservation';
interface BookingForm { interface BookingForm {
bookingId: string; bookingId: string;
@@ -118,7 +120,7 @@ interface BookingForm {
const auth = useAuthStore(); const auth = useAuthStore();
const dateFormat = 'MMM D, YYYY h:mm A'; const dateFormat = 'MMM D, YYYY h:mm A';
const resourceView = ref(true); const resourceView = ref(true);
const timeblock = ref<Interval>(); const interval = ref<Interval>();
const bookingForm = ref<BookingForm>({ const bookingForm = ref<BookingForm>({
bookingId: getNewId(), bookingId: getNewId(),
name: auth.currentUser?.name, name: auth.currentUser?.name,
@@ -128,26 +130,49 @@ const bookingForm = ref<BookingForm>({
members: [{ name: 'Karen Henrikso' }, { name: "Rich O'hare" }], members: [{ name: 'Karen Henrikso' }, { name: "Rich O'hare" }],
guests: [{ name: 'Bob Barker' }, { name: 'Taylor Swift' }], guests: [{ name: 'Bob Barker' }, { name: 'Taylor Swift' }],
}); });
const router = useRouter();
const reservationStore = useReservationStore();
const $q = useQuasar();
watch(timeblock, (tb_new) => { watch(interval, (new_interval) => {
bookingForm.value.boat = useBoatStore().boats.find( bookingForm.value.boat = useBoatStore().boats.find(
(b) => b.$id === tb_new?.boatId (b) => b.$id === new_interval?.boatId
); );
bookingForm.value.startDate = date.formatDate(tb_new?.start, dateFormat); bookingForm.value.startDate = date.formatDate(
bookingForm.value.endDate = date.formatDate(tb_new?.end, dateFormat); new_interval?.start,
dateFormat
);
bookingForm.value.endDate = date.formatDate(new_interval?.end, dateFormat);
}); });
// //TODO: Turn this into a validator.
// scheduleStore.isReservationOverlapped(newRes)
// ? Dialog.create({ message: 'This booking overlaps another!' })
// : scheduleStore.addOrCreateReservation(newRes);
const onReset = () => { const onReset = () => {
// TODO // TODO
}; };
const onSubmit = () => { const onSubmit = () => {
// TODO const booking = bookingForm.value;
if (
!(booking.boat && booking.startDate && booking.endDate && auth.currentUser)
) {
// TODO: Make a proper validator
return;
}
const reservation = <Reservation>{
resource: booking.boat.$id,
start: booking.startDate,
end: booking.endDate,
user: auth.currentUser.$id,
status: 'confirmed',
};
// TODO: Fix this. It will always look successful
reservationStore.createReservation(reservation);
$q.notify({
color: 'green-4',
textColor: 'white',
icon: 'cloud_done',
message: 'Submitted',
});
router.go(-1);
}; };
const bookingDuration = computed(() => { const bookingDuration = computed(() => {

View File

@@ -1,18 +1,20 @@
<template> <template>
<q-page padding> <q-page padding>
<div class="subcontent"> <div class="subcontent">
<!-- <navigation-bar @today="onToday" @prev="onPrev" @next="onNext" /> --> <navigation-bar @today="onToday" @prev="onPrev" @next="onNext" />
<div class="row justify-center"> <div class="row justify-center">
<q-calendar-day <q-calendar-scheduler
ref="calendar" ref="calendar"
v-model="selectedDate" v-model="selectedDate"
view="day" v-model:model-resources="boatStore.boats"
:max-days="3" resource-key="$id"
resource-label="displayName"
:weekdays="[1, 2, 3, 4, 5, 6, 0]"
view="week"
bordered bordered
animated animated
transition-next="slide-left" cell-width="150px"
transition-prev="slide-right" day-min-height="50px"
@change="onChange" @change="onChange"
@moved="onMoved" @moved="onMoved"
@click-date="onClickDate" @click-date="onClickDate"
@@ -20,22 +22,11 @@
@click-interval="onClickInterval" @click-interval="onClickInterval"
@click-head-day="onClickHeadDay" @click-head-day="onClickHeadDay"
> >
<template <template #day="{ scope }">
#day-body="{ <div v-for="event in boatReservationEvents(scope)" :key="event.id">
scope: { timestamp, timeStartPos, timeDurationHeight }, <div v-if="event.start !== undefined" class="booking-event">
}"
>
<template
v-for="event in reservationEvents(timestamp)"
:key="event.id"
>
<div
v-if="event.start !== undefined"
class="booking-event"
:style="slotStyle(event, timeStartPos, timeDurationHeight)"
>
<span class="title q-calendar__ellipsis"> <span class="title q-calendar__ellipsis">
{{ event.user }} {{ useAuthStore().getUserNameById(event.user) }}
<q-tooltip>{{ <q-tooltip>{{
event.start + event.start +
' - ' + ' - ' +
@@ -43,9 +34,9 @@
}}</q-tooltip> }}</q-tooltip>
</span> </span>
</div> </div>
</template> </div>
</template> </template>
</q-calendar-day> </q-calendar-scheduler>
</div> </div>
</div> </div>
</q-page> </q-page>
@@ -53,46 +44,57 @@
<script setup lang="ts"> <script setup lang="ts">
import { useReservationStore } from 'src/stores/reservation'; import { useReservationStore } from 'src/stores/reservation';
import { Reservation } from 'src/stores/schedule.types'; import { onMounted, ref } from 'vue';
import { ref } from 'vue'; import { useAuthStore } from 'src/stores/auth';
const reservationStore = useReservationStore(); const reservationStore = useReservationStore();
import { import { getDate, today } from '@quasar/quasar-ui-qcalendar';
TimestampOrNull, import { QCalendarScheduler } from '@quasar/quasar-ui-qcalendar';
getDate,
parsed,
today,
} from '@quasar/quasar-ui-qcalendar';
import { QCalendarDay } from '@quasar/quasar-ui-qcalendar';
import { date } from 'quasar';
import { Timestamp } from '@quasar/quasar-ui-qcalendar'; import { Timestamp } from '@quasar/quasar-ui-qcalendar';
import { useBoatStore } from 'src/stores/boat'; import { Boat, useBoatStore } from 'src/stores/boat';
import NavigationBar from 'src/components/scheduling/NavigationBar.vue';
const selectedDate = ref(today()); const selectedDate = ref(today());
const boatStore = useBoatStore(); const boatStore = useBoatStore();
const calendar = ref();
// Method declarations interface DayScope {
timestamp: Timestamp;
function slotStyle( columnIndex: number;
event: Reservation, resource: object;
timeStartPos: (time: TimestampOrNull) => string, resourceIndex: number;
timeDurationHeight: (minutes: number) => string indentLevel: number;
) { activeDate: boolean;
const s = { droppable: boolean;
top: '',
height: '',
'align-items': 'flex-start',
};
if (timeStartPos && timeDurationHeight) {
s.top = timeStartPos(parsed(event.start)) + 'px';
s.height =
timeDurationHeight(date.getDateDiff(event.end, event.start, 'minutes')) +
'px';
}
return s;
} }
function reservationEvents(timestamp: Timestamp) { onMounted(() => boatStore.fetchBoats());
return reservationStore.getReservationsByDate(getDate(timestamp)); // Method declarations
// function slotStyle(
// event: Reservation,
// timeStartPos: (time: TimestampOrNull) => string,
// timeDurationHeight: (minutes: number) => string
// ) {
// const s = {
// top: '',
// height: '',
// 'align-items': 'flex-start',
// };
// if (timeStartPos && timeDurationHeight) {
// s.top = timeStartPos(parsed(event.start)) + 'px';
// s.height =
// timeDurationHeight(date.getDateDiff(event.end, event.start, 'minutes')) +
// 'px';
// }
// return s;
// }
function boatReservationEvents({ timestamp, resource }: DayScope) {
return reservationStore.getReservationsByDate(
getDate(timestamp),
(resource as Boat).$id
);
} }
function onMoved(data: Event) { function onMoved(data: Event) {
console.log('onMoved', data); console.log('onMoved', data);
@@ -112,6 +114,15 @@ function onClickInterval(data: Event) {
function onClickHeadDay(data: Event) { function onClickHeadDay(data: Event) {
console.log('onClickHeadDay', data); console.log('onClickHeadDay', data);
} }
function onToday() {
calendar.value.moveToToday();
}
function onPrev() {
calendar.value.prev();
}
function onNext() {
calendar.value.next();
}
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View File

@@ -17,9 +17,8 @@
:drag-over-func="onDragOver" :drag-over-func="onDragOver"
:drag-leave-func="onDragLeave" :drag-leave-func="onDragLeave"
:drop-func="onDrop" :drop-func="onDrop"
:day-min-height="50" day-min-height="50px"
:cell-width="150" cell-width="150px"
:day-height="0"
> >
<template #day="{ scope }"> <template #day="{ scope }">
<div <div

View File

@@ -1,12 +1,12 @@
<template> <template>
<q-page padding> <q-page padding>
<q-item v-for="link in navlinks" :key="link.label"> <q-item v-for="link in navlinks" :key="link.name">
<q-btn <q-btn
:icon="link.icon" :icon="link.icon"
:color="link.color" color="primary"
size="1.25em" size="1.25em"
:to="link.to" :to="link.to"
:label="link.label" :label="link.name"
rounded rounded
class="full-width" class="full-width"
align="left" align="left"
@@ -16,24 +16,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const navlinks = [ import { enabledLinks } from 'src/router/navlinks';
{
icon: 'more_time', const navlinks = enabledLinks.find(
to: '/schedule/book', (link) => link.name === 'Schedule'
label: 'Create a Reservation', )?.sublinks;
color: 'primary',
},
{
icon: 'calendar_month',
to: '/schedule/view',
label: 'View Schedule',
color: 'primary',
},
{
icon: 'edit_calendar',
to: '/schedule/manage',
label: 'Manage Calendar',
color: 'accent',
},
];
</script> </script>

View File

@@ -26,6 +26,29 @@ export const links = [
icon: 'calendar_month', icon: 'calendar_month',
front_links: true, front_links: true,
enabled: true, enabled: true,
sublinks: [
{
name: 'Book',
to: '/schedule/book',
icon: 'more_time',
front_links: false,
enabled: true,
},
{
name: 'View',
to: '/schedule/view',
icon: 'calendar_month',
front_links: false,
enabled: true,
},
{
name: 'Manage',
to: '/schedule/manage',
icon: 'edit_calendar',
front_links: false,
enabled: true,
},
],
}, },
{ {
name: 'Certifications', name: 'Certifications',
@@ -57,4 +80,11 @@ export const links = [
}, },
]; ];
export const enabledLinks = links.filter((link) => link.enabled); export const enabledLinks = links
.filter((link) => link.enabled)
.map((link) => {
if (link.sublinks) {
link.sublinks = link.sublinks.filter((sublink) => sublink.enabled);
}
return link;
});

View File

@@ -2,7 +2,7 @@ import { defineStore } from 'pinia';
import type { Reservation } from './schedule.types'; 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 { Query } from 'appwrite'; import { ID, Query } from 'appwrite';
import { date } from 'quasar'; import { date } from 'quasar';
import { Timestamp, parseDate, today } from '@quasar/quasar-ui-qcalendar'; import { Timestamp, parseDate, today } from '@quasar/quasar-ui-qcalendar';
@@ -42,6 +42,19 @@ export const useReservationStore = defineStore('reservation', () => {
setDateLoaded(startDate, endDate, undefined); setDateLoaded(startDate, endDate, undefined);
} }
}; };
const createReservation = async (reservation: Reservation) => {
try {
const response = await databases.createDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.reservation,
ID.unique(),
reservation
);
reservations.value.set(response.$id, response as Reservation);
} catch (e) {
console.error('Error creating Reservation: ' + e);
}
};
// Set the loading state for dates // Set the loading state for dates
const setDateLoaded = (start: Date, end: Date, state: LoadingTypes) => { const setDateLoaded = (start: Date, end: Date, state: LoadingTypes) => {
@@ -124,6 +137,7 @@ export const useReservationStore = defineStore('reservation', () => {
return { return {
getReservationsByDate, getReservationsByDate,
createReservation,
fetchReservationsForDateRange, fetchReservationsForDateRange,
isReservationOverlapped, isReservationOverlapped,
isResourceTimeOverlapped, isResourceTimeOverlapped,