} />
@@ -28,9 +39,15 @@ export default async function MontonioCheckoutCallbackErrorPage() {
-
+ {failedBookingData.length ? (
+ failedBookingData.map((failureReason, index) => (
+
+
+
+ ))
+ ) : (
-
+ )}
diff --git a/app/home/(user)/(dashboard)/cart/page.tsx b/app/home/(user)/(dashboard)/cart/page.tsx
index 41dca03..4a2af1f 100644
--- a/app/home/(user)/(dashboard)/cart/page.tsx
+++ b/app/home/(user)/(dashboard)/cart/page.tsx
@@ -8,9 +8,12 @@ import { listProductTypes } from '@lib/data/products';
import { Trans } from '@kit/ui/trans';
import { withI18n } from '~/lib/i18n/with-i18n';
+import { getCartReservations } from '~/lib/services/reservation.service';
+import { findProductTypeIdByHandle } from '~/lib/utils';
import Cart from '../../_components/cart';
import CartTimer from '../../_components/cart/cart-timer';
+import { EnrichedCartItem } from '../../_components/cart/types';
export async function generateMetadata() {
const { t } = await createI18nServerInstance();
@@ -27,29 +30,33 @@ async function CartPage() {
});
const { productTypes } = await listProductTypes();
- const analysisPackagesType = productTypes.find(
- ({ metadata }) => metadata?.handle === 'analysis-packages',
+
+ const synlabAnalysisTypeId = findProductTypeIdByHandle(
+ productTypes,
+ 'synlab-analysis',
);
- const synlabAnalysisType = productTypes.find(
- ({ metadata }) => metadata?.handle === 'synlab-analysis',
+ const analysisPackagesTypeId = findProductTypeIdByHandle(
+ productTypes,
+ 'analysis-packages',
);
+
const synlabAnalyses =
- analysisPackagesType && synlabAnalysisType && cart?.items
+ analysisPackagesTypeId && synlabAnalysisTypeId && cart?.items
? cart.items.filter((item) => {
const productTypeId = item.product?.type_id;
if (!productTypeId) {
return false;
}
- return [analysisPackagesType.id, synlabAnalysisType.id].includes(
+ return [analysisPackagesTypeId, synlabAnalysisTypeId].includes(
productTypeId,
);
})
: [];
- const ttoServiceItems =
- cart?.items?.filter(
- (item) => !synlabAnalyses.some((analysis) => analysis.id === item.id),
- ) ?? [];
+ let ttoServiceItems: EnrichedCartItem[] = [];
+ if (cart?.items?.length) {
+ ttoServiceItems = await getCartReservations(cart);
+ }
const otherItemsSorted = ttoServiceItems.sort((a, b) =>
(a.updated_at ?? '') > (b.updated_at ?? '') ? -1 : 1,
);
diff --git a/app/home/(user)/(dashboard)/order/page.tsx b/app/home/(user)/(dashboard)/order/page.tsx
index 35e9c96..2e9fe68 100644
--- a/app/home/(user)/(dashboard)/order/page.tsx
+++ b/app/home/(user)/(dashboard)/order/page.tsx
@@ -11,7 +11,8 @@ import { PageBody } from '@kit/ui/makerkit/page';
import { Trans } from '@kit/ui/trans';
import { withI18n } from '~/lib/i18n/with-i18n';
-import { getAnalysisOrders } from '~/lib/services/order.service';
+import { getAnalysisOrders, getTtoOrders } from '~/lib/services/order.service';
+import { findProductTypeIdByHandle } from '~/lib/utils';
import { listOrders } from '~/medusa/lib/data/orders';
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
@@ -28,15 +29,21 @@ export async function generateMetadata() {
async function OrdersPage() {
const medusaOrders = await listOrders();
const analysisOrders = await getAnalysisOrders();
+ const ttoOrders = await getTtoOrders();
const { productTypes } = await listProductTypes();
- if (!medusaOrders || !productTypes) {
+ if (!medusaOrders || !productTypes || !ttoOrders) {
redirect(pathsConfig.auth.signIn);
}
- const analysisPackagesType = productTypes.find(
- ({ metadata }) => metadata?.handle === 'analysis-packages',
- )!;
+ const analysisPackagesTypeId = findProductTypeIdByHandle(
+ productTypes,
+ 'analysis-package',
+ );
+ const ttoServiceTypeId = findProductTypeIdByHandle(
+ productTypes,
+ 'tto-service',
+ );
return (
<>
@@ -45,9 +52,9 @@ async function OrdersPage() {
description={
}
/>
- {analysisOrders.map((analysisOrder) => {
- const medusaOrder = medusaOrders.find(
- ({ id }) => id === analysisOrder.medusa_order_id,
+ {medusaOrders.map((medusaOrder) => {
+ const analysisOrder = analysisOrders.find(
+ ({ medusa_order_id }) => medusa_order_id === medusaOrder.id,
);
if (!medusaOrder) {
return null;
@@ -55,18 +62,27 @@ async function OrdersPage() {
const medusaOrderItems = medusaOrder.items || [];
const medusaOrderItemsAnalysisPackages = medusaOrderItems.filter(
- (item) => item.product_type_id === analysisPackagesType?.id,
+ (item) => item.product_type_id === analysisPackagesTypeId,
+ );
+ const medusaOrderItemsTtoServices = medusaOrderItems.filter(
+ (item) => item.product_type_id === ttoServiceTypeId,
);
const medusaOrderItemsOther = medusaOrderItems.filter(
- (item) => item.product_type_id !== analysisPackagesType?.id,
+ (item) =>
+ !item.product_type_id ||
+ ![analysisPackagesTypeId, ttoServiceTypeId].includes(
+ item.product_type_id,
+ ),
);
return (
-
+
diff --git a/app/home/(user)/_components/booking/booking-calendar.tsx b/app/home/(user)/_components/booking/booking-calendar.tsx
new file mode 100644
index 0000000..f8afb0b
--- /dev/null
+++ b/app/home/(user)/_components/booking/booking-calendar.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+import { isBefore, isSameDay } from 'date-fns';
+import { uniq } from 'lodash';
+
+import { Calendar } from '@kit/ui/shadcn/calendar';
+import { Card } from '@kit/ui/shadcn/card';
+import { cn } from '@kit/ui/utils';
+
+import { useBooking } from './booking.provider';
+
+export default function BookingCalendar() {
+ const { selectedDate, setSelectedDate, isLoadingTimeSlots, timeSlots } =
+ useBooking();
+ const availableDates = uniq(timeSlots?.map((timeSlot) => timeSlot.StartTime));
+
+ return (
+
+ {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ return (
+ isBefore(date, today) ||
+ !availableDates.some((dateWithBooking) =>
+ isSameDay(date, dateWithBooking),
+ )
+ );
+ }}
+ className={cn('rounded-md border', {
+ 'pointer-events-none rounded-md border opacity-50':
+ isLoadingTimeSlots,
+ })}
+ />
+
+ );
+}
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..9d3b6e8
--- /dev/null
+++ b/app/home/(user)/_components/booking/booking-container.tsx
@@ -0,0 +1,49 @@
+'use client';
+
+import { StoreProduct } from '@medusajs/types';
+
+import { Trans } from '@kit/ui/trans';
+
+import { EnrichedCartItem } from '../cart/types';
+import BookingCalendar from './booking-calendar';
+import { BookingProvider } from './booking.provider';
+import LocationSelector from './location-selector';
+import ServiceSelector from './service-selector';
+import TimeSlots from './time-slots';
+
+const BookingContainer = ({
+ category,
+ cartItem,
+ onComplete,
+}: {
+ category: { products: StoreProduct[]; countryCode: string };
+ cartItem?: EnrichedCartItem;
+ onComplete?: () => void;
+}) => {
+ const products = cartItem?.product ? [cartItem.product] : category.products;
+
+ if (!cartItem || !products?.length) {
+
+
+
;
+ }
+
+ 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..ce59dba
--- /dev/null
+++ b/app/home/(user)/_components/booking/booking.context.ts
@@ -0,0 +1,77 @@
+import { createContext } from 'react';
+
+import { StoreProduct } from '@medusajs/types';
+import { noop } from 'lodash';
+
+import { Tables } from '@kit/supabase/database';
+
+export type Location = Tables<
+ { schema: 'medreport' },
+ 'connected_online_locations'
+>;
+
+export type TimeSlotResponse = {
+ timeSlots: TimeSlot[];
+ locations: Location[];
+};
+
+export type TimeSlot = {
+ ClinicID: number;
+ LocationID: number;
+ UserID: number;
+ SyncUserID: number;
+ ServiceID: number;
+ HKServiceID: number;
+ StartTime: Date;
+ EndTime: Date;
+ PayorCode: string;
+ serviceProvider?: ServiceProvider;
+ syncedService?: SyncedService;
+} & { location?: Location };
+
+export type ServiceProvider = {
+ name: string;
+ id: number;
+ jobTitleEn: string | null;
+ jobTitleEt: string | null;
+ jobTitleRu: string | null;
+ clinicId: number;
+};
+
+export type SyncedService = Tables<
+ { schema: 'medreport' },
+ 'connected_online_services'
+> & {
+ providerClinic: ProviderClinic;
+};
+
+export type ProviderClinic = Tables<
+ { schema: 'medreport' },
+ 'connected_online_providers'
+> & { locations: Location[] };
+
+const BookingContext = createContext<{
+ timeSlots: TimeSlot[] | null;
+ selectedService: StoreProduct | null;
+ locations: Location[] | null;
+ selectedLocationId: number | null;
+ selectedDate?: Date;
+ isLoadingTimeSlots?: boolean;
+ setSelectedService: (selectedService: StoreProduct | null) => void;
+ setSelectedLocationId: (selectedLocationId: number | null) => void;
+ updateTimeSlots: (serviceIds: number[]) => Promise;
+ setSelectedDate: (selectedDate?: Date) => void;
+}>({
+ timeSlots: null,
+ selectedService: null,
+ locations: null,
+ selectedLocationId: null,
+ selectedDate: new Date(),
+ isLoadingTimeSlots: false,
+ setSelectedService: (_) => _,
+ setSelectedLocationId: (_) => _,
+ updateTimeSlots: async (_) => noop(),
+ setSelectedDate: (_) => _,
+});
+
+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..b0c21e1
--- /dev/null
+++ b/app/home/(user)/_components/booking/booking.provider.tsx
@@ -0,0 +1,80 @@
+import React, { useContext, useEffect, useState } from 'react';
+
+import { StoreProduct } from '@medusajs/types';
+
+import { getAvailableTimeSlotsForDisplay } from '~/lib/services/connected-online.service';
+
+import { BookingContext, Location, TimeSlot } from './booking.context';
+
+export function useBooking() {
+ const context = 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: { products: StoreProduct[] };
+ service?: StoreProduct;
+}> = ({ children, category, service }) => {
+ const [selectedService, setSelectedService] = useState(
+ (service ?? category?.products?.[0]) || null,
+ );
+ const [selectedLocationId, setSelectedLocationId] = useState(
+ null,
+ );
+ const [selectedDate, setSelectedDate] = useState();
+ const [timeSlots, setTimeSlots] = useState(null);
+ const [locations, setLocations] = useState(null);
+ const [isLoadingTimeSlots, setIsLoadingTimeSlots] = useState(false);
+
+ useEffect(() => {
+ const metadataServiceIds = selectedService?.metadata?.serviceIds as string;
+ if (metadataServiceIds) {
+ const json = JSON.parse(metadataServiceIds);
+ if (Array.isArray(json)) {
+ updateTimeSlots(json);
+ }
+ }
+ }, [selectedService, selectedLocationId]);
+
+ const updateTimeSlots = async (serviceIds: number[]) => {
+ setIsLoadingTimeSlots(true);
+ try {
+ console.log('serviceIds', serviceIds, selectedLocationId);
+ const response = await getAvailableTimeSlotsForDisplay(
+ serviceIds,
+ selectedLocationId,
+ );
+ setTimeSlots(response.timeSlots);
+ setLocations(response.locations);
+ } catch (error) {
+ setTimeSlots(null);
+ } finally {
+ setIsLoadingTimeSlots(false);
+ }
+ };
+
+ 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..4cb02aa
--- /dev/null
+++ b/app/home/(user)/_components/booking/location-selector.tsx
@@ -0,0 +1,55 @@
+import { Label } from '@medusajs/ui';
+import { useTranslation } from 'react-i18next';
+
+import { RadioGroup, RadioGroupItem } from '@kit/ui/radio-group';
+import { Card } from '@kit/ui/shadcn/card';
+import { Trans } from '@kit/ui/trans';
+
+import { useBooking } from './booking.provider';
+
+const LocationSelector = () => {
+ const { t } = useTranslation();
+ const { selectedLocationId, setSelectedLocationId, locations } = useBooking();
+
+ const onLocationSelect = (locationId: number | string | null) => {
+ if (locationId === 'all') return setSelectedLocationId(null);
+ setSelectedLocationId(Number(locationId));
+ };
+
+ return (
+
+
+
+
+
+
onLocationSelect(val)}
+ >
+
+
+
+
+ {locations?.map((location) => (
+
+
+
+
+ ))}
+
+
+
+ );
+};
+
+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..8dd0907
--- /dev/null
+++ b/app/home/(user)/_components/booking/service-selector.tsx
@@ -0,0 +1,85 @@
+import { useState } from 'react';
+
+import { StoreProduct } from '@medusajs/types';
+import { ChevronDown } from 'lucide-react';
+
+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 { Trans } from '@kit/ui/trans';
+
+import { useBooking } from './booking.provider';
+
+const ServiceSelector = ({ products }: { products: StoreProduct[] }) => {
+ const { selectedService, setSelectedService } = useBooking();
+ const [collapsed, setCollapsed] = useState(false);
+ const [firstFourProducts] = useState(products?.slice(0, 4));
+
+ const onServiceSelect = async (productId: StoreProduct['id']) => {
+ const product = products.find((p) => p.id === productId);
+ setSelectedService(product ?? null);
+ setCollapsed(false);
+ };
+
+ return (
+
+
+
+
+
+
+
+ {firstFourProducts?.map((product) => (
+
+
+
+
+ ))}
+
+ {products.length > 4 && (
+
+ setCollapsed((_) => !_)}
+ className="flex cursor-pointer items-center justify-between border-t py-1"
+ >
+
+
+
+
+
+
+ )}
+
+
+
+ {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..220a79f
--- /dev/null
+++ b/app/home/(user)/_components/booking/time-slots.tsx
@@ -0,0 +1,317 @@
+import { useMemo, useState } from 'react';
+
+import { useRouter } from 'next/navigation';
+
+import { formatCurrency } from '@/packages/shared/src/utils';
+import { addHours, isAfter, isSameDay } from 'date-fns';
+import { orderBy } from 'lodash';
+import { useTranslation } from 'react-i18next';
+
+import { pathsConfig } from '@kit/shared/config';
+import { formatDateAndTime } from '@kit/shared/utils';
+import { Button } from '@kit/ui/shadcn/button';
+import { Card } from '@kit/ui/shadcn/card';
+import { toast } from '@kit/ui/sonner';
+import { Trans } from '@kit/ui/trans';
+import { cn } from '@kit/ui/utils';
+
+import { updateReservationTime } from '~/lib/services/reservation.service';
+
+import { createInitialReservationAction } from '../../_lib/server/actions';
+import { EnrichedCartItem } from '../cart/types';
+import { ServiceProvider, TimeSlot } from './booking.context';
+import { useBooking } from './booking.provider';
+
+const getServiceProviderTitle = (
+ currentLocale: string,
+ serviceProvider?: ServiceProvider,
+) => {
+ if (!serviceProvider) return null;
+ if (currentLocale === 'en') return serviceProvider.jobTitleEn;
+ if (currentLocale === 'ru') return serviceProvider.jobTitleRu;
+
+ return serviceProvider.jobTitleEt;
+};
+
+const PAGE_SIZE = 7;
+
+const TimeSlots = ({
+ countryCode,
+ cartItem,
+ onComplete,
+}: {
+ countryCode: string;
+ cartItem?: EnrichedCartItem;
+ onComplete?: () => void;
+}) => {
+ const [currentPage, setCurrentPage] = useState(1);
+
+ const {
+ t,
+ i18n: { language: currentLocale },
+ } = useTranslation();
+
+ const booking = useBooking();
+
+ const router = useRouter();
+
+ const selectedDate = booking.selectedDate ?? new Date();
+
+ const filteredBookings = useMemo(
+ () =>
+ orderBy(
+ booking?.timeSlots?.filter(({ StartTime }) => {
+ const firstAvailableTimeToSelect = isSameDay(selectedDate, new Date())
+ ? addHours(new Date(), 0.5)
+ : selectedDate;
+ return isAfter(StartTime, firstAvailableTimeToSelect);
+ }) ?? [],
+ 'StartTime',
+ 'asc',
+ ),
+ [booking.timeSlots, selectedDate],
+ );
+
+ const totalPages = Math.ceil(filteredBookings.length / PAGE_SIZE);
+
+ const paginatedBookings = useMemo(() => {
+ const startIndex = (currentPage - 1) * PAGE_SIZE;
+ const endIndex = startIndex + PAGE_SIZE;
+ return filteredBookings.slice(startIndex, endIndex);
+ }, [filteredBookings, currentPage, PAGE_SIZE]);
+
+ const handlePageChange = (page: number) => {
+ setCurrentPage(page);
+ };
+
+ const generatePageNumbers = () => {
+ const pages = [];
+ const maxVisiblePages = 5;
+
+ if (totalPages <= maxVisiblePages) {
+ for (let i = 1; i <= totalPages; i++) {
+ pages.push(i);
+ }
+ } else {
+ if (currentPage <= 3) {
+ for (let i = 1; i <= 4; i++) {
+ pages.push(i);
+ }
+ pages.push('...');
+ pages.push(totalPages);
+ } else if (currentPage >= totalPages - 2) {
+ pages.push(1);
+ pages.push('...');
+ for (let i = totalPages - 3; i <= totalPages; i++) {
+ pages.push(i);
+ }
+ } else {
+ pages.push(1);
+ pages.push('...');
+ for (let i = currentPage - 1; i <= currentPage + 1; i++) {
+ pages.push(i);
+ }
+ pages.push('...');
+ pages.push(totalPages);
+ }
+ }
+
+ return pages;
+ };
+
+ if (!booking?.timeSlots?.length) {
+ return null;
+ }
+
+ const handleBookTime = async (timeSlot: TimeSlot, comments?: string) => {
+ const selectedService = booking.selectedService;
+
+ const selectedVariant = selectedService?.variants?.[0];
+
+ const syncedService = timeSlot.syncedService;
+ if (!syncedService || !selectedVariant) {
+ return toast.error(t('booking:serviceNotFound'));
+ }
+
+ const bookTimePromise = createInitialReservationAction(
+ selectedVariant,
+ countryCode,
+ Number(syncedService.id),
+ syncedService?.clinic_id,
+ timeSlot.UserID,
+ timeSlot.SyncUserID,
+ timeSlot.StartTime,
+ booking.selectedLocationId ? booking.selectedLocationId : null,
+ comments,
+ ).then(() => {
+ if (onComplete) {
+ onComplete();
+ }
+ router.push(pathsConfig.app.cart);
+ });
+
+ toast.promise(() => bookTimePromise, {
+ success: ,
+ error: ,
+ loading: ,
+ });
+ };
+
+ const handleChangeTime = async (
+ timeSlot: TimeSlot,
+ reservationId: number,
+ cartId: string,
+ ) => {
+ const syncedService = timeSlot.syncedService;
+ if (!syncedService) {
+ return toast.error(t('booking:serviceNotFound'));
+ }
+
+ const bookTimePromise = updateReservationTime(
+ reservationId,
+ timeSlot.StartTime,
+ Number(syncedService.id),
+ timeSlot.UserID,
+ timeSlot.SyncUserID,
+ booking.selectedLocationId ? booking.selectedLocationId : null,
+ cartId,
+ );
+
+ toast.promise(() => bookTimePromise, {
+ success: ,
+ error: ,
+ loading: ,
+ });
+
+ if (onComplete) {
+ onComplete();
+ }
+ };
+
+ const handleTimeSelect = async (timeSlot: TimeSlot) => {
+ if (cartItem?.reservation.id) {
+ return handleChangeTime(
+ timeSlot,
+ cartItem.reservation.id,
+ cartItem.cart_id,
+ );
+ }
+
+ return handleBookTime(timeSlot);
+ };
+
+ return (
+
+
+ {paginatedBookings.map((timeSlot, index) => {
+ const isEHIF = timeSlot.HKServiceID > 0;
+ const serviceProviderTitle = getServiceProviderTitle(
+ currentLocale,
+ timeSlot.serviceProvider,
+ );
+ const price =
+ booking.selectedService?.variants?.[0]?.calculated_price
+ ?.calculated_amount ?? cartItem?.unit_price;
+ return (
+
+
+
{formatDateAndTime(timeSlot.StartTime.toString())}
+
+
+ {timeSlot.serviceProvider?.name}
+
+ {serviceProviderTitle && (
+
+ {serviceProviderTitle}
+
+ )}
+ {isEHIF && {t('booking:ehifBooking')}}
+
+
{timeSlot.location?.address}
+
+
+
+ {formatCurrency({
+ currencyCode: 'EUR',
+ locale: 'et-EE',
+ value: price ?? '',
+ })}
+
+
+
+
+ );
+ })}
+
+ {!paginatedBookings.length && (
+
+
{t('booking:noResults')}
+
+ )}
+
+
+ {totalPages > 1 && (
+
+
+ {t('common:pageOfPages', {
+ page: currentPage,
+ total: totalPages,
+ })}
+
+
+
+
+
+ {generatePageNumbers().map((page, index) => (
+
+ ))}
+
+
+
+
+ )}
+
+ );
+};
+
+export default TimeSlots;
diff --git a/app/home/(user)/_components/cart/cart-service-item.tsx b/app/home/(user)/_components/cart/cart-service-item.tsx
new file mode 100644
index 0000000..eed6fcd
--- /dev/null
+++ b/app/home/(user)/_components/cart/cart-service-item.tsx
@@ -0,0 +1,146 @@
+'use client';
+
+import { useState } from 'react';
+
+import { formatCurrency, formatDateAndTime } from '@/packages/shared/src/utils';
+import { useTranslation } from 'react-i18next';
+
+import { Button } from '@kit/ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from '@kit/ui/dialog';
+import { TableCell, TableRow } from '@kit/ui/table';
+import { Trans } from '@kit/ui/trans';
+
+import BookingContainer from '../booking/booking-container';
+import CartItemDelete from './cart-item-delete';
+import { EnrichedCartItem } from './types';
+
+const EditCartServiceItemModal = ({
+ item,
+ onComplete,
+}: {
+ item: EnrichedCartItem | null;
+ onComplete: () => void;
+}) => {
+ if (!item) return null;
+
+ return (
+
+ );
+};
+
+export default function CartServiceItem({
+ item,
+ currencyCode,
+ isUnavailable,
+}: {
+ item: EnrichedCartItem;
+ currencyCode: string;
+ isUnavailable?: boolean;
+}) {
+ const [editingItem, setEditingItem] = useState(null);
+ const {
+ i18n: { language },
+ } = useTranslation();
+
+ return (
+ <>
+
+
+
+ {item.product_title}
+
+
+
+
+ {formatDateAndTime(item.reservation.startTime.toString())}
+
+
+
+ {item.reservation.location?.address ?? '-'}
+
+
+ {item.quantity}
+
+
+ {formatCurrency({
+ value: item.unit_price,
+ currencyCode,
+ locale: language,
+ })}
+
+
+
+ {formatCurrency({
+ value: item.total,
+ currencyCode,
+ locale: language,
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isUnavailable && (
+
+
+
+
+
+ )}
+ setEditingItem(null)}
+ />
+ >
+ );
+}
diff --git a/app/home/(user)/_components/cart/cart-service-items.tsx b/app/home/(user)/_components/cart/cart-service-items.tsx
new file mode 100644
index 0000000..ad5cc12
--- /dev/null
+++ b/app/home/(user)/_components/cart/cart-service-items.tsx
@@ -0,0 +1,72 @@
+import { StoreCart } from '@medusajs/types';
+
+import {
+ Table,
+ TableBody,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@kit/ui/table';
+import { Trans } from '@kit/ui/trans';
+
+import CartServiceItem from './cart-service-item';
+import { EnrichedCartItem } from './types';
+
+export default function CartServiceItems({
+ cart,
+ items,
+ productColumnLabelKey,
+ unavailableLineItemIds,
+}: {
+ cart: StoreCart;
+ items: EnrichedCartItem[];
+ productColumnLabelKey: string;
+ unavailableLineItemIds?: string[];
+}) {
+ if (!items || items.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {items
+ .sort((a, b) =>
+ (a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
+ )
+ .map((item) => (
+
+ ))}
+
+
+ );
+}
diff --git a/app/home/(user)/_components/cart/index.tsx b/app/home/(user)/_components/cart/index.tsx
index 7887040..81fbdd4 100644
--- a/app/home/(user)/_components/cart/index.tsx
+++ b/app/home/(user)/_components/cart/index.tsx
@@ -15,7 +15,9 @@ import { Trans } from '@kit/ui/trans';
import AnalysisLocation from './analysis-location';
import CartItems from './cart-items';
+import CartServiceItems from './cart-service-items';
import DiscountCode from './discount-code';
+import { EnrichedCartItem } from './types';
const IS_DISCOUNT_SHOWN = true as boolean;
@@ -26,13 +28,15 @@ export default function Cart({
}: {
cart: StoreCart | null;
synlabAnalyses: StoreCartLineItem[];
- ttoServiceItems: StoreCartLineItem[];
+ ttoServiceItems: EnrichedCartItem[];
}) {
const {
i18n: { language },
} = useTranslation();
const [isInitiatingSession, setIsInitiatingSession] = useState(false);
+ const [unavailableLineItemIds, setUnavailableLineItemIds] =
+ useState();
const items = cart?.items ?? [];
@@ -64,8 +68,16 @@ export default function Cart({
if (response.payment_collection) {
const { payment_sessions } = response.payment_collection;
const paymentSessionId = payment_sessions![0]!.id;
- const url = await handleNavigateToPayment({ language, paymentSessionId });
- window.location.href = url;
+ const result = await handleNavigateToPayment({
+ language,
+ paymentSessionId,
+ });
+ if (result.url) {
+ window.location.href = result.url;
+ }
+ if (result.unavailableLineItemIds) {
+ setUnavailableLineItemIds(result.unavailableLineItemIds);
+ }
} else {
setIsInitiatingSession(false);
}
@@ -82,10 +94,11 @@ export default function Cart({
items={synlabAnalyses}
productColumnLabelKey="cart:items.synlabAnalyses.productColumnLabel"
/>
-
{hasCartItems && (
@@ -167,6 +180,10 @@ export default function Cart({
cart={{ ...cart }}
synlabAnalyses={synlabAnalyses}
/>
+