Compare commits

2 Commits

Author SHA1 Message Date
adc34a116b Tracked down a date bug. Also tried to optimize, but not sure it's necessary.
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m2s
2024-05-17 20:41:26 -04:00
b506ab7ca9 Many changes to try to improve reliability 2024-05-17 18:17:25 -04:00
12 changed files with 268 additions and 166 deletions

20
docs/time.md Normal file
View File

@@ -0,0 +1,20 @@
# Dealing with Time
Dealing with time sucks, okay? We have three different formats we need to deal with:
1. ES Date - The native ECMAScript Date object. This is saddled with all the legacy of the decades. Hopefully, we will be able to retire this one day... Ref: https://tc39.es/proposal-temporal/docs/index.html
2. ISO 8601 Date - Used by Appwrite backend. This is just a string, but can represent any date, with or without a timezone.
3. Timestamp - Used internally by QCalendar.
We can't just use one format. We need ISO8601 format for Appwrite, and we get passed Timestamp objects by QCalendar. In the middle of that, we need ES Date objects to do some underlying math.
Componentization:
In order to make things clean and modular, we will rely on Timestamp as the main format in a component.
In data that comes from, or goes to the backend, we will store absolute dates in ISO format.
For any user-facing dates / times, the data will be rendered in the users local time.
For time-only data (as used in Intervals, eg: '09:00'), the template will be stored as a string of 'hh:mm', and represent the users local time. We may want to change this in the future, as this could prove a problem when a user is travelling, but wants to apply a template to their home location.
For now, we'll use the Timestamp object provided by QCalendar. We might need to refactor this in the future.

View File

