Merge pull request #42 from MR-medreport/MED-105-1108
feat(MED-105): update analysis results page, analysis order new statuses
This commit is contained in:
@@ -19,7 +19,7 @@ const Level = ({
|
|||||||
isLast = false,
|
isLast = false,
|
||||||
}: {
|
}: {
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
color: 'destructive' | 'success' | 'warning';
|
color: 'destructive' | 'success' | 'warning' | 'gray-200';
|
||||||
isFirst?: boolean;
|
isFirst?: boolean;
|
||||||
isLast?: boolean;
|
isLast?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -40,6 +40,14 @@ const Level = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AnalysisLevelBarSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="mt-4 flex h-3 max-w-[360px] w-[35%] gap-1 sm:mt-0">
|
||||||
|
<Level color="gray-200" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const AnalysisLevelBar = ({
|
const AnalysisLevelBar = ({
|
||||||
normLowerIncluded = true,
|
normLowerIncluded = true,
|
||||||
normUpperIncluded = true,
|
normUpperIncluded = true,
|
||||||
@@ -50,7 +58,7 @@ const AnalysisLevelBar = ({
|
|||||||
level: AnalysisResultLevel;
|
level: AnalysisResultLevel;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex h-3 w-full max-w-[360px] gap-1 sm:mt-0">
|
<div className="mt-4 flex h-3 max-w-[360px] w-[35%] gap-1 sm:mt-0">
|
||||||
{normLowerIncluded && (
|
{normLowerIncluded && (
|
||||||
<>
|
<>
|
||||||
<Level
|
<Level
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
import AnalysisLevelBar, { AnalysisResultLevel } from './analysis-level-bar';
|
import AnalysisLevelBar, { AnalysisLevelBarSkeleton, AnalysisResultLevel } from './analysis-level-bar';
|
||||||
|
import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts';
|
||||||
|
import { AnalysisElement } from '~/lib/services/analysis-element.service';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
export enum AnalysisStatus {
|
export enum AnalysisStatus {
|
||||||
NORMAL = 0,
|
NORMAL = 0,
|
||||||
@@ -15,31 +19,27 @@ export enum AnalysisStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Analysis = ({
|
const Analysis = ({
|
||||||
analysis: {
|
analysisElement,
|
||||||
name,
|
results,
|
||||||
status,
|
|
||||||
unit,
|
|
||||||
value,
|
|
||||||
normLowerIncluded,
|
|
||||||
normUpperIncluded,
|
|
||||||
normLower,
|
|
||||||
normUpper,
|
|
||||||
},
|
|
||||||
}: {
|
}: {
|
||||||
analysis: {
|
analysisElement: AnalysisElement;
|
||||||
name: string;
|
results?: UserAnalysisElement;
|
||||||
status: AnalysisStatus;
|
|
||||||
unit: string;
|
|
||||||
value: number;
|
|
||||||
normLowerIncluded: boolean;
|
|
||||||
normUpperIncluded: boolean;
|
|
||||||
normLower: number;
|
|
||||||
normUpper: number;
|
|
||||||
};
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const name = analysisElement.analysis_name_lab || '';
|
||||||
|
const status = results?.norm_status || AnalysisStatus.NORMAL;
|
||||||
|
const value = results?.response_value || 0;
|
||||||
|
const unit = results?.unit || '';
|
||||||
|
const normLowerIncluded = results?.norm_lower_included || false;
|
||||||
|
const normUpperIncluded = results?.norm_upper_included || false;
|
||||||
|
const normLower = results?.norm_lower || 0;
|
||||||
|
const normUpper = results?.norm_upper || 0;
|
||||||
|
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
const isUnderNorm = value < normLower;
|
const isUnderNorm = value < normLower;
|
||||||
const getAnalysisResultLevel = () => {
|
const getAnalysisResultLevel = () => {
|
||||||
|
if (!results) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (isUnderNorm) {
|
if (isUnderNorm) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case AnalysisStatus.MEDIUM:
|
case AnalysisStatus.MEDIUM:
|
||||||
@@ -59,9 +59,10 @@ const Analysis = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-border grid grid-cols-2 items-center justify-between rounded-lg border px-5 py-3 sm:flex">
|
<div className="border-border items-center justify-between rounded-lg border px-5 py-3 sm:h-[65px] flex flex-col sm:flex-row px-12 gap-2 sm:gap-0">
|
||||||
<div className="flex items-center gap-2 font-semibold">
|
<div className="flex items-center gap-2 font-semibold">
|
||||||
{name}
|
{name}
|
||||||
|
{results?.response_time && (
|
||||||
<div
|
<div
|
||||||
className="group/tooltip relative"
|
className="group/tooltip relative"
|
||||||
onClick={() => setShowTooltip(!showTooltip)}
|
onClick={() => setShowTooltip(!showTooltip)}
|
||||||
@@ -74,23 +75,40 @@ const Analysis = ({
|
|||||||
{ block: showTooltip },
|
{ block: showTooltip },
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
This text changes when you hover the box above.
|
<Trans i18nKey="analysis-results:analysisDate" />{': '}{format(new Date(results.response_time), 'dd.MM.yyyy HH:mm')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
{results ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-3 sm:ml-auto">
|
||||||
<div className="font-semibold">{value}</div>
|
<div className="font-semibold">{value}</div>
|
||||||
<div className="text-muted-foreground text-sm">{unit}</div>
|
<div className="text-muted-foreground text-sm">{unit}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground mt-4 flex gap-2 text-center text-sm sm:mt-0 sm:block sm:gap-0">
|
<div className="text-muted-foreground flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0 mx-8">
|
||||||
{normLower} - {normUpper}
|
{normLower} - {normUpper}
|
||||||
<div>Normaalne vahemik</div>
|
<div>
|
||||||
|
<Trans i18nKey="analysis-results:results.range.normal" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AnalysisLevelBar
|
<AnalysisLevelBar
|
||||||
normLowerIncluded={normLowerIncluded}
|
normLowerIncluded={normLowerIncluded}
|
||||||
normUpperIncluded={normUpperIncluded}
|
normUpperIncluded={normUpperIncluded}
|
||||||
level={getAnalysisResultLevel()}
|
level={getAnalysisResultLevel()!}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-3 sm:ml-auto">
|
||||||
|
<div className="font-semibold">
|
||||||
|
<Trans i18nKey="analysis-results:waitingForResults" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-[60px] mx-8"></div>
|
||||||
|
<AnalysisLevelBarSkeleton />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Fragment } from 'react';
|
import Link from 'next/link';
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
@@ -7,11 +8,17 @@ import { PageBody } from '@kit/ui/page';
|
|||||||
import { Button } from '@kit/ui/shadcn/button';
|
import { Button } from '@kit/ui/shadcn/button';
|
||||||
|
|
||||||
import { loadUserAnalysis } from '../../_lib/server/load-user-analysis';
|
import { loadUserAnalysis } from '../../_lib/server/load-user-analysis';
|
||||||
import Analysis, { AnalysisStatus } from './_components/analysis';
|
import Analysis from './_components/analysis';
|
||||||
|
import pathsConfig from '~/config/paths.config';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { getAnalysisOrders } from '~/lib/services/order.service';
|
||||||
|
import { getAnalysisElements } from '~/lib/services/analysis-element.service';
|
||||||
|
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
|
||||||
|
import { createPageViewLog } from '~/lib/services/audit/pageView.service';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
const i18n = await createI18nServerInstance();
|
const i18n = await createI18nServerInstance();
|
||||||
const title = i18n.t('account:analysisResults.pageTitle');
|
const title = i18n.t('analysis-results:pageTitle');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
@@ -19,47 +26,81 @@ export const generateMetadata = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function AnalysisResultsPage() {
|
async function AnalysisResultsPage() {
|
||||||
const analysisList = await loadUserAnalysis();
|
const account = await loadCurrentUserAccount()
|
||||||
|
if (!account) {
|
||||||
|
throw new Error('Account not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const analysisResponses = await loadUserAnalysis();
|
||||||
|
const analysisResponseElements = analysisResponses?.flatMap(({ elements }) => elements);
|
||||||
|
|
||||||
|
const analysisOrders = await getAnalysisOrders().catch(() => null);
|
||||||
|
|
||||||
|
if (!analysisOrders) {
|
||||||
|
redirect(pathsConfig.auth.signIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
await createPageViewLog({
|
||||||
|
accountId: account.id,
|
||||||
|
action: 'VIEW_ANALYSIS_RESULTS',
|
||||||
|
});
|
||||||
|
|
||||||
|
const analysisElementIds = [
|
||||||
|
...new Set(analysisOrders?.flatMap((order) => order.analysis_element_ids).filter(Boolean) as number[]),
|
||||||
|
];
|
||||||
|
const analysisElements = await getAnalysisElements({ ids: analysisElementIds });
|
||||||
|
const analysisElementsWithResults = analysisResponseElements
|
||||||
|
?.sort((a, b) => {
|
||||||
|
if (!a.response_time || !b.response_time) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return new Date(b.response_time).getTime() - new Date(a.response_time).getTime();
|
||||||
|
})
|
||||||
|
.map((results) => ({ results })) ?? [];
|
||||||
|
const analysisElementsWithoutResults = analysisElements
|
||||||
|
.filter((element) => !analysisElementsWithResults?.some(({ results }) => results.analysis_element_original_id === element.analysis_id_original));
|
||||||
|
|
||||||
|
const hasNoAnalysisElements = analysisElementsWithResults.length === 0 && analysisElementsWithoutResults.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<div className="mt-8 flex items-center justify-between">
|
<div className="mt-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-0">
|
||||||
<div>
|
<div>
|
||||||
<h4>
|
<h4>
|
||||||
<Trans i18nKey="account:analysisResults.pageTitle" />
|
<Trans i18nKey="analysis-results:pageTitle" />
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
{analysisList && analysisList.length > 0 ? (
|
{analysisResponses && analysisResponses.length > 0 ? (
|
||||||
<Trans i18nKey="account:analysisResults.description" />
|
<Trans i18nKey="analysis-results:description" />
|
||||||
) : (
|
) : (
|
||||||
<Trans i18nKey="account:analysisResults.descriptionEmpty" />
|
<Trans i18nKey="analysis-results:descriptionEmpty" />
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button>
|
<Button asChild>
|
||||||
<Trans i18nKey="account:analysisResults.orderNewAnalysis" />
|
<Link href={pathsConfig.app.orderAnalysisPackage}>
|
||||||
|
<Trans i18nKey="analysis-results:orderNewAnalysis" />
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{analysisList?.map((analysis) => (
|
{analysisElementsWithResults.map(({ results }) => {
|
||||||
<Fragment key={analysis.id}>
|
const analysisElement = analysisElements.find((element) => element.analysis_id_original === results.analysis_element_original_id);
|
||||||
{analysis.elements.map((element) => (
|
if (!analysisElement) {
|
||||||
<Analysis
|
return null;
|
||||||
key={element.id}
|
}
|
||||||
analysis={{
|
return (
|
||||||
name: element.analysis_name || '',
|
<Analysis key={results.id} analysisElement={analysisElement} results={results} />
|
||||||
status: element.norm_status as AnalysisStatus,
|
);
|
||||||
unit: element.unit || '',
|
})}
|
||||||
value: element.response_value,
|
{analysisElementsWithoutResults.map((element) => (
|
||||||
normLowerIncluded: !!element.norm_lower_included,
|
<Analysis key={element.analysis_id_original} analysisElement={element} />
|
||||||
normUpperIncluded: !!element.norm_upper_included,
|
|
||||||
normLower: element.norm_lower || 0,
|
|
||||||
normUpper: element.norm_upper || 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Fragment>
|
|
||||||
))}
|
))}
|
||||||
|
{hasNoAnalysisElements && (
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
<Trans i18nKey="analysis-results:noAnalysisElements" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export async function processMontonioCallback(orderToken: string) {
|
|||||||
|
|
||||||
const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false });
|
const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false });
|
||||||
const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder });
|
const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder });
|
||||||
await createOrder({ medusaOrder, orderedAnalysisElements });
|
const orderId = await createOrder({ medusaOrder, orderedAnalysisElements });
|
||||||
|
|
||||||
const { productTypes } = await listProductTypes();
|
const { productTypes } = await listProductTypes();
|
||||||
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE);
|
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE);
|
||||||
@@ -119,10 +119,9 @@ export async function processMontonioCallback(orderToken: string) {
|
|||||||
console.error("Missing email or analysisPackageName", orderResult);
|
console.error("Missing email or analysisPackageName", orderResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send order to Medipost (no await to avoid blocking)
|
|
||||||
sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
|
sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
|
||||||
|
|
||||||
return { success: true };
|
return { success: true, orderId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to place order", error);
|
console.error("Failed to place order", error);
|
||||||
throw new Error(`Failed to place order, message=${error}`);
|
throw new Error(`Failed to place order, message=${error}`);
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ export default function MontonioCallbackClient({ orderToken, error }: {
|
|||||||
setHasProcessed(true);
|
setHasProcessed(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await processMontonioCallback(orderToken);
|
const { orderId } = await processMontonioCallback(orderToken);
|
||||||
router.push('/home/order');
|
router.push(`/home/order/${orderId}/confirmed`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to place order", error);
|
console.error("Failed to place order", error);
|
||||||
router.push('/home/cart/montonio-callback/error');
|
router.push('/home/cart/montonio-callback/error');
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export default async function MontonioCallbackPage({ searchParams }: {
|
|||||||
}) {
|
}) {
|
||||||
const orderToken = (await searchParams)['order-token'];
|
const orderToken = (await searchParams)['order-token'];
|
||||||
|
|
||||||
console.log('orderToken', orderToken);
|
|
||||||
if (!orderToken) {
|
if (!orderToken) {
|
||||||
return <MontonioCallbackClient error="Order token is missing" />;
|
return <MontonioCallbackClient error="Order token is missing" />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
import { retrieveOrder } from '~/medusa/lib/data/orders';
|
|
||||||
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||||
import OrderCompleted from '@/app/home/(user)/_components/order/order-completed';
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
import { getOrder } from '~/lib/services/order.service';
|
||||||
type Props = {
|
import { retrieveOrder } from '@lib/data/orders';
|
||||||
params: Promise<{ orderId: string }>;
|
import pathsConfig from '~/config/paths.config';
|
||||||
};
|
import Divider from "@modules/common/components/divider"
|
||||||
|
import OrderDetails from '@/app/home/(user)/_components/order/order-details';
|
||||||
|
import OrderItems from '@/app/home/(user)/_components/order/order-items';
|
||||||
|
import CartTotals from '@/app/home/(user)/_components/order/cart-totals';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
export async function generateMetadata() {
|
export async function generateMetadata() {
|
||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
@@ -17,15 +20,33 @@ export async function generateMetadata() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function OrderConfirmedPage(props: Props) {
|
async function OrderConfirmedPage(props: {
|
||||||
|
params: Promise<{ orderId: string }>;
|
||||||
|
}) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const order = await retrieveOrder(params.orderId).catch(() => null);
|
|
||||||
|
|
||||||
|
const order = await getOrder({ orderId: Number(params.orderId) }).catch(() => null);
|
||||||
if (!order) {
|
if (!order) {
|
||||||
return notFound();
|
redirect(pathsConfig.app.myOrders);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <OrderCompleted order={order} />;
|
const medusaOrder = await retrieveOrder(order.medusa_order_id).catch(() => null);
|
||||||
|
if (!medusaOrder) {
|
||||||
|
redirect(pathsConfig.app.myOrders);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageBody>
|
||||||
|
<PageHeader title={<Trans i18nKey="cart:orderConfirmed.title" />} />
|
||||||
|
<Divider />
|
||||||
|
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4 gap-y-6">
|
||||||
|
<OrderDetails order={order} />
|
||||||
|
<Divider />
|
||||||
|
<OrderItems medusaOrder={medusaOrder} />
|
||||||
|
<CartTotals medusaOrder={medusaOrder} />
|
||||||
|
</div>
|
||||||
|
</PageBody>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n(OrderConfirmedPage);
|
export default withI18n(OrderConfirmedPage);
|
||||||
|
|||||||
52
app/home/(user)/(dashboard)/order/[orderId]/page.tsx
Normal file
52
app/home/(user)/(dashboard)/order/[orderId]/page.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
|
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
import { getOrder } from '~/lib/services/order.service';
|
||||||
|
import { retrieveOrder } from '@lib/data/orders';
|
||||||
|
import pathsConfig from '~/config/paths.config';
|
||||||
|
import Divider from "@modules/common/components/divider"
|
||||||
|
import OrderDetails from '@/app/home/(user)/_components/order/order-details';
|
||||||
|
import OrderItems from '@/app/home/(user)/_components/order/order-items';
|
||||||
|
import CartTotals from '@/app/home/(user)/_components/order/cart-totals';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
export async function generateMetadata() {
|
||||||
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: t('cart:order.title'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function OrderConfirmedPage(props: {
|
||||||
|
params: Promise<{ orderId: string }>;
|
||||||
|
}) {
|
||||||
|
const params = await props.params;
|
||||||
|
|
||||||
|
const order = await getOrder({ orderId: Number(params.orderId) }).catch(() => null);
|
||||||
|
if (!order) {
|
||||||
|
redirect(pathsConfig.app.myOrders);
|
||||||
|
}
|
||||||
|
|
||||||
|
const medusaOrder = await retrieveOrder(order.medusa_order_id).catch(() => null);
|
||||||
|
if (!medusaOrder) {
|
||||||
|
redirect(pathsConfig.app.myOrders);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageBody>
|
||||||
|
<PageHeader title={<Trans i18nKey="cart:order.title" />} />
|
||||||
|
<Divider />
|
||||||
|
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4 gap-y-6">
|
||||||
|
<OrderDetails order={order} />
|
||||||
|
<Divider />
|
||||||
|
<OrderItems medusaOrder={medusaOrder} />
|
||||||
|
<CartTotals medusaOrder={medusaOrder} />
|
||||||
|
</div>
|
||||||
|
</PageBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n(OrderConfirmedPage);
|
||||||
@@ -3,7 +3,6 @@ import { redirect } from 'next/navigation';
|
|||||||
import { listOrders } from '~/medusa/lib/data/orders';
|
import { listOrders } from '~/medusa/lib/data/orders';
|
||||||
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||||
import { listProductTypes } from '@lib/data/products';
|
import { listProductTypes } from '@lib/data/products';
|
||||||
import { retrieveCustomer } from '@lib/data/customer';
|
|
||||||
import { PageBody } from '@kit/ui/makerkit/page';
|
import { PageBody } from '@kit/ui/makerkit/page';
|
||||||
import pathsConfig from '~/config/paths.config';
|
import pathsConfig from '~/config/paths.config';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
@@ -11,6 +10,7 @@ import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
|||||||
import OrdersTable from '../../_components/orders/orders-table';
|
import OrdersTable from '../../_components/orders/orders-table';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
import type { IOrderLineItem } from '../../_components/orders/types';
|
import type { IOrderLineItem } from '../../_components/orders/types';
|
||||||
|
import { getAnalysisOrders } from '~/lib/services/order.service';
|
||||||
|
|
||||||
export async function generateMetadata() {
|
export async function generateMetadata() {
|
||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
@@ -21,24 +21,49 @@ export async function generateMetadata() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function OrdersPage() {
|
async function OrdersPage() {
|
||||||
const customer = await retrieveCustomer();
|
const medusaOrders = await listOrders();
|
||||||
const orders = await listOrders().catch(() => null);
|
const analysisOrders = await getAnalysisOrders();
|
||||||
const { productTypes } = await listProductTypes();
|
const { productTypes } = await listProductTypes();
|
||||||
|
|
||||||
if (!customer || !orders || !productTypes) {
|
if (!medusaOrders || !productTypes) {
|
||||||
redirect(pathsConfig.auth.signIn);
|
redirect(pathsConfig.auth.signIn);
|
||||||
}
|
}
|
||||||
|
|
||||||
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages');
|
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages');
|
||||||
const analysisPackageOrders: IOrderLineItem[] = orders.flatMap(({ id, items, payment_status, fulfillment_status }) => items
|
const analysisPackageOrders: IOrderLineItem[] = medusaOrders.flatMap(({ id, items, payment_status, fulfillment_status }) => items
|
||||||
?.filter((item) => item.product_type_id === analysisPackagesType?.id)
|
?.filter((item) => item.product_type_id === analysisPackagesType?.id)
|
||||||
.map((item) => ({ item, orderId: id, orderStatus: `${payment_status}/${fulfillment_status}` }))
|
.map((item) => {
|
||||||
|
const localOrder = analysisOrders.find((order) => order.medusa_order_id === id);
|
||||||
|
if (!localOrder) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
item,
|
||||||
|
medusaOrderId: id,
|
||||||
|
orderId: localOrder?.id,
|
||||||
|
orderStatus: localOrder.status,
|
||||||
|
analysis_element_ids: localOrder.analysis_element_ids,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((order) => order !== null)
|
||||||
|| []);
|
|| []);
|
||||||
|
|
||||||
const otherOrders: IOrderLineItem[] = orders
|
const otherOrders: IOrderLineItem[] = medusaOrders
|
||||||
.filter(({ items }) => items?.some((item) => item.product_type_id !== analysisPackagesType?.id))
|
.filter(({ items }) => items?.some((item) => item.product_type_id !== analysisPackagesType?.id))
|
||||||
.flatMap(({ id, items, payment_status, fulfillment_status }) => items
|
.flatMap(({ id, items, payment_status, fulfillment_status }) => items
|
||||||
?.map((item) => ({ item, orderId: id, orderStatus: `${payment_status}/${fulfillment_status}` }))
|
?.map((item) => {
|
||||||
|
const analysisOrder = analysisOrders.find((order) => order.medusa_order_id === id);
|
||||||
|
if (!analysisOrder) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
item,
|
||||||
|
medusaOrderId: id,
|
||||||
|
orderId: analysisOrder.id,
|
||||||
|
orderStatus: analysisOrder.status,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((order) => order !== null)
|
||||||
|| []);
|
|| []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import React from "react"
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
export default function CartTotals({ order }: {
|
export default function CartTotals({ medusaOrder }: {
|
||||||
order: StoreOrder
|
medusaOrder: StoreOrder
|
||||||
}) {
|
}) {
|
||||||
const { i18n: { language } } = useTranslation()
|
const { i18n: { language } } = useTranslation()
|
||||||
const {
|
const {
|
||||||
@@ -17,7 +17,7 @@ export default function CartTotals({ order }: {
|
|||||||
tax_total,
|
tax_total,
|
||||||
discount_total,
|
discount_total,
|
||||||
gift_card_total,
|
gift_card_total,
|
||||||
} = order
|
} = medusaOrder
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
|
||||||
import { StoreOrder } from "@medusajs/types"
|
|
||||||
import Divider from "@modules/common/components/divider"
|
|
||||||
|
|
||||||
import CartTotals from "./cart-totals"
|
|
||||||
import OrderDetails from "./order-details"
|
|
||||||
import OrderItems from "./order-items"
|
|
||||||
|
|
||||||
export default async function OrderCompleted({
|
|
||||||
order,
|
|
||||||
}: {
|
|
||||||
order: StoreOrder,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<PageBody>
|
|
||||||
<PageHeader title={<Trans i18nKey="cart:orderConfirmed.title" />} />
|
|
||||||
<Divider />
|
|
||||||
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4 gap-y-6">
|
|
||||||
<OrderDetails order={order} />
|
|
||||||
<Divider />
|
|
||||||
<OrderItems order={order} />
|
|
||||||
<CartTotals order={order} />
|
|
||||||
</div>
|
|
||||||
</PageBody>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,47 +1,21 @@
|
|||||||
import { StoreOrder } from "@medusajs/types"
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
import { formatDate } from 'date-fns';
|
||||||
|
import { AnalysisOrder } from "~/lib/services/order.service";
|
||||||
|
|
||||||
export default function OrderDetails({ order, showStatus }: {
|
export default function OrderDetails({ order }: {
|
||||||
order: StoreOrder
|
order: AnalysisOrder
|
||||||
showStatus?: boolean
|
|
||||||
}) {
|
}) {
|
||||||
const formatStatus = (str: string) => {
|
|
||||||
const formatted = str.split("_").join(" ")
|
|
||||||
|
|
||||||
return formatted.slice(0, 1).toUpperCase() + formatted.slice(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-2">
|
<div className="flex flex-col gap-y-2">
|
||||||
<span>
|
<span>
|
||||||
<Trans i18nKey="cart:orderConfirmed.orderDate" />:{" "}
|
<Trans i18nKey="cart:orderConfirmed.orderDate" />:{" "}
|
||||||
<span>
|
<span>
|
||||||
{new Date(order.created_at).toLocaleDateString()}
|
{formatDate(order.created_at, 'dd.MM.yyyy HH:mm')}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-ui-fg-interactive">
|
<span className="text-ui-fg-interactive">
|
||||||
<Trans i18nKey="cart:orderConfirmed.orderNumber" />: <span data-testid="order-id">{order.display_id}</span>
|
<Trans i18nKey="cart:orderConfirmed.orderNumber" />: <span data-testid="order-id">{order.medusa_order_id}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{showStatus && (
|
|
||||||
<>
|
|
||||||
<span>
|
|
||||||
<Trans i18nKey="cart:orderConfirmed.orderStatus" />:{" "}
|
|
||||||
<span className="text-ui-fg-subtle">
|
|
||||||
{formatStatus(order.fulfillment_status)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<Trans i18nKey="cart:orderConfirmed.paymentStatus" />:{" "}
|
|
||||||
<span
|
|
||||||
className="text-ui-fg-subtle "
|
|
||||||
data-testid="order-payment-status"
|
|
||||||
>
|
|
||||||
{formatStatus(order.payment_status)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { StoreCartLineItem, StoreOrderLineItem } from "@medusajs/types"
|
import { StoreCartLineItem, StoreOrderLineItem } from "@medusajs/types"
|
||||||
import { TableCell, TableRow } from "@kit/ui/table"
|
import { TableCell, TableRow } from "@kit/ui/table"
|
||||||
|
|
||||||
import LineItemOptions from "@modules/common/components/line-item-options"
|
// import LineItemOptions from "@modules/common/components/line-item-options"
|
||||||
import LineItemPrice from "@modules/common/components/line-item-price"
|
import LineItemPrice from "@modules/common/components/line-item-price"
|
||||||
import LineItemUnitPrice from "@modules/common/components/line-item-unit-price"
|
import LineItemUnitPrice from "@modules/common/components/line-item-unit-price"
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ export default function OrderItem({ item, currencyCode }: {
|
|||||||
item: StoreCartLineItem | StoreOrderLineItem
|
item: StoreCartLineItem | StoreOrderLineItem
|
||||||
currencyCode: string
|
currencyCode: string
|
||||||
}) {
|
}) {
|
||||||
|
const partnerLocationName = item.metadata?.partner_location_name;
|
||||||
return (
|
return (
|
||||||
<TableRow className="w-full" data-testid="product-row">
|
<TableRow className="w-full" data-testid="product-row">
|
||||||
{/* <TableCell className="px-6 w-24">
|
{/* <TableCell className="px-6 w-24">
|
||||||
@@ -22,9 +23,9 @@ export default function OrderItem({ item, currencyCode }: {
|
|||||||
className="txt-medium-plus text-ui-fg-base"
|
className="txt-medium-plus text-ui-fg-base"
|
||||||
data-testid="product-name"
|
data-testid="product-name"
|
||||||
>
|
>
|
||||||
{item.product_title}{` (${item.metadata?.partner_location_name ?? "-"})`}
|
{item.product_title}{` ${partnerLocationName ? `(${partnerLocationName})` : ''}`}
|
||||||
</span>
|
</span>
|
||||||
<LineItemOptions variant={item.variant} data-testid="product-variant" />
|
{/* <LineItemOptions variant={item.variant} data-testid="product-variant" /> */}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="px-6">
|
<TableCell className="px-6">
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import OrderItem from "./order-item"
|
|||||||
import { Heading } from "@kit/ui/heading"
|
import { Heading } from "@kit/ui/heading"
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
export default function OrderItems({ order }: {
|
export default function OrderItems({ medusaOrder }: {
|
||||||
order: StoreOrder
|
medusaOrder: StoreOrder
|
||||||
}) {
|
}) {
|
||||||
const items = order.items
|
const items = medusaOrder.items
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
@@ -27,7 +27,7 @@ export default function OrderItems({ order }: {
|
|||||||
<OrderItem
|
<OrderItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
currencyCode={order.currency_code}
|
currencyCode={medusaOrder.currency_code}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: repeat(5).map((i) => <SkeletonLineItem key={i} />)}
|
: repeat(5).map((i) => <SkeletonLineItem key={i} />)}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Eye } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { formatDate } from "date-fns";
|
import { formatDate } from "date-fns";
|
||||||
import { IOrderLineItem } from "./types";
|
import { IOrderLineItem } from "./types";
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
export default function OrdersItem({ orderItem }: {
|
export default function OrdersItem({ orderItem }: {
|
||||||
orderItem: IOrderLineItem,
|
orderItem: IOrderLineItem,
|
||||||
@@ -22,15 +23,13 @@ export default function OrdersItem({ orderItem }: {
|
|||||||
{formatDate(orderItem.item.created_at, 'dd.MM.yyyy HH:mm')}
|
{formatDate(orderItem.item.created_at, 'dd.MM.yyyy HH:mm')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{orderItem.orderStatus && (
|
<TableCell className="px-6 whitespace-nowrap">
|
||||||
<TableCell className="px-6">
|
<Trans i18nKey={`orders:status.${orderItem.orderStatus}`} />
|
||||||
{orderItem.orderStatus}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
|
||||||
|
|
||||||
<TableCell className="text-right px-6">
|
<TableCell className="text-right px-6">
|
||||||
<span className="flex gap-x-1 justify-end w-[60px]">
|
<span className="flex gap-x-1 justify-end w-[60px]">
|
||||||
<Link href={`/home/analysis-results`} className="flex items-center justify-between text-small-regular">
|
<Link href={`/home/order/${orderItem.orderId}`} className="flex items-center justify-between text-small-regular">
|
||||||
<button
|
<button
|
||||||
className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer"
|
className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import {
|
|||||||
import OrdersItem from "./orders-item";
|
import OrdersItem from "./orders-item";
|
||||||
import { IOrderLineItem } from "./types";
|
import { IOrderLineItem } from "./types";
|
||||||
|
|
||||||
const IS_SHOWN_ORDER_STATUS = true as boolean;
|
|
||||||
|
|
||||||
export default function OrdersTable({ orderItems, title }: {
|
export default function OrdersTable({ orderItems, title }: {
|
||||||
orderItems: IOrderLineItem[];
|
orderItems: IOrderLineItem[];
|
||||||
title: string;
|
title: string;
|
||||||
@@ -29,10 +27,9 @@ export default function OrdersTable({ orderItems, title }: {
|
|||||||
<TableHead className="px-6">
|
<TableHead className="px-6">
|
||||||
<Trans i18nKey="orders:table.createdAt" />
|
<Trans i18nKey="orders:table.createdAt" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
{IS_SHOWN_ORDER_STATUS && (
|
|
||||||
<TableHead className="px-6">
|
<TableHead className="px-6">
|
||||||
|
<Trans i18nKey="orders:table.status" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
)}
|
|
||||||
<TableHead className="px-6">
|
<TableHead className="px-6">
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { StoreOrderLineItem } from "@medusajs/types";
|
|||||||
|
|
||||||
export interface IOrderLineItem {
|
export interface IOrderLineItem {
|
||||||
item: StoreOrderLineItem;
|
item: StoreOrderLineItem;
|
||||||
orderId: string;
|
medusaOrderId: string;
|
||||||
|
orderId: number;
|
||||||
orderStatus: string;
|
orderStatus: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
/**
|
|
||||||
* This file is used to register monitoring instrumentation
|
|
||||||
* for your Next.js application.
|
|
||||||
*/
|
|
||||||
import { type Instrumentation } from 'next';
|
import { type Instrumentation } from 'next';
|
||||||
|
|
||||||
|
const isEnabledInDev = process.env.ENABLE_LOCAL_JOBS === 'true';
|
||||||
|
|
||||||
export async function register() {
|
export async function register() {
|
||||||
const { registerMonitoringInstrumentation } = await import(
|
const { registerMonitoringInstrumentation } = await import(
|
||||||
'@kit/monitoring/instrumentation'
|
'@kit/monitoring/instrumentation'
|
||||||
@@ -12,6 +10,9 @@ export async function register() {
|
|||||||
// Register monitoring instrumentation
|
// Register monitoring instrumentation
|
||||||
// based on the MONITORING_PROVIDER environment variable.
|
// based on the MONITORING_PROVIDER environment variable.
|
||||||
await registerMonitoringInstrumentation();
|
await registerMonitoringInstrumentation();
|
||||||
|
|
||||||
|
// Register lightweight in-process job scheduler
|
||||||
|
await registerJobScheduler();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,3 +29,51 @@ export const onRequestError: Instrumentation.onRequestError = async (err) => {
|
|||||||
await service.ready();
|
await service.ready();
|
||||||
await service.captureException(err as Error);
|
await service.captureException(err as Error);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function registerJobScheduler() {
|
||||||
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
|
if (!isProd && !isEnabledInDev) {
|
||||||
|
console.info('Job scheduler disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent duplicate intervals on hot reloads/dev
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const globalAny = globalThis as any;
|
||||||
|
if (globalAny.__mrJobSchedulerInitialized) {
|
||||||
|
console.info('Job scheduler already initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
globalAny.__mrJobSchedulerInitialized = true;
|
||||||
|
|
||||||
|
let isRunning = false;
|
||||||
|
|
||||||
|
const runSyncAnalysisResults = async () => {
|
||||||
|
if (isRunning) {
|
||||||
|
console.info('Scheduled job syncAnalysisResults skipped: previous run still in progress');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isRunning = true;
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
const { default: loadEnv } = await import('./app/api/job/handler/load-env');
|
||||||
|
loadEnv();
|
||||||
|
} catch {
|
||||||
|
// ignore if not available or already loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
const { default: syncAnalysisResults } = await import(
|
||||||
|
'./app/api/job/handler/sync-analysis-results'
|
||||||
|
);
|
||||||
|
await syncAnalysisResults();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Scheduled job syncAnalysisResults failed:', error);
|
||||||
|
} finally {
|
||||||
|
isRunning = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run every 10 minutes
|
||||||
|
setTimeout(runSyncAnalysisResults, 15_000);
|
||||||
|
setInterval(runSyncAnalysisResults, 10 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export const defaultI18nNamespaces = [
|
|||||||
'order-analysis',
|
'order-analysis',
|
||||||
'cart',
|
'cart',
|
||||||
'orders',
|
'orders',
|
||||||
|
'analysis-results',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
|
||||||
import { Json, Tables } from '@kit/supabase/database';
|
import { Json, Tables } from '@kit/supabase/database';
|
||||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||||
import type { IMaterialGroup, IUuringElement } from './medipost.types';
|
import type { IMaterialGroup, IUuringElement } from './medipost.types';
|
||||||
@@ -9,10 +8,12 @@ export type AnalysisElement = Tables<{ schema: 'medreport' }, 'analysis_elements
|
|||||||
|
|
||||||
export async function getAnalysisElements({
|
export async function getAnalysisElements({
|
||||||
originalIds,
|
originalIds,
|
||||||
|
ids,
|
||||||
}: {
|
}: {
|
||||||
originalIds?: string[];
|
originalIds?: string[];
|
||||||
|
ids?: number[];
|
||||||
}): Promise<AnalysisElement[]> {
|
}): Promise<AnalysisElement[]> {
|
||||||
const query = getSupabaseServerClient()
|
const query = getSupabaseServerAdminClient()
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('analysis_elements')
|
.from('analysis_elements')
|
||||||
.select(`*, analysis_groups(*)`)
|
.select(`*, analysis_groups(*)`)
|
||||||
@@ -22,6 +23,10 @@ export async function getAnalysisElements({
|
|||||||
query.in('analysis_id_original', [...new Set(originalIds)]);
|
query.in('analysis_id_original', [...new Set(originalIds)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(ids)) {
|
||||||
|
query.in('id', ids);
|
||||||
|
}
|
||||||
|
|
||||||
const { data: analysisElements, error } = await query;
|
const { data: analysisElements, error } = await query;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
35
lib/services/audit/pageView.service.ts
Normal file
35
lib/services/audit/pageView.service.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
|
export const createPageViewLog = async ({
|
||||||
|
accountId,
|
||||||
|
action,
|
||||||
|
}: {
|
||||||
|
accountId: string;
|
||||||
|
action: 'VIEW_ANALYSIS_RESULTS';
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const supabase = getSupabaseServerClient();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
error: userError,
|
||||||
|
} = await supabase.auth.getUser();
|
||||||
|
|
||||||
|
if (userError || !user) {
|
||||||
|
console.error('No authenticated user found; skipping audit insert');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await supabase
|
||||||
|
.schema('audit')
|
||||||
|
.from('page_views')
|
||||||
|
.insert({
|
||||||
|
account_id: accountId,
|
||||||
|
action,
|
||||||
|
changed_by: user.id,
|
||||||
|
})
|
||||||
|
.throwOnError();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to insert page view log', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@ import { uniqBy } from 'lodash';
|
|||||||
import { Tables } from '@kit/supabase/database';
|
import { Tables } from '@kit/supabase/database';
|
||||||
import { createAnalysisGroup } from './analysis-group.service';
|
import { createAnalysisGroup } from './analysis-group.service';
|
||||||
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
||||||
import { getOrder } from './order.service';
|
import { getOrder, updateOrder } from './order.service';
|
||||||
import { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service';
|
import { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service';
|
||||||
import { getAnalyses } from './analyses.service';
|
import { getAnalyses } from './analyses.service';
|
||||||
import { getAccountAdmin } from './account.service';
|
import { getAccountAdmin } from './account.service';
|
||||||
@@ -235,6 +235,7 @@ export async function readPrivateMessageResponse({
|
|||||||
const status = await syncPrivateMessage({ messageResponse, order });
|
const status = await syncPrivateMessage({ messageResponse, order });
|
||||||
|
|
||||||
if (status === 'COMPLETED') {
|
if (status === 'COMPLETED') {
|
||||||
|
await updateOrder({ orderId: order.id, orderStatus: 'FULL_ANALYSIS_RESPONSE' });
|
||||||
await deletePrivateMessage(privateMessage.messageId);
|
await deletePrivateMessage(privateMessage.messageId);
|
||||||
messageIdProcessed = privateMessage.messageId;
|
messageIdProcessed = privateMessage.messageId;
|
||||||
}
|
}
|
||||||
@@ -658,6 +659,7 @@ export async function syncPrivateMessage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.info("status", AnalysisOrderStatus[messageResponse.TellimuseOlek], messageResponse.TellimuseOlek);
|
||||||
return AnalysisOrderStatus[messageResponse.TellimuseOlek];
|
return AnalysisOrderStatus[messageResponse.TellimuseOlek];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,6 +688,7 @@ export async function sendOrderToMedipost({
|
|||||||
});
|
});
|
||||||
|
|
||||||
await sendPrivateMessage(orderXml);
|
await sendPrivateMessage(orderXml);
|
||||||
|
await updateOrder({ orderId: medreportOrder.id, orderStatus: 'PROCESSING' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrderedAnalysisElementsIds({
|
export async function getOrderedAnalysisElementsIds({
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type { Tables } from '@kit/supabase/database';
|
|||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import type { StoreOrder } from '@medusajs/types';
|
import type { StoreOrder } from '@medusajs/types';
|
||||||
|
|
||||||
|
export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||||
|
|
||||||
export async function createOrder({
|
export async function createOrder({
|
||||||
medusaOrder,
|
medusaOrder,
|
||||||
orderedAnalysisElements,
|
orderedAnalysisElements,
|
||||||
@@ -32,6 +34,8 @@ export async function createOrder({
|
|||||||
if (orderResult.error || !orderResult.data?.id) {
|
if (orderResult.error || !orderResult.data?.id) {
|
||||||
throw new Error(`Failed to create order, message=${orderResult.error}, data=${JSON.stringify(orderResult)}`);
|
throw new Error(`Failed to create order, message=${orderResult.error}, data=${JSON.stringify(orderResult)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return orderResult.data.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateOrder({
|
export async function updateOrder({
|
||||||
@@ -41,7 +45,8 @@ export async function updateOrder({
|
|||||||
orderId: number;
|
orderId: number;
|
||||||
orderStatus: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
|
orderStatus: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
|
||||||
}) {
|
}) {
|
||||||
const { error } = await getSupabaseServerClient()
|
console.info(`Updating order id=${orderId} status=${orderStatus}`);
|
||||||
|
await getSupabaseServerAdminClient()
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('analysis_orders')
|
.from('analysis_orders')
|
||||||
.update({
|
.update({
|
||||||
@@ -49,27 +54,32 @@ export async function updateOrder({
|
|||||||
})
|
})
|
||||||
.eq('id', orderId)
|
.eq('id', orderId)
|
||||||
.throwOnError();
|
.throwOnError();
|
||||||
if (error) {
|
|
||||||
throw new Error(`Failed to update order, message=${error}, data=${JSON.stringify(error)}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrder({
|
export async function getOrder({
|
||||||
medusaOrderId,
|
medusaOrderId,
|
||||||
|
orderId,
|
||||||
}: {
|
}: {
|
||||||
medusaOrderId: string;
|
medusaOrderId?: string;
|
||||||
|
orderId?: number;
|
||||||
}) {
|
}) {
|
||||||
const query = getSupabaseServerAdminClient()
|
const query = getSupabaseServerAdminClient()
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('analysis_orders')
|
.from('analysis_orders')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('medusa_order_id', medusaOrderId)
|
if (medusaOrderId) {
|
||||||
|
query.eq('medusa_order_id', medusaOrderId);
|
||||||
|
} else if (orderId) {
|
||||||
|
query.eq('id', orderId);
|
||||||
|
} else {
|
||||||
|
throw new Error('Either medusaOrderId or orderId must be provided');
|
||||||
|
}
|
||||||
|
|
||||||
const { data: order } = await query.single().throwOnError();
|
const { data: order } = await query.single().throwOnError();
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrders({
|
export async function getAnalysisOrders({
|
||||||
orderStatus,
|
orderStatus,
|
||||||
}: {
|
}: {
|
||||||
orderStatus?: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
|
orderStatus?: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
export type UserAnalysis =
|
export type UserAnalysisElement = Database['medreport']['Tables']['analysis_response_elements']['Row'];
|
||||||
(Database['medreport']['Tables']['analysis_responses']['Row'] & {
|
export type UserAnalysisResponse = Database['medreport']['Tables']['analysis_responses']['Row'] & {
|
||||||
elements: Database['medreport']['Tables']['analysis_response_elements']['Row'][];
|
elements: UserAnalysisElement[];
|
||||||
})[];
|
};
|
||||||
|
export type UserAnalysis = UserAnalysisResponse[];
|
||||||
|
|||||||
@@ -81,6 +81,30 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Relationships: []
|
Relationships: []
|
||||||
}
|
}
|
||||||
|
page_views: {
|
||||||
|
Row: {
|
||||||
|
account_id: string
|
||||||
|
action: string
|
||||||
|
changed_by: string
|
||||||
|
created_at: string
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
account_id: string
|
||||||
|
action: string
|
||||||
|
changed_by: string
|
||||||
|
created_at?: string
|
||||||
|
id?: number
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
account_id?: string
|
||||||
|
action: string
|
||||||
|
changed_by?: string
|
||||||
|
created_at?: string
|
||||||
|
id?: number
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
request_entries: {
|
request_entries: {
|
||||||
Row: {
|
Row: {
|
||||||
comment: string | null
|
comment: string | null
|
||||||
@@ -1859,6 +1883,9 @@ export type Database = {
|
|||||||
| "QUEUED"
|
| "QUEUED"
|
||||||
| "ON_HOLD"
|
| "ON_HOLD"
|
||||||
| "PROCESSING"
|
| "PROCESSING"
|
||||||
|
| "PARTIAL_ANALYSIS_RESPONSE"
|
||||||
|
| "FULL_ANALYSIS_RESPONSE"
|
||||||
|
| "WAITING_FOR_DOCTOR_RESPONSE"
|
||||||
| "COMPLETED"
|
| "COMPLETED"
|
||||||
| "REJECTED"
|
| "REJECTED"
|
||||||
| "CANCELLED"
|
| "CANCELLED"
|
||||||
@@ -7745,12 +7772,15 @@ export const Constants = {
|
|||||||
medreport: {
|
medreport: {
|
||||||
Enums: {
|
Enums: {
|
||||||
analysis_order_status: [
|
analysis_order_status: [
|
||||||
"QUEUED",
|
"QUEUED", // makstud, ootab Synlabi saatmist
|
||||||
"ON_HOLD",
|
"ON_HOLD", //
|
||||||
"PROCESSING",
|
"PROCESSING", // ootab proovide tulemusi
|
||||||
"COMPLETED",
|
"PARTIAL_ANALYSIS_RESPONSE", // osalised tulemused
|
||||||
"REJECTED",
|
"FULL_ANALYSIS_RESPONSE", // kõik tulemused käes
|
||||||
"CANCELLED",
|
"WAITING_FOR_DOCTOR_RESPONSE", // ootab arsti kokkuvõtet
|
||||||
|
"COMPLETED", // kinnitatud, lõplik
|
||||||
|
"REJECTED", // tagastatud
|
||||||
|
"CANCELLED", // tühistatud
|
||||||
],
|
],
|
||||||
app_permissions: [
|
app_permissions: [
|
||||||
"roles.manage",
|
"roles.manage",
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ function PageWithSidebar(props: PageProps) {
|
|||||||
>
|
>
|
||||||
{MobileNavigation}
|
{MobileNavigation}
|
||||||
|
|
||||||
<div className={'bg-background flex flex-1 flex-col px-4 lg:px-0'}>
|
<div className={'bg-background flex flex-1 flex-col px-4 lg:px-0 pb-8'}>
|
||||||
{Children}
|
{Children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -122,11 +122,5 @@
|
|||||||
"consentToAnonymizedCompanyData": {
|
"consentToAnonymizedCompanyData": {
|
||||||
"label": "Consent to be included in employer statistics",
|
"label": "Consent to be included in employer statistics",
|
||||||
"description": "Consent to be included in anonymized company statistics"
|
"description": "Consent to be included in anonymized company statistics"
|
||||||
},
|
|
||||||
"analysisResults": {
|
|
||||||
"pageTitle": "My analysis results",
|
|
||||||
"description": "Super, you've already done your analysis. Here are your important results:",
|
|
||||||
"descriptionEmpty": "If you've already done your analysis, your results will appear here soon.",
|
|
||||||
"orderNewAnalysis": "Order new analyses"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
public/locales/en/analysis-results.json
Normal file
14
public/locales/en/analysis-results.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"pageTitle": "My analysis results",
|
||||||
|
"description": "All analysis results will appear here within 1-3 business days after they have been done.",
|
||||||
|
"descriptionEmpty": "If you've already done your analysis, your results will appear here soon.",
|
||||||
|
"orderNewAnalysis": "Order new analyses",
|
||||||
|
"waitingForResults": "Waiting for results",
|
||||||
|
"noAnalysisElements": "No analysis orders found",
|
||||||
|
"analysisDate": "Analysis result date",
|
||||||
|
"results": {
|
||||||
|
"range": {
|
||||||
|
"normal": "Normal range"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,9 @@
|
|||||||
"error": "Failed to update location"
|
"error": "Failed to update location"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"order": {
|
||||||
|
"title": "Order"
|
||||||
|
},
|
||||||
"orderConfirmed": {
|
"orderConfirmed": {
|
||||||
"title": "Order confirmed",
|
"title": "Order confirmed",
|
||||||
"summary": "Summary",
|
"summary": "Summary",
|
||||||
|
|||||||
@@ -4,6 +4,18 @@
|
|||||||
"table": {
|
"table": {
|
||||||
"analysisPackage": "Analysis package",
|
"analysisPackage": "Analysis package",
|
||||||
"otherOrders": "Order",
|
"otherOrders": "Order",
|
||||||
"createdAt": "Ordered at"
|
"createdAt": "Ordered at",
|
||||||
|
"status": "Status"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"QUEUED": "Waiting to send to lab",
|
||||||
|
"ON_HOLD": "Waiting for analysis results",
|
||||||
|
"PROCESSING": "In progress",
|
||||||
|
"PARTIAL_ANALYSIS_RESPONSE": "Partial analysis response",
|
||||||
|
"FULL_ANALYSIS_RESPONSE": "All analysis responses",
|
||||||
|
"WAITING_FOR_DOCTOR_RESPONSE": "Waiting for doctor response",
|
||||||
|
"COMPLETED": "Completed",
|
||||||
|
"REJECTED": "Rejected",
|
||||||
|
"CANCELLED": "Cancelled"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,11 +145,5 @@
|
|||||||
"successTitle": "Tere, {{firstName}} {{lastName}}",
|
"successTitle": "Tere, {{firstName}} {{lastName}}",
|
||||||
"successDescription": "Teie tervisekonto on aktiveeritud ja kasutamiseks valmis!",
|
"successDescription": "Teie tervisekonto on aktiveeritud ja kasutamiseks valmis!",
|
||||||
"successButton": "Jätka"
|
"successButton": "Jätka"
|
||||||
},
|
|
||||||
"analysisResults": {
|
|
||||||
"pageTitle": "Minu analüüside vastused",
|
|
||||||
"description": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad:",
|
|
||||||
"descriptionEmpty": "Kui oled juba käinud analüüse andmas, siis varsti jõuavad siia sinu analüüside vastused.",
|
|
||||||
"orderNewAnalysis": "Telli uued analüüsid"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
public/locales/et/analysis-results.json
Normal file
14
public/locales/et/analysis-results.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"pageTitle": "Minu analüüside vastused",
|
||||||
|
"description": "Kõikide analüüside tulemused ilmuvad 1-3 tööpäeva jooksul peale nende andmist.",
|
||||||
|
"descriptionEmpty": "Kui oled juba käinud analüüse andmas, siis varsti jõuavad siia sinu analüüside vastused.",
|
||||||
|
"orderNewAnalysis": "Telli uued analüüsid",
|
||||||
|
"waitingForResults": "Tulemuse ootel",
|
||||||
|
"noAnalysisElements": "Veel ei ole tellitud analüüse",
|
||||||
|
"analysisDate": "Analüüsi vastuse kuupäev",
|
||||||
|
"results": {
|
||||||
|
"range": {
|
||||||
|
"normal": "Normaalne vahemik"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,9 @@
|
|||||||
"error": "Asukoha uuendamine ebaõnnestus"
|
"error": "Asukoha uuendamine ebaõnnestus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"order": {
|
||||||
|
"title": "Tellimus"
|
||||||
|
},
|
||||||
"orderConfirmed": {
|
"orderConfirmed": {
|
||||||
"title": "Tellimus on edukalt esitatud",
|
"title": "Tellimus on edukalt esitatud",
|
||||||
"summary": "Teenused",
|
"summary": "Teenused",
|
||||||
|
|||||||
@@ -4,6 +4,18 @@
|
|||||||
"table": {
|
"table": {
|
||||||
"analysisPackage": "Analüüsi pakett",
|
"analysisPackage": "Analüüsi pakett",
|
||||||
"otherOrders": "Tellimus",
|
"otherOrders": "Tellimus",
|
||||||
"createdAt": "Tellitud"
|
"createdAt": "Tellitud",
|
||||||
|
"status": "Olek"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"QUEUED": "Esitatud",
|
||||||
|
"ON_HOLD": "Makstud",
|
||||||
|
"PROCESSING": "Synlabile edastatud",
|
||||||
|
"PARTIAL_ANALYSIS_RESPONSE": "Osalised tulemused",
|
||||||
|
"FULL_ANALYSIS_RESPONSE": "Kõik tulemused käes",
|
||||||
|
"WAITING_FOR_DOCTOR_RESPONSE": "Ootab arsti kokkuvõtet",
|
||||||
|
"COMPLETED": "Lõplikud tulemused",
|
||||||
|
"REJECTED": "Tagastatud",
|
||||||
|
"CANCELLED": "Tühistatud"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,8 +135,8 @@
|
|||||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||||
|
|
||||||
--breakpoint-xs: 30rem;
|
--breakpoint-xs: 48rem;
|
||||||
--breakpoint-sm: 48rem;
|
--breakpoint-sm: 64rem;
|
||||||
--breakpoint-md: 70rem;
|
--breakpoint-md: 70rem;
|
||||||
--breakpoint-lg: 80rem;
|
--breakpoint-lg: 80rem;
|
||||||
--breakpoint-xl: 96rem;
|
--breakpoint-xl: 96rem;
|
||||||
|
|||||||
24
supabase/migrations/20250811065135_audit_page_views.sql
Normal file
24
supabase/migrations/20250811065135_audit_page_views.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
create table "audit"."page_views" (
|
||||||
|
"id" bigint generated by default as identity not null,
|
||||||
|
"account_id" text not null,
|
||||||
|
"action" text not null,
|
||||||
|
"created_at" timestamp with time zone not null default now(),
|
||||||
|
"changed_by" uuid not null
|
||||||
|
);
|
||||||
|
|
||||||
|
grant usage on schema audit to authenticated;
|
||||||
|
grant select, insert, update, delete on table audit.page_views to authenticated;
|
||||||
|
|
||||||
|
alter table "audit"."page_views" enable row level security;
|
||||||
|
|
||||||
|
create policy "insert_own"
|
||||||
|
on "audit"."page_views"
|
||||||
|
as permissive
|
||||||
|
for insert
|
||||||
|
to authenticated
|
||||||
|
with check (auth.uid() = changed_by);
|
||||||
|
|
||||||
|
create policy "service_role_select" on "audit"."page_views" for select to service_role using (true);
|
||||||
|
create policy "service_role_insert" on "audit"."page_views" for insert to service_role with check (true);
|
||||||
|
create policy "service_role_update" on "audit"."page_views" for update to service_role using (true);
|
||||||
|
create policy "service_role_delete" on "audit"."page_views" for delete to service_role using (true);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
alter type medreport.analysis_order_status add value 'PARTIAL_ANALYSIS_RESPONSE';
|
||||||
|
alter type medreport.analysis_order_status add value 'FULL_ANALYSIS_RESPONSE';
|
||||||
|
alter type medreport.analysis_order_status add value 'WAITING_FOR_DOCTOR_RESPONSE';
|
||||||
@@ -5,7 +5,7 @@ export const getAnalysisElementMedusaProductIds = (products: ({ metadata?: { ana
|
|||||||
|
|
||||||
const mapped = products
|
const mapped = products
|
||||||
.flatMap((product) => {
|
.flatMap((product) => {
|
||||||
const value = product?.metadata?.analysisElementMedusaProductIds;
|
const value = product?.metadata?.analysisElementMedusaProductIds?.replaceAll("'", '"');
|
||||||
try {
|
try {
|
||||||
return JSON.parse(value as string);
|
return JSON.parse(value as string);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user