Implemented calendar selection for availability checking.
Also upgraded outlook integration to be able to check all calendars instead of only the default one.
This commit is contained in:
		
							parent
							
								
									a231ee6c0d
								
							
						
					
					
						commit
						d3b8431699
					
				
					 7 changed files with 359 additions and 47 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							|  | @ -35,3 +35,6 @@ yarn-error.log* | |||
| 
 | ||||
| # vercel | ||||
| .vercel | ||||
| 
 | ||||
| # Webstorm | ||||
| .idea | ||||
|  |  | |||
|  | @ -110,6 +110,17 @@ paths: | |||
|       summary: Deletes an event type | ||||
|       tags: | ||||
|         - Availability | ||||
|   /api/availability/calendars: | ||||
|     post: | ||||
|       description: Selects calendar for availability checking. | ||||
|       summary: Adds selected calendar | ||||
|       tags: | ||||
|         - Availability | ||||
|     delete: | ||||
|       description: Removes a calendar from availability checking. | ||||
|       summary: Deletes a selected calendar | ||||
|       tags: | ||||
|         - Availability | ||||
|   /api/book/:user: | ||||
|     post: | ||||
|       description: Creates a booking in the user's calendar. | ||||
|  |  | |||
|  | @ -66,6 +66,13 @@ interface CalendarEvent { | |||
|     attendees: Person[]; | ||||
| }; | ||||
| 
 | ||||
| interface IntegrationCalendar { | ||||
|     integration: string; | ||||
|     primary: boolean; | ||||
|     externalId: string; | ||||
|     name: string; | ||||
| } | ||||
| 
 | ||||
