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:
2025-08-11 10:45:32 +03:00
committed by GitHub
38 changed files with 542 additions and 227 deletions

View File

@@ -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

View File

@@ -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>
); );
}; };

View File

@@ -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>
); );

View File

@@ -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}`);

View File

@@ -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');

View File

@@ -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" />;
} }

View File

@@ -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);

View 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);

View File

@@ -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 (

View File

@@ -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>

View File

@@ -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>
)
}

View File

@@ -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>
) )
} }

View File

@@ -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">

View File

@@ -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} />)}

View File

@@ -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"
> >

View File

@@ -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>

View File

@@ -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;
} }

View File

@@ -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);
}

View File

@@ -39,6 +39,7 @@ export const defaultI18nNamespaces = [
'order-analysis', 'order-analysis',
'cart', 'cart',
'orders', 'orders',
'analysis-results',
]; ];
/** /**

View File

@@ -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) {

View 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);
}
}

View File

@@ -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({

View File

@@ -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'];

View File

@@ -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[];

View File

@@ -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",

View File

@@ -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>

View File

@@ -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"
} }
} }

View 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"
}
}
}

View File

@@ -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",

View File

@@ -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"
} }
} }

View File

@@ -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"
} }
} }

View 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"
}
}
}

View File

@@ -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",

View File

@@ -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"
} }
} }

View File

@@ -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;

View 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);

View File

@@ -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';

View File

@@ -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) {