Merge branch 'develop' of https://github.com/MR-medreport/MRB2B into MED-103

This commit is contained in:
Helena
2025-09-11 10:09:37 +03:00
164 changed files with 3059 additions and 1158 deletions

View File

@@ -55,11 +55,15 @@ export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: Store
}
return (
<div className="w-full bg-white flex flex-col txt-medium gap-y-2">
<div className="w-full h-full bg-white flex flex-col txt-medium gap-y-4">
<p className="text-sm text-muted-foreground">
<Trans i18nKey={'cart:locations.description'} />
</p>
<Form {...form}>
<form
onSubmit={form.handleSubmit((data) => onSubmit(data))}
className="w-full mb-2 flex gap-x-2"
className="w-full mb-2 flex gap-x-2 flex-1"
>
<Select
value={form.watch('locationId')}
@@ -106,11 +110,6 @@ export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: Store
</p>
</div>
)}
<p className="text-sm text-muted-foreground">
<Trans i18nKey={'cart:locations.description'} />
</p>
</div>
)
}

View File

@@ -17,7 +17,7 @@ export default function CartItem({ item, currencyCode }: {
return (
<TableRow className="w-full" data-testid="product-row">
<TableCell className="text-left w-[100%] px-6">
<TableCell className="text-left w-[100%] px-4 sm:px-6">
<p
className="txt-medium-plus text-ui-fg-base"
data-testid="product-title"
@@ -26,11 +26,11 @@ export default function CartItem({ item, currencyCode }: {
</p>
</TableCell>
<TableCell className="px-6">
<TableCell className="px-4 sm:px-6">
{item.quantity}
</TableCell>
<TableCell className="min-w-[80px] px-6">
<TableCell className="min-w-[80px] px-4 sm:px-6">
{formatCurrency({
value: item.unit_price,
currencyCode,
@@ -38,7 +38,7 @@ export default function CartItem({ item, currencyCode }: {
})}
</TableCell>
<TableCell className="min-w-[80px] px-6">
<TableCell className="min-w-[80px] px-4 sm:px-6 text-right">
{formatCurrency({
value: item.total,
currencyCode,
@@ -46,7 +46,7 @@ export default function CartItem({ item, currencyCode }: {
})}
</TableCell>
<TableCell className="text-right px-6">
<TableCell className="text-right px-4 sm:px-6">
<span className="flex gap-x-1 justify-end w-[60px]">
<CartItemDelete id={item.id} />
</span>

View File

@@ -22,19 +22,19 @@ export default function CartItems({ cart, items, productColumnLabelKey }: {
<Table className="rounded-lg border border-separate">
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
<TableRow>
<TableHead className="px-6">
<TableHead className="px-4 sm:px-6">
<Trans i18nKey={productColumnLabelKey} />
</TableHead>
<TableHead className="px-6">
<TableHead className="px-4 sm:px-6">
<Trans i18nKey="cart:table.quantity" />
</TableHead>
<TableHead className="px-6 min-w-[100px]">
<TableHead className="px-4 sm:px-6 min-w-[100px]">
<Trans i18nKey="cart:table.price" />
</TableHead>
<TableHead className="px-6 min-w-[100px]">
<TableHead className="px-4 sm:px-6 min-w-[100px] text-right">
<Trans i18nKey="cart:table.total" />
</TableHead>
<TableHead className="px-6">
<TableHead className="px-4 sm:px-6">
</TableHead>
</TableRow>
</TableHeader>

View File

@@ -0,0 +1,24 @@
"use server"
import { applyPromotions } from "@lib/data/cart"
export async function addPromotionCodeAction(code: string) {
try {
await applyPromotions([code]);
return { success: true, message: 'Discount code applied successfully' };
} catch (error) {
console.error('Error applying promotion code:', error);
return { success: false, message: 'Failed to apply discount code' };
}
}
export async function removePromotionCodeAction(codeToRemove: string, appliedCodes: string[]) {
try {
const updatedCodes = appliedCodes.filter((appliedCode) => appliedCode !== codeToRemove);
await applyPromotions(updatedCodes);
return { success: true, message: 'Discount code removed successfully' };
} catch (error) {
console.error('Error removing promotion code:', error);
return { success: false, message: 'Failed to remove discount code' };
}
}

View File

@@ -2,9 +2,8 @@
import { Badge, Text } from "@medusajs/ui"
import { toast } from '@kit/ui/sonner';
import React, { useActionState } from "react";
import React from "react";
import { applyPromotions, submitPromotionForm } from "@lib/data/cart"
import { convertToLocale } from "@lib/util/money"
import { StoreCart, StorePromotion } from "@medusajs/types"
import Trash from "@modules/common/icons/trash"
@@ -16,6 +15,7 @@ import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { addPromotionCodeAction, removePromotionCodeAction } from "./discount-code-actions";
const DiscountCodeSchema = z.object({
code: z.string().min(1),
@@ -31,42 +31,35 @@ export default function DiscountCode({ cart }: {
const { promotions = [] } = cart;
const removePromotionCode = async (code: string) => {
const validPromotions = promotions.filter(
(promotion) => promotion.code !== code,
)
const appliedCodes = promotions
.filter((p) => p.code !== undefined)
.map((p) => p.code!)
await applyPromotions(
validPromotions.filter((p) => p.code === undefined).map((p) => p.code!),
{
onSuccess: () => {
toast.success(t('cart:discountCode.removeSuccess'));
},
onError: () => {
toast.error(t('cart:discountCode.removeError'));
},
}
)
const loading = toast.loading(t('cart:discountCode.removeLoading'));
const result = await removePromotionCodeAction(code, appliedCodes)
toast.dismiss(loading);
if (result.success) {
toast.success(t('cart:discountCode.removeSuccess'));
} else {
toast.error(t('cart:discountCode.removeError'));
}
}
const addPromotionCode = async (code: string) => {
const codes = promotions
.filter((p) => p.code === undefined)
.map((p) => p.code!)
codes.push(code.toString())
const loading = toast.loading(t('cart:discountCode.addLoading'));
const result = await addPromotionCodeAction(code)
await applyPromotions(codes, {
onSuccess: () => {
toast.success(t('cart:discountCode.addSuccess'));
},
onError: () => {
toast.error(t('cart:discountCode.addError'));
},
});
form.reset()
toast.dismiss(loading);
if (result.success) {
toast.success(t('cart:discountCode.addSuccess'));
form.reset()
} else {
toast.error(t('cart:discountCode.addError'));
}
}
const [message, formAction] = useActionState(submitPromotionForm, null)
const form = useForm<z.infer<typeof DiscountCodeSchema>>({
defaultValues: {
@@ -76,11 +69,15 @@ export default function DiscountCode({ cart }: {
});
return (
<div className="w-full bg-white flex flex-col txt-medium">
<div className="w-full h-full bg-white flex flex-col txt-medium gap-y-4">
<p className="text-sm text-muted-foreground">
<Trans i18nKey={'cart:discountCode.subtitle'} />
</p>
<Form {...form}>
<form
onSubmit={form.handleSubmit((data) => addPromotionCode(data.code))}
className="w-full mb-2 flex gap-x-2 sm:flex-row flex-col gap-y-2"
className="w-full mb-2 flex gap-x-2 sm:flex-row flex-col gap-y-2 flex-1"
>
<FormField
name={'code'}
@@ -96,14 +93,14 @@ export default function DiscountCode({ cart }: {
<Button
type="submit"
variant="secondary"
className="h-full"
className="h-min"
>
<Trans i18nKey={'cart:discountCode.apply'} />
</Button>
</form>
</Form>
{promotions.length > 0 ? (
{promotions.length > 0 && (
<div className="w-full flex items-center mt-4">
<div className="flex flex-col w-full gap-y-2">
<p>
@@ -117,12 +114,12 @@ export default function DiscountCode({ cart }: {
className="flex items-center justify-between w-full max-w-full mb-2"
data-testid="discount-row"
>
<Text className="flex gap-x-1 items-baseline txt-small-plus w-4/5 pr-1">
<Text className="flex gap-x-1 items-baseline text-sm w-4/5 pr-1">
<span className="truncate" data-testid="discount-code">
<Badge
color={promotion.is_automatic ? "green" : "grey"}
size="small"
className="px-4"
className="px-4 text-sm"
>
{promotion.code}
</Badge>{" "}
@@ -135,7 +132,7 @@ export default function DiscountCode({ cart }: {
"percentage"
? `${promotion.application_method.value}%`
: convertToLocale({
amount: promotion.application_method.value,
amount: Number(promotion.application_method.value),
currency_code:
promotion.application_method
.currency_code,
@@ -173,10 +170,6 @@ export default function DiscountCode({ cart }: {
})}
</div>
</div>
) : (
<p className="text-sm text-muted-foreground">
<Trans i18nKey={'cart:discountCode.subtitle'} />
</p>
)}
</div>
)

View File

@@ -78,14 +78,14 @@ export default function Cart({
</div>
{hasCartItems && (
<>
<div className="flex justify-end gap-x-4 px-6 pt-4">
<div className="mr-[36px]">
<div className="flex sm:justify-end gap-x-4 px-4 sm:px-6 pt-2 sm:pt-4">
<div className="w-full sm:w-auto sm:mr-[42px]">
<p className="ml-0 font-bold text-sm text-muted-foreground">
<Trans i18nKey="cart:subtotal" />
<Trans i18nKey="cart:order.subtotal" />
</p>
</div>
<div className="mr-[116px]">
<p className="text-sm">
<div className={`sm:mr-[112px] sm:w-[50px]`}>
<p className="text-sm text-right">
{formatCurrency({
value: cart.subtotal,
currencyCode: cart.currency_code,
@@ -94,14 +94,14 @@ export default function Cart({
</p>
</div>
</div>
<div className="flex justify-end gap-x-4 px-6 py-2">
<div className="mr-[36px]">
<div className="flex sm:justify-end gap-x-4 px-4 sm:px-6 py-2 sm:py-4">
<div className="w-full sm:w-auto sm:mr-[42px]">
<p className="ml-0 font-bold text-sm text-muted-foreground">
<Trans i18nKey="cart:promotionsTotal" />
<Trans i18nKey="cart:order.promotionsTotal" />
</p>
</div>
<div className="mr-[116px]">
<p className="text-sm">
<div className={`sm:mr-[112px] sm:w-[50px]`}>
<p className="text-sm text-right">
{formatCurrency({
value: cart.discount_total,
currencyCode: cart.currency_code,
@@ -110,14 +110,14 @@ export default function Cart({
</p>
</div>
</div>
<div className="flex justify-end gap-x-4 px-6">
<div className="mr-[36px]">
<div className="flex sm:justify-end gap-x-4 px-4 sm:px-6">
<div className="w-full sm:w-auto sm:mr-[42px]">
<p className="ml-0 font-bold text-sm">
<Trans i18nKey="cart:total" />
<Trans i18nKey="cart:order.total" />
</p>
</div>
<div className="mr-[116px]">
<p className="text-sm">
<div className={`sm:mr-[112px] sm:w-[50px]`}>
<p className="text-sm text-right">
{formatCurrency({
value: cart.total,
currencyCode: cart.currency_code,
@@ -129,7 +129,7 @@ export default function Cart({
</>
)}
<div className="flex sm:flex-row flex-col gap-y-6 py-8 gap-x-4">
<div className="flex sm:flex-row flex-col gap-y-6 py-4 sm:py-8 gap-x-4">
{IS_DISCOUNT_SHOWN && (
<Card
className="flex flex-col justify-between w-full sm:w-1/2"
@@ -139,7 +139,7 @@ export default function Cart({
<Trans i18nKey="cart:discountCode.title" />
</h5>
</CardHeader>
<CardContent>
<CardContent className="h-full">
<DiscountCode cart={{ ...cart }} />
</CardContent>
</Card>
@@ -154,7 +154,7 @@ export default function Cart({
<Trans i18nKey="cart:locations.title" />
</h5>
</CardHeader>
<CardContent>
<CardContent className="h-full">
<AnalysisLocation cart={{ ...cart }} synlabAnalyses={synlabAnalyses} />
</CardContent>
</Card>

View File

@@ -128,7 +128,7 @@ const ComparePackagesModal = async ({
return (
<TableRow key={id}>
<TableCell className="py-6">
<TableCell className="py-6 sm:max-w-[30vw]">
{title}{' '}
{description && (<InfoTooltip content={description} icon={<QuestionMarkCircledIcon />} />)}
</TableCell>
@@ -136,10 +136,10 @@ const ComparePackagesModal = async ({
{isIncludedInStandard && <CheckWithBackground />}
</TableCell>
<TableCell align="center" className="py-6">
{(isIncludedInStandard || isIncludedInStandardPlus) && <CheckWithBackground />}
{isIncludedInStandardPlus && <CheckWithBackground />}
</TableCell>
<TableCell align="center" className="py-6">
{(isIncludedInStandard || isIncludedInStandardPlus || isIncludedInPremium) && <CheckWithBackground />}
{isIncludedInPremium && <CheckWithBackground />}
</TableCell>
</TableRow>
);

View File

@@ -8,7 +8,7 @@ import { Trans } from '@kit/ui/trans';
export default function DashboardCards() {
return (
<div className="flex gap-4 lg:px-4">
<div className="flex gap-4">
<Card
variant="gradient-success"
className="xs:w-1/2 sm:w-auto flex w-full flex-col justify-between"

View File

@@ -16,7 +16,6 @@ import {
} from 'lucide-react';
import { pathsConfig } from '@kit/shared/config';
import { getPersonParameters } from '@kit/shared/utils';
import { Button } from '@kit/ui/button';
import {
Card,
@@ -30,7 +29,7 @@ import { cn } from '@kit/ui/utils';
import { isNil } from 'lodash';
import { BmiCategory } from '~/lib/types/bmi';
import {
import PersonalCode, {
bmiFromMetric,
getBmiBackgroundColor,
getBmiStatus,
@@ -60,7 +59,7 @@ const cards = ({
}) => [
{
title: 'dashboard:gender',
description: gender ?? 'dashboard:male',
description: gender ?? '-',
icon: <User />,
iconBg: 'bg-success',
},
@@ -84,7 +83,7 @@ const cards = ({
},
{
title: 'dashboard:bmi',
description: bmiFromMetric(weight || 0, height || 0).toString(),
description: bmiFromMetric(weight || 0, height || 0)?.toString() ?? '-',
icon: <TrendingUp />,
iconBg: getBmiBackgroundColor(bmiStatus),
},
@@ -145,21 +144,26 @@ export default function Dashboard({
'id'
>[];
}) {
const params = getPersonParameters(account.personal_code!);
const bmiStatus = getBmiStatus(bmiThresholds, {
age: params?.age || 0,
height: account.accountParams?.height || 0,
weight: account.accountParams?.weight || 0,
});
const height = account.accountParams?.height || 0;
const weight = account.accountParams?.weight || 0;
let age: number = 0;
let gender: { label: string; value: string } | null = null;
try {
({ age = 0, gender } = PersonalCode.parsePersonalCode(account.personal_code!));
} catch (e) {
console.error("Failed to parse personal code", e);
}
const bmiStatus = getBmiStatus(bmiThresholds, { age, height, weight });
return (
<>
<div className="xs:grid-cols-2 grid auto-rows-fr gap-3 sm:grid-cols-4 lg:grid-cols-5">
{cards({
gender: params?.gender,
age: params?.age,
height: account.accountParams?.height,
weight: account.accountParams?.weight,
gender: gender?.label,
age,
height,
weight,
bmiStatus,
smoking: account.accountParams?.isSmoker,
}).map(

View File

@@ -21,7 +21,6 @@ import { formatCurrency } from '@/packages/shared/src/utils';
export type OrderAnalysisCard = Pick<
StoreProduct, 'title' | 'description' | 'subtitle'
> & {
isAvailable: boolean;
variant: { id: string };
price: number | null;
};
@@ -58,13 +57,12 @@ export default function OrderAnalysesCards({
}
return (
<div className="grid 2xs:grid-cols-3 gap-6 mt-4">
<div className="grid xs:grid-cols-3 gap-6 mt-4">
{analyses.map(({
title,
variant,
description,
subtitle,
isAvailable,
price,
}) => {
const formattedPrice = typeof price === 'number'
@@ -77,7 +75,7 @@ export default function OrderAnalysesCards({
return (
<Card
key={title}
variant={isAvailable ? "gradient-success" : "gradient-warning"}
variant="gradient-success"
className="flex flex-col justify-between"
>
<CardHeader className="flex-row">
@@ -86,46 +84,44 @@ export default function OrderAnalysesCards({
>
<HeartPulse className="size-4 fill-green-500" />
</div>
{isAvailable && (
<div className='ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-warning'>
<Button
size="icon"
variant="outline"
className="px-2 text-black"
onClick={() => handleSelect(variant.id)}
>
{variantAddingToCart ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
</Button>
</div>
)}
<div className='ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-warning'>
<Button
size="icon"
variant="outline"
className="px-2 text-black"
onClick={() => handleSelect(variant.id)}
>
{variantAddingToCart === variant.id ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
</Button>
</div>
</CardHeader>
<CardFooter className="flex flex-col items-start gap-2">
<h5>
{title}
{description && (
<>
{' '}
<InfoTooltip
content={
<div className='flex flex-col gap-2'>
<span>{formattedPrice}</span>
<span>{description}</span>
</div>
}
/>
</>
<CardFooter className="flex gap-2">
<div className="flex flex-col items-start gap-2 flex-1">
<h5>
{title}
{description && (
<>
{' '}
<InfoTooltip
content={
<div className='flex flex-col gap-2'>
<span>{formattedPrice}</span>
<span>{description}</span>
</div>
}
/>
</>
)}
</h5>
{subtitle && (
<CardDescription>
{subtitle}
</CardDescription>
)}
</h5>
{isAvailable && subtitle && (
<CardDescription>
{subtitle}
</CardDescription>
)}
{!isAvailable && (
<CardDescription>
<Trans i18nKey={'order-analysis:analysisNotAvailable'} />
</CardDescription>
)}
</div>
<div className="flex flex-col items-end gap-2 self-end text-sm">
<span>{formattedPrice}</span>
</div>
</CardFooter>
</Card>
);

View File

@@ -24,7 +24,7 @@ export default function CartTotals({ medusaOrder }: {
<div className="flex flex-col gap-y-2 txt-medium text-ui-fg-subtle ">
<div className="flex items-center justify-between">
<span className="flex gap-x-1 items-center">
<Trans i18nKey="cart:orderConfirmed.subtotal" />
<Trans i18nKey="cart:order.subtotal" />
</span>
<span data-testid="cart-subtotal" data-value={subtotal || 0}>
{formatCurrency({ value: subtotal ?? 0, currencyCode: currency_code, locale: language })}
@@ -32,7 +32,7 @@ export default function CartTotals({ medusaOrder }: {
</div>
{!!discount_total && (
<div className="flex items-center justify-between">
<span><Trans i18nKey="cart:orderConfirmed.discount" /></span>
<span><Trans i18nKey="cart:order.promotionsTotal" /></span>
<span
className="text-ui-fg-interactive"
data-testid="cart-discount"
@@ -43,17 +43,17 @@ export default function CartTotals({ medusaOrder }: {
</span>
</div>
)}
<div className="flex justify-between">
{/* <div className="flex justify-between">
<span className="flex gap-x-1 items-center ">
<Trans i18nKey="cart:orderConfirmed.taxes" />
</span>
<span data-testid="cart-taxes" data-value={tax_total || 0}>
{formatCurrency({ value: tax_total ?? 0, currencyCode: currency_code, locale: language })}
</span>
</div>
</div> */}
{!!gift_card_total && (
<div className="flex items-center justify-between">
<span><Trans i18nKey="cart:orderConfirmed.giftCard" /></span>
<span><Trans i18nKey="cart:order.giftCard" /></span>
<span
className="text-ui-fg-interactive"
data-testid="cart-gift-card-amount"
@@ -67,7 +67,7 @@ export default function CartTotals({ medusaOrder }: {
</div>
<div className="h-px w-full border-b border-gray-200 my-4" />
<div className="flex items-center justify-between text-ui-fg-base mb-2 txt-medium ">
<span className="font-bold"><Trans i18nKey="cart:orderConfirmed.total" /></span>
<span className="font-bold"><Trans i18nKey="cart:order.total" /></span>
<span
className="txt-xlarge-plus"
data-testid="cart-total"

View File

@@ -7,15 +7,23 @@ export default function OrderDetails({ order }: {
}) {
return (
<div className="flex flex-col gap-y-2">
<span>
<Trans i18nKey="cart:orderConfirmed.orderDate" />:{" "}
<div>
<span className="font-bold">
<Trans i18nKey="cart:orderConfirmed.orderNumber" />:{" "}
</span>
<span>
{order.medusa_order_id}
</span>
</div>
<div>
<span className="font-bold">
<Trans i18nKey="cart:orderConfirmed.orderDate" />:{" "}
</span>
<span>
{formatDate(order.created_at, 'dd.MM.yyyy HH:mm')}
</span>
</span>
<span className="text-ui-fg-interactive">
<Trans i18nKey="cart:orderConfirmed.orderNumber" />: <span data-testid="order-id">{order.medusa_order_id}</span>
</span>
</div>
</div>
)
}

View File

@@ -4,7 +4,7 @@ import { createPageViewLog, PageViewAction } from "~/lib/services/audit/pageView
import { loadCurrentUserAccount } from "../../_lib/server/load-user-account";
export async function logAnalysisResultsNavigateAction(analysisOrderId: string) {
const account = await loadCurrentUserAccount();
const { account } = await loadCurrentUserAccount();
if (!account) {
throw new Error('Account not found');
}