- {analysisResponse.elements ? (
- analysisResponse.elements.map((element, index) => (
-
+ {orderedAnalysisElements ? (
+ orderedAnalysisElements.map((element, index) => (
+
diff --git a/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx b/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx
index 71a7036..dadc42b 100644
--- a/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx
+++ b/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx
@@ -3,14 +3,12 @@ import { useMemo } from 'react';
import { ArrowDown } from 'lucide-react';
import { cn } from '@kit/ui/utils';
-import { AnalysisResultForDisplay } from './analysis';
+import { AnalysisResultDetailsElementResults } from '@/packages/features/accounts/src/types/analysis-results';
export enum AnalysisResultLevel {
- VERY_LOW = 0,
- LOW = 1,
- NORMAL = 2,
- HIGH = 3,
- VERY_HIGH = 4,
+ NORMAL = 0,
+ WARNING = 1,
+ CRITICAL = 2,
}
const Level = ({
@@ -19,17 +17,19 @@ const Level = ({
isFirst = false,
isLast = false,
arrowLocation,
+ normRangeText,
}: {
isActive?: boolean;
color: 'destructive' | 'success' | 'warning' | 'gray-200';
isFirst?: boolean;
isLast?: boolean;
arrowLocation?: number;
+ normRangeText?: string | null;
}) => {
return (
92.5 && { left: '92.5%' }),
+ ...(arrowLocation < 7.5 && { left: '7.5%' }),
+ }
+ } : {})}
>
)}
+
+ {color === 'success' && typeof normRangeText === 'string' && (
+
+ {normRangeText}
+
+ )}
+
);
};
const AnalysisLevelBar = ({
- normLowerIncluded = true,
- normUpperIncluded = true,
level,
results,
+ normRangeText,
}: {
- normLowerIncluded?: boolean;
- normUpperIncluded?: boolean;
level: AnalysisResultLevel;
- results: AnalysisResultForDisplay;
+ results: AnalysisResultDetailsElementResults;
+ normRangeText: string | null;
}) => {
- const { norm_lower: lower, norm_upper: upper, response_value: value } = results;
+ 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 (value < lower!) {
- return 0;
- }
-
- if (normLowerIncluded || normUpperIncluded) {
+ // If no response value, center the arrow
+ if (value === null || value === undefined) {
return 50;
}
- const calculated = ((value - lower!) / (upper! - lower!)) * 100;
-
- if (calculated > 100) {
- return 100;
+ // If no normal ranges defined, center the arrow
+ if (lower === null && upper === null) {
+ return 50;
}
-
- return calculated;
+
+ // 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
+ }
+ return 100; // Beyond upper bound
+ }
+
+ // If only lower bound exists
+ if (upper === null && lower !== null) {
+ 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
+ return normalizedPosition * 100;
+ }
+ // Value is below lower bound - position in the "below normal" section
+ const belowPosition = Math.max(0, Math.min(1, value / lower));
+ return belowPosition * 100;
+ }
+
+ // Both bounds exist
+ if (lower !== null && upper !== null) {
+ if (value < lower) {
+ return 0; // Below normal range
+ }
+ if (value > upper) {
+ return 100; // Above normal range
+ }
+ // Within normal range
+ return ((value - lower) / (upper - lower)) * 100;
+ }
+
+ return 50; // Fallback
}, [value, upper, lower]);
- const [isVeryLow, isLow, isHigh, isVeryHigh] = useMemo(() => [
- level === AnalysisResultLevel.VERY_LOW,
- level === AnalysisResultLevel.LOW,
- level === AnalysisResultLevel.HIGH,
- level === AnalysisResultLevel.VERY_HIGH,
- ], [level, value, upper, lower]);
+ // Determine level states based on normStatus
+ const isNormal = level === AnalysisResultLevel.NORMAL;
+ const isWarning = level === AnalysisResultLevel.WARNING;
+ const isCritical = level === AnalysisResultLevel.CRITICAL;
+ const isPending = level === null;
- const hasAbnormalLevel = isVeryLow || isLow || isHigh || isVeryHigh;
+ // If pending results, show gray bar
+ if (isPending) {
+ return (
+
+
+
+ );
+ }
+
+ // 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
+ const isValueBelowLower = hasLowerBound && value !== null && value < lower!;
+ const isValueAboveUpper = hasUpperBound && value !== null && value > upper!;
+ 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 [
+ {
+ isActive: isWarning,
+ color: "warning",
+ isFirst: true,
+ ...(isWarning ? { arrowLocation } : {}),
+ },
+ {
+ isActive: isNormal,
+ color: "success",
+ normRangeText,
+ ...(isNormal ? { arrowLocation } : {}),
+ },
+ {
+ isActive: isCritical,
+ color: "destructive",
+ isLast: true,
+ ...(isCritical ? { arrowLocation } : {}),
+ },
+ ] as const;
+ }, [isValueBelowLower, isValueAboveUpper, isValueInNormalRange, arrowLocation, normRangeText, isNormal, isWarning, isCritical]);
return (
-
- {normLowerIncluded && (
- <>
-
-
- >
- )}
-
-
-
- {normUpperIncluded && (
- <>
-
-
- >
- )}
+
+
+
+
);
};
diff --git a/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx b/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx
index c88e9c3..d19d0d1 100644
--- a/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx
+++ b/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx
@@ -1,33 +1,19 @@
'use client';
-import React, { ReactElement, ReactNode, useMemo, useState } from 'react';
+import React, { useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
-import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts';
+import { AnalysisResultDetailsElement } from '@/packages/features/accounts/src/types/analysis-results';
import { format } from 'date-fns';
import { Info } from 'lucide-react';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
-import { AnalysisElement } from '~/lib/services/analysis-element.service';
-
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 enum AnalysisStatus {
NORMAL = 0,
MEDIUM = 1,
@@ -35,26 +21,45 @@ export enum AnalysisStatus {
}
const Analysis = ({
- analysisElement,
- results,
- startIcon,
- endIcon,
- isCancelled,
+ element,
}: {
- analysisElement: Pick
;
- results?: AnalysisResultForDisplay;
- isCancelled?: boolean;
- startIcon?: ReactElement | null;
- endIcon?: ReactNode | null;
+ element: AnalysisResultDetailsElement;
}) => {
- const name = analysisElement.analysis_name_lab || '';
- const status = results?.norm_status || AnalysisStatus.NORMAL;
- const value = results?.response_value || 0;
+ const { t } = useTranslation();
+
+ const name = element.analysisName || '';
+ const results = element.results;
+
+ const hasIsWithinNorm = results?.responseValueIsWithinNorm !== null;
+ const hasIsNegative = results?.responseValueIsNegative !== null;
+
+ const value = (() => {
+ if (!results) {
+ return null;
+ }
+
+ const { responseValue, responseValueIsNegative, responseValueIsWithinNorm } = results;
+ if (responseValue === null || responseValue === undefined) {
+ if (hasIsNegative) {
+ if (responseValueIsNegative) {
+ return t('analysis-results:results.value.negative');
+ }
+ return t('analysis-results:results.value.positive');
+ }
+ if (hasIsWithinNorm) {
+ if (responseValueIsWithinNorm) {
+ return t('analysis-results:results.value.isWithinNorm');
+ }
+ return t('analysis-results:results.value.isNotWithinNorm');
+ }
+ return null;
+ }
+
+ return responseValue;
+ })();
const unit = results?.unit || '';
- const normLowerIncluded = results?.norm_lower_included || false;
- const normUpperIncluded = results?.norm_upper_included || false;
- const normLower = results?.norm_lower || 0;
- const normUpper = results?.norm_upper || 0;
+ const normLower = results?.normLower;
+ const normUpper = results?.normUpper;
const [showTooltip, setShowTooltip] = useState(false);
const analysisResultLevel = useMemo(() => {
@@ -62,32 +67,34 @@ const Analysis = ({
return null;
}
- const isUnderNorm = value < normLower;
- if (isUnderNorm) {
- switch (status) {
- case AnalysisStatus.MEDIUM:
- return AnalysisResultLevel.LOW;
- default:
- return AnalysisResultLevel.VERY_LOW;
- }
+ if (results.responseValue === null || results.responseValue === undefined) {
+ return null;
}
- switch (status) {
- case AnalysisStatus.MEDIUM:
- return AnalysisResultLevel.HIGH;
- case AnalysisStatus.HIGH:
- return AnalysisResultLevel.VERY_HIGH;
+
+ const normStatus = results.normStatus;
+
+ switch (normStatus) {
+ case 1:
+ return AnalysisResultLevel.WARNING;
+ case 2:
+ return AnalysisResultLevel.CRITICAL;
+ case 0:
default:
return AnalysisResultLevel.NORMAL;
}
- }, [results, value, normLower]);
+ }, [results]);
+
+ const isCancelled = Number(results?.status) === 5;
+ const hasNestedElements = results?.nestedElements.length > 0;
+
+ const normRangeText = normLower !== null ? `${normLower} - ${normUpper || ''}` : null;
return (
-
+
- {startIcon ||
}
{name}
- {results?.response_time && (
+ {results?.responseTime && (
{
@@ -105,42 +112,41 @@ const Analysis = ({
>
{': '}
- {format(new Date(results.response_time), 'dd.MM.yyyy HH:mm')}
+ {format(new Date(results.responseTime), 'dd.MM.yyyy HH:mm')}
)}
- {results ? (
+
+ {isCancelled && (
+
+
+
+ )}
+
+ {isCancelled || !results || hasNestedElements ? null : (
<>
-
- {normLower} - {normUpper}
-
-
-
-
-
- {endIcon ||
}
+ {!(hasIsNegative || hasIsWithinNorm) && (
+ <>
+
+ {normRangeText}
+
+
+
+
+
+ >
+ )}
>
- ) : (isCancelled ? null : (
- <>
-
-
-
- >
- ))}
+ )}
);
diff --git a/app/home/(user)/_lib/server/load-user-analysis.ts b/app/home/(user)/_lib/server/load-user-analysis.ts
index 09efd46..77c40bd 100644
--- a/app/home/(user)/_lib/server/load-user-analysis.ts
+++ b/app/home/(user)/_lib/server/load-user-analysis.ts
@@ -1,8 +1,8 @@
import { cache } from 'react';
-import { createAccountsApi } from '@kit/accounts/api';
-import { AnalysisResultDetails } from '@kit/accounts/types/accounts';
+import { AnalysisResultDetailsMapped } from '@kit/accounts/types/analysis-results';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
+import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
export type UserAnalyses = Awaited>;
@@ -15,9 +15,9 @@ export const loadUserAnalysis = cache(analysisLoader);
async function analysisLoader(
analysisOrderId: number,
-): Promise {
+): Promise {
const client = getSupabaseServerClient();
- const api = createAccountsApi(client);
+ const api = createUserAnalysesApi(client);
return api.getUserAnalysis(analysisOrderId);
}
diff --git a/lib/services/analysis-order.service.ts b/lib/services/analysis-order.service.ts
index 2d0f77e..cd57b9a 100644
--- a/lib/services/analysis-order.service.ts
+++ b/lib/services/analysis-order.service.ts
@@ -1,12 +1,13 @@
import { getSupabaseServerAdminClient } from "@/packages/supabase/src/clients/server-admin-client";
-import type { AnalysisResponseElement } from "../types/analysis-response-element";
import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis';
+import type { AnalysisResponseElement } from "../types/analysis-response-element";
+
export async function getExistingAnalysisResponseElements({
analysisResponseId,
}: {
analysisResponseId: number;
-}) {
+}): Promise {
const { data } = await getSupabaseServerAdminClient()
.schema('medreport')
.from('analysis_response_elements')
@@ -17,6 +18,18 @@ export async function getExistingAnalysisResponseElements({
return data as AnalysisResponseElement[];
}
+export async function createAnalysisResponseElement({
+ element,
+}: {
+ element: Omit;
+}) {
+ await getSupabaseServerAdminClient()
+ .schema('medreport')
+ .from('analysis_response_elements')
+ .insert(element)
+ .throwOnError();
+}
+
export async function upsertAnalysisResponse({
analysisOrderId,
orderNumber,
diff --git a/lib/services/medipost/medipost.types.ts b/lib/services/medipost/medipost.types.ts
index 3f14de8..ed2cbca 100644
--- a/lib/services/medipost/medipost.types.ts
+++ b/lib/services/medipost/medipost.types.ts
@@ -68,7 +68,7 @@ export interface IMedipostPublicMessageDataParsed {
Koefitsient: number;
Hind: number;
}[];
- UuringuElement: IUuringElement;
+ UuringuElement?: IUuringElement[];
}[];
MaterjalideGrupp: IMaterialGroup[];
Kood: {
diff --git a/lib/services/medipost/medipostMessageBase.service.ts b/lib/services/medipost/medipostMessageBase.service.ts
index 5faf012..ef452ec 100644
--- a/lib/services/medipost/medipostMessageBase.service.ts
+++ b/lib/services/medipost/medipostMessageBase.service.ts
@@ -4,7 +4,7 @@ import type { Message } from '@/lib/types/medipost';
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
-export function getLatestMessage({
+export async function getLatestMessage({
messages,
excludedMessageIds,
}: {
diff --git a/lib/services/medipost/medipostPrivateMessage.service.ts b/lib/services/medipost/medipostPrivateMessage.service.ts
index ba19fc2..f6356e3 100644
--- a/lib/services/medipost/medipostPrivateMessage.service.ts
+++ b/lib/services/medipost/medipostPrivateMessage.service.ts
@@ -11,7 +11,7 @@ import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analys
import type {
ResponseUuringuGrupp,
MedipostOrderResponse,
- ResponseUuring,
+ UuringElement,
} from '@/packages/shared/src/types/medipost-analysis';
import { toArray } from '@/lib/utils';
import type { AnalysisOrder } from '~/lib/types/analysis-order';
@@ -29,7 +29,7 @@ import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service';
import { getAccountAdmin } from '../account.service';
import { logMedipostDispatch } from '../audit.service';
import { MedipostValidationError } from './MedipostValidationError';
-import { getExistingAnalysisResponseElements, upsertAnalysisResponse } from '../analysis-order.service';
+import { createAnalysisResponseElement, getExistingAnalysisResponseElements, upsertAnalysisResponse } from '../analysis-order.service';
const BASE_URL = process.env.MEDIPOST_URL!;
const USER = process.env.MEDIPOST_USER!;
@@ -55,7 +55,7 @@ export async function getLatestPrivateMessageListItem({
throw new Error('Failed to get private message list');
}
- return getLatestMessage({ messages: data?.messages, excludedMessageIds });
+ return await getLatestMessage({ messages: data?.messages, excludedMessageIds });
}
const logger = (analysisOrder: AnalysisOrder, externalId: string, analysisResponseId: string) => (message: string, error?: PostgrestError | null) => {
@@ -67,7 +67,7 @@ const logger = (analysisOrder: AnalysisOrder, externalId: string, analysisRespon
}
};
-function canCreateAnalysisResponseElement({
+export async function canCreateAnalysisResponseElement({
existingElements,
groupUuring: {
UuringuElement: {
@@ -78,8 +78,8 @@ function canCreateAnalysisResponseElement({
responseValue,
log,
}: {
- existingElements: AnalysisResponseElement[];
- groupUuring: ResponseUuring;
+ existingElements: Pick[];
+ groupUuring: { UuringuElement: Pick };
responseValue: number | null;
log: ReturnType;
}) {
@@ -102,21 +102,19 @@ function canCreateAnalysisResponseElement({
}
-async function getAnalysisResponseElementsForGroup({
- analysisResponseId,
+export async function getAnalysisResponseElementsForGroup({
analysisGroup,
+ existingElements,
log,
}: {
- analysisResponseId: number;
- analysisGroup: ResponseUuringuGrupp;
+ analysisGroup: Pick;
+ existingElements: Pick[];
log: ReturnType;
}) {
const groupUuringItems = toArray(analysisGroup.Uuring as ResponseUuringuGrupp['Uuring']);
log(`Order has results in group '${analysisGroup.UuringuGruppNimi}' for ${groupUuringItems.length} analysis elements`);
- const existingElements = await getExistingAnalysisResponseElements({ analysisResponseId });
-
- const results: Omit[] = [];
+ const results: Omit[] = [];
for (const groupUuring of groupUuringItems) {
const groupUuringElement = groupUuring.UuringuElement;
@@ -127,21 +125,25 @@ async function getAnalysisResponseElementsForGroup({
for (const response of elementAnalysisResponses) {
const analysisElementOriginalId = groupUuringElement.UuringId;
+ const vastuseVaartus = response.VastuseVaartus;
const responseValue = (() => {
- const valueAsNumber = Number(response.VastuseVaartus);
+ const valueAsNumber = Number(vastuseVaartus);
if (isNaN(valueAsNumber)) {
return null;
}
return valueAsNumber;
})();
- if (!canCreateAnalysisResponseElement({ existingElements, groupUuring, responseValue, log })) {
+ if (!await canCreateAnalysisResponseElement({ existingElements, groupUuring, responseValue, log })) {
continue;
}
+ const responseValueIsNumeric = responseValue !== null;
+ const responseValueIsNegative = vastuseVaartus === 'Negatiivne';
+ const responseValueIsWithinNorm = vastuseVaartus === 'Normi piires';
+
results.push({
analysis_element_original_id: analysisElementOriginalId,
- analysis_response_id: analysisResponseId,
norm_lower: response.NormAlum?.['#text'] ?? null,
norm_lower_included:
response.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah',
@@ -156,6 +158,8 @@ async function getAnalysisResponseElementsForGroup({
analysis_name: groupUuringElement.UuringNimi || groupUuringElement.KNimetus,
comment: groupUuringElement.UuringuKommentaar ?? null,
status: status.toString(),
+ response_value_is_within_norm: responseValueIsNumeric ? null : responseValueIsWithinNorm,
+ response_value_is_negative: responseValueIsNumeric ? null : responseValueIsNegative,
});
}
}
@@ -163,18 +167,55 @@ async function getAnalysisResponseElementsForGroup({
return results;
}
-export async function syncPrivateMessage({
- messageResponse,
+async function getNewAnalysisResponseElements({
+ analysisGroups,
+ existingElements,
+ log,
+}: {
+ analysisGroups: ResponseUuringuGrupp[];
+ existingElements: AnalysisResponseElement[];
+ log: ReturnType;
+}) {
+ const newElements: Omit[] = [];
+ for (const analysisGroup of analysisGroups) {
+ log(`[${analysisGroups.indexOf(analysisGroup) + 1}/${analysisGroups.length}] Syncing analysis group '${analysisGroup.UuringuGruppNimi}'`);
+ const elements = await getAnalysisResponseElementsForGroup({
+ analysisGroup,
+ existingElements,
+ log,
+ });
+ newElements.push(...elements);
+ }
+ return newElements;
+}
+
+async function hasAllAnalysisResponseElements({
+ analysisResponseId,
order,
}: {
- messageResponse: NonNullable;
+ analysisResponseId: number;
+ order: Pick;
+}) {
+ const allOrderResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId });
+ const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0;
+ return allOrderResponseElements.length === expectedOrderResponseElements;
+}
+
+export async function syncPrivateMessage({
+ messageResponse: {
+ ValisTellimuseId: externalId,
+ TellimuseNumber: orderNumber,
+ TellimuseOlek,
+ UuringuGrupp,
+ },
+ order,
+}: {
+ messageResponse: Pick, 'ValisTellimuseId' | 'TellimuseNumber' | 'TellimuseOlek' | 'UuringuGrupp'>;
order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
}) {
const supabase = getSupabaseServerAdminClient();
- const externalId = messageResponse.ValisTellimuseId;
- const orderNumber = messageResponse.TellimuseNumber;
- const orderStatus = AnalysisOrderStatus[messageResponse.TellimuseOlek];
+ const orderStatus = AnalysisOrderStatus[TellimuseOlek];
const log = logger(order, externalId, orderNumber);
@@ -193,37 +234,28 @@ export async function syncPrivateMessage({
userId: analysisOrder.user_id,
});
- const analysisGroups = toArray(messageResponse.UuringuGrupp);
+ const existingElements = await getExistingAnalysisResponseElements({ analysisResponseId });
+ const analysisGroups = toArray(UuringuGrupp);
log(`Order has results for ${analysisGroups.length} analysis groups`);
+ const newElements = await getNewAnalysisResponseElements({ analysisGroups, existingElements, log });
- for (const analysisGroup of analysisGroups) {
- log(`[${analysisGroups.indexOf(analysisGroup) + 1}/${analysisGroups.length}] Syncing analysis group '${analysisGroup.UuringuGruppNimi}'`);
-
- const elements = await getAnalysisResponseElementsForGroup({
- analysisResponseId,
- analysisGroup,
- log,
- });
-
- for (const element of elements) {
- const { error } = await supabase
- .schema('medreport')
- .from('analysis_response_elements')
- .insert(element);
- if (error) {
- log(`Failed to insert order response elements for response id ${analysisResponseId} (order id: ${analysisOrder.id})`, error);
- }
+ for (const element of newElements) {
+ try {
+ await createAnalysisResponseElement({
+ element: {
+ ...element,
+ analysis_response_id: analysisResponseId,
+ },
+ });
+ } catch (e) {
+ log(`Failed to create order response element for response id ${analysisResponseId}, element id '${element.analysis_element_original_id}' (order id: ${order.id})`, e as PostgrestError);
}
}
- const allOrderResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId });
- const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0;
- if (allOrderResponseElements.length !== expectedOrderResponseElements) {
- return { isPartial: true };
- }
-
- return { isCompleted: orderStatus === 'COMPLETED' };
+ return await hasAllAnalysisResponseElements({ analysisResponseId, order })
+ ? { isCompleted: orderStatus === 'COMPLETED' }
+ : { isPartial: true };
}
export async function readPrivateMessageResponse({
@@ -297,6 +329,9 @@ export async function readPrivateMessageResponse({
analysisOrder = await getAnalysisOrder({ analysisOrderId })
medusaOrderId = analysisOrder.medusa_order_id;
} catch (e) {
+ if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) {
+ await deletePrivateMessage(privateMessageId);
+ }
throw new Error(`No analysis order found for Medipost message ValisTellimuseId=${medipostExternalOrderId}`);
}
@@ -305,17 +340,7 @@ export async function readPrivateMessageResponse({
throw new Error(`Order person personal code does not match Medipost message Patsient.Isikukood=${patientPersonalCode}, orderPerson.personal_code=${orderPerson.personal_code}`);
}
- let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
- try {
- order = await getAnalysisOrder({ medusaOrderId });
- } catch (e) {
- if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) {
- await deletePrivateMessage(privateMessageId);
- }
- throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`);
- }
-
- const status = await syncPrivateMessage({ messageResponse, order });
+ const status = await syncPrivateMessage({ messageResponse, order: analysisOrder });
await createMedipostActionLog({
action: 'sync_analysis_results_from_medipost',
@@ -394,7 +419,7 @@ export async function getPrivateMessage(messageId: string) {
await validateMedipostResponse(data, { canHaveEmptyCode: true });
return {
- message: parseXML(data) as MedipostOrderResponse,
+ message: (await parseXML(data)) as MedipostOrderResponse,
xml: data as string,
};
}
diff --git a/lib/services/medipost/medipostPublicMessage.service.ts b/lib/services/medipost/medipostPublicMessage.service.ts
index 21b21e4..c6c823e 100644
--- a/lib/services/medipost/medipostPublicMessage.service.ts
+++ b/lib/services/medipost/medipostPublicMessage.service.ts
@@ -29,5 +29,5 @@ export async function getLatestPublicMessageListItem() {
throw new Error('Failed to get public message list');
}
- return getLatestMessage({ messages: data?.messages });
+ return await getLatestMessage({ messages: data?.messages });
}
diff --git a/lib/services/medipost/medipostValidate.service.ts b/lib/services/medipost/medipostValidate.service.ts
index 655673b..aa85192 100644
--- a/lib/services/medipost/medipostValidate.service.ts
+++ b/lib/services/medipost/medipostValidate.service.ts
@@ -8,7 +8,7 @@ import { MedipostValidationError } from './MedipostValidationError';
import { parseXML } from '../util/xml.service';
export async function validateMedipostResponse(response: string, { canHaveEmptyCode = false }: { canHaveEmptyCode?: boolean } = {}) {
- const parsed: IMedipostResponseXMLBase = parseXML(response);
+ const parsed: IMedipostResponseXMLBase = await parseXML(response);
const code = parsed.ANSWER?.CODE;
if (canHaveEmptyCode) {
if (code && code !== 0) {
diff --git a/lib/services/util/xml.service.ts b/lib/services/util/xml.service.ts
index c3eb02c..a9d156e 100644
--- a/lib/services/util/xml.service.ts
+++ b/lib/services/util/xml.service.ts
@@ -2,7 +2,7 @@
import { XMLParser } from 'fast-xml-parser';
-export function parseXML(xml: string) {
+export async function parseXML(xml: string) {
const parser = new XMLParser({ ignoreAttributes: false });
return parser.parse(xml);
}
diff --git a/packages/features/accounts/src/types/analysis-results.ts b/packages/features/accounts/src/types/analysis-results.ts
index 64ddb83..e2b5e66 100644
--- a/packages/features/accounts/src/types/analysis-results.ts
+++ b/packages/features/accounts/src/types/analysis-results.ts
@@ -78,6 +78,7 @@ export type AnalysisResultDetailsElementResults = {
responseTime: string | null;
responseValue: number | null;
responseValueIsNegative: boolean | null;
+ responseValueIsWithinNorm: boolean | null;
normLowerIncluded: boolean;
normUpperIncluded: boolean;
status: string;
diff --git a/packages/features/user-analyses/src/server/api.ts b/packages/features/user-analyses/src/server/api.ts
index b01047c..6b74201 100644
--- a/packages/features/user-analyses/src/server/api.ts
+++ b/packages/features/user-analyses/src/server/api.ts
@@ -80,7 +80,7 @@ class UserAnalysesApi {
.from('analysis_responses')
.select(
`*,
- elements:analysis_response_elements(analysis_name,norm_status,response_value,unit,norm_lower_included,norm_upper_included,norm_lower,norm_upper,response_time,status,analysis_element_original_id,original_response_element,response_value_is_negative),
+ elements:analysis_response_elements(analysis_name,norm_status,response_value,unit,norm_lower_included,norm_upper_included,norm_lower,norm_upper,response_time,status,analysis_element_original_id,original_response_element,response_value_is_negative,response_value_is_within_norm),
summary:analysis_order_id(doctor_analysis_feedback(*))`,
)
.eq('user_id', user.id)
@@ -192,6 +192,10 @@ class UserAnalysesApi {
}
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 {
status: element.UuringOlek,
unit: element.Mootyhik,
@@ -199,8 +203,9 @@ class UserAnalysesApi {
normUpper: elementVastus?.NormYlem?.['#text'],
normStatus: elementVastus?.NormiStaatus,
responseTime: elementVastus?.VastuseAeg,
- responseValue: elementVastus?.VastuseVaartus,
- responseValueIsNegative: elementVastus?.VastuseVaartus === 'Negatiivne',
+ response_value: responseValueIsNegative || !responseValueIsNumeric ? null : (responseValue ?? null),
+ response_value_is_negative: responseValueIsNumeric ? null : responseValueIsNegative,
+ response_value_is_within_norm: responseValueIsNumeric ? null : responseValueIsWithinNorm,
normLowerIncluded: elementVastus?.NormAlum?.['@_kaasaarvatud'] === 'JAH',
normUpperIncluded: elementVastus?.NormYlem?.['@_kaasaarvatud'] === 'JAH',
analysisElementOriginalId: element.UuringId,
@@ -216,6 +221,7 @@ class UserAnalysesApi {
responseTime: elementResponse.response_time,
responseValue: elementResponse.response_value,
responseValueIsNegative: elementResponse.response_value_is_negative === true,
+ responseValueIsWithinNorm: elementResponse.response_value_is_within_norm === true,
normLowerIncluded: elementResponse.norm_lower_included,
normUpperIncluded: elementResponse.norm_upper_included,
status: elementResponse.status,
diff --git a/packages/features/user-analyses/src/types/analysis-results.ts b/packages/features/user-analyses/src/types/analysis-results.ts
index 64ddb83..3176499 100644
--- a/packages/features/user-analyses/src/types/analysis-results.ts
+++ b/packages/features/user-analyses/src/types/analysis-results.ts
@@ -19,6 +19,7 @@ const ElementSchema = z.object({
response_time: z.string(),
response_value: z.number(),
response_value_is_negative: z.boolean(),
+ response_value_is_within_norm: z.boolean(),
norm_lower_included: z.boolean(),
norm_upper_included: z.boolean(),
status: z.string(),
@@ -78,6 +79,7 @@ export type AnalysisResultDetailsElementResults = {
responseTime: string | null;
responseValue: number | null;
responseValueIsNegative: boolean | null;
+ responseValueIsWithinNorm: boolean | null;
normLowerIncluded: boolean;
normUpperIncluded: boolean;
status: string;
diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts
index e3f2af6..6907fac 100644
--- a/packages/supabase/src/database.types.ts
+++ b/packages/supabase/src/database.types.ts
@@ -688,6 +688,7 @@ export type Database = {
response_time: string
response_value: number | null
response_value_is_negative?: boolean | null
+ response_value_is_within_norm?: boolean | null
status: string
unit: string | null
updated_at: string | null
@@ -708,6 +709,7 @@ export type Database = {
response_time: string
response_value: number | null
response_value_is_negative?: boolean | null
+ response_value_is_within_norm?: boolean | null
status: string
unit?: string | null
updated_at?: string | null
@@ -728,6 +730,7 @@ export type Database = {
response_time?: string
response_value?: number | null
response_value_is_negative?: boolean | null
+ response_value_is_within_norm?: boolean | null
status: string
unit?: string | null
updated_at?: string | null
diff --git a/public/locales/en/analysis-results.json b/public/locales/en/analysis-results.json
index 571b1fb..02a2195 100644
--- a/public/locales/en/analysis-results.json
+++ b/public/locales/en/analysis-results.json
@@ -7,9 +7,16 @@
"noAnalysisElements": "No analysis orders found",
"noAnalysisOrders": "No analysis orders found",
"analysisDate": "Analysis result date",
+ "cancelled": "Cancelled",
"results": {
"range": {
"normal": "Normal range"
+ },
+ "value": {
+ "negative": "Negative",
+ "positive": "Positive",
+ "isWithinNorm": "Within norm",
+ "isNotWithinNorm": "Not within norm"
}
},
"orderTitle": "Order number {{orderNumber}}",
diff --git a/public/locales/et/analysis-results.json b/public/locales/et/analysis-results.json
index 9efe9bd..1fabbf8 100644
--- a/public/locales/et/analysis-results.json
+++ b/public/locales/et/analysis-results.json
@@ -7,9 +7,16 @@
"noAnalysisElements": "Veel ei ole tellitud analüüse",
"noAnalysisOrders": "Veel ei ole analüüside tellimusi",
"analysisDate": "Analüüsi vastuse kuupäev",
+ "cancelled": "Tühistatud",
"results": {
"range": {
"normal": "Normaalne vahemik"
+ },
+ "value": {
+ "negative": "Negatiivne",
+ "positive": "Positiivne",
+ "isWithinNorm": "Normi piires",
+ "isNotWithinNorm": "Normi piirest väljas"
}
},
"orderTitle": "Tellimus {{orderNumber}}",
diff --git a/public/locales/ru/analysis-results.json b/public/locales/ru/analysis-results.json
index 8a032b3..c79497e 100644
--- a/public/locales/ru/analysis-results.json
+++ b/public/locales/ru/analysis-results.json
@@ -7,9 +7,16 @@
"noAnalysisElements": "Анализы еще не заказаны",
"noAnalysisOrders": "Пока нет заказов на анализы",
"analysisDate": "Дата результата анализа",
+ "cancelled": "Отменен",
"results": {
"range": {
"normal": "Нормальный диапазон"
+ },
+ "value": {
+ "negative": "Отрицательный",
+ "positive": "Положительный",
+ "isWithinNorm": "В норме",
+ "isNotWithinNorm": "Не в норме"
}
},
"orderTitle": "Заказ {{orderNumber}}"
diff --git a/supabase/migrations/20250917073453_analysis_response_element_within_range.sql b/supabase/migrations/20250917073453_analysis_response_element_within_range.sql
new file mode 100644
index 0000000..657e79b
--- /dev/null
+++ b/supabase/migrations/20250917073453_analysis_response_element_within_range.sql
@@ -0,0 +1,2 @@
+ALTER TABLE medreport.analysis_response_elements
+ADD COLUMN response_value_is_within_norm BOOLEAN;