| interface CalendarApiAdapter { | ||||
|     createEvent(event: CalendarEvent): Promise<any>; | ||||
| 
 | ||||
|  | @ -73,7 +80,9 @@ interface CalendarApiAdapter { | |||
| 
 | ||||
|     deleteEvent(uid: String); | ||||
| 
 | ||||
|     getAvailability(dateFrom, dateTo): Promise<any>; | ||||
|     getAvailability(dateFrom, dateTo, selectedCalendars: IntegrationCalendar[]): Promise<any>; | ||||
| 
 | ||||
|     listCalendars(): Promise<IntegrationCalendar[]>; | ||||
| } | ||||
| 
 | ||||
| const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { | ||||
|  | @ -112,37 +121,59 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { | |||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     return { | ||||
|         getAvailability: (dateFrom, dateTo) => { | ||||
|             const payload = { | ||||
|                 schedules: [credential.key.email], | ||||
|                 startTime: { | ||||
|                     dateTime: dateFrom, | ||||
|                     timeZone: 'UTC', | ||||
|                 }, | ||||
|                 endTime: { | ||||
|                     dateTime: dateTo, | ||||
|                     timeZone: 'UTC', | ||||
|                 }, | ||||
|                 availabilityViewInterval: 60 | ||||
|             }; | ||||
|     const integrationType = "office365_calendar"; | ||||
| 
 | ||||
|             return auth.getToken().then( | ||||
|                 (accessToken) => fetch('https://graph.microsoft.com/v1.0/me/calendar/getSchedule', { | ||||
|                     method: 'post', | ||||
|     function listCalendars(): Promise<IntegrationCalendar[]> { | ||||
|         return auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendars', { | ||||
|               method: 'get', | ||||
|               headers: { | ||||
|                   'Authorization': 'Bearer ' + accessToken, | ||||
|                   'Content-Type': 'application/json' | ||||
|               }, | ||||
|                     body: JSON.stringify(payload) | ||||
|           }).then(handleErrorsJson) | ||||
|             .then(responseBody => { | ||||
|                 return responseBody.value.map(cal => { | ||||
|                     const calendar: IntegrationCalendar = { | ||||
|                         externalId: cal.id, integration: integrationType, name: cal.name, primary: cal.isDefaultCalendar | ||||
|                     } | ||||
|                     return calendar; | ||||
|                 }); | ||||
|             }) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|         getAvailability: (dateFrom, dateTo, selectedCalendars) => { | ||||
|             const filter = "?$filter=start/dateTime ge '" +  dateFrom + "' and end/dateTime le '" + dateTo + "'" | ||||
|             return auth.getToken().then( | ||||
|                 (accessToken) => { | ||||
|                     const selectedCalendarIds = selectedCalendars.filter(e => e.integration === integrationType).map(e => e.externalId); | ||||
|                     if (selectedCalendarIds.length == 0 && selectedCalendars.length > 0){ | ||||
|                         // Only calendars of other integrations selected
 | ||||
|                         return Promise.resolve([]); | ||||
|                     } | ||||
| 
 | ||||
|                     console.log("selectedCalendarIds.length: " + selectedCalendarIds.length) | ||||
|                     return (selectedCalendarIds.length == 0 | ||||
|                       ? listCalendars().then(cals => cals.map(e => e.externalId)) | ||||
|                       : Promise.resolve(selectedCalendarIds).then(x => x)).then((ids: string[]) => { | ||||
|                         const urls = ids.map(calendarId => 'https://graph.microsoft.com/v1.0/me/calendars/' + calendarId + '/events' + filter) | ||||
|                         console.log("urls", urls) | ||||
|                         return Promise.all(urls.map(url => fetch(url, { | ||||
|                             method: 'get', | ||||
|                             headers: { | ||||
|                                 'Authorization': 'Bearer ' + accessToken, | ||||
|                                 'Prefer': 'outlook.timezone="Etc/GMT"' | ||||
|                             } | ||||
|                         }) | ||||
|                           .then(handleErrorsJson) | ||||
|                     .then(responseBody => { | ||||
|                         return responseBody.value[0].scheduleItems.map((evt) => ({ | ||||
|                           .then(responseBody => responseBody.value.map((evt) => ({ | ||||
|                                 start: evt.start.dateTime + 'Z', | ||||
|                                 end: evt.end.dateTime + 'Z' | ||||
|                             })) | ||||
|                           ))).then(results => results.reduce((acc, events) => acc.concat(events), [])) | ||||
|                     }) | ||||
|                 } | ||||
|             ).catch((err) => { | ||||
|                 console.log(err); | ||||
|             }); | ||||
|  | @ -172,28 +203,37 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { | |||
|             }, | ||||
|             body: JSON.stringify(translateEvent(event)) | ||||
|         }).then(handleErrorsRaw)), | ||||
|         listCalendars | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| const GoogleCalendar = (credential): CalendarApiAdapter => { | ||||
|     const myGoogleAuth = googleAuth(); | ||||
|     myGoogleAuth.setCredentials(credential.key); | ||||
|     const integrationType = "google_calendar"; | ||||
| 
 | ||||
|     return { | ||||
|         getAvailability: (dateFrom, dateTo) => new Promise((resolve, reject) => { | ||||
|         getAvailability: (dateFrom, dateTo, selectedCalendars) => new Promise((resolve, reject) => { | ||||
|             const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); | ||||
|             calendar.calendarList | ||||
|                 .list() | ||||
|                 .then(cals => { | ||||
|                     const filteredItems = cals.data.items.filter(i => selectedCalendars.findIndex(e => e.externalId === i.id) > -1) | ||||
|                     if (filteredItems.length == 0 && selectedCalendars.length > 0){ | ||||
|                         // Only calendars of other integrations selected
 | ||||
|                         resolve([]); | ||||
|                     } | ||||
|                     calendar.freebusy.query({ | ||||
|                         requestBody: { | ||||
|                             timeMin: dateFrom, | ||||
|                             timeMax: dateTo, | ||||
|                             items: cals.data.items | ||||
|                             items: filteredItems.length > 0 ? filteredItems : cals.data.items | ||||
|                         } | ||||
|                     }, (err, apires) => { | ||||
|                         if (err) { | ||||
|                             reject(err); | ||||
|                         } | ||||
| 
 | ||||
|                         resolve( | ||||
|                             Object.values(apires.data.calendars).flatMap( | ||||
|                                 (item) => item["busy"] | ||||
|  | @ -300,6 +340,22 @@ const GoogleCalendar = (credential): CalendarApiAdapter => { | |||
|                 } | ||||
|                 return resolve(event.data); | ||||
|             }); | ||||
|         }), | ||||
|         listCalendars: () => new Promise((resolve, reject) => { | ||||
|             const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); | ||||
|             calendar.calendarList | ||||
|               .list() | ||||
|               .then(cals => { | ||||
|                   resolve(cals.data.items.map(cal => { | ||||
|                       const calendar: IntegrationCalendar = { | ||||
|                           externalId: cal.id, integration: integrationType, name: cal.summary, primary: cal.primary | ||||
|                       } | ||||
|                       return calendar; | ||||
|                   })) | ||||
|               }) | ||||
|               .catch((err) => { | ||||
|                   reject(err); | ||||
|               }); | ||||
|         }) | ||||
|     }; | ||||
| }; | ||||
|  | @ -316,11 +372,18 @@ const calendars = (withCredentials): CalendarApiAdapter[] => withCredentials.map | |||
|     } | ||||
| }).filter(Boolean); | ||||
| 
 | ||||
| 
 | ||||
| const getBusyTimes = (withCredentials, dateFrom, dateTo) => Promise.all( | ||||
|     calendars(withCredentials).map(c => c.getAvailability(dateFrom, dateTo)) | ||||
| const getBusyTimes = (withCredentials, dateFrom, dateTo, selectedCalendars) => Promise.all( | ||||
|     calendars(withCredentials).map(c => c.getAvailability(dateFrom, dateTo, selectedCalendars)) | ||||
| ).then( | ||||
|     (results) => results.reduce((acc, availability) => acc.concat(availability), []) | ||||
|     (results) => { | ||||
|         return results.reduce((acc, availability) => acc.concat(availability), []) | ||||
|     } | ||||
| ); | ||||
| 
 | ||||
| const listCalendars = (withCredentials) => Promise.all( | ||||
|   calendars(withCredentials).map(c => c.listCalendars()) | ||||
| ).then( | ||||
|   (results) => results.reduce((acc, calendars) => acc.concat(calendars), []) | ||||
| ); | ||||
| 
 | ||||
| const createEvent = (credential, calEvent: CalendarEvent): Promise<any> => { | ||||
|  | @ -352,4 +415,4 @@ const deleteEvent = (credential, uid: String): Promise<any> => { | |||
|     return Promise.resolve({}); | ||||
| }; | ||||
| 
 | ||||
| export {getBusyTimes, createEvent, updateEvent, deleteEvent, CalendarEvent}; | ||||
| export {getBusyTimes, createEvent, updateEvent, deleteEvent, CalendarEvent, listCalendars, IntegrationCalendar}; | ||||
|  |  | |||
|  | @ -15,6 +15,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     const availability = await getBusyTimes(currentUser.credentials, req.query.dateFrom, req.query.dateTo); | ||||
|     const selectedCalendars = (await prisma.selectedCalendar.findMany({ | ||||
|         where: { | ||||
|             userId: currentUser.id | ||||
|         } | ||||
|     })); | ||||
| 
 | ||||
|     const availability = await getBusyTimes(currentUser.credentials, req.query.dateFrom, req.query.dateTo, selectedCalendars); | ||||
|     res.status(200).json(availability); | ||||
| } | ||||
|  |  | |||
							
								
								
									
										69
									
								
								pages/api/availability/calendar.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								pages/api/availability/calendar.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,69 @@ | |||
| import type { NextApiRequest, NextApiResponse } from 'next'; | ||||
| import { getSession } from 'next-auth/client'; | ||||
| import prisma from '../../../lib/prisma'; | ||||
| import {IntegrationCalendar, listCalendars} from "../../../lib/calendarClient"; | ||||
| 
 | ||||
| export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||||
|     const session = await getSession({req: req}); | ||||
| 
 | ||||
|     if (!session) { | ||||
|         res.status(401).json({message: "Not authenticated"}); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const currentUser = await prisma.user.findFirst({ | ||||
|         where: { | ||||
|             id: session.user.id, | ||||
|         }, | ||||
|         select: { | ||||
|             credentials: true, | ||||
|             timeZone: true, | ||||
|             id: true | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     if (req.method == "POST") { | ||||
|         await prisma.selectedCalendar.create({ | ||||
|             data: { | ||||
|                 user: { | ||||
|                     connect: { | ||||
|                         id: currentUser.id | ||||
|                     } | ||||
|                 }, | ||||
|                 integration: req.body.integration, | ||||
|                 externalId: req.body.externalId | ||||
|             } | ||||
|         }); | ||||
|         res.status(200).json({message: "Calendar Selection Saved"}); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     if (req.method == "DELETE") { | ||||
|         await prisma.selectedCalendar.delete({ | ||||
|             where: { | ||||
|                 userId_integration_externalId: { | ||||
|                     userId: currentUser.id, | ||||
|                     externalId: req.body.externalId, | ||||
|                     integration: req.body.integration | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         res.status(200).json({message: "Calendar Selection Saved"}); | ||||
|     } | ||||
| 
 | ||||
|     if (req.method == "GET") { | ||||
|         const selectedCalendarIds = await prisma.selectedCalendar.findMany({ | ||||
|             where: { | ||||
|                 userId: currentUser.id | ||||
|             }, | ||||
|             select: { | ||||
|                 externalId: true | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         const calendars: IntegrationCalendar[] = await listCalendars(currentUser.credentials); | ||||
|         const selectableCalendars = calendars.map(cal => {return {selected: selectedCalendarIds.findIndex(s => s.externalId === cal.externalId) > -1, ...cal}}); | ||||
|         res.status(200).json(selectableCalendars); | ||||
|     } | ||||
| } | ||||
|  | @ -2,29 +2,82 @@ import Head from 'next/head'; | |||
| import Link from 'next/link'; | ||||
| import prisma from '../../lib/prisma'; | ||||
| import Shell from '../../components/Shell'; | ||||
| import {useState} from 'react'; | ||||
| import {useEffect, useState} from 'react'; | ||||
| import {getSession, useSession} from 'next-auth/client'; | ||||
| import {CheckCircleIcon, ChevronRightIcon, PlusIcon, XCircleIcon} from '@heroicons/react/solid'; | ||||
| import {CalendarIcon, CheckCircleIcon, ChevronRightIcon, PlusIcon, XCircleIcon} from '@heroicons/react/solid'; | ||||
| import {InformationCircleIcon} from '@heroicons/react/outline'; | ||||
| import { Switch } from '@headlessui/react' | ||||
| 
 | ||||
| export default function Home({ integrations }) { | ||||
|     const [session, loading] = useSession(); | ||||
|     const [showAddModal, setShowAddModal] = useState(false); | ||||
| 
 | ||||
|     if (loading) { | ||||
|         return <p className="text-gray-400">Loading...</p>; | ||||
|     } | ||||
|     const [showSelectCalendarModal, setShowSelectCalendarModal] = useState(false); | ||||
|     const [selectableCalendars, setSelectableCalendars] = useState([]); | ||||
| 
 | ||||
|     function toggleAddModal() { | ||||
|         setShowAddModal(!showAddModal); | ||||
|     } | ||||
| 
 | ||||
|     function toggleShowCalendarModal() { | ||||
|         setShowSelectCalendarModal(!showSelectCalendarModal); | ||||
|     } | ||||
| 
 | ||||
|     function loadCalendars() { | ||||
|         fetch('api/availability/calendar') | ||||
|           .then((response) => response.json()) | ||||
|           .then(data => { | ||||
|               setSelectableCalendars(data) | ||||
|           }); | ||||
|     } | ||||
| 
 | ||||
|     function integrationHandler(type) { | ||||
|         fetch('/api/integrations/' + type.replace('_', '') + '/add') | ||||
|             .then((response) => response.json()) | ||||
|             .then((data) => window.location.href = data.url); | ||||
|     } | ||||
| 
 | ||||
|     function calendarSelectionHandler(calendar) { | ||||
|         return (selected) => { | ||||
|             let cals = [...selectableCalendars]; | ||||
|             let i = cals.findIndex(c => c.externalId === calendar.externalId); | ||||
|             cals[i].selected = selected; | ||||
|             setSelectableCalendars(cals); | ||||
|             if (selected) { | ||||
|                 fetch('api/availability/calendar', { | ||||
|                     method: 'POST', | ||||
|                     headers: { | ||||
|                         'Content-Type': 'application/json' | ||||
|                     }, | ||||
|                     body: JSON.stringify(cals[i]) | ||||
|                 }).then((response) => response.json()); | ||||
|             } else { | ||||
|                 fetch('api/availability/calendar', { | ||||
|                     method: 'DELETE', headers: { | ||||
|                         'Content-Type': 'application/json' | ||||
|                     }, body: JSON.stringify(cals[i]) | ||||
|                 }).then((response) => response.json()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     function getCalendarIntegrationImage(integrationType: string){ | ||||
|         switch (integrationType) { | ||||
|             case "google_calendar": return "integrations/google-calendar.png"; | ||||
|             case "office365_calendar": return "integrations/office-365.png"; | ||||
|             default: return ""; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     function classNames(...classes) { | ||||
|         return classes.filter(Boolean).join(' ') | ||||
|     } | ||||
| 
 | ||||
|     useEffect(loadCalendars, [integrations]); | ||||
| 
 | ||||
|     if (loading) { | ||||
|         return <p className="text-gray-400">Loading...</p>; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <div> | ||||
|             <Head> | ||||
|  | @ -39,7 +92,7 @@ export default function Home({ integrations }) { | |||
|                         Add new integration | ||||
|                     </button> | ||||
|                 </div> | ||||
|                 <div className="bg-white shadow overflow-hidden rounded-lg"> | ||||
|                 <div className="bg-white shadow overflow-hidden rounded-lg mb-8"> | ||||
|                     {integrations.filter( (ig) => ig.credential ).length !== 0 ? <ul className="divide-y divide-gray-200"> | ||||
|                         {integrations.filter(ig => ig.credential).map( (ig) => (<li> | ||||
|                             <Link href={"/integrations/" + ig.credential.id}> | ||||
|  | @ -165,6 +218,104 @@ export default function Home({ integrations }) { | |||
|                     </div> | ||||
|                 </div> | ||||
|                 } | ||||
|                 <div className="bg-white shadow rounded-lg"> | ||||
|                     <div className="px-4 py-5 sm:p-6"> | ||||
|                         <h3 className="text-lg leading-6 font-medium text-gray-900"> | ||||
|                             Select calendars | ||||
|                         </h3> | ||||
|                         <div className="mt-2 max-w-xl text-sm text-gray-500"> | ||||
|                             <p> | ||||
|                                Select which calendars are checked for availability to prevent double bookings. | ||||
|                             </p> | ||||
|                         </div> | ||||
|                         <div className="mt-5"> | ||||
|                             <button type="button" onClick={toggleShowCalendarModal} className="btn btn-primary"> | ||||
|                                 Select calendars | ||||
|                             </button> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 {showSelectCalendarModal && | ||||
|                 <div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true"> | ||||
|                     <div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> | ||||
|                         {/* <!-- | ||||
|                           Background overlay, show/hide based on modal state. | ||||
| 
 | ||||
|                           Entering: "ease-out duration-300" | ||||
|                             From: "opacity-0" | ||||
|                             To: "opacity-100" | ||||
|                           Leaving: "ease-in duration-200" | ||||
|                             From: "opacity-100" | ||||
|                             To: "opacity-0" | ||||
|                         --> */} | ||||
|                         <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div> | ||||
|                         <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span> | ||||
|                         {/* <!-- | ||||
|                           Modal panel, show/hide based on modal state. | ||||
| 
 | ||||
|                           Entering: "ease-out duration-300" | ||||
|                             From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" | ||||
|                             To: "opacity-100 translate-y-0 sm:scale-100" | ||||
|                           Leaving: "ease-in duration-200" | ||||
|                             From: "opacity-100 translate-y-0 sm:scale-100" | ||||
|                             To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" | ||||
|                         --> */} | ||||
|                         <div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6"> | ||||
|                             <div className="sm:flex sm:items-start"> | ||||
|                                 <div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10"> | ||||
|                                     <CalendarIcon className="h-6 w-6 text-blue-600" /> | ||||
|                                 </div> | ||||
|                                 <div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> | ||||
|                                     <h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title"> | ||||
|                                         Select calendars | ||||
|                                     </h3> | ||||
|                                     <div> | ||||
|                                         <p className="text-sm text-gray-400"> | ||||
|                                             If no entry is selected, all calendars will be checked | ||||
|                                         </p> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                             <div className="my-4"> | ||||
|                                 <ul className="divide-y divide-gray-200"> | ||||
|                                     {selectableCalendars.map( (calendar) => (<li className="flex py-4"> | ||||
|                                         <div className="w-1/12 mr-4 pt-2"> | ||||
|                                             <img className="h-8 w-8 mr-2" src={getCalendarIntegrationImage(calendar.integration)} alt={calendar.integration} /> | ||||
|                                         </div> | ||||
|                                         <div className="w-10/12"> | ||||
|                                             <h2 className="text-gray-800 font-medium">{ calendar.name }</h2> | ||||
|                                         </div> | ||||
|                                         <div className="w-2/12 text-right pt-2"> | ||||
|                                             <Switch | ||||
|                                               checked={calendar.selected} | ||||
|                                               onChange={calendarSelectionHandler(calendar)} | ||||
|                                               className={classNames( | ||||
|                                                 calendar.selected ? 'bg-indigo-600' : 'bg-gray-200', | ||||
|                                                 'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500' | ||||
|                                               )} | ||||
|                                             > | ||||
|                                                 <span className="sr-only">Select calendar</span> | ||||
|                                                 <span | ||||
|                                                   aria-hidden="true" | ||||
|                                                   className={classNames( | ||||
|                                                     calendar.selected ? 'translate-x-5' : 'translate-x-0', | ||||
|                                                     'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200' | ||||
|                                                   )} | ||||
|                                                 /> | ||||
|                                             </Switch> | ||||
|                                         </div> | ||||
|                                     </li>))} | ||||
|                                 </ul> | ||||
|                             </div> | ||||
|                             <div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"> | ||||
|                                 <button onClick={toggleShowCalendarModal} type="button" className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm"> | ||||
|                                     Close | ||||
|                                 </button> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 } | ||||
|             </Shell> | ||||
|         </div> | ||||
|     ); | ||||
|  |  | |||
|  | @ -49,6 +49,7 @@ model User { | |||
|   credentials   Credential[] | ||||
|   teams         Membership[] | ||||
|   bookings      Booking[] | ||||
|   selectedCalendars      SelectedCalendar[] | ||||
|   @@map(name: "users") | ||||
| } | ||||
| 
 | ||||
|  | @ -120,3 +121,11 @@ model Booking { | |||
|   createdAt     DateTime    @default(now()) | ||||
|   updatedAt     DateTime? | ||||
| } | ||||
| 
 | ||||
| model SelectedCalendar { | ||||
|   user          User       @relation(fields: [userId], references: [id]) | ||||
|   userId        Int | ||||
|   integration   String | ||||
|   externalId    String | ||||
|   @@id([userId,integration,externalId]) | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 Malte Delfs
						Malte Delfs