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