diff --git a/frontend/src/definitions.ts b/frontend/src/definitions.ts index abc24162d..02b47f3d2 100644 --- a/frontend/src/definitions.ts +++ b/frontend/src/definitions.ts @@ -8,6 +8,8 @@ export enum DateFormatStrings { // Time display formats Display12Hour = 'hh:mma', Display24Hour = 'HH:mm', + // Other formats + UniqueMonth = 'YYYY-MM', } /** diff --git a/frontend/src/models.ts b/frontend/src/models.ts index 84d985dbe..7b709fda4 100644 --- a/frontend/src/models.ts +++ b/frontend/src/models.ts @@ -292,7 +292,7 @@ export type ExceptionDetail = { status?: number; } export type PydanticExceptionDetail = { - ctx: { reason: string }, + ctx: { reason: string, ge?: string }, input: string, loc: string[], msg: string, @@ -333,7 +333,7 @@ export type Fetch = (url: string) => UseFetchReturn & PromiseLike; export type Refresh = () => Promise; export type RemoteEventListResponse = UseFetchReturn; -export type ScheduleResponse = UseFetchReturn; +export type ScheduleResponse = UseFetchReturn; export type ScheduleListResponse = UseFetchReturn; export type SignatureResponse = UseFetchReturn; export type SlotResponse = UseFetchReturn; diff --git a/frontend/src/stores/calendar-store.ts b/frontend/src/stores/calendar-store.ts index 146d01c31..313b211f4 100644 --- a/frontend/src/stores/calendar-store.ts +++ b/frontend/src/stores/calendar-store.ts @@ -1,9 +1,14 @@ -import { Calendar, CalendarListResponse, Fetch } from '@/models'; +import { Calendar, CalendarListResponse, Fetch, RemoteEvent, RemoteEventListResponse } from '@/models'; import { defineStore } from 'pinia'; -import { ref, computed } from 'vue'; +import { ref, computed, inject } from 'vue'; +import { dayjsKey } from '@/keys'; +import { Dayjs } from 'dayjs'; +import { DateFormatStrings } from '@/definitions'; // eslint-disable-next-line import/prefer-default-export export const useCalendarStore = defineStore('calendars', () => { + const dj = inject(dayjsKey); + // State const isLoaded = ref(false); @@ -11,9 +16,13 @@ export const useCalendarStore = defineStore('calendars', () => { const calendars = ref([]); const unconnectedCalendars = computed((): Calendar[] => calendars.value.filter((cal) => !cal.connected)); const connectedCalendars = computed((): Calendar[] => calendars.value.filter((cal) => cal.connected)); - const hasConnectedCalendars = computed(() => connectedCalendars.value.length > 0); + // List of remote events. Retrieved in batches per month. + const remoteEvents = ref([]); + // List of month batches already called. + const remoteMonthsRetrieved = ref([]); + const connectGoogleCalendar = async (call: Fetch, email: string) => { const urlFriendlyEmail = encodeURIComponent(email); const googleUrl = await call(`google/auth?email=${urlFriendlyEmail}`).get(); @@ -44,6 +53,52 @@ export const useCalendarStore = defineStore('calendars', () => { } }; + /** + * Get all cremote events from connected calendars for given time span + * @param call preconfigured API fetch function + * @param activeDate Dayjs object defining the current month + * @param force force a refetch + */ + const getRemoteEvents = async (call: Fetch, activeDate: Dayjs, force = false) => { + // Get month identifier to remember this month's events are already retrieved + const month = activeDate.format(DateFormatStrings.UniqueMonth); + + // Most calendar impl are non-inclusive of the last day, so just add one day to the end. + const from = activeDate.startOf('month').format(DateFormatStrings.QalendarFullDay); + const to = activeDate.endOf('month').add(1, 'day').format(DateFormatStrings.QalendarFullDay); + + // If retrieval is forced, delete cache and start with zero events again + if (force) { + remoteMonthsRetrieved.value = []; + } + + // If month is already cached, there's nothing more to do + if (remoteMonthsRetrieved.value.includes(month)) { + return; + } + + const calendarEvents = force ? [] : [...remoteEvents.value]; + + // Only retrieve remote events if we don't have this month already cached + await Promise.all(connectedCalendars.value.map(async (calendar) => { + const { data }: RemoteEventListResponse = await call(`rmt/cal/${calendar.id}/${from}/${to}`).get().json(); + if (Array.isArray(data.value)) { + calendarEvents.push( + ...data.value.map((event) => ({ + ...event, + duration: dj(event.end).diff(dj(event.start), 'minutes'), + })), + ); + } + })); + + // Remember month + remoteMonthsRetrieved.value.push(month); + + // Update remote event list + remoteEvents.value = calendarEvents; + }; + /** * Restore default state, empty and unload calendars */ @@ -55,9 +110,11 @@ export const useCalendarStore = defineStore('calendars', () => { const connectCalendar = async (call: Fetch, id: number) => { await call(`cal/${id}/connect`).post(); }; + const disconnectCalendar = async (call: Fetch, id: number) => { await call(`cal/${id}/disconnect`).post(); }; + const syncCalendars = async (call: Fetch) => { await call('rmt/sync').post(); }; @@ -68,6 +125,7 @@ export const useCalendarStore = defineStore('calendars', () => { calendars, unconnectedCalendars, connectedCalendars, + remoteEvents, fetch, $reset, connectGoogleCalendar, @@ -75,5 +133,6 @@ export const useCalendarStore = defineStore('calendars', () => { disconnectCalendar, syncCalendars, calendarById, + getRemoteEvents, }; }); diff --git a/frontend/src/stores/schedule-store.ts b/frontend/src/stores/schedule-store.ts index 099e85a1f..9cce02eb9 100644 --- a/frontend/src/stores/schedule-store.ts +++ b/frontend/src/stores/schedule-store.ts @@ -1,10 +1,16 @@ import { i18n } from '@/composables/i18n'; import { defineStore } from 'pinia'; -import { ref, computed, inject } from 'vue'; +import { ref, computed, inject, Ref } from 'vue'; import { useUserStore } from '@/stores/user-store'; -import { DateFormatStrings, MetricEvents } from '@/definitions'; import { - Error, Fetch, Schedule, ScheduleListResponse, ScheduleResponse, + DateFormatStrings, + MetricEvents, + DEFAULT_SLOT_DURATION, + EventLocationType, + MeetingLinkProviderType, +} from '@/definitions'; +import { + Error, Fetch, Schedule, ScheduleListResponse, ScheduleResponse, Exception, ExceptionDetail } from '@/models'; import { dayjsKey } from '@/keys'; import { posthog, usePosthog } from '@/composables/posthog'; @@ -14,16 +20,42 @@ import { timeFormat } from '@/utils'; export const useScheduleStore = defineStore('schedules', () => { const dj = inject(dayjsKey); + const defaultSchedule = { + active: false, + name: '', + calendar_id: 0, + location_type: EventLocationType.InPerson, + location_url: '', + details: '', + start_date: dj().format(DateFormatStrings.QalendarFullDay), + end_date: null, + start_time: '09:00', + end_time: '17:00', + earliest_booking: 1440, + farthest_booking: 20160, + weekdays: [1, 2, 3, 4, 5], + slot_duration: DEFAULT_SLOT_DURATION, + meeting_link_provider: MeetingLinkProviderType.None, + booking_confirmation: true, + calendar: { + id: 0, + title: '', + color: '#000', + connected: true, + }, + }; + // State const isLoaded = ref(false); // Data const schedules = ref([]); + const firstSchedule = computed((): Schedule => schedules.value?.length > 0 ? schedules.value[0] : null); const inactiveSchedules = computed((): Schedule[] => schedules.value.filter((schedule) => !schedule.active)); const activeSchedules = computed((): Schedule[] => schedules.value.filter((schedule) => schedule.active)); /** - * Get all calendars for current user + * Get all schedules for current user * @param call preconfigured API fetch function * @param force Force a fetch even if we already have data */ @@ -43,18 +75,18 @@ export const useScheduleStore = defineStore('schedules', () => { }; /** - * Restore default state, empty and unload calendars + * Restore default state, empty and unload schedules */ const $reset = () => { schedules.value = []; isLoaded.value = false; }; - const handleErrorResponse = (responseData) => { + const handleErrorResponse = (responseData: Ref) => { const { value } = responseData; - if (value?.detail?.message) { - return value?.detail?.message; + if ((value?.detail as ExceptionDetail)?.message) { + return (value?.detail as ExceptionDetail)?.message; } if (value?.detail instanceof Array) { @@ -108,7 +140,7 @@ export const useScheduleStore = defineStore('schedules', () => { if (error.value) { return { error: true, - message: handleErrorResponse(data), + message: handleErrorResponse(data as Ref), } as Error; } @@ -129,7 +161,7 @@ export const useScheduleStore = defineStore('schedules', () => { if (error.value) { return { error: true, - message: handleErrorResponse(data), + message: handleErrorResponse(data as Ref), } as Error; } @@ -172,6 +204,17 @@ export const useScheduleStore = defineStore('schedules', () => { }; return { - isLoaded, schedules, inactiveSchedules, activeSchedules, fetch, $reset, createSchedule, updateSchedule, timeToBackendTime, timeToFrontendTime, + isLoaded, + defaultSchedule, + schedules, + firstSchedule, + inactiveSchedules, + activeSchedules, + fetch, + $reset, + createSchedule, + updateSchedule, + timeToBackendTime, + timeToFrontendTime, }; }); diff --git a/frontend/src/views/ScheduleView.vue b/frontend/src/views/ScheduleView.vue index c538c29dd..50ef73a27 100644 --- a/frontend/src/views/ScheduleView.vue +++ b/frontend/src/views/ScheduleView.vue @@ -1,32 +1,18 @@