From 4503ef16e52cb8e7b4827b082716589c37fa675e Mon Sep 17 00:00:00 2001 From: Brieuc Dubois <git@bhasher.com> Date: Wed, 9 Oct 2024 20:55:36 +0300 Subject: [PATCH] Reimplementation of the booking logic --- backend/app/main.py | 24 +++++++ backend/app/schemas.py | 5 ++ frontend/src/lang/fr.json | 5 +- frontend/src/lib/api/sessions.ts | 19 ++++++ frontend/src/routes/+page.svelte | 65 +++++++++++++++++-- .../src/routes/tutor/timeslots/+page.svelte | 50 ++++++++++++-- 6 files changed, 156 insertions(+), 12 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 8b578828..5dd053f1 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -402,6 +402,30 @@ def create_weekly_survey( return crud.create_user_survey_weekly(db, user_id, survey).id +@usersRouter.post('/{user_id}/contacts/{contact_id}/bookings', status_code=status.HTTP_201_CREATED) +def create_booking( + user_id: int, + contact_id: int, + booking: schemas.SessionBookingCreate, + db: Session = Depends(get_db), + current_user: schemas.User = Depends(get_jwt_user), + ): + if ( + not check_user_level(current_user, models.UserType.ADMIN) + and current_user.id != user_id + ): + raise HTTPException( + status_code=401, + detail="You do not have permission to create a booking for this user", + ) + db_user = crud.get_user(db, user_id) + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + db_contact = crud.get_user(db, contact_id) + if db_contact is None: + raise HTTPException(status_code=404, detail="Contact not found") + return crud.create_session_with_users(db, [db_user, db_contact], booking.start_time, booking.end_time).id + @sessionsRouter.post("", response_model=schemas.Session) def create_session( db: Session = Depends(get_db), diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 721a36b5..051e04f7 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -102,6 +102,11 @@ class SessionSatisfyCreate(BaseModel): remarks: str | None = None +class SessionBookingCreate(BaseModel): + start_time: NaiveDatetime + end_time: NaiveDatetime + + class MessageFeedback(BaseModel): id: int message_id: int diff --git a/frontend/src/lang/fr.json b/frontend/src/lang/fr.json index 80642565..fcd432fa 100644 --- a/frontend/src/lang/fr.json +++ b/frontend/src/lang/fr.json @@ -260,7 +260,10 @@ "login": "Se connecter", "cancel": "Annuler", "save": "Sauvegarder", - "close": "Fermer" + "close": "Fermer", + "tryit": "Essayer", + "update": "Mettre à jour", + "updated": "Mis à jour!" }, "utils": { "month": { diff --git a/frontend/src/lib/api/sessions.ts b/frontend/src/lib/api/sessions.ts index f5b7050e..31e47b1f 100644 --- a/frontend/src/lib/api/sessions.ts +++ b/frontend/src/lib/api/sessions.ts @@ -1,3 +1,4 @@ +import { formatToUTCDate } from '$lib/utils/date'; import { toastAlert } from '$lib/utils/toasts'; import { axiosInstance } from './apiInstance'; @@ -124,3 +125,21 @@ export async function createSessionSatisfyAPI( return true; } + +export async function createSessionFromCalComAPI( + user_id: number, + contact_id: number, + start: Date, + end: Date +): Promise<number | null> { + const response = await axiosInstance.post(`/users/${user_id}/contacts/${contact_id}/bookings`, { + start_time: formatToUTCDate(start), + end_time: formatToUTCDate(end) + }); + if (response.status !== 201) { + toastAlert('Failed to create cal.com session'); + return null; + } + + return response.data; +} diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 5e591d7e..5b3bd918 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -16,7 +16,8 @@ getUserContactsAPI, getUserContactSessionsAPI } from '$lib/api/users'; - import { toastWarning } from '$lib/utils/toasts'; + import { createSessionFromCalComAPI } from '$lib/api/sessions'; + import { toastAlert, toastWarning } from '$lib/utils/toasts'; let ready = false; $: contacts = [] as User[]; @@ -43,6 +44,56 @@ } ready = true; + + (function (C: any, A: any, L: any) { + let p = function (a: any, ar: any) { + a.q.push(ar); + }; + let d = C.document; + C.Cal = + C.Cal || + function () { + let cal = C.Cal; + let ar = arguments; + if (!cal.loaded) { + cal.ns = {}; + cal.q = cal.q || []; + d.head.appendChild(d.createElement('script')).src = A; + cal.loaded = true; + } + if (ar[0] === L) { + const api: any = function () { + p(api, arguments); + }; + const namespace = ar[1]; + api.q = api.q || []; + if (typeof namespace === 'string') { + cal.ns[namespace] = cal.ns[namespace] || api; + p(cal.ns[namespace], ar); + p(cal, ['initNamespace', namespace]); + } else p(cal, ar); + return; + } + p(cal, ar); + }; + })(window, 'https://app.cal.com/embed/embed.js', 'init'); + // @ts-ignore + Cal('init'); + // @ts-ignore + Cal('on', { + action: 'bookingSuccessful', + callback: (e: any) => { + if (!contact || !$user || !e.detail.data) { + toastAlert('Automatic session creation failed'); + return; + } + + let date = new Date(e.detail.data.date); + let duration = e.detail.data.duration; + let end = new Date(date.getTime() + duration * 60000); + createSessionFromCalComAPI($user.id, contact.id, date, end); + } + }); }); async function createSession() { @@ -67,6 +118,11 @@ } </script> +<svelte:head> + <script> + </script> +</svelte:head> + {#if ready} <div class="h-full w-full flex"> <ul class="h-full [width:_clamp(200px,25%,500px)] overflow-y-scroll border-r-2 flex flex-col"> @@ -110,14 +166,13 @@ {$t('home.createSession')} </button> <div class="size-4 float-end"></div> - <a + <button class="button float-end" class:btn-disabled={!contact || !contact.calcom_link} - href={contact.calcom_link} - target="_blank" + data-cal-link={`${contact.calcom_link}?email=${$user?.email}&name=${$user?.nickname}`} > {$t('home.bookSession')} - </a> + </button> </div> <div class="flex-grow p-2"> <h2 class="text-xl my-4 font-bold">{$t('home.currentSessions')}</h2> diff --git a/frontend/src/routes/tutor/timeslots/+page.svelte b/frontend/src/routes/tutor/timeslots/+page.svelte index 0b0f3d2b..c38fea0b 100644 --- a/frontend/src/routes/tutor/timeslots/+page.svelte +++ b/frontend/src/routes/tutor/timeslots/+page.svelte @@ -24,7 +24,7 @@ }); async function send() { - if (!calcom_link || !calcom_link.startsWith('https://cal.com/')) { + if (!calcom_link || calcom_link.length == 0) { toastWarning($t('timeslots.calcomWarning')); return; } @@ -34,16 +34,53 @@ if (!res) return; lastTimeslots = timeslots; + last_calcom_link = calcom_link; sent = true; setTimeout(() => (sent = false), 3000); } </script> +<svelte:head> + <script> + (function (C, A, L) { + let p = function (a, ar) { + a.q.push(ar); + }; + let d = C.document; + C.Cal = + C.Cal || + function () { + let cal = C.Cal; + let ar = arguments; + if (!cal.loaded) { + cal.ns = {}; + cal.q = cal.q || []; + d.head.appendChild(d.createElement('script')).src = A; + cal.loaded = true; + } + if (ar[0] === L) { + const api = function () { + p(api, arguments); + }; + const namespace = ar[1]; + api.q = api.q || []; + if (typeof namespace === 'string') { + cal.ns[namespace] = cal.ns[namespace] || api; + p(cal.ns[namespace], ar); + p(cal, ['initNamespace', namespace]); + } else p(cal, ar); + return; + } + p(cal, ar); + }; + })(window, 'https://app.cal.com/embed/embed.js', 'init'); + Cal('init'); + </script> +</svelte:head> + <div class="max-w-screen-md mx-auto p-2"> <h2 class="my-4 text-xl">{$t('timeslots.setAvailabilities')}</h2> {#if ready} - <Timeslots bind:timeslots /> - <div class="form-control mt-4"> <label class="label" for="calcom"> <span class="label-text"> @@ -67,17 +104,18 @@ type="text" id="calcom" class="grow" - placeholder="https://cal.com/username/tutoring" + placeholder="username/tutoring" bind:value={calcom_link} /> </div> </div> <div class="form-control mt-4"> + <button class="button" data-cal-link={calcom_link}>{$t('button.tryit')}</button> <button - class="button" + class="button mt-4" disabled={sent || (lastTimeslots === timeslots && calcom_link === last_calcom_link)} - on:click={send}>{$t(sent ? 'button.sent' : 'button.submit')}</button + on:click={send}>{$t(sent ? 'button.updated' : 'button.update')}</button > </div> {/if} -- GitLab