Merge branch 'develop' into MED-157

This commit is contained in:
Danel Kungla
2025-09-17 11:52:04 +03:00
70 changed files with 4513 additions and 1353 deletions

View File

@@ -165,7 +165,7 @@ async function createProducts({
medusa.admin.product.list({
category_id: allCategories.map(({ id }) => id),
}),
getAnalysisElements({}),
getAnalysisElements({ getAll: true }),
getAnalysisPackagesType(),
getProductDefaultFields({ medusa }),
])

View File

@@ -2,12 +2,12 @@ import axios from 'axios';
import { XMLParser } from 'fast-xml-parser';
import fs from 'fs';
import { createAnalysisGroup, getAnalysisGroups } from '~/lib/services/analysis-group.service';
import { IMedipostPublicMessageDataParsed } from '~/lib/services/medipost.types';
import { createAnalysis, createNoDataReceivedEntry, createNoNewDataReceivedEntry, createSyncFailEntry, createSyncSuccessEntry } from '~/lib/services/analyses.service';
import { IMedipostPublicMessageDataParsed } from '~/lib/services/medipost/medipost.types';
import { createAnalysis, createNoDataReceivedEntry, createNoNewDataReceivedEntry, createSyncFailEntry, createSyncSuccessEntry, getAnalyses } from '~/lib/services/analyses.service';
import { getLastCheckedDate } from '~/lib/services/sync-entries.service';
import { createAnalysisElement } from '~/lib/services/analysis-element.service';
import { AnalysisElement, createAnalysisElement, getAnalysisElements } from '~/lib/services/analysis-element.service';
import { createCodes } from '~/lib/services/codes.service';
import { getLatestPublicMessageListItem } from '~/lib/services/medipost.service';
import { getLatestPublicMessageListItem } from '~/lib/services/medipost/medipostPublicMessage.service';
import type { ICode } from '~/lib/types/code';
function toArray<T>(input?: T | T[] | null): T[] {
@@ -82,42 +82,60 @@ export default async function syncAnalysisGroups() {
const codes: ICode[] = [];
for (const analysisGroup of analysisGroups) {
const existingAnalysisGroup = existingAnalysisGroups?.find(({ original_id }) => original_id === analysisGroup.UuringuGruppId);
let groupExistingAnalysisElements: AnalysisElement[] = [];
let analysisGroupId: number;
if (existingAnalysisGroup) {
console.info(`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists`);
continue;
console.info(`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists, only creating new analysis elements`);
groupExistingAnalysisElements = await getAnalysisElements({ analysisGroupId: existingAnalysisGroup.id });
analysisGroupId = existingAnalysisGroup.id;
} else {
analysisGroupId = await createAnalysisGroup({
id: analysisGroup.UuringuGruppId,
name: analysisGroup.UuringuGruppNimi,
order: analysisGroup.UuringuGruppJarjekord,
});
const analysisGroupCodes = toArray(analysisGroup.Kood);
codes.push(
...analysisGroupCodes.map((kood) => ({
hk_code: kood.HkKood,
hk_code_multiplier: kood.HkKoodiKordaja,
coefficient: kood.Koefitsient,
price: kood.Hind,
analysis_group_id: analysisGroupId,
analysis_element_id: null,
analysis_id: null,
})),
);
}
// SAVE ANALYSIS GROUP
const analysisGroupId = await createAnalysisGroup({
id: analysisGroup.UuringuGruppId,
name: analysisGroup.UuringuGruppNimi,
order: analysisGroup.UuringuGruppJarjekord,
});
const analysisGroupCodes = toArray(analysisGroup.Kood);
codes.push(
...analysisGroupCodes.map((kood) => ({
hk_code: kood.HkKood,
hk_code_multiplier: kood.HkKoodiKordaja,
coefficient: kood.Koefitsient,
price: kood.Hind,
analysis_group_id: analysisGroupId,
analysis_element_id: null,
analysis_id: null,
})),
);
const analysisGroupItems = toArray(analysisGroup.Uuring);
for (const item of analysisGroupItems) {
const analysisElement = item.UuringuElement;
const analysisElement = item.UuringuElement!;
const isExistingAnalysisElement = groupExistingAnalysisElements
.find(({ analysis_id_original }) => analysis_id_original === analysisElement.UuringId);
if (isExistingAnalysisElement) {
console.info(`Analysis element '${analysisElement.UuringNimi}' already exists`);
continue;
}
const insertedAnalysisElementId = await createAnalysisElement({
analysisElement,
analysisElement: analysisElement!,
analysisGroupId,
materialGroups: toArray(item.MaterjalideGrupp),
});
if (Array.isArray(analysisElement.UuringuElement)) {
for (const nestedAnalysisElement of analysisElement.UuringuElement) {
await createAnalysisElement({
analysisElement: nestedAnalysisElement,
analysisGroupId,
materialGroups: toArray(item.MaterjalideGrupp),
});
}
}
if (analysisElement.Kood) {
const analysisElementCodes = toArray(analysisElement.Kood);
codes.push(
@@ -135,7 +153,15 @@ export default async function syncAnalysisGroups() {
const analyses = analysisElement.UuringuElement;
if (analyses?.length) {
const existingAnalyses = await getAnalyses({ originalIds: analyses.map(({ UuringId }) => UuringId) });
for (const analysis of analyses) {
const isExistingAnalysis = existingAnalyses.find(({ analysis_id_original }) => analysis_id_original === analysis.UuringId);
if (isExistingAnalysis) {
console.info(`Analysis '${analysis.UuringNimi}' already exists`);
continue;
}
const insertedAnalysisId = await createAnalysis(analysis, analysisGroupId);
if (analysis.Kood) {

View File

@@ -1,4 +1,4 @@
import { readPrivateMessageResponse } from "~/lib/services/medipost.service";
import { readPrivateMessageResponse } from "~/lib/services/medipost/medipostPrivateMessage.service";
type ProcessedMessage = {
messageId: string;

View File

@@ -1,7 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key";
import { getOrderedAnalysisIds, sendOrderToMedipost } from "~/lib/services/medipost.service";
import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service";
import { sendOrderToMedipost } from "~/lib/services/medipost/medipostPrivateMessage.service";
import { retrieveOrder } from "@lib/data/orders";
import { getMedipostDispatchTries } from "~/lib/services/audit.service";

View File

@@ -1,9 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import { getAnalysisOrdersAdmin } from "~/lib/services/order.service";
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipost/medipostTest.service";
import { retrieveOrder } from "@lib/data";
import { getAccountAdmin } from "~/lib/services/account.service";
import { getOrderedAnalysisIds } from "~/lib/services/medipost.service";
import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service";
import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key";

View File

@@ -1,9 +1,10 @@
import { NextResponse } from "next/server";
import { getAnalysisOrder } from "~/lib/services/order.service";
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipost/medipostTest.service";
import { retrieveOrder } from "@lib/data";
import { getAccountAdmin } from "~/lib/services/account.service";
import { createMedipostActionLog, getOrderedAnalysisIds } from "~/lib/services/medipost.service";
import { createMedipostActionLog } from "~/lib/services/medipost/medipostMessageBase.service";
import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service";
export async function POST(request: Request) {
// const isDev = process.env.NODE_ENV === 'development';

View File

@@ -0,0 +1,149 @@
'use client';
import React, { ReactElement, ReactNode, useMemo, useState } from 'react';
import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts';
import { format } from 'date-fns';
import { Info } from 'lucide-react';
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,
HIGH = 2,
}
const AnalysisDoctor = ({
analysisElement,
results,
startIcon,
endIcon,
isCancelled,
}: {
analysisElement: Pick<AnalysisElement, 'analysis_name_lab'>;
results?: AnalysisResultForDisplay;
isCancelled?: boolean;
startIcon?: ReactElement | null;
endIcon?: ReactNode | null;
}) => {
const name = analysisElement.analysis_name_lab || '';
const status = results?.norm_status || AnalysisStatus.NORMAL;
const value = results?.response_value || 0;
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 [showTooltip, setShowTooltip] = useState(false);
const analysisResultLevel = useMemo(() => {
if (!results) {
return null;
}
const isUnderNorm = value < normLower;
if (isUnderNorm) {
switch (status) {
case AnalysisStatus.MEDIUM:
return AnalysisResultLevel.LOW;
default:
return AnalysisResultLevel.VERY_LOW;
}
}
switch (status) {
case AnalysisStatus.MEDIUM:
return AnalysisResultLevel.HIGH;
case AnalysisStatus.HIGH:
return AnalysisResultLevel.VERY_HIGH;
default:
return AnalysisResultLevel.NORMAL;
}
}, [results, value, normLower]);
return (
<div className="border-border rounded-lg border px-5">
<div className="flex flex-col items-center justify-between gap-2 py-3 sm:h-[65px] sm:flex-row sm:gap-0">
<div className="flex items-center gap-2 font-semibold">
{startIcon || <div className="w-4" />}
{name}
{results?.response_time && (
<div
className="group/tooltip relative"
onClick={(e) => {
e.stopPropagation();
setShowTooltip(!showTooltip);
}}
onMouseLeave={() => setShowTooltip(false)}
>
<Info className="hover" />{' '}
<div
className={cn(
'absolute bottom-full left-1/2 z-10 mb-2 hidden -translate-x-1/2 rounded border bg-white p-4 text-sm whitespace-nowrap group-hover/tooltip:block',
{ block: showTooltip },
)}
>
<Trans i18nKey="analysis-results:analysisDate" />
{': '}
{format(new Date(results.response_time), 'dd.MM.yyyy HH:mm')}
</div>
</div>
)}
</div>
{results ? (
<>
<div className="flex items-center gap-3 sm:ml-auto">
<div className="font-semibold">{value}</div>
<div className="text-muted-foreground text-sm">{unit}</div>
</div>
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
{normLower} - {normUpper}
<div>
<Trans i18nKey="analysis-results:results.range.normal" />
</div>
</div>
<AnalysisLevelBar
results={results}
normLowerIncluded={normLowerIncluded}
normUpperIncluded={normUpperIncluded}
level={analysisResultLevel!}
/>
{endIcon || <div className="mx-2 w-4" />}
</>
) : (isCancelled ? null : (
<>
<div className="flex items-center gap-3 sm:ml-auto">
<div className="font-semibold">
<Trans i18nKey="analysis-results:waitingForResults" />
</div>
</div>
<div className="mx-8 w-[60px]"></div>
<AnalysisLevelBarSkeleton />
</>
))}
</div>
</div>
);
};
export default AnalysisDoctor;

View File

@@ -0,0 +1,134 @@
import { useMemo } from 'react';
import { ArrowDown } from 'lucide-react';
import { cn } from '@kit/ui/utils';
import { AnalysisResultForDisplay } from './analysis-doctor';
export enum AnalysisResultLevel {
VERY_LOW = 0,
LOW = 1,
NORMAL = 2,
HIGH = 3,
VERY_HIGH = 4,
}
const Level = ({
isActive = false,
color,
isFirst = false,
isLast = false,
arrowLocation,
}: {
isActive?: boolean;
color: 'destructive' | 'success' | 'warning' | 'gray-200';
isFirst?: boolean;
isLast?: boolean;
arrowLocation?: number;
}) => {
return (
<div
className={cn(`bg-${color} relative h-3 flex-1`, {
'opacity-20': !isActive,
'rounded-l-lg': isFirst,
'rounded-r-lg': isLast,
})}
>
{isActive && (
<div
className="absolute top-[-14px] left-1/2 -translate-x-1/2 rounded-[10px] bg-white p-[2px]"
style={{ left: `${arrowLocation}%` }}
>
<ArrowDown strokeWidth={2} />
</div>
)}
</div>
);
};
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 = ({
normLowerIncluded = true,
normUpperIncluded = true,
level,
results,
}: {
normLowerIncluded?: boolean;
normUpperIncluded?: boolean;
level: AnalysisResultLevel;
results: AnalysisResultForDisplay;
}) => {
const { norm_lower: lower, norm_upper: upper, response_value: value } = results;
const arrowLocation = useMemo(() => {
if (value < lower!) {
return 0;
}
if (normLowerIncluded || normUpperIncluded) {
return 50;
}
const calculated = ((value - lower!) / (upper! - lower!)) * 100;
if (calculated > 100) {
return 100;
}
return calculated;
}, [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]);
const hasAbnormalLevel = isVeryLow || isLow || isHigh || isVeryHigh;
return (
<div className="mt-4 flex h-3 w-[60%] sm:w-[35%] max-w-[360px] gap-1 sm:mt-0">
{normLowerIncluded && (
<>
<Level
isActive={isVeryLow}
color="destructive"
isFirst
/>
<Level isActive={isLow} color="warning" />
</>
)}
<Level
isFirst={!normLowerIncluded}
isLast={!normUpperIncluded}
{...(hasAbnormalLevel ? { color: "warning", isActive: false } : { color: "success", isActive: true })}
arrowLocation={arrowLocation}
/>
{normUpperIncluded && (
<>
<Level
isActive={isHigh}
color="warning"
/>
<Level
isActive={isVeryHigh}
color="destructive"
isLast
/>
</>
)}
</div>
);
};
export default AnalysisLevelBar;

View File

@@ -13,7 +13,7 @@ import {
} from '@kit/ui/collapsible';
import { Trans } from '@kit/ui/trans';
import Analysis from '~/home/(user)/(dashboard)/analysis-results/_components/analysis';
import AnalysisDoctor from './analysis-doctor';
export default function DoctorAnalysisWrapper({
analysisData,
@@ -29,7 +29,7 @@ export default function DoctorAnalysisWrapper({
asChild
>
<div className="[&[data-state=open]_.caret-icon]:rotate-180">
<Analysis
<AnalysisDoctor
startIcon={
analysisData.latestPreviousAnalysis && (
<CaretDownIcon className="caret-icon transition-transform duration-200" />
@@ -65,7 +65,7 @@ export default function DoctorAnalysisWrapper({
{analysisData.latestPreviousAnalysis && (
<CollapsibleContent>
<div className="my-1 flex flex-col">
<Analysis
<AnalysisDoctor
endIcon={
analysisData.latestPreviousAnalysis.comment && (
<>

View File

@@ -52,6 +52,8 @@ export default async function AnalysisResultsPage({
);
}
const orderedAnalysisElements = analysisResponse.orderedAnalysisElements;
return (
<>
<PageHeader />
@@ -80,7 +82,7 @@ export default async function AnalysisResultsPage({
<h4>
<Trans
i18nKey="analysis-results:orderTitle"
values={{ orderNumber: analysisResponse.order.medusa_order_id }}
values={{ orderNumber: analysisResponse.order.medusaOrderId }}
/>
</h4>
<h5>
@@ -88,7 +90,7 @@ export default async function AnalysisResultsPage({
i18nKey={`orders:status.${analysisResponse.order.status}`}
/>
<ButtonTooltip
content={`${analysisResponse.order.created_at ? new Date(analysisResponse?.order?.created_at).toLocaleString() : ''}`}
content={`${analysisResponse.order.createdAt ? new Date(analysisResponse?.order?.createdAt).toLocaleString() : ''}`}
className="ml-6"
/>
</h5>
@@ -102,13 +104,9 @@ export default async function AnalysisResultsPage({
</div>
)}
<div className="flex flex-col gap-2">
{analysisResponse.elements ? (
analysisResponse.elements.map((element, index) => (
<Analysis
key={index}
analysisElement={{ analysis_name_lab: element.analysis_name }}
results={element}
/>
{orderedAnalysisElements ? (
orderedAnalysisElements.map((element, index) => (
<Analysis key={index} element={element} />
))
) : (
<div className="text-muted-foreground text-sm">

View File

@@ -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 (
<div
className={cn(`bg-${color} relative h-3 flex-1`, {
'opacity-20': !isActive,
'opacity-60': !isActive,
'rounded-l-lg': isFirst,
'rounded-r-lg': isLast,
})}
@@ -37,96 +37,176 @@ const Level = ({
{isActive && (
<div
className="absolute top-[-14px] left-1/2 -translate-x-1/2 rounded-[10px] bg-white p-[2px]"
style={{ left: `${arrowLocation}%` }}
{...(arrowLocation ? {
style: {
left: `${arrowLocation}%`,
...(arrowLocation > 92.5 && { left: '92.5%' }),
...(arrowLocation < 7.5 && { left: '7.5%' }),
}
} : {})}
>
<ArrowDown strokeWidth={2} />
</div>
)}
{color === 'success' && typeof normRangeText === 'string' && (
<p className={cn("absolute bottom-[-18px] left-3/8 text-xs text-muted-foreground font-bold", {
'opacity-60': isActive,
})}>
{normRangeText}
</p>
)}
</div>
);
};
export const AnalysisLevelBarSkeleton = () => {
return (
<div className="mt-4 flex h-3 w-[35%] max-w-[360px] gap-1 sm:mt-0">
<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 = ({
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 (
<div className="mt-4 flex h-3 w-60% sm:w-[35%] max-w-[360px] gap-1 sm:mt-0">
<Level color="gray-200" isFirst isLast />
</div>
);
}
// 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 (
<div className="mt-4 flex h-3 w-[35%] max-w-[360px] gap-1 sm:mt-0">
{normLowerIncluded && (
<>
<Level
isActive={isVeryLow}
color="destructive"
isFirst
/>
<Level isActive={isLow} color="warning" />
</>
)}
<Level
isFirst={!normLowerIncluded}
isLast={!normUpperIncluded}
{...(hasAbnormalLevel ? { color: "warning", isActive: false } : { color: "success", isActive: true })}
arrowLocation={arrowLocation}
/>
{normUpperIncluded && (
<>
<Level
isActive={isHigh}
color="warning"
/>
<Level
isActive={isVeryHigh}
color="destructive"
isLast
/>
</>
)}
<div className="mt-4 flex h-3 w-[60%] sm:w-[35%] max-w-[360px] gap-1 sm:mt-0">
<Level {...first} />
<Level {...second} />
<Level {...third} />
</div>
);
};

View File

@@ -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<AnalysisElement, 'analysis_name_lab'>;
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 (
<div className="border-border rounded-lg border px-5">
<div className="flex flex-col items-center justify-between gap-2 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">
{startIcon || <div className="w-4" />}
{name}
{results?.response_time && (
{results?.responseTime && (
<div
className="group/tooltip relative"
onClick={(e) => {
@@ -105,42 +112,41 @@ const Analysis = ({
>
<Trans i18nKey="analysis-results:analysisDate" />
{': '}
{format(new Date(results.response_time), 'dd.MM.yyyy HH:mm')}
{format(new Date(results.responseTime), 'dd.MM.yyyy HH:mm')}
</div>
</div>
)}
</div>
{results ? (
{isCancelled && (
<div className="text-red-600 font-semibold text-sm">
<Trans i18nKey="analysis-results:cancelled" />
</div>
)}
{isCancelled || !results || hasNestedElements ? null : (
<>
<div className="flex items-center gap-3 sm:ml-auto">
<div className="font-semibold">{value}</div>
<div className="text-muted-foreground text-sm">{unit}</div>
</div>
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
{normLower} - {normUpper}
<div>
<Trans i18nKey="analysis-results:results.range.normal" />
</div>
</div>
<AnalysisLevelBar
results={results}
normLowerIncluded={normLowerIncluded}
normUpperIncluded={normUpperIncluded}
level={analysisResultLevel!}
/>
{endIcon || <div className="mx-2 w-4" />}
{!(hasIsNegative || hasIsWithinNorm) && (
<>
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
{normRangeText}
<div>
<Trans i18nKey="analysis-results:results.range.normal" />
</div>
</div>
<AnalysisLevelBar
results={results}
level={analysisResultLevel!}
normRangeText={normRangeText}
/>
</>
)}
</>
) : (isCancelled ? null : (
<>
<div className="flex items-center gap-3 sm:ml-auto">
<div className="font-semibold">
<Trans i18nKey="analysis-results:waitingForResults" />
</div>
</div>
<div className="mx-8 w-[60px]"></div>
<AnalysisLevelBarSkeleton />
</>
))}
)}
</div>
</div>
);

View File

@@ -0,0 +1,99 @@
'use client';
import React, { useState } from 'react';
import { PageBody, PageHeader } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { Button } from '@kit/ui/shadcn/button';
import Modal from "@modules/common/components/modal"
import Analysis from '../_components/analysis';
import { analysisResponses } from './test-responses';
export default function AnalysisResultsPage() {
const [openBlocks, setOpenBlocks] = useState<number[]>([]);
return (
<>
<PageHeader />
<PageBody className="gap-4">
<div className="mt-8 flex flex-col justify-between gap-4 sm:flex-row sm:items-center sm:gap-0">
<div>
<h2>
Analüüsi tulemused demo
</h2>
</div>
</div>
<div className="flex flex-col gap-2">
{analysisResponses.map(({ id, orderedAnalysisElements }, index) => {
const isOpen = openBlocks.includes(id);
const closeModal = () => setOpenBlocks(openBlocks.filter((block) => block !== id));
return (
<div key={index} className="flex flex-col gap-2 py-4">
<div className="flex flex-col gap-2 pb-4">
<h3>AnalysisOrderId: {id}</h3>
<div className="flex flex-col gap-2">
<h5>OrderedAnalysisElements</h5>
<Button
onClick={() => {
if (isOpen) {
closeModal();
} else {
setOpenBlocks([...openBlocks, id]);
}
}}
className="w-fit"
color={isOpen ? 'orange' : 'grey'}
>
{isOpen ? 'Close' : 'Open'}
</Button>
{isOpen && (
<Modal isOpen={isOpen} close={closeModal} size="large">
<div className="overflow-y-auto">
<p>NormiStaatus</p>
<ul>
<li>0 - testi väärtus jääb normaalväärtuste piirkonda või on määramata,</li>
<li>1 - testi väärtus jääb hoiatava (tähelepanu suunava) märkega piirkonda,</li>
<li>2 - testi väärtus on normaalväärtuste piirkonnast väljas või kõrgendatud tähelepanu nõudvas piirkonnas.</li>
</ul>
<p>UuringOlek</p>
<ul>
<li>1 - Järjekorras,</li>
<li>2 - Ootel,</li>
<li>3 - Töös,</li>
<li>4 - Lõpetatud,</li>
<li>5 - Tagasi lükatud,</li>
<li>6 - Tühistatud,</li>
</ul>
<pre className="text-sm bg-muted p-4 rounded-md">
{JSON.stringify(orderedAnalysisElements, null, 2)}
</pre>
</div>
</Modal>
)}
</div>
{orderedAnalysisElements ? (
orderedAnalysisElements.map((element, index) => (
<Analysis key={index} element={element} />
))
) : (
<div className="text-muted-foreground text-sm">
<Trans i18nKey="analysis-results:noAnalysisElements" />
</div>
)}
</div>
<hr />
</div>
)
})}
</div>
</PageBody>
</>
);
}

View File

@@ -0,0 +1,840 @@
import { AnalysisResultDetailsMapped } from "@/packages/features/accounts/src/types/analysis-results";
type AnalysisTestResponse = Omit<AnalysisResultDetailsMapped, 'order' | 'orderedAnalysisElementIds' | 'summary' | 'elements'>;
const empty1: AnalysisTestResponse = {
"id": 1,
"orderedAnalysisElements": [],
};
const big1: AnalysisTestResponse = {
"id": 2,
"orderedAnalysisElements": [
{
"analysisIdOriginal": "1744-2",
"isWaitingForResults": false,
"analysisName": "ALAT",
"results": {
"nestedElements": [],
"unit": "U/l",
"normLower": null,
"normUpper": 45,
"normStatus": 2,
"responseTime": "2024-02-29T10:42:25+00:00",
"responseValue": 84,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "1744-2"
}
},
{
"analysisIdOriginal": "1920-8",
"isWaitingForResults": false,
"analysisName": "ASAT",
"results": {
"nestedElements": [],
"unit": "U/l",
"normLower": 15,
"normUpper": 45,
"normStatus": 0,
"responseTime": "2024-02-29T10:20:55+00:00",
"responseValue": 45,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "1920-8"
}
},
{
"analysisIdOriginal": "1988-5",
"isWaitingForResults": false,
"analysisName": "CRP",
"results": {
"nestedElements": [],
"unit": "mg/l",
"normLower": null,
"normUpper": 5,
"normStatus": 0,
"responseTime": "2024-02-29T10:18:49+00:00",
"responseValue": 0.79,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "1988-5"
}
},
{
"analysisIdOriginal": "57747-8",
"isWaitingForResults": false,
"analysisName": "Erütrotsüüdid",
"results": {
"nestedElements": [],
"unit": null,
"normLower": null,
"normUpper": 5,
"normStatus": 0,
"responseTime": "2024-02-29T10:13:01+00:00",
"responseValue": null,
"responseValueIsNegative": true,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "57747-8"
}
},
{
"analysisIdOriginal": "2276-4",
"isWaitingForResults": false,
"analysisName": "Ferritiin",
"results": {
"nestedElements": [],
"unit": "µg/l",
"normLower": 28,
"normUpper": 370,
"normStatus": 0,
"responseTime": "2024-02-29T10:46:54+00:00",
"responseValue": 204.1,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "2276-4"
}
},
{
"analysisIdOriginal": "14771-0",
"isWaitingForResults": false,
"analysisName": "Glükoos",
"results": {
"nestedElements": [],
"unit": "mmol/l",
"normLower": 4.1,
"normUpper": 6,
"normStatus": 0,
"responseTime": "2024-02-29T10:06:24+00:00",
"responseValue": 5.4,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "14771-0"
}
},
{
"analysisIdOriginal": "59156-0",
"isWaitingForResults": false,
"analysisName": "Glükoos",
"results": {
"nestedElements": [],
"unit": null,
"normLower": null,
"normUpper": 2,
"normStatus": 0,
"responseTime": "2024-02-29T10:13:01+00:00",
"responseValue": null,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": false,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "59156-0"
}
},
{
"analysisIdOriginal": "13955-0",
"isWaitingForResults": false,
"analysisName": "HCV Ab",
"results": {
"nestedElements": [],
"unit": null,
"normLower": null,
"normUpper": null,
"normStatus": 0,
"responseTime": "2024-02-29T13:44:48+00:00",
"responseValue": null,
"responseValueIsNegative": true,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "13955-0"
}
},
{
"analysisIdOriginal": "14646-4",
"isWaitingForResults": false,
"analysisName": "HDL kolesterool",
"results": {
"nestedElements": [],
"unit": "mmol/l",
"normLower": 1,
"normUpper": null,
"normStatus": 1,
"responseTime": "2024-02-29T10:20:55+00:00",
"responseValue": 0.8,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "14646-4"
}
},
{
"analysisIdOriginal": "2000-8",
"isWaitingForResults": false,
"analysisName": "Kaltsium",
"results": {
"nestedElements": [],
"unit": "mmol/l",
"normLower": 2.1,
"normUpper": 2.55,
"normStatus": 0,
"responseTime": "2024-02-29T10:12:10+00:00",
"responseValue": 2.49,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "2000-8"
}
},
{
"analysisIdOriginal": "59158-6",
"isWaitingForResults": false,
"analysisName": "Ketokehad",
"results": {
"nestedElements": [],
"unit": null,
"normLower": null,
"normUpper": 0.5,
"normStatus": 0,
"responseTime": "2024-02-29T10:13:01+00:00",
"responseValue": null,
"responseValueIsNegative": true,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "59158-6"
}
},
{
"analysisIdOriginal": "14647-2",
"isWaitingForResults": false,
"analysisName": "Kolesterool",
"results": {
"nestedElements": [],
"unit": "mmol/l",
"normLower": null,
"normUpper": 5,
"normStatus": 1,
"responseTime": "2024-02-29T10:20:34+00:00",
"responseValue": 5.7,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "14647-2"
}
},
{
"analysisIdOriginal": "14682-9",
"isWaitingForResults": false,
"analysisName": "Kreatiniin",
"results": {
"nestedElements": [],
"unit": "µmol/l",
"normLower": 64,
"normUpper": 111,
"normStatus": 0,
"responseTime": "2024-02-29T10:19:00+00:00",
"responseValue": 89,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "14682-9"
}
},
{
"analysisIdOriginal": "22748-8",
"isWaitingForResults": false,
"analysisName": "LDL kolesterool",
"results": {
"nestedElements": [],
"unit": "mmol/l",
"normLower": null,
"normUpper": 3,
"normStatus": 1,
"responseTime": "2024-02-29T10:21:15+00:00",
"responseValue": 4.3,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "22748-8"
}
},
{
"analysisIdOriginal": "58805-3",
"isWaitingForResults": false,
"analysisName": "Leukotsüüdid",
"results": {
"nestedElements": [],
"unit": null,
"normLower": null,
"normUpper": 10,
"normStatus": 0,
"responseTime": "2024-02-29T10:13:01+00:00",
"responseValue": null,
"responseValueIsNegative": true,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "58805-3"
}
},
{
"analysisIdOriginal": "2601-3",
"isWaitingForResults": false,
"analysisName": "Magneesium",
"results": {
"nestedElements": [],
"unit": "mmol/l",
"normLower": 0.66,
"normUpper": 1.07,
"normStatus": 0,
"responseTime": "2024-02-29T10:17:26+00:00",
"responseValue": 0.82,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "2601-3"
}
},
{
"analysisIdOriginal": "70204-3",
"isWaitingForResults": false,
"analysisName": "Mitte-HDL kolesterool",
"results": {
"nestedElements": [],
"labComment": "Mitte-paastu veri <3,9 mmol/L",
"unit": "mmol/l",
"normLower": null,
"normUpper": 3.8,
"normStatus": 1,
"responseTime": "2024-02-29T10:20:55+00:00",
"responseValue": 4.9,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "70204-3"
}
},
{
"analysisIdOriginal": "14798-3",
"isWaitingForResults": false,
"analysisName": "Raud",
"results": {
"nestedElements": [],
"unit": "µmol/l",
"normLower": 11.6,
"normUpper": 31.3,
"normStatus": 0,
"responseTime": "2024-02-29T10:21:16+00:00",
"responseValue": 16.5,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "14798-3"
}
},
{
"analysisIdOriginal": "14927-8",
"isWaitingForResults": false,
"analysisName": "Triglütseriidid",
"results": {
"nestedElements": [],
"labComment": "Mitte-paastu veri <2,0 mmol/L",
"unit": "mmol/l",
"normLower": null,
"normUpper": 1.7,
"normStatus": 1,
"responseTime": "2024-02-29T10:21:16+00:00",
"responseValue": 1.89,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "14927-8"
}
},
{
"analysisIdOriginal": "3016-3",
"isWaitingForResults": false,
"analysisName": "TSH",
"results": {
"nestedElements": [],
"unit": "mIU/l",
"normLower": 0.4,
"normUpper": 4,
"normStatus": 0,
"responseTime": "2024-02-29T10:49:02+00:00",
"responseValue": 1.27,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "3016-3"
}
},
{
"analysisIdOriginal": "22664-7",
"isWaitingForResults": false,
"analysisName": "Uurea",
"results": {
"nestedElements": [],
"unit": "mmol/l",
"normLower": 3.2,
"normUpper": 7.4,
"normStatus": 0,
"responseTime": "2024-02-29T10:19:11+00:00",
"responseValue": 6.4,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "22664-7"
}
},
{
"analysisIdOriginal": "50561-0",
"isWaitingForResults": false,
"analysisName": "Valk",
"results": {
"nestedElements": [],
"unit": null,
"normLower": null,
"normUpper": 0.25,
"normStatus": 0,
"responseTime": "2024-02-29T10:13:01+00:00",
"responseValue": null,
"responseValueIsNegative": true,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "50561-0"
}
},
{
"analysisIdOriginal": "60493-4",
"isWaitingForResults": false,
"analysisName": "Vitamiin D (25-OH)",
"results": {
"nestedElements": [],
"labComment": "Väärtus >75 nmol/l on D-vitamiini tervislik tase",
"unit": "nmol/l",
"normLower": 75,
"normUpper": null,
"normStatus": 0,
"responseTime": "2024-02-29T10:49:22+00:00",
"responseValue": 105.5,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "60493-4"
}
},
{
"analysisIdOriginal": "60025-4",
"isWaitingForResults": false,
"analysisName": "Urobilinogeen",
"results": {
"nestedElements": [],
"unit": null,
"normLower": null,
"normUpper": 17,
"normStatus": 0,
"responseTime": "2024-02-29T10:13:01+00:00",
"responseValue": null,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": true,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "60025-4"
},
}
],
};
const big2: AnalysisTestResponse = {
"id": 3,
"orderedAnalysisElements": [
{
"analysisIdOriginal": "1988-5",
"isWaitingForResults": false,
"analysisName": "CRP",
"results": {
"nestedElements": [],
"unit": "mg/L",
"normLower": null,
"normUpper": 5,
"normStatus": 0,
"responseTime": "2025-09-12T14:02:04+00:00",
"responseValue": 1,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "1988-5"
}
},
{
"analysisIdOriginal": "57021-8",
"isWaitingForResults": false,
"analysisName": "Hemogramm",
"results": {
"nestedElements": [
{
"status": 4,
"unit": "g/L",
"normLower": 134,
"normUpper": 170,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:03",
"responseValue": 150,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "718-7"
},
{
"status": 4,
"unit": "%",
"normLower": 40,
"normUpper": 49,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:03",
"responseValue": 45,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "4544-3"
},
{
"status": 4,
"unit": "E9/L",
"normLower": 4.1,
"normUpper": 9.7,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:03",
"responseValue": 5,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "6690-2"
},
{
"status": 4,
"unit": "E12/L",
"normLower": 4.5,
"normUpper": 5.7,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:03",
"responseValue": 5,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "789-8"
},
{
"status": 4,
"unit": "fL",
"normLower": 82,
"normUpper": 95,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 85,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "787-2"
},
{
"status": 4,
"unit": "pg",
"normLower": 28,
"normUpper": 33,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 30,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "785-6"
},
{
"status": 4,
"unit": "g/L",
"normLower": 322,
"normUpper": 356,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 355,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "786-4"
},
{
"status": 4,
"unit": "%",
"normLower": 12,
"normUpper": 15,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 15,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "788-0"
},
{
"status": 4,
"unit": "E9/L",
"normLower": 157,
"normUpper": 372,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 255,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "777-3"
},
{
"status": 4,
"unit": "%",
"normLower": 0.18,
"normUpper": 0.38,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 0.2,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "51637-7"
},
{
"status": 4,
"unit": "fL",
"normLower": 9.2,
"normUpper": 12.3,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 10,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "32623-1"
},
{
"status": 4,
"unit": "fL",
"normLower": 10.1,
"normUpper": 16.2,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 15,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "32207-3"
},
{
"status": 4,
"unit": "E9/L",
"normLower": 0.01,
"normUpper": 0.08,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 0.05,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "704-7"
},
{
"status": 4,
"unit": "E9/L",
"normLower": 0.02,
"normUpper": 0.4,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 0.05,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "711-2"
},
{
"status": 4,
"unit": "E9/L",
"normLower": 1.9,
"normUpper": 6.7,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 5,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "751-8"
},
{
"status": 4,
"unit": "E9/L",
"normLower": 0.24,
"normUpper": 0.8,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 0.5,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "742-7"
},
{
"status": 4,
"unit": "E9/L",
"normLower": 1.3,
"normUpper": 3.1,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 1.5,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "731-0"
},
{
"status": 4,
"unit": "E9/L",
"normLower": 0,
"normUpper": 0.03,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:03",
"responseValue": 0,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "51584-1"
},
{
"status": 4,
"unit": "%",
"normLower": 0,
"normUpper": 0.5,
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 0,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "38518-7"
},
{
"status": 4,
"unit": "E9/L",
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 0,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "771-6"
},
{
"status": 4,
"unit": "/100WBC",
"normStatus": 0,
"responseTime": "2025-09-12 14:02:04",
"responseValue": 0,
"normLowerIncluded": false,
"normUpperIncluded": false,
"analysisElementOriginalId": "58413-6"
}
],
"unit": null,
"normLower": null,
"normUpper": null,
"normStatus": null,
"responseTime": null,
"responseValue": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"responseValueIsNegative": false,
"responseValueIsWithinNorm": false,
"status": "4",
"analysisElementOriginalId": "57021-8"
}
},
{
"analysisIdOriginal": "43583-4",
"isWaitingForResults": false,
"analysisName": "Lipoproteiin a",
"results": {
"nestedElements": [],
"labComment": "Kliendi soovil analüüs tühistatud.",
"unit": null,
"normLower": null,
"normUpper": null,
"normStatus": null,
"responseTime": null,
"responseValue": null,
"normLowerIncluded": false,
"normUpperIncluded": false,
"responseValueIsNegative": false,
"responseValueIsWithinNorm": false,
"status": "5",
"analysisElementOriginalId": "43583-4"
}
},
{
"analysisIdOriginal": "60493-4",
"isWaitingForResults": false,
"analysisName": "Vitamiin D (25-OH)",
"results": {
"nestedElements": [],
"labComment": "Väärtus vahemikus 30-49.9 nmol/L on D-vitamiini ebapiisav tase.",
"unit": "nmol/L",
"normLower": 75,
"normUpper": null,
"normStatus": 1,
"responseTime": "2025-09-12T14:02:04+00:00",
"responseValue": 30,
"normLowerIncluded": false,
"normUpperIncluded": false,
"responseValueIsNegative": null,
"responseValueIsWithinNorm": null,
"status": "4",
"analysisElementOriginalId": "60493-4"
}
}
],
};
export const analysisResponses: AnalysisTestResponse[] = [
empty1,
big1,
big2,
];

View File

@@ -8,10 +8,11 @@ import { listProductTypes } from "@lib/data/products";
import { placeOrder, retrieveCart } from "@lib/data/cart";
import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
import { createAnalysisOrder, getAnalysisOrder } from '~/lib/services/order.service';
import { getOrderedAnalysisIds, sendOrderToMedipost } from '~/lib/services/medipost.service';
import { sendOrderToMedipost } from '~/lib/services/medipost/medipostPrivateMessage.service';
import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service';
import { createNotificationsApi } from '@kit/notifications/api';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import type { AccountWithParams } from '@kit/accounts/api';
import type { AccountWithParams } from '@kit/accounts/types/accounts';
import type { StoreOrder } from '@medusajs/types';
const ANALYSIS_PACKAGES_TYPE_HANDLE = 'analysis-packages';

View File

@@ -1,7 +1,7 @@
import { redirect } from 'next/navigation';
import { toTitleCase } from '@/lib/utils';
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
import { createUserAnalysesApi } from '@kit/user-analyses/api';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import { PageBody, PageHeader } from '@kit/ui/page';
@@ -29,7 +29,7 @@ async function UserHomePage() {
const client = getSupabaseServerClient();
const { account } = await loadCurrentUserAccount();
const api = createAccountsApi(client);
const api = createUserAnalysesApi(client);
const bmiThresholds = await api.fetchBmiThresholds();
if (!account) {

View File

@@ -2,7 +2,7 @@
import Link from 'next/link';
import type { AccountWithParams } from '@/packages/features/accounts/src/server/api';
import type { AccountWithParams } from '@kit/accounts/types/accounts';
import { Database } from '@/packages/supabase/src/database.types';
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
import {

View File

@@ -1,6 +1,6 @@
import { Trans } from '@kit/ui/trans';
import { formatDate } from 'date-fns';
import { AnalysisOrder } from "~/lib/services/order.service";
import type { AnalysisOrder } from "~/lib/types/analysis-order";
export default function OrderDetails({ order }: {
order: AnalysisOrder

View File

@@ -1,4 +1,4 @@
import { AnalysisOrder } from "~/lib/services/order.service";
import type { AnalysisOrder } from "~/lib/types/analysis-order";
import { Trans } from '@kit/ui/makerkit/trans';
import { StoreOrderLineItem } from "@medusajs/types";
import OrderItemsTable from "./order-items-table";

View File

@@ -18,7 +18,7 @@ import {
} from '@kit/ui/table';
import { Trans } from '@kit/ui/trans';
import { AnalysisOrder } from '~/lib/services/order.service';
import type { AnalysisOrder } from '~/lib/types/analysis-order';
import { logAnalysisResultsNavigateAction } from './actions';

View File

@@ -5,8 +5,8 @@ import { listRegions } from '@lib/data/regions';
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
import type { StoreProduct } from '@medusajs/types';
import { loadCurrentUserAccount } from './load-user-account';
import { AccountWithParams } from '@/packages/features/accounts/src/server/api';
import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
import type { AccountWithParams } from '@kit/accounts/types/accounts';
import type { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
import PersonalCode from '~/lib/utils';
async function countryCodesLoader() {

View File

@@ -1,22 +0,0 @@
import { cache } from 'react';
import { createAccountsApi } from '@kit/accounts/api';
import { UserAnalysis } from '@kit/accounts/types/accounts';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export type UserAnalyses = Awaited<ReturnType<typeof loadUserAnalyses>>;
/**
* @name loadUserAnalyses
* @description
* Load the user's analyses. It's a cached per-request function that fetches the user workspace data.
* It can be used across the server components to load the user workspace data.
*/
export const loadUserAnalyses = cache(analysesLoader);
async function analysesLoader(): Promise<UserAnalysis | null> {
const client = getSupabaseServerClient();
const api = createAccountsApi(client);
return api.getUserAnalyses();
}

View File

@@ -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<ReturnType<typeof loadUserAnalysis>>;
@@ -15,9 +15,9 @@ export const loadUserAnalysis = cache(analysisLoader);
async function analysisLoader(
analysisOrderId: number,
): Promise<AnalysisResultDetails | null> {
): Promise<AnalysisResultDetailsMapped | null> {
const client = getSupabaseServerClient();
const api = createAccountsApi(client);
const api = createUserAnalysesApi(client);
return api.getUserAnalysis(analysisOrderId);
}

View File

@@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { Trans } from 'react-i18next';
import { AccountWithParams } from '@kit/accounts/api';
import type { AccountWithParams } from '@kit/accounts/types/accounts';
import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data';
import { Button } from '@kit/ui/button';
import { Card, CardDescription, CardTitle } from '@kit/ui/card';

View File

@@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { Trans } from 'react-i18next';
import { AccountWithParams } from '@kit/accounts/api';
import type { AccountWithParams } from '@kit/accounts/types/accounts';
import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data';
import { Button } from '@kit/ui/button';
import {

View File

@@ -2,10 +2,10 @@
import { use } from 'react';
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
import { CompanyGuard } from '@/packages/features/team-accounts/src/components';
import { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import { createUserAnalysesApi } from '@kit/user-analyses/api';
import { PageBody } from '@kit/ui/page';
@@ -35,10 +35,10 @@ function TeamAccountHomePage({ params }: TeamAccountHomePageProps) {
const account = use(params).account;
const client = getSupabaseServerClient();
const teamAccountsApi = createTeamAccountsApi(client);
const accountsApi = createAccountsApi(client);
const userAnalysesApi = createUserAnalysesApi(client);
const teamAccount = use(teamAccountsApi.getTeamAccount(account));
const { memberParams, members } = use(teamAccountsApi.getMembers(account));
const bmiThresholds = use(accountsApi.fetchBmiThresholds());
const bmiThresholds = use(userAnalysesApi.fetchBmiThresholds());
const companyParams = use(
teamAccountsApi.getTeamAccountParams(teamAccount.id),
);