Merge pull request #97 from MR-medreport/improvements-1709
feat: improvements for medipost service xml building, analysis results view, add jest
This commit is contained in:
44
__mocks__/isikukood.ts
Normal file
44
__mocks__/isikukood.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Mock for isikukood library to avoid ES module issues in tests
|
||||||
|
|
||||||
|
export enum Gender {
|
||||||
|
MALE = 'male',
|
||||||
|
FEMALE = 'female',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Isikukood {
|
||||||
|
private code: string;
|
||||||
|
|
||||||
|
constructor(code: string) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
static validate(code: string): boolean {
|
||||||
|
return true; // Mock always returns true for tests
|
||||||
|
}
|
||||||
|
|
||||||
|
static generate(options?: { gender?: Gender; century?: number }): string {
|
||||||
|
return '39001010002'; // Mock Estonian ID code
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGender(): Gender {
|
||||||
|
return Gender.MALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBirthDate(): Date {
|
||||||
|
return new Date('1990-01-01');
|
||||||
|
}
|
||||||
|
|
||||||
|
getAge(): number {
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCentury(): number {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Isikukood };
|
||||||
3
__mocks__/server-only.ts
Normal file
3
__mocks__/server-only.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Mock for server-only to avoid Next.js server component issues in tests
|
||||||
|
// This module does nothing in tests - it's just a marker for Next.js
|
||||||
|
export {};
|
||||||
@@ -165,7 +165,7 @@ async function createProducts({
|
|||||||
medusa.admin.product.list({
|
medusa.admin.product.list({
|
||||||
category_id: allCategories.map(({ id }) => id),
|
category_id: allCategories.map(({ id }) => id),
|
||||||
}),
|
}),
|
||||||
getAnalysisElements({}),
|
getAnalysisElements({ getAll: true }),
|
||||||
getAnalysisPackagesType(),
|
getAnalysisPackagesType(),
|
||||||
getProductDefaultFields({ medusa }),
|
getProductDefaultFields({ medusa }),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import axios from 'axios';
|
|||||||
import { XMLParser } from 'fast-xml-parser';
|
import { XMLParser } from 'fast-xml-parser';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { createAnalysisGroup, getAnalysisGroups } from '~/lib/services/analysis-group.service';
|
import { createAnalysisGroup, getAnalysisGroups } from '~/lib/services/analysis-group.service';
|
||||||
import { IMedipostPublicMessageDataParsed } from '~/lib/services/medipost.types';
|
import { IMedipostPublicMessageDataParsed } from '~/lib/services/medipost/medipost.types';
|
||||||
import { createAnalysis, createNoDataReceivedEntry, createNoNewDataReceivedEntry, createSyncFailEntry, createSyncSuccessEntry } from '~/lib/services/analyses.service';
|
import { createAnalysis, createNoDataReceivedEntry, createNoNewDataReceivedEntry, createSyncFailEntry, createSyncSuccessEntry, getAnalyses } from '~/lib/services/analyses.service';
|
||||||
import { getLastCheckedDate } from '~/lib/services/sync-entries.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 { 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';
|
import type { ICode } from '~/lib/types/code';
|
||||||
|
|
||||||
function toArray<T>(input?: T | T[] | null): T[] {
|
function toArray<T>(input?: T | T[] | null): T[] {
|
||||||
@@ -82,42 +82,60 @@ export default async function syncAnalysisGroups() {
|
|||||||
const codes: ICode[] = [];
|
const codes: ICode[] = [];
|
||||||
for (const analysisGroup of analysisGroups) {
|
for (const analysisGroup of analysisGroups) {
|
||||||
const existingAnalysisGroup = existingAnalysisGroups?.find(({ original_id }) => original_id === analysisGroup.UuringuGruppId);
|
const existingAnalysisGroup = existingAnalysisGroups?.find(({ original_id }) => original_id === analysisGroup.UuringuGruppId);
|
||||||
|
let groupExistingAnalysisElements: AnalysisElement[] = [];
|
||||||
|
let analysisGroupId: number;
|
||||||
if (existingAnalysisGroup) {
|
if (existingAnalysisGroup) {
|
||||||
console.info(`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists`);
|
console.info(`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists, only creating new analysis elements`);
|
||||||
continue;
|
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);
|
const analysisGroupItems = toArray(analysisGroup.Uuring);
|
||||||
|
|
||||||
for (const item of analysisGroupItems) {
|
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({
|
const insertedAnalysisElementId = await createAnalysisElement({
|
||||||
analysisElement,
|
analysisElement: analysisElement!,
|
||||||
analysisGroupId,
|
analysisGroupId,
|
||||||
materialGroups: toArray(item.MaterjalideGrupp),
|
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) {
|
if (analysisElement.Kood) {
|
||||||
const analysisElementCodes = toArray(analysisElement.Kood);
|
const analysisElementCodes = toArray(analysisElement.Kood);
|
||||||
codes.push(
|
codes.push(
|
||||||
@@ -135,7 +153,15 @@ export default async function syncAnalysisGroups() {
|
|||||||
|
|
||||||
const analyses = analysisElement.UuringuElement;
|
const analyses = analysisElement.UuringuElement;
|
||||||
if (analyses?.length) {
|
if (analyses?.length) {
|
||||||
|
const existingAnalyses = await getAnalyses({ originalIds: analyses.map(({ UuringId }) => UuringId) });
|
||||||
|
|
||||||
for (const analysis of analyses) {
|
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);
|
const insertedAnalysisId = await createAnalysis(analysis, analysisGroupId);
|
||||||
|
|
||||||
if (analysis.Kood) {
|
if (analysis.Kood) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { readPrivateMessageResponse } from "~/lib/services/medipost.service";
|
import { readPrivateMessageResponse } from "~/lib/services/medipost/medipostPrivateMessage.service";
|
||||||
|
|
||||||
type ProcessedMessage = {
|
type ProcessedMessage = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import loadEnv from "../handler/load-env";
|
import loadEnv from "../handler/load-env";
|
||||||
import validateApiKey from "../handler/validate-api-key";
|
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 { retrieveOrder } from "@lib/data/orders";
|
||||||
import { getMedipostDispatchTries } from "~/lib/services/audit.service";
|
import { getMedipostDispatchTries } from "~/lib/services/audit.service";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getAnalysisOrdersAdmin } from "~/lib/services/order.service";
|
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 { retrieveOrder } from "@lib/data";
|
||||||
import { getAccountAdmin } from "~/lib/services/account.service";
|
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 loadEnv from "../handler/load-env";
|
||||||
import validateApiKey from "../handler/validate-api-key";
|
import validateApiKey from "../handler/validate-api-key";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { getAnalysisOrder } from "~/lib/services/order.service";
|
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 { retrieveOrder } from "@lib/data";
|
||||||
import { getAccountAdmin } from "~/lib/services/account.service";
|
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) {
|
export async function POST(request: Request) {
|
||||||
// const isDev = process.env.NODE_ENV === 'development';
|
// const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|||||||
149
app/doctor/_components/analysis-doctor.tsx
Normal file
149
app/doctor/_components/analysis-doctor.tsx
Normal 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;
|
||||||
134
app/doctor/_components/analysis-level-bar.tsx
Normal file
134
app/doctor/_components/analysis-level-bar.tsx
Normal 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;
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from '@kit/ui/collapsible';
|
} from '@kit/ui/collapsible';
|
||||||
import { Trans } from '@kit/ui/trans';
|
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({
|
export default function DoctorAnalysisWrapper({
|
||||||
analysisData,
|
analysisData,
|
||||||
@@ -29,7 +29,7 @@ export default function DoctorAnalysisWrapper({
|
|||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<div className="[&[data-state=open]_.caret-icon]:rotate-180">
|
<div className="[&[data-state=open]_.caret-icon]:rotate-180">
|
||||||
<Analysis
|
<AnalysisDoctor
|
||||||
startIcon={
|
startIcon={
|
||||||
analysisData.latestPreviousAnalysis && (
|
analysisData.latestPreviousAnalysis && (
|
||||||
<CaretDownIcon className="caret-icon transition-transform duration-200" />
|
<CaretDownIcon className="caret-icon transition-transform duration-200" />
|
||||||
@@ -65,7 +65,7 @@ export default function DoctorAnalysisWrapper({
|
|||||||
{analysisData.latestPreviousAnalysis && (
|
{analysisData.latestPreviousAnalysis && (
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="my-1 flex flex-col">
|
<div className="my-1 flex flex-col">
|
||||||
<Analysis
|
<AnalysisDoctor
|
||||||
endIcon={
|
endIcon={
|
||||||
analysisData.latestPreviousAnalysis.comment && (
|
analysisData.latestPreviousAnalysis.comment && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export default async function AnalysisResultsPage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const orderedAnalysisElements = analysisResponse.orderedAnalysisElements;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
@@ -80,7 +82,7 @@ export default async function AnalysisResultsPage({
|
|||||||
<h4>
|
<h4>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="analysis-results:orderTitle"
|
i18nKey="analysis-results:orderTitle"
|
||||||
values={{ orderNumber: analysisResponse.order.medusa_order_id }}
|
values={{ orderNumber: analysisResponse.order.medusaOrderId }}
|
||||||
/>
|
/>
|
||||||
</h4>
|
</h4>
|
||||||
<h5>
|
<h5>
|
||||||
@@ -88,7 +90,7 @@ export default async function AnalysisResultsPage({
|
|||||||
i18nKey={`orders:status.${analysisResponse.order.status}`}
|
i18nKey={`orders:status.${analysisResponse.order.status}`}
|
||||||
/>
|
/>
|
||||||
<ButtonTooltip
|
<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"
|
className="ml-6"
|
||||||
/>
|
/>
|
||||||
</h5>
|
</h5>
|
||||||
@@ -102,13 +104,9 @@ export default async function AnalysisResultsPage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{analysisResponse.elements ? (
|
{orderedAnalysisElements ? (
|
||||||
analysisResponse.elements.map((element, index) => (
|
orderedAnalysisElements.map((element, index) => (
|
||||||
<Analysis
|
<Analysis key={index} element={element} />
|
||||||
key={index}
|
|
||||||
analysisElement={{ analysis_name_lab: element.analysis_name }}
|
|
||||||
results={element}
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="text-muted-foreground text-sm">
|
<div className="text-muted-foreground text-sm">
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ 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 { AnalysisResultForDisplay } from './analysis';
|
import { AnalysisResultDetailsElementResults } from '@/packages/features/accounts/src/types/analysis-results';
|
||||||
|
|
||||||
export enum AnalysisResultLevel {
|
export enum AnalysisResultLevel {
|
||||||
VERY_LOW = 0,
|
NORMAL = 0,
|
||||||
LOW = 1,
|
WARNING = 1,
|
||||||
NORMAL = 2,
|
CRITICAL = 2,
|
||||||
HIGH = 3,
|
|
||||||
VERY_HIGH = 4,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Level = ({
|
const Level = ({
|
||||||
@@ -19,17 +17,19 @@ const Level = ({
|
|||||||
isFirst = false,
|
isFirst = false,
|
||||||
isLast = false,
|
isLast = false,
|
||||||
arrowLocation,
|
arrowLocation,
|
||||||
|
normRangeText,
|
||||||
}: {
|
}: {
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
color: 'destructive' | 'success' | 'warning' | 'gray-200';
|
color: 'destructive' | 'success' | 'warning' | 'gray-200';
|
||||||
isFirst?: boolean;
|
isFirst?: boolean;
|
||||||
isLast?: boolean;
|
isLast?: boolean;
|
||||||
arrowLocation?: number;
|
arrowLocation?: number;
|
||||||
|
normRangeText?: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(`bg-${color} relative h-3 flex-1`, {
|
className={cn(`bg-${color} relative h-3 flex-1`, {
|
||||||
'opacity-20': !isActive,
|
'opacity-60': !isActive,
|
||||||
'rounded-l-lg': isFirst,
|
'rounded-l-lg': isFirst,
|
||||||
'rounded-r-lg': isLast,
|
'rounded-r-lg': isLast,
|
||||||
})}
|
})}
|
||||||
@@ -37,96 +37,176 @@ const Level = ({
|
|||||||
{isActive && (
|
{isActive && (
|
||||||
<div
|
<div
|
||||||
className="absolute top-[-14px] left-1/2 -translate-x-1/2 rounded-[10px] bg-white p-[2px]"
|
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} />
|
<ArrowDown strokeWidth={2} />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AnalysisLevelBarSkeleton = () => {
|
export const AnalysisLevelBarSkeleton = () => {
|
||||||
return (
|
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" />
|
<Level color="gray-200" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AnalysisLevelBar = ({
|
const AnalysisLevelBar = ({
|
||||||
normLowerIncluded = true,
|
|
||||||
normUpperIncluded = true,
|
|
||||||
level,
|
level,
|
||||||
results,
|
results,
|
||||||
|
normRangeText,
|
||||||
}: {
|
}: {
|
||||||
normLowerIncluded?: boolean;
|
|
||||||
normUpperIncluded?: boolean;
|
|
||||||
level: AnalysisResultLevel;
|
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 arrowLocation = useMemo(() => {
|
const normLowerIncluded = results?.normLowerIncluded || false;
|
||||||
if (value < lower!) {
|
const normUpperIncluded = results?.normUpperIncluded || false;
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normLowerIncluded || normUpperIncluded) {
|
// Calculate arrow position based on value within normal range
|
||||||
|
const arrowLocation = useMemo(() => {
|
||||||
|
// If no response value, center the arrow
|
||||||
|
if (value === null || value === undefined) {
|
||||||
return 50;
|
return 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculated = ((value - lower!) / (upper! - lower!)) * 100;
|
// If no normal ranges defined, center the arrow
|
||||||
|
if (lower === null && upper === null) {
|
||||||
if (calculated > 100) {
|
return 50;
|
||||||
return 100;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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]);
|
}, [value, upper, lower]);
|
||||||
|
|
||||||
const [isVeryLow, isLow, isHigh, isVeryHigh] = useMemo(() => [
|
// Determine level states based on normStatus
|
||||||
level === AnalysisResultLevel.VERY_LOW,
|
const isNormal = level === AnalysisResultLevel.NORMAL;
|
||||||
level === AnalysisResultLevel.LOW,
|
const isWarning = level === AnalysisResultLevel.WARNING;
|
||||||
level === AnalysisResultLevel.HIGH,
|
const isCritical = level === AnalysisResultLevel.CRITICAL;
|
||||||
level === AnalysisResultLevel.VERY_HIGH,
|
const isPending = level === null;
|
||||||
], [level, value, upper, lower]);
|
|
||||||
|
|
||||||
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 (
|
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">
|
||||||
{normLowerIncluded && (
|
<Level {...first} />
|
||||||
<>
|
<Level {...second} />
|
||||||
<Level
|
<Level {...third} />
|
||||||
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,33 +1,19 @@
|
|||||||
'use client';
|
'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 { format } from 'date-fns';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
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 { AnalysisElement } from '~/lib/services/analysis-element.service';
|
|
||||||
|
|
||||||
import AnalysisLevelBar, {
|
import AnalysisLevelBar, {
|
||||||
AnalysisLevelBarSkeleton,
|
|
||||||
AnalysisResultLevel,
|
AnalysisResultLevel,
|
||||||
} from './analysis-level-bar';
|
} 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 {
|
export enum AnalysisStatus {
|
||||||
NORMAL = 0,
|
NORMAL = 0,
|
||||||
MEDIUM = 1,
|
MEDIUM = 1,
|
||||||
@@ -35,26 +21,45 @@ export enum AnalysisStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Analysis = ({
|
const Analysis = ({
|
||||||
analysisElement,
|
element,
|
||||||
results,
|
|
||||||
startIcon,
|
|
||||||
endIcon,
|
|
||||||
isCancelled,
|
|
||||||
}: {
|
}: {
|
||||||
analysisElement: Pick<AnalysisElement, 'analysis_name_lab'>;
|
element: AnalysisResultDetailsElement;
|
||||||
results?: AnalysisResultForDisplay;
|
|
||||||
isCancelled?: boolean;
|
|
||||||
startIcon?: ReactElement | null;
|
|
||||||
endIcon?: ReactNode | null;
|
|
||||||
}) => {
|
}) => {
|
||||||
const name = analysisElement.analysis_name_lab || '';
|
const { t } = useTranslation();
|
||||||
const status = results?.norm_status || AnalysisStatus.NORMAL;
|
|
||||||
const value = results?.response_value || 0;
|
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 unit = results?.unit || '';
|
||||||
const normLowerIncluded = results?.norm_lower_included || false;
|
const normLower = results?.normLower;
|
||||||
const normUpperIncluded = results?.norm_upper_included || false;
|
const normUpper = results?.normUpper;
|
||||||
const normLower = results?.norm_lower || 0;
|
|
||||||
const normUpper = results?.norm_upper || 0;
|
|
||||||
|
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
const analysisResultLevel = useMemo(() => {
|
const analysisResultLevel = useMemo(() => {
|
||||||
@@ -62,32 +67,34 @@ const Analysis = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUnderNorm = value < normLower;
|
if (results.responseValue === null || results.responseValue === undefined) {
|
||||||
if (isUnderNorm) {
|
return null;
|
||||||
switch (status) {
|
|
||||||
case AnalysisStatus.MEDIUM:
|
|
||||||
return AnalysisResultLevel.LOW;
|
|
||||||
default:
|
|
||||||
return AnalysisResultLevel.VERY_LOW;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
switch (status) {
|
|
||||||
case AnalysisStatus.MEDIUM:
|
const normStatus = results.normStatus;
|
||||||
return AnalysisResultLevel.HIGH;
|
|
||||||
case AnalysisStatus.HIGH:
|
switch (normStatus) {
|
||||||
return AnalysisResultLevel.VERY_HIGH;
|
case 1:
|
||||||
|
return AnalysisResultLevel.WARNING;
|
||||||
|
case 2:
|
||||||
|
return AnalysisResultLevel.CRITICAL;
|
||||||
|
case 0:
|
||||||
default:
|
default:
|
||||||
return AnalysisResultLevel.NORMAL;
|
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 (
|
return (
|
||||||
<div className="border-border rounded-lg border px-5">
|
<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">
|
<div className="flex items-center gap-2 font-semibold">
|
||||||
{startIcon || <div className="w-4" />}
|
|
||||||
{name}
|
{name}
|
||||||
{results?.response_time && (
|
{results?.responseTime && (
|
||||||
<div
|
<div
|
||||||
className="group/tooltip relative"
|
className="group/tooltip relative"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -105,42 +112,41 @@ const Analysis = ({
|
|||||||
>
|
>
|
||||||
<Trans i18nKey="analysis-results:analysisDate" />
|
<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>
|
</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="flex items-center gap-3 sm:ml-auto">
|
||||||
<div className="font-semibold">{value}</div>
|
<div className="font-semibold">{value}</div>
|
||||||
<div className="text-muted-foreground text-sm">{unit}</div>
|
<div className="text-muted-foreground text-sm">{unit}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
|
{!(hasIsNegative || hasIsWithinNorm) && (
|
||||||
{normLower} - {normUpper}
|
<>
|
||||||
<div>
|
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
|
||||||
<Trans i18nKey="analysis-results:results.range.normal" />
|
{normRangeText}
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<Trans i18nKey="analysis-results:results.range.normal" />
|
||||||
<AnalysisLevelBar
|
</div>
|
||||||
results={results}
|
</div>
|
||||||
normLowerIncluded={normLowerIncluded}
|
<AnalysisLevelBar
|
||||||
normUpperIncluded={normUpperIncluded}
|
results={results}
|
||||||
level={analysisResultLevel!}
|
level={analysisResultLevel!}
|
||||||
/>
|
normRangeText={normRangeText}
|
||||||
{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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
99
app/home/(user)/(dashboard)/analysis-results/test/page.tsx
Normal file
99
app/home/(user)/(dashboard)/analysis-results/test/page.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
@@ -8,10 +8,11 @@ import { listProductTypes } from "@lib/data/products";
|
|||||||
import { placeOrder, retrieveCart } from "@lib/data/cart";
|
import { placeOrder, retrieveCart } from "@lib/data/cart";
|
||||||
import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
|
import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
|
||||||
import { createAnalysisOrder, getAnalysisOrder } from '~/lib/services/order.service';
|
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 { createNotificationsApi } from '@kit/notifications/api';
|
||||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
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';
|
import type { StoreOrder } from '@medusajs/types';
|
||||||
|
|
||||||
const ANALYSIS_PACKAGES_TYPE_HANDLE = 'analysis-packages';
|
const ANALYSIS_PACKAGES_TYPE_HANDLE = 'analysis-packages';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { toTitleCase } from '@/lib/utils';
|
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 { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||||
|
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
@@ -27,7 +27,7 @@ async function UserHomePage() {
|
|||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
const { account } = await loadCurrentUserAccount();
|
const { account } = await loadCurrentUserAccount();
|
||||||
const api = createAccountsApi(client);
|
const api = createUserAnalysesApi(client);
|
||||||
const bmiThresholds = await api.fetchBmiThresholds();
|
const bmiThresholds = await api.fetchBmiThresholds();
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
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 { Database } from '@/packages/supabase/src/database.types';
|
||||||
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
|
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
import { formatDate } from 'date-fns';
|
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 }: {
|
export default function OrderDetails({ order }: {
|
||||||
order: AnalysisOrder
|
order: AnalysisOrder
|
||||||
|
|||||||
@@ -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 { Trans } from '@kit/ui/makerkit/trans';
|
||||||
import { StoreOrderLineItem } from "@medusajs/types";
|
import { StoreOrderLineItem } from "@medusajs/types";
|
||||||
import OrderItemsTable from "./order-items-table";
|
import OrderItemsTable from "./order-items-table";
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
} from '@kit/ui/table';
|
} from '@kit/ui/table';
|
||||||
import { Trans } from '@kit/ui/trans';
|
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';
|
import { logAnalysisResultsNavigateAction } from './actions';
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { listRegions } from '@lib/data/regions';
|
|||||||
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
||||||
import type { StoreProduct } from '@medusajs/types';
|
import type { StoreProduct } from '@medusajs/types';
|
||||||
import { loadCurrentUserAccount } from './load-user-account';
|
import { loadCurrentUserAccount } from './load-user-account';
|
||||||
import { AccountWithParams } from '@/packages/features/accounts/src/server/api';
|
import type { AccountWithParams } from '@kit/accounts/types/accounts';
|
||||||
import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
|
import type { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
|
||||||
import PersonalCode from '~/lib/utils';
|
import PersonalCode from '~/lib/utils';
|
||||||
|
|
||||||
async function countryCodesLoader() {
|
async function countryCodesLoader() {
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
|
|
||||||
import { createAccountsApi } from '@kit/accounts/api';
|
import { AnalysisResultDetailsMapped } from '@kit/accounts/types/analysis-results';
|
||||||
import { AnalysisResultDetails } from '@kit/accounts/types/accounts';
|
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
||||||
|
|
||||||
export type UserAnalyses = Awaited<ReturnType<typeof loadUserAnalysis>>;
|
export type UserAnalyses = Awaited<ReturnType<typeof loadUserAnalysis>>;
|
||||||
|
|
||||||
@@ -15,9 +15,9 @@ export const loadUserAnalysis = cache(analysisLoader);
|
|||||||
|
|
||||||
async function analysisLoader(
|
async function analysisLoader(
|
||||||
analysisOrderId: number,
|
analysisOrderId: number,
|
||||||
): Promise<AnalysisResultDetails | null> {
|
): Promise<AnalysisResultDetailsMapped | null> {
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
const api = createAccountsApi(client);
|
const api = createUserAnalysesApi(client);
|
||||||
|
|
||||||
return api.getUserAnalysis(analysisOrderId);
|
return api.getUserAnalysis(analysisOrderId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Trans } from 'react-i18next';
|
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 { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Card, CardDescription, CardTitle } from '@kit/ui/card';
|
import { Card, CardDescription, CardTitle } from '@kit/ui/card';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Trans } from 'react-i18next';
|
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 { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import { use } from 'react';
|
import { use } from 'react';
|
||||||
|
|
||||||
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
|
|
||||||
import { CompanyGuard } from '@/packages/features/team-accounts/src/components';
|
import { CompanyGuard } from '@/packages/features/team-accounts/src/components';
|
||||||
import { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api';
|
import { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api';
|
||||||
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||||
|
import { createUserAnalysesApi } from '@kit/user-analyses/api';
|
||||||
|
|
||||||
import { PageBody } from '@kit/ui/page';
|
import { PageBody } from '@kit/ui/page';
|
||||||
|
|
||||||
@@ -35,10 +35,10 @@ function TeamAccountHomePage({ params }: TeamAccountHomePageProps) {
|
|||||||
const account = use(params).account;
|
const account = use(params).account;
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
const teamAccountsApi = createTeamAccountsApi(client);
|
const teamAccountsApi = createTeamAccountsApi(client);
|
||||||
const accountsApi = createAccountsApi(client);
|
const userAnalysesApi = createUserAnalysesApi(client);
|
||||||
const teamAccount = use(teamAccountsApi.getTeamAccount(account));
|
const teamAccount = use(teamAccountsApi.getTeamAccount(account));
|
||||||
const { memberParams, members } = use(teamAccountsApi.getMembers(account));
|
const { memberParams, members } = use(teamAccountsApi.getMembers(account));
|
||||||
const bmiThresholds = use(accountsApi.fetchBmiThresholds());
|
const bmiThresholds = use(userAnalysesApi.fetchBmiThresholds());
|
||||||
const companyParams = use(
|
const companyParams = use(
|
||||||
teamAccountsApi.getTeamAccountParams(teamAccount.id),
|
teamAccountsApi.getTeamAccountParams(teamAccount.id),
|
||||||
);
|
);
|
||||||
|
|||||||
66
jest.config.js
Normal file
66
jest.config.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { pathsToModuleNameMapper } from 'ts-jest';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
|
||||||
|
const tsconfig = JSON.parse(readFileSync('./tsconfig.json', 'utf8'));
|
||||||
|
|
||||||
|
/** @type {import('jest').Config} */
|
||||||
|
export default {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
|
||||||
|
// Handle module resolution for TypeScript paths
|
||||||
|
moduleNameMapper: {
|
||||||
|
...pathsToModuleNameMapper(tsconfig.compilerOptions.paths, {
|
||||||
|
prefix: '<rootDir>/',
|
||||||
|
}),
|
||||||
|
// Mock problematic libraries
|
||||||
|
'^isikukood$': '<rootDir>/__mocks__/isikukood.ts',
|
||||||
|
'^server-only$': '<rootDir>/__mocks__/server-only.ts',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test file patterns
|
||||||
|
testMatch: [
|
||||||
|
'**/__tests__/**/*.(ts|tsx|js)',
|
||||||
|
'**/*.(test|spec).(ts|tsx|js)'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Setup files
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||||
|
|
||||||
|
// Coverage configuration
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'lib/**/*.{ts,tsx}',
|
||||||
|
'app/**/*.{ts,tsx}',
|
||||||
|
'components/**/*.{ts,tsx}',
|
||||||
|
'!**/*.d.ts',
|
||||||
|
'!**/node_modules/**',
|
||||||
|
'!**/.next/**'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Transform configuration
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(ts|tsx)$': ['ts-jest', {
|
||||||
|
useESM: true,
|
||||||
|
tsconfig: {
|
||||||
|
jsx: 'react-jsx',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Module file extensions
|
||||||
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
||||||
|
|
||||||
|
// Ignore patterns
|
||||||
|
testPathIgnorePatterns: [
|
||||||
|
'<rootDir>/.next/',
|
||||||
|
'<rootDir>/node_modules/',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Transform ignore patterns for node_modules
|
||||||
|
transformIgnorePatterns: [
|
||||||
|
'node_modules/(?!(.*\\.mjs$))',
|
||||||
|
],
|
||||||
|
|
||||||
|
// ESM support
|
||||||
|
extensionsToTreatAsEsm: ['.ts'],
|
||||||
|
};
|
||||||
59
jest.setup.js
Normal file
59
jest.setup.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Jest setup file for global test configuration
|
||||||
|
|
||||||
|
// Mock Next.js router
|
||||||
|
jest.mock('next/router', () => ({
|
||||||
|
useRouter() {
|
||||||
|
return {
|
||||||
|
route: '/',
|
||||||
|
pathname: '/',
|
||||||
|
query: {},
|
||||||
|
asPath: '/',
|
||||||
|
push: jest.fn(),
|
||||||
|
pop: jest.fn(),
|
||||||
|
reload: jest.fn(),
|
||||||
|
back: jest.fn(),
|
||||||
|
prefetch: jest.fn().mockResolvedValue(undefined),
|
||||||
|
beforePopState: jest.fn(),
|
||||||
|
events: {
|
||||||
|
on: jest.fn(),
|
||||||
|
off: jest.fn(),
|
||||||
|
emit: jest.fn(),
|
||||||
|
},
|
||||||
|
isFallback: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock Next.js navigation
|
||||||
|
jest.mock('next/navigation', () => ({
|
||||||
|
useRouter() {
|
||||||
|
return {
|
||||||
|
push: jest.fn(),
|
||||||
|
replace: jest.fn(),
|
||||||
|
prefetch: jest.fn(),
|
||||||
|
back: jest.fn(),
|
||||||
|
forward: jest.fn(),
|
||||||
|
refresh: jest.fn(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
useSearchParams() {
|
||||||
|
return new URLSearchParams();
|
||||||
|
},
|
||||||
|
usePathname() {
|
||||||
|
return '/';
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Global test utilities
|
||||||
|
global.console = {
|
||||||
|
...console,
|
||||||
|
// Suppress console.log in tests unless needed
|
||||||
|
log: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set up environment variables for tests
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Tables } from '@/packages/supabase/src/database.types';
|
import type { Tables } from '@/packages/supabase/src/database.types';
|
||||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||||
import type { IUuringElement } from "./medipost.types";
|
import type { IUuringElement } from "./medipost/medipost.types";
|
||||||
|
|
||||||
export type AnalysesWithGroupsAndElements = ({
|
export type AnalysesWithGroupsAndElements = ({
|
||||||
analysis_elements: Tables<{ schema: 'medreport' }, 'analysis_elements'> & {
|
analysis_elements: Tables<{ schema: 'medreport' }, 'analysis_elements'> & {
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { Json, Tables } from '@kit/supabase/database';
|
import { Json, Tables } from '@kit/supabase/database';
|
||||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||||
import type { IMaterialGroup, IUuringElement } from './medipost.types';
|
import type { IMaterialGroup, IUuringElement } from './medipost/medipost.types';
|
||||||
|
|
||||||
export type AnalysisElement = Tables<{ schema: 'medreport' }, 'analysis_elements'> & {
|
export type AnalysisElement = Tables<{ schema: 'medreport' }, 'analysis_elements'> & {
|
||||||
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
|
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getAnalysisElements({
|
export async function getAnalysisElements({
|
||||||
|
getAll,
|
||||||
originalIds,
|
originalIds,
|
||||||
ids,
|
ids,
|
||||||
|
analysisGroupId,
|
||||||
}: {
|
}: {
|
||||||
|
getAll?: boolean;
|
||||||
originalIds?: string[];
|
originalIds?: string[];
|
||||||
ids?: number[];
|
ids?: number[];
|
||||||
|
analysisGroupId?: number;
|
||||||
}): Promise<AnalysisElement[]> {
|
}): Promise<AnalysisElement[]> {
|
||||||
const query = getSupabaseServerAdminClient()
|
const query = getSupabaseServerAdminClient()
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
@@ -19,14 +23,26 @@ export async function getAnalysisElements({
|
|||||||
.select(`*, analysis_groups(*)`)
|
.select(`*, analysis_groups(*)`)
|
||||||
.order('order', { ascending: true });
|
.order('order', { ascending: true });
|
||||||
|
|
||||||
if (Array.isArray(originalIds)) {
|
const hasOriginalIdsFilter = Array.isArray(originalIds);
|
||||||
|
const hasIdsFilter = Array.isArray(ids);
|
||||||
|
const hasAnalysisGroupIdFilter = typeof analysisGroupId === 'number';
|
||||||
|
|
||||||
|
if (!hasOriginalIdsFilter && !hasIdsFilter && !hasAnalysisGroupIdFilter && getAll !== true) {
|
||||||
|
throw new Error('Either originalIds, ids, or analysisGroupId must be provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOriginalIdsFilter) {
|
||||||
query.in('analysis_id_original', [...new Set(originalIds)]);
|
query.in('analysis_id_original', [...new Set(originalIds)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(ids)) {
|
if (hasIdsFilter) {
|
||||||
query.in('id', ids);
|
query.in('id', ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasAnalysisGroupIdFilter) {
|
||||||
|
query.eq('parent_analysis_group_id', analysisGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
const { data: analysisElements, error } = await query;
|
const { data: analysisElements, error } = await query;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
68
lib/services/analysis-order.service.ts
Normal file
68
lib/services/analysis-order.service.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { getSupabaseServerAdminClient } from "@/packages/supabase/src/clients/server-admin-client";
|
||||||
|
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<AnalysisResponseElement[]> {
|
||||||
|
const { data } = await getSupabaseServerAdminClient()
|
||||||
|
.schema('medreport')
|
||||||
|
.from('analysis_response_elements')
|
||||||
|
.select('*')
|
||||||
|
.eq('analysis_response_id', analysisResponseId)
|
||||||
|
.throwOnError();
|
||||||
|
|
||||||
|
return data as AnalysisResponseElement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAnalysisResponseElement({
|
||||||
|
element,
|
||||||
|
}: {
|
||||||
|
element: Omit<AnalysisResponseElement, 'created_at' | 'updated_at' | 'id'>;
|
||||||
|
}) {
|
||||||
|
await getSupabaseServerAdminClient()
|
||||||
|
.schema('medreport')
|
||||||
|
.from('analysis_response_elements')
|
||||||
|
.insert(element)
|
||||||
|
.throwOnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertAnalysisResponse({
|
||||||
|
analysisOrderId,
|
||||||
|
orderNumber,
|
||||||
|
orderStatus,
|
||||||
|
userId,
|
||||||
|
}: {
|
||||||
|
analysisOrderId: number;
|
||||||
|
orderNumber: string;
|
||||||
|
orderStatus: typeof AnalysisOrderStatus[keyof typeof AnalysisOrderStatus];
|
||||||
|
userId: string;
|
||||||
|
}) {
|
||||||
|
const { data: analysisResponse } = await getSupabaseServerAdminClient()
|
||||||
|
.schema('medreport')
|
||||||
|
.from('analysis_responses')
|
||||||
|
.upsert(
|
||||||
|
{
|
||||||
|
analysis_order_id: analysisOrderId,
|
||||||
|
order_number: orderNumber,
|
||||||
|
order_status: orderStatus,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
{ onConflict: 'order_number', ignoreDuplicates: false },
|
||||||
|
)
|
||||||
|
.select('id')
|
||||||
|
.throwOnError();
|
||||||
|
|
||||||
|
|
||||||
|
const analysisResponseId = analysisResponse?.[0]?.id;
|
||||||
|
if (!analysisResponseId) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to insert or update analysis order response (order id: ${analysisOrderId}, order number: ${orderNumber})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { analysisResponseId };
|
||||||
|
}
|
||||||
@@ -1,791 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import {
|
|
||||||
SupabaseClient,
|
|
||||||
createClient as createCustomClient,
|
|
||||||
} from '@supabase/supabase-js';
|
|
||||||
|
|
||||||
import { SyncStatus } from '@/lib/types/audit';
|
|
||||||
import {
|
|
||||||
AnalysisOrderStatus,
|
|
||||||
GetMessageListResponse,
|
|
||||||
IMedipostResponseXMLBase,
|
|
||||||
MedipostAction,
|
|
||||||
MedipostOrderResponse,
|
|
||||||
MedipostPublicMessageResponse,
|
|
||||||
Message,
|
|
||||||
ResponseUuringuGrupp,
|
|
||||||
UuringuGrupp,
|
|
||||||
} from '@/lib/types/medipost';
|
|
||||||
import { toArray } from '@/lib/utils';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { XMLParser } from 'fast-xml-parser';
|
|
||||||
|
|
||||||
import { Tables } from '@kit/supabase/database';
|
|
||||||
import { createAnalysisGroup } from './analysis-group.service';
|
|
||||||
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
|
||||||
import { getAnalysisOrder, updateAnalysisOrderStatus } from './order.service';
|
|
||||||
import { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service';
|
|
||||||
import { getAnalyses } from './analyses.service';
|
|
||||||
import { getAccountAdmin } from './account.service';
|
|
||||||
import { StoreOrder } from '@medusajs/types';
|
|
||||||
import { listProducts } from '@lib/data/products';
|
|
||||||
import { listRegions } from '@lib/data/regions';
|
|
||||||
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
|
||||||
import { MedipostValidationError } from './medipost/MedipostValidationError';
|
|
||||||
import { logMedipostDispatch } from './audit.service';
|
|
||||||
import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service';
|
|
||||||
|
|
||||||
const BASE_URL = process.env.MEDIPOST_URL!;
|
|
||||||
const USER = process.env.MEDIPOST_USER!;
|
|
||||||
const PASSWORD = process.env.MEDIPOST_PASSWORD!;
|
|
||||||
const RECIPIENT = process.env.MEDIPOST_RECIPIENT!;
|
|
||||||
|
|
||||||
const ANALYSIS_PACKAGE_HANDLE_PREFIX = 'analysis-package-';
|
|
||||||
|
|
||||||
function parseXML(xml: string) {
|
|
||||||
const parser = new XMLParser({ ignoreAttributes: false });
|
|
||||||
return parser.parse(xml);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function validateMedipostResponse(response: string, { canHaveEmptyCode = false }: { canHaveEmptyCode?: boolean } = {}) {
|
|
||||||
const parsed: IMedipostResponseXMLBase = parseXML(response);
|
|
||||||
const code = parsed.ANSWER?.CODE;
|
|
||||||
if (canHaveEmptyCode) {
|
|
||||||
if (code && code !== 0) {
|
|
||||||
console.error("Bad response", response);
|
|
||||||
throw new MedipostValidationError(response);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof code !== 'number' || (code !== 0 && !canHaveEmptyCode)) {
|
|
||||||
console.error("Bad response", response);
|
|
||||||
throw new MedipostValidationError(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getMessages() {
|
|
||||||
try {
|
|
||||||
const publicMessage = await getLatestPublicMessageListItem();
|
|
||||||
|
|
||||||
if (!publicMessage) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Teenused tuleb mappida kokku MedReport teenustega. <UuringId> alusel
|
|
||||||
return getPublicMessage(publicMessage.messageId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getLatestPublicMessageListItem() {
|
|
||||||
const { data } = await axios.get<GetMessageListResponse>(BASE_URL, {
|
|
||||||
params: {
|
|
||||||
Action: MedipostAction.GetPublicMessageList,
|
|
||||||
User: USER,
|
|
||||||
Password: PASSWORD,
|
|
||||||
Sender: RECIPIENT,
|
|
||||||
// LastChecked (date+time) can be used here to get only messages since the last check - add when cron is created
|
|
||||||
// MessageType check only for messages of certain type
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data.code && data.code !== 0) {
|
|
||||||
throw new Error('Failed to get public message list');
|
|
||||||
}
|
|
||||||
|
|
||||||
return getLatestMessage({ messages: data?.messages });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPublicMessage(messageId: string) {
|
|
||||||
const { data } = await axios.get(BASE_URL, {
|
|
||||||
params: {
|
|
||||||
Action: MedipostAction.GetPublicMessage,
|
|
||||||
User: USER,
|
|
||||||
Password: PASSWORD,
|
|
||||||
MessageId: messageId,
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/xml',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await validateMedipostResponse(data);
|
|
||||||
return parseXML(data) as MedipostPublicMessageResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sendPrivateMessage(messageXml: string) {
|
|
||||||
const body = new FormData();
|
|
||||||
body.append('Action', MedipostAction.SendPrivateMessage);
|
|
||||||
body.append('User', USER);
|
|
||||||
body.append('Password', PASSWORD);
|
|
||||||
body.append('Receiver', RECIPIENT);
|
|
||||||
body.append('MessageType', 'Tellimus');
|
|
||||||
body.append(
|
|
||||||
'Message',
|
|
||||||
new Blob([messageXml], {
|
|
||||||
type: 'text/xml; charset=UTF-8',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data } = await axios.post(BASE_URL, body);
|
|
||||||
|
|
||||||
await validateMedipostResponse(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getLatestPrivateMessageListItem({
|
|
||||||
excludedMessageIds,
|
|
||||||
}: {
|
|
||||||
excludedMessageIds: string[];
|
|
||||||
}) {
|
|
||||||
const { data } = await axios.get<GetMessageListResponse>(BASE_URL, {
|
|
||||||
params: {
|
|
||||||
Action: MedipostAction.GetPrivateMessageList,
|
|
||||||
User: USER,
|
|
||||||
Password: PASSWORD,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data.code && data.code !== 0) {
|
|
||||||
throw new Error('Failed to get private message list');
|
|
||||||
}
|
|
||||||
|
|
||||||
return getLatestMessage({ messages: data?.messages, excludedMessageIds });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPrivateMessage(messageId: string) {
|
|
||||||
const { data } = await axios.get(BASE_URL, {
|
|
||||||
params: {
|
|
||||||
Action: MedipostAction.GetPrivateMessage,
|
|
||||||
User: USER,
|
|
||||||
Password: PASSWORD,
|
|
||||||
MessageId: messageId,
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/xml',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await validateMedipostResponse(data, { canHaveEmptyCode: true });
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: parseXML(data) as MedipostOrderResponse,
|
|
||||||
xml: data as string,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deletePrivateMessage(messageId: string) {
|
|
||||||
const { data } = await axios.get(BASE_URL, {
|
|
||||||
params: {
|
|
||||||
Action: MedipostAction.DeletePrivateMessage,
|
|
||||||
User: USER,
|
|
||||||
Password: PASSWORD,
|
|
||||||
MessageId: messageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data.code && data.code !== 0) {
|
|
||||||
throw new Error(`Failed to delete private message (id: ${messageId})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readPrivateMessageResponse({
|
|
||||||
excludedMessageIds,
|
|
||||||
}: {
|
|
||||||
excludedMessageIds: string[];
|
|
||||||
}): Promise<{ messageId: string | null; hasAnalysisResponse: boolean; hasPartialAnalysisResponse: boolean; hasFullAnalysisResponse: boolean; medusaOrderId: string | undefined; analysisOrderId: number | undefined }> {
|
|
||||||
let messageId: string | null = null;
|
|
||||||
let hasAnalysisResponse = false;
|
|
||||||
let hasPartialAnalysisResponse = false;
|
|
||||||
let hasFullAnalysisResponse = false;
|
|
||||||
let medusaOrderId: string | undefined = undefined;
|
|
||||||
let analysisOrderId: number | undefined = undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const privateMessage = await getLatestPrivateMessageListItem({ excludedMessageIds });
|
|
||||||
messageId = privateMessage?.messageId ?? null;
|
|
||||||
|
|
||||||
if (!privateMessage || !messageId) {
|
|
||||||
return {
|
|
||||||
messageId: null,
|
|
||||||
hasAnalysisResponse: false,
|
|
||||||
hasPartialAnalysisResponse: false,
|
|
||||||
hasFullAnalysisResponse: false,
|
|
||||||
medusaOrderId: undefined,
|
|
||||||
analysisOrderId: undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { message: privateMessageContent, xml: privateMessageXml } = await getPrivateMessage(
|
|
||||||
privateMessage.messageId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const messageResponse = privateMessageContent?.Saadetis?.Vastus;
|
|
||||||
analysisOrderId = Number(privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId);
|
|
||||||
|
|
||||||
const hasInvalidOrderId = isNaN(analysisOrderId)
|
|
||||||
|
|
||||||
if (hasInvalidOrderId || !messageResponse) {
|
|
||||||
await createMedipostActionLog({
|
|
||||||
action: 'sync_analysis_results_from_medipost',
|
|
||||||
xml: privateMessageXml,
|
|
||||||
hasAnalysisResults: false,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
messageId,
|
|
||||||
hasAnalysisResponse: false,
|
|
||||||
hasPartialAnalysisResponse: false,
|
|
||||||
hasFullAnalysisResponse: false,
|
|
||||||
medusaOrderId: hasInvalidOrderId ? undefined : medusaOrderId,
|
|
||||||
analysisOrderId: hasInvalidOrderId ? undefined : analysisOrderId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const analysisOrder = await getAnalysisOrder({ analysisOrderId: analysisOrderId })
|
|
||||||
medusaOrderId = analysisOrder.medusa_order_id;
|
|
||||||
|
|
||||||
let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
|
||||||
try {
|
|
||||||
order = await getAnalysisOrder({ medusaOrderId });
|
|
||||||
} catch (e) {
|
|
||||||
await deletePrivateMessage(privateMessage.messageId);
|
|
||||||
throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = await syncPrivateMessage({ messageResponse, order });
|
|
||||||
|
|
||||||
if (status.isPartial) {
|
|
||||||
await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' });
|
|
||||||
hasAnalysisResponse = true;
|
|
||||||
hasPartialAnalysisResponse = true;
|
|
||||||
} else if (status.isCompleted) {
|
|
||||||
await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' });
|
|
||||||
await deletePrivateMessage(privateMessage.messageId);
|
|
||||||
hasAnalysisResponse = true;
|
|
||||||
hasFullAnalysisResponse = true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Failed to process private message id=${messageId}, message=${(e as Error).message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { messageId, hasAnalysisResponse, hasPartialAnalysisResponse, hasFullAnalysisResponse, medusaOrderId, analysisOrderId };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveAnalysisGroup(
|
|
||||||
analysisGroup: UuringuGrupp,
|
|
||||||
supabase: SupabaseClient,
|
|
||||||
) {
|
|
||||||
const analysisGroupId = await createAnalysisGroup({
|
|
||||||
id: analysisGroup.UuringuGruppId,
|
|
||||||
name: analysisGroup.UuringuGruppNimi,
|
|
||||||
order: analysisGroup.UuringuGruppJarjekord,
|
|
||||||
});
|
|
||||||
|
|
||||||
const analysisGroupCodes = toArray(analysisGroup.Kood);
|
|
||||||
const codes: Partial<Tables<{ schema: 'medreport' }, 'codes'>>[] =
|
|
||||||
analysisGroupCodes.map((kood) => ({
|
|
||||||
hk_code: kood.HkKood,
|
|
||||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
|
||||||
coefficient: kood.Koefitsient,
|
|
||||||
price: kood.Hind,
|
|
||||||
analysis_group_id: analysisGroupId,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const analysisGroupItems = toArray(analysisGroup.Uuring);
|
|
||||||
|
|
||||||
for (const item of analysisGroupItems) {
|
|
||||||
const analysisElement = item.UuringuElement;
|
|
||||||
|
|
||||||
const { data: insertedAnalysisElement, error } = await supabase
|
|
||||||
.schema('medreport')
|
|
||||||
.from('analysis_elements')
|
|
||||||
.upsert(
|
|
||||||
{
|
|
||||||
analysis_id_oid: analysisElement.UuringIdOID,
|
|
||||||
analysis_id_original: analysisElement.UuringId,
|
|
||||||
tehik_short_loinc: analysisElement.TLyhend,
|
|
||||||
tehik_loinc_name: analysisElement.KNimetus,
|
|
||||||
analysis_name_lab: analysisElement.UuringNimi,
|
|
||||||
order: analysisElement.Jarjekord,
|
|
||||||
parent_analysis_group_id: analysisGroupId,
|
|
||||||
material_groups: toArray(item.MaterjalideGrupp),
|
|
||||||
},
|
|
||||||
{ onConflict: 'analysis_id_original', ignoreDuplicates: false },
|
|
||||||
)
|
|
||||||
.select('id');
|
|
||||||
|
|
||||||
if (error || !insertedAnalysisElement[0]?.id) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to insert analysis element (id: ${analysisElement.UuringId}), error: ${error?.message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertedAnalysisElementId = insertedAnalysisElement[0].id;
|
|
||||||
|
|
||||||
if (analysisElement.Kood) {
|
|
||||||
const analysisElementCodes = toArray(analysisElement.Kood);
|
|
||||||
codes.push(
|
|
||||||
...analysisElementCodes.map((kood) => ({
|
|
||||||
hk_code: kood.HkKood,
|
|
||||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
|
||||||
coefficient: kood.Koefitsient,
|
|
||||||
price: kood.Hind,
|
|
||||||
analysis_element_id: insertedAnalysisElementId,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const analyses = analysisElement.UuringuElement;
|
|
||||||
if (analyses?.length) {
|
|
||||||
for (const analysis of analyses) {
|
|
||||||
const { data: insertedAnalysis, error } = await supabase
|
|
||||||
.schema('medreport')
|
|
||||||
.from('analyses')
|
|
||||||
.upsert(
|
|
||||||
{
|
|
||||||
analysis_id_oid: analysis.UuringIdOID,
|
|
||||||
analysis_id_original: analysis.UuringId,
|
|
||||||
tehik_short_loinc: analysis.TLyhend,
|
|
||||||
tehik_loinc_name: analysis.KNimetus,
|
|
||||||
analysis_name_lab: analysis.UuringNimi,
|
|
||||||
order: analysis.Jarjekord,
|
|
||||||
parent_analysis_element_id: insertedAnalysisElementId,
|
|
||||||
},
|
|
||||||
{ onConflict: 'analysis_id_original', ignoreDuplicates: false },
|
|
||||||
)
|
|
||||||
.select('id');
|
|
||||||
|
|
||||||
if (error || !insertedAnalysis[0]?.id) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to insert analysis (id: ${analysis.UuringId}) error: ${error?.message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertedAnalysisId = insertedAnalysis[0].id;
|
|
||||||
if (analysisElement.Kood) {
|
|
||||||
const analysisCodes = toArray(analysis.Kood);
|
|
||||||
|
|
||||||
codes.push(
|
|
||||||
...analysisCodes.map((kood) => ({
|
|
||||||
hk_code: kood.HkKood,
|
|
||||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
|
||||||
coefficient: kood.Koefitsient,
|
|
||||||
price: kood.Hind,
|
|
||||||
analysis_id: insertedAnalysisId,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { error: codesError } = await supabase
|
|
||||||
.schema('medreport')
|
|
||||||
.from('codes')
|
|
||||||
.upsert(codes, { ignoreDuplicates: false });
|
|
||||||
|
|
||||||
if (codesError?.code) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to insert codes (analysis group id: ${analysisGroup.UuringuGruppId})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function syncPublicMessage(
|
|
||||||
message?: MedipostPublicMessageResponse | null,
|
|
||||||
) {
|
|
||||||
const supabase = createCustomClient(
|
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
||||||
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
|
||||||
{
|
|
||||||
auth: {
|
|
||||||
persistSession: false,
|
|
||||||
autoRefreshToken: false,
|
|
||||||
detectSessionInUrl: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const providers = toArray(message?.Saadetis?.Teenused.Teostaja);
|
|
||||||
const analysisGroups = providers.flatMap((provider) =>
|
|
||||||
toArray(provider.UuringuGrupp),
|
|
||||||
);
|
|
||||||
if (!message || !analysisGroups.length) {
|
|
||||||
return supabase.schema('audit').from('sync_entries').insert({
|
|
||||||
operation: 'ANALYSES_SYNC',
|
|
||||||
comment: 'No data received',
|
|
||||||
status: SyncStatus.Fail,
|
|
||||||
changed_by_role: 'service_role',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const analysisGroup of analysisGroups) {
|
|
||||||
await saveAnalysisGroup(analysisGroup, supabase);
|
|
||||||
}
|
|
||||||
|
|
||||||
await supabase.schema('audit').from('sync_entries').insert({
|
|
||||||
operation: 'ANALYSES_SYNC',
|
|
||||||
status: SyncStatus.Success,
|
|
||||||
changed_by_role: 'service_role',
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
await supabase
|
|
||||||
.schema('audit')
|
|
||||||
.from('sync_entries')
|
|
||||||
.insert({
|
|
||||||
operation: 'ANALYSES_SYNC',
|
|
||||||
status: SyncStatus.Fail,
|
|
||||||
comment: JSON.stringify(e),
|
|
||||||
changed_by_role: 'service_role',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLatestMessage({
|
|
||||||
messages,
|
|
||||||
excludedMessageIds,
|
|
||||||
}: {
|
|
||||||
messages?: Message[];
|
|
||||||
excludedMessageIds?: string[];
|
|
||||||
}) {
|
|
||||||
if (!messages?.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filtered = messages.filter(({ messageId }) => !excludedMessageIds?.includes(messageId));
|
|
||||||
|
|
||||||
if (!filtered.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered.reduce((prev, current) =>
|
|
||||||
Number(prev.messageId) > Number(current.messageId) ? prev : current,
|
|
||||||
{ messageId: '' } as Message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncPrivateMessage({
|
|
||||||
messageResponse,
|
|
||||||
order,
|
|
||||||
}: {
|
|
||||||
messageResponse: NonNullable<MedipostOrderResponse['Saadetis']['Vastus']>;
|
|
||||||
order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
|
||||||
}) {
|
|
||||||
const supabase = getSupabaseServerAdminClient()
|
|
||||||
|
|
||||||
const { data: analysisOrder, error: analysisOrderError } = await supabase
|
|
||||||
.schema('medreport')
|
|
||||||
.from('analysis_orders')
|
|
||||||
.select('user_id')
|
|
||||||
.eq('id', order.id);
|
|
||||||
|
|
||||||
if (analysisOrderError || !analysisOrder?.[0]?.user_id) {
|
|
||||||
throw new Error(
|
|
||||||
`Could not find analysis order with id ${messageResponse.ValisTellimuseId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: analysisResponse, error } = await supabase
|
|
||||||
.schema('medreport')
|
|
||||||
.from('analysis_responses')
|
|
||||||
.upsert(
|
|
||||||
{
|
|
||||||
analysis_order_id: order.id,
|
|
||||||
order_number: messageResponse.TellimuseNumber,
|
|
||||||
order_status: AnalysisOrderStatus[messageResponse.TellimuseOlek],
|
|
||||||
user_id: analysisOrder[0].user_id,
|
|
||||||
},
|
|
||||||
{ onConflict: 'order_number', ignoreDuplicates: false },
|
|
||||||
)
|
|
||||||
.select('id');
|
|
||||||
|
|
||||||
if (error || !analysisResponse?.[0]?.id) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to insert or update analysis order response (external id: ${messageResponse?.TellimuseNumber})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const analysisGroups = toArray(messageResponse.UuringuGrupp);
|
|
||||||
console.info(`Order has results for ${analysisGroups.length} analysis groups`);
|
|
||||||
|
|
||||||
const responses: Omit<
|
|
||||||
Tables<{ schema: 'medreport' }, 'analysis_response_elements'>,
|
|
||||||
'id' | 'created_at' | 'updated_at'
|
|
||||||
>[] = [];
|
|
||||||
|
|
||||||
const analysisResponseId = analysisResponse[0]!.id;
|
|
||||||
|
|
||||||
for (const analysisGroup of analysisGroups) {
|
|
||||||
const groupItems = toArray(
|
|
||||||
analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'],
|
|
||||||
);
|
|
||||||
console.info(`Order has results in group ${analysisGroup.UuringuGruppNimi} for ${groupItems.length} analysis elements`);
|
|
||||||
for (const item of groupItems) {
|
|
||||||
const element = item.UuringuElement;
|
|
||||||
const elementAnalysisResponses = toArray(element.UuringuVastus);
|
|
||||||
|
|
||||||
responses.push(
|
|
||||||
...elementAnalysisResponses.map((response) => ({
|
|
||||||
analysis_element_original_id: element.UuringId,
|
|
||||||
analysis_response_id: analysisResponseId,
|
|
||||||
norm_lower: response.NormAlum?.['#text'] ?? null,
|
|
||||||
norm_lower_included:
|
|
||||||
response.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah',
|
|
||||||
norm_status: response.NormiStaatus,
|
|
||||||
norm_upper: response.NormYlem?.['#text'] ?? null,
|
|
||||||
norm_upper_included:
|
|
||||||
response.NormYlem?.['@_kaasaarvatud'].toLowerCase() === 'jah',
|
|
||||||
response_time: response.VastuseAeg ?? null,
|
|
||||||
response_value: response.VastuseVaartus,
|
|
||||||
unit: element.Mootyhik ?? null,
|
|
||||||
original_response_element: element,
|
|
||||||
analysis_name: element.UuringNimi || element.KNimetus,
|
|
||||||
comment: element.UuringuKommentaar ?? '',
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { error: deleteError } = await supabase
|
|
||||||
.schema('medreport')
|
|
||||||
.from('analysis_response_elements')
|
|
||||||
.delete()
|
|
||||||
.eq('analysis_response_id', analysisResponseId);
|
|
||||||
|
|
||||||
if (deleteError) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to clean up response elements for response id ${analysisResponseId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { error: elementInsertError } = await supabase
|
|
||||||
.schema('medreport')
|
|
||||||
.from('analysis_response_elements')
|
|
||||||
.insert(responses);
|
|
||||||
|
|
||||||
if (elementInsertError) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to insert order response elements for response id ${analysisResponseId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: allOrderResponseElements } = await supabase
|
|
||||||
.schema('medreport')
|
|
||||||
.from('analysis_response_elements')
|
|
||||||
.select('*')
|
|
||||||
.eq('analysis_response_id', analysisResponseId)
|
|
||||||
.throwOnError();
|
|
||||||
const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0;
|
|
||||||
if (allOrderResponseElements.length !== expectedOrderResponseElements) {
|
|
||||||
return { isPartial: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusFromResponse = AnalysisOrderStatus[messageResponse.TellimuseOlek];
|
|
||||||
return { isCompleted: statusFromResponse === 'COMPLETED' };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sendOrderToMedipost({
|
|
||||||
medusaOrderId,
|
|
||||||
orderedAnalysisElements,
|
|
||||||
}: {
|
|
||||||
medusaOrderId: string;
|
|
||||||
orderedAnalysisElements: OrderedAnalysisElement[];
|
|
||||||
}) {
|
|
||||||
const medreportOrder = await getAnalysisOrder({ medusaOrderId });
|
|
||||||
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
|
|
||||||
|
|
||||||
const orderedAnalysesIds = orderedAnalysisElements
|
|
||||||
.map(({ analysisId }) => analysisId)
|
|
||||||
.filter(Boolean) as number[];
|
|
||||||
const orderedAnalysisElementsIds = orderedAnalysisElements
|
|
||||||
.map(({ analysisElementId }) => analysisElementId)
|
|
||||||
.filter(Boolean) as number[];
|
|
||||||
|
|
||||||
const analyses = await getAnalyses({ ids: orderedAnalysesIds });
|
|
||||||
if (analyses.length !== orderedAnalysesIds.length) {
|
|
||||||
throw new Error(`Got ${analyses.length} analyses, expected ${orderedAnalysesIds.length}`);
|
|
||||||
}
|
|
||||||
const analysisElements = await getAnalysisElementsAdmin({ ids: orderedAnalysisElementsIds });
|
|
||||||
if (analysisElements.length !== orderedAnalysisElementsIds.length) {
|
|
||||||
throw new Error(`Got ${analysisElements.length} analysis elements, expected ${orderedAnalysisElementsIds.length}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderXml = await composeOrderXML({
|
|
||||||
analyses,
|
|
||||||
analysisElements,
|
|
||||||
person: {
|
|
||||||
idCode: account.personal_code!,
|
|
||||||
firstName: account.name ?? '',
|
|
||||||
lastName: account.last_name ?? '',
|
|
||||||
phone: account.phone ?? '',
|
|
||||||
},
|
|
||||||
orderId: medreportOrder.id,
|
|
||||||
orderCreatedAt: new Date(medreportOrder.created_at),
|
|
||||||
comment: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await sendPrivateMessage(orderXml);
|
|
||||||
} catch (e) {
|
|
||||||
const isMedipostError = e instanceof MedipostValidationError;
|
|
||||||
if (isMedipostError) {
|
|
||||||
await logMedipostDispatch({
|
|
||||||
medusaOrderId,
|
|
||||||
isSuccess: false,
|
|
||||||
isMedipostError,
|
|
||||||
errorMessage: e.response,
|
|
||||||
});
|
|
||||||
await createMedipostActionLog({
|
|
||||||
action: 'send_order_to_medipost',
|
|
||||||
xml: orderXml,
|
|
||||||
hasAnalysisResults: false,
|
|
||||||
medusaOrderId,
|
|
||||||
responseXml: e.response,
|
|
||||||
hasError: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await logMedipostDispatch({
|
|
||||||
medusaOrderId,
|
|
||||||
isSuccess: false,
|
|
||||||
isMedipostError,
|
|
||||||
});
|
|
||||||
await createMedipostActionLog({
|
|
||||||
action: 'send_order_to_medipost',
|
|
||||||
xml: orderXml,
|
|
||||||
hasAnalysisResults: false,
|
|
||||||
medusaOrderId,
|
|
||||||
hasError: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
await logMedipostDispatch({
|
|
||||||
medusaOrderId,
|
|
||||||
isSuccess: true,
|
|
||||||
isMedipostError: false,
|
|
||||||
});
|
|
||||||
await createMedipostActionLog({
|
|
||||||
action: 'send_order_to_medipost',
|
|
||||||
xml: orderXml,
|
|
||||||
hasAnalysisResults: false,
|
|
||||||
medusaOrderId,
|
|
||||||
});
|
|
||||||
await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getOrderedAnalysisIds({
|
|
||||||
medusaOrder,
|
|
||||||
}: {
|
|
||||||
medusaOrder: StoreOrder;
|
|
||||||
}): Promise<{
|
|
||||||
analysisElementId?: number;
|
|
||||||
analysisId?: number;
|
|
||||||
}[]> {
|
|
||||||
const countryCodes = await listRegions();
|
|
||||||
const countryCode = countryCodes[0]!.countries![0]!.iso_2!;
|
|
||||||
|
|
||||||
async function getOrderedAnalysisElements(medusaOrder: StoreOrder) {
|
|
||||||
const originalIds = (medusaOrder?.items ?? [])
|
|
||||||
.map((a) => a.product?.metadata?.analysisIdOriginal)
|
|
||||||
.filter((a) => typeof a === 'string') as string[];
|
|
||||||
const analysisElements = await getAnalysisElements({ originalIds });
|
|
||||||
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getOrderedAnalyses(medusaOrder: StoreOrder) {
|
|
||||||
const originalIds = (medusaOrder?.items ?? [])
|
|
||||||
.map((a) => a.product?.metadata?.analysisIdOriginal)
|
|
||||||
.filter((a) => typeof a === 'string') as string[];
|
|
||||||
const analyses = await getAnalyses({ originalIds });
|
|
||||||
return analyses.map(({ id }) => ({ analysisId: id }));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getOrderedAnalysisPackages(medusaOrder: StoreOrder) {
|
|
||||||
const orderedPackages = (medusaOrder?.items ?? []).filter(({ product }) => product?.handle.startsWith(ANALYSIS_PACKAGE_HANDLE_PREFIX));
|
|
||||||
const orderedPackageIds = orderedPackages.map(({ product }) => product?.id).filter(Boolean) as string[];
|
|
||||||
if (orderedPackageIds.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
console.info(`Order has ${orderedPackageIds.length} packages`);
|
|
||||||
const { response: { products: orderedPackagesProducts } } = await listProducts({
|
|
||||||
countryCode,
|
|
||||||
queryParams: { limit: 100, id: orderedPackageIds },
|
|
||||||
});
|
|
||||||
console.info(`Order has ${orderedPackagesProducts.length} packages = ${JSON.stringify(orderedPackageIds, null, 2)}`);
|
|
||||||
if (orderedPackagesProducts.length !== orderedPackageIds.length) {
|
|
||||||
throw new Error(`Got ${orderedPackagesProducts.length} ordered packages products, expected ${orderedPackageIds.length}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ids = getAnalysisElementMedusaProductIds(
|
|
||||||
orderedPackagesProducts.map(({ id, metadata }) => ({
|
|
||||||
metadata,
|
|
||||||
variant: orderedPackages.find(({ product }) => product?.id === id)?.variant,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
if (ids.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const { response: { products: analysisPackagesProducts } } = await listProducts({
|
|
||||||
countryCode,
|
|
||||||
queryParams: { limit: 100, id: ids },
|
|
||||||
});
|
|
||||||
if (analysisPackagesProducts.length !== ids.length) {
|
|
||||||
throw new Error(`Got ${analysisPackagesProducts.length} analysis packages products, expected ${ids.length}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalIds = analysisPackagesProducts
|
|
||||||
.map(({ metadata }) => metadata?.analysisIdOriginal)
|
|
||||||
.filter((id) => typeof id === 'string');
|
|
||||||
if (originalIds.length !== ids.length) {
|
|
||||||
throw new Error(`Got ${originalIds.length} analysis packages products with analysisIdOriginal, expected ${ids.length}`);
|
|
||||||
}
|
|
||||||
const analysisElements = await getAnalysisElements({ originalIds });
|
|
||||||
|
|
||||||
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const [analysisPackageElements, orderedAnalysisElements, orderedAnalyses] = await Promise.all([
|
|
||||||
getOrderedAnalysisPackages(medusaOrder),
|
|
||||||
getOrderedAnalysisElements(medusaOrder),
|
|
||||||
getOrderedAnalyses(medusaOrder),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [...analysisPackageElements, ...orderedAnalysisElements, ...orderedAnalyses];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createMedipostActionLog({
|
|
||||||
action,
|
|
||||||
xml,
|
|
||||||
hasAnalysisResults = false,
|
|
||||||
medusaOrderId,
|
|
||||||
responseXml,
|
|
||||||
hasError = false,
|
|
||||||
}: {
|
|
||||||
action:
|
|
||||||
| 'send_order_to_medipost'
|
|
||||||
| 'sync_analysis_results_from_medipost'
|
|
||||||
| 'send_fake_analysis_results_to_medipost'
|
|
||||||
| 'send_analysis_results_to_medipost';
|
|
||||||
xml: string;
|
|
||||||
hasAnalysisResults?: boolean;
|
|
||||||
medusaOrderId?: string | null;
|
|
||||||
responseXml?: string | null;
|
|
||||||
hasError?: boolean;
|
|
||||||
}) {
|
|
||||||
await getSupabaseServerAdminClient()
|
|
||||||
.schema('medreport')
|
|
||||||
.from('medipost_actions')
|
|
||||||
.insert({
|
|
||||||
action,
|
|
||||||
xml,
|
|
||||||
has_analysis_results: hasAnalysisResults,
|
|
||||||
medusa_order_id: medusaOrderId,
|
|
||||||
response_xml: responseXml,
|
|
||||||
has_error: hasError,
|
|
||||||
})
|
|
||||||
.select('id')
|
|
||||||
.throwOnError();
|
|
||||||
}
|
|
||||||
@@ -68,7 +68,7 @@ export interface IMedipostPublicMessageDataParsed {
|
|||||||
Koefitsient: number;
|
Koefitsient: number;
|
||||||
Hind: number;
|
Hind: number;
|
||||||
}[];
|
}[];
|
||||||
UuringuElement: IUuringElement;
|
UuringuElement?: IUuringElement[];
|
||||||
}[];
|
}[];
|
||||||
MaterjalideGrupp: IMaterialGroup[];
|
MaterjalideGrupp: IMaterialGroup[];
|
||||||
Kood: {
|
Kood: {
|
||||||
68
lib/services/medipost/medipostMessageBase.service.ts
Normal file
68
lib/services/medipost/medipostMessageBase.service.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import type { Message } from '@/lib/types/medipost';
|
||||||
|
|
||||||
|
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
||||||
|
|
||||||
|
export async function getLatestMessage({
|
||||||
|
messages,
|
||||||
|
excludedMessageIds,
|
||||||
|
}: {
|
||||||
|
messages?: Message[];
|
||||||
|
excludedMessageIds?: string[];
|
||||||
|
}) {
|
||||||
|
if (!messages?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = messages.filter(({ messageId }) => !excludedMessageIds?.includes(messageId));
|
||||||
|
|
||||||
|
if (!filtered.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered.reduce((prev, current) =>
|
||||||
|
Number(prev.messageId) > Number(current.messageId) ? prev : current,
|
||||||
|
{ messageId: '' } as Message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMedipostActionLog({
|
||||||
|
action,
|
||||||
|
xml,
|
||||||
|
hasAnalysisResults = false,
|
||||||
|
medusaOrderId,
|
||||||
|
responseXml,
|
||||||
|
hasError = false,
|
||||||
|
medipostExternalOrderId,
|
||||||
|
medipostPrivateMessageId,
|
||||||
|
}: {
|
||||||
|
action:
|
||||||
|
| 'send_order_to_medipost'
|
||||||
|
| 'sync_analysis_results_from_medipost'
|
||||||
|
| 'send_fake_analysis_results_to_medipost'
|
||||||
|
| 'send_analysis_results_to_medipost';
|
||||||
|
xml: string;
|
||||||
|
hasAnalysisResults?: boolean;
|
||||||
|
medusaOrderId?: string | null;
|
||||||
|
responseXml?: string | null;
|
||||||
|
hasError?: boolean;
|
||||||
|
medipostExternalOrderId?: string | null;
|
||||||
|
medipostPrivateMessageId?: string | null;
|
||||||
|
}) {
|
||||||
|
await getSupabaseServerAdminClient()
|
||||||
|
.schema('medreport')
|
||||||
|
.from('medipost_actions')
|
||||||
|
.insert({
|
||||||
|
action,
|
||||||
|
xml,
|
||||||
|
has_analysis_results: hasAnalysisResults,
|
||||||
|
medusa_order_id: medusaOrderId,
|
||||||
|
response_xml: responseXml,
|
||||||
|
has_error: hasError,
|
||||||
|
medipost_external_order_id: medipostExternalOrderId,
|
||||||
|
medipost_private_message_id: medipostPrivateMessageId,
|
||||||
|
})
|
||||||
|
.select('id')
|
||||||
|
.throwOnError();
|
||||||
|
}
|
||||||
112
lib/services/medipost/medipostPrivateMessage.service.test.ts
Normal file
112
lib/services/medipost/medipostPrivateMessage.service.test.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { AnalysisResponseElement } from "~/lib/types/analysis-response-element";
|
||||||
|
import { canCreateAnalysisResponseElement, getAnalysisResponseElementsForGroup } from "./medipostPrivateMessage.service";
|
||||||
|
import { ResponseUuring } from "@/packages/shared/src/types/medipost-analysis";
|
||||||
|
|
||||||
|
type TestExistingElement = Pick<AnalysisResponseElement, 'analysis_element_original_id' | 'status' | 'response_value'>;
|
||||||
|
|
||||||
|
describe('medipostPrivateMessage.service', () => {
|
||||||
|
describe('canCreateAnalysisResponseElement', () => {
|
||||||
|
it('should return true if the analysis response element does not exist', async () => {
|
||||||
|
const existingElements = [] as TestExistingElement[];
|
||||||
|
const groupUuring = {
|
||||||
|
UuringuElement: {
|
||||||
|
UuringOlek: 1,
|
||||||
|
UuringId: '1',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
const responseValue = 1;
|
||||||
|
const log = jest.fn();
|
||||||
|
expect(await canCreateAnalysisResponseElement({ existingElements, groupUuring, responseValue, log })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if the analysis response element exists and the status is higher', async () => {
|
||||||
|
const existingElements = [{ analysis_element_original_id: '1', status: '2', response_value: 1 }] as TestExistingElement[];
|
||||||
|
const groupUuring = {
|
||||||
|
UuringuElement: {
|
||||||
|
UuringOlek: 1,
|
||||||
|
UuringId: '1',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
const responseValue = 1;
|
||||||
|
const log = jest.fn();
|
||||||
|
expect(await canCreateAnalysisResponseElement({ existingElements, groupUuring, responseValue, log })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAnalysisResponseElementsForGroup', () => {
|
||||||
|
it('should return single new element', async () => {
|
||||||
|
const analysisGroup = {
|
||||||
|
UuringuGruppNimi: '1',
|
||||||
|
Uuring: [
|
||||||
|
{
|
||||||
|
UuringuElement: {
|
||||||
|
UuringOlek: 1,
|
||||||
|
UuringId: '1',
|
||||||
|
UuringuVastus: [{ VastuseVaartus: '1' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as unknown as ResponseUuring[],
|
||||||
|
} as const;
|
||||||
|
const existingElements = [] as TestExistingElement[];
|
||||||
|
const log = jest.fn();
|
||||||
|
expect(await getAnalysisResponseElementsForGroup({ analysisGroup, existingElements, log }))
|
||||||
|
.toEqual([{
|
||||||
|
analysis_element_original_id: '1',
|
||||||
|
analysis_name: undefined,
|
||||||
|
comment: null,
|
||||||
|
norm_lower: null,
|
||||||
|
norm_lower_included: false,
|
||||||
|
norm_status: undefined,
|
||||||
|
norm_upper: null,
|
||||||
|
norm_upper_included: false,
|
||||||
|
response_time: null,
|
||||||
|
response_value: 1,
|
||||||
|
unit: null,
|
||||||
|
original_response_element: {
|
||||||
|
UuringOlek: 1,
|
||||||
|
UuringId: '1',
|
||||||
|
UuringuVastus: [{ VastuseVaartus: '1' }],
|
||||||
|
},
|
||||||
|
status: '1',
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return no new element if element already exists in higher status', async () => {
|
||||||
|
const analysisGroup = {
|
||||||
|
UuringuGruppNimi: '1',
|
||||||
|
Uuring: [
|
||||||
|
{
|
||||||
|
UuringuElement: {
|
||||||
|
UuringOlek: 1,
|
||||||
|
UuringId: '1',
|
||||||
|
UuringuVastus: [{ VastuseVaartus: '1' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as unknown as ResponseUuring[],
|
||||||
|
} as const;
|
||||||
|
const existingElements = [{ analysis_element_original_id: '1', status: '2', response_value: 1 }] as TestExistingElement[];
|
||||||
|
const log = jest.fn();
|
||||||
|
expect(await getAnalysisResponseElementsForGroup({ analysisGroup, existingElements, log }))
|
||||||
|
.toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return no new element if element already exists with response value', async () => {
|
||||||
|
const analysisGroup = {
|
||||||
|
UuringuGruppNimi: '1',
|
||||||
|
Uuring: [
|
||||||
|
{
|
||||||
|
UuringuElement: {
|
||||||
|
UuringOlek: 1,
|
||||||
|
UuringId: '1',
|
||||||
|
UuringuVastus: [{ VastuseVaartus: '' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as unknown as ResponseUuring[],
|
||||||
|
} as const;
|
||||||
|
const existingElements = [{ analysis_element_original_id: '1', status: '1', response_value: 1 }] as TestExistingElement[];
|
||||||
|
const log = jest.fn();
|
||||||
|
expect(await getAnalysisResponseElementsForGroup({ analysisGroup, existingElements, log }))
|
||||||
|
.toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
515
lib/services/medipost/medipostPrivateMessage.service.ts
Normal file
515
lib/services/medipost/medipostPrivateMessage.service.ts
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import type { PostgrestError } from '@supabase/supabase-js';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import {
|
||||||
|
GetMessageListResponse,
|
||||||
|
MedipostAction,
|
||||||
|
} from '@/lib/types/medipost';
|
||||||
|
import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis';
|
||||||
|
import type {
|
||||||
|
ResponseUuringuGrupp,
|
||||||
|
MedipostOrderResponse,
|
||||||
|
UuringElement,
|
||||||
|
} from '@/packages/shared/src/types/medipost-analysis';
|
||||||
|
import { toArray } from '@/lib/utils';
|
||||||
|
import type { AnalysisOrder } from '~/lib/types/analysis-order';
|
||||||
|
import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element';
|
||||||
|
|
||||||
|
import { Tables } from '@kit/supabase/database';
|
||||||
|
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
||||||
|
import { getAnalysisElementsAdmin } from '../analysis-element.service';
|
||||||
|
import { getAnalyses } from '../analyses.service';
|
||||||
|
import { createMedipostActionLog, getLatestMessage } from './medipostMessageBase.service';
|
||||||
|
import { validateMedipostResponse } from './medipostValidate.service';
|
||||||
|
import { getAnalysisOrder, updateAnalysisOrderStatus } from '../order.service';
|
||||||
|
import { parseXML } from '../util/xml.service';
|
||||||
|
import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service';
|
||||||
|
import { getAccountAdmin } from '../account.service';
|
||||||
|
import { logMedipostDispatch } from '../audit.service';
|
||||||
|
import { MedipostValidationError } from './MedipostValidationError';
|
||||||
|
import { createAnalysisResponseElement, getExistingAnalysisResponseElements, upsertAnalysisResponse } from '../analysis-order.service';
|
||||||
|
|
||||||
|
const BASE_URL = process.env.MEDIPOST_URL!;
|
||||||
|
const USER = process.env.MEDIPOST_USER!;
|
||||||
|
const PASSWORD = process.env.MEDIPOST_PASSWORD!;
|
||||||
|
const RECIPIENT = process.env.MEDIPOST_RECIPIENT!;
|
||||||
|
|
||||||
|
const IS_ENABLED_DELETE_PRIVATE_MESSAGE = false as boolean;
|
||||||
|
|
||||||
|
export async function getLatestPrivateMessageListItem({
|
||||||
|
excludedMessageIds,
|
||||||
|
}: {
|
||||||
|
excludedMessageIds: string[];
|
||||||
|
}) {
|
||||||
|
const { data } = await axios.get<GetMessageListResponse>(BASE_URL, {
|
||||||
|
params: {
|
||||||
|
Action: MedipostAction.GetPrivateMessageList,
|
||||||
|
User: USER,
|
||||||
|
Password: PASSWORD,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.code && data.code !== 0) {
|
||||||
|
throw new Error('Failed to get private message list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getLatestMessage({ messages: data?.messages, excludedMessageIds });
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = (analysisOrder: AnalysisOrder, externalId: string, analysisResponseId: string) => (message: string, error?: PostgrestError | null) => {
|
||||||
|
const messageFormatted = `[${analysisOrder.id}] [${externalId}] [${analysisResponseId}] ${message}`;
|
||||||
|
if (error) {
|
||||||
|
console.info(messageFormatted, error);
|
||||||
|
} else {
|
||||||
|
console.info(messageFormatted);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function canCreateAnalysisResponseElement({
|
||||||
|
existingElements,
|
||||||
|
groupUuring: {
|
||||||
|
UuringuElement: {
|
||||||
|
UuringOlek: status,
|
||||||
|
UuringId: analysisElementOriginalId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responseValue,
|
||||||
|
log,
|
||||||
|
}: {
|
||||||
|
existingElements: Pick<AnalysisResponseElement, 'analysis_element_original_id' | 'status' | 'response_value'>[];
|
||||||
|
groupUuring: { UuringuElement: Pick<UuringElement, 'UuringOlek' | 'UuringId'> };
|
||||||
|
responseValue: number | null;
|
||||||
|
log: ReturnType<typeof logger>;
|
||||||
|
}) {
|
||||||
|
const existingAnalysisResponseElement = existingElements.find(({ analysis_element_original_id }) => analysis_element_original_id === analysisElementOriginalId);
|
||||||
|
if (!existingAnalysisResponseElement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number(existingAnalysisResponseElement.status) > status) {
|
||||||
|
log(`Analysis response element id=${analysisElementOriginalId} already exists for order in higher status ${existingAnalysisResponseElement.status} than ${status}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingAnalysisResponseElement.response_value && !responseValue) {
|
||||||
|
log(`Analysis response element id=${analysisElementOriginalId} already exists for order with response value ${existingAnalysisResponseElement.response_value} but new response has no value`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function getAnalysisResponseElementsForGroup({
|
||||||
|
analysisGroup,
|
||||||
|
existingElements,
|
||||||
|
log,
|
||||||
|
}: {
|
||||||
|
analysisGroup: Pick<ResponseUuringuGrupp, 'UuringuGruppNimi' | 'Uuring'>;
|
||||||
|
existingElements: Pick<AnalysisResponseElement, 'analysis_element_original_id' | 'status' | 'response_value'>[];
|
||||||
|
log: ReturnType<typeof logger>;
|
||||||
|
}) {
|
||||||
|
const groupUuringItems = toArray(analysisGroup.Uuring as ResponseUuringuGrupp['Uuring']);
|
||||||
|
log(`Order has results in group '${analysisGroup.UuringuGruppNimi}' for ${groupUuringItems.length} analysis elements`);
|
||||||
|
|
||||||
|
const results: Omit<AnalysisResponseElement, 'created_at' | 'updated_at' | 'id' | 'analysis_response_id'>[] = [];
|
||||||
|
|
||||||
|
for (const groupUuring of groupUuringItems) {
|
||||||
|
const groupUuringElement = groupUuring.UuringuElement;
|
||||||
|
const elementAnalysisResponses = toArray(groupUuringElement.UuringuVastus);
|
||||||
|
|
||||||
|
const status = groupUuringElement.UuringOlek;
|
||||||
|
log(`Group uuring '${analysisGroup.UuringuGruppNimi}' has status ${status}`);
|
||||||
|
|
||||||
|
for (const response of elementAnalysisResponses) {
|
||||||
|
const analysisElementOriginalId = groupUuringElement.UuringId;
|
||||||
|
const vastuseVaartus = response.VastuseVaartus;
|
||||||
|
const responseValue = (() => {
|
||||||
|
const valueAsNumber = Number(vastuseVaartus);
|
||||||
|
if (isNaN(valueAsNumber)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return valueAsNumber;
|
||||||
|
})();
|
||||||
|
|
||||||
|
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,
|
||||||
|
norm_lower: response.NormAlum?.['#text'] ?? null,
|
||||||
|
norm_lower_included:
|
||||||
|
response.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah',
|
||||||
|
norm_status: response.NormiStaatus,
|
||||||
|
norm_upper: response.NormYlem?.['#text'] ?? null,
|
||||||
|
norm_upper_included:
|
||||||
|
response.NormYlem?.['@_kaasaarvatud'].toLowerCase() === 'jah',
|
||||||
|
response_time: response.VastuseAeg ?? null,
|
||||||
|
response_value: responseValue,
|
||||||
|
unit: groupUuringElement.Mootyhik ?? null,
|
||||||
|
original_response_element: groupUuringElement,
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getNewAnalysisResponseElements({
|
||||||
|
analysisGroups,
|
||||||
|
existingElements,
|
||||||
|
log,
|
||||||
|
}: {
|
||||||
|
analysisGroups: ResponseUuringuGrupp[];
|
||||||
|
existingElements: AnalysisResponseElement[];
|
||||||
|
log: ReturnType<typeof logger>;
|
||||||
|
}) {
|
||||||
|
const newElements: Omit<AnalysisResponseElement, 'created_at' | 'updated_at' | 'id' | 'analysis_response_id'>[] = [];
|
||||||
|
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,
|
||||||
|
}: {
|
||||||
|
analysisResponseId: number;
|
||||||
|
order: Pick<AnalysisOrder, 'analysis_element_ids'>;
|
||||||
|
}) {
|
||||||
|
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<NonNullable<MedipostOrderResponse['Saadetis']['Vastus']>, 'ValisTellimuseId' | 'TellimuseNumber' | 'TellimuseOlek' | 'UuringuGrupp'>;
|
||||||
|
order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||||
|
}) {
|
||||||
|
const supabase = getSupabaseServerAdminClient();
|
||||||
|
|
||||||
|
const orderStatus = AnalysisOrderStatus[TellimuseOlek];
|
||||||
|
|
||||||
|
const log = logger(order, externalId, orderNumber);
|
||||||
|
|
||||||
|
const { data: analysisOrder } = await supabase
|
||||||
|
.schema('medreport')
|
||||||
|
.from('analysis_orders')
|
||||||
|
.select('id, user_id')
|
||||||
|
.eq('id', order.id)
|
||||||
|
.single()
|
||||||
|
.throwOnError();
|
||||||
|
|
||||||
|
const { analysisResponseId } = await upsertAnalysisResponse({
|
||||||
|
analysisOrderId: order.id,
|
||||||
|
orderNumber,
|
||||||
|
orderStatus,
|
||||||
|
userId: analysisOrder.user_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await hasAllAnalysisResponseElements({ analysisResponseId, order })
|
||||||
|
? { isCompleted: orderStatus === 'COMPLETED' }
|
||||||
|
: { isPartial: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readPrivateMessageResponse({
|
||||||
|
excludedMessageIds,
|
||||||
|
}: {
|
||||||
|
excludedMessageIds: string[];
|
||||||
|
}): Promise<{
|
||||||
|
messageId: string | null;
|
||||||
|
hasAnalysisResponse: boolean;
|
||||||
|
hasPartialAnalysisResponse: boolean;
|
||||||
|
hasFullAnalysisResponse: boolean;
|
||||||
|
medusaOrderId: string | undefined;
|
||||||
|
analysisOrderId: number | undefined;
|
||||||
|
}> {
|
||||||
|
let messageId: string | null = null;
|
||||||
|
let hasAnalysisResponse = false;
|
||||||
|
let hasPartialAnalysisResponse = false;
|
||||||
|
let hasFullAnalysisResponse = false;
|
||||||
|
let medusaOrderId: string | undefined = undefined;
|
||||||
|
let analysisOrderId: number | undefined = undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const privateMessage = await getLatestPrivateMessageListItem({ excludedMessageIds });
|
||||||
|
messageId = privateMessage?.messageId ?? null;
|
||||||
|
|
||||||
|
if (!privateMessage || !messageId) {
|
||||||
|
return {
|
||||||
|
messageId: null,
|
||||||
|
hasAnalysisResponse: false,
|
||||||
|
hasPartialAnalysisResponse: false,
|
||||||
|
hasFullAnalysisResponse: false,
|
||||||
|
medusaOrderId: undefined,
|
||||||
|
analysisOrderId: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { messageId: privateMessageId } = privateMessage;
|
||||||
|
const { message: privateMessageContent, xml: privateMessageXml } = await getPrivateMessage(
|
||||||
|
privateMessageId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const messageResponse = privateMessageContent?.Saadetis?.Vastus;
|
||||||
|
const medipostExternalOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId;
|
||||||
|
const patientPersonalCode = messageResponse?.Patsient.Isikukood?.toString();
|
||||||
|
analysisOrderId = Number(medipostExternalOrderId);
|
||||||
|
|
||||||
|
const hasInvalidOrderId = isNaN(analysisOrderId);
|
||||||
|
|
||||||
|
if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) {
|
||||||
|
await createMedipostActionLog({
|
||||||
|
action: 'sync_analysis_results_from_medipost',
|
||||||
|
xml: privateMessageXml,
|
||||||
|
hasAnalysisResults: false,
|
||||||
|
medipostPrivateMessageId: privateMessageId,
|
||||||
|
medusaOrderId,
|
||||||
|
medipostExternalOrderId,
|
||||||
|
hasError: true,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
messageId,
|
||||||
|
hasAnalysisResponse: false,
|
||||||
|
hasPartialAnalysisResponse: false,
|
||||||
|
hasFullAnalysisResponse: false,
|
||||||
|
medusaOrderId: hasInvalidOrderId ? undefined : medusaOrderId,
|
||||||
|
analysisOrderId: hasInvalidOrderId ? undefined : analysisOrderId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let analysisOrder: AnalysisOrder;
|
||||||
|
try {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderPerson = await getAccountAdmin({ primaryOwnerUserId: analysisOrder.user_id });
|
||||||
|
if (orderPerson.personal_code !== patientPersonalCode) {
|
||||||
|
throw new Error(`Order person personal code does not match Medipost message Patsient.Isikukood=${patientPersonalCode}, orderPerson.personal_code=${orderPerson.personal_code}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = await syncPrivateMessage({ messageResponse, order: analysisOrder });
|
||||||
|
|
||||||
|
await createMedipostActionLog({
|
||||||
|
action: 'sync_analysis_results_from_medipost',
|
||||||
|
xml: privateMessageXml,
|
||||||
|
hasAnalysisResults: true,
|
||||||
|
medipostPrivateMessageId: privateMessageId,
|
||||||
|
medusaOrderId,
|
||||||
|
medipostExternalOrderId,
|
||||||
|
});
|
||||||
|
if (status.isPartial) {
|
||||||
|
await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' });
|
||||||
|
hasAnalysisResponse = true;
|
||||||
|
hasPartialAnalysisResponse = true;
|
||||||
|
} else if (status.isCompleted) {
|
||||||
|
await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' });
|
||||||
|
if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) {
|
||||||
|
await deletePrivateMessage(privateMessageId);
|
||||||
|
}
|
||||||
|
hasAnalysisResponse = true;
|
||||||
|
hasFullAnalysisResponse = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to process private message id=${messageId}, message=${(e as Error).message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { messageId, hasAnalysisResponse, hasPartialAnalysisResponse, hasFullAnalysisResponse, medusaOrderId, analysisOrderId };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePrivateMessage(messageId: string) {
|
||||||
|
const { data } = await axios.get(BASE_URL, {
|
||||||
|
params: {
|
||||||
|
Action: MedipostAction.DeletePrivateMessage,
|
||||||
|
User: USER,
|
||||||
|
Password: PASSWORD,
|
||||||
|
MessageId: messageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.code && data.code !== 0) {
|
||||||
|
throw new Error(`Failed to delete private message (id: ${messageId})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendPrivateMessage(messageXml: string) {
|
||||||
|
const body = new FormData();
|
||||||
|
body.append('Action', MedipostAction.SendPrivateMessage);
|
||||||
|
body.append('User', USER);
|
||||||
|
body.append('Password', PASSWORD);
|
||||||
|
body.append('Receiver', RECIPIENT);
|
||||||
|
body.append('MessageType', 'Tellimus');
|
||||||
|
body.append(
|
||||||
|
'Message',
|
||||||
|
new Blob([messageXml], {
|
||||||
|
type: 'text/xml; charset=UTF-8',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data } = await axios.post(BASE_URL, body);
|
||||||
|
|
||||||
|
await validateMedipostResponse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPrivateMessage(messageId: string) {
|
||||||
|
const { data } = await axios.get(BASE_URL, {
|
||||||
|
params: {
|
||||||
|
Action: MedipostAction.GetPrivateMessage,
|
||||||
|
User: USER,
|
||||||
|
Password: PASSWORD,
|
||||||
|
MessageId: messageId,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/xml',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await validateMedipostResponse(data, { canHaveEmptyCode: true });
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: (await parseXML(data)) as MedipostOrderResponse,
|
||||||
|
xml: data as string,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendOrderToMedipost({
|
||||||
|
medusaOrderId,
|
||||||
|
orderedAnalysisElements,
|
||||||
|
}: {
|
||||||
|
medusaOrderId: string;
|
||||||
|
orderedAnalysisElements: OrderedAnalysisElement[];
|
||||||
|
}) {
|
||||||
|
const medreportOrder = await getAnalysisOrder({ medusaOrderId });
|
||||||
|
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
|
||||||
|
|
||||||
|
const orderedAnalysesIds = orderedAnalysisElements
|
||||||
|
.map(({ analysisId }) => analysisId)
|
||||||
|
.filter(Boolean) as number[];
|
||||||
|
const orderedAnalysisElementsIds = orderedAnalysisElements
|
||||||
|
.map(({ analysisElementId }) => analysisElementId)
|
||||||
|
.filter(Boolean) as number[];
|
||||||
|
|
||||||
|
const analyses = await getAnalyses({ ids: orderedAnalysesIds });
|
||||||
|
if (analyses.length !== orderedAnalysesIds.length) {
|
||||||
|
throw new Error(`Got ${analyses.length} analyses, expected ${orderedAnalysesIds.length}`);
|
||||||
|
}
|
||||||
|
const analysisElements = await getAnalysisElementsAdmin({ ids: orderedAnalysisElementsIds });
|
||||||
|
if (analysisElements.length !== orderedAnalysisElementsIds.length) {
|
||||||
|
throw new Error(`Got ${analysisElements.length} analysis elements, expected ${orderedAnalysisElementsIds.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderXml = await composeOrderXML({
|
||||||
|
analyses,
|
||||||
|
analysisElements,
|
||||||
|
person: {
|
||||||
|
idCode: account.personal_code!,
|
||||||
|
firstName: account.name ?? '',
|
||||||
|
lastName: account.last_name ?? '',
|
||||||
|
phone: account.phone ?? '',
|
||||||
|
},
|
||||||
|
orderId: medreportOrder.id,
|
||||||
|
orderCreatedAt: new Date(medreportOrder.created_at),
|
||||||
|
comment: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendPrivateMessage(orderXml);
|
||||||
|
} catch (e) {
|
||||||
|
const isMedipostError = e instanceof MedipostValidationError;
|
||||||
|
if (isMedipostError) {
|
||||||
|
await logMedipostDispatch({
|
||||||
|
medusaOrderId,
|
||||||
|
isSuccess: false,
|
||||||
|
isMedipostError,
|
||||||
|
errorMessage: e.response,
|
||||||
|
});
|
||||||
|
await createMedipostActionLog({
|
||||||
|
action: 'send_order_to_medipost',
|
||||||
|
xml: orderXml,
|
||||||
|
hasAnalysisResults: false,
|
||||||
|
medusaOrderId,
|
||||||
|
responseXml: e.response,
|
||||||
|
hasError: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await logMedipostDispatch({
|
||||||
|
medusaOrderId,
|
||||||
|
isSuccess: false,
|
||||||
|
isMedipostError,
|
||||||
|
});
|
||||||
|
await createMedipostActionLog({
|
||||||
|
action: 'send_order_to_medipost',
|
||||||
|
xml: orderXml,
|
||||||
|
hasAnalysisResults: false,
|
||||||
|
medusaOrderId,
|
||||||
|
hasError: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
await logMedipostDispatch({
|
||||||
|
medusaOrderId,
|
||||||
|
isSuccess: true,
|
||||||
|
isMedipostError: false,
|
||||||
|
});
|
||||||
|
await createMedipostActionLog({
|
||||||
|
action: 'send_order_to_medipost',
|
||||||
|
xml: orderXml,
|
||||||
|
hasAnalysisResults: false,
|
||||||
|
medusaOrderId,
|
||||||
|
});
|
||||||
|
await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
|
||||||
|
}
|
||||||
33
lib/services/medipost/medipostPublicMessage.service.ts
Normal file
33
lib/services/medipost/medipostPublicMessage.service.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import {
|
||||||
|
GetMessageListResponse,
|
||||||
|
MedipostAction,
|
||||||
|
} from '@/lib/types/medipost';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { getLatestMessage } from './medipostMessageBase.service';
|
||||||
|
|
||||||
|
const BASE_URL = process.env.MEDIPOST_URL!;
|
||||||
|
const USER = process.env.MEDIPOST_USER!;
|
||||||
|
const PASSWORD = process.env.MEDIPOST_PASSWORD!;
|
||||||
|
const RECIPIENT = process.env.MEDIPOST_RECIPIENT!;
|
||||||
|
|
||||||
|
export async function getLatestPublicMessageListItem() {
|
||||||
|
const { data } = await axios.get<GetMessageListResponse>(BASE_URL, {
|
||||||
|
params: {
|
||||||
|
Action: MedipostAction.GetPublicMessageList,
|
||||||
|
User: USER,
|
||||||
|
Password: PASSWORD,
|
||||||
|
Sender: RECIPIENT,
|
||||||
|
// LastChecked (date+time) can be used here to get only messages since the last check - add when cron is created
|
||||||
|
// MessageType check only for messages of certain type
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.code && data.code !== 0) {
|
||||||
|
throw new Error('Failed to get public message list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getLatestMessage({ messages: data?.messages });
|
||||||
|
}
|
||||||
@@ -16,9 +16,9 @@ import { uniqBy } from 'lodash';
|
|||||||
|
|
||||||
import { Tables } from '@kit/supabase/database';
|
import { Tables } from '@kit/supabase/database';
|
||||||
import { formatDate } from 'date-fns';
|
import { formatDate } from 'date-fns';
|
||||||
import { getAnalyses } from './analyses.service';
|
import { getAnalyses } from '../analyses.service';
|
||||||
import { getAnalysisElementsAdmin } from './analysis-element.service';
|
import { getAnalysisElementsAdmin } from '../analysis-element.service';
|
||||||
import { validateMedipostResponse } from './medipost.service';
|
import { validateMedipostResponse } from './medipostValidate.service';
|
||||||
|
|
||||||
const BASE_URL = process.env.MEDIPOST_URL!;
|
const BASE_URL = process.env.MEDIPOST_URL!;
|
||||||
const USER = process.env.MEDIPOST_USER!;
|
const USER = process.env.MEDIPOST_USER!;
|
||||||
25
lib/services/medipost/medipostValidate.service.ts
Normal file
25
lib/services/medipost/medipostValidate.service.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
IMedipostResponseXMLBase,
|
||||||
|
} from '@/packages/shared/src/types/medipost-analysis';
|
||||||
|
|
||||||
|
import { MedipostValidationError } from './MedipostValidationError';
|
||||||
|
import { parseXML } from '../util/xml.service';
|
||||||
|
|
||||||
|
export async function validateMedipostResponse(response: string, { canHaveEmptyCode = false }: { canHaveEmptyCode?: boolean } = {}) {
|
||||||
|
const parsed: IMedipostResponseXMLBase = await parseXML(response);
|
||||||
|
const code = parsed.ANSWER?.CODE;
|
||||||
|
if (canHaveEmptyCode) {
|
||||||
|
if (code && code !== 0) {
|
||||||
|
console.error("Bad response", response);
|
||||||
|
throw new MedipostValidationError(response);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof code !== 'number' || (code !== 0 && !canHaveEmptyCode)) {
|
||||||
|
console.error("Bad response", response);
|
||||||
|
throw new MedipostValidationError(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,8 +18,8 @@ import { toArray } from '@/lib/utils';
|
|||||||
import { uniqBy } from 'lodash';
|
import { uniqBy } from 'lodash';
|
||||||
|
|
||||||
import { Tables } from '@kit/supabase/database';
|
import { Tables } from '@kit/supabase/database';
|
||||||
import { AnalysisElement } from './analysis-element.service';
|
import { AnalysisElement } from '../analysis-element.service';
|
||||||
import { AnalysesWithGroupsAndElements } from './analyses.service';
|
import { AnalysesWithGroupsAndElements } from '../analyses.service';
|
||||||
|
|
||||||
const USER = process.env.MEDIPOST_USER!;
|
const USER = process.env.MEDIPOST_USER!;
|
||||||
const RECIPIENT = process.env.MEDIPOST_RECIPIENT!;
|
const RECIPIENT = process.env.MEDIPOST_RECIPIENT!;
|
||||||
@@ -145,7 +145,6 @@ export async function composeOrderXML({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the specimen order numbers for analysis elements
|
|
||||||
const uuringElementInputs: {
|
const uuringElementInputs: {
|
||||||
analysisElement: Tables<{ schema: 'medreport' }, 'analysis_elements'>,
|
analysisElement: Tables<{ schema: 'medreport' }, 'analysis_elements'>,
|
||||||
specimenOrderNr: number,
|
specimenOrderNr: number,
|
||||||
@@ -156,6 +155,7 @@ export async function composeOrderXML({
|
|||||||
for (const material of materials) {
|
for (const material of materials) {
|
||||||
const uniqueMaterial = uniqueMaterials.get(material.MaterjaliTyyp);
|
const uniqueMaterial = uniqueMaterials.get(material.MaterjaliTyyp);
|
||||||
if (!uniqueMaterial) {
|
if (!uniqueMaterial) {
|
||||||
|
console.info(`Unique material not found for material: ${material.MaterjaliTyyp}, analysis element: ${analysisElement.id} ${analysisElement.analysis_id_original} ${analysisElement.analysis_name_lab}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
uuringElementInputs.push({
|
uuringElementInputs.push({
|
||||||
@@ -177,7 +177,7 @@ export async function composeOrderXML({
|
|||||||
|
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Saadetis xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="TellimusLOINC.xsd">
|
<Saadetis xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="TellimusLOINC.xsd">
|
||||||
${getPais(USER, RECIPIENT, orderId)}
|
${getPais(USER, RECIPIENT, orderId, "OL")}
|
||||||
<Tellimus cito="EI">
|
<Tellimus cito="EI">
|
||||||
<ValisTellimuseId>${orderId}</ValisTellimuseId>
|
<ValisTellimuseId>${orderId}</ValisTellimuseId>
|
||||||
${getClientInstitution()}
|
${getClientInstitution()}
|
||||||
90
lib/services/medusaOrder.service.ts
Normal file
90
lib/services/medusaOrder.service.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { getAnalysisElements } from './analysis-element.service';
|
||||||
|
import { getAnalyses } from './analyses.service';
|
||||||
|
import { StoreOrder } from '@medusajs/types';
|
||||||
|
import { listProducts } from '@lib/data/products';
|
||||||
|
import { listRegions } from '@lib/data/regions';
|
||||||
|
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
||||||
|
|
||||||
|
const ANALYSIS_PACKAGE_HANDLE_PREFIX = 'analysis-package-';
|
||||||
|
|
||||||
|
export async function getOrderedAnalysisIds({
|
||||||
|
medusaOrder,
|
||||||
|
}: {
|
||||||
|
medusaOrder: StoreOrder;
|
||||||
|
}): Promise<{
|
||||||
|
analysisElementId?: number;
|
||||||
|
analysisId?: number;
|
||||||
|
}[]> {
|
||||||
|
const countryCodes = await listRegions();
|
||||||
|
const countryCode = countryCodes[0]!.countries![0]!.iso_2!;
|
||||||
|
|
||||||
|
async function getOrderedAnalysisElements(medusaOrder: StoreOrder) {
|
||||||
|
const originalIds = (medusaOrder?.items ?? [])
|
||||||
|
.map((a) => a.product?.metadata?.analysisIdOriginal)
|
||||||
|
.filter((a) => typeof a === 'string') as string[];
|
||||||
|
const analysisElements = await getAnalysisElements({ originalIds });
|
||||||
|
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getOrderedAnalyses(medusaOrder: StoreOrder) {
|
||||||
|
const originalIds = (medusaOrder?.items ?? [])
|
||||||
|
.map((a) => a.product?.metadata?.analysisIdOriginal)
|
||||||
|
.filter((a) => typeof a === 'string') as string[];
|
||||||
|
const analyses = await getAnalyses({ originalIds });
|
||||||
|
return analyses.map(({ id }) => ({ analysisId: id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getOrderedAnalysisPackages(medusaOrder: StoreOrder) {
|
||||||
|
const orderedPackages = (medusaOrder?.items ?? []).filter(({ product }) => product?.handle.startsWith(ANALYSIS_PACKAGE_HANDLE_PREFIX));
|
||||||
|
const orderedPackageIds = orderedPackages.map(({ product }) => product?.id).filter(Boolean) as string[];
|
||||||
|
if (orderedPackageIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
console.info(`Order has ${orderedPackageIds.length} packages`);
|
||||||
|
const { response: { products: orderedPackagesProducts } } = await listProducts({
|
||||||
|
countryCode,
|
||||||
|
queryParams: { limit: 100, id: orderedPackageIds },
|
||||||
|
});
|
||||||
|
console.info(`Order has ${orderedPackagesProducts.length} packages = ${JSON.stringify(orderedPackageIds, null, 2)}`);
|
||||||
|
if (orderedPackagesProducts.length !== orderedPackageIds.length) {
|
||||||
|
throw new Error(`Got ${orderedPackagesProducts.length} ordered packages products, expected ${orderedPackageIds.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = getAnalysisElementMedusaProductIds(
|
||||||
|
orderedPackagesProducts.map(({ id, metadata }) => ({
|
||||||
|
metadata,
|
||||||
|
variant: orderedPackages.find(({ product }) => product?.id === id)?.variant,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
if (ids.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const { response: { products: analysisPackagesProducts } } = await listProducts({
|
||||||
|
countryCode,
|
||||||
|
queryParams: { limit: 100, id: ids },
|
||||||
|
});
|
||||||
|
if (analysisPackagesProducts.length !== ids.length) {
|
||||||
|
throw new Error(`Got ${analysisPackagesProducts.length} analysis packages products, expected ${ids.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalIds = analysisPackagesProducts
|
||||||
|
.map(({ metadata }) => metadata?.analysisIdOriginal)
|
||||||
|
.filter((id) => typeof id === 'string');
|
||||||
|
if (originalIds.length !== ids.length) {
|
||||||
|
throw new Error(`Got ${originalIds.length} analysis packages products with analysisIdOriginal, expected ${ids.length}`);
|
||||||
|
}
|
||||||
|
const analysisElements = await getAnalysisElements({ originalIds });
|
||||||
|
|
||||||
|
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const [analysisPackageElements, orderedAnalysisElements, orderedAnalyses] = await Promise.all([
|
||||||
|
getOrderedAnalysisPackages(medusaOrder),
|
||||||
|
getOrderedAnalysisElements(medusaOrder),
|
||||||
|
getOrderedAnalyses(medusaOrder),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [...analysisPackageElements, ...orderedAnalysisElements, ...orderedAnalyses];
|
||||||
|
}
|
||||||
@@ -2,8 +2,7 @@ import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'
|
|||||||
import type { Tables } from '@kit/supabase/database';
|
import type { Tables } from '@kit/supabase/database';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import type { StoreOrder } from '@medusajs/types';
|
import type { StoreOrder } from '@medusajs/types';
|
||||||
|
import type { AnalysisOrder } from '../types/analysis-order';
|
||||||
export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
|
||||||
|
|
||||||
export async function createAnalysisOrder({
|
export async function createAnalysisOrder({
|
||||||
medusaOrder,
|
medusaOrder,
|
||||||
@@ -103,7 +102,7 @@ export async function getAnalysisOrder({
|
|||||||
if (error) {
|
if (error) {
|
||||||
throw new Error(`Failed to get order by medusaOrderId=${medusaOrderId} or analysisOrderId=${analysisOrderId}, message=${error.message}, data=${JSON.stringify(order)}`);
|
throw new Error(`Failed to get order by medusaOrderId=${medusaOrderId} or analysisOrderId=${analysisOrderId}, message=${error.message}, data=${JSON.stringify(order)}`);
|
||||||
}
|
}
|
||||||
return order;
|
return order as AnalysisOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAnalysisOrders({
|
export async function getAnalysisOrders({
|
||||||
|
|||||||
8
lib/services/util/xml.service.ts
Normal file
8
lib/services/util/xml.service.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { XMLParser } from 'fast-xml-parser';
|
||||||
|
|
||||||
|
export async function parseXML(xml: string) {
|
||||||
|
const parser = new XMLParser({ ignoreAttributes: false });
|
||||||
|
return parser.parse(xml);
|
||||||
|
}
|
||||||
@@ -9,11 +9,8 @@ export const getPais = (
|
|||||||
sender: string,
|
sender: string,
|
||||||
recipient: string,
|
recipient: string,
|
||||||
orderId: number,
|
orderId: number,
|
||||||
packageName = "OL",
|
packageName: string,
|
||||||
) => {
|
) => {
|
||||||
if (isProd) {
|
|
||||||
// return correct data
|
|
||||||
}
|
|
||||||
return `<Pais>
|
return `<Pais>
|
||||||
<Pakett versioon="20">${packageName}</Pakett>
|
<Pakett versioon="20">${packageName}</Pakett>
|
||||||
<Saatja>${sender}</Saatja>
|
<Saatja>${sender}</Saatja>
|
||||||
|
|||||||
3
lib/types/analysis-order.ts
Normal file
3
lib/types/analysis-order.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { Tables } from '@kit/supabase/database';
|
||||||
|
|
||||||
|
export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||||
3
lib/types/analysis-response-element.ts
Normal file
3
lib/types/analysis-response-element.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { Database } from "@/packages/supabase/src/database.types";
|
||||||
|
|
||||||
|
export type AnalysisResponseElement = Database['medreport']['Tables']['analysis_response_elements']['Row'];
|
||||||
@@ -1,11 +1,4 @@
|
|||||||
export interface IMedipostResponseXMLBase {
|
import type { IMedipostResponseXMLBase } from "@/packages/shared/src/types/medipost-analysis";
|
||||||
'?xml': {
|
|
||||||
'@_version': string;
|
|
||||||
'@_encoding': string;
|
|
||||||
'@_standalone': 'yes' | 'no';
|
|
||||||
};
|
|
||||||
ANSWER?: { CODE: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Message = {
|
export type Message = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
@@ -143,125 +136,3 @@ export type MedipostPublicMessageResponse = IMedipostResponseXMLBase & {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UuringuVastus = {
|
|
||||||
VastuseVaartus: number; // text according to docs, but based on examples and logically, float
|
|
||||||
VastuseAeg: string;
|
|
||||||
NormYlem?: {
|
|
||||||
'#text': number;
|
|
||||||
'@_kaasaarvatud': string;
|
|
||||||
}; // 0..1
|
|
||||||
NormAlum?: {
|
|
||||||
'#text': number;
|
|
||||||
'@_kaasaarvatud': string;
|
|
||||||
};
|
|
||||||
NormiStaatus: keyof typeof NormStatus;
|
|
||||||
ProoviJarjenumber: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ResponseUuring = {
|
|
||||||
UuringuElement: {
|
|
||||||
UuringIdOID: string;
|
|
||||||
UuringId: string;
|
|
||||||
TLyhend: string;
|
|
||||||
KNimetus: string;
|
|
||||||
UuringNimi: string;
|
|
||||||
TellijaUuringId: number;
|
|
||||||
TeostajaUuringId: string;
|
|
||||||
UuringOlek: keyof typeof AnalysisOrderStatus;
|
|
||||||
Mootyhik?: string; // 0..1
|
|
||||||
Kood: {
|
|
||||||
HkKood: number;
|
|
||||||
HkKoodiKordaja: number;
|
|
||||||
Koefitsient: number;
|
|
||||||
Hind: number;
|
|
||||||
};
|
|
||||||
UuringuVastus?: UuringuVastus | UuringuVastus[]; // 0..n
|
|
||||||
UuringuKommentaar?: string;
|
|
||||||
}; // 1..1
|
|
||||||
UuringuTaitjaAsutuseJnr: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ResponseUuringuGrupp = {
|
|
||||||
UuringuGruppId: string;
|
|
||||||
UuringuGruppNimi: string;
|
|
||||||
Uuring: ResponseUuring | ResponseUuring[]; // 1..n
|
|
||||||
};
|
|
||||||
|
|
||||||
// type for UuringuGrupp is correct, but some of this is generated by an LLM and should be checked if data in use
|
|
||||||
export type MedipostOrderResponse = IMedipostResponseXMLBase & {
|
|
||||||
Saadetis: {
|
|
||||||
Pais: {
|
|
||||||
Pakett: {
|
|
||||||
'#text': string;
|
|
||||||
'@_versioon': string;
|
|
||||||
};
|
|
||||||
Saatja: string;
|
|
||||||
Saaja: string;
|
|
||||||
Aeg: string;
|
|
||||||
SaadetisId: string;
|
|
||||||
Email: string;
|
|
||||||
};
|
|
||||||
Vastus?: {
|
|
||||||
ValisTellimuseId: string;
|
|
||||||
Asutus: {
|
|
||||||
'@_tyyp': string; // TEOSTAJA
|
|
||||||
'@_jarjenumber': string;
|
|
||||||
AsutuseId: number;
|
|
||||||
AsutuseNimi: string;
|
|
||||||
AsutuseKood: string;
|
|
||||||
AllyksuseNimi?: string;
|
|
||||||
Telefon: number;
|
|
||||||
}[]; //@_tyyp = TELLIJA 1..1, @_tyyp = TEOSTAJA 1..n
|
|
||||||
Personal: {
|
|
||||||
'@_tyyp': string;
|
|
||||||
'@_jarjenumber': string;
|
|
||||||
PersonalOID: string;
|
|
||||||
PersonalKood: string;
|
|
||||||
PersonalPerekonnaNimi: string;
|
|
||||||
PersonalEesNimi: string;
|
|
||||||
Telefon: number;
|
|
||||||
};
|
|
||||||
Patsient: {
|
|
||||||
IsikukoodiOID: string;
|
|
||||||
Isikukood: string;
|
|
||||||
PerekonnaNimi: string;
|
|
||||||
EesNimi: string;
|
|
||||||
SynniAeg: string;
|
|
||||||
SuguOID: string;
|
|
||||||
Sugu: string;
|
|
||||||
};
|
|
||||||
Proov: Array<{
|
|
||||||
ProovinouIdOID: string;
|
|
||||||
ProovinouId: string;
|
|
||||||
MaterjaliTyypOID: string;
|
|
||||||
MaterjaliTyyp: number;
|
|
||||||
MaterjaliNimi: string;
|
|
||||||
Ribakood: string;
|
|
||||||
Jarjenumber: number;
|
|
||||||
VotmisAeg: string;
|
|
||||||
SaabumisAeg: string;
|
|
||||||
}>;
|
|
||||||
TellimuseNumber: string;
|
|
||||||
TellimuseOlek: keyof typeof AnalysisOrderStatus;
|
|
||||||
UuringuGrupp?: ResponseUuringuGrupp | ResponseUuringuGrupp[];
|
|
||||||
};
|
|
||||||
Tellimus?: {
|
|
||||||
ValisTellimuseId: string;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AnalysisOrderStatus = {
|
|
||||||
1: 'QUEUED',
|
|
||||||
2: 'ON_HOLD',
|
|
||||||
3: 'PROCESSING',
|
|
||||||
4: 'COMPLETED',
|
|
||||||
5: 'REJECTED',
|
|
||||||
6: 'CANCELLED',
|
|
||||||
} as const;
|
|
||||||
export const NormStatus: Record<number, string> = {
|
|
||||||
0: 'NORMAL',
|
|
||||||
1: 'WARNING',
|
|
||||||
2: 'REQUIRES_ATTENTION',
|
|
||||||
} as const;
|
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -16,6 +16,10 @@
|
|||||||
"start": "next start",
|
"start": "next start",
|
||||||
"start:test": "NODE_ENV=test next start",
|
"start:test": "NODE_ENV=test next start",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:coverage": "jest --coverage",
|
||||||
|
"test:ci": "jest --ci --coverage --watchAll=false",
|
||||||
"supabase": "supabase",
|
"supabase": "supabase",
|
||||||
"supabase:start": "supabase status || supabase start",
|
"supabase:start": "supabase status || supabase start",
|
||||||
"supabase:stop": "supabase stop",
|
"supabase:stop": "supabase stop",
|
||||||
@@ -50,6 +54,7 @@
|
|||||||
"@kit/supabase": "workspace:*",
|
"@kit/supabase": "workspace:*",
|
||||||
"@kit/team-accounts": "workspace:*",
|
"@kit/team-accounts": "workspace:*",
|
||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
|
"@kit/user-analyses": "workspace:*",
|
||||||
"@makerkit/data-loader-supabase-core": "^0.0.10",
|
"@makerkit/data-loader-supabase-core": "^0.0.10",
|
||||||
"@makerkit/data-loader-supabase-nextjs": "^1.2.5",
|
"@makerkit/data-loader-supabase-nextjs": "^1.2.5",
|
||||||
"@marsidev/react-turnstile": "^1.1.0",
|
"@marsidev/react-turnstile": "^1.1.0",
|
||||||
@@ -87,6 +92,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
|
"@jest/globals": "^30.1.2",
|
||||||
"@kit/eslint-config": "workspace:*",
|
"@kit/eslint-config": "workspace:*",
|
||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
@@ -94,6 +100,7 @@
|
|||||||
"@medusajs/ui-preset": "latest",
|
"@medusajs/ui-preset": "latest",
|
||||||
"@next/bundle-analyzer": "15.3.2",
|
"@next/bundle-analyzer": "15.3.2",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
"@types/jsonwebtoken": "9.0.10",
|
"@types/jsonwebtoken": "9.0.10",
|
||||||
"@types/lodash": "^4.17.17",
|
"@types/lodash": "^4.17.17",
|
||||||
"@types/node": "^22.15.32",
|
"@types/node": "^22.15.32",
|
||||||
@@ -102,11 +109,14 @@
|
|||||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||||
"cssnano": "^7.0.7",
|
"cssnano": "^7.0.7",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
|
"jest": "^30.1.3",
|
||||||
|
"jest-environment-node": "^30.1.2",
|
||||||
"pino-pretty": "13.0.0",
|
"pino-pretty": "13.0.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"supabase": "^2.30.4",
|
"supabase": "^2.30.4",
|
||||||
"tailwindcss": "4.1.7",
|
"tailwindcss": "4.1.7",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"ts-jest": "^29.4.2",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"yup": "^1.6.1"
|
"yup": "^1.6.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
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 { AnalysisResultDetails, UserAnalysis } from '../types/accounts';
|
|
||||||
import PersonalCode from '~/lib/utils';
|
import PersonalCode from '~/lib/utils';
|
||||||
|
|
||||||
export type AccountWithParams =
|
import { AccountWithParams } from '../types/accounts';
|
||||||
Database['medreport']['Tables']['accounts']['Row'] & {
|
|
||||||
accountParams:
|
|
||||||
| (Pick<
|
|
||||||
Database['medreport']['Tables']['account_params']['Row'],
|
|
||||||
'weight' | 'height'
|
|
||||||
> & {
|
|
||||||
isSmoker:
|
|
||||||
| Database['medreport']['Tables']['account_params']['Row']['is_smoker']
|
|
||||||
| null;
|
|
||||||
})
|
|
||||||
| null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class representing an API for interacting with user accounts.
|
* Class representing an API for interacting with user accounts.
|
||||||
@@ -25,7 +11,7 @@ export type AccountWithParams =
|
|||||||
* @param {SupabaseClient<Database>} client - The Supabase client instance.
|
* @param {SupabaseClient<Database>} client - The Supabase client instance.
|
||||||
*/
|
*/
|
||||||
class AccountsApi {
|
class AccountsApi {
|
||||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
constructor(private readonly client: SupabaseClient<Database>) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name getAccount
|
* @name getAccount
|
||||||
@@ -218,89 +204,6 @@ class AccountsApi {
|
|||||||
return response.data?.customer_id;
|
return response.data?.customer_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserAnalysis(
|
|
||||||
analysisOrderId: number,
|
|
||||||
): Promise<AnalysisResultDetails | 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 { data: analysisResponse } = await this.client
|
|
||||||
.schema('medreport')
|
|
||||||
.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),
|
|
||||||
order:analysis_order_id(medusa_order_id, status, created_at),
|
|
||||||
summary:analysis_order_id(doctor_analysis_feedback(*))`,
|
|
||||||
)
|
|
||||||
.eq('user_id', user.id)
|
|
||||||
.eq('analysis_order_id', analysisOrderId)
|
|
||||||
.throwOnError();
|
|
||||||
|
|
||||||
const responseWithElements = analysisResponse?.[0];
|
|
||||||
if (!responseWithElements) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const feedback = responseWithElements.summary.doctor_analysis_feedback?.[0];
|
|
||||||
|
|
||||||
return {
|
|
||||||
...responseWithElements,
|
|
||||||
summary:
|
|
||||||
feedback?.status === 'COMPLETED'
|
|
||||||
? responseWithElements.summary.doctor_analysis_feedback?.[0]
|
|
||||||
: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserAnalyses(): Promise<UserAnalysis | 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 { data: analysisResponses } = await this.client
|
|
||||||
.schema('medreport')
|
|
||||||
.from('analysis_responses')
|
|
||||||
.select('*')
|
|
||||||
.eq('user_id', user.id);
|
|
||||||
|
|
||||||
if (!analysisResponses) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const analysisResponseIds = analysisResponses.map((r) => r.id);
|
|
||||||
|
|
||||||
const { data: analysisResponseElements } = await this.client
|
|
||||||
.schema('medreport')
|
|
||||||
.from('analysis_response_elements')
|
|
||||||
.select('*')
|
|
||||||
.in('analysis_response_id', analysisResponseIds);
|
|
||||||
|
|
||||||
if (!analysisResponseElements) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return analysisResponses.map((r) => ({
|
|
||||||
...r,
|
|
||||||
elements: analysisResponseElements.filter(
|
|
||||||
(e) => e.analysis_response_id === r.id,
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async hasAccountTeamMembership(accountId?: string) {
|
async hasAccountTeamMembership(accountId?: string) {
|
||||||
if (!accountId) {
|
if (!accountId) {
|
||||||
return false;
|
return false;
|
||||||
@@ -318,23 +221,6 @@ class AccountsApi {
|
|||||||
|
|
||||||
return (count ?? 0) > 0;
|
return (count ?? 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchBmiThresholds() {
|
|
||||||
// Fetch BMI
|
|
||||||
const { data, error } = await this.client
|
|
||||||
.schema('medreport')
|
|
||||||
.from('bmi_thresholds')
|
|
||||||
.select(
|
|
||||||
'age_min,age_max,underweight_max,normal_min,normal_max,overweight_min,strong_min,obesity_min',
|
|
||||||
)
|
|
||||||
.order('age_min', { ascending: true });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error fetching BMI thresholds:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAccountsApi(client: SupabaseClient<Database>) {
|
export function createAccountsApi(client: SupabaseClient<Database>) {
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
import * as z from 'zod';
|
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
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[];
|
|
||||||
|
|
||||||
export type ApplicationRole =
|
export type ApplicationRole =
|
||||||
Database['medreport']['Tables']['accounts']['Row']['application_role'];
|
Database['medreport']['Tables']['accounts']['Row']['application_role'];
|
||||||
export enum ApplicationRoleEnum {
|
export enum ApplicationRoleEnum {
|
||||||
@@ -18,50 +8,16 @@ export enum ApplicationRoleEnum {
|
|||||||
SuperAdmin = 'super_admin',
|
SuperAdmin = 'super_admin',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ElementSchema = z.object({
|
export type AccountWithParams =
|
||||||
unit: z.string(),
|
Database['medreport']['Tables']['accounts']['Row'] & {
|
||||||
norm_lower: z.number(),
|
accountParams:
|
||||||
norm_upper: z.number(),
|
| (Pick<
|
||||||
norm_status: z.number(),
|
Database['medreport']['Tables']['account_params']['Row'],
|
||||||
analysis_name: z.string(),
|
'weight' | 'height'
|
||||||
response_time: z.string(),
|
> & {
|
||||||
response_value: z.number(),
|
isSmoker:
|
||||||
norm_lower_included: z.boolean(),
|
| Database['medreport']['Tables']['account_params']['Row']['is_smoker']
|
||||||
norm_upper_included: z.boolean(),
|
| null;
|
||||||
});
|
})
|
||||||
export type Element = z.infer<typeof ElementSchema>;
|
| null;
|
||||||
|
};
|
||||||
export const OrderSchema = z.object({
|
|
||||||
status: z.string(),
|
|
||||||
medusa_order_id: z.string(),
|
|
||||||
created_at: z.coerce.date(),
|
|
||||||
});
|
|
||||||
export type Order = z.infer<typeof OrderSchema>;
|
|
||||||
|
|
||||||
export 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(),
|
|
||||||
});
|
|
||||||
export type Summary = z.infer<typeof SummarySchema>;
|
|
||||||
|
|
||||||
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>;
|
|
||||||
|
|||||||
3
packages/features/accounts/src/types/analysis-orders.ts
Normal file
3
packages/features/accounts/src/types/analysis-orders.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { Tables } from '@kit/supabase/database';
|
||||||
|
|
||||||
|
export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||||
139
packages/features/accounts/src/types/analysis-results.ts
Normal file
139
packages/features/accounts/src/types/analysis-results.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
3
packages/features/user-analyses/eslint.config.mjs
Normal file
3
packages/features/user-analyses/eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import eslintConfigBase from '@kit/eslint-config/base.js';
|
||||||
|
|
||||||
|
export default eslintConfigBase;
|
||||||
33
packages/features/user-analyses/package.json
Normal file
33
packages/features/user-analyses/package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@kit/user-analyses",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"scripts": {
|
||||||
|
"clean": "git clean -xdf .turbo node_modules",
|
||||||
|
"format": "prettier --check \"**/*.{ts,tsx}\"",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"./api": "./src/server/api.ts",
|
||||||
|
"./types/*": "./src/types/*.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^5.1.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@kit/eslint-config": "workspace:*",
|
||||||
|
"@kit/prettier-config": "workspace:*",
|
||||||
|
"@kit/shared": "workspace:*",
|
||||||
|
"@kit/supabase": "workspace:*",
|
||||||
|
"@kit/tsconfig": "workspace:*"
|
||||||
|
},
|
||||||
|
"prettier": "@kit/prettier-config",
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
313
packages/features/user-analyses/src/server/api.ts
Normal file
313
packages/features/user-analyses/src/server/api.ts
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
import { Database } from '@kit/supabase/database';
|
||||||
|
import type { UuringElement, UuringuVastus } from '@kit/shared/types/medipost-analysis';
|
||||||
|
|
||||||
|
import type { AnalysisResultDetails, AnalysisResultDetailsMapped, UserAnalysis } from '../types/analysis-results';
|
||||||
|
import type { AnalysisOrder } from '../types/analysis-orders';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing an API for interacting with user accounts.
|
||||||
|
* @constructor
|
||||||
|
* @param {SupabaseClient<Database>} client - The Supabase client instance.
|
||||||
|
*/
|
||||||
|
class UserAnalysesApi {
|
||||||
|
constructor(private readonly client: SupabaseClient<Database>) { }
|
||||||
|
|
||||||
|
async getAnalysisOrder({
|
||||||
|
medusaOrderId,
|
||||||
|
analysisOrderId,
|
||||||
|
}: {
|
||||||
|
medusaOrderId?: string;
|
||||||
|
analysisOrderId?: number;
|
||||||
|
}) {
|
||||||
|
const query = this.client
|
||||||
|
.schema('medreport')
|
||||||
|
.from('analysis_orders')
|
||||||
|
.select('*')
|
||||||
|
if (medusaOrderId) {
|
||||||
|
query.eq('medusa_order_id', medusaOrderId);
|
||||||
|
} else if (analysisOrderId) {
|
||||||
|
query.eq('id', analysisOrderId);
|
||||||
|
} else {
|
||||||
|
throw new Error('Either medusaOrderId or orderId must be provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: order, error } = await query.single();
|
||||||
|
if (error) {
|
||||||
|
throw new Error(`Failed to get order by medusaOrderId=${medusaOrderId} or analysisOrderId=${analysisOrderId}, message=${error.message}, data=${JSON.stringify(order)}`);
|
||||||
|
}
|
||||||
|
return order as AnalysisOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserAnalysis(
|
||||||
|
analysisOrderId: number,
|
||||||
|
): 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 orderedAnalysisElementIds = analysisOrder.analysis_element_ids ?? [];
|
||||||
|
if (orderedAnalysisElementIds.length === 0) {
|
||||||
|
console.error('No ordered analysis element ids found for analysis order id=', analysisOrderId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderedAnalysisElementOriginalIds = orderedAnalysisElements.map(({ analysis_id_original }) => analysis_id_original);
|
||||||
|
if (orderedAnalysisElementOriginalIds.length === 0) {
|
||||||
|
console.error('No ordered analysis element original ids found for analysis order id=', analysisOrderId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: analysisResponse } = await this.client
|
||||||
|
.schema('medreport')
|
||||||
|
.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,response_value_is_within_norm),
|
||||||
|
summary:analysis_order_id(doctor_analysis_feedback(*))`,
|
||||||
|
)
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.eq('analysis_order_id', analysisOrderId)
|
||||||
|
.throwOnError();
|
||||||
|
|
||||||
|
const responseWithElements = analysisResponse?.[0] as AnalysisResultDetails | null;
|
||||||
|
if (!responseWithElements) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const analysisResponseElements = responseWithElements.elements;
|
||||||
|
|
||||||
|
const feedback = responseWithElements.summary?.doctor_analysis_feedback?.[0];
|
||||||
|
|
||||||
|
const mappedOrderedAnalysisElements = orderedAnalysisElements.map(({ analysis_id_original, analysis_name_lab }) => {
|
||||||
|
return this.getOrderedAnalysisElements({
|
||||||
|
analysisIdOriginal: analysis_id_original,
|
||||||
|
analysisNameLab: analysis_name_lab,
|
||||||
|
analysisResponseElements,
|
||||||
|
});
|
||||||
|
}).sort((a, b) => a.analysisName.localeCompare(b.analysisName));
|
||||||
|
const nestedAnalysisElementIds = mappedOrderedAnalysisElements.map(({ results }) => results?.nestedElements.map(({ analysisElementOriginalId }) => analysisElementOriginalId)).flat();
|
||||||
|
if (nestedAnalysisElementIds.length > 0) {
|
||||||
|
const { data: nestedAnalysisElements, error: nestedAnalysisElementsError } = await this.client
|
||||||
|
.schema('medreport')
|
||||||
|
.from('analysis_elements')
|
||||||
|
.select('*')
|
||||||
|
.in('id', nestedAnalysisElementIds);
|
||||||
|
console.info('analysisResponse nestedAnalysisElementIds', nestedAnalysisElementIds)
|
||||||
|
if (!nestedAnalysisElementsError && nestedAnalysisElements) {
|
||||||
|
for (const mappedOrderedAnalysisElement of mappedOrderedAnalysisElements) {
|
||||||
|
const { results } = mappedOrderedAnalysisElement;
|
||||||
|
if (!results) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const nestedElement of results.nestedElements) {
|
||||||
|
const { analysisElementOriginalId } = nestedElement;
|
||||||
|
const nestedAnalysisElement = nestedAnalysisElements.find(({ id }) => id === analysisElementOriginalId);
|
||||||
|
if (!nestedAnalysisElement) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
results.nestedElements.push({
|
||||||
|
...nestedAnalysisElement,
|
||||||
|
analysisElementOriginalId,
|
||||||
|
analysisName: nestedAnalysisElement.analysis_name_lab,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mappedOrderedAnalysisElements.forEach(({ results }) => {
|
||||||
|
results?.nestedElements.forEach(({ analysisElementOriginalId }) => {
|
||||||
|
const nestedAnalysisElement = nestedAnalysisElements.find(({ id }) => id === analysisElementOriginalId);
|
||||||
|
if (nestedAnalysisElement) {
|
||||||
|
results?.nestedElements.push({
|
||||||
|
...nestedAnalysisElement,
|
||||||
|
analysisElementOriginalId,
|
||||||
|
analysisName: nestedAnalysisElement.analysis_name_lab,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
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,
|
||||||
|
analysisNameLab,
|
||||||
|
analysisResponseElements,
|
||||||
|
}: {
|
||||||
|
analysisIdOriginal: string;
|
||||||
|
analysisNameLab: string;
|
||||||
|
analysisResponseElements: AnalysisResultDetails['elements'];
|
||||||
|
}) {
|
||||||
|
const elementResponse = analysisResponseElements.find((element) => element.analysis_element_original_id === analysisIdOriginal);
|
||||||
|
if (!elementResponse) {
|
||||||
|
return {
|
||||||
|
analysisIdOriginal,
|
||||||
|
isWaitingForResults: true,
|
||||||
|
analysisName: analysisNameLab,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const labComment = elementResponse.original_response_element?.UuringuKommentaar;
|
||||||
|
return {
|
||||||
|
analysisIdOriginal,
|
||||||
|
isWaitingForResults: false,
|
||||||
|
analysisName: analysisNameLab,
|
||||||
|
results: {
|
||||||
|
nestedElements: (() => {
|
||||||
|
const nestedElements = elementResponse.original_response_element?.UuringuElement as UuringElement[] | undefined;
|
||||||
|
if (!nestedElements) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
normLower: elementVastus?.NormAlum?.['#text'],
|
||||||
|
normUpper: elementVastus?.NormYlem?.['#text'],
|
||||||
|
normStatus: elementVastus?.NormiStaatus,
|
||||||
|
responseTime: elementVastus?.VastuseAeg,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})(),
|
||||||
|
labComment,
|
||||||
|
//originalResponseElement: elementResponse.original_response_element ?? null,
|
||||||
|
unit: elementResponse.unit,
|
||||||
|
normLower: elementResponse.norm_lower,
|
||||||
|
normUpper: elementResponse.norm_upper,
|
||||||
|
normStatus: elementResponse.norm_status,
|
||||||
|
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,
|
||||||
|
analysisElementOriginalId: elementResponse.analysis_element_original_id,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO unused currently
|
||||||
|
async getUserAnalyses(): Promise<UserAnalysis | 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 { data: analysisResponses } = await this.client
|
||||||
|
.schema('medreport')
|
||||||
|
.from('analysis_responses')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', user.id);
|
||||||
|
|
||||||
|
if (!analysisResponses) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const analysisResponseIds = analysisResponses.map((r) => r.id);
|
||||||
|
|
||||||
|
const { data: analysisResponseElements } = await this.client
|
||||||
|
.schema('medreport')
|
||||||
|
.from('analysis_response_elements')
|
||||||
|
.select('*')
|
||||||
|
.in('analysis_response_id', analysisResponseIds);
|
||||||
|
|
||||||
|
if (!analysisResponseElements) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return analysisResponses.map((r) => ({
|
||||||
|
...r,
|
||||||
|
elements: analysisResponseElements.filter(
|
||||||
|
(e) => e.analysis_response_id === r.id,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasAccountTeamMembership(accountId?: string) {
|
||||||
|
if (!accountId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { count, error } = await this.client
|
||||||
|
.schema('medreport')
|
||||||
|
.from('accounts_memberships')
|
||||||
|
.select('account_id', { count: 'exact', head: true })
|
||||||
|
.eq('account_id', accountId);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (count ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchBmiThresholds() {
|
||||||
|
// Fetch BMI
|
||||||
|
const { data, error } = await this.client
|
||||||
|
.schema('medreport')
|
||||||
|
.from('bmi_thresholds')
|
||||||
|
.select(
|
||||||
|
'age_min,age_max,underweight_max,normal_min,normal_max,overweight_min,strong_min,obesity_min',
|
||||||
|
)
|
||||||
|
.order('age_min', { ascending: true });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error fetching BMI thresholds:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createUserAnalysesApi(client: SupabaseClient<Database>) {
|
||||||
|
return new UserAnalysesApi(client);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { Tables } from '@kit/supabase/database';
|
||||||
|
|
||||||
|
export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||||
140
packages/features/user-analyses/src/types/analysis-results.ts
Normal file
140
packages/features/user-analyses/src/types/analysis-results.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
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(),
|
||||||
|
response_value_is_within_norm: 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;
|
||||||
|
};
|
||||||
11
packages/features/user-analyses/tsconfig.json
Normal file
11
packages/features/user-analyses/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "@kit/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
|
||||||
|
"paths": {
|
||||||
|
"~/lib/utils": ["../../../lib/utils.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["*.ts", "*.tsx", "src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -16,7 +16,8 @@
|
|||||||
"./events": "./src/events/index.tsx",
|
"./events": "./src/events/index.tsx",
|
||||||
"./components/*": "./src/components/*.tsx",
|
"./components/*": "./src/components/*.tsx",
|
||||||
"./registry": "./src/registry/index.ts",
|
"./registry": "./src/registry/index.ts",
|
||||||
"./config": "./src/config/index.ts"
|
"./config": "./src/config/index.ts",
|
||||||
|
"./types/*": "./src/types/*.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/eslint-config": "workspace:*",
|
"@kit/eslint-config": "workspace:*",
|
||||||
|
|||||||
132
packages/shared/src/types/medipost-analysis.ts
Normal file
132
packages/shared/src/types/medipost-analysis.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
export const NormStatus: Record<number, string> = {
|
||||||
|
0: 'NORMAL',
|
||||||
|
1: 'WARNING',
|
||||||
|
2: 'REQUIRES_ATTENTION',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const AnalysisOrderStatus = {
|
||||||
|
1: 'QUEUED',
|
||||||
|
2: 'ON_HOLD',
|
||||||
|
3: 'PROCESSING',
|
||||||
|
4: 'COMPLETED',
|
||||||
|
5: 'REJECTED',
|
||||||
|
6: 'CANCELLED',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type UuringuVastus = {
|
||||||
|
VastuseVaartus: string; // numeric or text like 'Negatiivne'
|
||||||
|
VastuseAeg: string;
|
||||||
|
NormYlem?: {
|
||||||
|
'#text': number;
|
||||||
|
'@_kaasaarvatud': string;
|
||||||
|
}; // 0..1
|
||||||
|
NormAlum?: {
|
||||||
|
'#text': number;
|
||||||
|
'@_kaasaarvatud': string;
|
||||||
|
};
|
||||||
|
NormiStaatus: keyof typeof NormStatus;
|
||||||
|
ProoviJarjenumber: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UuringElement = {
|
||||||
|
UuringIdOID: string;
|
||||||
|
UuringId: string;
|
||||||
|
TLyhend: string;
|
||||||
|
KNimetus: string;
|
||||||
|
UuringNimi: string;
|
||||||
|
TellijaUuringId: number;
|
||||||
|
TeostajaUuringId: string;
|
||||||
|
UuringOlek: keyof typeof AnalysisOrderStatus;
|
||||||
|
Mootyhik?: string; // 0..1
|
||||||
|
Kood: {
|
||||||
|
HkKood: number;
|
||||||
|
HkKoodiKordaja: number;
|
||||||
|
Koefitsient: number;
|
||||||
|
Hind: number;
|
||||||
|
};
|
||||||
|
UuringuVastus?: UuringuVastus | UuringuVastus[]; // 0..n
|
||||||
|
UuringuKommentaar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResponseUuring = {
|
||||||
|
UuringuElement: UuringElement;
|
||||||
|
UuringuTaitjaAsutuseJnr: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResponseUuringuGrupp = {
|
||||||
|
UuringuGruppId: string;
|
||||||
|
UuringuGruppNimi: string;
|
||||||
|
Uuring: ResponseUuring | ResponseUuring[]; // 1..n
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IMedipostResponseXMLBase {
|
||||||
|
'?xml': {
|
||||||
|
'@_version': string;
|
||||||
|
'@_encoding': string;
|
||||||
|
'@_standalone': 'yes' | 'no';
|
||||||
|
};
|
||||||
|
ANSWER?: { CODE: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MedipostOrderResponse = IMedipostResponseXMLBase & {
|
||||||
|
Saadetis: {
|
||||||
|
Pais: {
|
||||||
|
Pakett: {
|
||||||
|
'#text': string;
|
||||||
|
'@_versioon': string;
|
||||||
|
};
|
||||||
|
Saatja: string;
|
||||||
|
Saaja: string;
|
||||||
|
Aeg: string;
|
||||||
|
SaadetisId: string;
|
||||||
|
Email: string;
|
||||||
|
};
|
||||||
|
Vastus?: {
|
||||||
|
ValisTellimuseId: string;
|
||||||
|
Asutus: {
|
||||||
|
'@_tyyp': string; // TEOSTAJA
|
||||||
|
'@_jarjenumber': string;
|
||||||
|
AsutuseId: number;
|
||||||
|
AsutuseNimi: string;
|
||||||
|
AsutuseKood: string;
|
||||||
|
AllyksuseNimi?: string;
|
||||||
|
Telefon: number;
|
||||||
|
}[]; //@_tyyp = TELLIJA 1..1, @_tyyp = TEOSTAJA 1..n
|
||||||
|
Personal: {
|
||||||
|
'@_tyyp': string;
|
||||||
|
'@_jarjenumber': string;
|
||||||
|
PersonalOID: string;
|
||||||
|
PersonalKood: string;
|
||||||
|
PersonalPerekonnaNimi: string;
|
||||||
|
PersonalEesNimi: string;
|
||||||
|
Telefon: number;
|
||||||
|
};
|
||||||
|
Patsient: {
|
||||||
|
IsikukoodiOID: string;
|
||||||
|
Isikukood: string;
|
||||||
|
PerekonnaNimi: string;
|
||||||
|
EesNimi: string;
|
||||||
|
SynniAeg: string;
|
||||||
|
SuguOID: string;
|
||||||
|
Sugu: string;
|
||||||
|
};
|
||||||
|
Proov: Array<{
|
||||||
|
ProovinouIdOID: string;
|
||||||
|
ProovinouId: string;
|
||||||
|
MaterjaliTyypOID: string;
|
||||||
|
MaterjaliTyyp: number;
|
||||||
|
MaterjaliNimi: string;
|
||||||
|
Ribakood: string;
|
||||||
|
Jarjenumber: number;
|
||||||
|
VotmisAeg: string;
|
||||||
|
SaabumisAeg: string;
|
||||||
|
}>;
|
||||||
|
TellimuseNumber: string;
|
||||||
|
TellimuseOlek: keyof typeof AnalysisOrderStatus;
|
||||||
|
UuringuGrupp?: ResponseUuringuGrupp | ResponseUuringuGrupp[];
|
||||||
|
};
|
||||||
|
Tellimus?: {
|
||||||
|
ValisTellimuseId: string;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -686,7 +686,10 @@ export type Database = {
|
|||||||
norm_upper_included: boolean | null
|
norm_upper_included: boolean | null
|
||||||
original_response_element: Json
|
original_response_element: Json
|
||||||
response_time: string
|
response_time: string
|
||||||
response_value: number
|
response_value: number | null
|
||||||
|
response_value_is_negative?: boolean | null
|
||||||
|
response_value_is_within_norm?: boolean | null
|
||||||
|
status: string
|
||||||
unit: string | null
|
unit: string | null
|
||||||
updated_at: string | null
|
updated_at: string | null
|
||||||
}
|
}
|
||||||
@@ -704,7 +707,10 @@ export type Database = {
|
|||||||
norm_upper_included?: boolean | null
|
norm_upper_included?: boolean | null
|
||||||
original_response_element: Json
|
original_response_element: Json
|
||||||
response_time: string
|
response_time: string
|
||||||
response_value: number
|
response_value: number | null
|
||||||
|
response_value_is_negative?: boolean | null
|
||||||
|
response_value_is_within_norm?: boolean | null
|
||||||
|
status: string
|
||||||
unit?: string | null
|
unit?: string | null
|
||||||
updated_at?: string | null
|
updated_at?: string | null
|
||||||
}
|
}
|
||||||
@@ -722,7 +728,10 @@ export type Database = {
|
|||||||
norm_upper_included?: boolean | null
|
norm_upper_included?: boolean | null
|
||||||
original_response_element?: Json
|
original_response_element?: Json
|
||||||
response_time?: string
|
response_time?: string
|
||||||
response_value?: number
|
response_value?: number | null
|
||||||
|
response_value_is_negative?: boolean | null
|
||||||
|
response_value_is_within_norm?: boolean | null
|
||||||
|
status: string
|
||||||
unit?: string | null
|
unit?: string | null
|
||||||
updated_at?: string | null
|
updated_at?: string | null
|
||||||
}
|
}
|
||||||
@@ -1264,6 +1273,8 @@ export type Database = {
|
|||||||
action: string
|
action: string
|
||||||
xml: string
|
xml: string
|
||||||
has_analysis_results: boolean
|
has_analysis_results: boolean
|
||||||
|
medipost_external_order_id: string
|
||||||
|
medipost_private_message_id: string
|
||||||
medusa_order_id: string
|
medusa_order_id: string
|
||||||
response_xml: string
|
response_xml: string
|
||||||
has_error: boolean
|
has_error: boolean
|
||||||
@@ -1272,6 +1283,8 @@ export type Database = {
|
|||||||
action: string
|
action: string
|
||||||
xml: string
|
xml: string
|
||||||
has_analysis_results: boolean
|
has_analysis_results: boolean
|
||||||
|
medipost_external_order_id: string
|
||||||
|
medipost_private_message_id: string
|
||||||
medusa_order_id: string
|
medusa_order_id: string
|
||||||
response_xml: string
|
response_xml: string
|
||||||
has_error: boolean
|
has_error: boolean
|
||||||
|
|||||||
1286
pnpm-lock.yaml
generated
1286
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,16 @@
|
|||||||
"noAnalysisElements": "No analysis orders found",
|
"noAnalysisElements": "No analysis orders found",
|
||||||
"noAnalysisOrders": "No analysis orders found",
|
"noAnalysisOrders": "No analysis orders found",
|
||||||
"analysisDate": "Analysis result date",
|
"analysisDate": "Analysis result date",
|
||||||
|
"cancelled": "Cancelled",
|
||||||
"results": {
|
"results": {
|
||||||
"range": {
|
"range": {
|
||||||
"normal": "Normal range"
|
"normal": "Normal range"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"negative": "Negative",
|
||||||
|
"positive": "Positive",
|
||||||
|
"isWithinNorm": "Within norm",
|
||||||
|
"isNotWithinNorm": "Not within norm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"orderTitle": "Order number {{orderNumber}}",
|
"orderTitle": "Order number {{orderNumber}}",
|
||||||
|
|||||||
@@ -7,9 +7,16 @@
|
|||||||
"noAnalysisElements": "Veel ei ole tellitud analüüse",
|
"noAnalysisElements": "Veel ei ole tellitud analüüse",
|
||||||
"noAnalysisOrders": "Veel ei ole analüüside tellimusi",
|
"noAnalysisOrders": "Veel ei ole analüüside tellimusi",
|
||||||
"analysisDate": "Analüüsi vastuse kuupäev",
|
"analysisDate": "Analüüsi vastuse kuupäev",
|
||||||
|
"cancelled": "Tühistatud",
|
||||||
"results": {
|
"results": {
|
||||||
"range": {
|
"range": {
|
||||||
"normal": "Normaalne vahemik"
|
"normal": "Normaalne vahemik"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"negative": "Negatiivne",
|
||||||
|
"positive": "Positiivne",
|
||||||
|
"isWithinNorm": "Normi piires",
|
||||||
|
"isNotWithinNorm": "Normi piirest väljas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"orderTitle": "Tellimus {{orderNumber}}",
|
"orderTitle": "Tellimus {{orderNumber}}",
|
||||||
|
|||||||
@@ -7,9 +7,16 @@
|
|||||||
"noAnalysisElements": "Анализы еще не заказаны",
|
"noAnalysisElements": "Анализы еще не заказаны",
|
||||||
"noAnalysisOrders": "Пока нет заказов на анализы",
|
"noAnalysisOrders": "Пока нет заказов на анализы",
|
||||||
"analysisDate": "Дата результата анализа",
|
"analysisDate": "Дата результата анализа",
|
||||||
|
"cancelled": "Отменен",
|
||||||
"results": {
|
"results": {
|
||||||
"range": {
|
"range": {
|
||||||
"normal": "Нормальный диапазон"
|
"normal": "Нормальный диапазон"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"negative": "Отрицательный",
|
||||||
|
"positive": "Положительный",
|
||||||
|
"isWithinNorm": "В норме",
|
||||||
|
"isNotWithinNorm": "Не в норме"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"orderTitle": "Заказ {{orderNumber}}"
|
"orderTitle": "Заказ {{orderNumber}}"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- store extra audit info
|
||||||
|
ALTER TABLE medreport.medipost_actions ADD COLUMN medipost_private_message_id TEXT;
|
||||||
|
ALTER TABLE medreport.medipost_actions ADD COLUMN medipost_external_order_id TEXT;
|
||||||
|
|
||||||
|
-- store status info for frontend texts
|
||||||
|
ALTER TABLE medreport.analysis_response_elements ADD COLUMN status TEXT;
|
||||||
|
|
||||||
|
-- cancelled orders
|
||||||
|
ALTER TABLE medreport.analysis_response_elements ALTER COLUMN response_time DROP NOT NULL;
|
||||||
|
ALTER TABLE medreport.analysis_response_elements ALTER COLUMN response_value DROP NOT NULL;
|
||||||
|
ALTER TABLE medreport.analysis_response_elements ALTER COLUMN comment DROP NOT NULL;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE medreport.analysis_response_elements
|
||||||
|
ADD COLUMN response_value_is_negative BOOLEAN;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE medreport.analysis_response_elements
|
||||||
|
ADD COLUMN response_value_is_within_norm BOOLEAN;
|
||||||
Reference in New Issue
Block a user