Merge pull request #100 from MR-medreport/MED-168-fixes

feat(MED-168): analysis results view improvements / fixes
This commit is contained in:
2025-09-19 15:30:30 +03:00
committed by GitHub
23 changed files with 413 additions and 464 deletions

View File

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

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';
@@ -53,47 +54,44 @@ export default async function AnalysisResultsPage({
} }
const orderedAnalysisElements = analysisResponse.orderedAnalysisElements; const orderedAnalysisElements = analysisResponse.orderedAnalysisElements;
const hasOrderedAnalysisElements = orderedAnalysisElements.length > 0;
const isPartialStatus = analysisResponse.order.status === 'PARTIAL_ANALYSIS_RESPONSE';
return ( return (
<> <>
<PageHeader /> <PageHeader
<PageBody className="gap-4"> title={<Trans i18nKey="analysis-results:pageTitle" />}
<div className="mt-8 flex flex-col justify-between gap-4 sm:flex-row sm:items-center sm:gap-0"> description={hasOrderedAnalysisElements ? (
<div> isPartialStatus
<h4> ? <Trans i18nKey="analysis-results:descriptionPartial" />
<Trans i18nKey="analysis-results:pageTitle" /> : <Trans i18nKey="analysis-results:description" />
</h4>
<p className="text-muted-foreground text-sm">
{analysisResponse?.elements &&
analysisResponse.elements?.length > 0 ? (
<Trans i18nKey="analysis-results:description" />
) : ( ) : (
<Trans i18nKey="analysis-results:descriptionEmpty" /> <Trans i18nKey="analysis-results:descriptionEmpty" />
)} )}
</p> >
</div> <div>
<Button asChild> <Button asChild>
<Link href={pathsConfig.app.orderAnalysisPackage}> <Link href={pathsConfig.app.orderAnalysisPackage}>
<Trans i18nKey="analysis-results:orderNewAnalysis" /> <Trans i18nKey="analysis-results:orderNewAnalysis" />
</Link> </Link>
</Button> </Button>
</div> </div>
</PageHeader>
<PageBody className="gap-4 pt-4">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<h4> <h5 className="break-all">
<Trans <Trans
i18nKey="analysis-results:orderTitle" i18nKey="analysis-results:orderTitle"
values={{ orderNumber: analysisResponse.order.medusaOrderId }} values={{ orderNumber: analysisResponse.order.medusaOrderId }}
/> />
</h4> </h5>
<h5> <h6>
<Trans <Trans i18nKey={`orders:status.${analysisResponse.order.status}`} />
i18nKey={`orders:status.${analysisResponse.order.status}`}
/>
<ButtonTooltip <ButtonTooltip
content={`${analysisResponse.order.createdAt ? new Date(analysisResponse?.order?.createdAt).toLocaleString() : ''}`} content={`${analysisResponse.order.createdAt ? new Date(analysisResponse?.order?.createdAt).toLocaleString() : ''}`}
className="ml-6" className="ml-6"
/> />
</h5> </h6>
</div> </div>
{analysisResponse?.summary?.value && ( {analysisResponse?.summary?.value && (
<div> <div>
@@ -106,7 +104,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,13 +3,10 @@ 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';
import { AnalysisResultLevel } from '@/packages/features/user-analyses/src/types/analysis-results';
export enum AnalysisResultLevel { type AnalysisResultLevelBarResults = Pick<AnalysisResultDetailsElementResults, 'normLower' | 'normUpper' | 'responseValue'>;
NORMAL = 0,
WARNING = 1,
CRITICAL = 2,
}
const Level = ({ const Level = ({
isActive = false, isActive = false,
@@ -50,7 +47,7 @@ const Level = ({
)} )}
{color === 'success' && typeof normRangeText === 'string' && ( {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, 'opacity-60': isActive,
})}> })}>
{normRangeText} {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 = ({ 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,8 +135,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;
console.info('isLowerBoundZero', results.analysisElementOriginalId, { isLowerBoundZero, hasLowerBound, lower });
const hasUpperBound = upper !== null; const hasUpperBound = upper !== null;
// Determine which section the value falls into // Determine which section the value falls into
@@ -157,20 +143,18 @@ const AnalysisLevelBar = ({
const isValueInNormalRange = !isValueBelowLower && !isValueAboveUpper; const isValueInNormalRange = !isValueBelowLower && !isValueAboveUpper;
const [first, second, third] = useMemo(() => { const [first, second, third] = useMemo(() => {
if (!hasLowerBound) { const [warning, normal, critical] = [
return [
{
isActive: isNormal,
color: "success",
isFirst: true,
normRangeText,
...(isNormal ? { arrowLocation } : {}),
},
{ {
isActive: isWarning, isActive: isWarning,
color: "warning", color: "warning",
...(isWarning ? { arrowLocation } : {}), ...(isWarning ? { arrowLocation } : {}),
}, },
{
isActive: isNormal,
color: "success",
normRangeText,
...(isNormal ? { arrowLocation } : {}),
},
{ {
isActive: isCritical, isActive: isCritical,
color: "destructive", color: "destructive",
@@ -178,32 +162,30 @@ const AnalysisLevelBar = ({
...(isCritical ? { arrowLocation } : {}), ...(isCritical ? { arrowLocation } : {}),
}, },
] as const; ] as const;
if (!hasLowerBound) {
return [
{ ...normal, isFirst: true },
warning,
critical,
] as const;
} }
return [ return [
{ { ...warning, isFirst: true },
isActive: isWarning, normal,
color: "warning", { ...critical, isLast: true },
isFirst: true,
...(isWarning ? { arrowLocation } : {}),
},
{
isActive: isNormal,
color: "success",
normRangeText,
...(isNormal ? { arrowLocation } : {}),
},
{
isActive: isCritical,
color: "destructive",
isLast: true,
...(isCritical ? { arrowLocation } : {}),
},
] as const; ] as const;
}, [isValueBelowLower, isValueAboveUpper, isValueInNormalRange, arrowLocation, normRangeText, isNormal, isWarning, isCritical]); }, [isValueBelowLower, isValueAboveUpper, isValueInNormalRange, arrowLocation, normRangeText, isNormal, isWarning, isCritical]);
return ( 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 {...first} />
<Level {...second} /> <Level {...second} />
<Level {...third} /> <Level {...third} />

View File

@@ -2,33 +2,59 @@
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 { format } from 'date-fns'; import { format } from 'date-fns';
import { Info } from 'lucide-react'; 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 { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils'; import { cn } from '@kit/ui/utils';
import AnalysisLevelBar, { import AnalysisLevelBar from './analysis-level-bar';
AnalysisResultLevel,
} from './analysis-level-bar';
export enum AnalysisStatus {
NORMAL = 0,
MEDIUM = 1,
HIGH = 2,
}
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: 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 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 +84,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);
@@ -80,15 +106,21 @@ const Analysis = ({
}, [normStatus]); }, [normStatus]);
const isCancelled = Number(results?.status) === 5; 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; 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,10 +1,9 @@
'use server'; 'use server';
import { toArray } from '@/lib/utils';
import { getMailer } from '@kit/mailers'; import { getMailer } from '@kit/mailers';
import { enhanceAction } from '@kit/next/actions'; import { enhanceAction } from '@kit/next/actions';
import { getLogger } from '@kit/shared/logger'; import { getLogger } from '@kit/shared/logger';
import { toArray } from '@kit/shared/utils';
import { emailSchema } from '~/lib/validations/email.schema'; import { emailSchema } from '~/lib/validations/email.schema';

View File

@@ -13,9 +13,10 @@ import type {
MedipostOrderResponse, MedipostOrderResponse,
UuringElement, UuringElement,
} from '@/packages/shared/src/types/medipost-analysis'; } from '@/packages/shared/src/types/medipost-analysis';
import { toArray } from '@/lib/utils'; import { toArray } from '@kit/shared/utils';
import type { AnalysisOrder } from '~/lib/types/analysis-order'; import type { AnalysisOrder } from '~/lib/types/analysis-order';
import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element'; import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element';
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
import { Tables } from '@kit/supabase/database'; import { Tables } from '@kit/supabase/database';
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
@@ -138,28 +139,25 @@ export async function getAnalysisResponseElementsForGroup({
continue; continue;
} }
const responseValueIsNumeric = responseValue !== null; const mappedResponse = createUserAnalysesApi(getSupabaseServerAdminClient())
const responseValueIsNegative = vastuseVaartus === 'Negatiivne'; .mapUuringVastus({ uuringVastus: response });
const responseValueIsWithinNorm = vastuseVaartus === 'Normi piires';
results.push({ results.push({
analysis_element_original_id: analysisElementOriginalId, analysis_element_original_id: analysisElementOriginalId,
norm_lower: response.NormAlum?.['#text'] ?? null, norm_lower: mappedResponse.normLower,
norm_lower_included: norm_lower_included: mappedResponse.normLowerIncluded,
response.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah', norm_status: mappedResponse.normStatus,
norm_status: response.NormiStaatus, norm_upper: mappedResponse.normUpper,
norm_upper: response.NormYlem?.['#text'] ?? null, norm_upper_included: mappedResponse.normUpperIncluded,
norm_upper_included: response_time: mappedResponse.responseTime,
response.NormYlem?.['@_kaasaarvatud'].toLowerCase() === 'jah', response_value: mappedResponse.responseValue,
response_time: response.VastuseAeg ?? null,
response_value: responseValue,
unit: groupUuringElement.Mootyhik ?? null, unit: groupUuringElement.Mootyhik ?? null,
original_response_element: groupUuringElement, original_response_element: groupUuringElement,
analysis_name: groupUuringElement.UuringNimi || groupUuringElement.KNimetus, analysis_name: groupUuringElement.UuringNimi || groupUuringElement.KNimetus,
comment: groupUuringElement.UuringuKommentaar ?? null, comment: groupUuringElement.UuringuKommentaar ?? null,
status: status.toString(), status: status.toString(),
response_value_is_within_norm: responseValueIsNumeric ? null : responseValueIsWithinNorm, response_value_is_within_norm: mappedResponse.responseValueIsWithinNorm,
response_value_is_negative: responseValueIsNumeric ? null : responseValueIsNegative, response_value_is_negative: mappedResponse.responseValueIsNegative,
}); });
} }
} }
@@ -198,7 +196,7 @@ async function hasAllAnalysisResponseElements({
}) { }) {
const allOrderResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId }); const allOrderResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId });
const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0; const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0;
return allOrderResponseElements.length === expectedOrderResponseElements; return allOrderResponseElements.length >= expectedOrderResponseElements;
} }
export async function syncPrivateMessage({ export async function syncPrivateMessage({

View File

@@ -14,7 +14,7 @@ import {
import { import {
MaterjalideGrupp, MaterjalideGrupp,
} from '@/lib/types/medipost'; } from '@/lib/types/medipost';
import { toArray } from '@/lib/utils'; import { toArray } from '@kit/shared/utils';
import { uniqBy } from 'lodash'; import { uniqBy } from 'lodash';
import { Tables } from '@kit/supabase/database'; import { Tables } from '@kit/supabase/database';

View File

@@ -9,11 +9,6 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
} }
export function toArray<T>(input?: T | T[] | null): T[] {
if (!input) return [];
return Array.isArray(input) ? input : [input];
}
export function toTitleCase(str?: string) { export function toTitleCase(str?: string) {
return ( return (
str str

View File

@@ -4,6 +4,9 @@ const noop = (event: string) => {
// do nothing - this is to prevent errors when the analytics service is not initialized // do nothing - this is to prevent errors when the analytics service is not initialized
return async (...args: unknown[]) => { return async (...args: unknown[]) => {
if (typeof window !== 'undefined') {
return;
}
console.debug( console.debug(
`Noop analytics service called with event: ${event}`, `Noop analytics service called with event: ${event}`,
...args.filter(Boolean), ...args.filter(Boolean),

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

@@ -1,9 +1,10 @@
import { SupabaseClient } from '@supabase/supabase-js'; import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@kit/supabase/database'; import { Database } from '@kit/supabase/database';
import type { UuringElement, UuringuVastus } from '@kit/shared/types/medipost-analysis'; import { toArray } from '@kit/shared/utils';
import type { UuringuVastus } from '@kit/shared/types/medipost-analysis';
import type { AnalysisResultDetails, AnalysisResultDetailsMapped, UserAnalysis } from '../types/analysis-results'; import type { AnalysisResultsQuery, AnalysisResultDetailsElement, AnalysisResultDetailsMapped, AnalysisResultLevel, AnalysisResultsDetailsElementNested, AnalysisStatus, UserAnalysis } from '../types/analysis-results';
import type { AnalysisOrder } from '../types/analysis-orders'; import type { AnalysisOrder } from '../types/analysis-orders';
/** /**
@@ -43,31 +44,13 @@ class UserAnalysesApi {
async getUserAnalysis( async getUserAnalysis(
analysisOrderId: number, analysisOrderId: number,
): Promise<AnalysisResultDetailsMapped | null> { ): Promise<AnalysisResultDetailsMapped | null> {
const authUser = await this.client.auth.getUser();
const { data, error: userError } = authUser;
if (userError) {
console.error('Failed to get user', userError);
throw userError;
}
const { user } = data;
const analysisOrder = await this.getAnalysisOrder({ analysisOrderId }); const analysisOrder = await this.getAnalysisOrder({ analysisOrderId });
const orderedAnalysisElementIds = analysisOrder.analysis_element_ids ?? []; const orderedAnalysisElementIds = analysisOrder.analysis_element_ids ?? [];
if (orderedAnalysisElementIds.length === 0) { if (orderedAnalysisElementIds.length === 0) {
console.error('No ordered analysis element ids found for analysis order id=', analysisOrderId); console.error('No ordered analysis element ids found for analysis order id=', analysisOrderId);
return null; return null;
} }
const { data: orderedAnalysisElements, error: orderedAnalysisElementsError } = await this.client const orderedAnalysisElements = await this.getOrderedAnalysisElements({ analysisOrderId, orderedAnalysisElementIds });
.schema('medreport')
.from('analysis_elements')
.select('analysis_id_original,analysis_name_lab')
.in('id', orderedAnalysisElementIds);
if (orderedAnalysisElementsError) {
console.error('Failed to get ordered analysis elements for analysis order id=', analysisOrderId, orderedAnalysisElementsError);
throw orderedAnalysisElementsError;
}
const orderedAnalysisElementOriginalIds = orderedAnalysisElements.map(({ analysis_id_original }) => analysis_id_original); const orderedAnalysisElementOriginalIds = orderedAnalysisElements.map(({ analysis_id_original }) => analysis_id_original);
if (orderedAnalysisElementOriginalIds.length === 0) { if (orderedAnalysisElementOriginalIds.length === 0) {
@@ -75,6 +58,43 @@ class UserAnalysesApi {
return null; return null;
} }
const responseWithElements = await this.getAnalysisResponseWithElements({ analysisOrderId });
if (!responseWithElements) {
return null;
}
const mappedOrderedAnalysisElements = await this.getMappedOrderedAnalysisElements({
analysisResponseElements: responseWithElements.elements,
orderedAnalysisElements,
});
const feedback = responseWithElements.summary?.doctor_analysis_feedback?.[0];
return {
id: analysisOrderId,
order: {
status: analysisOrder.status,
medusaOrderId: analysisOrder.medusa_order_id,
createdAt: analysisOrder.created_at,
},
orderedAnalysisElements: mappedOrderedAnalysisElements,
summary:
feedback?.status === 'COMPLETED'
? (responseWithElements.summary?.doctor_analysis_feedback?.[0] ?? null)
: null,
};
}
async getAnalysisResponseWithElements({
analysisOrderId,
}: {
analysisOrderId: number;
}) {
const { data, error: userError } = await this.client.auth.getUser();
if (userError) {
throw userError;
}
const { user } = data;
const { data: analysisResponse } = await this.client const { data: analysisResponse } = await this.client
.schema('medreport') .schema('medreport')
.from('analysis_responses') .from('analysis_responses')
@@ -87,29 +107,52 @@ class UserAnalysesApi {
.eq('analysis_order_id', analysisOrderId) .eq('analysis_order_id', analysisOrderId)
.throwOnError(); .throwOnError();
const responseWithElements = analysisResponse?.[0] as AnalysisResultDetails | null; return analysisResponse?.[0] as AnalysisResultsQuery | null;
if (!responseWithElements) {
return null;
} }
const analysisResponseElements = responseWithElements.elements; async getOrderedAnalysisElements({
analysisOrderId,
const feedback = responseWithElements.summary?.doctor_analysis_feedback?.[0]; orderedAnalysisElementIds,
}: {
analysisOrderId: number;
orderedAnalysisElementIds: number[];
}) {
const { data: orderedAnalysisElements, error: orderedAnalysisElementsError } = await this.client
.schema('medreport')
.from('analysis_elements')
.select('analysis_id_original,analysis_name_lab')
.in('id', orderedAnalysisElementIds);
if (orderedAnalysisElementsError) {
console.error(`Failed to get ordered analysis elements for analysis order id=${analysisOrderId}`, orderedAnalysisElementsError);
throw orderedAnalysisElementsError;
}
return orderedAnalysisElements;
}
async getMappedOrderedAnalysisElements({
analysisResponseElements,
orderedAnalysisElements,
}: {
analysisResponseElements: AnalysisResultsQuery['elements'];
orderedAnalysisElements: { analysis_id_original: string; analysis_name_lab: string }[];
}): Promise<AnalysisResultDetailsElement[]> {
const mappedOrderedAnalysisElements = orderedAnalysisElements.map(({ analysis_id_original, analysis_name_lab }) => { const mappedOrderedAnalysisElements = orderedAnalysisElements.map(({ analysis_id_original, analysis_name_lab }) => {
return this.getOrderedAnalysisElements({ return this.getOrderedAnalysisElement({
analysisIdOriginal: analysis_id_original, analysisIdOriginal: analysis_id_original,
analysisNameLab: analysis_name_lab, analysisNameLab: analysis_name_lab,
analysisResponseElements, analysisResponseElements,
}); });
}).sort((a, b) => a.analysisName.localeCompare(b.analysisName)); }).sort((a, b) => a.analysisName.localeCompare(b.analysisName));
const nestedAnalysisElementIds = mappedOrderedAnalysisElements.map(({ results }) => results?.nestedElements.map(({ analysisElementOriginalId }) => analysisElementOriginalId)).flat().filter(Boolean);
const nestedAnalysisElementIds = mappedOrderedAnalysisElements
.map(({ results }) => results?.nestedElements.map(({ analysisElementOriginalId }) => analysisElementOriginalId))
.flat().filter(Boolean);
if (nestedAnalysisElementIds.length > 0) { if (nestedAnalysisElementIds.length > 0) {
const { data: nestedAnalysisElements, error: nestedAnalysisElementsError } = await this.client const { data: nestedAnalysisElements, error: nestedAnalysisElementsError } = await this.client
.schema('medreport') .schema('medreport')
.from('analysis_elements') .from('analysis_elements')
.select('*') .select('*')
.in('id', nestedAnalysisElementIds); .in('analysis_id_original', nestedAnalysisElementIds);
if (!nestedAnalysisElementsError && nestedAnalysisElements) { if (!nestedAnalysisElementsError && nestedAnalysisElements) {
for (const mappedOrderedAnalysisElement of mappedOrderedAnalysisElements) { for (const mappedOrderedAnalysisElement of mappedOrderedAnalysisElements) {
const { results } = mappedOrderedAnalysisElement; const { results } = mappedOrderedAnalysisElement;
@@ -118,58 +161,33 @@ class UserAnalysesApi {
} }
for (const nestedElement of results.nestedElements) { for (const nestedElement of results.nestedElements) {
const { analysisElementOriginalId } = nestedElement; const { analysisElementOriginalId } = nestedElement;
const nestedAnalysisElement = nestedAnalysisElements.find(({ id }) => id === analysisElementOriginalId); const nestedAnalysisElement = nestedAnalysisElements.find(({ analysis_id_original }) => analysis_id_original === analysisElementOriginalId);
if (!nestedAnalysisElement) { if (!nestedAnalysisElement) {
continue; continue;
} }
results.nestedElements.push({
...nestedAnalysisElement, nestedElement.analysisElementOriginalId = analysisElementOriginalId;
analysisElementOriginalId, nestedElement.analysisName = nestedAnalysisElement.analysis_name_lab as string | undefined;
analysisName: nestedAnalysisElement.analysis_name_lab, }
}); results.nestedElements = results.nestedElements.sort((a, b) => a.analysisName?.localeCompare(b.analysisName ?? '') ?? 0);
}
} else {
console.error('Failed to get nested analysis elements by ids=', nestedAnalysisElementIds, nestedAnalysisElementsError);
} }
} }
mappedOrderedAnalysisElements.forEach(({ results }) => { return mappedOrderedAnalysisElements;
results?.nestedElements.forEach(({ analysisElementOriginalId }) => {
const nestedAnalysisElement = nestedAnalysisElements.find(({ id }) => id === analysisElementOriginalId);
if (nestedAnalysisElement) {
results?.nestedElements.push({
...nestedAnalysisElement,
analysisElementOriginalId,
analysisName: nestedAnalysisElement.analysis_name_lab,
});
}
});
});
}
} }
return { getOrderedAnalysisElement({
id: analysisOrderId,
order: {
status: analysisOrder.status,
medusaOrderId: analysisOrder.medusa_order_id,
createdAt: new Date(analysisOrder.created_at),
},
orderedAnalysisElementIds,
orderedAnalysisElements: mappedOrderedAnalysisElements,
summary:
feedback?.status === 'COMPLETED'
? (responseWithElements.summary?.doctor_analysis_feedback?.[0] ?? null)
: null,
};
}
getOrderedAnalysisElements({
analysisIdOriginal, analysisIdOriginal,
analysisNameLab, analysisNameLab,
analysisResponseElements, analysisResponseElements,
}: { }: {
analysisIdOriginal: string; analysisIdOriginal: string;
analysisNameLab: string; analysisNameLab: string;
analysisResponseElements: AnalysisResultDetails['elements']; analysisResponseElements: AnalysisResultsQuery['elements'];
}) { }): AnalysisResultDetailsElement {
const elementResponse = analysisResponseElements.find((element) => element.analysis_element_original_id === analysisIdOriginal); const elementResponse = analysisResponseElements.find((element) => element.analysis_element_original_id === analysisIdOriginal);
if (!elementResponse) { if (!elementResponse) {
return { return {
@@ -184,51 +202,74 @@ class UserAnalysesApi {
isWaitingForResults: false, isWaitingForResults: false,
analysisName: analysisNameLab, analysisName: analysisNameLab,
results: { results: {
nestedElements: (() => { nestedElements: ((): AnalysisResultsDetailsElementNested[] => {
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 []; const mappedResponse = this.mapUuringVastus({
} uuringVastus: element.UuringuVastus as UuringuVastus | undefined,
return nestedElements.map((element) => { });
const elementVastus = element.UuringuVastus as UuringuVastus | undefined;
const responseValue = elementVastus?.VastuseVaartus;
const responseValueIsNumeric = !isNaN(Number(responseValue));
const responseValueIsNegative = responseValue === 'Negatiivne';
const responseValueIsWithinNorm = responseValue === 'Normi piires';
return { return {
status: element.UuringOlek, unit: element.Mootyhik ?? null,
unit: element.Mootyhik, normLower: mappedResponse.normLower,
normLower: elementVastus?.NormAlum?.['#text'], normUpper: mappedResponse.normUpper,
normUpper: elementVastus?.NormYlem?.['#text'], normStatus: mappedResponse.normStatus,
normStatus: elementVastus?.NormiStaatus, responseTime: mappedResponse.responseTime,
responseTime: elementVastus?.VastuseAeg, responseValue: mappedResponse.responseValue,
response_value: responseValueIsNegative || !responseValueIsNumeric ? null : (responseValue ?? null), responseValueIsNegative: mappedResponse.responseValueIsNegative,
response_value_is_negative: responseValueIsNumeric ? null : responseValueIsNegative, responseValueIsWithinNorm: mappedResponse.responseValueIsWithinNorm,
response_value_is_within_norm: responseValueIsNumeric ? null : responseValueIsWithinNorm, normLowerIncluded: mappedResponse.normLowerIncluded,
normLowerIncluded: elementVastus?.NormAlum?.['@_kaasaarvatud'] === 'JAH', normUpperIncluded: mappedResponse.normUpperIncluded,
normUpperIncluded: elementVastus?.NormYlem?.['@_kaasaarvatud'] === 'JAH',
analysisElementOriginalId: element.UuringId, analysisElementOriginalId: element.UuringId,
status: Number(elementResponse.status) as AnalysisStatus,
analysisName: undefined,
}; };
}); });
})(), })(),
labComment, labComment,
//originalResponseElement: elementResponse.original_response_element ?? null,
unit: elementResponse.unit, unit: elementResponse.unit,
normLower: elementResponse.norm_lower, normLower: elementResponse.norm_lower,
normUpper: elementResponse.norm_upper, normUpper: elementResponse.norm_upper,
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: Number(elementResponse.status) as AnalysisStatus,
analysisElementOriginalId: elementResponse.analysis_element_original_id, analysisElementOriginalId: elementResponse.analysis_element_original_id,
} }
}; };
} }
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);
const responseValueIsNegative = vastuseVaartus === 'Negatiivne';
const responseValueIsWithinNorm = vastuseVaartus === 'Normi piires';
return {
normLower: uuringVastus?.NormAlum?.['#text'] ?? null,
normUpper: uuringVastus?.NormYlem?.['#text'] ?? null,
normStatus: (uuringVastus?.NormiStaatus ?? null) as AnalysisResultLevel | null,
responseTime: uuringVastus?.VastuseAeg ?? null,
responseValue: responseValueIsNegative || !responseValueIsNumeric ? null : (responseValueNumber ?? null),
responseValueIsNegative: responseValueIsNumeric ? null : responseValueIsNegative,
responseValueIsWithinNorm: responseValueIsNumeric ? null : responseValueIsWithinNorm,
normLowerIncluded:
uuringVastus?.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah',
normUpperIncluded:
uuringVastus?.NormYlem?.['@_kaasaarvatud'].toLowerCase() === 'jah',
};
}
// @TODO unused currently // @TODO unused currently
async getUserAnalyses(): Promise<UserAnalysis | null> { async getUserAnalyses(): Promise<UserAnalysis | null> {
const authUser = await this.client.auth.getUser(); const authUser = await this.client.auth.getUser();

View File

@@ -1,6 +1,7 @@
import * as z from 'zod'; import type { Database } from '@kit/supabase/database';
import type { AnalysisOrderStatus, NormStatus } from '@kit/shared/types/medipost-analysis';
import { Database } from '@kit/supabase/database'; import type { AnalysisOrder } from './analysis-orders';
export type UserAnalysisElement = export type UserAnalysisElement =
Database['medreport']['Tables']['analysis_response_elements']['Row']; Database['medreport']['Tables']['analysis_response_elements']['Row'];
@@ -10,92 +11,131 @@ export type UserAnalysisResponse =
}; };
export type UserAnalysis = UserAnalysisResponse[]; export type UserAnalysis = UserAnalysisResponse[];
const ElementSchema = z.object({ export type AnalysisResultsQuery = {
unit: z.string(), id: number,
norm_lower: z.number(), analysis_order_id: number,
norm_upper: z.number(), order_number: string,
norm_status: z.number(), order_status: string,
analysis_name: z.string(), user_id: string,
response_time: z.string(), created_at: string,
response_value: z.number(), updated_at: string | null,
response_value_is_negative: z.boolean(), elements: {
response_value_is_within_norm: z.boolean(), unit: string,
norm_lower_included: z.boolean(), norm_lower: number,
norm_upper_included: z.boolean(), norm_upper: number,
status: z.string(), norm_status: number,
analysis_element_original_id: z.string(), analysis_name: string,
original_response_element: z.object({ response_time: string,
response_value: number,
response_value_is_negative: boolean,
response_value_is_within_norm: boolean,
norm_lower_included: boolean,
norm_upper_included: boolean,
status: string,
analysis_element_original_id: string,
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,
},
}[],
order: {
status: string,
medusa_order_id: string,
created_at: string,
},
summary: {
id: number,
value: string,
status: string,
user_id: 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: {
id: number,
status: string,
user_id: string,
created_at: string,
created_by: string,
}[],
} | null,
};
}), export type AnalysisResultsDetailsElementNested = {
}); analysisElementOriginalId: string;
analysisName?: string;
} & Pick<
AnalysisResultDetailsElementResults,
'unit' |
'normLower' |
'normUpper' |
'normStatus' |
'responseTime' |
'responseValue' |
'responseValueIsNegative' |
'responseValueIsWithinNorm' |
'normLowerIncluded' |
'normUpperIncluded' |
'status' |
'analysisElementOriginalId'
>;
const OrderSchema = z.object({ export enum AnalysisResultLevel {
status: z.string(), NORMAL = 0,
medusa_order_id: z.string(), WARNING = 1,
created_at: z.coerce.date(), CRITICAL = 2,
}); }
const DoctorAnalysisFeedbackSchema = z.object({ export enum AnalysisStatus {
id: z.number(), QUEUED = 1,
status: z.string(), PENDING = 2,
user_id: z.string(), ONGOING = 3,
created_at: z.coerce.date(), COMPLETED = 4,
created_by: z.string(), REFUSED = 5,
}); CANCELLED = 6,
}
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 = { export type AnalysisResultDetailsElementResults = {
unit: string | null; unit: string | null;
normLower: number | null; normLower: number | null;
normUpper: number | null; normUpper: number | null;
normStatus: number | null; normStatus: AnalysisResultLevel | null;
responseTime: string | null; responseTime: string | null;
responseValue: number | null; responseValue: number | null;
responseValueIsNegative: boolean | null; responseValueIsNegative: boolean | null;
responseValueIsWithinNorm: boolean | null; responseValueIsWithinNorm: boolean | null;
normLowerIncluded: boolean; normLowerIncluded: boolean;
normUpperIncluded: boolean; normUpperIncluded: boolean;
status: string; status: AnalysisStatus;
analysisElementOriginalId: string; analysisElementOriginalId: string;
nestedElements: { nestedElements: AnalysisResultsDetailsElementNested[];
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; labComment?: string | null;
}; };
@@ -103,37 +143,21 @@ export type AnalysisResultDetailsElement = {
analysisIdOriginal: string; analysisIdOriginal: string;
isWaitingForResults: boolean; isWaitingForResults: boolean;
analysisName: string; analysisName: string;
results: AnalysisResultDetailsElementResults; results?: AnalysisResultDetailsElementResults;
}; };
export type AnalysisResultDetailsMapped = { export type AnalysisResultDetailsMapped = {
id: number; id: number;
order: { order: {
status: string;
medusaOrderId: string; medusaOrderId: string;
createdAt: Date | string; createdAt: string;
}; } & Pick<AnalysisOrder, 'status'>;
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[]; 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;

View File

@@ -2,6 +2,9 @@ import { MonitoringService } from '@kit/monitoring-core';
export class ConsoleMonitoringService implements MonitoringService { export class ConsoleMonitoringService implements MonitoringService {
identifyUser(data: { id: string }) { identifyUser(data: { id: string }) {
if (typeof window !== 'undefined') {
return;
}
console.log(`[Console Monitoring] Identified user`, data); console.log(`[Console Monitoring] Identified user`, data);
} }
@@ -12,6 +15,9 @@ export class ConsoleMonitoringService implements MonitoringService {
} }
captureEvent(event: string) { captureEvent(event: string) {
if (typeof window !== 'undefined') {
return;
}
console.log(`[Console Monitoring] Captured event: ${event}`); console.log(`[Console Monitoring] Captured event: ${event}`);
} }

View File

@@ -57,3 +57,8 @@ export const getPersonParameters = (personalCode: string) => {
return null; return null;
} }
}; };
export function toArray<T>(input?: T | T[] | null): T[] {
if (!input) return [];
return Array.isArray(input) ? input : [input];
}

View File

@@ -685,7 +685,7 @@ export type Database = {
norm_upper: number | null norm_upper: number | null
norm_upper_included: boolean | null norm_upper_included: boolean | null
original_response_element: Json original_response_element: Json
response_time: string response_time: string | null
response_value: number | null response_value: number | null
response_value_is_negative?: boolean | null response_value_is_negative?: boolean | null
response_value_is_within_norm?: boolean | null response_value_is_within_norm?: boolean | null
@@ -706,7 +706,7 @@ export type Database = {
norm_upper?: number | null norm_upper?: number | null
norm_upper_included?: boolean | null norm_upper_included?: boolean | null
original_response_element: Json original_response_element: Json
response_time: string response_time: string | null
response_value: number | null response_value: number | null
response_value_is_negative?: boolean | null response_value_is_negative?: boolean | null
response_value_is_within_norm?: boolean | null response_value_is_within_norm?: boolean | null
@@ -727,7 +727,7 @@ export type Database = {
norm_upper?: number | null norm_upper?: number | null
norm_upper_included?: boolean | null norm_upper_included?: boolean | null
original_response_element?: Json original_response_element?: Json
response_time?: string response_time?: string | null
response_value?: number | null response_value?: number | null
response_value_is_negative?: boolean | null response_value_is_negative?: boolean | null
response_value_is_within_norm?: boolean | null response_value_is_within_norm?: boolean | null

View File

@@ -158,11 +158,11 @@ export function PageHeader({
return ( return (
<div <div
className={cn( className={cn(
'flex items-center justify-between py-5', 'flex py-5 flex-col sm:flex-row items-start sm:items-center sm:justify-between',
className, className,
)} )}
> >
<div className={'flex flex-col gap-y-2'}> <div className={'flex flex-col gap-y-4 sm:gap-y-2'}>
<If condition={title}> <If condition={title}>
<PageTitle>{title}</PageTitle> <PageTitle>{title}</PageTitle>
</If> </If>

View File

@@ -1,6 +1,7 @@
{ {
"pageTitle": "My analysis results", "pageTitle": "My analysis results",
"description": "All analysis results will appear here within 1-3 business days after they have been done.", "description": "",
"descriptionPartial": "All analysis results will appear here within 1-3 business days after they have been done.",
"descriptionEmpty": "If you've already done your analysis, your results will appear here soon.", "descriptionEmpty": "If you've already done your analysis, your results will appear here soon.",
"orderNewAnalysis": "Order new analyses", "orderNewAnalysis": "Order new analyses",
"waitingForResults": "Waiting for results", "waitingForResults": "Waiting for results",

View File

@@ -1,6 +1,7 @@
{ {
"pageTitle": "Minu analüüside vastused", "pageTitle": "Minu analüüside vastused",
"description": "Kõikide analüüside tulemused ilmuvad 1-3 tööpäeva jooksul peale nende andmist.", "description": "",
"descriptionPartial": "Kõikide analüüside tulemused ilmuvad 1-3 tööpäeva jooksul peale nende andmist.",
"descriptionEmpty": "Kui oled juba käinud analüüse andmas, siis varsti jõuavad siia sinu analüüside vastused.", "descriptionEmpty": "Kui oled juba käinud analüüse andmas, siis varsti jõuavad siia sinu analüüside vastused.",
"orderNewAnalysis": "Telli uued analüüsid", "orderNewAnalysis": "Telli uued analüüsid",
"waitingForResults": "Tulemuse ootel", "waitingForResults": "Tulemuse ootel",

View File

@@ -1,6 +1,7 @@
{ {
"pageTitle": "Мои результаты анализов", "pageTitle": "Мои результаты анализов",
"description": "Все результаты анализов появляются в течение 1-3 рабочих дней после их сдачи.", "description": "",
"descriptionPartial": "Все результаты анализов появляются в течение 1-3 рабочих дней после их сдачи.",
"descriptionEmpty": "Если вы уже сдали анализы, то вскоре здесь появятся ваши результаты.", "descriptionEmpty": "Если вы уже сдали анализы, то вскоре здесь появятся ваши результаты.",
"orderNewAnalysis": "Заказать новые анализы", "orderNewAnalysis": "Заказать новые анализы",
"waitingForResults": "Ожидание результатов", "waitingForResults": "Ожидание результатов",

View File

@@ -75,11 +75,11 @@
} }
h5 { h5 {
@apply text-base; @apply text-lg;
} }
h6 { h6 {
@apply text-lg; @apply text-base;
} }
.lucide { .lucide {