Merge branch 'develop' into MED-97

This commit is contained in:
2025-09-26 17:01:24 +03:00
86 changed files with 11249 additions and 3151 deletions

View File

@@ -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 (
<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,
}: {
item: EnrichedCartItem;
currencyCode: string;
isUnavailable?: boolean;
}) {
const [editingItem, setEditingItem] = useState<EnrichedCartItem | null>(null);
const {
i18n: { language },
} = useTranslation();
return (
<>
<TableRow className="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"
data-testid="product-title"
>
{item.product_title}
</p>
</TableCell>
<TableCell className="px-4 sm:px-6">
{formatDateAndTime(item.reservation.startTime.toString())}
</TableCell>
<TableCell className="px-4 sm:px-6">
{item.reservation.location?.address ?? '-'}
</TableCell>
<TableCell className="px-4 sm:px-6">{item.quantity}</TableCell>
<TableCell className="min-w-[80px] px-4 sm:px-6">
{formatCurrency({
value: item.unit_price,
currencyCode,
locale: language,
})}
</TableCell>
<TableCell className="min-w-[80px] px-4 text-right sm:px-6">
{formatCurrency({
value: item.total,
currencyCode,
locale: language,
})}
</TableCell>
<TableCell className="px-4 text-right sm:px-6">
<span className="flex justify-end gap-x-1">
<Button size="sm" onClick={() => setEditingItem(item)}>
<Trans i18nKey="common:change" />
</Button>
</span>
</TableCell>
<TableCell className="px-4 text-right sm:px-6">
<span className="flex w-[60px] justify-end gap-x-1">
<CartItemDelete id={item.id} />
</span>
</TableCell>
</TableRow>
{isUnavailable && (
<TableRow>
<TableCell
colSpan={8}
className="text-destructive px-4 text-left sm:px-6"
>
<Trans i18nKey="booking:timeSlotUnavailable" />
</TableCell>
</TableRow>
)}
<EditCartServiceItemModal
item={editingItem}
onComplete={() => setEditingItem(null)}
/>
</>
);
}

View File

@@ -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 (
<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>
{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)}
/>
))}
</TableBody>
</Table>
);
}

View File

@@ -13,10 +13,12 @@ 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 { initiatePayment } from '../../_lib/server/cart-actions';
import { useRouter } from 'next/navigation';
import { AccountBalanceSummary } from '@kit/accounts/services/account-balance.service';
import { EnrichedCartItem } from './types';
const IS_DISCOUNT_SHOWN = true as boolean;
@@ -30,7 +32,7 @@ export default function Cart({
accountId: string;
cart: StoreCart | null;
synlabAnalyses: StoreCartLineItem[];
ttoServiceItems: StoreCartLineItem[];
ttoServiceItems: EnrichedCartItem[];
balanceSummary: AccountBalanceSummary | null;
}) {
const {
@@ -39,6 +41,9 @@ export default function Cart({
const [isInitiatingSession, setIsInitiatingSession] = useState(false);
const router = useRouter();
const [unavailableLineItemIds, setUnavailableLineItemIds] =
useState<string[]>();
const items = cart?.items ?? [];
const hasCartItems = cart && Array.isArray(items) && items.length > 0;
@@ -66,12 +71,15 @@ export default function Cart({
setIsInitiatingSession(true);
try {
const { url, isFullyPaidByBenefits, orderId } = await initiatePayment({
const { url, isFullyPaidByBenefits, orderId, unavailableLineItemIds } = await initiatePayment({
accountId,
balanceSummary: balanceSummary!,
cart: cart!,
language,
});
if (unavailableLineItemIds) {
setUnavailableLineItemIds(unavailableLineItemIds);
}
if (url) {
window.location.href = url;
} else if (isFullyPaidByBenefits) {
@@ -99,10 +107,11 @@ export default function Cart({
items={synlabAnalyses}
productColumnLabelKey="cart:items.synlabAnalyses.productColumnLabel"
/>
<CartItems
<CartServiceItems
cart={cart}
items={ttoServiceItems}
productColumnLabelKey="cart:items.ttoServices.productColumnLabel"
unavailableLineItemIds={unavailableLineItemIds}
/>
</div>
{hasCartItems && (
@@ -202,6 +211,10 @@ export default function Cart({
cart={{ ...cart }}
synlabAnalyses={synlabAnalyses}
/>
<AnalysisLocation
cart={{ ...cart }}
synlabAnalyses={synlabAnalyses}
/>
</CardContent>
</Card>
)}

View File

@@ -1,3 +1,6 @@
import { StoreCartLineItem } from "@medusajs/types";
import { Reservation } from "~/lib/types/reservation";
export interface MontonioOrderToken {
uuid: string;
accessKey: string;
@@ -10,6 +13,12 @@ export interface MontonioOrderToken {
| 'PENDING'
| 'EXPIRED'
| 'REFUNDED';
| 'PAID'
| 'FAILED'
| 'CANCELLED'
| 'PENDING'
| 'EXPIRED'
| 'REFUNDED';
paymentMethod: string;
grandTotal: number;
currency: string;
@@ -20,3 +29,10 @@ export interface MontonioOrderToken {
iat: number;
exp: number;
}
export enum CartItemType {
analysisOrders = 'analysisOrders',
ttoServices = 'ttoServices',
}
export type EnrichedCartItem = StoreCartLineItem & { reservation: Reservation };