import { defineStore } from 'pinia'; import { ref } from 'vue'; import { Boat } from './boat'; import { Timestamp, parseDate, parsed, compareDate, } from '@quasar/quasar-ui-qcalendar'; import { Reservation, IntervalTemplate, TimeTuple, Interval, } from './schedule.types'; import { AppwriteIds, databases } from 'src/boot/appwrite'; import { ID, Models } from 'appwrite'; import { buildISODate } from 'src/utils/misc'; export function arrayToTimeTuples(arr: string[]) { const timeTuples: TimeTuple[] = []; for (let i = 0; i < arr.length; i += 2) { timeTuples.push([arr[i], arr[i + 1]]); } return timeTuples; } export function timeTuplesOverlapped(tuples: TimeTuple[]): Interval[] { return blocksOverlapped( tuples.map((tuples) => { return { boatId: '', start: '01/01/2001 ' + tuples[0], end: '01/01/2001 ' + tuples[1], }; }) ).map((t) => { return { ...t, start: t.start.split(' ')[1], end: t.end.split(' ')[1] }; }); } export function blocksOverlapped(blocks: Interval[] | Interval[]): Interval[] { return Array.from( new Set( blocks .sort((a, b) => Date.parse(a.start) - Date.parse(b.start)) .reduce((acc: Interval[], block, i, arr) => { if (i > 0 && block.start < arr[i - 1].end) acc.push(arr[i - 1], block); return acc; }, []) ) ); } export function copyTimeTuples(tuples: TimeTuple[]): TimeTuple[] { return tuples.map((t) => Object.assign([], t)); } export function copyIntervalTemplate( template: IntervalTemplate ): IntervalTemplate { return { ...template, timeTuples: copyTimeTuples(template.timeTuples || []), }; } export function buildInterval( resource: Boat, time: TimeTuple, blockDate: string ): Interval { /* 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. */ const result = { boatId: resource.$id, start: buildISODate(blockDate, time[0]), end: buildISODate(blockDate, time[1]), }; return result; } export const useScheduleStore = defineStore('schedule', () => { // TODO: Implement functions to dynamically pull this data. const reservations = ref([]); const timeblocks = ref([]); const timeblockTemplates = ref([]); const getIntervals = (date: Timestamp, boat: Boat): Interval[] => { return timeblocks.value.filter((block) => { return ( compareDate(parseDate(new Date(block.start)) as Timestamp, date) && block.boatId === boat.$id ); }); }; const getIntervalsForDate = (date: string): Interval[] => { // TODO: This needs to actually make sure we have the dates we need, stay in sync, etc. return timeblocks.value.filter((b) => { return compareDate( parseDate(new Date(b.start)) as Timestamp, parsed(date) as Timestamp ); }); }; 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; }; async function fetchIntervals() { try { const response = await databases.listDocuments( AppwriteIds.databaseId, AppwriteIds.collection.timeBlock ); timeblocks.value = response.documents as Interval[]; } catch (error) { console.error('Failed to fetch timeblocks', error); } } async function fetchIntervalTemplates() { try { const response = await databases.listDocuments( AppwriteIds.databaseId, AppwriteIds.collection.timeBlockTemplate ); timeblockTemplates.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 getConflicts = (timeblock: Interval, 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); // }; const getConflictingReservations = ( resource: string, start: Date, end: Date ): Reservation[] => { const overlapped = reservations.value.filter( (entry: Reservation) => entry.resource == resource && new Date(entry.start) < end && new Date(entry.end) > start ); return overlapped; }; const isResourceTimeOverlapped = ( resource: string, start: Date, end: Date ): boolean => { return getConflictingReservations(resource, start, end).length > 0; }; const isReservationOverlapped = (res: Reservation): boolean => { return isResourceTimeOverlapped( res.resource, new Date(res.start), new Date(res.end) ); }; const addOrCreateReservation = (reservation: Reservation) => { const index = reservations.value.findIndex( (res) => res.id == reservation.id ); index != -1 ? (reservations.value[index] = reservation) : reservations.value.push(reservation); }; const createInterval = async (interval: Interval) => { try { const response = await databases.createDocument( AppwriteIds.databaseId, AppwriteIds.collection.timeBlock, ID.unique(), interval ); timeblocks.value.push(response as Interval); } catch (e) { console.error('Error creating Interval: ' + e); } }; const updateInterval = async (interval: Interval) => { try { if (interval.$id) { const response = await databases.updateDocument( AppwriteIds.databaseId, AppwriteIds.collection.timeBlock, interval.$id, { ...interval, $id: undefined } ); timeblocks.value.push(response as Interval); } else { console.error('Update interval called without an ID'); } } catch (e) { console.error('Error updating Interval: ' + e); } }; const deleteInterval = async (id: string) => { try { await databases.deleteDocument( AppwriteIds.databaseId, AppwriteIds.collection.timeBlock, id ); timeblocks.value = timeblocks.value.filter((block) => block.$id !== id); } catch (e) { console.error('Error deleting Interval: ' + e); } }; const createIntervalTemplate = async (template: IntervalTemplate) => { try { const response = await databases.createDocument( AppwriteIds.databaseId, AppwriteIds.collection.timeBlockTemplate, ID.unique(), { name: template.name, timeTuple: template.timeTuples.flat(2) } ); timeblockTemplates.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.timeBlockTemplate, id ); timeblockTemplates.value = timeblockTemplates.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.timeBlockTemplate, id, { name: template.name, timeTuple: template.timeTuples.flat(2), } ); timeblockTemplates.value = timeblockTemplates.value.map((b) => b.$id !== id ? b : ({ ...response, timeTuples: arrayToTimeTuples(response.timeTuple), } as IntervalTemplate) ); } catch (e) { console.error('Error updating IntervalTemplate: ' + e); } }; return { reservations, timeblocks, timeblockTemplates, getBoatReservations, getConflictingReservations, addOrCreateReservation, isReservationOverlapped, isResourceTimeOverlapped, fetchReservations, getIntervalsForDate, getIntervals, fetchIntervals, fetchIntervalTemplates, createInterval, updateInterval, deleteInterval, createIntervalTemplate, deleteIntervalTemplate, updateIntervalTemplate, }; });