feat(MED-168): clean up types, show nested elements

This commit is contained in:
2025-09-19 15:28:56 +03:00
parent e7b484e1d4
commit 091144d942
9 changed files with 188 additions and 289 deletions

View File

@@ -1,3 +1,4 @@
import React from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
@@ -64,8 +65,7 @@ export default async function AnalysisResultsPage({
<Trans i18nKey="analysis-results:pageTitle" /> <Trans i18nKey="analysis-results:pageTitle" />
</h4> </h4>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
{analysisResponse?.elements && {orderedAnalysisElements.length > 0 ? (
analysisResponse.elements?.length > 0 ? (
<Trans i18nKey="analysis-results:description" /> <Trans i18nKey="analysis-results:description" />
) : ( ) : (
<Trans i18nKey="analysis-results:descriptionEmpty" /> <Trans i18nKey="analysis-results:descriptionEmpty" />
@@ -106,7 +106,16 @@ export default async function AnalysisResultsPage({
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{orderedAnalysisElements ? ( {orderedAnalysisElements ? (
orderedAnalysisElements.map((element, index) => ( 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"> <div className="text-muted-foreground text-sm">

View File

@@ -3,7 +3,7 @@ import { useMemo } from 'react';
import { ArrowDown } from 'lucide-react'; import { ArrowDown } from 'lucide-react';
import { cn } from '@kit/ui/utils'; 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';
export enum AnalysisResultLevel { export enum AnalysisResultLevel {
NORMAL = 0, NORMAL = 0,
@@ -11,6 +11,8 @@ export enum AnalysisResultLevel {
CRITICAL = 2, CRITICAL = 2,
} }
type AnalysisResultLevelBarResults = Pick<AnalysisResultDetailsElementResults, 'normLower' | 'normUpper' | 'responseValue'>;
const Level = ({ const Level = ({
isActive = false, isActive = false,
color, color,
@@ -60,28 +62,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 = ({ const AnalysisLevelBar = ({
level, level,
results, results: {
normLower: lower,
normUpper: upper,
responseValue: value,
},
normRangeText, normRangeText,
}: { }: {
level: AnalysisResultLevel; level: AnalysisResultLevel;
results: AnalysisResultDetailsElementResults; results: AnalysisResultLevelBarResults;
normRangeText: string | null; 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 // Calculate arrow position based on value within normal range
const arrowLocation = useMemo(() => { const arrowLocation = useMemo(() => {
// If no response value, center the arrow // If no response value, center the arrow
@@ -147,7 +140,6 @@ const AnalysisLevelBar = ({
// Show appropriate levels based on available norm bounds // Show appropriate levels based on available norm bounds
const hasLowerBound = lower !== null; const hasLowerBound = lower !== null;
const isLowerBoundZero = hasLowerBound && lower === 0;
const hasUpperBound = upper !== null; const hasUpperBound = upper !== null;
// Determine which section the value falls into // Determine which section the value falls into

View File

@@ -3,7 +3,11 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AnalysisResultDetailsElement } from '@/packages/features/accounts/src/types/analysis-results'; import type {
AnalysisResultDetailsElementResults,
AnalysisResultDetailsElement,
AnalysisResultsDetailsElementNested,
} from '@/packages/features/user-analyses/src/types/analysis-results';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { Info } from 'lucide-react'; import { Info } from 'lucide-react';
@@ -21,14 +25,45 @@ export enum AnalysisStatus {
} }
const Analysis = ({ const Analysis = ({
element, element: elementOriginal,
nestedElement,
isNestedElement = false,
}: { }: {
element: AnalysisResultDetailsElement; element?: AnalysisResultDetailsElement;
nestedElement?: AnalysisResultsDetailsElementNested;
isNestedElement?: boolean;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const element = (() => {
if (isNestedElement) {
return nestedElement!;
}
return elementOriginal!;
})();
const results: AnalysisResultDetailsElementResults = 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 name = element.analysisName || '';
const results = element.results;
const hasIsWithinNorm = results?.responseValueIsWithinNorm !== null; const hasIsWithinNorm = results?.responseValueIsWithinNorm !== null;
const hasIsNegative = results?.responseValueIsNegative !== null; const hasIsNegative = results?.responseValueIsNegative !== null;
@@ -58,8 +93,8 @@ const Analysis = ({
return responseValue; return responseValue;
})(); })();
const unit = results?.unit || ''; const unit = results?.unit || '';
const normLower = results?.normLower; const normLower = results?.normLower ?? null;
const normUpper = results?.normUpper; const normUpper = results?.normUpper ?? null;
const normStatus = results?.normStatus ?? null; const normStatus = results?.normStatus ?? null;
const [showTooltip, setShowTooltip] = useState(false); const [showTooltip, setShowTooltip] = useState(false);
@@ -82,13 +117,18 @@ const Analysis = ({
const isCancelled = Number(results?.status) === 5; const isCancelled = Number(results?.status) === 5;
const hasNestedElements = results?.nestedElements.length > 0; const hasNestedElements = results?.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; const hasTextualResponse = hasIsNegative || hasIsWithinNorm;
return ( 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 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} {name}
{results?.responseTime && ( {results?.responseTime && (
<div <div

View File

@@ -1,4 +1,4 @@
import { AnalysisResultDetailsMapped } from "@/packages/features/accounts/src/types/analysis-results"; import type { AnalysisResultDetailsMapped } from "@/packages/features/user-analyses/src/types/analysis-results";
type AnalysisTestResponse = Omit<AnalysisResultDetailsMapped, 'order' | 'orderedAnalysisElementIds' | 'summary' | 'elements'>; type AnalysisTestResponse = Omit<AnalysisResultDetailsMapped, 'order' | 'orderedAnalysisElementIds' | 'summary' | 'elements'>;

View File

@@ -1,6 +1,6 @@
import { cache } from 'react'; 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 { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';

View File

@@ -1,3 +0,0 @@
import { Tables } from '@kit/supabase/database';
export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>;

View File

@@ -1,139 +0,0 @@
import * as z from 'zod';
import { Database } from '@kit/supabase/database';
export type UserAnalysisElement =
Database['medreport']['Tables']['analysis_response_elements']['Row'];
export type UserAnalysisResponse =
Database['medreport']['Tables']['analysis_responses']['Row'] & {
elements: UserAnalysisElement[];
};
export type UserAnalysis = UserAnalysisResponse[];
const ElementSchema = z.object({
unit: z.string(),
norm_lower: z.number(),
norm_upper: z.number(),
norm_status: z.number(),
analysis_name: z.string(),
response_time: z.string(),
response_value: z.number(),
response_value_is_negative: z.boolean(),
norm_lower_included: z.boolean(),
norm_upper_included: z.boolean(),
status: z.string(),
analysis_element_original_id: z.string(),
original_response_element: z.object({
}),
});
const OrderSchema = z.object({
status: z.string(),
medusa_order_id: z.string(),
created_at: z.coerce.date(),
});
const DoctorAnalysisFeedbackSchema = z.object({
id: z.number(),
status: z.string(),
user_id: z.string(),
created_at: z.coerce.date(),
created_by: z.string(),
});
const SummarySchema = z.object({
id: z.number(),
value: z.string(),
status: z.string(),
user_id: z.string(),
created_at: z.coerce.date(),
created_by: z.string(),
updated_at: z.coerce.date().nullable(),
updated_by: z.string(),
doctor_user_id: z.string().nullable(),
analysis_order_id: z.number(),
doctor_analysis_feedback: z.array(DoctorAnalysisFeedbackSchema),
});
export const AnalysisResultDetailsSchema = z.object({
id: z.number(),
analysis_order_id: z.number(),
order_number: z.string(),
order_status: z.string(),
user_id: z.string(),
created_at: z.coerce.date(),
updated_at: z.coerce.date().nullable(),
elements: z.array(ElementSchema),
order: OrderSchema,
summary: SummarySchema.nullable(),
});
export type AnalysisResultDetails = z.infer<typeof AnalysisResultDetailsSchema>;
export type AnalysisResultDetailsElementResults = {
unit: string | null;
normLower: number | null;
normUpper: number | null;
normStatus: number | null;
responseTime: string | null;
responseValue: number | null;
responseValueIsNegative: boolean | null;
responseValueIsWithinNorm: boolean | null;
normLowerIncluded: boolean;
normUpperIncluded: boolean;
status: string;
analysisElementOriginalId: string;
nestedElements: {
analysisElementOriginalId: string;
normLower?: number | null;
normLowerIncluded: boolean;
normStatus: number;
normUpper?: number | null;
normUpperIncluded: boolean;
responseTime: string;
responseValue: number;
status: number;
unit: string;
}[];
labComment?: string | null;
};
export type AnalysisResultDetailsElement = {
analysisIdOriginal: string;
isWaitingForResults: boolean;
analysisName: string;
results: AnalysisResultDetailsElementResults;
};
export type AnalysisResultDetailsMapped = {
id: number;
order: {
status: string;
medusaOrderId: string;
createdAt: Date | string;
};
elements: {
id: string;
unit: string;
norm_lower: number;
norm_upper: number;
norm_status: number;
analysis_name: string;
response_time: string;
response_value: number;
norm_lower_included: boolean;
norm_upper_included: boolean;
status: string;
analysis_element_original_id: string;
}[];
orderedAnalysisElementIds: number[];
orderedAnalysisElements: AnalysisResultDetailsElement[];
summary: {
id: number;
status: string;
user_id: string;
created_at: Date;
created_by: string;
value?: string;
} | null;
};

View File

@@ -2,9 +2,9 @@ import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@kit/supabase/database'; import { Database } from '@kit/supabase/database';
import { toArray } from '@kit/shared/utils'; import { toArray } from '@kit/shared/utils';
import type { UuringElement, UuringuVastus } from '@kit/shared/types/medipost-analysis'; import type { UuringuVastus } from '@kit/shared/types/medipost-analysis';
import type { AnalysisResultDetails, AnalysisResultDetailsMapped, UserAnalysis } from '../types/analysis-results'; import type { AnalysisResultDetails, AnalysisResultDetailsMapped, AnalysisResultsDetailsElementNested, UserAnalysis } from '../types/analysis-results';
import type { AnalysisOrder } from '../types/analysis-orders'; import type { AnalysisOrder } from '../types/analysis-orders';
/** /**
@@ -166,28 +166,14 @@ class UserAnalysesApi {
if (!nestedAnalysisElement) { if (!nestedAnalysisElement) {
continue; continue;
} }
results.nestedElements.push({
...nestedAnalysisElement,
analysisElementOriginalId,
analysisName: nestedAnalysisElement.analysis_name_lab,
});
}
}
mappedOrderedAnalysisElements.forEach(({ results }) => { nestedElement.analysisElementOriginalId = analysisElementOriginalId;
results?.nestedElements.forEach(({ analysisElementOriginalId }) => { nestedElement.analysisName = nestedAnalysisElement.analysis_name_lab as string | undefined;
const nestedAnalysisElement = nestedAnalysisElements.find(({ id }) => id === analysisElementOriginalId); }
if (nestedAnalysisElement) { results.nestedElements = results.nestedElements.sort((a, b) => a.analysisName?.localeCompare(b.analysisName ?? '') ?? 0);
results?.nestedElements.push({ }
...nestedAnalysisElement, } else {
analysisElementOriginalId, console.error('Failed to get nested analysis elements by ids=', nestedAnalysisElementIds, nestedAnalysisElementsError);
analysisName: nestedAnalysisElement.analysis_name_lab,
});
} else {
console.error('Nested analysis element not found for analysis element original id=', analysisElementOriginalId);
}
});
});
} }
} }
@@ -218,16 +204,12 @@ class UserAnalysesApi {
analysisName: analysisNameLab, analysisName: analysisNameLab,
results: { results: {
nestedElements: (() => { nestedElements: (() => {
const nestedElements = elementResponse.original_response_element?.UuringuElement as UuringElement[] | undefined; const nestedElements = toArray(elementResponse.original_response_element?.UuringuElement)
if (!nestedElements) { return nestedElements.map<AnalysisResultsDetailsElementNested>((element) => {
return [];
}
return toArray(nestedElements).map((element) => {
const mappedResponse = this.mapUuringVastus({ const mappedResponse = this.mapUuringVastus({
uuringVastus: element.UuringuVastus as UuringuVastus | undefined, uuringVastus: element.UuringuVastus as UuringuVastus | undefined,
}); });
return { return {
status: element.UuringOlek,
unit: element.Mootyhik ?? null, unit: element.Mootyhik ?? null,
normLower: mappedResponse.normLower, normLower: mappedResponse.normLower,
normUpper: mappedResponse.normUpper, normUpper: mappedResponse.normUpper,
@@ -239,6 +221,7 @@ class UserAnalysesApi {
normLowerIncluded: mappedResponse.normLowerIncluded, normLowerIncluded: mappedResponse.normLowerIncluded,
normUpperIncluded: mappedResponse.normUpperIncluded, normUpperIncluded: mappedResponse.normUpperIncluded,
analysisElementOriginalId: element.UuringId, analysisElementOriginalId: element.UuringId,
status: elementResponse.status,
analysisName: undefined, analysisName: undefined,
}; };
}); });
@@ -251,12 +234,13 @@ class UserAnalysesApi {
normStatus: elementResponse.norm_status, normStatus: elementResponse.norm_status,
responseTime: elementResponse.response_time, responseTime: elementResponse.response_time,
responseValue: elementResponse.response_value, responseValue: elementResponse.response_value,
responseValueIsNegative: elementResponse.response_value_is_negative === true, responseValueIsNegative: elementResponse.response_value_is_negative === null ? null : elementResponse.response_value_is_negative === true,
responseValueIsWithinNorm: elementResponse.response_value_is_within_norm === true, responseValueIsWithinNorm: elementResponse.response_value_is_within_norm === null ? null : elementResponse.response_value_is_within_norm === true,
normLowerIncluded: elementResponse.norm_lower_included, normLowerIncluded: elementResponse.norm_lower_included,
normUpperIncluded: elementResponse.norm_upper_included, normUpperIncluded: elementResponse.norm_upper_included,
status: elementResponse.status, status: elementResponse.status,
analysisElementOriginalId: elementResponse.analysis_element_original_id, analysisElementOriginalId: elementResponse.analysis_element_original_id,
elementResponse,
} }
}; };
} }

View File

@@ -1,6 +1,5 @@
import * as z from 'zod';
import { Database } from '@kit/supabase/database'; import { Database } from '@kit/supabase/database';
import { AnalysisOrderStatus, NormStatus } from '@kit/shared/types/medipost-analysis';
export type UserAnalysisElement = export type UserAnalysisElement =
Database['medreport']['Tables']['analysis_response_elements']['Row']; Database['medreport']['Tables']['analysis_response_elements']['Row'];
@@ -10,66 +9,109 @@ export type UserAnalysisResponse =
}; };
export type UserAnalysis = UserAnalysisResponse[]; export type UserAnalysis = UserAnalysisResponse[];
const ElementSchema = z.object({ type ElementSchema = {
unit: z.string(), unit: string,
norm_lower: z.number(), norm_lower: number,
norm_upper: z.number(), norm_upper: number,
norm_status: z.number(), norm_status: number,
analysis_name: z.string(), analysis_name: string,
response_time: z.string(), response_time: string,
response_value: z.number(), response_value: number,
response_value_is_negative: z.boolean(), response_value_is_negative: boolean,
response_value_is_within_norm: z.boolean(), response_value_is_within_norm: boolean,
norm_lower_included: z.boolean(), norm_lower_included: boolean,
norm_upper_included: z.boolean(), norm_upper_included: boolean,
status: z.string(), status: string,
analysis_element_original_id: z.string(), analysis_element_original_id: string,
original_response_element: z.object({ original_response_element: {
UuringuElement: {
UuringIdOID: string,
UuringId: string,
TLyhend: string,
KNimetus: string,
UuringNimi: string,
UuringuKommentaar: string | null,
TellijaUuringId: number,
TeostajaUuringId: string,
UuringOlek: keyof typeof AnalysisOrderStatus,
Mootyhik: string | null,
Kood: {
HkKood: number,
HkKoodiKordaja: number,
Koefitsient: number,
Hind: number,
},
UuringuVastus: {
VastuseVaartus: string,
VastuseAeg: string,
NormiStaatus: keyof typeof NormStatus,
ProoviJarjenumber: number,
},
UuringuTaitjaAsutuseJnr: number,
},
UuringuKommentaar: string | null,
},
};
}), type OrderSchema = {
}); status: string,
medusa_order_id: string,
created_at: string,
};
const OrderSchema = z.object({ type DoctorAnalysisFeedbackSchema = {
status: z.string(), id: number,
medusa_order_id: z.string(), status: string,
created_at: z.coerce.date(), user_id: string,
}); created_at: string,
created_by: string,
};
const DoctorAnalysisFeedbackSchema = z.object({ type SummarySchema = {
id: z.number(), id: number,
status: z.string(), value: string,
user_id: z.string(), status: string,
created_at: z.coerce.date(), user_id: string,
created_by: z.string(), created_at: string,
}); created_by: string,
updated_at: string | null,
updated_by: string,
doctor_user_id: string | null,
analysis_order_id: number,
doctor_analysis_feedback: DoctorAnalysisFeedbackSchema[],
};
const SummarySchema = z.object({ export type AnalysisResultDetails = {
id: z.number(), id: number,
value: z.string(), analysis_order_id: number,
status: z.string(), order_number: string,
user_id: z.string(), order_status: string,
created_at: z.coerce.date(), user_id: string,
created_by: z.string(), created_at: string,
updated_at: z.coerce.date().nullable(), updated_at: string | null,
updated_by: z.string(), elements: ElementSchema[],
doctor_user_id: z.string().nullable(),
analysis_order_id: z.number(),
doctor_analysis_feedback: z.array(DoctorAnalysisFeedbackSchema),
});
export const AnalysisResultDetailsSchema = z.object({
id: z.number(),
analysis_order_id: z.number(),
order_number: z.string(),
order_status: z.string(),
user_id: z.string(),
created_at: z.coerce.date(),
updated_at: z.coerce.date().nullable(),
elements: z.array(ElementSchema),
order: OrderSchema, order: OrderSchema,
summary: SummarySchema.nullable(), summary: SummarySchema | null,
}); };
export type AnalysisResultDetails = z.infer<typeof AnalysisResultDetailsSchema>;
export type AnalysisResultsDetailsElementNested = {
analysisElementOriginalId: string;
analysisName?: string;
} & Pick<
AnalysisResultDetailsElementResults,
'unit' |
'normLower' |
'normUpper' |
'normStatus' |
'responseTime' |
'responseValue' |
'responseValueIsNegative' |
'responseValueIsWithinNorm' |
'normLowerIncluded' |
'normUpperIncluded' |
'status' |
'analysisElementOriginalId'
>;
export type AnalysisResultDetailsElementResults = { export type AnalysisResultDetailsElementResults = {
unit: string | null; unit: string | null;
@@ -84,19 +126,7 @@ export type AnalysisResultDetailsElementResults = {
normUpperIncluded: boolean; normUpperIncluded: boolean;
status: string; status: string;
analysisElementOriginalId: string; analysisElementOriginalId: string;
nestedElements: { nestedElements: AnalysisResultsDetailsElementNested[];
analysisElementOriginalId: string;
normLower?: number | null;
normLowerIncluded: boolean;
normStatus: number | null;
normUpper?: number | null;
normUpperIncluded: boolean;
responseTime: string | null;
responseValue: number | null;
status: number;
unit: string | null;
analysisName?: string | null;
}[];
labComment?: string | null; labComment?: string | null;
}; };
@@ -114,27 +144,13 @@ export type AnalysisResultDetailsMapped = {
medusaOrderId: string; medusaOrderId: string;
createdAt: Date | string; createdAt: Date | string;
}; };
elements: {
id: string;
unit: string;
norm_lower: number;
norm_upper: number;
norm_status: number;
analysis_name: string;
response_time: string;
response_value: number;
norm_lower_included: boolean;
norm_upper_included: boolean;
status: string;
analysis_element_original_id: string;
}[];
orderedAnalysisElementIds: number[]; orderedAnalysisElementIds: number[];
orderedAnalysisElements: AnalysisResultDetailsElement[]; orderedAnalysisElements: AnalysisResultDetailsElement[];
summary: { summary: {
id: number; id: number;
status: string; status: string;
user_id: string; user_id: string;
created_at: Date; created_at: string;
created_by: string; created_by: string;
value?: string; value?: string;
} | null; } | null;