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

@@ -1,12 +1,20 @@
import { redirect } from 'next/navigation';
import { HomeLayoutPageHeader } from '@/app/home/(user)/_components/home-page-header';
import { loadCategory } from '@/app/home/(user)/_lib/server/load-category';
import { pathsConfig } from '@kit/shared/config';
import { AppBreadcrumbs } from '@kit/ui/makerkit/app-breadcrumbs';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import BookingContainer from '~/home/(user)/_components/booking/booking-container';
import { loadCurrentUserAccount } from '~/home/(user)/_lib/server/load-user-account';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import {
PageViewAction,
createPageViewLog,
} from '~/lib/services/audit/pageView.service';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
@@ -17,9 +25,30 @@ export const generateMetadata = async () => {
};
};
async function BookingHandlePage({ params }: { params: { handle: string } }) {
const handle = await params.handle;
async function BookingHandlePage({
params,
}: {
params: Promise<{ handle: string }>;
}) {
const { handle } = await params;
const { category } = await loadCategory({ handle });
const { account } = await loadCurrentUserAccount();
if (!category) {
return <div>Category not found</div>;
}
if (!account) {
return redirect(pathsConfig.auth.signIn);
}
await createPageViewLog({
accountId: account.id,
action: PageViewAction.VIEW_TTO_SERVICE_BOOKING,
extraData: {
handle,
},
});
return (
<>
@@ -30,10 +59,10 @@ async function BookingHandlePage({ params }: { params: { handle: string } }) {
/>
<HomeLayoutPageHeader
title={<Trans i18nKey={'booking:title'} />}
description={<Trans i18nKey={'booking:description'} />}
description=""
/>
<PageBody></PageBody>
<BookingContainer category={category} />
</>
);
}

View File

@@ -34,8 +34,15 @@ export default function MontonioCallbackClient({
setHasProcessed(true);
try {
const { orderId } = await processMontonioCallback(orderToken);
router.push(`/home/order/${orderId}/confirmed`);
const result = await processMontonioCallback(orderToken);
if (result.success) {
return router.push(`/home/order/${result.orderId}/confirmed`);
}
if (result.failedServiceBookings?.length) {
router.push(
`/home/cart/montonio-callback/error?reasonFailed=${result.failedServiceBookings.map(({ reason }) => reason).join(',')}`,
);
}
} catch (error) {
console.error('Failed to place order', error);
router.push('/home/cart/montonio-callback/error');

View File

@@ -1,8 +1,11 @@
import { use } from 'react';
import Link from 'next/link';
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
import { PageBody, PageHeader } from '@/packages/ui/src/makerkit/page';
import { toArray } from '@kit/shared/utils';
import { Button } from '@kit/ui/button';
import { Alert, AlertDescription } from '@kit/ui/shadcn/alert';
import { AlertTitle } from '@kit/ui/shadcn/alert';
@@ -16,7 +19,15 @@ export async function generateMetadata() {
};
}
export default async function MontonioCheckoutCallbackErrorPage() {
export default async function MontonioCheckoutCallbackErrorPage({
searchParams,
}: {
searchParams: Promise<{ reasonFailed: string }>;
}) {
const params = await searchParams;
const failedBookingData: string[] = toArray(params.reasonFailed?.split(','));
return (
<div className={'flex h-full flex-1 flex-col'}>
<PageHeader title={<Trans i18nKey="cart:montonioCallback.title" />} />
@@ -28,9 +39,15 @@ export default async function MontonioCheckoutCallbackErrorPage() {
</AlertTitle>
<AlertDescription>
<p>
{failedBookingData.length ? (
failedBookingData.map((failureReason, index) => (
<p key={index}>
<Trans i18nKey={`checkout.error.${failureReason}`} />
</p>
))
) : (
<Trans i18nKey={'checkout.error.description'} />
</p>
)}
</AlertDescription>
</Alert>

View File

@@ -6,11 +6,14 @@ import { listProductTypes } from '@lib/data/products';
import { Trans } from '@kit/ui/trans';
import { withI18n } from '~/lib/i18n/with-i18n';
import { getCartReservations } from '~/lib/services/reservation.service';
import { findProductTypeIdByHandle } from '~/lib/utils';
import Cart from '../../_components/cart';
import CartTimer from '../../_components/cart/cart-timer';
import { loadCurrentUserAccount } from '../../_lib/server/load-user-account';
import { AccountBalanceService } from '@kit/accounts/services/account-balance.service';
import { EnrichedCartItem } from '../../_components/cart/types';
export async function generateMetadata() {
const { t } = await createI18nServerInstance();
@@ -37,29 +40,32 @@ async function CartPage() {
const balanceSummary = await new AccountBalanceService().getBalanceSummary(account.id);
const analysisPackagesType = productTypes.find(
({ metadata }) => metadata?.handle === 'analysis-packages',
const synlabAnalysisTypeId = findProductTypeIdByHandle(
productTypes,
'synlab-analysis',
);
const synlabAnalysisType = productTypes.find(
({ metadata }) => metadata?.handle === 'synlab-analysis',
const analysisPackagesTypeId = findProductTypeIdByHandle(
productTypes,
'analysis-packages',
);
const synlabAnalyses =
analysisPackagesType && synlabAnalysisType && cart?.items
analysisPackagesTypeId && synlabAnalysisTypeId && cart?.items
? cart.items.filter((item) => {
const productTypeId = item.product?.type_id;
if (!productTypeId) {
return false;
}
return [analysisPackagesType.id, synlabAnalysisType.id].includes(
return [analysisPackagesTypeId, synlabAnalysisTypeId].includes(
productTypeId,
);
})
: [];
const ttoServiceItems =
cart?.items?.filter(
(item) => !synlabAnalyses.some((analysis) => analysis.id === item.id),
) ?? [];
let ttoServiceItems: EnrichedCartItem[] = [];
if (cart?.items?.length) {
ttoServiceItems = await getCartReservations(cart);
}
const otherItemsSorted = ttoServiceItems.sort((a, b) =>
(a.updated_at ?? '') > (b.updated_at ?? '') ? -1 : 1,
);

View File

@@ -26,17 +26,7 @@ async function OrderConfirmedPage(props: {
params: Promise<{ orderId: string }>;
}) {
const params = await props.params;
const order = await getAnalysisOrder({
analysisOrderId: Number(params.orderId),
}).catch(() => null);
if (!order) {
redirect(pathsConfig.app.myOrders);
}
const medusaOrder = await retrieveOrder(order.medusa_order_id).catch(
() => null,
);
const medusaOrder = await retrieveOrder(params.orderId).catch(() => null);
if (!medusaOrder) {
redirect(pathsConfig.app.myOrders);
}
@@ -46,7 +36,12 @@ async function OrderConfirmedPage(props: {
<PageHeader title={<Trans i18nKey="cart:order.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: medusaOrder.id,
created_at: medusaOrder.created_at,
}}
/>
<Divider />
<OrderItems medusaOrder={medusaOrder} />
<CartTotals medusaOrder={medusaOrder} />

View File

@@ -11,7 +11,8 @@ import { PageBody } from '@kit/ui/makerkit/page';
import { Trans } from '@kit/ui/trans';
import { withI18n } from '~/lib/i18n/with-i18n';
import { getAnalysisOrders } from '~/lib/services/order.service';
import { getAnalysisOrders, getTtoOrders } from '~/lib/services/order.service';
import { findProductTypeIdByHandle } from '~/lib/utils';
import { listOrders } from '~/medusa/lib/data/orders';
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
@@ -28,19 +29,25 @@ export async function generateMetadata() {
}
async function OrdersPage() {
const [medusaOrders, analysisOrders, { productTypes }] = await Promise.all([
const [medusaOrders, analysisOrders, ttoOrders, { productTypes }] = await Promise.all([
listOrders(ORDERS_LIMIT),
getAnalysisOrders(),
getTtoOrders(),
listProductTypes(),
]);
if (!medusaOrders || !productTypes) {
if (!medusaOrders || !productTypes || !ttoOrders) {
redirect(pathsConfig.auth.signIn);
}
const analysisPackagesType = productTypes.find(
({ metadata }) => metadata?.handle === 'analysis-packages',
)!;
const analysisPackagesTypeId = findProductTypeIdByHandle(
productTypes,
'analysis-package',
);
const ttoServiceTypeId = findProductTypeIdByHandle(
productTypes,
'tto-service',
);
return (
<>
@@ -49,34 +56,45 @@ async function OrdersPage() {
description={<Trans i18nKey={'orders:description'} />}
/>
<PageBody>
{analysisOrders.map((analysisOrder) => {
const medusaOrder = medusaOrders.find(
({ id }) => id === analysisOrder.medusa_order_id,
{medusaOrders.map((medusaOrder) => {
const analysisOrder = analysisOrders.find(
({ medusa_order_id }) => medusa_order_id === medusaOrder.id,
);
if (!medusaOrder) {
return null;
}
const medusaOrderItems = medusaOrder.items || [];
const medusaOrderItemsAnalysisPackages = medusaOrderItems.filter(
(item) => item.product_type_id === analysisPackagesType?.id,
(item) => item.product_type_id === analysisPackagesTypeId,
);
const medusaOrderItemsTtoServices = medusaOrderItems.filter(
(item) => item.product_type_id === ttoServiceTypeId,
);
const medusaOrderItemsOther = medusaOrderItems.filter(
(item) => item.product_type_id !== analysisPackagesType?.id,
(item) =>
!item.product_type_id ||
![analysisPackagesTypeId, ttoServiceTypeId].includes(
item.product_type_id,
),
);
return (
<React.Fragment key={analysisOrder.id}>
<React.Fragment key={medusaOrder.id}>
<Divider className="my-6" />
<OrderBlock
medusaOrderId={medusaOrder.id}
analysisOrder={analysisOrder}
medusaOrderStatus={medusaOrder.status}
itemsAnalysisPackage={medusaOrderItemsAnalysisPackages}
itemsTtoService={medusaOrderItemsTtoServices}
itemsOther={medusaOrderItemsOther}
/>
</React.Fragment>
);
})}
{analysisOrders.length === 0 && (
{analysisOrders.length === 0 && ttoOrders.length === 0 && (
<h5 className="mt-6">
<Trans i18nKey="orders:noOrders" />
</h5>

View File

@@ -16,6 +16,7 @@ import Dashboard from '../_components/dashboard';
import DashboardCards from '../_components/dashboard-cards';
import Recommendations from '../_components/recommendations';
import RecommendationsSkeleton from '../_components/recommendations-skeleton';
import { isValidOpenAiEnv } from '../_lib/server/is-valid-open-ai-env';
import { loadCurrentUserAccount } from '../_lib/server/load-user-account';
export const generateMetadata = async () => {
@@ -52,17 +53,16 @@ async function UserHomePage() {
/>
<PageBody>
<Dashboard account={account} bmiThresholds={bmiThresholds} />
{process.env.OPENAI_API_KEY &&
process.env.PROMPT_ID_ANALYSIS_RECOMMENDATIONS && (
<>
<h4>
<Trans i18nKey="dashboard:recommendations.title" />
</h4>
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations account={account} />
</Suspense>
</>
)}
{(await isValidOpenAiEnv()) && (
<>
<h4>
<Trans i18nKey="dashboard:recommendations.title" />
</h4>
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations account={account} />
</Suspense>
</>
)}
</PageBody>
</>
);