feat(MED-105): update analysis results view to be by analysis order

This commit is contained in:
2025-08-14 12:10:12 +03:00
parent d3b393156a
commit 1285b02f9c
11 changed files with 140 additions and 59 deletions

View File

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

View File

@@ -19,7 +19,7 @@ export async function POST(request: Request) {
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id }); const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
const orderedAnalysisElementsIds = await getOrderedAnalysisElementsIds({ medusaOrder }); 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 idsToSend = typeof maxItems === 'number' ? orderedAnalysisElementsIds.slice(0, maxItems) : orderedAnalysisElementsIds;
const messageXml = await composeOrderTestResponseXML({ const messageXml = await composeOrderTestResponseXML({
person: { person: {

View File

@@ -11,10 +11,11 @@ import { loadUserAnalysis } from '../../_lib/server/load-user-analysis';
import Analysis from './_components/analysis'; import Analysis from './_components/analysis';
import pathsConfig from '~/config/paths.config'; import pathsConfig from '~/config/paths.config';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { getAnalysisOrders } from '~/lib/services/order.service'; import { AnalysisOrder, getAnalysisOrders } from '~/lib/services/order.service';
import { getAnalysisElements } from '~/lib/services/analysis-element.service'; import { getAnalysisElements } from '~/lib/services/analysis-element.service';
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account'; import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
import { createPageViewLog } from '~/lib/services/audit/pageView.service'; import { createPageViewLog } from '~/lib/services/audit/pageView.service';
import { ButtonTooltip } from '~/components/ui/button-tooltip';
export const generateMetadata = async () => { export const generateMetadata = async () => {
const i18n = await createI18nServerInstance(); const i18n = await createI18nServerInstance();
@@ -45,25 +46,15 @@ async function AnalysisResultsPage() {
action: 'VIEW_ANALYSIS_RESULTS', action: 'VIEW_ANALYSIS_RESULTS',
}); });
const analysisElementIds = [ const getAnalysisElementIds = (analysisOrders: AnalysisOrder[]) => [
...new Set(analysisOrders?.flatMap((order) => order.analysis_element_ids).filter(Boolean) as number[]), ...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 ( return (
<PageBody> <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 className="mt-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-0">
<div> <div>
<h4> <h4>
@@ -83,25 +74,47 @@ async function AnalysisResultsPage() {
</Link> </Link>
</Button> </Button>
</div> </div>
<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 (
<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"> <div className="flex flex-col gap-2">
{analysisElementsWithResults.map(({ results }) => { {analysisElementsForOrder.length > 0 ? analysisElementsForOrder.map((analysisElement) => {
const analysisElement = analysisElements.find((element) => element.analysis_id_original === results.analysis_element_original_id); const results = analysisResponseElements?.find((element) => element.analysis_element_original_id === analysisElement.analysis_id_original);
if (!analysisElement) { if (!results) {
return null; return (
<Analysis key={`${analysisOrder.id}-${analysisElement.id}`} analysisElement={analysisElement} />
);
} }
return ( return (
<Analysis key={results.id} analysisElement={analysisElement} results={results} /> <Analysis key={`${analysisOrder.id}-${analysisElement.id}`} analysisElement={analysisElement} results={results} />
); );
})} }) : (
{analysisElementsWithoutResults.map((element) => (
<Analysis key={element.analysis_id_original} analysisElement={element} />
))}
{hasNoAnalysisElements && (
<div className="text-muted-foreground text-sm"> <div className="text-muted-foreground text-sm">
<Trans i18nKey="analysis-results:noAnalysisElements" /> <Trans i18nKey="analysis-results:noAnalysisElements" />
</div> </div>
)} )}
</div> </div>
</div>
);
}) : (
<div className="text-muted-foreground text-sm">
<Trans i18nKey="analysis-results:noAnalysisOrders" />
</div>
)}
</div>
</PageBody> </PageBody>
); );
} }

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { use, useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';

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, updateOrder } from './order.service'; import { getOrder, updateOrderStatus } 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';
@@ -218,24 +218,30 @@ export async function readPrivateMessageResponse({
privateMessage.messageId, privateMessage.messageId,
); );
const messageResponse = privateMessageContent?.Saadetis?.Vastus; const messageResponse = privateMessageContent?.Saadetis?.Vastus;
const medusaOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId;
if (!messageResponse) { 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'>; let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
try { try {
order = await getOrder({ medusaOrderId: messageResponse.ValisTellimuseId }); order = await getOrder({ medusaOrderId });
} catch (e) { } catch (e) {
await deletePrivateMessage(privateMessage.messageId); 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 }); const status = await syncPrivateMessage({ messageResponse, order });
if (status === 'COMPLETED') { if (status.isPartial) {
await updateOrder({ orderId: order.id, orderStatus: 'FULL_ANALYSIS_RESPONSE' }); 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); await deletePrivateMessage(privateMessage.messageId);
messageIdProcessed = privateMessage.messageId; messageIdProcessed = privateMessage.messageId;
} }
@@ -559,11 +565,11 @@ function getLatestMessage({
); );
} }
export async function syncPrivateMessage({ async function syncPrivateMessage({
messageResponse, messageResponse,
order, order,
}: { }: {
messageResponse: MedipostOrderResponse['Saadetis']['Vastus']; messageResponse: NonNullable<MedipostOrderResponse['Saadetis']['Vastus']>;
order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
}) { }) {
const supabase = getSupabaseServerAdminClient() const supabase = getSupabaseServerAdminClient()
@@ -606,6 +612,9 @@ export async function syncPrivateMessage({
Tables<{ schema: 'medreport' }, 'analysis_response_elements'>, Tables<{ schema: 'medreport' }, 'analysis_response_elements'>,
'id' | 'created_at' | 'updated_at' 'id' | 'created_at' | 'updated_at'
>[] = []; >[] = [];
const analysisResponseId = analysisResponse[0]!.id;
for (const analysisGroup of analysisGroups) { for (const analysisGroup of analysisGroups) {
const groupItems = toArray( const groupItems = toArray(
analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'], analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'],
@@ -618,7 +627,7 @@ export async function syncPrivateMessage({
responses.push( responses.push(
...elementAnalysisResponses.map((response) => ({ ...elementAnalysisResponses.map((response) => ({
analysis_element_original_id: element.UuringId, analysis_element_original_id: element.UuringId,
analysis_response_id: analysisResponse[0]!.id, analysis_response_id: analysisResponseId,
norm_lower: response.NormAlum?.['#text'] ?? null, norm_lower: response.NormAlum?.['#text'] ?? null,
norm_lower_included: norm_lower_included:
response.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah', response.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah',
@@ -640,11 +649,11 @@ export async function syncPrivateMessage({
.schema('medreport') .schema('medreport')
.from('analysis_response_elements') .from('analysis_response_elements')
.delete() .delete()
.eq('analysis_response_id', analysisResponse[0].id); .eq('analysis_response_id', analysisResponseId);
if (deleteError) { if (deleteError) {
throw new Error( 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) { if (elementInsertError) {
throw new Error( 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); const { data: allOrderResponseElements} = await supabase
return AnalysisOrderStatus[messageResponse.TellimuseOlek]; .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({ export async function sendOrderToMedipost({
@@ -688,7 +708,7 @@ export async function sendOrderToMedipost({
}); });
await sendPrivateMessage(orderXml); await sendPrivateMessage(orderXml);
await updateOrder({ orderId: medreportOrder.id, orderStatus: 'PROCESSING' }); await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
} }
export async function getOrderedAnalysisElementsIds({ export async function getOrderedAnalysisElementsIds({
@@ -720,7 +740,7 @@ export async function getOrderedAnalysisElementsIds({
countryCode, countryCode,
queryParams: { limit: 100, id: orderedPackageIds }, 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) { if (orderedPackagesProducts.length !== orderedPackageIds.length) {
throw new Error(`Got ${orderedPackagesProducts.length} ordered packages products, expected ${orderedPackageIds.length}`); throw new Error(`Got ${orderedPackagesProducts.length} ordered packages products, expected ${orderedPackageIds.length}`);
} }

View File

@@ -56,6 +56,30 @@ export async function updateOrder({
.throwOnError(); .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({ export async function getOrder({
medusaOrderId, medusaOrderId,
orderId, orderId,
@@ -91,6 +115,6 @@ export async function getAnalysisOrders({
if (orderStatus) { if (orderStatus) {
query.eq('status', orderStatus); query.eq('status', orderStatus);
} }
const orders = await query.throwOnError(); const orders = await query.order('created_at', { ascending: false }).throwOnError();
return orders.data; return orders.data;
} }

View File

@@ -202,7 +202,7 @@ export type MedipostOrderResponse = IMedipostResponseXMLBase & {
SaadetisId: string; SaadetisId: string;
Email: string; Email: string;
}; };
Vastus: { Vastus?: {
ValisTellimuseId: string; ValisTellimuseId: string;
Asutus: { Asutus: {
'@_tyyp': string; // TEOSTAJA '@_tyyp': string; // TEOSTAJA
@@ -246,6 +246,9 @@ export type MedipostOrderResponse = IMedipostResponseXMLBase & {
TellimuseOlek: keyof typeof AnalysisOrderStatus; TellimuseOlek: keyof typeof AnalysisOrderStatus;
UuringuGrupp?: ResponseUuringuGrupp | ResponseUuringuGrupp[]; UuringuGrupp?: ResponseUuringuGrupp | ResponseUuringuGrupp[];
}; };
Tellimus?: {
ValisTellimuseId: string;
}
}; };
}; };

View File

@@ -18,3 +18,12 @@ export function toTitleCase(str?: string) {
text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(), 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();
});
}

View File

@@ -1819,6 +1819,22 @@ export type Database = {
} }
Returns: undefined 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: { upsert_order: {
Args: { Args: {
target_account_id: string target_account_id: string

View File

@@ -5,10 +5,12 @@
"orderNewAnalysis": "Order new analyses", "orderNewAnalysis": "Order new analyses",
"waitingForResults": "Waiting for results", "waitingForResults": "Waiting for results",
"noAnalysisElements": "No analysis orders found", "noAnalysisElements": "No analysis orders found",
"noAnalysisOrders": "No analysis orders found",
"analysisDate": "Analysis result date", "analysisDate": "Analysis result date",
"results": { "results": {
"range": { "range": {
"normal": "Normal range" "normal": "Normal range"
} }
} },
"orderTitle": "Order number {{orderNumber}}"
} }

View File

@@ -5,10 +5,12 @@
"orderNewAnalysis": "Telli uued analüüsid", "orderNewAnalysis": "Telli uued analüüsid",
"waitingForResults": "Tulemuse ootel", "waitingForResults": "Tulemuse ootel",
"noAnalysisElements": "Veel ei ole tellitud analüüse", "noAnalysisElements": "Veel ei ole tellitud analüüse",
"noAnalysisOrders": "Veel ei ole analüüside tellimusi",
"analysisDate": "Analüüsi vastuse kuupäev", "analysisDate": "Analüüsi vastuse kuupäev",
"results": { "results": {
"range": { "range": {
"normal": "Normaalne vahemik" "normal": "Normaalne vahemik"
} }
} },
"orderTitle": "Tellimus {{orderNumber}}"
} }