@@ -4,10 +4,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, onMounted } from 'vue'; import { defineComponent, onMounted } from 'vue';
import { useScheduleStore } from './stores/schedule';
import { useBoatStore } from './stores/boat';
import { useAuthStore } from './stores/auth'; import { useAuthStore } from './stores/auth';
import { useReservationStore } from './stores/reservation';
defineComponent({ defineComponent({
name: 'OYS Borrow-a-Boat', name: 'OYS Borrow-a-Boat',
@@ -15,9 +12,5 @@ defineComponent({
onMounted(async () => { onMounted(async () => {
await useAuthStore().init(); await useAuthStore().init();
await useScheduleStore().fetchIntervalTemplates();
await useScheduleStore().fetchIntervals();
await useReservationStore().fetchReservations();
await useBoatStore().fetchBoats();
}); });
</script> </script>

View File

@@ -107,7 +107,6 @@ import {
QCalendarResource, QCalendarResource,
TimestampOrNull, TimestampOrNull,
today, today,
parseDate,
parseTimestamp, parseTimestamp,
addToDate, addToDate,
Timestamp, Timestamp,
@@ -172,8 +171,8 @@ function monthFormatter() {
} }
function getEvents(scope: ResourceIntervalScope) { function getEvents(scope: ResourceIntervalScope) {
const resourceEvents = reservationStore.getBoatReservations( const resourceEvents = reservationStore.getReservationsByDate(
parseDate(date.extractDate(selectedDate.value, 'YYYY-MM-DD')) as Timestamp, selectedDate.value,
scope.resource.$id scope.resource.$id
); );

View File

@@ -73,7 +73,7 @@ import {
} from '@quasar/quasar-ui-qcalendar'; } from '@quasar/quasar-ui-qcalendar';
import CalendarHeaderComponent from './CalendarHeaderComponent.vue'; import CalendarHeaderComponent from './CalendarHeaderComponent.vue';
import { ref, computed } 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 { useScheduleStore } from 'src/stores/schedule';
import { useAuthStore } from 'src/stores/auth'; import { useAuthStore } from 'src/stores/auth';
@@ -89,6 +89,12 @@ const selectedDate = ref(today());
const calendar = ref<QCalendarDay | null>(null); const calendar = ref<QCalendarDay | null>(null);
onMounted(async () => {
await useBoatStore().fetchBoats();
await scheduleStore.fetchIntervals();
await scheduleStore.fetchIntervalTemplates();
});
function handleSwipe({ ...event }) { function handleSwipe({ ...event }) {
event.direction === 'right' ? calendar.value?.prev() : calendar.value?.next(); event.direction === 'right' ? calendar.value?.prev() : calendar.value?.next();
} }
@@ -168,18 +174,24 @@ function selectBlock(event: MouseEvent, scope: DayBodyScope, block: Interval) {
: (selectedBlock.value = block); : (selectedBlock.value = block);
} }
interface BoatBlocks { const boatBlocks = computed((): Record<string, Interval[]> => {
[key: string]: Interval[];
}
const boatBlocks = computed((): BoatBlocks => {
return scheduleStore return scheduleStore
.getIntervalsForDate(selectedDate.value) .getIntervalsForDate(selectedDate.value)
.reduce((result, tb) => { .reduce((result, tb) => {
if (!result[tb.boatId]) result[tb.boatId] = []; if (!result[tb.boatId]) result[tb.boatId] = [];
result[tb.boatId].push(tb); result[tb.boatId].push(tb);
return result; return result;
}, <BoatBlocks>{}); }, <Record<string, Interval[]>>{});
});
const boatReservations = computed((): Record<string, Reservation[]> => {
return reservationStore
.getReservationsByDate(selectedDate.value)
.reduce((result, reservation) => {
if (!result[reservation.resource]) result[reservation.resource] = [];
result[reservation.resource].push(reservation);
return result;
}, <Record<string, Reservation[]>>{});
}); });
function getBoatBlocks(scope: DayBodyScope): Interval[] { function getBoatBlocks(scope: DayBodyScope): Interval[] {
@@ -188,14 +200,12 @@ function getBoatBlocks(scope: DayBodyScope): Interval[] {
} }
function getBoatReservations(scope: DayBodyScope): Reservation[] { function getBoatReservations(scope: DayBodyScope): Reservation[] {
const boat = boats.value[scope.columnIndex]; const boat = boats.value[scope.columnIndex];
return boat return boat ? boatReservations.value[boat.$id] : [];
? reservationStore.getBoatReservations(scope.timestamp, boat.$id)
: [];
} }
// function changeEvent({ start }: { start: string }) { // function changeEvent({ start }: { start: string }) {
// const newBlocks = scheduleStore.getIntervalsForDate(start); // const newBlocks = scheduleStore.getIntervalsForDate(start);
// const reservations = scheduleStore.getBoatReservations( // const reservations = scheduleStore.getReservationsByDate(
// parsed(start) as Timestamp // parsed(start) as Timestamp
// ); // );
// boats.value.map((boat) => { // boats.value.map((boat) => {
@@ -268,4 +278,7 @@ const disabledBefore = computed(() => {
font-size: 0.8em font-size: 0.8em
.q-calendar-day__day.q-current-day .q-calendar-day__day.q-current-day
padding: 1px padding: 1px
.q-calendar-day__head--days__column
background: $primary
color: white
</style> </style>

View File

@@ -56,7 +56,12 @@ import { useReservationStore } from 'src/stores/reservation';
import { Reservation } from 'src/stores/schedule.types'; import { Reservation } from 'src/stores/schedule.types';
import { ref } from 'vue'; import { ref } from 'vue';
const reservationStore = useReservationStore(); const reservationStore = useReservationStore();
import { TimestampOrNull, parsed, today } from '@quasar/quasar-ui-qcalendar'; import {
TimestampOrNull,
getDate,
parsed,
today,
} from '@quasar/quasar-ui-qcalendar';
import { QCalendarDay } from '@quasar/quasar-ui-qcalendar'; import { QCalendarDay } from '@quasar/quasar-ui-qcalendar';
import { date } from 'quasar'; import { date } from 'quasar';
import { Timestamp } from '@quasar/quasar-ui-qcalendar'; import { Timestamp } from '@quasar/quasar-ui-qcalendar';
@@ -87,7 +92,7 @@ function slotStyle(
} }
function reservationEvents(timestamp: Timestamp) { function reservationEvents(timestamp: Timestamp) {
return reservationStore.getBoatReservations(timestamp); return reservationStore.getReservationsByDate(getDate(timestamp));
} }
function onMoved(data: Event) { function onMoved(data: Event) {
console.log('onMoved', data); console.log('onMoved', data);

View File

@@ -23,7 +23,7 @@
> >
<template #day="{ scope }"> <template #day="{ scope }">
<div <div
v-if="getIntervals(scope.timestamp, scope.resource)" v-if="filteredIntervals(scope.timestamp, scope.resource).length"
style=" style="
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -33,21 +33,22 @@
" "
> >
<template <template
v-for="block in getIntervals( v-for="block in sortedIntervals(
scope.timestamp, scope.timestamp,
scope.resource scope.resource
).sort((a, b) => Date.parse(a.start) - Date.parse(b.start))" )"
: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') }}
<q-popup-edit <!-- <q-popup-edit
:model-value="block" :model-value="block"
v-slot="scope" v-slot="scope"
buttons buttons
@save="updateInterval(block)" @save="saveInterval"
> >
TODO: Why isn't this saving?
<q-input <q-input
:model-value="date.formatDate(scope.value.start, 'HH:mm')" :model-value="date.formatDate(scope.value.start, 'HH:mm')"
dense dense
@@ -56,13 +57,14 @@
label="start" label="start"
@keyup.enter="scope.set" @keyup.enter="scope.set"
@update:model-value=" @update:model-value="
(t) => (t) => {
(block.start = buildISODate( block.start = new Date(
date.formatDate(scope.value.start, 'YYYY-MM-DD'),t as string scope.value.start.split('T')[0] + 'T' + t
)) ).toISOString();
}
" "
/> />
<!-- TODO: Clean this up --> TODO: Clean this up
<q-input <q-input
:model-value="date.formatDate(scope.value.end, 'HH:mm')" :model-value="date.formatDate(scope.value.end, 'HH:mm')"
dense dense
@@ -71,12 +73,12 @@
@keyup.enter="scope.set" @keyup.enter="scope.set"
@update:model-value=" @update:model-value="
(t) => (t) =>
(block.end = buildISODate( (block.end = new Date(
date.formatDate(scope.value.end, 'YYYY-MM-DD'),t as string scope.value.end.split('T')[0] + 'T' + t
)) ).toISOString())
" "
/> />
</q-popup-edit> </q-chip </q-popup-edit>--> </q-chip
><q-btn ><q-btn
size="xs" size="xs"
icon="delete" icon="delete"
@@ -89,7 +91,7 @@
</q-calendar-scheduler> </q-calendar-scheduler>
</div> </div>
</div> </div>
<div class="q-pa-md" style="width: 400"> <div class="q-pa-md" style="width: 400px">
<q-list padding bordered class="rounded-borders"> <q-list padding bordered class="rounded-borders">
<q-item> <q-item>
<q-item-section> <q-item-section>
@@ -125,8 +127,12 @@
<div class="text-h6">Warning!</div> <div class="text-h6">Warning!</div>
</q-card-section> </q-card-section>
<q-card-section class="q-pt-none"> <q-card-section class="q-pt-none">
This will overwrite existing blocks! Conflicting times! Please delete overlapped items!
{{ overlapped }} <q-chip 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.end, 'hh:mm') }}
</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 flat label="OK" color="primary" v-close-popup />
@@ -147,7 +153,6 @@ import {
useScheduleStore, useScheduleStore,
} from 'src/stores/schedule'; } from 'src/stores/schedule';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { buildISODate } from 'src/utils/misc';
import type { import type {
Interval, Interval,
IntervalTemplate, IntervalTemplate,
@@ -160,19 +165,17 @@ import { storeToRefs } from 'pinia';
const selectedDate = ref(today()); const selectedDate = ref(today());
const { fetchBoats } = useBoatStore(); const { fetchBoats } = useBoatStore();
const { getIntervals, fetchIntervals, updateInterval, fetchIntervalTemplates } = const scheduleStore = useScheduleStore();
useScheduleStore();
const { boats } = storeToRefs(useBoatStore()); const { boats } = storeToRefs(useBoatStore());
const { timeblockTemplates } = storeToRefs(useScheduleStore()); const { timeblockTemplates } = storeToRefs(useScheduleStore());
const calendar = ref(); const calendar = ref();
const overlapped = ref(); const overlapped = ref();
const blankTemplate: IntervalTemplate = { const alert = ref(false);
const newTemplate = ref<IntervalTemplate>({
$id: '', $id: '',
name: 'NewTemplate', name: 'NewTemplate',
timeTuples: [['09:00', '12:00']], timeTuples: [['09:00', '12:00']],
}; });
const newTemplate = ref<IntervalTemplate>({ ...blankTemplate });
const alert = ref(false);
/* TODOS: /* TODOS:
* Need more validation: * Need more validation:
@@ -183,20 +186,37 @@ const alert = ref(false);
onMounted(async () => { onMounted(async () => {
await fetchBoats(); await fetchBoats();
await fetchIntervals(); await scheduleStore.fetchIntervals();
await fetchIntervalTemplates(); await scheduleStore.fetchIntervalTemplates();
}); });
const filteredIntervals = (date: Timestamp, boat: Boat) => {
return scheduleStore.getIntervals(date, boat).value;
};
const sortedIntervals = (date: Timestamp, boat: Boat) => {
return filteredIntervals(date, boat).sort(
(a, b) => Date.parse(a.start) - Date.parse(b.start)
);
};
function resetNewTemplate() { function resetNewTemplate() {
newTemplate.value = { ...blankTemplate }; newTemplate.value = {
$id: 'unsaved',
name: 'NewTemplate',
timeTuples: [['09:00', '12:00']],
};
} }
function createTemplate() { function createTemplate() {
newTemplate.value.$id = 'unsaved'; newTemplate.value.$id = 'unsaved';
} }
function createIntervals(boat: Boat, templateId: string, date: string) { function createIntervals(boat: Boat, templateId: string, date: string) {
timeBlocksFromTemplate(boat, templateId, date)?.map((block) => const intervals = timeBlocksFromTemplate(boat, templateId, date);
useScheduleStore().createInterval(block) intervals.forEach((interval) => scheduleStore.createInterval(interval));
); }
function getIntervals(date: Timestamp, boat: Boat) {
return scheduleStore.getIntervals(date, boat);
} }
function timeBlocksFromTemplate( function timeBlocksFromTemplate(
@@ -204,25 +224,28 @@ function timeBlocksFromTemplate(
templateId: string, templateId: string,
date: string date: string
): Interval[] { ): Interval[] {
const timeBlock = timeblockTemplates.value.find((t) => t.$id === templateId); const template = timeblockTemplates.value.find((t) => t.$id === templateId);
return ( return template
timeBlock?.timeTuples.map((tb: TimeTuple) => ? template.timeTuples.map((timeTuple: TimeTuple) =>
buildInterval(boat, tb, date) buildInterval(boat, timeTuple, date)
) || [] )
); : [];
} }
function deleteBlock(block: Interval) { function deleteBlock(block: Interval) {
if (block.$id) { if (block.$id) {
useScheduleStore().deleteInterval(block.$id); scheduleStore.deleteInterval(block.$id);
} }
return false;
} }
function onDragEnter(e: DragEvent, type: string) { function onDragEnter(e: DragEvent, type: string) {
if (type === 'day' || type === 'head-day') { if (type === 'day' || type === 'head-day') {
e.preventDefault(); e.preventDefault();
if (e.target instanceof HTMLDivElement) if (
e.target instanceof HTMLDivElement &&
(e.target.classList.contains('q-calendar-scheduler__head--day') ||
e.target.classList.contains('q-calendar-scheduler__day'))
)
e.target.classList.add('bg-secondary'); e.target.classList.add('bg-secondary');
} }
} }
@@ -230,16 +253,18 @@ function onDragEnter(e: DragEvent, type: string) {
function onDragOver(e: DragEvent, type: string) { function onDragOver(e: DragEvent, type: string) {
if (type === 'day' || type === 'head-day') { if (type === 'day' || type === 'head-day') {
e.preventDefault(); e.preventDefault();
return true;
} }
} }
function onDragLeave(e: DragEvent, type: string) { function onDragLeave(e: DragEvent, type: string) {
if (type === 'day' || type === 'head-day') { if (type === 'day' || type === 'head-day') {
e.preventDefault(); e.preventDefault();
if (e.target instanceof HTMLDivElement) if (
e.target instanceof HTMLDivElement &&
(e.target.classList.contains('q-calendar-scheduler__head--day') ||
e.target.classList.contains('q-calendar-scheduler__day'))
)
e.target.classList.remove('bg-secondary'); e.target.classList.remove('bg-secondary');
return false;
} }
} }
@@ -249,34 +274,28 @@ function onDrop(
type: string, type: string,
scope: { resource: Boat; timestamp: Timestamp } scope: { resource: Boat; timestamp: Timestamp }
) { ) {
if (e.target instanceof HTMLDivElement)
e.target.classList.remove('bg-secondary');
if ((type === 'day' || type === 'head-day') && e.dataTransfer) { if ((type === 'day' || type === 'head-day') && e.dataTransfer) {
const templateId = e.dataTransfer.getData('ID'); const templateId = e.dataTransfer.getData('ID');
const date = scope.timestamp.date; const date = scope.timestamp.date;
if (type === 'head-day') { const resource = scope.resource;
overlapped.value = boats.value.map((boat) => const existingIntervals = getIntervals(scope.timestamp, resource).value;
const boatsToApply = type === 'head-day' ? boats.value : [resource];
overlapped.value = boatsToApply
.map((boat) =>
blocksOverlapped( blocksOverlapped(
getIntervals(scope.timestamp, boat).concat( existingIntervals.concat(
timeBlocksFromTemplate(boat, templateId, date) timeBlocksFromTemplate(boat, templateId, date)
) )
) )
); )
if (overlapped.value.length === 0) { .flat(1);
boats.value.map((b) => createIntervals(b, templateId, date)); if (overlapped.value.length === 0) {
} else { boatsToApply.map((b) => createIntervals(b, templateId, date));
alert.value = true;
}
} else { } else {
overlapped.value = blocksOverlapped( alert.value = true;
getIntervals(scope.timestamp, scope.resource).concat(
timeBlocksFromTemplate(scope.resource, templateId, date)
)
);
if (overlapped.value.length === 0) {
createIntervals(scope.resource, templateId, date);
} else {
alert.value = true;
}
} }
} }
if (e.target instanceof HTMLDivElement) if (e.target instanceof HTMLDivElement)

View File

@@ -44,12 +44,16 @@ export const useAuthStore = defineStore('auth', () => {
'/userinfo/' + id, '/userinfo/' + id,
ExecutionMethod.GET ExecutionMethod.GET
) )
.then( .then((res) => {
(res) => (userNames.value[id] = JSON.parse(res.responseBody).name) if (res.responseBody) {
); userNames.value[id] = JSON.parse(res.responseBody).name;
} else {
console.error(res, id);
}
});
} }
} catch (e) { } catch (e) {
console.log('Failed to get username. Error: ' + e); console.error('Failed to get username. Error: ' + e);
} }
return userNames.value[id]; return userNames.value[id];
} }

View File

@@ -1,26 +1,109 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import type { Reservation } from './schedule.types'; import type { Reservation } from './schedule.types';
import { ref } from 'vue'; import { computed, ref } from 'vue';
import { AppwriteIds, databases } from 'src/boot/appwrite'; import { AppwriteIds, databases } from 'src/boot/appwrite';
import { Timestamp, parsed } from '@quasar/quasar-ui-qcalendar'; import { Query } from 'appwrite';
import { date } from 'quasar';
import { Timestamp, parseDate, today } from '@quasar/quasar-ui-qcalendar';
export const useReservationStore = defineStore('reservation', () => { export const useReservationStore = defineStore('reservation', () => {
const reservations = ref<Reservation[]>([]); type LoadingTypes = 'loaded' | 'pending' | undefined;
const reservations = ref<Map<string, Reservation>>(new Map());
const datesLoaded = ref<Record<string, LoadingTypes>>({});
// Fetch reservations for a specific date range
const fetchReservationsForDateRange = async (
start: string = today(),
end: string = start
) => {
const startDate = new Date(start + 'T00:00');
const endDate = new Date(end + 'T23:59');
if (getUnloadedDates(startDate, endDate).length === 0) return;
setDateLoaded(startDate, endDate, 'pending');
try {
const response = await databases.listDocuments(
AppwriteIds.databaseId,
AppwriteIds.collection.reservation,
[
Query.greaterThanEqual('end', startDate.toISOString()),
Query.lessThanEqual('start', endDate.toISOString()),
]
);
response.documents.forEach((d) =>
reservations.value.set(d.$id, d as Reservation)
);
setDateLoaded(startDate, endDate, 'loaded');
} catch (error) {
console.error('Failed to fetch reservations', error);
setDateLoaded(startDate, endDate, undefined);
}
};
// Set the loading state for dates
const setDateLoaded = (start: Date, end: Date, state: LoadingTypes) => {
if (start > end) return [];
let curDate = start;
while (curDate < end) {
datesLoaded.value[(parseDate(curDate) as Timestamp).date] = state;
curDate = date.addToDate(curDate, { days: 1 });
}
};
const getUnloadedDates = (start: Date, end: Date): string[] => {
if (start > end) return [];
let curDate = start;
const unloaded = [];
while (curDate < end) {
const parsedDate = (parseDate(curDate) as Timestamp).date;
if (datesLoaded.value[parsedDate] === undefined)
unloaded.push(parsedDate);
curDate = date.addToDate(curDate, { days: 1 });
}
return unloaded;
};
// Get reservations by date and optionally filter by boat
const getReservationsByDate = (
searchDate: string,
boat?: string
): Reservation[] => {
if (!datesLoaded.value[searchDate]) {
fetchReservationsForDateRange(searchDate);
}
const dayStart = new Date(searchDate + 'T00:00');
const dayEnd = new Date(searchDate + 'T23:59');
return computed(() => {
return Array.from(reservations.value.values()).filter((reservation) => {
const reservationStart = new Date(reservation.start);
const reservationEnd = new Date(reservation.end);
const isWithinDay =
reservationStart < dayEnd && reservationEnd > dayStart;
const matchesBoat = boat ? boat === reservation.resource : true;
return isWithinDay && matchesBoat;
});
}).value;
};
// Get conflicting reservations for a resource within a time range
const getConflictingReservations = ( const getConflictingReservations = (
resource: string, resource: string,
start: Date, start: Date,
end: Date end: Date
): Reservation[] => { ): Reservation[] => {
const overlapped = reservations.value.filter( return Array.from(reservations.value.values()).filter(
(entry: Reservation) => (entry) =>
entry.resource == resource && entry.resource === resource &&
new Date(entry.start) < end && new Date(entry.start) < end &&
new Date(entry.end) > start new Date(entry.end) > start
); );
return overlapped;
}; };
// Check if a resource has time overlap
const isResourceTimeOverlapped = ( const isResourceTimeOverlapped = (
resource: string, resource: string,
start: Date, start: Date,
@@ -29,6 +112,7 @@ export const useReservationStore = defineStore('reservation', () => {
return getConflictingReservations(resource, start, end).length > 0; return getConflictingReservations(resource, start, end).length > 0;
}; };
// Check if a reservation overlaps with existing reservations
const isReservationOverlapped = (res: Reservation): boolean => { const isReservationOverlapped = (res: Reservation): boolean => {
return isResourceTimeOverlapped( return isResourceTimeOverlapped(
res.resource, res.resource,
@@ -37,46 +121,9 @@ export const useReservationStore = defineStore('reservation', () => {
); );
}; };
const addOrCreateReservation = (reservation: Reservation) => {
const index = reservations.value.findIndex(
(res) => res.id == reservation.id
);
index != -1
? (reservations.value[index] = reservation)
: reservations.value.push(reservation);
};
async function fetchReservations() {
try {
const response = await databases.listDocuments(
AppwriteIds.databaseId,
AppwriteIds.collection.reservation
);
reservations.value = response.documents as Reservation[];
} catch (error) {
console.error('Failed to fetch timeblocks', error);
}
}
const getBoatReservations = (
searchDate: Timestamp,
boat?: string
): Reservation[] => {
const result = reservations.value.filter((x) => {
return (
((parsed(x.start)?.date == searchDate.date ||
parsed(x.end)?.date == searchDate.date) && // Part of reservation falls on day
x.resource != undefined && // A boat is defined
!boat) ||
x.resource == boat // A specific boat has been passed, and matches
);
});
return result;
};
return { return {
getBoatReservations, getReservationsByDate,
fetchReservations, fetchReservationsForDateRange,
addOrCreateReservation,
isReservationOverlapped, isReservationOverlapped,
isResourceTimeOverlapped, isResourceTimeOverlapped,
getConflictingReservations, getConflictingReservations,

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ref } from 'vue'; import { ComputedRef, computed, ref } from 'vue';
import { Boat } from './boat'; import { Boat } from './boat';
import { import {
Timestamp, Timestamp,
@@ -11,7 +11,6 @@ import {
import { IntervalTemplate, TimeTuple, Interval } from './schedule.types'; import { IntervalTemplate, TimeTuple, Interval } from './schedule.types';
import { AppwriteIds, databases } from 'src/boot/appwrite'; import { AppwriteIds, databases } from 'src/boot/appwrite';
import { ID, Models } from 'appwrite'; import { ID, Models } from 'appwrite';
import { buildISODate } from 'src/utils/misc';
export function arrayToTimeTuples(arr: string[]) { export function arrayToTimeTuples(arr: string[]) {
const timeTuples: TimeTuple[] = []; const timeTuples: TimeTuple[] = [];
@@ -70,28 +69,35 @@ export function buildInterval(
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, boatId: resource.$id,
start: buildISODate(blockDate, time[0]), start: new Date(blockDate + 'T' + time[0]).toISOString(),
end: buildISODate(blockDate, time[1]), end: new Date(blockDate + 'T' + time[1]).toISOString(),
}; };
return result; return result;
} }
export const useScheduleStore = defineStore('schedule', () => { export const useScheduleStore = defineStore('schedule', () => {
// TODO: Implement functions to dynamically pull this data. // TODO: Implement functions to dynamically pull this data.
const timeblocks = ref<Interval[]>([]); const intervals = ref<Interval[]>([]);
const timeblockTemplates = ref<IntervalTemplate[]>([]); const intervalTemplates = ref<IntervalTemplate[]>([]);
const getIntervals = (date: Timestamp, boat: Boat): Interval[] => { const getIntervals = (
return timeblocks.value.filter((block) => { date: Timestamp,
return ( boat: Boat
compareDate(parseDate(new Date(block.start)) as Timestamp, date) && ): ComputedRef<Interval[]> => {
block.boatId === boat.$id return computed(() =>
); intervals.value.filter((block) => {
}); return (
compareDate(parseDate(new Date(block.start)) as Timestamp, date) &&
block.boatId === boat.$id
);
})
);
}; };
const getIntervalsForDate = (date: string): Interval[] => { const getIntervalsForDate = (date: string): Interval[] => {
// TODO: This needs to actually make sure we have the dates we need, stay in sync, etc. // TODO: This needs to actually make sure we have the dates we need, stay in sync, etc.
return timeblocks.value.filter((b) => {
return intervals.value.filter((b) => {
return compareDate( return compareDate(
parseDate(new Date(b.start)) as Timestamp, parseDate(new Date(b.start)) as Timestamp,
parsed(date) as Timestamp parsed(date) as Timestamp
@@ -105,7 +111,7 @@ export const useScheduleStore = defineStore('schedule', () => {
AppwriteIds.databaseId, AppwriteIds.databaseId,
AppwriteIds.collection.timeBlock AppwriteIds.collection.timeBlock
); );
timeblocks.value = response.documents as Interval[]; intervals.value = response.documents as Interval[];
} catch (error) { } catch (error) {
console.error('Failed to fetch timeblocks', error); console.error('Failed to fetch timeblocks', error);
} }
@@ -117,7 +123,7 @@ export const useScheduleStore = defineStore('schedule', () => {
AppwriteIds.databaseId, AppwriteIds.databaseId,
AppwriteIds.collection.timeBlockTemplate AppwriteIds.collection.timeBlockTemplate
); );
timeblockTemplates.value = response.documents.map( intervalTemplates.value = response.documents.map(
(d: Models.Document): IntervalTemplate => { (d: Models.Document): IntervalTemplate => {
return { return {
...d, ...d,
@@ -153,7 +159,7 @@ export const useScheduleStore = defineStore('schedule', () => {
ID.unique(), ID.unique(),
interval interval
); );
timeblocks.value.push(response as Interval); intervals.value.push(response as Interval);
} catch (e) { } catch (e) {
console.error('Error creating Interval: ' + e); console.error('Error creating Interval: ' + e);
} }
@@ -167,7 +173,8 @@ export const useScheduleStore = defineStore('schedule', () => {
interval.$id, interval.$id,
{ ...interval, $id: undefined } { ...interval, $id: undefined }
); );
timeblocks.value.push(response as Interval); intervals.value.push(response as Interval);
console.log(`Saved Interval: ${interval.$id}`);
} else { } else {
console.error('Update interval called without an ID'); console.error('Update interval called without an ID');
} }
@@ -182,7 +189,7 @@ export const useScheduleStore = defineStore('schedule', () => {
AppwriteIds.collection.timeBlock, AppwriteIds.collection.timeBlock,
id id
); );
timeblocks.value = timeblocks.value.filter((block) => block.$id !== id); intervals.value = intervals.value.filter((block) => block.$id !== id);
} catch (e) { } catch (e) {
console.error('Error deleting Interval: ' + e); console.error('Error deleting Interval: ' + e);
} }
@@ -195,7 +202,7 @@ export const useScheduleStore = defineStore('schedule', () => {
ID.unique(), ID.unique(),
{ name: template.name, timeTuple: template.timeTuples.flat(2) } { name: template.name, timeTuple: template.timeTuples.flat(2) }
); );
timeblockTemplates.value.push(response as IntervalTemplate); intervalTemplates.value.push(response as IntervalTemplate);
} catch (e) { } catch (e) {
console.error('Error updating IntervalTemplate: ' + e); console.error('Error updating IntervalTemplate: ' + e);
} }
@@ -207,7 +214,7 @@ export const useScheduleStore = defineStore('schedule', () => {
AppwriteIds.collection.timeBlockTemplate, AppwriteIds.collection.timeBlockTemplate,
id id
); );
timeblockTemplates.value = timeblockTemplates.value.filter( intervalTemplates.value = intervalTemplates.value.filter(
(template) => template.$id !== id (template) => template.$id !== id
); );
} catch (e) { } catch (e) {
@@ -228,7 +235,7 @@ export const useScheduleStore = defineStore('schedule', () => {
timeTuple: template.timeTuples.flat(2), timeTuple: template.timeTuples.flat(2),
} }
); );
timeblockTemplates.value = timeblockTemplates.value.map((b) => intervalTemplates.value = intervalTemplates.value.map((b) =>
b.$id !== id b.$id !== id
? b ? b
: ({ : ({
@@ -242,8 +249,8 @@ export const useScheduleStore = defineStore('schedule', () => {
}; };
return { return {
timeblocks, timeblocks: intervals,
timeblockTemplates, timeblockTemplates: intervalTemplates,
getIntervalsForDate, getIntervalsForDate,
getIntervals, getIntervals,
fetchIntervals, fetchIntervals,

View File

@@ -3,8 +3,8 @@ import { Models } from 'appwrite';
export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined; export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined;
export type Reservation = Partial<Models.Document> & { export type Reservation = Partial<Models.Document> & {
user: string; user: string;
start: string; // ISODate start: string;
end: string; //ISODate end: string;
resource: string; // Boat ID resource: string; // Boat ID
status?: StatusTypes; status?: StatusTypes;
}; };

View File

@@ -151,7 +151,6 @@ export const useTaskStore = defineStore('tasks', {
const result = state.tasks.filter((task) => const result = state.tasks.filter((task) =>
task.title.toLowerCase().includes(searchQuery.toLowerCase()) task.title.toLowerCase().includes(searchQuery.toLowerCase())
); );
console.log(result);
return result; return result;
}, },
}, },

View File

@@ -1,7 +1,3 @@
export function buildISODate(date: string, time: string | null): string {
return new Date(date + 'T' + time || '00:00').toISOString();
}
export function getNewId(): string { export function getNewId(): string {
return [...Array(20)] return [...Array(20)]
.map(() => Math.floor(Math.random() * 16).toString(16)) .map(() => Math.floor(Math.random() * 16).toString(16))