diff --git a/app/doctor/_components/analysis-doctor.tsx b/app/doctor/_components/analysis-doctor.tsx
index 4b7e4ee..5166e26 100644
--- a/app/doctor/_components/analysis-doctor.tsx
+++ b/app/doctor/_components/analysis-doctor.tsx
@@ -2,7 +2,6 @@
import React, { ReactElement, ReactNode, useMemo, useState } from 'react';
-import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts';
import { format } from 'date-fns';
import { Info } from 'lucide-react';
@@ -10,23 +9,24 @@ import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
import { AnalysisElement } from '~/lib/services/analysis-element.service';
+import { NestedAnalysisElement } from '@/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema';
import AnalysisLevelBar, {
AnalysisLevelBarSkeleton,
AnalysisResultLevel,
} from './analysis-level-bar';
-export type AnalysisResultForDisplay = Pick<
- UserAnalysisElement,
- | 'norm_status'
- | 'response_value'
- | 'unit'
- | 'norm_lower_included'
- | 'norm_upper_included'
- | 'norm_lower'
- | 'norm_upper'
- | 'response_time'
->;
+export type AnalysisResultForDisplay = {
+ norm_status?: number | null;
+ response_value?: number | null;
+ unit?: string | null;
+ norm_lower_included?: boolean | null;
+ norm_upper_included?: boolean | null;
+ norm_lower?: number | null;
+ norm_upper?: number | null;
+ response_time?: string | null;
+ nestedElements?: NestedAnalysisElement[];
+};
const AnalysisDoctor = ({
analysisElement,
@@ -72,6 +72,12 @@ const AnalysisDoctor = ({
return `${normLower ?? '...'} - ${normUpper ?? '...'}`;
}, [normLower, normUpper]);
+ const nestedElements = results?.nestedElements ?? null;
+ const hasNestedElements =
+ Array.isArray(nestedElements) && nestedElements.length > 0;
+
+ const isAnalysisLevelBarHidden = isCancelled || !results || hasNestedElements;
+
return (
@@ -101,7 +107,7 @@ const AnalysisDoctor = ({
)}
- {results ? (
+ {isAnalysisLevelBarHidden ? null : (
<>
{value}
@@ -120,17 +126,33 @@ const AnalysisDoctor = ({
/>
{endIcon ||
}
>
- ) : isCancelled ? null : (
- <>
-
-
-
- >
)}
+ {(() => {
+ // If parent has nested elements, don't show anything
+ if (hasNestedElements) {
+ return null;
+ }
+ // If we're showing the level bar, don't show waiting
+ if (!isAnalysisLevelBarHidden) {
+ return null;
+ }
+ // If cancelled, don't show waiting
+ if (isCancelled) {
+ return null;
+ }
+ // Otherwise, show waiting for results
+ return (
+ <>
+
+
+
+ >
+ );
+ })()}
);
diff --git a/app/doctor/_components/analysis-level-bar.tsx b/app/doctor/_components/analysis-level-bar.tsx
index c22e357..f868ccb 100644
--- a/app/doctor/_components/analysis-level-bar.tsx
+++ b/app/doctor/_components/analysis-level-bar.tsx
@@ -105,35 +105,38 @@ const AnalysisLevelBar = ({
// If only upper bound exists
if (lower === null && upper !== null) {
- if (value <= upper) {
- return Math.min(75, (value / upper) * 75); // Show in left 75% of normal range
+ if (value <= upper!) {
+ return Math.min(75, (value / upper!) * 75); // Show in left 75% of normal range
}
return 100; // Beyond upper bound
}
// If only lower bound exists
if (upper === null && lower !== null) {
- if (value >= lower) {
+ if (value >= lower!) {
// Value is in normal range (above lower bound)
// Position proportionally in the normal range section
- const normalizedPosition = Math.min((value - lower) / (lower * 0.5), 1); // Use 50% of lower as scale
+ const normalizedPosition = Math.min(
+ (value - lower!) / (lower! * 0.5),
+ 1,
+ ); // Use 50% of lower as scale
return normalizedPosition * 100;
}
// Value is below lower bound - position in the "below normal" section
- const belowPosition = Math.max(0, Math.min(1, value / lower));
+ const belowPosition = Math.max(0, Math.min(1, value / lower!));
return belowPosition * 100;
}
// Both bounds exist
if (lower !== null && upper !== null) {
- if (value < lower) {
+ if (value < lower!) {
return 0; // Below normal range
}
- if (value > upper) {
+ if (value > upper!) {
return 100; // Above normal range
}
// Within normal range
- return ((value - lower) / (upper - lower)) * 100;
+ return ((value - lower!) / (upper! - lower!)) * 100;
}
return 50; // Fallback
@@ -145,18 +148,10 @@ const AnalysisLevelBar = ({
const isCritical = level === AnalysisResultLevel.CRITICAL;
const isPending = level === null;
- // If pending results, show gray bar
- if (isPending) {
- return (
-
-
-
- );
- }
-
// Show appropriate levels based on available norm bounds
const hasLowerBound = lower !== null;
+ // Calculate level configuration (must be called before any returns)
const [first, second, third] = useMemo(() => {
const [warning, normal, critical] = [
{
@@ -196,6 +191,15 @@ const AnalysisLevelBar = ({
hasLowerBound,
]);
+ // If pending results, show gray bar
+ if (isPending) {
+ return (
+
+
+
+ );
+ }
+
return (
-
-
-
- )
- }
- endIcon={
- analysisData.comment && (
- <>
-
-
- }
- />
-
-
-
- :
- {' '}
- {analysisData.comment}
-
- >
- )
- }
- analysisElement={{
- analysis_name_lab: analysisData.analysis_name,
- }}
- results={analysisData}
- />
-
-
- {analysisData.latestPreviousAnalysis && (
-
-
+ <>
+
+
+
+ )
+ }
endIcon={
- analysisData.latestPreviousAnalysis.comment && (
+ analysisData.comment && (
<>
}
@@ -79,25 +51,103 @@ export default function DoctorAnalysisWrapper({
- :{' '}
-
- {analysisData.latestPreviousAnalysis.comment}
+ :
+ {' '}
+ {analysisData.comment}
>
)
}
analysisElement={{
- analysis_name_lab: t('doctor:previousResults', {
- date: formatDate(
- analysisData.latestPreviousAnalysis.response_time,
- ),
- }),
+ analysis_name_lab: analysisData.analysis_name,
}}
- results={analysisData.latestPreviousAnalysis}
+ results={analysisData}
/>
-
- )}
-
+
+ {analysisData.latestPreviousAnalysis && (
+
+
+
+
+
+ }
+ />
+
+
+
+ :{' '}
+
+ {analysisData.latestPreviousAnalysis.comment}
+
+ >
+ )
+ }
+ analysisElement={{
+ analysis_name_lab: t('doctor:previousResults', {
+ date: formatDate(
+ analysisData.latestPreviousAnalysis.response_time!,
+ ),
+ }),
+ }}
+ results={analysisData.latestPreviousAnalysis}
+ />
+ {analysisData.latestPreviousAnalysis.nestedElements?.map(
+ (nestedElement, nestedIndex) => (
+
+ ),
+ )}
+
+
+ )}
+
+ {analysisData.nestedElements?.map((nestedElement, nestedIndex) => (
+
+ ))}
+ >
);
}
diff --git a/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema.ts b/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema.ts
index 6730a10..3afa65b 100644
--- a/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema.ts
+++ b/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema.ts
@@ -52,6 +52,23 @@ export const AnalysisResponsesSchema = z.object({
});
export type AnalysisResponses = z.infer
;
+// Nested element schema (used recursively)
+export const NestedAnalysisElementSchema = z.object({
+ analysisElementOriginalId: z.string(),
+ analysisName: z.string().optional().nullable(),
+ unit: z.string().nullable(),
+ normLower: z.number().nullable(),
+ normUpper: z.number().nullable(),
+ normStatus: z.number().nullable(),
+ responseTime: z.string().nullable(),
+ responseValue: z.number().nullable(),
+ normLowerIncluded: z.boolean(),
+ normUpperIncluded: z.boolean(),
+ status: z.number(),
+ analysisNameLab: z.string().optional().nullable(),
+});
+export type NestedAnalysisElement = z.infer;
+
export const AnalysisResponseSchema = z.object({
id: z.number(),
analysis_response_id: z.number(),
@@ -69,6 +86,7 @@ export const AnalysisResponseSchema = z.object({
analysis_name: z.string().nullable(),
analysis_responses: AnalysisResponsesSchema,
comment: z.string().nullable(),
+ nestedElements: z.array(NestedAnalysisElementSchema).optional(),
latestPreviousAnalysis: z
.object({
id: z.number(),
@@ -86,6 +104,7 @@ export const AnalysisResponseSchema = z.object({
updated_at: z.string().nullable(),
analysis_name: z.string().nullable(),
comment: z.string().nullable(),
+ nestedElements: z.array(NestedAnalysisElementSchema).optional(),
})
.optional()
.nullable(),
diff --git a/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts b/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts
index 983241d..6ed99fd 100644
--- a/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts
+++ b/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts
@@ -5,12 +5,16 @@ import { isBefore } from 'date-fns';
import { renderDoctorSummaryReceivedEmail } from '@kit/email-templates';
import { getLogger } from '@kit/shared/logger';
-import { getFullName } from '@kit/shared/utils';
+import type { UuringuVastus, ResponseUuring } from '@kit/shared/types/medipost-analysis';
+import { getFullName, toArray } from '@kit/shared/utils';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createUserAnalysesApi } from '@kit/user-analyses/api';
import { sendEmailFromTemplate } from '../../../../../../../lib/services/mailer.service';
-import { AnalysisResultDetails } from '../schema/doctor-analysis-detail-view.schema';
+import {
+ AnalysisResultDetails,
+ NestedAnalysisElement,
+} from '../schema/doctor-analysis-detail-view.schema';
import {
AnalysisResponseBase,
DoctorAnalysisFeedbackTable,
@@ -20,6 +24,63 @@ import {
} from '../schema/doctor-analysis.schema';
import { ErrorReason } from '../schema/error.type';
+function mapUuringVastus({ uuringVastus }: { uuringVastus?: UuringuVastus }) {
+ const vastuseVaartus = uuringVastus?.VastuseVaartus;
+ const responseValue = (() => {
+ const valueAsNumber = Number(vastuseVaartus);
+ if (isNaN(valueAsNumber)) {
+ return null;
+ }
+ return valueAsNumber;
+ })();
+ const responseValueNumber = Number(responseValue);
+ const responseValueIsNumeric = !isNaN(responseValueNumber);
+ return {
+ normLower: uuringVastus?.NormAlum?.['#text'] ?? null,
+ normUpper: uuringVastus?.NormYlem?.['#text'] ?? null,
+ normStatus: (uuringVastus?.NormiStaatus ?? null) as number | null,
+ responseTime: uuringVastus?.VastuseAeg ?? null,
+ responseValue:
+ responseValueIsNumeric ? (responseValueNumber ?? null) : null,
+ normLowerIncluded:
+ uuringVastus?.NormAlum?.['@_kaasaarvatud']?.toLowerCase() === 'jah',
+ normUpperIncluded:
+ uuringVastus?.NormYlem?.['@_kaasaarvatud']?.toLowerCase() === 'jah',
+ };
+}
+
+function parseNestedElements(
+ originalResponseElement: ResponseUuring | null | undefined,
+ status: number,
+): NestedAnalysisElement[] {
+ if (!originalResponseElement?.UuringuElement) {
+ return [];
+ }
+
+ const nestedElements = toArray(originalResponseElement.UuringuElement);
+
+ return nestedElements.map((element) => {
+ const mappedResponse = mapUuringVastus({
+ uuringVastus: element.UuringuVastus as UuringuVastus | undefined,
+ });
+
+ return {
+ analysisElementOriginalId: element.UuringId,
+ analysisName: undefined, // Will be populated later from analysis_elements table
+ unit: element.Mootyhik ?? null,
+ normLower: mappedResponse.normLower,
+ normUpper: mappedResponse.normUpper,
+ normStatus: mappedResponse.normStatus,
+ responseTime: mappedResponse.responseTime,
+ responseValue: mappedResponse.responseValue,
+ normLowerIncluded: mappedResponse.normLowerIncluded,
+ normUpperIncluded: mappedResponse.normUpperIncluded,
+ status,
+ analysisNameLab: element.UuringNimi,
+ };
+ });
+}
+
async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
const supabase = getSupabaseServerClient();
@@ -388,7 +449,8 @@ export async function getAnalysisResultsForDoctor(
.from(`analysis_response_elements`)
.select(
`*,
- analysis_responses(user_id, analysis_order:analysis_order_id(id,medusa_order_id, analysis_element_ids))`,
+ analysis_responses(user_id, analysis_order:analysis_order_id(id,medusa_order_id, analysis_element_ids)),
+ original_response_element`,
)
.eq('analysis_response_id', analysisResponseId);
@@ -452,7 +514,8 @@ export async function getAnalysisResultsForDoctor(
*,
analysis_responses!inner(
user_id
- )
+ ),
+ original_response_element
`,
)
.in(
@@ -491,8 +554,14 @@ export async function getAnalysisResultsForDoctor(
preferred_locale,
} = accountWithParams[0];
+ // Parse nested elements for current and previous analyses
const analysisResponseElementsWithPreviousData = [];
for (const analysisResponseElement of analysisResponsesData) {
+ const nestedElements = parseNestedElements(
+ analysisResponseElement.original_response_element as ResponseUuring,
+ Number(analysisResponseElement.status),
+ );
+
const latestPreviousAnalysis = previousAnalyses.find(
({ analysis_element_original_id, response_time }) => {
if (response_time && analysisResponseElement.response_time) {
@@ -507,12 +576,95 @@ export async function getAnalysisResultsForDoctor(
}
},
);
+
+ // Parse nested elements for previous analysis if it exists
+ const latestPreviousAnalysisWithNested = latestPreviousAnalysis
+ ? {
+ ...latestPreviousAnalysis,
+ nestedElements: parseNestedElements(
+ latestPreviousAnalysis.original_response_element as ResponseUuring,
+ Number(latestPreviousAnalysis.status),
+ ),
+ }
+ : undefined;
+
analysisResponseElementsWithPreviousData.push({
...analysisResponseElement,
- latestPreviousAnalysis,
+ nestedElements,
+ latestPreviousAnalysis: latestPreviousAnalysisWithNested,
});
}
+ // Collect all nested element IDs to fetch their names
+ const nestedElementIds = analysisResponseElementsWithPreviousData
+ .flatMap((element) => [
+ ...(element.nestedElements?.map((ne) => ne.analysisElementOriginalId) ??
+ []),
+ ...(element.latestPreviousAnalysis?.nestedElements?.map(
+ (ne) => ne.analysisElementOriginalId,
+ ) ?? []),
+ ])
+ .filter(Boolean);
+
+ // Fetch analysis names for nested elements
+ if (nestedElementIds.length > 0) {
+ const { data: nestedAnalysisElements, error: nestedAnalysisElementsError } =
+ await supabase
+ .schema('medreport')
+ .from('analysis_elements')
+ .select('*')
+ .in('analysis_id_original', nestedElementIds);
+
+ if (!nestedAnalysisElementsError && nestedAnalysisElements) {
+ // Populate analysis names for current nested elements
+ for (const element of analysisResponseElementsWithPreviousData) {
+ if (element.nestedElements) {
+ for (const nestedElement of element.nestedElements) {
+ const analysisElement = nestedAnalysisElements.find(
+ (ae) =>
+ ae.analysis_id_original ===
+ nestedElement.analysisElementOriginalId,
+ );
+ if (analysisElement) {
+ nestedElement.analysisName =
+ analysisElement.analysis_name_lab as string | undefined;
+ }
+ }
+ // Sort nested elements by name
+ element.nestedElements.sort(
+ (a, b) => a.analysisName?.localeCompare(b.analysisName ?? '') ?? 0,
+ );
+ }
+
+ // Populate analysis names for previous nested elements
+ if (element.latestPreviousAnalysis?.nestedElements) {
+ for (const nestedElement of element.latestPreviousAnalysis
+ .nestedElements) {
+ const analysisElement = nestedAnalysisElements.find(
+ (ae) =>
+ ae.analysis_id_original ===
+ nestedElement.analysisElementOriginalId,
+ );
+ if (analysisElement) {
+ nestedElement.analysisName =
+ analysisElement.analysis_name_lab as string | undefined;
+ }
+ }
+ // Sort nested elements by name
+ element.latestPreviousAnalysis.nestedElements.sort(
+ (a, b) => a.analysisName?.localeCompare(b.analysisName ?? '') ?? 0,
+ );
+ }
+ }
+ } else {
+ console.error(
+ 'Failed to get nested analysis elements by ids=',
+ nestedElementIds,
+ nestedAnalysisElementsError,
+ );
+ }
+ }
+
return {
analysisResponse: analysisResponseElementsWithPreviousData,
order: {