From f7514c698eec3c7f1dca76f7067712802692462c Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Wed, 3 Sep 2025 10:04:00 +0300 Subject: [PATCH] feat: implement booking feature with service and time slot selection --- .../(dashboard)/booking/[handle]/page.tsx | 13 ++- .../_components/booking/booking-container.tsx | 31 ++++++ .../_components/booking/booking.context.ts | 18 ++++ .../_components/booking/booking.provider.tsx | 47 +++++++++ .../_components/booking/location-selector.tsx | 9 ++ .../_components/booking/service-selector.tsx | 83 ++++++++++++++++ .../(user)/_components/booking/time-slots.tsx | 96 +++++++++++++++++++ .../(user)/_components/service-categories.tsx | 6 +- app/home/(user)/_lib/server/load-category.ts | 1 + lib/services/connected-online.service.ts | 12 +-- packages/ui/src/shadcn/radio-group.tsx | 4 +- 11 files changed, 306 insertions(+), 14 deletions(-) create mode 100644 app/home/(user)/_components/booking/booking-container.tsx create mode 100644 app/home/(user)/_components/booking/booking.context.ts create mode 100644 app/home/(user)/_components/booking/booking.provider.tsx create mode 100644 app/home/(user)/_components/booking/location-selector.tsx create mode 100644 app/home/(user)/_components/booking/service-selector.tsx create mode 100644 app/home/(user)/_components/booking/time-slots.tsx diff --git a/app/home/(user)/(dashboard)/booking/[handle]/page.tsx b/app/home/(user)/(dashboard)/booking/[handle]/page.tsx index ebce187..a125346 100644 --- a/app/home/(user)/(dashboard)/booking/[handle]/page.tsx +++ b/app/home/(user)/(dashboard)/booking/[handle]/page.tsx @@ -2,12 +2,13 @@ import { HomeLayoutPageHeader } from '@/app/home/(user)/_components/home-page-he import { loadCategory } from '@/app/home/(user)/_lib/server/load-category'; import { AppBreadcrumbs } from '@kit/ui/makerkit/app-breadcrumbs'; -import { PageBody } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; +import BookingContainer from '../../../_components/booking/booking-container'; + export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); const title = i18n.t('booking:title'); @@ -18,9 +19,13 @@ export const generateMetadata = async () => { }; async function BookingHandlePage({ params }: { params: { handle: string } }) { - const handle = await params.handle; + const { handle } = await params; const { category } = await loadCategory({ handle }); + if (!category) { + return
Category not found
; + } + return ( <> } - description={} + description="" /> - + ); } diff --git a/app/home/(user)/_components/booking/booking-container.tsx b/app/home/(user)/_components/booking/booking-container.tsx new file mode 100644 index 0000000..fb760fa --- /dev/null +++ b/app/home/(user)/_components/booking/booking-container.tsx @@ -0,0 +1,31 @@ +'use client'; + +import React from 'react'; + +import { Calendar } from '@kit/ui/shadcn/calendar'; +import { Card } from '@kit/ui/shadcn/card'; + +import { ServiceCategory } from '../service-categories'; +import { BookingProvider } from './booking.provider'; +import LocationSelector from './location-selector'; +import ServiceSelector from './service-selector'; +import TimeSlots from './time-slots'; + +const BookingContainer = ({ category }: { category: ServiceCategory }) => { + return ( + +
+
+ + + + + {/* */} +
+ +
+
+ ); +}; + +export default BookingContainer; diff --git a/app/home/(user)/_components/booking/booking.context.ts b/app/home/(user)/_components/booking/booking.context.ts new file mode 100644 index 0000000..36d9035 --- /dev/null +++ b/app/home/(user)/_components/booking/booking.context.ts @@ -0,0 +1,18 @@ +import { createContext } from 'react'; + +import { StoreProduct } from '@medusajs/types'; +import { noop } from 'lodash'; + +const BookingContext = createContext<{ + timeSlots: string[]; + selectedService: StoreProduct | null; + setSelectedService: (selectedService: any) => void; + updateTimeSlots: (serviceId: number) => Promise; +}>({ + timeSlots: [], + selectedService: null, + setSelectedService: (_) => _, + updateTimeSlots: async (_) => noop(), +}); + +export { BookingContext }; diff --git a/app/home/(user)/_components/booking/booking.provider.tsx b/app/home/(user)/_components/booking/booking.provider.tsx new file mode 100644 index 0000000..700ceee --- /dev/null +++ b/app/home/(user)/_components/booking/booking.provider.tsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; + +import { StoreProduct } from '@medusajs/types'; + +import { getAvailableAppointmentsForService } from '~/lib/services/connected-online.service'; + +import { ServiceCategory } from '../service-categories'; +import { BookingContext } from './booking.context'; + +export function useBooking() { + const context = React.useContext(BookingContext); + + if (!context) { + throw new Error('useBooking must be used within a BookingProvider.'); + } + + return context; +} + +export const BookingProvider: React.FC<{ + children: React.ReactElement; + category: ServiceCategory; +}> = ({ children, category }) => { + const [selectedService, setSelectedService] = useState( + category.products[0] || null, + ); + const [timeSlots, setTimeSlots] = useState([]); + + const updateTimeSlots = async (serviceId: number) => { + const response = await getAvailableAppointmentsForService(serviceId); + console.log('updateTimeSlots response', response); + // Fetch time slots based on the selected service ID + }; + + return ( + + {children} + + ); +}; diff --git a/app/home/(user)/_components/booking/location-selector.tsx b/app/home/(user)/_components/booking/location-selector.tsx new file mode 100644 index 0000000..e2e7de1 --- /dev/null +++ b/app/home/(user)/_components/booking/location-selector.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +import { Card } from '@kit/ui/shadcn/card'; + +const LocationSelector = () => { + return LocationSelector; +}; + +export default LocationSelector; diff --git a/app/home/(user)/_components/booking/service-selector.tsx b/app/home/(user)/_components/booking/service-selector.tsx new file mode 100644 index 0000000..557048e --- /dev/null +++ b/app/home/(user)/_components/booking/service-selector.tsx @@ -0,0 +1,83 @@ +import React, { useState } from 'react'; + +import { StoreProduct } from '@medusajs/types'; +import { ArrowUp, ChevronDown } from 'lucide-react'; + +import { Button } from '@kit/ui/shadcn/button'; +import { Card } from '@kit/ui/shadcn/card'; +import { Label } from '@kit/ui/shadcn/label'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@kit/ui/shadcn/popover'; +import { RadioGroup, RadioGroupItem } from '@kit/ui/shadcn/radio-group'; + +import { useBooking } from './booking.provider'; + +const ServiceSelector = ({ products }: { products: StoreProduct[] }) => { + const { selectedService, setSelectedService, updateTimeSlots } = useBooking(); + const [collapsed, setCollapsed] = React.useState(false); + const [firstFourProducts, setFirstFourProducts] = useState( + products.slice(0, 4), + ); + + const onServiceSelect = async (productId: StoreProduct['id']) => { + const product = products.find((p) => p.id === productId); + setSelectedService(product); + setCollapsed(false); + await updateTimeSlots((product!.metadata!.serviceId as number) || 0); + }; + + console.log('selectedService', selectedService); + return ( + +
Teenused
+ +
+ + {firstFourProducts.map((product) => ( +
+ + +
+ ))} +
+ +
setCollapsed((_) => !_)} + className="flex cursor-pointer items-center justify-between border-t py-1" + > + Kuva kõik + +
+
+
+ + + {products.map((product) => ( +
+ + +
+ ))} +
+
+
+
+ ); +}; + +export default ServiceSelector; diff --git a/app/home/(user)/_components/booking/time-slots.tsx b/app/home/(user)/_components/booking/time-slots.tsx new file mode 100644 index 0000000..5860e80 --- /dev/null +++ b/app/home/(user)/_components/booking/time-slots.tsx @@ -0,0 +1,96 @@ +import React from 'react'; + +import { formatCurrency } from '@/packages/shared/src/utils'; +import { format } from 'date-fns'; + +import { Button } from '@kit/ui/shadcn/button'; +import { Card } from '@kit/ui/shadcn/card'; +import { Trans } from '@kit/ui/trans'; + +import { AvailableAppointmentsResponse } from '~/lib/types/connected-online'; + +const dummyData: AvailableAppointmentsResponse['Data']['T_Booking'] = [ + { + ServiceID: 1, + StartTime: new Date('2024-10-10T10:00:00Z'), + EndTime: new Date('2024-10-10T11:00:00Z'), + HKServiceID: 0, + ClinicID: '', + LocationID: 0, + UserID: 0, + SyncUserID: 0, + PayorCode: '', + }, + { + ServiceID: 1, + StartTime: new Date('2024-10-10T11:00:00Z'), + EndTime: new Date('2024-10-10T12:00:00Z'), + HKServiceID: 0, + ClinicID: '', + LocationID: 0, + UserID: 0, + SyncUserID: 0, + PayorCode: '', + }, + { + ServiceID: 2, + StartTime: new Date('2024-10-10T12:00:00Z'), + EndTime: new Date('2024-10-10T13:00:00Z'), + HKServiceID: 0, + ClinicID: '', + LocationID: 0, + UserID: 0, + SyncUserID: 0, + PayorCode: '', + }, +]; + +const TimeSlots = () => { + return ( +
+ {dummyData.map((data) => ( + +
+ {format(data.StartTime.toString(), 'HH:mm')} +
+
+ Dr. Jüri Mardikas +
+ Kardioloog + Tervisekassa aeg +
+
+ + Ülemiste Tervisemaja 2 + + + Ülemiste füsioteraapiakliinik + + + Sepapaja 2/1 + + Tallinn +
+
+
+ + {formatCurrency({ + currencyCode: 'EUR', + locale: 'et-EE', + value: 20, + })} + + +
+
+ ))} +
+ ); +}; + +export default TimeSlots; diff --git a/app/home/(user)/_components/service-categories.tsx b/app/home/(user)/_components/service-categories.tsx index 9ef3e25..4e0aa20 100644 --- a/app/home/(user)/_components/service-categories.tsx +++ b/app/home/(user)/_components/service-categories.tsx @@ -4,17 +4,19 @@ import React from 'react'; import { redirect } from 'next/navigation'; -import { createPath, pathsConfig } from '@/packages/shared/src/config'; +import { pathsConfig } from '@/packages/shared/src/config'; +import { StoreProduct } from '@medusajs/types'; import { ComponentInstanceIcon } from '@radix-ui/react-icons'; import { cn } from '@kit/ui/shadcn'; -import { Card, CardDescription, CardTitle } from '@kit/ui/shadcn/card'; +import { Card, CardDescription } from '@kit/ui/shadcn/card'; export interface ServiceCategory { name: string; handle: string; color: string; description: string; + products: StoreProduct[]; } const ServiceCategories = ({ diff --git a/app/home/(user)/_lib/server/load-category.ts b/app/home/(user)/_lib/server/load-category.ts index 2c0479c..1a2514c 100644 --- a/app/home/(user)/_lib/server/load-category.ts +++ b/app/home/(user)/_lib/server/load-category.ts @@ -25,6 +25,7 @@ async function categoryLoader({ description: category?.description || '', handle: category?.handle || '', name: category?.name || '', + products: category?.products || [], }, }; } diff --git a/lib/services/connected-online.service.ts b/lib/services/connected-online.service.ts index b0ad1f6..e0628cc 100644 --- a/lib/services/connected-online.service.ts +++ b/lib/services/connected-online.service.ts @@ -51,12 +51,12 @@ export async function getAvailableAppointmentsForService( : `No booking times present in appointment availability response, service id: ${serviceId}, start time: ${startTime}`; } - await logRequestResult( - ExternalApi.ConnectedOnline, - ConnectedOnlineMethodName.GetAvailabilities, - RequestStatus.Fail, - comment, - ); + // await logRequestResult( + // ExternalApi.ConnectedOnline, + // ConnectedOnlineMethodName.GetAvailabilities, + // RequestStatus.Fail, + // comment, + // ); return null; } diff --git a/packages/ui/src/shadcn/radio-group.tsx b/packages/ui/src/shadcn/radio-group.tsx index 7779e73..f98a019 100644 --- a/packages/ui/src/shadcn/radio-group.tsx +++ b/packages/ui/src/shadcn/radio-group.tsx @@ -25,12 +25,12 @@ const RadioGroupItem: React.FC< return ( - +