diff --git a/package.json b/package.json index 493f4b4..39e6d34 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@quasar/quasar-app-extension-qcalendar": "https://github.com/ptoal/quasar-ui-qcalendar/releases/download/v4.0.0-beta.19/app-extension.tgz", "@quasar/quasar-ui-qcalendar": "https://github.com/ptoal/quasar-ui-qcalendar/releases/download/v4.0.0-beta.19/qcalendar-ui.tgz", "appwrite": "^14.0.1", + "axios": "^1.6.8", "file": "^0.2.2", "pinia": "^2.1.7", "vue": "3", diff --git a/quasar.config.js b/quasar.config.js index f98860a..dbb0627 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -102,6 +102,12 @@ module.exports = configure(function (/* ctx */) { secure: false, rewrite: (path) => path.replace(/^\/api/, ''), }, + '/function': { + target: 'https://6640382951eacb568371.f.appwrite.toal.ca/', + changeOrigin: true, + secure: false, + rewrite: (path) => path.replace(/^\/function/, ''), + }, }, // For reverse-proxying via haproxy // hmr: { diff --git a/src/App.vue b/src/App.vue index e2eb8f3..b502525 100644 --- a/src/App.vue +++ b/src/App.vue @@ -16,6 +16,7 @@ onMounted(async () => { await useAuthStore().init(); await useScheduleStore().fetchIntervalTemplates(); await useScheduleStore().fetchIntervals(); + await useScheduleStore().fetchReservations(); await useBoatStore().fetchBoats(); }); diff --git a/src/boot/appwrite.ts b/src/boot/appwrite.ts index f432d99..ac8feae 100644 --- a/src/boot/appwrite.ts +++ b/src/boot/appwrite.ts @@ -1,5 +1,5 @@ import { boot } from 'quasar/wrappers'; -import { Client, Account, Databases, ID } from 'appwrite'; +import { Client, Account, Databases, Functions, ID } from 'appwrite'; import { useAuthStore } from 'src/stores/auth'; import { Dialog, Notify } from 'quasar'; import type { Router } from 'vue-router'; @@ -22,10 +22,11 @@ if (process.env.APPWRITE_API_ENDPOINT && process.env.APPWRITE_API_PROJECT) const AppwriteIds = { databaseId: '65ee1cbf9c2493faf15f', collection: { + boat: '66341910003e287cd71c', + reservation: '663f8847000b8f5e29bb', + skillTags: '66072582a74d94a4bd01', task: '65ee1cd5b550023fae4f', taskTags: '65ee21d72d5c8007c34c', - skillTags: '66072582a74d94a4bd01', - boat: '66341910003e287cd71c', timeBlock: '66361869002883fb4c4b', timeBlockTemplate: '66361f480007fdd639af', }, @@ -33,6 +34,8 @@ const AppwriteIds = { const account = new Account(client); const databases = new Databases(client); +const functions = new Functions(client); + let appRouter: Router; export default boot(async ({ router }) => { @@ -98,4 +101,13 @@ function login(email: string, password: string) { }); }); } -export { client, account, databases, ID, AppwriteIds, login, logout }; +export { + client, + account, + databases, + functions, + ID, + AppwriteIds, + login, + logout, +}; diff --git a/src/components/ResourceScheduleViewerComponent.vue b/src/components/ResourceScheduleViewerComponent.vue index 2965a02..70d6fb3 100644 --- a/src/components/ResourceScheduleViewerComponent.vue +++ b/src/components/ResourceScheduleViewerComponent.vue @@ -111,6 +111,7 @@ import { parseTimestamp, addToDate, Timestamp, + parsed, } from '@quasar/quasar-ui-qcalendar'; import { Boat, useBoatStore } from 'src/stores/boat'; import { useScheduleStore } from 'src/stores/schedule'; @@ -178,7 +179,7 @@ function getEvents(scope: ResourceIntervalScope) { return resourceEvents.map((event) => { return { - left: scope.timeStartPosX(parseDate(event.start)), + left: scope.timeStartPosX(parsed(event.start)), width: scope.timeDurationWidth( date.getDateDiff(event.end, event.start, 'minutes') ), diff --git a/src/components/scheduling/IntervalTemplateComponent.vue b/src/components/scheduling/IntervalTemplateComponent.vue index 7190e78..48889ff 100644 --- a/src/components/scheduling/IntervalTemplateComponent.vue +++ b/src/components/scheduling/IntervalTemplateComponent.vue @@ -155,7 +155,6 @@ function onDragStart(e: DragEvent, template: IntervalTemplate) { } } const saveTemplate = (evt: Event, template: IntervalTemplate | undefined) => { - console.log(template); if (!template) return false; overlapped.value = timeTuplesOverlapped(template.timeTuples); if (overlapped.value.length > 0) { @@ -163,7 +162,6 @@ const saveTemplate = (evt: Event, template: IntervalTemplate | undefined) => { } else { edit.value = false; if (template.$id && template.$id !== 'unsaved') { - console.log(template.$id); scheduleStore.updateIntervalTemplate(template, template.$id); } else { scheduleStore.createIntervalTemplate(template); diff --git a/src/components/scheduling/boat/BoatScheduleTableComponent.vue b/src/components/scheduling/boat/BoatScheduleTableComponent.vue index e565f44..cfe50d9 100644 --- a/src/components/scheduling/boat/BoatScheduleTableComponent.vue +++ b/src/components/scheduling/boat/BoatScheduleTableComponent.vue @@ -38,23 +38,23 @@ {{ selectedBlock?.$id === block.$id ? 'Selected' : 'Available' }} - + @@ -76,7 +76,8 @@ import CalendarHeaderComponent from './CalendarHeaderComponent.vue'; import { ref, computed } from 'vue'; import { useBoatStore } from 'src/stores/boat'; import { useScheduleStore } from 'src/stores/schedule'; -import { Interval } from 'src/stores/schedule.types'; +import { useAuthStore } from 'src/stores/auth'; +import { Interval, Reservation } from 'src/stores/schedule.types'; import { storeToRefs } from 'pinia'; const scheduleStore = useScheduleStore(); @@ -89,18 +90,22 @@ const calendar = ref(null); function handleSwipe({ ...event }) { event.direction === 'right' ? calendar.value?.prev() : calendar.value?.next(); } -// function reservationStyles( -// reservation: Reservation, -// timeStartPos: (t: string) => string, -// timeDurationHeight: (d: number) => string -// ) { -// return genericBlockStyle( -// parseDate(reservation.start) as Timestamp, -// parseDate(reservation.end) as Timestamp, -// timeStartPos, -// timeDurationHeight -// ); -// } +function reservationStyles( + reservation: Reservation, + timeStartPos: (t: string) => string, + timeDurationHeight: (d: number) => string +) { + return genericBlockStyle( + parseDate(new Date(reservation.start)) as Timestamp, + parseDate(new Date(reservation.end)) as Timestamp, + timeStartPos, + timeDurationHeight + ); +} + +function getUserName(userid: string) { + return useAuthStore().getUserNameById(userid); +} function blockStyles( block: Interval, @@ -176,8 +181,13 @@ const boatBlocks = computed((): BoatBlocks => { }); function getBoatBlocks(scope: DayBodyScope): Interval[] { - return boats.value[scope.columnIndex] - ? boatBlocks.value[boats.value[scope.columnIndex].$id] + const boat = boats.value[scope.columnIndex]; + return boat ? boatBlocks.value[boat.$id] : []; +} +function getBoatReservations(scope: DayBodyScope): Reservation[] { + const boat = boats.value[scope.columnIndex]; + return boat + ? scheduleStore.getBoatReservations(scope.timestamp, boat.$id) : []; } diff --git a/src/components/task/TaskEditComponent.vue b/src/components/task/TaskEditComponent.vue index 9edd645..b00af94 100644 --- a/src/components/task/TaskEditComponent.vue +++ b/src/components/task/TaskEditComponent.vue @@ -252,14 +252,11 @@ const dateRule = (val: string) => { const router = useRouter(); async function onSubmit() { - //console.log(modifiedTask); try { if (modifiedTask.$id) { await taskStore.updateTask(modifiedTask); - console.log('Updated Task: ' + modifiedTask.$id); } else { await taskStore.addTask(modifiedTask); - console.log('Created Task'); } router.go(-1); } catch (error) { diff --git a/src/pages/schedule/BoatReservationPage.vue b/src/pages/schedule/BoatReservationPage.vue index 2ea4a78..1a0d65a 100644 --- a/src/pages/schedule/BoatReservationPage.vue +++ b/src/pages/schedule/BoatReservationPage.vue @@ -136,7 +136,6 @@ watch(timeblock, (tb_new) => { ); bookingForm.value.startDate = date.formatDate(tb_new?.start, dateFormat); bookingForm.value.endDate = date.formatDate(tb_new?.end, dateFormat); - console.log(tb_new); }); // //TODO: Turn this into a validator. diff --git a/src/pages/schedule/BoatScheduleView.vue b/src/pages/schedule/BoatScheduleView.vue index 26d9883..f550de5 100644 --- a/src/pages/schedule/BoatScheduleView.vue +++ b/src/pages/schedule/BoatScheduleView.vue @@ -37,7 +37,9 @@ {{ event.user }} {{ - event.start + ' - ' + event.resource.name + event.start + + ' - ' + + boatStore.getBoatById(event.resource)?.name }} @@ -54,12 +56,14 @@ import { useScheduleStore } from 'src/stores/schedule'; import { Reservation } from 'src/stores/schedule.types'; import { ref } from 'vue'; const scheduleStore = useScheduleStore(); -import { TimestampOrNull, parseDate, today } from '@quasar/quasar-ui-qcalendar'; +import { TimestampOrNull, 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 { useBoatStore } from 'src/stores/boat'; const selectedDate = ref(today()); +const boatStore = useBoatStore(); // Method declarations @@ -74,7 +78,7 @@ function slotStyle( 'align-items': 'flex-start', }; if (timeStartPos && timeDurationHeight) { - s.top = timeStartPos(parseDate(event.start)) + 'px'; + s.top = timeStartPos(parsed(event.start)) + 'px'; s.height = timeDurationHeight(date.getDateDiff(event.end, event.start, 'minutes')) + 'px'; diff --git a/src/pages/schedule/ManageCalendar.vue b/src/pages/schedule/ManageCalendar.vue index dea5f13..291927a 100644 --- a/src/pages/schedule/ManageCalendar.vue +++ b/src/pages/schedule/ManageCalendar.vue @@ -260,7 +260,6 @@ function onDrop( ) ) ); - console.log(overlapped); if (overlapped.value.length === 0) { boats.value.map((b) => createIntervals(b, templateId, date)); } else { diff --git a/src/stores/auth.ts b/src/stores/auth.ts index ecf904f..c76ac4c 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -1,10 +1,11 @@ import { defineStore } from 'pinia'; -import { ID, account } from 'boot/appwrite'; -import { OAuthProvider, type Models } from 'appwrite'; +import { ID, account, functions } from 'boot/appwrite'; +import { ExecutionMethod, OAuthProvider, type Models } from 'appwrite'; import { ref } from 'vue'; export const useAuthStore = defineStore('auth', () => { const currentUser = ref | null>(null); + const userNames = ref>({}); async function init() { try { @@ -31,9 +32,39 @@ export const useAuthStore = defineStore('auth', () => { currentUser.value = await account.get(); } + function getUserNameById(id: string) { + try { + if (!userNames.value[id]) { + userNames.value[id] = ''; + functions + .createExecution( + '664038294b5473ef0c8d', + '', + false, + '/userinfo/' + id, + ExecutionMethod.GET + ) + .then( + (res) => (userNames.value[id] = JSON.parse(res.responseBody).name) + ); + } + } catch (e) { + console.log('Failed to get username. Error: ' + e); + } + return userNames.value[id]; + } + function logout() { return account.deleteSession('current').then((currentUser.value = null)); } - return { currentUser, register, login, googleLogin, logout, init }; + return { + currentUser, + getUserNameById, + register, + login, + googleLogin, + logout, + init, + }; }); diff --git a/src/stores/boat.ts b/src/stores/boat.ts index 68cf8b7..1ed1b9b 100644 --- a/src/stores/boat.ts +++ b/src/stores/boat.ts @@ -39,5 +39,9 @@ export const useBoatStore = defineStore('boat', () => { } } - return { boats, fetchBoats }; + const getBoatById = (id: string): Boat | null => { + return boats.value.find((b) => b.$id === id) || null; + }; + + return { boats, fetchBoats, getBoatById }; }); diff --git a/src/stores/sampledata/schedule.ts b/src/stores/sampledata/schedule.ts index 101d0de..d080ff5 100644 --- a/src/stores/sampledata/schedule.ts +++ b/src/stores/sampledata/schedule.ts @@ -69,7 +69,7 @@ export function getSampleReservations(): Reservation[] { user: 'John Smith', start: '7:00', end: '10:00', - boat: '1', + boat: '66359729003825946ae1', status: 'confirmed', }, { @@ -77,7 +77,7 @@ export function getSampleReservations(): Reservation[] { user: 'Bob Barker', start: '16:00', end: '19:00', - boat: '1', + boat: '66359729003825946ae1', status: 'confirmed', }, { @@ -85,7 +85,7 @@ export function getSampleReservations(): Reservation[] { user: 'Peter Parker', start: '7:00', end: '13:00', - boat: '4', + boat: '663597030029b71c7a9b', status: 'tentative', }, { @@ -93,7 +93,7 @@ export function getSampleReservations(): Reservation[] { user: 'Vince McMahon', start: '10:00', end: '13:00', - boat: '2', + boat: '663597030029b71c7a9b', status: 'pending', }, { @@ -101,7 +101,7 @@ export function getSampleReservations(): Reservation[] { user: 'Heather Graham', start: '13:00', end: '19:00', - boat: '4', + boat: '663596b9000235ffea55', status: 'confirmed', }, { @@ -109,7 +109,7 @@ export function getSampleReservations(): Reservation[] { user: 'Lawrence Fishburne', start: '13:00', end: '16:00', - boat: '3', + boat: '663596b9000235ffea55', }, ]; const boatStore = useBoatStore(); @@ -131,9 +131,11 @@ export function getSampleReservations(): Reservation[] { return { id: entry.id, user: entry.user, - start: date.adjustDate(now, makeOpts(splitTime(entry.start))), - end: date.adjustDate(now, makeOpts(splitTime(entry.end))), - resource: boat, + start: date + .adjustDate(now, makeOpts(splitTime(entry.start))) + .toISOString(), + end: date.adjustDate(now, makeOpts(splitTime(entry.end))).toISOString(), + resource: boat.$id, reservationDate: now, status: entry.status as StatusTypes, }; diff --git a/src/stores/schedule.ts b/src/stores/schedule.ts index 090d9bd..69673d1 100644 --- a/src/stores/schedule.ts +++ b/src/stores/schedule.ts @@ -108,19 +108,32 @@ export const useScheduleStore = defineStore('schedule', () => { }); }; + 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[] => { - return reservations.value.filter((x) => { + const result = reservations.value.filter((x) => { return ( - ((parseDate(x.start)?.date == searchDate.date || - parseDate(x.end)?.date == searchDate.date) && // Part of reservation falls on day + ((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.$id == boat // A specific boat has been passed, and matches + x.resource == boat // A specific boat has been passed, and matches ); }); + return result; }; async function fetchIntervals() { @@ -170,21 +183,21 @@ export const useScheduleStore = defineStore('schedule', () => { // }; const getConflictingReservations = ( - resource: Boat, + resource: string, start: Date, end: Date ): Reservation[] => { const overlapped = reservations.value.filter( (entry: Reservation) => - entry.resource.$id == resource.$id && - entry.start < end && - entry.end > start + entry.resource == resource && + new Date(entry.start) < end && + new Date(entry.end) > start ); return overlapped; }; const isResourceTimeOverlapped = ( - resource: Boat, + resource: string, start: Date, end: Date ): boolean => { @@ -192,7 +205,11 @@ export const useScheduleStore = defineStore('schedule', () => { }; const isReservationOverlapped = (res: Reservation): boolean => { - return isResourceTimeOverlapped(res.resource, res.start, res.end); + return isResourceTimeOverlapped( + res.resource, + new Date(res.start), + new Date(res.end) + ); }; const getNewId = (): string => { @@ -235,7 +252,6 @@ export const useScheduleStore = defineStore('schedule', () => { { ...interval, $id: undefined } ); timeblocks.value.push(response as Interval); - console.log(interval, response); } else { console.error('Update interval called without an ID'); } @@ -315,19 +331,20 @@ export const useScheduleStore = defineStore('schedule', () => { timeblockTemplates, getBoatReservations, getConflictingReservations, + addOrCreateReservation, + isReservationOverlapped, + isResourceTimeOverlapped, + fetchReservations, getIntervalsForDate, getIntervals, fetchIntervals, fetchIntervalTemplates, getNewId, - addOrCreateReservation, createInterval, updateInterval, deleteInterval, createIntervalTemplate, deleteIntervalTemplate, updateIntervalTemplate, - isReservationOverlapped, - isResourceTimeOverlapped, }; }); diff --git a/src/stores/schedule.types.ts b/src/stores/schedule.types.ts index 1f9d23c..ef06f44 100644 --- a/src/stores/schedule.types.ts +++ b/src/stores/schedule.types.ts @@ -1,16 +1,14 @@ import { Models } from 'appwrite'; -import type { Boat } from './boat'; export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined; -export interface Reservation { - id: string; +export type Reservation = Partial & { user: string; - start: Date; - end: Date; - resource: Boat; - reservationDate: Date; + start: string; // ISODate + end: string; //ISODate + resource: string; // Boat ID status?: StatusTypes; -} +}; + // 24 hrs in advance only 2 weekday, and 1 weekend slot // Within 24 hrs, any available slot /* TODO: Figure out how best to separate out where qcalendar bits should be. diff --git a/yarn.lock b/yarn.lock index 3819f20..a2ee3ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1808,6 +1808,15 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" +axios@^1.6.8: + version "1.6.8" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66" + integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-plugin-polyfill-corejs2@^0.4.10: version "0.4.11" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33" @@ -2086,7 +2095,7 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -combined-stream@^1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -3079,6 +3088,11 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -3095,6 +3109,15 @@ form-data@^2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -4282,6 +4305,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"