import { defineStore } from 'pinia'; import type { Reservation } from './schedule.types'; import { computed, ref, watch } from 'vue'; import { AppwriteIds, databases } from 'src/boot/appwrite'; import { ID, Query } from 'appwrite'; import { date, useQuasar } from 'quasar'; import { Timestamp, parseDate, today } from '@quasar/quasar-ui-qcalendar'; import { LoadingTypes } from 'src/utils/misc'; import { useAuthStore } from './auth'; import { isPast } from 'src/utils/schedule'; export const useReservationStore = defineStore('reservation', () => { const reservations = ref>(new Map()); const datesLoaded = ref>({}); const userReservations = ref>(new Map()); // TODO: Come up with a better way of storing reservations by date & reservations for user const authStore = useAuthStore(); const $q = useQuasar(); // Fetch reservations for a specific date range const fetchReservationsForDateRange = async ( start: string = today(), end: string = start ) => { const startDate = new Date(start < end ? start : end + 'T00:00'); const endDate = new Date(start < end ? end : start + '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, 'error'); } }; const getReservationById = async (id: string) => { try { const response = await databases.getDocument( AppwriteIds.databaseId, AppwriteIds.collection.reservation, id ); return response as Reservation; } catch (error) { console.error('Failed to fetch reservation: ', error); } }; const createOrUpdateReservation = async ( reservation: Reservation ): Promise => { let response; try { if (reservation.$id) { response = await databases.updateDocument( AppwriteIds.databaseId, AppwriteIds.collection.reservation, reservation.$id, reservation ); } else { response = await databases.createDocument( AppwriteIds.databaseId, AppwriteIds.collection.reservation, ID.unique(), reservation ); } reservations.value.set(response.$id, response as Reservation); userReservations.value.set(response.$id, response as Reservation); console.info('Reservation booked: ', response); return response as Reservation; } catch (e) { console.error('Error creating Reservation: ' + e); throw e; } }; const deleteReservation = async ( reservation: string | Reservation | null | undefined ) => { if (!reservation) return false; let id; if (typeof reservation === 'string') { id = reservation; } else if ('$id' in reservation && typeof reservation.$id === 'string') { id = reservation.$id; } else { return false; } const status = $q.notify({ color: 'secondary', textColor: 'white', message: 'Deleting Reservation', spinner: true, closeBtn: 'Dismiss', position: 'top', timeout: 0, group: false, }); try { await databases.deleteDocument( AppwriteIds.databaseId, AppwriteIds.collection.reservation, id ); reservations.value.delete(id); userReservations.value.delete(id); console.info(`Deleted reservation: ${id}`); status({ color: 'warning', message: 'Reservation Deleted', spinner: false, icon: 'delete', timeout: 4000, }); } catch (e) { console.error('Error deleting reservation: ' + e); status({ color: 'negative', message: 'Failed to Delete Reservation', spinner: false, icon: 'error', }); } }; // 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 = ( resource: string, start: Date, end: Date ): Reservation[] => { return Array.from(reservations.value.values()).filter( (entry) => entry.resource === resource && new Date(entry.start) < end && new Date(entry.end) > start ); }; // Check if a resource has time overlap const isResourceTimeOverlapped = ( resource: string, start: Date, end: Date ): boolean => { return getConflictingReservations(resource, start, end).length > 0; }; // Check if a reservation overlaps with existing reservations const isReservationOverlapped = (res: Reservation): boolean => { return isResourceTimeOverlapped( res.resource, new Date(res.start), new Date(res.end) ); }; const fetchUserReservations = async () => { if (!authStore.currentUser) return; try { const response = await databases.listDocuments( AppwriteIds.databaseId, AppwriteIds.collection.reservation, [Query.equal('user', authStore.currentUser.$id)] ); response.documents.forEach((d) => userReservations.value.set(d.$id, d as Reservation) ); } catch (error) { console.error('Failed to fetch reservations for user: ', error); } }; const sortedUserReservations = computed((): Reservation[] => [...userReservations.value?.values()].sort( (a, b) => new Date(b.start).getTime() - new Date(a.start).getTime() ) ); const futureUserReservations = computed((): Reservation[] => { if (!sortedUserReservations.value) return []; return sortedUserReservations.value.filter((b) => !isPast(b.end)); }); const pastUserReservations = computed((): Reservation[] => { if (!sortedUserReservations.value) return []; return sortedUserReservations.value?.filter((b) => isPast(b.end)); }); // Ensure reactivity for computed properties when Map is modified watch( reservations, () => { sortedUserReservations.value; futureUserReservations.value; pastUserReservations.value; }, { deep: true } ); watch( userReservations, () => { sortedUserReservations.value; futureUserReservations.value; pastUserReservations.value; }, { deep: true } ); return { getReservationsByDate, getReservationById, createOrUpdateReservation, deleteReservation, fetchReservationsForDateRange, isReservationOverlapped, isResourceTimeOverlapped, getConflictingReservations, fetchUserReservations, sortedUserReservations, futureUserReservations, pastUserReservations, userReservations, }; });