Adapting to time blocks for bookings
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 2m11s
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 2m11s
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
<!-- This has been abandoned for now. Going to block-based booking. Will probably need the schedule viewer functionality at some point in the future, though -->
|
||||||
<template>
|
<template>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-caption text-justify">
|
<div class="text-caption text-justify">
|
||||||
|
|||||||
183
src/components/scheduling/BoatSelection.vue
Normal file
183
src/components/scheduling/BoatSelection.vue
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<template>
|
||||||
|
<q-card-section style="max-width: 320px">
|
||||||
|
<div class="text-caption text-justify">
|
||||||
|
Use the calendar to pick a date. Tap a box in the grid for the boat and
|
||||||
|
start time. Select the duration below.
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
width: 50%;
|
||||||
|
max-width: 350px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="q-button"
|
||||||
|
style="cursor: pointer; user-select: none"
|
||||||
|
@click="onPrev"
|
||||||
|
><</span
|
||||||
|
>
|
||||||
|
{{ formattedMonth }}
|
||||||
|
<span
|
||||||
|
class="q-button"
|
||||||
|
style="cursor: pointer; user-select: none"
|
||||||
|
@click="onNext"
|
||||||
|
>></span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div style="display: flex; width: 100%">
|
||||||
|
<q-calendar-month
|
||||||
|
ref="calendar"
|
||||||
|
v-model="selectedDate"
|
||||||
|
:disabled-before="disabledBefore"
|
||||||
|
animated
|
||||||
|
bordered
|
||||||
|
mini-mode
|
||||||
|
date-type="rounded"
|
||||||
|
@click-date="onClickDate"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
</div></div
|
||||||
|
></q-card-section>
|
||||||
|
<q-card-section style="max-width: 320px">
|
||||||
|
<div v-for="boat in boatStore.boats" :key="boat.name">
|
||||||
|
<q-item-label header>{{ boat.name }}</q-item-label>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
<q-option-group
|
||||||
|
:options="boatoptions(boat)"
|
||||||
|
type="radio"
|
||||||
|
:model-value="selectedTime"
|
||||||
|
>
|
||||||
|
<template v-slot:label="opt">
|
||||||
|
<div class="row items-center">
|
||||||
|
{{ opt.label }}
|
||||||
|
<span v-if="opt.disable">Reserved by {{ opt.user }}</span>
|
||||||
|
</div></template
|
||||||
|
>
|
||||||
|
</q-option-group>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import {
|
||||||
|
today,
|
||||||
|
parsed,
|
||||||
|
addToDate,
|
||||||
|
Timestamp,
|
||||||
|
} from '@quasar/quasar-ui-qcalendar';
|
||||||
|
import { Boat, useBoatStore } from 'src/stores/boat';
|
||||||
|
import { useScheduleStore, Timeblock } from 'src/stores/schedule';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { date } from 'quasar';
|
||||||
|
|
||||||
|
type EventData = {
|
||||||
|
event: object;
|
||||||
|
scope: {
|
||||||
|
timestamp: object;
|
||||||
|
columnindex: number;
|
||||||
|
activeDate: boolean;
|
||||||
|
droppable: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const calendar = ref();
|
||||||
|
const boatStore = useBoatStore();
|
||||||
|
const scheduleStore = useScheduleStore();
|
||||||
|
const selectedDate = ref(today());
|
||||||
|
|
||||||
|
const formattedMonth = computed(() => {
|
||||||
|
const date = new Date(selectedDate.value);
|
||||||
|
|
||||||
|
return monthFormatter()?.format(date);
|
||||||
|
});
|
||||||
|
|
||||||
|
const disabledBefore = computed(() => {
|
||||||
|
const todayTs = parsed(today()) as Timestamp;
|
||||||
|
return addToDate(todayTs, { day: -1 }).date;
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedTime = computed(() => {
|
||||||
|
return boatStore.boats[0] + ':09:00';
|
||||||
|
});
|
||||||
|
|
||||||
|
function monthFormatter() {
|
||||||
|
try {
|
||||||
|
return new Intl.DateTimeFormat('en-CA' || undefined, {
|
||||||
|
month: 'long',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const boatoptions = (boat: Boat) => {
|
||||||
|
const options = useScheduleStore()
|
||||||
|
.getTimeblocksForDate(date.extractDate(selectedDate.value, 'YYYY-MM-DD'))
|
||||||
|
.map((x: Timeblock) => {
|
||||||
|
const conflicts = getConflicts(x, boat);
|
||||||
|
return {
|
||||||
|
label: x.start.time + ' to ' + x.end.time,
|
||||||
|
value: boat.name + ':' + x.start.time,
|
||||||
|
disable: conflicts.length > 0,
|
||||||
|
user: conflicts[0]?.user,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits(['onClickTime', 'onUpdateDuration']);
|
||||||
|
|
||||||
|
function onPrev() {
|
||||||
|
calendar.value.prev();
|
||||||
|
}
|
||||||
|
function onNext() {
|
||||||
|
calendar.value.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickDate(data: EventData) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
function onChange(data: EventData) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getConflicts = (timeblock: Timeblock, boat: Boat) => {
|
||||||
|
const start = date.buildDate({
|
||||||
|
hour: timeblock.start.hour,
|
||||||
|
minute: timeblock.start.minute,
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0,
|
||||||
|
});
|
||||||
|
const end = date.buildDate({
|
||||||
|
hour: timeblock.end.hour,
|
||||||
|
minute: timeblock.end.minute,
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0,
|
||||||
|
});
|
||||||
|
return scheduleStore.getConflictingReservations(boat, start, end);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -21,14 +21,8 @@
|
|||||||
:caption="bookingSummary"
|
:caption="bookingSummary"
|
||||||
>
|
>
|
||||||
<q-separator />
|
<q-separator />
|
||||||
<resource-schedule-viewer-component
|
<boat-selection />
|
||||||
@on-click-time="onClickTime"
|
|
||||||
@on-update-duration="
|
|
||||||
(value) => {
|
|
||||||
bookingForm.duration = value;
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<q-banner
|
<q-banner
|
||||||
rounded
|
rounded
|
||||||
class="bg-warning text-grey-10"
|
class="bg-warning text-grey-10"
|
||||||
@@ -78,7 +72,7 @@ import { reactive, 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 { Dialog, date } from 'quasar';
|
import { Dialog, date } from 'quasar';
|
||||||
import ResourceScheduleViewerComponent from 'src/components/ResourceScheduleViewerComponent.vue';
|
import BoatSelection from 'src/components/scheduling/BoatSelection.vue';
|
||||||
import { makeDateTime } from '@quasar/quasar-ui-qcalendar';
|
import { makeDateTime } from '@quasar/quasar-ui-qcalendar';
|
||||||
import { useScheduleStore, Reservation } from 'src/stores/schedule';
|
import { useScheduleStore, Reservation } from 'src/stores/schedule';
|
||||||
|
|
||||||
@@ -113,7 +107,7 @@ watch(bookingForm, (b, a) => {
|
|||||||
status: 'tentative',
|
status: 'tentative',
|
||||||
};
|
};
|
||||||
//TODO: Turn this into a validator.
|
//TODO: Turn this into a validator.
|
||||||
scheduleStore.isOverlapped(newRes)
|
scheduleStore.isReservationOverlapped(newRes)
|
||||||
? Dialog.create({ message: 'This booking overlaps another!' })
|
? Dialog.create({ message: 'This booking overlaps another!' })
|
||||||
: scheduleStore.addOrCreateReservation(newRes);
|
: scheduleStore.addOrCreateReservation(newRes);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
parseTimestamp,
|
parseTimestamp,
|
||||||
TimestampArray,
|
TimestampArray,
|
||||||
} from '@quasar/quasar-ui-qcalendar';
|
} from '@quasar/quasar-ui-qcalendar';
|
||||||
|
import { timeStamp } from 'console';
|
||||||
|
|
||||||
export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined;
|
export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined;
|
||||||
export type Reservation = {
|
export type Reservation = {
|
||||||
@@ -26,10 +27,22 @@ export type Timeblock = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sampleBlocks = [
|
const sampleBlocks = [
|
||||||
{ start: { hour: 9, minute: 0 }, end: { hour: 12, minute: 0 } },
|
{
|
||||||
{ start: { hour: 12, minute: 0 }, end: { hour: 15, minute: 0 } },
|
start: { time: '09:00', hour: 9, minute: 0, hasDay: false, hasTime: true },
|
||||||
{ start: { hour: 15, minute: 0 }, end: { hour: 18, minute: 0 } },
|
end: { time: '12:00', hour: 12, minute: 0, hasDay: false, hasTime: true },
|
||||||
{ start: { hour: 18, minute: 0 }, end: { hour: 21, minute: 0 } },
|
},
|
||||||
|
{
|
||||||
|
start: { time: '12:00', hour: 12, minute: 0, hasDay: false, hasTime: true },
|
||||||
|
end: { time: '15:00', hour: 15, minute: 0, hasDay: false, hasTime: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: { time: '15:00', hour: 15, minute: 0, hasDay: false, hasTime: true },
|
||||||
|
end: { time: '18:00', hour: 18, minute: 0, hasDay: false, hasTime: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: { time: '18:00', hour: 18, minute: 0, hasDay: false, hasTime: true },
|
||||||
|
end: { time: '21:00', hour: 21, minute: 0, hasDay: false, hasTime: true },
|
||||||
|
},
|
||||||
] as Timeblock[];
|
] as Timeblock[];
|
||||||
|
|
||||||
function getSampleReservations(): Reservation[] {
|
function getSampleReservations(): Reservation[] {
|
||||||
@@ -38,7 +51,7 @@ function getSampleReservations(): Reservation[] {
|
|||||||
id: 1,
|
id: 1,
|
||||||
user: 'John Smith',
|
user: 'John Smith',
|
||||||
start: '12:00',
|
start: '12:00',
|
||||||
end: '14:00',
|
end: '15:00',
|
||||||
boat: 1,
|
boat: 1,
|
||||||
status: 'confirmed',
|
status: 'confirmed',
|
||||||
},
|
},
|
||||||
@@ -46,31 +59,31 @@ function getSampleReservations(): Reservation[] {
|
|||||||
id: 2,
|
id: 2,
|
||||||
user: 'Bob Barker',
|
user: 'Bob Barker',
|
||||||
start: '18:00',
|
start: '18:00',
|
||||||
end: '20:00',
|
end: '21:00',
|
||||||
boat: 1,
|
boat: 1,
|
||||||
status: 'confirmed',
|
status: 'confirmed',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
user: 'Peter Parker',
|
user: 'Peter Parker',
|
||||||
start: '8:00',
|
start: '9:00',
|
||||||
end: '10:00',
|
end: '12:00',
|
||||||
boat: 2,
|
boat: 2,
|
||||||
status: 'tentative',
|
status: 'tentative',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
user: 'Vince McMahon',
|
user: 'Vince McMahon',
|
||||||
start: '13:00',
|
start: '15:00',
|
||||||
end: '17:00',
|
end: '18:00',
|
||||||
boat: 2,
|
boat: 2,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
user: 'Heather Graham',
|
user: 'Heather Graham',
|
||||||
start: '06:00',
|
start: '09:00',
|
||||||
end: '09:00',
|
end: '12:00',
|
||||||
boat: 3,
|
boat: 3,
|
||||||
status: 'confirmed',
|
status: 'confirmed',
|
||||||
},
|
},
|
||||||
@@ -78,7 +91,7 @@ function getSampleReservations(): Reservation[] {
|
|||||||
id: 6,
|
id: 6,
|
||||||
user: 'Lawrence Fishburne',
|
user: 'Lawrence Fishburne',
|
||||||
start: '18:00',
|
start: '18:00',
|
||||||
end: '20:00',
|
end: '21:00',
|
||||||
boat: 3,
|
boat: 3,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -88,7 +101,12 @@ function getSampleReservations(): Reservation[] {
|
|||||||
return x.split(':');
|
return x.split(':');
|
||||||
};
|
};
|
||||||
const makeOpts = (x: string[]): DateOptions => {
|
const makeOpts = (x: string[]): DateOptions => {
|
||||||
return { hour: parseInt(x[0]), minute: parseInt(x[1]) };
|
return {
|
||||||
|
hour: parseInt(x[0]),
|
||||||
|
minute: parseInt(x[1]),
|
||||||
|
seconds: 0,
|
||||||
|
milliseconds: 0,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
return sampleData.map((entry): Reservation => {
|
return sampleData.map((entry): Reservation => {
|
||||||
@@ -128,15 +146,30 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isOverlapped = (res: Reservation) => {
|
const getConflictingReservations = (
|
||||||
const lapped = reservations.value.filter(
|
resource: Boat,
|
||||||
|
start: Date,
|
||||||
|
end: Date
|
||||||
|
): Reservation[] => {
|
||||||
|
const overlapped = reservations.value.filter(
|
||||||
(entry: Reservation) =>
|
(entry: Reservation) =>
|
||||||
entry.id != res.id &&
|
entry.resource.id == resource.id &&
|
||||||
entry.resource == res.resource &&
|
entry.start < end &&
|
||||||
((entry.start <= res.start && entry.end > res.start) ||
|
entry.end > start
|
||||||
(entry.end >= res.end && entry.start <= res.end))
|
|
||||||
);
|
);
|
||||||
return lapped.length > 0;
|
return overlapped;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isResourceTimeOverlapped = (
|
||||||
|
resource: Boat,
|
||||||
|
start: Date,
|
||||||
|
end: Date
|
||||||
|
): boolean => {
|
||||||
|
return getConflictingReservations(resource, start, end).length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isReservationOverlapped = (res: Reservation): boolean => {
|
||||||
|
return isResourceTimeOverlapped(res.resource, res.start, res.end);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNewId = () => {
|
const getNewId = () => {
|
||||||
@@ -156,8 +189,11 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
return {
|
return {
|
||||||
reservations,
|
reservations,
|
||||||
getBoatReservations,
|
getBoatReservations,
|
||||||
|
getConflictingReservations,
|
||||||
|
getTimeblocksForDate,
|
||||||
getNewId,
|
getNewId,
|
||||||
addOrCreateReservation,
|
addOrCreateReservation,
|
||||||
isOverlapped,
|
isReservationOverlapped,
|
||||||
|
isResourceTimeOverlapped,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user