MAIN <- develop
MAIN <- develop
This commit is contained in:
@@ -3,6 +3,9 @@ import React from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { redirect } from 'next/navigation';
|
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 { ButtonTooltip } from '@kit/shared/components/ui/button-tooltip';
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
@@ -25,7 +28,9 @@ export default async function AnalysisResultsPage({
|
|||||||
id: string;
|
id: string;
|
||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
|
const supabaseClient = getSupabaseServerClient();
|
||||||
const { id: analysisOrderId } = await params;
|
const { id: analysisOrderId } = await params;
|
||||||
|
const notificationsApi = createNotificationsApi(supabaseClient);
|
||||||
|
|
||||||
const [{ account }, analysisResponse] = await Promise.all([
|
const [{ account }, analysisResponse] = await Promise.all([
|
||||||
loadCurrentUserAccount(),
|
loadCurrentUserAccount(),
|
||||||
@@ -41,6 +46,11 @@ export default async function AnalysisResultsPage({
|
|||||||
action: PageViewAction.VIEW_ANALYSIS_RESULTS,
|
action: PageViewAction.VIEW_ANALYSIS_RESULTS,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await notificationsApi.dismissNotification(
|
||||||
|
`/home/analysis-results/${analysisOrderId}`,
|
||||||
|
'link',
|
||||||
|
);
|
||||||
|
|
||||||
if (!analysisResponse) {
|
if (!analysisResponse) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -90,6 +90,14 @@ async function OrdersPage() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
medusaOrderItemsAnalysisPackages.length === 0 &&
|
||||||
|
medusaOrderItemsOther.length === 0 &&
|
||||||
|
medusaOrderItemsTtoServices.length === 0
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={medusaOrder.id}>
|
<React.Fragment key={medusaOrder.id}>
|
||||||
<Divider className="my-6" />
|
<Divider className="my-6" />
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ const TimeSlots = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<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}
|
key={index}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { formatCurrency } from '@/packages/shared/src/utils';
|
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 { Loader2 } from 'lucide-react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -30,7 +34,9 @@ export default function CartFormContent({
|
|||||||
isInitiatingSession,
|
isInitiatingSession,
|
||||||
getBalanceSummarySelection,
|
getBalanceSummarySelection,
|
||||||
}: {
|
}: {
|
||||||
cart: StoreCart;
|
cart: StoreCart & {
|
||||||
|
promotions: StoreCartPromotion[];
|
||||||
|
};
|
||||||
synlabAnalyses: StoreCartLineItem[];
|
synlabAnalyses: StoreCartLineItem[];
|
||||||
ttoServiceItems: EnrichedCartItem[];
|
ttoServiceItems: EnrichedCartItem[];
|
||||||
unavailableLineItemIds?: string[];
|
unavailableLineItemIds?: string[];
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function CartItem({
|
|||||||
} = useTranslation();
|
} = useTranslation();
|
||||||
|
|
||||||
return (
|
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">
|
<TableCell className="w-[100%] px-4 text-left sm:px-6">
|
||||||
<p
|
<p
|
||||||
className="txt-medium-plus text-ui-fg-base"
|
className="txt-medium-plus text-ui-fg-base"
|
||||||
@@ -41,11 +41,12 @@ export default function CartItem({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="min-w-[80px] px-4 text-right sm:px-6">
|
<TableCell className="min-w-[80px] px-4 text-right sm:px-6">
|
||||||
{formatCurrency({
|
{item.total &&
|
||||||
value: item.total,
|
formatCurrency({
|
||||||
currencyCode,
|
value: item.total,
|
||||||
locale: language,
|
currencyCode,
|
||||||
})}
|
locale: language,
|
||||||
|
})}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="px-4 text-right sm:px-6">
|
<TableCell className="px-4 text-right sm:px-6">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import CartItem from './cart-item';
|
import CartItem from './cart-item';
|
||||||
|
import MobileCartItems from './mobile-cart-items';
|
||||||
|
|
||||||
export default function CartItems({
|
export default function CartItems({
|
||||||
cart,
|
cart,
|
||||||
@@ -25,37 +26,54 @@ export default function CartItems({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className="border-separate rounded-lg border">
|
<>
|
||||||
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
<Table className="hidden border-separate rounded-lg border sm:block">
|
||||||
<TableRow>
|
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
||||||
<TableHead className="px-4 sm:px-6">
|
<TableRow>
|
||||||
<Trans i18nKey={productColumnLabelKey} />
|
<TableHead className="px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey={productColumnLabelKey} />
|
||||||
<TableHead className="px-4 sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.quantity" />
|
<TableHead className="px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.quantity" />
|
||||||
<TableHead className="min-w-[100px] px-4 sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.price" />
|
<TableHead className="min-w-[100px] px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.price" />
|
||||||
<TableHead className="min-w-[100px] px-4 text-right sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.total" />
|
<TableHead className="min-w-[100px] px-4 text-right sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.total" />
|
||||||
<TableHead className="px-4 sm:px-6"></TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
<TableHead className="px-4 sm:px-6"></TableHead>
|
||||||
</TableHeader>
|
</TableRow>
|
||||||
<TableBody>
|
</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
|
{items
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
||||||
)
|
)
|
||||||
.map((item) => (
|
.map((item) => (
|
||||||
<CartItem
|
<MobileCartItems
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
currencyCode={cart.currency_code}
|
currencyCode={cart.currency_code}
|
||||||
|
productColumnLabelKey={productColumnLabelKey}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</div>
|
||||||
</Table>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +1,26 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { formatCurrency, formatDateAndTime } from '@/packages/shared/src/utils';
|
import { formatCurrency, formatDateAndTime } from '@/packages/shared/src/utils';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@kit/ui/dialog';
|
|
||||||
import { TableCell, TableRow } from '@kit/ui/table';
|
import { TableCell, TableRow } from '@kit/ui/table';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import BookingContainer from '../booking/booking-container';
|
|
||||||
import CartItemDelete from './cart-item-delete';
|
import CartItemDelete from './cart-item-delete';
|
||||||
import { EnrichedCartItem } from './types';
|
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({
|
export default function CartServiceItem({
|
||||||
item,
|
item,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
isUnavailable,
|
isUnavailable,
|
||||||
|
setEditingItem,
|
||||||
}: {
|
}: {
|
||||||
item: EnrichedCartItem;
|
item: EnrichedCartItem;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
isUnavailable?: boolean;
|
isUnavailable?: boolean;
|
||||||
|
setEditingItem: (item: EnrichedCartItem | null) => void;
|
||||||
}) {
|
}) {
|
||||||
const [editingItem, setEditingItem] = useState<EnrichedCartItem | null>(null);
|
|
||||||
const {
|
const {
|
||||||
i18n: { language },
|
i18n: { language },
|
||||||
} = useTranslation();
|
} = useTranslation();
|
||||||
@@ -106,11 +56,12 @@ export default function CartServiceItem({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="min-w-[80px] px-4 text-right sm:px-6">
|
<TableCell className="min-w-[80px] px-4 text-right sm:px-6">
|
||||||
{formatCurrency({
|
{item.total &&
|
||||||
value: item.total,
|
formatCurrency({
|
||||||
currencyCode,
|
value: item.total,
|
||||||
locale: language,
|
currencyCode,
|
||||||
})}
|
locale: language,
|
||||||
|
})}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="px-4 text-right sm:px-6">
|
<TableCell className="px-4 text-right sm:px-6">
|
||||||
@@ -137,10 +88,6 @@ export default function CartServiceItem({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
<EditCartServiceItemModal
|
|
||||||
item={editingItem}
|
|
||||||
onComplete={() => setEditingItem(null)}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { StoreCart } from '@medusajs/types';
|
import { StoreCart } from '@medusajs/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@kit/ui/shadcn/dialog';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -9,9 +18,52 @@ import {
|
|||||||
} from '@kit/ui/table';
|
} from '@kit/ui/table';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import BookingContainer from '../booking/booking-container';
|
||||||
import CartServiceItem from './cart-service-item';
|
import CartServiceItem from './cart-service-item';
|
||||||
|
import MobileCartServiceItems from './mobile-cart-service-items';
|
||||||
import { EnrichedCartItem } from './types';
|
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({
|
export default function CartServiceItems({
|
||||||
cart,
|
cart,
|
||||||
items,
|
items,
|
||||||
@@ -23,50 +75,75 @@ export default function CartServiceItems({
|
|||||||
productColumnLabelKey: string;
|
productColumnLabelKey: string;
|
||||||
unavailableLineItemIds?: string[];
|
unavailableLineItemIds?: string[];
|
||||||
}) {
|
}) {
|
||||||
|
const [editingItem, setEditingItem] = useState<EnrichedCartItem | null>(null);
|
||||||
if (!items || items.length === 0) {
|
if (!items || items.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className="border-separate rounded-lg border">
|
<>
|
||||||
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
<Table className="hidden border-separate rounded-lg border sm:block">
|
||||||
<TableRow>
|
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
||||||
<TableHead className="px-4 sm:px-6">
|
<TableRow>
|
||||||
<Trans i18nKey={productColumnLabelKey} />
|
<TableHead className="px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey={productColumnLabelKey} />
|
||||||
<TableHead className="px-4 sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.time" />
|
<TableHead className="px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.time" />
|
||||||
<TableHead className="px-4 sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.location" />
|
<TableHead className="px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.location" />
|
||||||
<TableHead className="px-4 sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.quantity" />
|
<TableHead className="px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.quantity" />
|
||||||
<TableHead className="min-w-[100px] px-4 sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.price" />
|
<TableHead className="min-w-[100px] px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.price" />
|
||||||
<TableHead className="min-w-[100px] px-4 text-right sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.total" />
|
<TableHead className="min-w-[100px] px-4 text-right sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.total" />
|
||||||
<TableHead className="px-4 sm:px-6"></TableHead>
|
</TableHead>
|
||||||
<TableHead className="px-4 sm:px-6"></TableHead>
|
<TableHead className="px-4 sm:px-6"></TableHead>
|
||||||
</TableRow>
|
<TableHead className="px-4 sm:px-6"></TableHead>
|
||||||
</TableHeader>
|
</TableRow>
|
||||||
<TableBody>
|
</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
|
{items
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
||||||
)
|
)
|
||||||
.map((item) => (
|
.map((item) => (
|
||||||
<CartServiceItem
|
<MobileCartServiceItems
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
currencyCode={cart.currency_code}
|
currencyCode={cart.currency_code}
|
||||||
isUnavailable={unavailableLineItemIds?.includes(item.id)}
|
isUnavailable={unavailableLineItemIds?.includes(item.id)}
|
||||||
|
productColumnLabelKey={productColumnLabelKey}
|
||||||
|
setEditingItem={setEditingItem}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</div>
|
||||||
</Table>
|
|
||||||
|
<EditCartServiceItemModal
|
||||||
|
item={editingItem}
|
||||||
|
onComplete={() => setEditingItem(null)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { convertToLocale } from '@lib/util/money';
|
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 { Badge, Text } from '@medusajs/ui';
|
||||||
import Trash from '@modules/common/icons/trash';
|
import Trash from '@modules/common/icons/trash';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
@@ -24,7 +24,7 @@ export default function DiscountCode({
|
|||||||
cart,
|
cart,
|
||||||
}: {
|
}: {
|
||||||
cart: StoreCart & {
|
cart: StoreCart & {
|
||||||
promotions: StorePromotion[];
|
promotions: StoreCartPromotion[];
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('cart');
|
const { t } = useTranslation('cart');
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { useCallback, useState } from 'react';
|
|||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { StoreCart, StoreCartLineItem } from '@medusajs/types';
|
import {
|
||||||
|
StoreCart,
|
||||||
|
StoreCartLineItem,
|
||||||
|
StoreCartPromotion,
|
||||||
|
} from '@medusajs/types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { AccountBalanceSummary } from '@kit/accounts/services/account-balance.service';
|
import { AccountBalanceSummary } from '@kit/accounts/services/account-balance.service';
|
||||||
@@ -23,7 +27,11 @@ export default function Cart({
|
|||||||
balanceSummary,
|
balanceSummary,
|
||||||
}: {
|
}: {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
cart: StoreCart | null;
|
cart:
|
||||||
|
| (StoreCart & {
|
||||||
|
promotions: StoreCartPromotion[];
|
||||||
|
})
|
||||||
|
| null;
|
||||||
synlabAnalyses: StoreCartLineItem[];
|
synlabAnalyses: StoreCartLineItem[];
|
||||||
ttoServiceItems: EnrichedCartItem[];
|
ttoServiceItems: EnrichedCartItem[];
|
||||||
balanceSummary: AccountBalanceSummary | null;
|
balanceSummary: AccountBalanceSummary | null;
|
||||||
|
|||||||
56
app/home/(user)/_components/cart/mobile-cart-items.tsx
Normal file
56
app/home/(user)/_components/cart/mobile-cart-items.tsx
Normal 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 MobileCartRow from './mobile-cart-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>
|
||||||
|
<MobileCartRow
|
||||||
|
titleKey={productColumnLabelKey}
|
||||||
|
value={item.product_title}
|
||||||
|
/>
|
||||||
|
<MobileCartRow titleKey="cart:table.time" value={item.quantity} />
|
||||||
|
<MobileCartRow
|
||||||
|
titleKey="cart:table.price"
|
||||||
|
value={formatCurrency({
|
||||||
|
value: item.unit_price,
|
||||||
|
currencyCode,
|
||||||
|
locale: language,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<MobileCartRow
|
||||||
|
titleKey="cart:table.total"
|
||||||
|
value={
|
||||||
|
item.total &&
|
||||||
|
formatCurrency({
|
||||||
|
value: item.total,
|
||||||
|
currencyCode,
|
||||||
|
locale: language,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileCartItems;
|
||||||
29
app/home/(user)/_components/cart/mobile-cart-row.tsx
Normal file
29
app/home/(user)/_components/cart/mobile-cart-row.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Trans } from '@kit/ui/makerkit/trans';
|
||||||
|
import { TableCell, TableHead, TableRow } from '@kit/ui/shadcn/table';
|
||||||
|
|
||||||
|
const MobileCartRow = ({
|
||||||
|
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"
|
||||||
|
data-testid="product-title"
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</p>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MobileCartRow;
|
||||||
@@ -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 MobileCartRow from './mobile-cart-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>
|
||||||
|
<MobileCartRow
|
||||||
|
titleKey={productColumnLabelKey}
|
||||||
|
value={item.product_title}
|
||||||
|
/>
|
||||||
|
<MobileCartRow
|
||||||
|
titleKey="cart:table.time"
|
||||||
|
value={formatDateAndTime(item.reservation.startTime.toString())}
|
||||||
|
/>
|
||||||
|
<MobileCartRow
|
||||||
|
titleKey="cart:table.location"
|
||||||
|
value={item.reservation.location?.address ?? '-'}
|
||||||
|
/>
|
||||||
|
<MobileCartRow titleKey="cart:table.quantity" value={item.quantity} />
|
||||||
|
<MobileCartRow
|
||||||
|
titleKey="cart:table.price"
|
||||||
|
value={formatCurrency({
|
||||||
|
value: item.unit_price,
|
||||||
|
currencyCode,
|
||||||
|
locale: language,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<MobileCartRow
|
||||||
|
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;
|
||||||
11
app/home/(user)/_components/check-with-background.tsx
Normal file
11
app/home/(user)/_components/check-with-background.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import { JSX } from 'react';
|
|||||||
import { StoreProduct } from '@medusajs/types';
|
import { StoreProduct } from '@medusajs/types';
|
||||||
import { QuestionMarkCircledIcon } from '@radix-ui/react-icons';
|
import { QuestionMarkCircledIcon } from '@radix-ui/react-icons';
|
||||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
|
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 { PackageHeader } from '@kit/shared/components/package-header';
|
||||||
import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
|
import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
|
||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
import { CheckWithBackground } from './check-with-background';
|
||||||
import ComparePackagesAddToCartButtons from './compare-packages-add-to-cart-buttons';
|
import ComparePackagesAddToCartButtons from './compare-packages-add-to-cart-buttons';
|
||||||
import DefaultPackageFeaturesRows from './default-package-features-rows';
|
import DefaultPackageFeaturesRows from './default-package-features-rows';
|
||||||
|
|
||||||
@@ -38,14 +39,6 @@ export type AnalysisPackageElement = Pick<
|
|||||||
isIncludedInPremium: boolean;
|
isIncludedInPremium: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export 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 ({
|
const PackageTableHead = async ({
|
||||||
product,
|
product,
|
||||||
}: {
|
}: {
|
||||||
@@ -97,7 +90,7 @@ const ComparePackagesModal = async ({
|
|||||||
<DialogContent
|
<DialogContent
|
||||||
className="min-h-screen max-w-fit min-w-screen"
|
className="min-h-screen max-w-fit min-w-screen"
|
||||||
customClose={
|
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">
|
<p className="text-sm font-medium text-black">
|
||||||
{t('common:close')}
|
{t('common:close')}
|
||||||
</p>
|
</p>
|
||||||
@@ -111,8 +104,10 @@ const ComparePackagesModal = async ({
|
|||||||
</VisuallyHidden>
|
</VisuallyHidden>
|
||||||
<div className="m-auto">
|
<div className="m-auto">
|
||||||
<div className="space-y-6 text-center">
|
<div className="space-y-6 text-center">
|
||||||
<h3>{t('product:healthPackageComparison.label')}</h3>
|
<h3 className="sm:text-xxl text-lg">
|
||||||
<p className="text-muted-foreground mx-auto w-3/5 text-sm">
|
{t('product:healthPackageComparison.label')}
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground text-sm sm:mx-auto sm:w-3/5">
|
||||||
{t('product:healthPackageComparison.description')}
|
{t('product:healthPackageComparison.description')}
|
||||||
</p>
|
</p>
|
||||||
<div className="max-h-[50vh] overflow-y-auto rounded-md border sm:max-h-[70vh]">
|
<div className="max-h-[50vh] overflow-y-auto rounded-md border sm:max-h-[70vh]">
|
||||||
@@ -143,12 +138,14 @@ const ComparePackagesModal = async ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={id}>
|
<TableRow key={id}>
|
||||||
<TableCell className="py-6 sm:w-[30vw]">
|
<TableCell className="relative py-6 sm:w-[30vw]">
|
||||||
{title}{' '}
|
{title}{' '}
|
||||||
{description && (
|
{description && (
|
||||||
<InfoTooltip
|
<InfoTooltip
|
||||||
content={description}
|
content={description}
|
||||||
icon={<QuestionMarkCircledIcon />}
|
icon={
|
||||||
|
<QuestionMarkCircledIcon className="absolute top-2 right-0 size-5 sm:static sm:ml-2 sm:size-4" />
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
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 { Trans } from '@kit/ui/makerkit/trans';
|
||||||
import { TableCell, TableRow } from '@kit/ui/shadcn/table';
|
import { TableCell, TableRow } from '@kit/ui/shadcn/table';
|
||||||
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { CheckWithBackground } from './check-with-background';
|
||||||
|
|
||||||
import { CheckWithBackground } from './compare-packages-modal';
|
|
||||||
|
|
||||||
const DefaultPackageFeaturesRows = () => {
|
const DefaultPackageFeaturesRows = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableRow key="digital-doctor-feedback">
|
<TableRow key="digital-doctor-feedback">
|
||||||
<TableCell className="max-w-[30vw] py-6">
|
<TableCell className="relative max-w-[30vw] py-6">
|
||||||
<Trans i18nKey="order-analysis-package:digitalDoctorFeedback" />
|
<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>
|
||||||
<TableCell align="center" className="py-6">
|
<TableCell align="center" className="py-6">
|
||||||
<CheckWithBackground />
|
<CheckWithBackground />
|
||||||
@@ -43,4 +54,4 @@ const DefaultPackageFeaturesRows = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n(DefaultPackageFeaturesRows);
|
export default DefaultPackageFeaturesRows;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
|
|
||||||
// home imports
|
// home imports
|
||||||
import type { UserWorkspace } from '../_lib/server/load-user-workspace';
|
import type { UserWorkspace } from '../_lib/server/load-user-workspace';
|
||||||
|
import { UserNotifications } from './user-notifications';
|
||||||
|
|
||||||
const PERSONAL_ACCOUNT_SLUG = 'personal';
|
const PERSONAL_ACCOUNT_SLUG = 'personal';
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ export function HomeMobileNavigation(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<div className="flex justify-between gap-4">
|
<div className="flex justify-between gap-3">
|
||||||
<Link href={pathsConfig.app.cart}>
|
<Link href={pathsConfig.app.cart}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -108,6 +109,9 @@ export function HomeMobileNavigation(props: {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<UserNotifications userId={user.id} />
|
||||||
|
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<Menu className="h-6 w-6" />
|
<Menu className="h-6 w-6" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export default function OrderBlock({
|
|||||||
id: analysisOrder.id,
|
id: analysisOrder.id,
|
||||||
status: analysisOrder.status,
|
status: analysisOrder.status,
|
||||||
}}
|
}}
|
||||||
|
isPackage
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{itemsTtoService && (
|
{itemsTtoService && (
|
||||||
|
|||||||
@@ -32,11 +32,13 @@ export default function OrderItemsTable({
|
|||||||
title,
|
title,
|
||||||
order,
|
order,
|
||||||
type = 'analysisOrder',
|
type = 'analysisOrder',
|
||||||
|
isPackage = false,
|
||||||
}: {
|
}: {
|
||||||
items: StoreOrderLineItem[];
|
items: StoreOrderLineItem[];
|
||||||
title: string;
|
title: string;
|
||||||
order: Order;
|
order: Order;
|
||||||
type?: OrderItemType;
|
type?: OrderItemType;
|
||||||
|
isPackage?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
||||||
@@ -100,9 +102,15 @@ export default function OrderItemsTable({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
<TableCell className="min-w-[180px] px-6">
|
<TableCell className="min-w-[180px] px-6">
|
||||||
<Trans
|
{isPackage ? (
|
||||||
i18nKey={`orders:status.${type}.${order?.status ?? 'CONFIRMED'}`}
|
<Trans
|
||||||
/>
|
i18nKey={`orders:status.analysisPackageOrder.${order?.status ?? 'CONFIRMED'}`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Trans
|
||||||
|
i18nKey={`orders:status.${type}.${order?.status ?? 'CONFIRMED'}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="px-6 text-right">
|
<TableCell className="px-6 text-right">
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
|
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 { listProductTypes } from '@lib/data';
|
||||||
import { initiateMultiPaymentSession, placeOrder } from '@lib/data/cart';
|
import { initiateMultiPaymentSession, placeOrder } from '@lib/data/cart';
|
||||||
import type { StoreCart, StoreOrder } from '@medusajs/types';
|
import type { StoreCart, StoreOrder } from '@medusajs/types';
|
||||||
@@ -327,7 +325,6 @@ const sendEmail = async ({
|
|||||||
partnerLocationName: string;
|
partnerLocationName: string;
|
||||||
language: string;
|
language: string;
|
||||||
}) => {
|
}) => {
|
||||||
const client = getSupabaseServerAdminClient();
|
|
||||||
try {
|
try {
|
||||||
const { renderSynlabAnalysisPackageEmail } = await import(
|
const { renderSynlabAnalysisPackageEmail } = await import(
|
||||||
'@kit/email-templates'
|
'@kit/email-templates'
|
||||||
@@ -353,10 +350,6 @@ const sendEmail = async ({
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw new Error(`Failed to send email, message=${error}`);
|
throw new Error(`Failed to send email, message=${error}`);
|
||||||
});
|
});
|
||||||
await createNotificationsApi(client).createNotification({
|
|
||||||
account_id: account.id,
|
|
||||||
body: html,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to send email, message=${error}`);
|
throw new Error(`Failed to send email, message=${error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
import type { PostgrestError } from '@supabase/supabase-js';
|
import type { PostgrestError } from '@supabase/supabase-js';
|
||||||
|
|
||||||
import { GetMessageListResponse, MedipostAction } from '@/lib/types/medipost';
|
import { GetMessageListResponse, MedipostAction } from '@/lib/types/medipost';
|
||||||
|
import { createNotificationsApi } from '@/packages/features/notifications/src/server/api';
|
||||||
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
||||||
|
import { pathsConfig } from '@/packages/shared/src/config';
|
||||||
import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis';
|
import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis';
|
||||||
import type {
|
import type {
|
||||||
MedipostOrderResponse,
|
MedipostOrderResponse,
|
||||||
@@ -16,6 +18,7 @@ import axios from 'axios';
|
|||||||
import { toArray } from '@kit/shared/utils';
|
import { toArray } from '@kit/shared/utils';
|
||||||
import { Tables } from '@kit/supabase/database';
|
import { Tables } from '@kit/supabase/database';
|
||||||
|
|
||||||
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element';
|
import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element';
|
||||||
import type { AnalysisOrder } from '~/lib/types/order';
|
import type { AnalysisOrder } from '~/lib/types/order';
|
||||||
|
|
||||||
@@ -268,6 +271,7 @@ export async function syncPrivateMessage({
|
|||||||
order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||||
}) {
|
}) {
|
||||||
const supabase = getSupabaseServerAdminClient();
|
const supabase = getSupabaseServerAdminClient();
|
||||||
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
const orderStatus = AnalysisOrderStatus[TellimuseOlek];
|
const orderStatus = AnalysisOrderStatus[TellimuseOlek];
|
||||||
|
|
||||||
@@ -300,6 +304,7 @@ export async function syncPrivateMessage({
|
|||||||
log,
|
log,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let newElementsAdded = 0;
|
||||||
for (const element of newElements) {
|
for (const element of newElements) {
|
||||||
try {
|
try {
|
||||||
await upsertAnalysisResponseElement({
|
await upsertAnalysisResponseElement({
|
||||||
@@ -308,6 +313,7 @@ export async function syncPrivateMessage({
|
|||||||
analysis_response_id: analysisResponseId,
|
analysis_response_id: analysisResponseId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
newElementsAdded++;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(
|
log(
|
||||||
`Failed to create order response element for response id ${analysisResponseId}, element id '${element.analysis_element_original_id}' (order id: ${order.id})`,
|
`Failed to create order response element for response id ${analysisResponseId}, element id '${element.analysis_element_original_id}' (order id: ${order.id})`,
|
||||||
@@ -316,6 +322,16 @@ export async function syncPrivateMessage({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log(`Added ${newElementsAdded} new elements`);
|
||||||
|
|
||||||
|
if (newElementsAdded !== 0) {
|
||||||
|
await createNotificationsApi(supabase).createNotification({
|
||||||
|
account_id: analysisOrder.user_id,
|
||||||
|
body: t('analysis-results:notification.body'),
|
||||||
|
link: `${pathsConfig.app.analysisResults}/${order.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (await hasAllAnalysisResponseElements({ analysisResponseId, order }))
|
return (await hasAllAnalysisResponseElements({ analysisResponseId, order }))
|
||||||
? { isCompleted: orderStatus === 'COMPLETED' }
|
? { isCompleted: orderStatus === 'COMPLETED' }
|
||||||
: { isPartial: true };
|
: { isPartial: true };
|
||||||
@@ -371,8 +387,13 @@ export async function readPrivateMessageResponse({
|
|||||||
const hasInvalidOrderId = isNaN(analysisOrderId);
|
const hasInvalidOrderId = isNaN(analysisOrderId);
|
||||||
|
|
||||||
if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) {
|
if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) {
|
||||||
|
console.log({
|
||||||
|
privateMessageContent,
|
||||||
|
saadetis: privateMessageContent?.Saadetis,
|
||||||
|
messageResponse,
|
||||||
|
});
|
||||||
console.error(
|
console.error(
|
||||||
`Invalid order id or message response or patient personal code, medipostExternalOrderId=${medipostExternalOrderId}, privateMessageId=${privateMessageId}`,
|
`Invalid !order id or message response or patient personal code, medipostExternalOrderId=${medipostExternalOrderId}, privateMessageId=${privateMessageId}`,
|
||||||
);
|
);
|
||||||
await upsertMedipostActionLog({
|
await upsertMedipostActionLog({
|
||||||
action: 'sync_analysis_results_from_medipost',
|
action: 'sync_analysis_results_from_medipost',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { redirect } from 'next/navigation';
|
|||||||
|
|
||||||
import { sdk } from '@lib/config';
|
import { sdk } from '@lib/config';
|
||||||
import medusaError from '@lib/util/medusa-error';
|
import medusaError from '@lib/util/medusa-error';
|
||||||
import { HttpTypes, StoreCart } from '@medusajs/types';
|
import { HttpTypes, StoreCart, StoreCartPromotion } from '@medusajs/types';
|
||||||
|
|
||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
|
|
||||||
@@ -25,7 +25,9 @@ import { getRegion } from './regions';
|
|||||||
* @param cartId - optional - The ID of the cart to retrieve.
|
* @param cartId - optional - The ID of the cart to retrieve.
|
||||||
* @returns The cart object if found, or null if not found.
|
* @returns The cart object if found, or null if not found.
|
||||||
*/
|
*/
|
||||||
export async function retrieveCart(cartId?: string) {
|
export async function retrieveCart(
|
||||||
|
cartId?: string,
|
||||||
|
): Promise<(StoreCart & { promotions: StoreCartPromotion[] }) | null> {
|
||||||
const id = cartId || (await getCartId());
|
const id = cartId || (await getCartId());
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ export const listOrders = async (
|
|||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
.then(({ orders }) => orders)
|
.then(({ orders }) => orders)
|
||||||
.catch((err) => medusaError(err));
|
.catch((err) => {
|
||||||
|
console.error('Error receiving orders', { err });
|
||||||
|
return medusaError(err);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listOrdersByIds = async (ids: string[]) => {
|
export const listOrdersByIds = async (ids: string[]) => {
|
||||||
|
|||||||
@@ -50,4 +50,13 @@ class NotificationsApi {
|
|||||||
createNotification(params: Notification['Insert']) {
|
createNotification(params: Notification['Insert']) {
|
||||||
return this.service.createNotification(params);
|
return this.service.createNotification(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name createNotification
|
||||||
|
* @description Create a new notification in the database
|
||||||
|
* @param params
|
||||||
|
*/
|
||||||
|
dismissNotification(eqValue: string, eqColumn?: string) {
|
||||||
|
return this.service.dismissNotification(eqColumn, eqValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,4 +29,21 @@ class NotificationsService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async dismissNotification(eqColumn = 'id', eqValue: string) {
|
||||||
|
const logger = await getLogger();
|
||||||
|
const { error } = await this.client
|
||||||
|
.schema('medreport')
|
||||||
|
.from('notifications')
|
||||||
|
.update({ dismissed: true })
|
||||||
|
.eq(eqColumn, eqValue);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logger.error(
|
||||||
|
{ eqColumn, eqValue },
|
||||||
|
`Could not dismiss notification: ${error.message}`,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function PageWithSidebar(props: PageProps) {
|
|||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
props.contentContainerClassName ??
|
props.contentContainerClassName ??
|
||||||
'mx-auto flex h-screen w-full flex-col bg-inherit'
|
'mx-auto flex w-full flex-col bg-inherit'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{MobileNavigation}
|
{MobileNavigation}
|
||||||
@@ -71,7 +71,7 @@ function PageWithHeader(props: PageProps) {
|
|||||||
const { Navigation, Children, MobileNavigation } = getSlotsFromPage(props);
|
const { Navigation, Children, MobileNavigation } = getSlotsFromPage(props);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('z-900 flex h-screen flex-1 flex-col', props.className)}>
|
<div className={cn('flex h-screen flex-1 flex-col', props.className)}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
props.contentContainerClassName ?? 'flex flex-1 flex-col space-y-4'
|
props.contentContainerClassName ?? 'flex flex-1 flex-col space-y-4'
|
||||||
@@ -81,7 +81,7 @@ function PageWithHeader(props: PageProps) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'bg-bg-background light:border-border dark:border-border dark:shadow-primary/10 flex h-15 items-center justify-between border-1 border-b px-4 py-1 lg:justify-start lg:shadow-xs',
|
'bg-bg-background light:border-border dark:border-border dark:shadow-primary/10 flex h-15 items-center justify-between border-1 border-b px-4 py-1 lg:justify-start lg:shadow-xs',
|
||||||
{
|
{
|
||||||
'sticky top-0 z-1000 backdrop-blur-md': props.sticky ?? true,
|
'sticky top-0 z-1000': props.sticky ?? true,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,5 +21,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"orderTitle": "Order number {{orderNumber}}",
|
"orderTitle": "Order number {{orderNumber}}",
|
||||||
"view": "View results"
|
"view": "View results",
|
||||||
|
"notification": {
|
||||||
|
"body": "You have new analysis results"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,6 @@
|
|||||||
"analysisPackageAddedToCart": "Analysis package added to cart",
|
"analysisPackageAddedToCart": "Analysis package added to cart",
|
||||||
"analysisPackageAddToCartError": "Adding analysis package to cart failed",
|
"analysisPackageAddToCartError": "Adding analysis package to cart failed",
|
||||||
"digitalDoctorFeedback": "Digital doctor feedback",
|
"digitalDoctorFeedback": "Digital doctor feedback",
|
||||||
|
"digitalDoctorFeedbackInfo": "Kui oled analüüsid proovivõtupunktis ära andnud ning kõik vastused on tulnud MedReport kontole, vaatab arst tulemused üle. Tulemuste alt näed oma analüüsi tulemusi ja arsti poolset kokkuvõtet.",
|
||||||
"giveAnalyses": "Analyses"
|
"giveAnalyses": "Analyses"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,15 @@
|
|||||||
"REJECTED": "Rejected",
|
"REJECTED": "Rejected",
|
||||||
"CANCELLED": "Cancelled",
|
"CANCELLED": "Cancelled",
|
||||||
"analysisOrder": {
|
"analysisOrder": {
|
||||||
|
"QUEUED": "Queued",
|
||||||
|
"PROCESSING": "Sent to Synlab",
|
||||||
|
"PARTIAL_ANALYSIS_RESPONSE": "Partial results",
|
||||||
|
"FULL_ANALYSIS_RESPONSE": "All results received",
|
||||||
|
"COMPLETED": "Confirmed",
|
||||||
|
"REJECTED": "Rejected",
|
||||||
|
"CANCELLED": "Cancelled"
|
||||||
|
},
|
||||||
|
"analysisPackageOrder": {
|
||||||
"QUEUED": "Queued",
|
"QUEUED": "Queued",
|
||||||
"PROCESSING": "Sent to Synlab",
|
"PROCESSING": "Sent to Synlab",
|
||||||
"PARTIAL_ANALYSIS_RESPONSE": "Partial results",
|
"PARTIAL_ANALYSIS_RESPONSE": "Partial results",
|
||||||
|
|||||||
@@ -21,5 +21,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"orderTitle": "Tellimus {{orderNumber}}",
|
"orderTitle": "Tellimus {{orderNumber}}",
|
||||||
"view": "Vaata tulemusi"
|
"view": "Vaata tulemusi",
|
||||||
|
"notification": {
|
||||||
|
"body": "Teil on valmis uued analüüsi tulemused"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,6 @@
|
|||||||
"analysisPackageAddedToCart": "Analüüsi pakett lisatud ostukorvi",
|
"analysisPackageAddedToCart": "Analüüsi pakett lisatud ostukorvi",
|
||||||
"analysisPackageAddToCartError": "Analüüsi paketi lisamine ostukorvi ebaõnnestus",
|
"analysisPackageAddToCartError": "Analüüsi paketi lisamine ostukorvi ebaõnnestus",
|
||||||
"digitalDoctorFeedback": "Digitaalne arsti kokkuvõte",
|
"digitalDoctorFeedback": "Digitaalne arsti kokkuvõte",
|
||||||
|
"digitalDoctorFeedbackInfo": "Kui oled analüüsid proovivõtupunktis ära andnud ning kõik vastused on tulnud MedReport kontole, vaatab arst tulemused üle. Tulemuste alt näed oma analüüsi tulemusi ja arsti poolset kokkuvõtet.",
|
||||||
"giveAnalyses": "Analüüside võtmine"
|
"giveAnalyses": "Analüüside võtmine"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,15 @@
|
|||||||
"REJECTED": "Tagastatud",
|
"REJECTED": "Tagastatud",
|
||||||
"CANCELLED": "Tühistatud",
|
"CANCELLED": "Tühistatud",
|
||||||
"analysisOrder": {
|
"analysisOrder": {
|
||||||
|
"QUEUED": "Esitatud",
|
||||||
|
"PROCESSING": "Synlabile edastatud",
|
||||||
|
"PARTIAL_ANALYSIS_RESPONSE": "Osalised tulemused",
|
||||||
|
"FULL_ANALYSIS_RESPONSE": "Kõik tulemused käes",
|
||||||
|
"COMPLETED": "Kinnitatud",
|
||||||
|
"REJECTED": "Tagastatud",
|
||||||
|
"CANCELLED": "Tühistatud"
|
||||||
|
},
|
||||||
|
"analysisPackageOrder": {
|
||||||
"QUEUED": "Esitatud",
|
"QUEUED": "Esitatud",
|
||||||
"PROCESSING": "Synlabile edastatud",
|
"PROCESSING": "Synlabile edastatud",
|
||||||
"PARTIAL_ANALYSIS_RESPONSE": "Osalised tulemused",
|
"PARTIAL_ANALYSIS_RESPONSE": "Osalised tulemused",
|
||||||
|
|||||||
@@ -20,5 +20,8 @@
|
|||||||
"isNotWithinNorm": "Не в норме"
|
"isNotWithinNorm": "Не в норме"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"orderTitle": "Заказ {{orderNumber}}"
|
"orderTitle": "Заказ {{orderNumber}}",
|
||||||
|
"notification": {
|
||||||
|
"body": "Teil on valmis uued analüüsi tulemused"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,6 @@
|
|||||||
"analysisPackageAddedToCart": "Пакет анализов добавлен в корзину",
|
"analysisPackageAddedToCart": "Пакет анализов добавлен в корзину",
|
||||||
"analysisPackageAddToCartError": "Не удалось добавить пакет анализов в корзину",
|
"analysisPackageAddToCartError": "Не удалось добавить пакет анализов в корзину",
|
||||||
"digitalDoctorFeedback": "Digital doctor feedback",
|
"digitalDoctorFeedback": "Digital doctor feedback",
|
||||||
|
"digitalDoctorFeedbackInfo": "Kui oled analüüsid proovivõtupunktis ära andnud ning kõik vastused on tulnud MedReport kontole, vaatab arst tulemused üle. Tulemuste alt näed oma analüüsi tulemusi ja arsti poolset kokkuvõtet.",
|
||||||
"giveAnalyses": "Analyses"
|
"giveAnalyses": "Analyses"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,15 @@
|
|||||||
"REJECTED": "Отклонено",
|
"REJECTED": "Отклонено",
|
||||||
"CANCELLED": "Отменено",
|
"CANCELLED": "Отменено",
|
||||||
"analysisOrder": {
|
"analysisOrder": {
|
||||||
|
"QUEUED": "Отправлено",
|
||||||
|
"PROCESSING": "Отправлено в Synlab",
|
||||||
|
"PARTIAL_ANALYSIS_RESPONSE": "Частичные результаты",
|
||||||
|
"FULL_ANALYSIS_RESPONSE": "Все результаты получены",
|
||||||
|
"COMPLETED": "Подтверждено",
|
||||||
|
"REJECTED": "Отклонено",
|
||||||
|
"CANCELLED": "Отменено"
|
||||||
|
},
|
||||||
|
"analysisPackageOrder": {
|
||||||
"QUEUED": "Отправлено",
|
"QUEUED": "Отправлено",
|
||||||
"PROCESSING": "Отправлено в Synlab",
|
"PROCESSING": "Отправлено в Synlab",
|
||||||
"PARTIAL_ANALYSIS_RESPONSE": "Частичные результаты",
|
"PARTIAL_ANALYSIS_RESPONSE": "Частичные результаты",
|
||||||
|
|||||||
Reference in New Issue
Block a user