Merge branch 'develop' into MED-177

This commit is contained in:
Danel Kungla
2025-10-21 17:27:54 +03:00
131 changed files with 2202 additions and 921 deletions

View File

@@ -3,6 +3,9 @@ import React from 'react';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { createNotificationsApi } from '@/packages/features/notifications/src/server/api';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import { ButtonTooltip } from '@kit/shared/components/ui/button-tooltip';
import { pathsConfig } from '@kit/shared/config';
import { Button } from '@kit/ui/button';
@@ -25,7 +28,9 @@ export default async function AnalysisResultsPage({
id: string;
}>;
}) {
const supabaseClient = getSupabaseServerClient();
const { id: analysisOrderId } = await params;
const notificationsApi = createNotificationsApi(supabaseClient);
const [{ account }, analysisResponse] = await Promise.all([
loadCurrentUserAccount(),
@@ -41,6 +46,11 @@ export default async function AnalysisResultsPage({
action: PageViewAction.VIEW_ANALYSIS_RESULTS,
});
await notificationsApi.dismissNotification(
`/home/analysis-results/${analysisOrderId}`,
'link',
);
if (!analysisResponse) {
return (
<>
@@ -108,7 +118,7 @@ export default async function AnalysisResultsPage({
)}
<div className="flex flex-col gap-2">
{orderedAnalysisElements ? (
orderedAnalysisElements.map((element, index) => (
orderedAnalysisElements.map((element) => (
<React.Fragment key={element.analysisIdOriginal}>
<Analysis element={element} />
{element.results?.nestedElements?.map(

View File

@@ -1,5 +1,3 @@
import { use } from 'react';
import Link from 'next/link';
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';

View File

@@ -28,17 +28,18 @@ async function OrderAnalysisPackagePage() {
<PageBody>
<div className="space-y-3 text-center">
<h3>
<Trans i18nKey={'marketing:selectPackage'} />
<Trans i18nKey="order-analysis-package:selectPackage" />
</h3>
<ComparePackagesModal
analysisPackages={analysisPackages}
analysisPackageElements={analysisPackageElements}
triggerElement={
<Button variant="secondary" className="gap-2">
<Trans i18nKey={'marketing:comparePackages'} />
<Trans i18nKey="order-analysis-package:comparePackages" />
<Scale className="size-4 stroke-[1.5px]" />
</Button>
}
countryCode={countryCode}
/>
</div>
<SelectAnalysisPackages

View File

@@ -13,7 +13,7 @@ import { GlobalLoader } from '@kit/ui/makerkit/global-loader';
import { PageBody, PageHeader } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { AnalysisOrder } from '~/lib/types/analysis-order';
import { AnalysisOrder } from '~/lib/types/order';
function OrderConfirmedLoadingWrapper({
medusaOrder: initialMedusaOrder,
@@ -71,7 +71,14 @@ function OrderConfirmedLoadingWrapper({
<PageHeader title={<Trans i18nKey="cart:orderConfirmed.title" />} />
<Divider />
<div className="small:grid-cols-[1fr_360px] grid grid-cols-1 gap-x-40 gap-y-6 lg:px-4">
<OrderDetails order={order} />
<OrderDetails
order={{
id: order.medusa_order_id,
created_at: order.created_at,
location: null,
serviceProvider: null,
}}
/>
<Divider />
<OrderItems medusaOrder={medusaOrder} />
<CartTotals medusaOrder={medusaOrder} />

View File

@@ -90,6 +90,14 @@ async function OrdersPage() {
),
);
if (
medusaOrderItemsAnalysisPackages.length === 0 &&
medusaOrderItemsOther.length === 0 &&
medusaOrderItemsTtoServices.length === 0
) {
return null;
}
return (
<React.Fragment key={medusaOrder.id}>
<Divider className="my-6" />

View File

@@ -51,7 +51,7 @@ export const BookingProvider: React.FC<{
);
setTimeSlots(response.timeSlots);
setLocations(response.locations);
} catch (error) {
} catch {
setTimeSlots(null);
} finally {
setIsLoadingTimeSlots(false);

View File

@@ -186,7 +186,7 @@ const TimeSlots = ({
return (
<Card
className="xs:flex xs:justify-between grid w-full justify-center-safe gap-3 p-4"
className="xs:flex xs:justify-between w-full justify-center-safe gap-3 p-4"
key={index}
>
<div>

View File

@@ -1,7 +1,11 @@
'use client';
import { formatCurrency } from '@/packages/shared/src/utils';
import { StoreCart, StoreCartLineItem } from '@medusajs/types';
import {
StoreCart,
StoreCartLineItem,
StoreCartPromotion,
} from '@medusajs/types';
import { Loader2 } from 'lucide-react';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
@@ -30,7 +34,9 @@ export default function CartFormContent({
isInitiatingSession,
getBalanceSummarySelection,
}: {
cart: StoreCart;
cart: StoreCart & {
promotions: StoreCartPromotion[];
};
synlabAnalyses: StoreCartLineItem[];
ttoServiceItems: EnrichedCartItem[];
unavailableLineItemIds?: string[];

View File

@@ -20,7 +20,7 @@ export default function CartItem({
} = useTranslation();
return (
<TableRow className="w-full" data-testid="product-row">
<TableRow className="sm:w-full" data-testid="product-row">
<TableCell className="w-[100%] px-4 text-left sm:px-6">
<p
className="txt-medium-plus text-ui-fg-base"
@@ -41,11 +41,12 @@ export default function CartItem({
</TableCell>
<TableCell className="min-w-[80px] px-4 text-right sm:px-6">
{formatCurrency({
value: item.total,
currencyCode,
locale: language,
})}
{item.total &&
formatCurrency({
value: item.total,
currencyCode,
locale: language,
})}
</TableCell>
<TableCell className="px-4 text-right sm:px-6">

View File

@@ -10,6 +10,7 @@ import {
import { Trans } from '@kit/ui/trans';
import CartItem from './cart-item';
import MobileCartItems from './mobile-cart-items';
export default function CartItems({
cart,
@@ -25,37 +26,54 @@ export default function CartItems({
}
return (
<Table className="border-separate rounded-lg border">
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
<TableRow>
<TableHead className="px-4 sm:px-6">
<Trans i18nKey={productColumnLabelKey} />
</TableHead>
<TableHead className="px-4 sm:px-6">
<Trans i18nKey="cart:table.quantity" />
</TableHead>
<TableHead className="min-w-[100px] px-4 sm:px-6">
<Trans i18nKey="cart:table.price" />
</TableHead>
<TableHead className="min-w-[100px] px-4 text-right sm:px-6">
<Trans i18nKey="cart:table.total" />
</TableHead>
<TableHead className="px-4 sm:px-6"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<>
<Table className="hidden border-separate rounded-lg border sm:block">
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
<TableRow>
<TableHead className="px-4 sm:px-6">
<Trans i18nKey={productColumnLabelKey} />
</TableHead>
<TableHead className="px-4 sm:px-6">
<Trans i18nKey="cart:table.quantity" />
</TableHead>
<TableHead className="min-w-[100px] px-4 sm:px-6">
<Trans i18nKey="cart:table.price" />
</TableHead>
<TableHead className="min-w-[100px] px-4 text-right sm:px-6">
<Trans i18nKey="cart:table.total" />
</TableHead>
<TableHead className="px-4 sm:px-6"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items
.sort((a, b) =>
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
)
.map((item) => (
<CartItem
key={item.id}
item={item}
currencyCode={cart.currency_code}
/>
))}
</TableBody>
</Table>
<div className="sm:hidden">
{items
.sort((a, b) =>
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
)
.map((item) => (
<CartItem
<MobileCartItems
key={item.id}
item={item}
currencyCode={cart.currency_code}
productColumnLabelKey={productColumnLabelKey}
/>
))}
</TableBody>
</Table>
</div>
</>
);
}

View File

@@ -1,76 +1,26 @@
'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 (
<Dialog defaultOpen>
<DialogContent className="xs:max-w-[90vw] flex max-h-screen max-w-full flex-col items-center gap-4 space-y-4 overflow-y-scroll">
<DialogHeader className="items-center text-center">
<DialogTitle>
<Trans i18nKey="cart:editServiceItem.title" />
</DialogTitle>
<DialogDescription>
<Trans i18nKey="cart:editServiceItem.description" />
</DialogDescription>
</DialogHeader>
<div>
{item.product && item.reservation.countryCode ? (
<BookingContainer
category={{
products: [item.product],
countryCode: item.reservation.countryCode,
}}
cartItem={item}
onComplete={onComplete}
/>
) : (
<p>
<Trans i18nKey="booking:noProducts" />
</p>
)}
</div>
</DialogContent>
</Dialog>
);
};
export default function CartServiceItem({
item,
currencyCode,
isUnavailable,
setEditingItem,
}: {
item: EnrichedCartItem;
currencyCode: string;
isUnavailable?: boolean;
setEditingItem: (item: EnrichedCartItem | null) => void;
}) {
const [editingItem, setEditingItem] = useState<EnrichedCartItem | null>(null);
const {
i18n: { language },
} = useTranslation();
@@ -106,11 +56,12 @@ export default function CartServiceItem({
</TableCell>
<TableCell className="min-w-[80px] px-4 text-right sm:px-6">
{formatCurrency({
value: item.total,
currencyCode,
locale: language,
})}
{item.total &&
formatCurrency({
value: item.total,
currencyCode,
locale: language,
})}
</TableCell>
<TableCell className="px-4 text-right sm:px-6">
@@ -137,10 +88,6 @@ export default function CartServiceItem({
</TableCell>
</TableRow>
)}
<EditCartServiceItemModal
item={editingItem}
onComplete={() => setEditingItem(null)}
/>
</>
);
}

View File

@@ -1,5 +1,14 @@
import { useState } from 'react';
import { StoreCart } from '@medusajs/types';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@kit/ui/shadcn/dialog';
import {
Table,
TableBody,
@@ -9,9 +18,52 @@ import {
} from '@kit/ui/table';
import { Trans } from '@kit/ui/trans';
import BookingContainer from '../booking/booking-container';
import CartServiceItem from './cart-service-item';
import MobileCartServiceItems from './mobile-cart-service-items';
import { EnrichedCartItem } from './types';
const EditCartServiceItemModal = ({
item,
onComplete,
}: {
item: EnrichedCartItem | null;
onComplete: () => void;
}) => {
if (!item) return null;
return (
<Dialog defaultOpen>
<DialogContent className="xs:max-w-[90vw] flex max-h-screen max-w-full flex-col items-center gap-4 space-y-4 overflow-y-scroll">
<DialogHeader className="items-center text-center">
<DialogTitle>
<Trans i18nKey="cart:editServiceItem.title" />
</DialogTitle>
<DialogDescription>
<Trans i18nKey="cart:editServiceItem.description" />
</DialogDescription>
</DialogHeader>
<div>
{item.product && item.reservation.countryCode ? (
<BookingContainer
category={{
products: [item.product],
countryCode: item.reservation.countryCode,
}}
cartItem={item}
onComplete={onComplete}
/>
) : (
<p>
<Trans i18nKey="booking:noProducts" />
</p>
)}
</div>
</DialogContent>
</Dialog>
);
};
export default function CartServiceItems({
cart,
items,
@@ -23,50 +75,75 @@ export default function CartServiceItems({
productColumnLabelKey: string;
unavailableLineItemIds?: string[];
}) {
const [editingItem, setEditingItem] = useState<EnrichedCartItem | null>(null);
if (!items || items.length === 0) {
return null;
}
return (
<Table className="border-separate rounded-lg border">
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
<TableRow>
<TableHead className="px-4 sm:px-6">
<Trans i18nKey={productColumnLabelKey} />
</TableHead>
<TableHead className="px-4 sm:px-6">
<Trans i18nKey="cart:table.time" />
</TableHead>
<TableHead className="px-4 sm:px-6">
<Trans i18nKey="cart:table.location" />
</TableHead>
<TableHead className="px-4 sm:px-6">
<Trans i18nKey="cart:table.quantity" />
</TableHead>
<TableHead className="min-w-[100px] px-4 sm:px-6">
<Trans i18nKey="cart:table.price" />
</TableHead>
<TableHead className="min-w-[100px] px-4 text-right sm:px-6">
<Trans i18nKey="cart:table.total" />
</TableHead>
<TableHead className="px-4 sm:px-6"></TableHead>
<TableHead className="px-4 sm:px-6"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<>
<Table className="hidden border-separate rounded-lg border sm:block">
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
<TableRow>
<TableHead className="px-4 sm:px-6">
<Trans i18nKey={productColumnLabelKey} />
</TableHead>
<TableHead className="px-4 sm:px-6">
<Trans i18nKey="cart:table.time" />
</TableHead>
<TableHead className="px-4 sm:px-6">
<Trans i18nKey="cart:table.location" />
</TableHead>
<TableHead className="px-4 sm:px-6">
<Trans i18nKey="cart:table.quantity" />
</TableHead>
<TableHead className="min-w-[100px] px-4 sm:px-6">
<Trans i18nKey="cart:table.price" />
</TableHead>
<TableHead className="min-w-[100px] px-4 text-right sm:px-6">
<Trans i18nKey="cart:table.total" />
</TableHead>
<TableHead className="px-4 sm:px-6"></TableHead>
<TableHead className="px-4 sm:px-6"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items
.sort((a, b) =>
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
)
.map((item) => (
<CartServiceItem
key={item.id}
item={item}
currencyCode={cart.currency_code}
isUnavailable={unavailableLineItemIds?.includes(item.id)}
setEditingItem={setEditingItem}
/>
))}
</TableBody>
</Table>
<div className="sm:hidden">
{items
.sort((a, b) =>
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
)
.map((item) => (
<CartServiceItem
<MobileCartServiceItems
key={item.id}
item={item}
currencyCode={cart.currency_code}
isUnavailable={unavailableLineItemIds?.includes(item.id)}
productColumnLabelKey={productColumnLabelKey}
setEditingItem={setEditingItem}
/>
))}
</TableBody>
</Table>
</div>
<EditCartServiceItemModal
item={editingItem}
onComplete={() => setEditingItem(null)}
/>
</>
);
}

View File

@@ -3,7 +3,7 @@
import React, { useState } from 'react';
import { convertToLocale } from '@lib/util/money';
import { StoreCart, StorePromotion } from '@medusajs/types';
import { StoreCart, StoreCartPromotion } from '@medusajs/types';
import { Badge, Text } from '@medusajs/ui';
import Trash from '@modules/common/icons/trash';
import { useFormContext } from 'react-hook-form';
@@ -24,7 +24,7 @@ export default function DiscountCode({
cart,
}: {
cart: StoreCart & {
promotions: StorePromotion[];
promotions: StoreCartPromotion[];
};
}) {
const { t } = useTranslation('cart');

View File

@@ -4,7 +4,11 @@ import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import { StoreCart, StoreCartLineItem } from '@medusajs/types';
import {
StoreCart,
StoreCartLineItem,
StoreCartPromotion,
} from '@medusajs/types';
import { useTranslation } from 'react-i18next';
import { AccountBalanceSummary } from '@kit/accounts/services/account-balance.service';
@@ -23,7 +27,11 @@ export default function Cart({
balanceSummary,
}: {
accountId: string;
cart: StoreCart | null;
cart:
| (StoreCart & {
promotions: StoreCartPromotion[];
})
| null;
synlabAnalyses: StoreCartLineItem[];
ttoServiceItems: EnrichedCartItem[];
balanceSummary: AccountBalanceSummary | null;

View File

@@ -0,0 +1,56 @@
import React from 'react';
import { formatCurrency } from '@/packages/shared/src/utils';
import { StoreCartLineItem } from '@medusajs/types';
import { useTranslation } from 'react-i18next';
import { Table, TableBody } from '@kit/ui/shadcn/table';
import MobileTableRow from './mobile-table-row';
const MobileCartItems = ({
item,
currencyCode,
productColumnLabelKey,
}: {
item: StoreCartLineItem;
currencyCode: string;
productColumnLabelKey: string;
}) => {
const {
i18n: { language },
} = useTranslation();
return (
<Table className="border-separate rounded-lg border p-2">
<TableBody>
<MobileTableRow
titleKey={productColumnLabelKey}
value={item.product_title}
/>
<MobileTableRow titleKey="cart:table.time" value={item.quantity} />
<MobileTableRow
titleKey="cart:table.price"
value={formatCurrency({
value: item.unit_price,
currencyCode,
locale: language,
})}
/>
<MobileTableRow
titleKey="cart:table.total"
value={
item.total &&
formatCurrency({
value: item.total,
currencyCode,
locale: language,
})
}
/>
</TableBody>
</Table>
);
};
export default MobileCartItems;

View File

@@ -0,0 +1,91 @@
import React from 'react';
import { formatCurrency, formatDateAndTime } from '@/packages/shared/src/utils';
import { useTranslation } from 'react-i18next';
import { Trans } from '@kit/ui/makerkit/trans';
import { Button } from '@kit/ui/shadcn/button';
import { Table, TableBody, TableCell, TableRow } from '@kit/ui/shadcn/table';
import CartItemDelete from './cart-item-delete';
import MobileTableRow from './mobile-table-row';
import { EnrichedCartItem } from './types';
const MobileCartServiceItems = ({
item,
currencyCode,
isUnavailable,
productColumnLabelKey,
setEditingItem,
}: {
item: EnrichedCartItem;
currencyCode: string;
isUnavailable?: boolean;
productColumnLabelKey: string;
setEditingItem: (item: EnrichedCartItem | null) => void;
}) => {
const {
i18n: { language },
} = useTranslation();
return (
<Table className="border-separate rounded-lg border p-2">
<TableBody>
<MobileTableRow
titleKey={productColumnLabelKey}
value={item.product_title}
/>
<MobileTableRow
titleKey="cart:table.time"
value={formatDateAndTime(item.reservation.startTime.toString())}
/>
<MobileTableRow
titleKey="cart:table.location"
value={item.reservation.location?.address ?? '-'}
/>
<MobileTableRow titleKey="cart:table.quantity" value={item.quantity} />
<MobileTableRow
titleKey="cart:table.price"
value={formatCurrency({
value: item.unit_price,
currencyCode,
locale: language,
})}
/>
<MobileTableRow
titleKey="cart:table.total"
value={
item.total &&
formatCurrency({
value: item.total,
currencyCode,
locale: language,
})
}
/>
<TableRow>
<TableCell />
<TableCell className="flex w-full items-center justify-end gap-4 p-0 pt-2">
<CartItemDelete id={item.id} />
<Button onClick={() => setEditingItem(item)}>
<Trans i18nKey="common:change" />
</Button>
</TableCell>
</TableRow>
{isUnavailable && (
<TableRow>
<TableCell
colSpan={8}
className="text-destructive px-4 text-left sm:px-6"
>
<Trans i18nKey="booking:timeSlotUnavailable" />
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
);
};
export default MobileCartServiceItems;

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { Trans } from '@kit/ui/makerkit/trans';
import { TableCell, TableHead, TableRow } from '@kit/ui/shadcn/table';
const MobleTableRow = ({
titleKey,
value,
}: {
titleKey?: string;
value?: string | number;
}) => (
<TableRow>
<TableHead className="h-2 font-bold">
<Trans i18nKey={titleKey} />
</TableHead>
<TableCell className="p-0 text-right">
<p className="txt-medium-plus text-ui-fg-base">{value}</p>
</TableCell>
</TableRow>
);
export default MobleTableRow;

View File

@@ -0,0 +1,11 @@
'use client';
import { Check } from 'lucide-react';
export const CheckWithBackground = () => {
return (
<div className="bg-primary w-min rounded-full p-1 text-white">
<Check className="size-3 stroke-2" />
</div>
);
};

View File

@@ -0,0 +1,115 @@
'use client';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { AnalysisPackageWithVariant } from '@/packages/shared/src/components/select-analysis-package';
import { pathsConfig } from '@/packages/shared/src/config';
import { Spinner } from '@kit/ui/makerkit/spinner';
import { Trans } from '@kit/ui/makerkit/trans';
import { Button } from '@kit/ui/shadcn/button';
import { toast } from '@kit/ui/shadcn/sonner';
import { Table, TableBody, TableCell, TableRow } from '@kit/ui/shadcn/table';
import { handleAddToCart } from '~/lib/services/medusaCart.service';
import { cn } from '~/lib/utils';
const AddToCartButton = ({
onClick,
disabled,
isLoading,
}: {
onClick: () => void;
disabled: boolean;
isLoading: boolean;
}) => {
return (
<TableCell align="center" className="xs:px-2 px-1 py-6">
<Button
onClick={onClick}
disabled={disabled}
className="xs:p-6 xs:text-sm relative p-2 text-[10px]"
>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center">
<Spinner />
</div>
)}
<div
className={cn({
invisible: isLoading,
})}
>
<Trans i18nKey="compare-packages-modal:selectThisPackage" />
</div>
</Button>
</TableCell>
);
};
const ComparePackagesAddToCartButtons = ({
countryCode,
standardPackage,
standardPlusPackage,
premiumPackage,
}: {
countryCode: string;
standardPackage: AnalysisPackageWithVariant;
standardPlusPackage: AnalysisPackageWithVariant;
premiumPackage: AnalysisPackageWithVariant;
}) => {
const [addedPackage, setAddedPackage] = useState<string | null>(null);
const router = useRouter();
const handleSelect = async ({ variantId }: AnalysisPackageWithVariant) => {
setAddedPackage(variantId);
try {
await handleAddToCart({
selectedVariant: { id: variantId },
countryCode,
});
setAddedPackage(null);
toast.success(
<Trans i18nKey={'order-analysis-package:analysisPackageAddedToCart'} />,
);
router.push(pathsConfig.app.cart);
} catch (e) {
toast.error(
<Trans
i18nKey={'order-analysis-package:analysisPackageAddToCartError'}
/>,
);
setAddedPackage(null);
console.error(e);
}
};
return (
<Table>
<TableBody>
<TableRow>
<TableCell className="w-[30vw] py-6" />
<AddToCartButton
onClick={() => handleSelect(standardPackage)}
disabled={!!addedPackage}
isLoading={addedPackage === standardPackage.variantId}
/>
<AddToCartButton
onClick={() => handleSelect(standardPlusPackage)}
disabled={!!addedPackage}
isLoading={addedPackage === standardPlusPackage.variantId}
/>
<AddToCartButton
onClick={() => handleSelect(premiumPackage)}
disabled={!!addedPackage}
isLoading={addedPackage === premiumPackage.variantId}
/>
</TableRow>
</TableBody>
</Table>
);
};
export default ComparePackagesAddToCartButtons;

View File

@@ -3,7 +3,7 @@ import { JSX } from 'react';
import { StoreProduct } from '@medusajs/types';
import { QuestionMarkCircledIcon } from '@radix-ui/react-icons';
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
import { Check, X } from 'lucide-react';
import { X } from 'lucide-react';
import { PackageHeader } from '@kit/shared/components/package-header';
import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
@@ -26,6 +26,10 @@ import {
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { CheckWithBackground } from './check-with-background';
import ComparePackagesAddToCartButtons from './compare-packages-add-to-cart-buttons';
import DefaultPackageFeaturesRows from './default-package-features-rows';
export type AnalysisPackageElement = Pick<
StoreProduct,
'title' | 'id' | 'description'
@@ -35,14 +39,6 @@ export type AnalysisPackageElement = Pick<
isIncludedInPremium: boolean;
};
const CheckWithBackground = () => {
return (
<div className="bg-primary w-min rounded-full p-1 text-white">
<Check className="size-3 stroke-2" />
</div>
);
};
const PackageTableHead = async ({
product,
}: {
@@ -53,7 +49,7 @@ const PackageTableHead = async ({
const { title, price, nrOfAnalyses } = product;
return (
<TableHead className="py-2">
<TableHead className="xs:content-normal content-start py-2">
<PackageHeader
title={t(title)}
tagColor="bg-cyan"
@@ -69,10 +65,12 @@ const ComparePackagesModal = async ({
analysisPackages,
analysisPackageElements,
triggerElement,
countryCode,
}: {
analysisPackages: AnalysisPackageWithVariant[];
analysisPackageElements: AnalysisPackageElement[];
triggerElement: JSX.Element;
countryCode: string;
}) => {
const { t } = await createI18nServerInstance();
@@ -92,7 +90,7 @@ const ComparePackagesModal = async ({
<DialogContent
className="min-h-screen max-w-fit min-w-screen"
customClose={
<div className="inline-flex place-items-center-safe gap-1 align-middle">
<div className="absolute top-6 right-0 flex place-items-center-safe sm:top-0">
<p className="text-sm font-medium text-black">
{t('common:close')}
</p>
@@ -106,11 +104,13 @@ const ComparePackagesModal = async ({
</VisuallyHidden>
<div className="m-auto">
<div className="space-y-6 text-center">
<h3>{t('product:healthPackageComparison.label')}</h3>
<p className="text-muted-foreground mx-auto w-3/5 text-sm">
<h3 className="sm:text-xxl text-lg">
{t('product:healthPackageComparison.label')}
</h3>
<p className="text-muted-foreground text-sm sm:mx-auto sm:w-3/5">
{t('product:healthPackageComparison.description')}
</p>
<div className="max-h-[80vh] overflow-y-auto rounded-md border">
<div className="max-h-[50vh] overflow-y-auto rounded-md border sm:max-h-[70vh]">
<Table>
<TableHeader>
<TableRow>
@@ -121,6 +121,8 @@ const ComparePackagesModal = async ({
</TableRow>
</TableHeader>
<TableBody>
<DefaultPackageFeaturesRows />
{analysisPackageElements.map(
({
title,
@@ -136,12 +138,14 @@ const ComparePackagesModal = async ({
return (
<TableRow key={id}>
<TableCell className="py-6 sm:max-w-[30vw]">
<TableCell className="relative py-6 sm:w-[30vw]">
{title}{' '}
{description && (
<InfoTooltip
content={description}
icon={<QuestionMarkCircledIcon />}
icon={
<QuestionMarkCircledIcon className="absolute top-2 right-0 size-5 sm:static sm:ml-2 sm:size-4" />
}
/>
)}
</TableCell>
@@ -164,6 +168,12 @@ const ComparePackagesModal = async ({
</Table>
</div>
</div>
<ComparePackagesAddToCartButtons
countryCode={countryCode}
standardPackage={standardPackage}
premiumPackage={premiumPackage}
standardPlusPackage={standardPlusPackage}
/>
</div>
</DialogContent>
</Dialog>

View File

@@ -0,0 +1,57 @@
'use client';
import React from 'react';
import { InfoTooltip } from '@/packages/shared/src/components/ui/info-tooltip';
import { QuestionMarkCircledIcon } from '@radix-ui/react-icons';
import { Trans } from '@kit/ui/makerkit/trans';
import { TableCell, TableRow } from '@kit/ui/shadcn/table';
import { CheckWithBackground } from './check-with-background';
const DefaultPackageFeaturesRows = () => {
return (
<>
<TableRow key="digital-doctor-feedback">
<TableCell className="relative max-w-[30vw] py-6">
<Trans i18nKey="order-analysis-package:digitalDoctorFeedback" />
<InfoTooltip
content={
<Trans i18nKey="order-analysis-package:digitalDoctorFeedbackInfo" />
}
icon={
<QuestionMarkCircledIcon className="absolute top-2 right-0 size-5 sm:static sm:ml-2 sm:size-4" />
}
/>
</TableCell>
<TableCell align="center" className="py-6">
<CheckWithBackground />
</TableCell>
<TableCell align="center" className="py-6">
<CheckWithBackground />
</TableCell>
<TableCell align="center" className="py-6">
<CheckWithBackground />
</TableCell>
</TableRow>
<TableRow key="give-analyses">
<TableCell className="py-6 sm:max-w-[30vw]">
<Trans i18nKey="order-analysis-package:giveAnalyses" />
</TableCell>
<TableCell align="center" className="py-6">
<CheckWithBackground />
</TableCell>
<TableCell align="center" className="py-6">
<CheckWithBackground />
</TableCell>
<TableCell align="center" className="py-6">
<CheckWithBackground />
</TableCell>
</TableRow>
</>
);
};
export default DefaultPackageFeaturesRows;

View File

@@ -9,12 +9,12 @@ import { ShoppingCart } from 'lucide-react';
import { AppLogo } from '@kit/shared/components/app-logo';
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
import { Search } from '@kit/shared/components/ui/search';
import { Button } from '@kit/ui/button';
import { Card } from '@kit/ui/shadcn/card';
import { Trans } from '@kit/ui/trans';
import { UserNotifications } from '../_components/user-notifications';
import { getAccountBalanceSummary } from '../_lib/server/balance-actions';
import { type UserWorkspace } from '../_lib/server/load-user-workspace';
export async function HomeMenuNavigation(props: {
@@ -23,13 +23,9 @@ export async function HomeMenuNavigation(props: {
}) {
const { language } = await createI18nServerInstance();
const { workspace, user, accounts } = props.workspace;
const totalValue = props.cart?.total
? formatCurrency({
currencyCode: props.cart.currency_code,
locale: language,
value: props.cart.total,
})
: 0;
const balanceSummary = workspace?.id
? await getAccountBalanceSummary(workspace.id)
: null;
const cartQuantityTotal =
props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
@@ -47,29 +43,32 @@ export async function HomeMenuNavigation(props: {
/> */}
<div className="flex items-center justify-end gap-3">
{/* TODO: add wallet functionality
<Card className="px-6 py-2">
<span>€ {Number(0).toFixed(2).replace('.', ',')}</span>
<span>
{formatCurrency({
value: balanceSummary?.totalBalance || 0,
locale: language,
currencyCode: 'EUR',
})}
</span>
</Card>
*/}
{hasCartItems && (
<Button
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"
variant="ghost"
>
<span className="flex items-center text-nowrap">{totalValue}</span>
</Button>
)}
<Link href={pathsConfig.app.cart}>
<Button
variant="ghost"
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"
>
<ShoppingCart className="stroke-[1.5px]" />
<Trans
i18nKey="common:shoppingCartCount"
values={{ count: cartQuantityTotal }}
/>
<Trans i18nKey="common:shoppingCart" />{' '}
{hasCartItems && (
<>
(
<span className="text-success font-bold">
{cartQuantityTotal}
</span>
)
</>
)}
</Button>
</Link>
<UserNotifications userId={user.id} />

View File

@@ -25,16 +25,22 @@ import {
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import { If } from '@kit/ui/if';
import { cn } from '@kit/ui/shadcn';
import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/shadcn/avatar';
import { Button } from '@kit/ui/shadcn/button';
import { Trans } from '@kit/ui/trans';
// home imports
import type { UserWorkspace } from '../_lib/server/load-user-workspace';
import { UserNotifications } from './user-notifications';
const PERSONAL_ACCOUNT_SLUG = 'personal';
export function HomeMobileNavigation(props: {
workspace: UserWorkspace;
cart: StoreCart | null;
}) {
const user = props.workspace.user;
const { user, accounts } = props.workspace;
const signOut = useSignOut();
const { data: personalAccountData } = usePersonalAccountData(user.id);
@@ -85,10 +91,31 @@ export function HomeMobileNavigation(props: {
return (
<DropdownMenu>
<DropdownMenuTrigger>
<Menu className={'h-9'} />
</DropdownMenuTrigger>
<div className="flex justify-between gap-3">
<Link href={pathsConfig.app.cart}>
<Button
variant="ghost"
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"
>
<ShoppingCart className="stroke-[1.5px]" />
{hasCartItems && (
<>
(
<span className="text-success font-bold">
{cartQuantityTotal}
</span>
)
</>
)}
</Button>
</Link>
<UserNotifications userId={user.id} />
<DropdownMenuTrigger>
<Menu className="h-6 w-6" />
</DropdownMenuTrigger>
</div>
<DropdownMenuContent sideOffset={10} className={'w-screen rounded-none'}>
<If condition={props.cart && hasCartItems}>
<DropdownMenuGroup>
@@ -148,6 +175,46 @@ export function HomeMobileNavigation(props: {
</If>
<DropdownMenuSeparator />
<If condition={accounts.length > 0}>
<span className="text-muted-foreground px-2 text-xs">
<Trans
i18nKey={'teams:yourTeams'}
values={{ teamsCount: accounts.length }}
/>
</span>
{accounts.map((account) => (
<DropdownMenuItem key={account.value} asChild>
<Link
className={'s-full flex cursor-pointer items-center space-x-2'}
href={`${pathsConfig.app.home}/${account.value}`}
>
<div className={'flex items-center'}>
<Avatar className={'h-5 w-5 rounded-xs ' + account.image}>
<AvatarImage
{...(account.image && { src: account.image })}
/>
<AvatarFallback
className={cn('rounded-md', {
['bg-background']:
PERSONAL_ACCOUNT_SLUG === account.value,
['group-hover:bg-background']:
PERSONAL_ACCOUNT_SLUG !== account.value,
})}
>
{account.label ? account.label[0] : ''}
</AvatarFallback>
</Avatar>
<span className={'pl-3'}>{account.label}</span>
</div>
</Link>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
</If>
<SignOutDropdownItem onSignOut={() => signOut.mutateAsync()} />
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -5,7 +5,7 @@ import Link from 'next/link';
import { cn } from '@/lib/utils';
import { pathsConfig } from '@/packages/shared/src/config';
import { ComponentInstanceIcon } from '@radix-ui/react-icons';
import { ChevronRight, HeartPulse } from 'lucide-react';
import { ChevronRight } from 'lucide-react';
import { Button } from '@kit/ui/button';
import {

View File

@@ -51,7 +51,7 @@ export default function OrderBlock({
</Link>
</div>
)}
<div className="flex flex-col gap-4">
<div className="flex flex-col sm:gap-4">
{analysisOrder && (
<OrderItemsTable
items={itemsAnalysisPackage}
@@ -61,6 +61,7 @@ export default function OrderBlock({
id: analysisOrder.id,
status: analysisOrder.status,
}}
isPackage
/>
)}
{itemsTtoService && (
@@ -82,6 +83,8 @@ export default function OrderBlock({
items={itemsOther}
title="orders:table.otherOrders"
order={{
medusaOrderId: analysisOrder?.medusa_order_id,
id: analysisOrder?.id,
status: analysisOrder?.status,
}}
/>

View File

@@ -23,6 +23,7 @@ import { Trans } from '@kit/ui/trans';
import type { Order } from '~/lib/types/order';
import { cancelTtoBooking } from '../../_lib/server/actions';
import MobileTableRow from '../cart/mobile-table-row';
import { logAnalysisResultsNavigateAction } from './actions';
export type OrderItemType = 'analysisOrder' | 'ttoService';
@@ -32,11 +33,13 @@ export default function OrderItemsTable({
title,
order,
type = 'analysisOrder',
isPackage = false,
}: {
items: StoreOrderLineItem[];
title: string;
order: Order;
type?: OrderItemType;
isPackage?: boolean;
}) {
const router = useRouter();
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
@@ -63,52 +66,111 @@ export default function OrderItemsTable({
};
return (
<Table className="border-separate rounded-lg border">
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
<TableRow>
<TableHead className="px-6">
<Trans i18nKey={title} />
</TableHead>
<TableHead className="px-6">
<Trans i18nKey="orders:table.createdAt" />
</TableHead>
{order.location && (
<TableHead className="px-6">
<Trans i18nKey="orders:table.location" />
</TableHead>
)}
<TableHead className="px-6">
<Trans i18nKey="orders:table.status" />
</TableHead>
{isAnalysisOrder && <TableHead className="px-6"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{items
.sort((a, b) =>
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
)
.map((orderItem) => (
<TableRow className="w-full" key={orderItem.id}>
<TableCell className="w-[100%] px-6 text-left">
<p className="txt-medium-plus text-ui-fg-base">
{orderItem.product_title}
</p>
</TableCell>
<TableCell className="px-6 whitespace-nowrap">
{formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')}
</TableCell>
{order.location && (
<TableCell className="min-w-[180px] px-6">
{order.location}
</TableCell>
)}
<TableCell className="min-w-[180px] px-6">
<Trans
i18nKey={`orders:status.${type}.${order?.status ?? 'CONFIRMED'}`}
<>
<Table className="border-separate rounded-lg border p-2 sm:hidden">
<TableBody>
{items
.sort((a, b) =>
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
)
.map((orderItem) => (
<div key={`${orderItem.id}-mobile`}>
<MobileTableRow
titleKey={title}
value={orderItem.product_title || ''}
/>
</TableCell>
<MobileTableRow
titleKey="orders:table.createdAt"
value={formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')}
/>
{order.location && (
<MobileTableRow
titleKey="orders:table.location"
value={order.location}
/>
)}
<MobileTableRow
titleKey="orders:table.status"
value={
isPackage
? `orders:status.analysisPackageOrder.${order?.status ?? 'CONFIRMED'}`
: `orders:status.${type}.${order?.status ?? 'CONFIRMED'}`
}
/>
<TableRow>
<TableCell />
<TableCell className="flex w-full items-center justify-end p-0 pt-2">
<Button size="sm" onClick={openDetailedView}>
<Trans i18nKey="analysis-results:view" />
</Button>
{isTtoservice && order.bookingCode && (
<Button
size="sm"
className="bg-warning/90 hover:bg-warning"
onClick={() => setIsConfirmOpen(true)}
>
<Trans i18nKey="analysis-results:cancel" />
</Button>
)}
</TableCell>
</TableRow>
</div>
))}
</TableBody>
</Table>
<Table className="hidden border-separate rounded-lg border sm:block">
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
<TableRow>
<TableHead className="px-6">
<Trans i18nKey={title} />
</TableHead>
<TableHead className="px-6">
<Trans i18nKey="orders:table.createdAt" />
</TableHead>
{order.location && (
<TableHead className="px-6">
<Trans i18nKey="orders:table.location" />
</TableHead>
)}
<TableHead className="px-6">
<Trans i18nKey="orders:table.status" />
</TableHead>
{isAnalysisOrder && <TableHead className="px-6"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{items
.sort((a, b) =>
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
)
.map((orderItem) => (
<TableRow className="w-full" key={orderItem.id}>
<TableCell className="w-[100%] px-6 text-left">
<p className="txt-medium-plus text-ui-fg-base">
{orderItem.product_title}
</p>
</TableCell>
<TableCell className="px-6 whitespace-nowrap">
{formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')}
</TableCell>
{order.location && (
<TableCell className="min-w-[180px] px-6">
{order.location}
</TableCell>
)}
<TableCell className="min-w-[180px] px-6">
{isPackage ? (
<Trans
i18nKey={`orders:status.analysisPackageOrder.${order?.status ?? 'CONFIRMED'}`}
/>
) : (
<Trans
i18nKey={`orders:status.${type}.${order?.status ?? 'CONFIRMED'}`}
/>
)}
</TableCell>
<TableCell className="px-6 text-right">
<Button size="sm" onClick={openDetailedView}>
@@ -144,6 +206,6 @@ export default function OrderItemsTable({
descriptionKey="orders:confirmBookingCancel.description"
/>
)}
</Table>
</>
);
}

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { InfoTooltip } from '@/packages/shared/src/components/ui/info-tooltip';
import { HeartPulse } from 'lucide-react';
import { Button } from '@kit/ui/shadcn/button';
import {

View File

@@ -1,8 +1,6 @@
'use server';
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
import { createNotificationsApi } from '@/packages/features/notifications/src/server/api';
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
import { listProductTypes } from '@lib/data';
import { initiateMultiPaymentSession, placeOrder } from '@lib/data/cart';
import type { StoreCart, StoreOrder } from '@medusajs/types';
@@ -346,7 +344,6 @@ const sendEmail = async ({
partnerLocationName: string;
language: string;
}) => {
const client = getSupabaseServerAdminClient();
try {
const { renderSynlabAnalysisPackageEmail } = await import(
'@kit/email-templates'
@@ -372,10 +369,6 @@ const sendEmail = async ({
.catch((error) => {
throw new Error(`Failed to send email, message=${error}`);
});
await createNotificationsApi(client).createNotification({
account_id: account.id,
body: html,
});
} catch (error) {
throw new Error(`Failed to send email, message=${error}`);
}

View File

@@ -5,7 +5,7 @@ export const isValidOpenAiEnv = async () => {
const client = new OpenAI();
await client.models.list();
return true;
} catch (e) {
} catch {
return false;
}
};

View File

@@ -1,9 +1,6 @@
import { useMemo } from 'react';
import {
getTeamAccountSidebarConfig,
pathsConfig,
} from '@/packages/shared/src/config';
import { pathsConfig } from '@/packages/shared/src/config';
import { AppLogo } from '@kit/shared/components/app-logo';
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
@@ -28,25 +25,6 @@ export function TeamAccountNavigationMenu(props: {
[rawAccounts],
);
const routes = getTeamAccountSidebarConfig(account.slug).routes.reduce<
Array<{
path: string;
label: string;
Icon?: React.ReactNode;
end?: boolean | ((path: string) => boolean);
}>
>((acc, item) => {
if ('children' in item) {
return [...acc, ...item.children];
}
if ('divider' in item) {
return acc;
}
return [...acc, item];
}, []);
return (
<div className={'flex w-full flex-1 justify-between'}>
<div className={'flex items-center space-x-8'}>

View File

@@ -1,6 +1,6 @@
'use client';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { redirect } from 'next/navigation';
@@ -43,11 +43,13 @@ export default function TeamAccountStatistics({
accountBenefitStatistics,
expensesOverview,
}: TeamAccountStatisticsProps) {
const currentDate = new Date();
const [date, setDate] = useState<DateRange | undefined>({
from: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1),
to: new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0),
});
const date = useMemo<DateRange | undefined>(() => {
const currentDate = new Date();
return {
from: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1),
to: new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0),
};
}, []);
const {
i18n: { language },
} = useTranslation();

View File

@@ -29,10 +29,13 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
const account = await api.getTeamAccount(accountSlug);
const { members } = await api.getMembers(accountSlug);
const eligibleMembersCount = members.filter(
({ is_eligible_for_benefits }) => !!is_eligible_for_benefits,
).length;
const [expensesOverview, companyParams] = await Promise.all([
loadTeamAccountBenefitExpensesOverview({
companyId: account.id,
employeeCount: members.length,
employeeCount: eligibleMembersCount,
}),
api.getTeamAccountParams(account.id),
]);
@@ -42,7 +45,7 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
<HealthBenefitForm
account={account}
companyParams={companyParams}
employeeCount={members.length}
employeeCount={eligibleMembersCount}
expensesOverview={expensesOverview}
/>
</PageBody>

View File

@@ -99,11 +99,7 @@ export async function loadAccountMembersBenefitsUsage(
return [];
}
return (data ?? []) as unknown as {
personal_account_id: string;
benefit_amount: number;
benefit_unused_amount: number;
}[];
return data ?? [];
}
/**

View File

@@ -52,6 +52,7 @@ async function TeamAccountMembersPage({ params }: TeamAccountMembersPageProps) {
const canManageRoles = account.permissions.includes('roles.manage');
const canManageInvitations = account.permissions.includes('invites.manage');
const canUpdateBenefit = account.permissions.includes('benefit.manage');
const isPrimaryOwner = account.primary_owner_user_id === user.id;
const currentUserRoleHierarchy = account.role_hierarchy_level;
@@ -103,6 +104,7 @@ async function TeamAccountMembersPage({ params }: TeamAccountMembersPageProps) {
members={members}
isPrimaryOwner={isPrimaryOwner}
canManageRoles={canManageRoles}
canUpdateBenefit={canUpdateBenefit}
membersBenefitsUsage={membersBenefitsUsage}
/>
</CardContent>