Merge pull request #49 from MR-medreport/MED-105-v3
feat(MED-105): update analysis results view to be by analysis order, create notifications entry on email
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { sendOrderToMedipost } from "~/lib/services/medipost.service";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
const { medusaOrderId } = (await request.json()) as { medusaOrderId: string };
|
||||
await sendOrderToMedipost({ medusaOrderId });
|
||||
return NextResponse.json({ success: true });
|
||||
};
|
||||
@@ -11,7 +11,7 @@ export async function POST(request: Request) {
|
||||
// return NextResponse.json({ error: 'This endpoint is only available in development mode' }, { status: 403 });
|
||||
// }
|
||||
|
||||
const { medusaOrderId } = await request.json();
|
||||
const { medusaOrderId, maxItems = null } = await request.json();
|
||||
|
||||
const medusaOrder = await retrieveOrder(medusaOrderId)
|
||||
const medreportOrder = await getOrder({ medusaOrderId });
|
||||
@@ -19,7 +19,8 @@ export async function POST(request: Request) {
|
||||
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
|
||||
const orderedAnalysisElementsIds = await getOrderedAnalysisElementsIds({ medusaOrder });
|
||||
|
||||
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`);
|
||||
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} (${maxItems ?? 'all'}) ordered analysis elements`);
|
||||
const idsToSend = typeof maxItems === 'number' ? orderedAnalysisElementsIds.slice(0, maxItems) : orderedAnalysisElementsIds;
|
||||
const messageXml = await composeOrderTestResponseXML({
|
||||
person: {
|
||||
idCode: account.personal_code!,
|
||||
@@ -27,7 +28,7 @@ export async function POST(request: Request) {
|
||||
lastName: account.last_name ?? '',
|
||||
phone: account.phone ?? '',
|
||||
},
|
||||
orderedAnalysisElementsIds: orderedAnalysisElementsIds.map(({ analysisElementId }) => analysisElementId),
|
||||
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId),
|
||||
orderedAnalysesIds: [],
|
||||
orderId: medusaOrderId,
|
||||
orderCreatedAt: new Date(medreportOrder.created_at),
|
||||
|
||||
@@ -15,10 +15,11 @@ import {
|
||||
PAGE_VIEW_ACTION,
|
||||
createPageViewLog,
|
||||
} from '~/lib/services/audit/pageView.service';
|
||||
import { getAnalysisOrders } from '~/lib/services/order.service';
|
||||
import { AnalysisOrder, getAnalysisOrders } from '~/lib/services/order.service';
|
||||
import { ButtonTooltip } from '~/components/ui/button-tooltip';
|
||||
|
||||
import { loadUserAnalysis } from '../../_lib/server/load-user-analysis';
|
||||
import Analysis from './_components/analysis';
|
||||
import { loadUserAnalysis } from '../../_lib/server/load-user-analysis';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const i18n = await createI18nServerInstance();
|
||||
@@ -51,43 +52,16 @@ async function AnalysisResultsPage() {
|
||||
action: PAGE_VIEW_ACTION.VIEW_ANALYSIS_RESULTS,
|
||||
});
|
||||
|
||||
const analysisElementIds = [
|
||||
...new Set(
|
||||
analysisOrders
|
||||
?.flatMap((order) => order.analysis_element_ids)
|
||||
.filter(Boolean) as number[],
|
||||
),
|
||||
const getAnalysisElementIds = (analysisOrders: AnalysisOrder[]) => [
|
||||
...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;
|
||||
const analysisElementIds = getAnalysisElementIds(analysisOrders);
|
||||
const analysisElements = await getAnalysisElements({ ids: analysisElementIds });
|
||||
|
||||
return (
|
||||
<PageBody>
|
||||
<div className="mt-8 flex flex-col justify-between gap-4 sm:flex-row sm:items-center sm:gap-0">
|
||||
<PageBody className="gap-4">
|
||||
<div className="mt-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-0">
|
||||
<div>
|
||||
<h4>
|
||||
<Trans i18nKey="analysis-results:pageTitle" />
|
||||
@@ -106,33 +80,44 @@ async function AnalysisResultsPage() {
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{analysisElementsWithResults.map(({ results }) => {
|
||||
const analysisElement = analysisElements.find(
|
||||
(element) =>
|
||||
element.analysis_id_original ===
|
||||
results.analysis_element_original_id,
|
||||
);
|
||||
if (!analysisElement) {
|
||||
return null;
|
||||
}
|
||||
<div className="flex flex-col gap-8">
|
||||
{analysisOrders.length > 0 && analysisElements.length > 0 ? analysisOrders.map((analysisOrder) => {
|
||||
const analysisElementIds = getAnalysisElementIds([analysisOrder]);
|
||||
const analysisElementsForOrder = analysisElements.filter((element) => analysisElementIds.includes(element.id));
|
||||
return (
|
||||
<Analysis
|
||||
key={results.id}
|
||||
analysisElement={analysisElement}
|
||||
results={results}
|
||||
/>
|
||||
<div key={analysisOrder.id} className="flex flex-col gap-4">
|
||||
<h4>
|
||||
<Trans i18nKey="analysis-results:orderTitle" values={{ orderNumber: analysisOrder.medusa_order_id }} />
|
||||
</h4>
|
||||
<h5>
|
||||
<Trans i18nKey={`orders:status.${analysisOrder.status}`} />
|
||||
<ButtonTooltip
|
||||
content={`${new Date(analysisOrder.created_at).toLocaleString()}`}
|
||||
className="ml-6"
|
||||
/>
|
||||
</h5>
|
||||
<div className="flex flex-col gap-2">
|
||||
{analysisElementsForOrder.length > 0 ? analysisElementsForOrder.map((analysisElement) => {
|
||||
const results = analysisResponseElements?.find((element) => element.analysis_element_original_id === analysisElement.analysis_id_original);
|
||||
if (!results) {
|
||||
return (
|
||||
<Analysis key={`${analysisOrder.id}-${analysisElement.id}`} analysisElement={analysisElement} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Analysis key={`${analysisOrder.id}-${analysisElement.id}`} analysisElement={analysisElement} results={results} />
|
||||
);
|
||||
}) : (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<Trans i18nKey="analysis-results:noAnalysisElements" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{analysisElementsWithoutResults.map((element) => (
|
||||
<Analysis
|
||||
key={element.analysis_id_original}
|
||||
analysisElement={element}
|
||||
/>
|
||||
))}
|
||||
{hasNoAnalysisElements && (
|
||||
}) : (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<Trans i18nKey="analysis-results:noAnalysisElements" />
|
||||
<Trans i18nKey="analysis-results:noAnalysisOrders" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,9 @@ import { placeOrder, retrieveCart } from "@lib/data/cart";
|
||||
import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
|
||||
import { createOrder } from '~/lib/services/order.service';
|
||||
import { getOrderedAnalysisElementsIds, sendOrderToMedipost } from '~/lib/services/medipost.service';
|
||||
import { createNotificationsApi } from '@kit/notifications/api';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
import { AccountWithParams } from '@kit/accounts/api';
|
||||
|
||||
const ANALYSIS_PACKAGES_TYPE_HANDLE = 'analysis-packages';
|
||||
const MONTONIO_PAID_STATUS = 'PAID';
|
||||
@@ -31,7 +34,22 @@ const env = () => z
|
||||
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
|
||||
});
|
||||
|
||||
const sendEmail = async ({ email, analysisPackageName, personName, partnerLocationName, language }: { email: string, analysisPackageName: string, personName: string, partnerLocationName: string, language: string }) => {
|
||||
const sendEmail = async ({
|
||||
account,
|
||||
email,
|
||||
analysisPackageName,
|
||||
personName,
|
||||
partnerLocationName,
|
||||
language,
|
||||
}: {
|
||||
account: AccountWithParams,
|
||||
email: string,
|
||||
analysisPackageName: string,
|
||||
personName: string,
|
||||
partnerLocationName: string,
|
||||
language: string,
|
||||
}) => {
|
||||
const client = getSupabaseServerAdminClient();
|
||||
try {
|
||||
const { renderSynlabAnalysisPackageEmail } = await import('@kit/email-templates');
|
||||
const { getMailer } = await import('@kit/mailers');
|
||||
@@ -55,6 +73,11 @@ const sendEmail = async ({ email, analysisPackageName, personName, partnerLocati
|
||||
.catch((error) => {
|
||||
throw new Error(`Failed to send email, message=${error}`);
|
||||
});
|
||||
await createNotificationsApi(client)
|
||||
.createNotification({
|
||||
account_id: account.id,
|
||||
body: html,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to send email, message=${error}`);
|
||||
}
|
||||
@@ -79,7 +102,7 @@ export async function processMontonioCallback(orderToken: string) {
|
||||
}
|
||||
|
||||
try {
|
||||
const [,, cartId] = decoded.merchantReferenceDisplay.split(':');
|
||||
const [, , cartId] = decoded.merchantReferenceDisplay.split(':');
|
||||
if (!cartId) {
|
||||
throw new Error("Cart ID not found");
|
||||
}
|
||||
@@ -89,6 +112,7 @@ export async function processMontonioCallback(orderToken: string) {
|
||||
throw new Error("Cart not found");
|
||||
}
|
||||
|
||||
|
||||
const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false });
|
||||
const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder });
|
||||
const orderId = await createOrder({ medusaOrder, orderedAnalysisElements });
|
||||
@@ -110,7 +134,7 @@ export async function processMontonioCallback(orderToken: string) {
|
||||
|
||||
if (email && analysisPackageName) {
|
||||
try {
|
||||
await sendEmail({ email, analysisPackageName, personName, partnerLocationName, language });
|
||||
await sendEmail({ account, email, analysisPackageName, personName, partnerLocationName, language });
|
||||
} catch (error) {
|
||||
console.error("Failed to send email", error);
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@ import { PageBody } from '@kit/ui/makerkit/page';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
||||
import OrdersTable from '../../_components/orders/orders-table';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import type { IOrderLineItem } from '../../_components/orders/types';
|
||||
import { getAnalysisOrders } from '~/lib/services/order.service';
|
||||
import OrderBlock from '../../_components/orders/order-block';
|
||||
import React from 'react';
|
||||
import { Divider } from '@medusajs/ui';
|
||||
|
||||
export async function generateMetadata() {
|
||||
const { t } = await createI18nServerInstance();
|
||||
@@ -29,42 +30,7 @@ async function OrdersPage() {
|
||||
redirect(pathsConfig.auth.signIn);
|
||||
}
|
||||
|
||||
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages');
|
||||
const analysisPackageOrders: IOrderLineItem[] = medusaOrders.flatMap(({ id, items, payment_status, fulfillment_status }) => items
|
||||
?.filter((item) => item.product_type_id === analysisPackagesType?.id)
|
||||
.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[] = medusaOrders
|
||||
.filter(({ items }) => items?.some((item) => item.product_type_id !== analysisPackagesType?.id))
|
||||
.flatMap(({ id, items, payment_status, fulfillment_status }) => items
|
||||
?.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)
|
||||
|| []);
|
||||
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages')!;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -73,8 +39,27 @@ async function OrdersPage() {
|
||||
description={<Trans i18nKey={'orders:description'} />}
|
||||
/>
|
||||
<PageBody>
|
||||
<OrdersTable orderItems={analysisPackageOrders} title="orders:table.analysisPackage" />
|
||||
<OrdersTable orderItems={otherOrders} title="orders:table.otherOrders" />
|
||||
{analysisOrders.map((analysisOrder) => {
|
||||
const medusaOrder = medusaOrders.find(({ id }) => id === analysisOrder.medusa_order_id);
|
||||
if (!medusaOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const medusaOrderItems = medusaOrder.items || [];
|
||||
const medusaOrderItemsAnalysisPackages = medusaOrderItems.filter((item) => item.product_type_id === analysisPackagesType?.id);
|
||||
const medusaOrderItemsOther = medusaOrderItems.filter((item) => item.product_type_id !== analysisPackagesType?.id);
|
||||
|
||||
return (
|
||||
<React.Fragment key={analysisOrder.id}>
|
||||
<Divider className="my-6" />
|
||||
<OrderBlock
|
||||
analysisOrder={analysisOrder}
|
||||
itemsAnalysisPackage={medusaOrderItemsAnalysisPackages}
|
||||
itemsOther={medusaOrderItemsOther}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</PageBody>
|
||||
</>
|
||||
);
|
||||
|
||||
36
app/home/(user)/_components/orders/order-block.tsx
Normal file
36
app/home/(user)/_components/orders/order-block.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { AnalysisOrder } from "~/lib/services/order.service";
|
||||
import { Trans } from '@kit/ui/makerkit/trans';
|
||||
import { StoreOrderLineItem } from "@medusajs/types";
|
||||
import OrderItemsTable from "./order-items-table";
|
||||
import Link from "next/link";
|
||||
import { Eye } from "lucide-react";
|
||||
|
||||
export default function OrderBlock({ analysisOrder, itemsAnalysisPackage, itemsOther }: {
|
||||
analysisOrder: AnalysisOrder,
|
||||
itemsAnalysisPackage: StoreOrderLineItem[],
|
||||
itemsOther: StoreOrderLineItem[],
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h4>
|
||||
<Trans i18nKey="analysis-results:orderTitle" values={{ orderNumber: analysisOrder.medusa_order_id }} />
|
||||
</h4>
|
||||
<div className="flex gap-2">
|
||||
<h5>
|
||||
<Trans i18nKey={`orders:status.${analysisOrder.status}`} />
|
||||
</h5>
|
||||
<Link href={`/home/order/${analysisOrder.id}`} className="flex items-center justify-between text-small-regular">
|
||||
<button
|
||||
className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer"
|
||||
>
|
||||
<Eye />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<OrderItemsTable items={itemsAnalysisPackage} title="orders:table.analysisPackage" analysisOrder={analysisOrder} />
|
||||
<OrderItemsTable items={itemsOther} title="orders:table.otherOrders" analysisOrder={analysisOrder} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
77
app/home/(user)/_components/orders/order-items-table.tsx
Normal file
77
app/home/(user)/_components/orders/order-items-table.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
} from '@kit/ui/table';
|
||||
import { StoreOrderLineItem } from "@medusajs/types";
|
||||
import { AnalysisOrder } from '~/lib/services/order.service';
|
||||
import { formatDate } from 'date-fns';
|
||||
import Link from 'next/link';
|
||||
import { Eye } from 'lucide-react';
|
||||
|
||||
export default function OrderItemsTable({ items, title, analysisOrder }: {
|
||||
items: StoreOrderLineItem[];
|
||||
title: string;
|
||||
analysisOrder: AnalysisOrder;
|
||||
}) {
|
||||
if (!items || items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table className="rounded-lg border border-separate">
|
||||
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
||||
<TableRow>
|
||||
<TableHead className="px-6">
|
||||
<Trans i18nKey={title} />
|
||||
</TableHead>
|
||||
<TableHead className="px-6">
|
||||
<Trans i18nKey="orders:table.createdAt" />
|
||||
</TableHead>
|
||||
<TableHead className="px-6">
|
||||
<Trans i18nKey="orders:table.status" />
|
||||
</TableHead>
|
||||
<TableHead className="px-6">
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items
|
||||
.sort((a, b) => (a.created_at ?? "") > (b.created_at ?? "") ? -1 : 1)
|
||||
.map((orderItem) => (
|
||||
<TableRow className="w-full" key={orderItem.id}>
|
||||
<TableCell className="text-left w-[100%] px-6">
|
||||
<p className="txt-medium-plus text-ui-fg-base">
|
||||
{orderItem.product_title}
|
||||
</p>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="px-6 whitespace-nowrap">
|
||||
{formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="px-6 min-w-[180px]">
|
||||
<Trans i18nKey={`orders:status.${analysisOrder.status}`} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right px-6">
|
||||
<span className="flex gap-x-1 justify-end w-[30px]">
|
||||
<Link href={`/home/analysis-results`} className="flex items-center justify-between text-small-regular">
|
||||
<button
|
||||
className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer"
|
||||
>
|
||||
<Eye />
|
||||
</button>
|
||||
</Link>
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import {
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from '@kit/ui/table';
|
||||
import { Eye } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { formatDate } from "date-fns";
|
||||
import { IOrderLineItem } from "./types";
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export default function OrdersItem({ orderItem }: {
|
||||
orderItem: IOrderLineItem,
|
||||
}) {
|
||||
return (
|
||||
<TableRow className="w-full">
|
||||
<TableCell className="text-left w-[100%] px-6">
|
||||
<p className="txt-medium-plus text-ui-fg-base">
|
||||
{orderItem.item.product_title}
|
||||
</p>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="px-6 whitespace-nowrap">
|
||||
{formatDate(orderItem.item.created_at, 'dd.MM.yyyy HH:mm')}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="px-6 whitespace-nowrap">
|
||||
<Trans i18nKey={`orders:status.${orderItem.orderStatus}`} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right px-6">
|
||||
<span className="flex gap-x-1 justify-end w-[60px]">
|
||||
<Link href={`/home/order/${orderItem.orderId}`} className="flex items-center justify-between text-small-regular">
|
||||
<button
|
||||
className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer"
|
||||
>
|
||||
<Eye />
|
||||
</button>
|
||||
</Link>
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
} from '@kit/ui/table';
|
||||
import OrdersItem from "./orders-item";
|
||||
import { IOrderLineItem } from "./types";
|
||||
|
||||
export default function OrdersTable({ orderItems, title }: {
|
||||
orderItems: IOrderLineItem[];
|
||||
title: string;
|
||||
}) {
|
||||
if (!orderItems || orderItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table className="rounded-lg border border-separate">
|
||||
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
||||
<TableRow>
|
||||
<TableHead className="px-6">
|
||||
<Trans i18nKey={title} />
|
||||
</TableHead>
|
||||
<TableHead className="px-6">
|
||||
<Trans i18nKey="orders:table.createdAt" />
|
||||
</TableHead>
|
||||
<TableHead className="px-6">
|
||||
<Trans i18nKey="orders:table.status" />
|
||||
</TableHead>
|
||||
<TableHead className="px-6">
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{orderItems
|
||||
.sort((a, b) => (a.item.created_at ?? "") > (b.item.created_at ?? "") ? -1 : 1)
|
||||
.map((orderItem) => (<OrdersItem key={orderItem.item.id} orderItem={orderItem} />))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { StoreOrderLineItem } from "@medusajs/types";
|
||||
|
||||
export interface IOrderLineItem {
|
||||
item: StoreOrderLineItem;
|
||||
medusaOrderId: string;
|
||||
orderId: number;
|
||||
orderStatus: string;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { use, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
@@ -36,7 +36,7 @@ import { uniqBy } from 'lodash';
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
import { createAnalysisGroup } from './analysis-group.service';
|
||||
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
||||
import { getOrder, updateOrder } from './order.service';
|
||||
import { getOrder, updateOrderStatus } from './order.service';
|
||||
import { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service';
|
||||
import { getAnalyses } from './analyses.service';
|
||||
import { getAccountAdmin } from './account.service';
|
||||
@@ -218,24 +218,30 @@ export async function readPrivateMessageResponse({
|
||||
privateMessage.messageId,
|
||||
);
|
||||
const messageResponse = privateMessageContent?.Saadetis?.Vastus;
|
||||
const medusaOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId;
|
||||
|
||||
if (!messageResponse) {
|
||||
throw new Error(`Private message response has no results yet`);
|
||||
if (medusaOrderId === 'order_01K2JSJXR5XVNRWEAGB199RCKP') {
|
||||
console.info("messageResponse", JSON.stringify(privateMessageContent, null, 2));
|
||||
}
|
||||
throw new Error(`Private message response has no results yet for order=${medusaOrderId}`);
|
||||
}
|
||||
console.info(`Private message content: ${JSON.stringify(privateMessageContent)}`);
|
||||
|
||||
let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||
try {
|
||||
order = await getOrder({ medusaOrderId: messageResponse.ValisTellimuseId });
|
||||
order = await getOrder({ medusaOrderId });
|
||||
} catch (e) {
|
||||
await deletePrivateMessage(privateMessage.messageId);
|
||||
throw new Error(`Order not found by Medipost message ValisTellimuseId=${messageResponse.ValisTellimuseId}`);
|
||||
throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`);
|
||||
}
|
||||
|
||||
const status = await syncPrivateMessage({ messageResponse, order });
|
||||
|
||||
if (status === 'COMPLETED') {
|
||||
await updateOrder({ orderId: order.id, orderStatus: 'FULL_ANALYSIS_RESPONSE' });
|
||||
if (status.isPartial) {
|
||||
await updateOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' });
|
||||
messageIdProcessed = privateMessage.messageId;
|
||||
} else if (status.isCompleted) {
|
||||
await updateOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' });
|
||||
await deletePrivateMessage(privateMessage.messageId);
|
||||
messageIdProcessed = privateMessage.messageId;
|
||||
}
|
||||
@@ -559,11 +565,11 @@ function getLatestMessage({
|
||||
);
|
||||
}
|
||||
|
||||
export async function syncPrivateMessage({
|
||||
async function syncPrivateMessage({
|
||||
messageResponse,
|
||||
order,
|
||||
}: {
|
||||
messageResponse: MedipostOrderResponse['Saadetis']['Vastus'];
|
||||
messageResponse: NonNullable<MedipostOrderResponse['Saadetis']['Vastus']>;
|
||||
order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||
}) {
|
||||
const supabase = getSupabaseServerAdminClient()
|
||||
@@ -606,6 +612,9 @@ export async function syncPrivateMessage({
|
||||
Tables<{ schema: 'medreport' }, 'analysis_response_elements'>,
|
||||
'id' | 'created_at' | 'updated_at'
|
||||
>[] = [];
|
||||
|
||||
const analysisResponseId = analysisResponse[0]!.id;
|
||||
|
||||
for (const analysisGroup of analysisGroups) {
|
||||
const groupItems = toArray(
|
||||
analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'],
|
||||
@@ -618,7 +627,7 @@ export async function syncPrivateMessage({
|
||||
responses.push(
|
||||
...elementAnalysisResponses.map((response) => ({
|
||||
analysis_element_original_id: element.UuringId,
|
||||
analysis_response_id: analysisResponse[0]!.id,
|
||||
analysis_response_id: analysisResponseId,
|
||||
norm_lower: response.NormAlum?.['#text'] ?? null,
|
||||
norm_lower_included:
|
||||
response.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah',
|
||||
@@ -640,11 +649,11 @@ export async function syncPrivateMessage({
|
||||
.schema('medreport')
|
||||
.from('analysis_response_elements')
|
||||
.delete()
|
||||
.eq('analysis_response_id', analysisResponse[0].id);
|
||||
.eq('analysis_response_id', analysisResponseId);
|
||||
|
||||
if (deleteError) {
|
||||
throw new Error(
|
||||
`Failed to clean up response elements for response id ${analysisResponse[0].id}`,
|
||||
`Failed to clean up response elements for response id ${analysisResponseId}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -655,12 +664,23 @@ export async function syncPrivateMessage({
|
||||
|
||||
if (elementInsertError) {
|
||||
throw new Error(
|
||||
`Failed to insert order response elements for response id ${analysisResponse[0].id}`,
|
||||
`Failed to insert order response elements for response id ${analysisResponseId}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.info("status", AnalysisOrderStatus[messageResponse.TellimuseOlek], messageResponse.TellimuseOlek);
|
||||
return AnalysisOrderStatus[messageResponse.TellimuseOlek];
|
||||
const { data: allOrderResponseElements} = await supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_response_elements')
|
||||
.select('*')
|
||||
.eq('analysis_response_id', analysisResponseId)
|
||||
.throwOnError();
|
||||
const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0;
|
||||
if (allOrderResponseElements.length !== expectedOrderResponseElements) {
|
||||
return { isPartial: true };
|
||||
}
|
||||
|
||||
const statusFromResponse = AnalysisOrderStatus[messageResponse.TellimuseOlek];
|
||||
return { isCompleted: statusFromResponse === 'COMPLETED' };
|
||||
}
|
||||
|
||||
export async function sendOrderToMedipost({
|
||||
@@ -688,7 +708,7 @@ export async function sendOrderToMedipost({
|
||||
});
|
||||
|
||||
await sendPrivateMessage(orderXml);
|
||||
await updateOrder({ orderId: medreportOrder.id, orderStatus: 'PROCESSING' });
|
||||
await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
|
||||
}
|
||||
|
||||
export async function getOrderedAnalysisElementsIds({
|
||||
@@ -720,7 +740,7 @@ export async function getOrderedAnalysisElementsIds({
|
||||
countryCode,
|
||||
queryParams: { limit: 100, id: orderedPackageIds },
|
||||
});
|
||||
console.info(`Order has ${orderedPackagesProducts.length} packages`);
|
||||
console.info(`Order has ${orderedPackagesProducts.length} packages = ${JSON.stringify(orderedPackageIds, null, 2)}`);
|
||||
if (orderedPackagesProducts.length !== orderedPackageIds.length) {
|
||||
throw new Error(`Got ${orderedPackagesProducts.length} ordered packages products, expected ${orderedPackageIds.length}`);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,30 @@ export async function updateOrder({
|
||||
.throwOnError();
|
||||
}
|
||||
|
||||
export async function updateOrderStatus({
|
||||
orderId,
|
||||
medusaOrderId,
|
||||
orderStatus,
|
||||
}: {
|
||||
orderId?: number;
|
||||
medusaOrderId?: string;
|
||||
orderStatus: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
|
||||
}) {
|
||||
const orderIdParam = orderId;
|
||||
const medusaOrderIdParam = medusaOrderId;
|
||||
if (!orderIdParam && !medusaOrderIdParam) {
|
||||
throw new Error('Either orderId or medusaOrderId must be provided');
|
||||
}
|
||||
await getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.rpc('update_analysis_order_status', {
|
||||
order_id: orderIdParam ?? -1,
|
||||
status_param: orderStatus,
|
||||
medusa_order_id_param: medusaOrderIdParam ?? '',
|
||||
})
|
||||
.throwOnError();
|
||||
}
|
||||
|
||||
export async function getOrder({
|
||||
medusaOrderId,
|
||||
orderId,
|
||||
@@ -91,6 +115,6 @@ export async function getAnalysisOrders({
|
||||
if (orderStatus) {
|
||||
query.eq('status', orderStatus);
|
||||
}
|
||||
const orders = await query.throwOnError();
|
||||
const orders = await query.order('created_at', { ascending: false }).throwOnError();
|
||||
return orders.data;
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ export type MedipostOrderResponse = IMedipostResponseXMLBase & {
|
||||
SaadetisId: string;
|
||||
Email: string;
|
||||
};
|
||||
Vastus: {
|
||||
Vastus?: {
|
||||
ValisTellimuseId: string;
|
||||
Asutus: {
|
||||
'@_tyyp': string; // TEOSTAJA
|
||||
@@ -246,6 +246,9 @@ export type MedipostOrderResponse = IMedipostResponseXMLBase & {
|
||||
TellimuseOlek: keyof typeof AnalysisOrderStatus;
|
||||
UuringuGrupp?: ResponseUuringuGrupp | ResponseUuringuGrupp[];
|
||||
};
|
||||
Tellimus?: {
|
||||
ValisTellimuseId: string;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -18,3 +18,12 @@ export function toTitleCase(str?: string) {
|
||||
text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(),
|
||||
);
|
||||
}
|
||||
|
||||
export function sortByDate<T>(a: T[] | undefined, key: keyof T): T[] | undefined {
|
||||
return a?.sort((a, b) => {
|
||||
if (!a[key] || !b[key]) {
|
||||
return 0;
|
||||
}
|
||||
return new Date(b[key] as string).getTime() - new Date(a[key] as string).getTime();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"subject": "Your Synlab order has been placed - {{analysisPackageName}}",
|
||||
"previewText": "Your Synlab order has been placed - {{analysisPackageName}}",
|
||||
"heading": "Your Synlab order has been placed - {{analysisPackageName}}",
|
||||
"subject": "Your Medreport order has been placed - {{analysisPackageName}}",
|
||||
"previewText": "Your Medreport order has been placed - {{analysisPackageName}}",
|
||||
"heading": "Your Medreport order has been placed - {{analysisPackageName}}",
|
||||
"hello": "Hello {{personName}},",
|
||||
"lines1": "The order for {{analysisPackageName}} analysis package has been sent to the lab. Please go to the lab to collect the sample: SYNLAB - {{partnerLocationName}}",
|
||||
"lines1": "The order for {{analysisPackageName}} analysis package has been sent to the lab. Please go to the lab to collect the sample: Medreport - {{partnerLocationName}}",
|
||||
"lines2": "<i>If you are unable to go to the lab to collect the sample, you can go to any other suitable collection point - <a href=\"https://medreport.ee/et/verevotupunktid\">view locations and opening hours</a>.</i>",
|
||||
"lines3": "It is recommended to collect the sample in the morning (before 12:00) and not to eat or drink (water can be drunk).",
|
||||
"lines4": "At the collection point, select the order from the queue: the order from the doctor.",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"subject": "Teie SYNLAB tellimus on kinnitatud - {{analysisPackageName}}",
|
||||
"previewText": "Teie SYNLAB tellimus on kinnitatud - {{analysisPackageName}}",
|
||||
"heading": "Teie SYNLAB tellimus on kinnitatud - {{analysisPackageName}}",
|
||||
"subject": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
|
||||
"previewText": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
|
||||
"heading": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
|
||||
"hello": "Tere {{personName}},",
|
||||
"lines1": "Saatekiri {{analysisPackageName}} analüüsi uuringuteks on saadetud laborisse digitaalselt. Palun mine proove andma: SYNLAB - {{partnerLocationName}}",
|
||||
"lines1": "Saatekiri {{analysisPackageName}} analüüsi uuringuteks on saadetud laborisse digitaalselt. Palun mine proove andma: Medreport - {{partnerLocationName}}",
|
||||
"lines2": "<i>Kui Teil ei ole võimalik valitud asukohta minna proove andma, siis võite minna endale sobivasse proovivõtupunkti - <a href=\"https://medreport.ee/et/verevotupunktid\">vaata asukohti ja lahtiolekuaegasid</a>.</i>",
|
||||
"lines3": "Soovituslik on proove anda pigem hommikul (enne 12:00) ning söömata ja joomata (vett võib juua).",
|
||||
"lines4": "Proovivõtupunktis valige järjekorrasüsteemis: <strong>saatekirjad</strong> alt <strong>eriarsti saatekiri</strong>.",
|
||||
|
||||
@@ -1828,6 +1828,22 @@ export type Database = {
|
||||
}
|
||||
Returns: undefined
|
||||
}
|
||||
update_analysis_order_status: {
|
||||
Args: {
|
||||
order_id: number
|
||||
medusa_order_id_param: string
|
||||
status_param: Database["medreport"]["Enums"]["analysis_order_status"]
|
||||
}
|
||||
Returns: {
|
||||
analysis_element_ids: number[] | null
|
||||
analysis_ids: number[] | null
|
||||
created_at: string
|
||||
id: number
|
||||
medusa_order_id: string
|
||||
status: Database["medreport"]["Enums"]["analysis_order_status"]
|
||||
user_id: string
|
||||
}
|
||||
}
|
||||
upsert_order: {
|
||||
Args: {
|
||||
target_account_id: string
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
"orderNewAnalysis": "Order new analyses",
|
||||
"waitingForResults": "Waiting for results",
|
||||
"noAnalysisElements": "No analysis orders found",
|
||||
"noAnalysisOrders": "No analysis orders found",
|
||||
"analysisDate": "Analysis result date",
|
||||
"results": {
|
||||
"range": {
|
||||
"normal": "Normal range"
|
||||
}
|
||||
}
|
||||
},
|
||||
"orderTitle": "Order number {{orderNumber}}"
|
||||
}
|
||||
@@ -5,10 +5,12 @@
|
||||
"orderNewAnalysis": "Telli uued analüüsid",
|
||||
"waitingForResults": "Tulemuse ootel",
|
||||
"noAnalysisElements": "Veel ei ole tellitud analüüse",
|
||||
"noAnalysisOrders": "Veel ei ole analüüside tellimusi",
|
||||
"analysisDate": "Analüüsi vastuse kuupäev",
|
||||
"results": {
|
||||
"range": {
|
||||
"normal": "Normaalne vahemik"
|
||||
}
|
||||
}
|
||||
},
|
||||
"orderTitle": "Tellimus {{orderNumber}}"
|
||||
}
|
||||
@@ -12,7 +12,7 @@ function send_medipost_test_response() {
|
||||
curl -X POST "$HOSTNAME/api/order/medipost-test-response" \
|
||||
--header "x-jobs-api-key: $JOBS_API_TOKEN" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{ "medusaOrderId": "'$MEDUSA_ORDER_ID'" }'
|
||||
--data '{ "medusaOrderId": "'$MEDUSA_ORDER_ID'", "maxItems": 2 }'
|
||||
}
|
||||
|
||||
function sync_analysis_results() {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
-- Function "medreport.update_analysis_order_status"
|
||||
-- Update an analysis order status
|
||||
create
|
||||
or replace function medreport.update_analysis_order_status (
|
||||
order_id bigint,
|
||||
medusa_order_id_param text,
|
||||
status_param medreport.analysis_order_status
|
||||
) returns medreport.analysis_orders
|
||||
set
|
||||
search_path = '' as $$
|
||||
declare
|
||||
updated_order medreport.analysis_orders;
|
||||
begin
|
||||
update medreport.analysis_orders
|
||||
set status = status_param
|
||||
where (id = order_id OR medusa_order_id = medusa_order_id_param)
|
||||
returning * into updated_order;
|
||||
|
||||
return updated_order;
|
||||
|
||||
end;
|
||||
|
||||
$$ language plpgsql;
|
||||
|
||||
grant
|
||||
execute on function medreport.update_analysis_order_status (
|
||||
bigint,
|
||||
text,
|
||||
medreport.analysis_order_status
|
||||
) to service_role;
|
||||
|
||||
-- example:
|
||||
-- select medreport.update_analysis_order_status(-1, 'order_01K1TQQHZGPXKDHAH81TDSNGXR', 'CANCELLED')
|
||||
Reference in New Issue
Block a user