Merge branch 'develop' into MED-157

This commit is contained in:
Danel Kungla
2025-09-20 18:44:10 +03:00
27 changed files with 510 additions and 499 deletions

View File

@@ -9,11 +9,7 @@ import { AnalysisElement, createAnalysisElement, getAnalysisElements } from '~/l
import { createCodes } from '~/lib/services/codes.service';
import { getLatestPublicMessageListItem } from '~/lib/services/medipost/medipostPublicMessage.service';
import type { ICode } from '~/lib/types/code';
function toArray<T>(input?: T | T[] | null): T[] {
if (!input) return [];
return Array.isArray(input) ? input : [input];
}
import { toArray } from '@kit/shared/utils';
const WRITE_XML_TO_FILE = false as boolean;

View File

@@ -3,7 +3,7 @@ import { getAnalysisOrder } from "~/lib/services/order.service";
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipost/medipostTest.service";
import { retrieveOrder } from "@lib/data";
import { getAccountAdmin } from "~/lib/services/account.service";
import { createMedipostActionLog } from "~/lib/services/medipost/medipostMessageBase.service";
import { upsertMedipostActionLog } from "~/lib/services/medipost/medipostMessageBase.service";
import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service";
export async function POST(request: Request) {
@@ -35,7 +35,7 @@ export async function POST(request: Request) {
});
try {
await createMedipostActionLog({
await upsertMedipostActionLog({
action: 'send_fake_analysis_results_to_medipost',
xml: messageXml,
medusaOrderId,

View File

@@ -1,3 +1,4 @@
import React from 'react';
import Link from 'next/link';
import { redirect } from 'next/navigation';
@@ -53,47 +54,44 @@ export default async function AnalysisResultsPage({
}
const orderedAnalysisElements = analysisResponse.orderedAnalysisElements;
const hasOrderedAnalysisElements = orderedAnalysisElements.length > 0;
const isPartialStatus = analysisResponse.order.status === 'PARTIAL_ANALYSIS_RESPONSE';
return (
<>
<PageHeader />
<PageBody className="gap-4">
<div className="mt-8 flex flex-col justify-between gap-4 sm:flex-row sm:items-center sm:gap-0">
<div>
<h4>
<Trans i18nKey="analysis-results:pageTitle" />
</h4>
<p className="text-muted-foreground text-sm">
{analysisResponse?.elements &&
analysisResponse.elements?.length > 0 ? (
<Trans i18nKey="analysis-results:description" />
) : (
<Trans i18nKey="analysis-results:descriptionEmpty" />
)}
</p>
</div>
<PageHeader
title={<Trans i18nKey="analysis-results:pageTitle" />}
description={hasOrderedAnalysisElements ? (
isPartialStatus
? <Trans i18nKey="analysis-results:descriptionPartial" />
: <Trans i18nKey="analysis-results:description" />
) : (
<Trans i18nKey="analysis-results:descriptionEmpty" />
)}
>
<div>
<Button asChild>
<Link href={pathsConfig.app.orderAnalysisPackage}>
<Trans i18nKey="analysis-results:orderNewAnalysis" />
</Link>
</Button>
</div>
</PageHeader>
<PageBody className="gap-4 pt-4">
<div className="flex flex-col gap-4">
<h4>
<h5 className="break-all">
<Trans
i18nKey="analysis-results:orderTitle"
values={{ orderNumber: analysisResponse.order.medusaOrderId }}
/>
</h4>
<h5>
<Trans
i18nKey={`orders:status.${analysisResponse.order.status}`}
/>
</h5>
<h6>
<Trans i18nKey={`orders:status.${analysisResponse.order.status}`} />
<ButtonTooltip
content={`${analysisResponse.order.createdAt ? new Date(analysisResponse?.order?.createdAt).toLocaleString() : ''}`}
className="ml-6"
/>
</h5>
</h6>
</div>
{analysisResponse?.summary?.value && (
<div>
@@ -106,7 +104,16 @@ export default async function AnalysisResultsPage({
<div className="flex flex-col gap-2">
{orderedAnalysisElements ? (
orderedAnalysisElements.map((element, index) => (
<Analysis key={index} element={element} />
<React.Fragment key={element.analysisIdOriginal}>
<Analysis element={element} />
{element.results?.nestedElements?.map((nestedElement, nestedIndex) => (
<Analysis
key={`nested-${nestedElement.analysisElementOriginalId}-${nestedIndex}`}
nestedElement={nestedElement}
isNestedElement
/>
))}
</React.Fragment>
))
) : (
<div className="text-muted-foreground text-sm">

View File

@@ -3,13 +3,10 @@ import { useMemo } from 'react';
import { ArrowDown } from 'lucide-react';
import { cn } from '@kit/ui/utils';
import { AnalysisResultDetailsElementResults } from '@/packages/features/accounts/src/types/analysis-results';
import type { AnalysisResultDetailsElementResults } from '@/packages/features/user-analyses/src/types/analysis-results';
import { AnalysisResultLevel } from '@/packages/features/user-analyses/src/types/analysis-results';
export enum AnalysisResultLevel {
NORMAL = 0,
WARNING = 1,
CRITICAL = 2,
}
type AnalysisResultLevelBarResults = Pick<AnalysisResultDetailsElementResults, 'normLower' | 'normUpper' | 'responseValue'>;
const Level = ({
isActive = false,
@@ -50,7 +47,7 @@ const Level = ({
)}
{color === 'success' && typeof normRangeText === 'string' && (
<p className={cn("absolute bottom-[-18px] left-3/8 text-xs text-muted-foreground font-bold", {
<p className={cn("absolute bottom-[-18px] left-3/8 text-xs text-muted-foreground font-bold whitespace-nowrap", {
'opacity-60': isActive,
})}>
{normRangeText}
@@ -60,28 +57,19 @@ const Level = ({
);
};
export const AnalysisLevelBarSkeleton = () => {
return (
<div className="mt-4 flex h-3 w-[60%] sm:w-[35%] max-w-[360px] gap-1 sm:mt-0">
<Level color="gray-200" />
</div>
);
};
const AnalysisLevelBar = ({
level,
results,
results: {
normLower: lower,
normUpper: upper,
responseValue: value,
},
normRangeText,
}: {
level: AnalysisResultLevel;
results: AnalysisResultDetailsElementResults;
results: AnalysisResultLevelBarResults;
normRangeText: string | null;
}) => {
const { normLower: lower, normUpper: upper, responseValue: value, normStatus } = results;
const normLowerIncluded = results?.normLowerIncluded || false;
const normUpperIncluded = results?.normUpperIncluded || false;
// Calculate arrow position based on value within normal range
const arrowLocation = useMemo(() => {
// If no response value, center the arrow
@@ -147,8 +135,6 @@ const AnalysisLevelBar = ({
// Show appropriate levels based on available norm bounds
const hasLowerBound = lower !== null;
const isLowerBoundZero = hasLowerBound && lower === 0;
console.info('isLowerBoundZero', results.analysisElementOriginalId, { isLowerBoundZero, hasLowerBound, lower });
const hasUpperBound = upper !== null;
// Determine which section the value falls into
@@ -157,34 +143,10 @@ const AnalysisLevelBar = ({
const isValueInNormalRange = !isValueBelowLower && !isValueAboveUpper;
const [first, second, third] = useMemo(() => {
if (!hasLowerBound) {
return [
{
isActive: isNormal,
color: "success",
isFirst: true,
normRangeText,
...(isNormal ? { arrowLocation } : {}),
},
{
isActive: isWarning,
color: "warning",
...(isWarning ? { arrowLocation } : {}),
},
{
isActive: isCritical,
color: "destructive",
isLast: true,
...(isCritical ? { arrowLocation } : {}),
},
] as const;
}
return [
const [warning, normal, critical] = [
{
isActive: isWarning,
color: "warning",
isFirst: true,
...(isWarning ? { arrowLocation } : {}),
},
{
@@ -200,10 +162,30 @@ const AnalysisLevelBar = ({
...(isCritical ? { arrowLocation } : {}),
},
] as const;
if (!hasLowerBound) {
return [
{ ...normal, isFirst: true },
warning,
critical,
] as const;
}
return [
{ ...warning, isFirst: true },
normal,
{ ...critical, isLast: true },
] as const;
}, [isValueBelowLower, isValueAboveUpper, isValueInNormalRange, arrowLocation, normRangeText, isNormal, isWarning, isCritical]);
return (
<div className="mt-4 flex h-3 w-[60%] sm:w-[35%] max-w-[360px] gap-1 sm:mt-0">
<div className={cn(
"flex h-3 gap-1",
"mt-4 sm:mt-0",
"w-[60%] sm:w-[35%]",
"min-w-[50vw] sm:min-w-auto",
"max-w-[360px]",
)}>
<Level {...first} />
<Level {...second} />
<Level {...third} />

View File

@@ -2,33 +2,59 @@
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AnalysisResultDetailsElement } from '@/packages/features/accounts/src/types/analysis-results';
import { format } from 'date-fns';
import { Info } from 'lucide-react';
import type {
AnalysisResultDetailsElement,
AnalysisResultsDetailsElementNested,
} from '@/packages/features/user-analyses/src/types/analysis-results';
import { AnalysisResultLevel } from '@/packages/features/user-analyses/src/types/analysis-results';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
import AnalysisLevelBar, {
AnalysisResultLevel,
} from './analysis-level-bar';
export enum AnalysisStatus {
NORMAL = 0,
MEDIUM = 1,
HIGH = 2,
}
import AnalysisLevelBar from './analysis-level-bar';
const Analysis = ({
element,
element: elementOriginal,
nestedElement,
isNestedElement = false,
}: {
element: AnalysisResultDetailsElement;
element?: AnalysisResultDetailsElement;
nestedElement?: AnalysisResultsDetailsElementNested;
isNestedElement?: boolean;
}) => {
const { t } = useTranslation();
const element = (() => {
if (isNestedElement) {
return nestedElement!;
}
return elementOriginal!;
})();
const results: AnalysisResultDetailsElement['results'] = useMemo(() => {
if (isNestedElement) {
const nestedElement = element as AnalysisResultsDetailsElementNested;
return {
analysisElementOriginalId: nestedElement.analysisElementOriginalId,
normLower: nestedElement.normLower,
normUpper: nestedElement.normUpper,
normStatus: nestedElement.normStatus,
responseTime: nestedElement.responseTime,
responseValue: nestedElement.responseValue,
responseValueIsNegative: nestedElement.responseValueIsNegative,
responseValueIsWithinNorm: nestedElement.responseValueIsWithinNorm,
normLowerIncluded: nestedElement.normLowerIncluded,
normUpperIncluded: nestedElement.normUpperIncluded,
unit: nestedElement.unit,
status: nestedElement.status,
nestedElements: [],
};
}
return (element as AnalysisResultDetailsElement).results;
}, [element, isNestedElement]);
const name = element.analysisName || '';
const results = element.results;
const hasIsWithinNorm = results?.responseValueIsWithinNorm !== null;
const hasIsNegative = results?.responseValueIsNegative !== null;
@@ -58,21 +84,16 @@ const Analysis = ({
return responseValue;
})();
const unit = results?.unit || '';
const normLower = results?.normLower;
const normUpper = results?.normUpper;
const normLower = results?.normLower ?? null;
const normUpper = results?.normUpper ?? null;
const normStatus = results?.normStatus ?? null;
const [showTooltip, setShowTooltip] = useState(false);
const analysisResultLevel = useMemo(() => {
if (!results) {
if (normStatus === null) {
return null;
}
if (results.responseValue === null || results.responseValue === undefined) {
return null;
}
const normStatus = results.normStatus;
switch (normStatus) {
case 1:
return AnalysisResultLevel.WARNING;
@@ -82,17 +103,24 @@ const Analysis = ({
default:
return AnalysisResultLevel.NORMAL;
}
}, [results]);
}, [normStatus]);
const isCancelled = Number(results?.status) === 5;
const hasNestedElements = results?.nestedElements.length > 0;
const nestedElements = results?.nestedElements ?? null;
const hasNestedElements = Array.isArray(nestedElements) && nestedElements.length > 0;
const normRangeText = normLower !== null ? `${normLower} - ${normUpper || ''}` : null;
const normRangeText = (() => {
if (normLower === null && normUpper === null) {
return null;
}
return `${normLower ?? '...'} - ${normUpper ?? '...'}`;
})();
const hasTextualResponse = hasIsNegative || hasIsWithinNorm;
return (
<div className="border-border rounded-lg border px-5">
<div className={cn("border-border rounded-lg border px-5", { 'ml-8': isNestedElement })}>
<div className="flex flex-col items-center justify-between gap-2 pt-3 pb-6 sm:py-3 sm:h-[65px] sm:flex-row sm:gap-0">
<div className="flex items-center gap-2 font-semibold">
<div className={cn("flex items-center gap-2 font-semibold", { 'font-bold': isNestedElement })}>
{name}
{results?.responseTime && (
<div
@@ -127,10 +155,18 @@ const Analysis = ({
{isCancelled || !results || hasNestedElements ? null : (
<>
<div className="flex items-center gap-3 sm:ml-auto">
<div className="font-semibold">{value}</div>
<div
className={cn('font-semibold', {
'text-yellow-600': hasTextualResponse && analysisResultLevel === AnalysisResultLevel.WARNING,
'text-red-600': hasTextualResponse && analysisResultLevel === AnalysisResultLevel.CRITICAL,
'text-green-600': hasTextualResponse && analysisResultLevel === AnalysisResultLevel.NORMAL,
})}
>
{value}
</div>
<div className="text-muted-foreground text-sm">{unit}</div>
</div>
{!(hasIsNegative || hasIsWithinNorm) && (
{!hasTextualResponse && (
<>
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
{normRangeText}

View File

@@ -1,6 +1,6 @@
import { cache } from 'react';
import { AnalysisResultDetailsMapped } from '@kit/accounts/types/analysis-results';
import type { AnalysisResultDetailsMapped } from '@/packages/features/user-analyses/src/types/analysis-results';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';