merge
This commit is contained in:
@@ -9,11 +9,7 @@ import { AnalysisElement, createAnalysisElement, getAnalysisElements } from '~/l
|
|||||||
import { createCodes } from '~/lib/services/codes.service';
|
import { createCodes } from '~/lib/services/codes.service';
|
||||||
import { getLatestPublicMessageListItem } from '~/lib/services/medipost/medipostPublicMessage.service';
|
import { getLatestPublicMessageListItem } from '~/lib/services/medipost/medipostPublicMessage.service';
|
||||||
import type { ICode } from '~/lib/types/code';
|
import type { ICode } from '~/lib/types/code';
|
||||||
|
import { toArray } from '@kit/shared/utils';
|
||||||
function toArray<T>(input?: T | T[] | null): T[] {
|
|
||||||
if (!input) return [];
|
|
||||||
return Array.isArray(input) ? input : [input];
|
|
||||||
}
|
|
||||||
|
|
||||||
const WRITE_XML_TO_FILE = false as boolean;
|
const WRITE_XML_TO_FILE = false as boolean;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getAnalysisOrder } from "~/lib/services/order.service";
|
|||||||
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipost/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 } from "~/lib/services/medipost/medipostMessageBase.service";
|
import { upsertMedipostActionLog } from "~/lib/services/medipost/medipostMessageBase.service";
|
||||||
import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service";
|
import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service";
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
@@ -35,7 +35,7 @@ export async function POST(request: Request) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createMedipostActionLog({
|
await upsertMedipostActionLog({
|
||||||
action: 'send_fake_analysis_results_to_medipost',
|
action: 'send_fake_analysis_results_to_medipost',
|
||||||
xml: messageXml,
|
xml: messageXml,
|
||||||
medusaOrderId,
|
medusaOrderId,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
@@ -53,47 +54,44 @@ export default async function AnalysisResultsPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const orderedAnalysisElements = analysisResponse.orderedAnalysisElements;
|
const orderedAnalysisElements = analysisResponse.orderedAnalysisElements;
|
||||||
|
const hasOrderedAnalysisElements = orderedAnalysisElements.length > 0;
|
||||||
|
const isPartialStatus = analysisResponse.order.status === 'PARTIAL_ANALYSIS_RESPONSE';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader />
|
<PageHeader
|
||||||
<PageBody className="gap-4">
|
title={<Trans i18nKey="analysis-results:pageTitle" />}
|
||||||
<div className="mt-8 flex flex-col justify-between gap-4 sm:flex-row sm:items-center sm:gap-0">
|
description={hasOrderedAnalysisElements ? (
|
||||||
<div>
|
isPartialStatus
|
||||||
<h4>
|
? <Trans i18nKey="analysis-results:descriptionPartial" />
|
||||||
<Trans i18nKey="analysis-results:pageTitle" />
|
: <Trans i18nKey="analysis-results:description" />
|
||||||
</h4>
|
) : (
|
||||||
<p className="text-muted-foreground text-sm">
|
<Trans i18nKey="analysis-results:descriptionEmpty" />
|
||||||
{analysisResponse?.elements &&
|
)}
|
||||||
analysisResponse.elements?.length > 0 ? (
|
>
|
||||||
<Trans i18nKey="analysis-results:description" />
|
<div>
|
||||||
) : (
|
|
||||||
<Trans i18nKey="analysis-results:descriptionEmpty" />
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href={pathsConfig.app.orderAnalysisPackage}>
|
<Link href={pathsConfig.app.orderAnalysisPackage}>
|
||||||
<Trans i18nKey="analysis-results:orderNewAnalysis" />
|
<Trans i18nKey="analysis-results:orderNewAnalysis" />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageBody className="gap-4 pt-4">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<h4>
|
<h5 className="break-all">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="analysis-results:orderTitle"
|
i18nKey="analysis-results:orderTitle"
|
||||||
values={{ orderNumber: analysisResponse.order.medusaOrderId }}
|
values={{ orderNumber: analysisResponse.order.medusaOrderId }}
|
||||||
/>
|
/>
|
||||||
</h4>
|
</h5>
|
||||||
<h5>
|
<h6>
|
||||||
<Trans
|
<Trans i18nKey={`orders:status.${analysisResponse.order.status}`} />
|
||||||
i18nKey={`orders:status.${analysisResponse.order.status}`}
|
|
||||||
/>
|
|
||||||
<ButtonTooltip
|
<ButtonTooltip
|
||||||
content={`${analysisResponse.order.createdAt ? new Date(analysisResponse?.order?.createdAt).toLocaleString() : ''}`}
|
content={`${analysisResponse.order.createdAt ? new Date(analysisResponse?.order?.createdAt).toLocaleString() : ''}`}
|
||||||
className="ml-6"
|
className="ml-6"
|
||||||
/>
|
/>
|
||||||
</h5>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
{analysisResponse?.summary?.value && (
|
{analysisResponse?.summary?.value && (
|
||||||
<div>
|
<div>
|
||||||
@@ -106,7 +104,16 @@ export default async function AnalysisResultsPage({
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{orderedAnalysisElements ? (
|
{orderedAnalysisElements ? (
|
||||||
orderedAnalysisElements.map((element, index) => (
|
orderedAnalysisElements.map((element, index) => (
|
||||||
<Analysis key={index} element={element} />
|
<React.Fragment key={element.analysisIdOriginal}>
|
||||||
|
<Analysis element={element} />
|
||||||
|
{element.results?.nestedElements?.map((nestedElement, nestedIndex) => (
|
||||||
|
<Analysis
|
||||||
|
key={`nested-${nestedElement.analysisElementOriginalId}-${nestedIndex}`}
|
||||||
|
nestedElement={nestedElement}
|
||||||
|
isNestedElement
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="text-muted-foreground text-sm">
|
<div className="text-muted-foreground text-sm">
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ import { useMemo } from 'react';
|
|||||||
import { ArrowDown } from 'lucide-react';
|
import { ArrowDown } from 'lucide-react';
|
||||||
|
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
import { AnalysisResultDetailsElementResults } from '@/packages/features/accounts/src/types/analysis-results';
|
import type { AnalysisResultDetailsElementResults } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||||
|
import { AnalysisResultLevel } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||||
|
|
||||||
export enum AnalysisResultLevel {
|
type AnalysisResultLevelBarResults = Pick<AnalysisResultDetailsElementResults, 'normLower' | 'normUpper' | 'responseValue'>;
|
||||||
NORMAL = 0,
|
|
||||||
WARNING = 1,
|
|
||||||
CRITICAL = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
const Level = ({
|
const Level = ({
|
||||||
isActive = false,
|
isActive = false,
|
||||||
@@ -50,7 +47,7 @@ const Level = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{color === 'success' && typeof normRangeText === 'string' && (
|
{color === 'success' && typeof normRangeText === 'string' && (
|
||||||
<p className={cn("absolute bottom-[-18px] left-3/8 text-xs text-muted-foreground font-bold", {
|
<p className={cn("absolute bottom-[-18px] left-3/8 text-xs text-muted-foreground font-bold whitespace-nowrap", {
|
||||||
'opacity-60': isActive,
|
'opacity-60': isActive,
|
||||||
})}>
|
})}>
|
||||||
{normRangeText}
|
{normRangeText}
|
||||||
@@ -60,28 +57,19 @@ const Level = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AnalysisLevelBarSkeleton = () => {
|
|
||||||
return (
|
|
||||||
<div className="mt-4 flex h-3 w-[60%] sm:w-[35%] max-w-[360px] gap-1 sm:mt-0">
|
|
||||||
<Level color="gray-200" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AnalysisLevelBar = ({
|
const AnalysisLevelBar = ({
|
||||||
level,
|
level,
|
||||||
results,
|
results: {
|
||||||
|
normLower: lower,
|
||||||
|
normUpper: upper,
|
||||||
|
responseValue: value,
|
||||||
|
},
|
||||||
normRangeText,
|
normRangeText,
|
||||||
}: {
|
}: {
|
||||||
level: AnalysisResultLevel;
|
level: AnalysisResultLevel;
|
||||||
results: AnalysisResultDetailsElementResults;
|
results: AnalysisResultLevelBarResults;
|
||||||
normRangeText: string | null;
|
normRangeText: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const { normLower: lower, normUpper: upper, responseValue: value, normStatus } = results;
|
|
||||||
const normLowerIncluded = results?.normLowerIncluded || false;
|
|
||||||
const normUpperIncluded = results?.normUpperIncluded || false;
|
|
||||||
|
|
||||||
// Calculate arrow position based on value within normal range
|
// Calculate arrow position based on value within normal range
|
||||||
const arrowLocation = useMemo(() => {
|
const arrowLocation = useMemo(() => {
|
||||||
// If no response value, center the arrow
|
// If no response value, center the arrow
|
||||||
@@ -147,8 +135,6 @@ const AnalysisLevelBar = ({
|
|||||||
|
|
||||||
// Show appropriate levels based on available norm bounds
|
// Show appropriate levels based on available norm bounds
|
||||||
const hasLowerBound = lower !== null;
|
const hasLowerBound = lower !== null;
|
||||||
const isLowerBoundZero = hasLowerBound && lower === 0;
|
|
||||||
console.info('isLowerBoundZero', results.analysisElementOriginalId, { isLowerBoundZero, hasLowerBound, lower });
|
|
||||||
const hasUpperBound = upper !== null;
|
const hasUpperBound = upper !== null;
|
||||||
|
|
||||||
// Determine which section the value falls into
|
// Determine which section the value falls into
|
||||||
@@ -157,34 +143,10 @@ const AnalysisLevelBar = ({
|
|||||||
const isValueInNormalRange = !isValueBelowLower && !isValueAboveUpper;
|
const isValueInNormalRange = !isValueBelowLower && !isValueAboveUpper;
|
||||||
|
|
||||||
const [first, second, third] = useMemo(() => {
|
const [first, second, third] = useMemo(() => {
|
||||||
if (!hasLowerBound) {
|
const [warning, normal, critical] = [
|
||||||
return [
|
|
||||||
{
|
|
||||||
isActive: isNormal,
|
|
||||||
color: "success",
|
|
||||||
isFirst: true,
|
|
||||||
normRangeText,
|
|
||||||
...(isNormal ? { arrowLocation } : {}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isActive: isWarning,
|
|
||||||
color: "warning",
|
|
||||||
...(isWarning ? { arrowLocation } : {}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isActive: isCritical,
|
|
||||||
color: "destructive",
|
|
||||||
isLast: true,
|
|
||||||
...(isCritical ? { arrowLocation } : {}),
|
|
||||||
},
|
|
||||||
] as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
{
|
||||||
isActive: isWarning,
|
isActive: isWarning,
|
||||||
color: "warning",
|
color: "warning",
|
||||||
isFirst: true,
|
|
||||||
...(isWarning ? { arrowLocation } : {}),
|
...(isWarning ? { arrowLocation } : {}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -200,10 +162,30 @@ const AnalysisLevelBar = ({
|
|||||||
...(isCritical ? { arrowLocation } : {}),
|
...(isCritical ? { arrowLocation } : {}),
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
if (!hasLowerBound) {
|
||||||
|
return [
|
||||||
|
{ ...normal, isFirst: true },
|
||||||
|
warning,
|
||||||
|
critical,
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ ...warning, isFirst: true },
|
||||||
|
normal,
|
||||||
|
{ ...critical, isLast: true },
|
||||||
|
] as const;
|
||||||
}, [isValueBelowLower, isValueAboveUpper, isValueInNormalRange, arrowLocation, normRangeText, isNormal, isWarning, isCritical]);
|
}, [isValueBelowLower, isValueAboveUpper, isValueInNormalRange, arrowLocation, normRangeText, isNormal, isWarning, isCritical]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex h-3 w-[60%] sm:w-[35%] max-w-[360px] gap-1 sm:mt-0">
|
<div className={cn(
|
||||||
|
"flex h-3 gap-1",
|
||||||
|
"mt-4 sm:mt-0",
|
||||||
|
"w-[60%] sm:w-[35%]",
|
||||||
|
"min-w-[50vw] sm:min-w-auto",
|
||||||
|
"max-w-[360px]",
|
||||||
|
)}>
|
||||||
<Level {...first} />
|
<Level {...first} />
|
||||||
<Level {...second} />
|
<Level {...second} />
|
||||||
<Level {...third} />
|
<Level {...third} />
|
||||||
|
|||||||
@@ -2,33 +2,59 @@
|
|||||||
|
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { AnalysisResultDetailsElement } from '@/packages/features/accounts/src/types/analysis-results';
|
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
AnalysisResultDetailsElement,
|
||||||
|
AnalysisResultsDetailsElementNested,
|
||||||
|
} from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||||
|
import { AnalysisResultLevel } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
import AnalysisLevelBar, {
|
import AnalysisLevelBar from './analysis-level-bar';
|
||||||
AnalysisResultLevel,
|
|
||||||
} from './analysis-level-bar';
|
|
||||||
|
|
||||||
export enum AnalysisStatus {
|
|
||||||
NORMAL = 0,
|
|
||||||
MEDIUM = 1,
|
|
||||||
HIGH = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
const Analysis = ({
|
const Analysis = ({
|
||||||
element,
|
element: elementOriginal,
|
||||||
|
nestedElement,
|
||||||
|
isNestedElement = false,
|
||||||
}: {
|
}: {
|
||||||
element: AnalysisResultDetailsElement;
|
element?: AnalysisResultDetailsElement;
|
||||||
|
nestedElement?: AnalysisResultsDetailsElementNested;
|
||||||
|
isNestedElement?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const element = (() => {
|
||||||
|
if (isNestedElement) {
|
||||||
|
return nestedElement!;
|
||||||
|
}
|
||||||
|
return elementOriginal!;
|
||||||
|
})();
|
||||||
|
const results: AnalysisResultDetailsElement['results'] = useMemo(() => {
|
||||||
|
if (isNestedElement) {
|
||||||
|
const nestedElement = element as AnalysisResultsDetailsElementNested;
|
||||||
|
return {
|
||||||
|
analysisElementOriginalId: nestedElement.analysisElementOriginalId,
|
||||||
|
normLower: nestedElement.normLower,
|
||||||
|
normUpper: nestedElement.normUpper,
|
||||||
|
normStatus: nestedElement.normStatus,
|
||||||
|
responseTime: nestedElement.responseTime,
|
||||||
|
responseValue: nestedElement.responseValue,
|
||||||
|
responseValueIsNegative: nestedElement.responseValueIsNegative,
|
||||||
|
responseValueIsWithinNorm: nestedElement.responseValueIsWithinNorm,
|
||||||
|
normLowerIncluded: nestedElement.normLowerIncluded,
|
||||||
|
normUpperIncluded: nestedElement.normUpperIncluded,
|
||||||
|
unit: nestedElement.unit,
|
||||||
|
status: nestedElement.status,
|
||||||
|
nestedElements: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return (element as AnalysisResultDetailsElement).results;
|
||||||
|
}, [element, isNestedElement]);
|
||||||
|
|
||||||
const name = element.analysisName || '';
|
const name = element.analysisName || '';
|
||||||
const results = element.results;
|
|
||||||
|
|
||||||
const hasIsWithinNorm = results?.responseValueIsWithinNorm !== null;
|
const hasIsWithinNorm = results?.responseValueIsWithinNorm !== null;
|
||||||
const hasIsNegative = results?.responseValueIsNegative !== null;
|
const hasIsNegative = results?.responseValueIsNegative !== null;
|
||||||
@@ -58,21 +84,16 @@ const Analysis = ({
|
|||||||
return responseValue;
|
return responseValue;
|
||||||
})();
|
})();
|
||||||
const unit = results?.unit || '';
|
const unit = results?.unit || '';
|
||||||
const normLower = results?.normLower;
|
const normLower = results?.normLower ?? null;
|
||||||
const normUpper = results?.normUpper;
|
const normUpper = results?.normUpper ?? null;
|
||||||
|
const normStatus = results?.normStatus ?? null;
|
||||||
|
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
const analysisResultLevel = useMemo(() => {
|
const analysisResultLevel = useMemo(() => {
|
||||||
if (!results) {
|
if (normStatus === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results.responseValue === null || results.responseValue === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normStatus = results.normStatus;
|
|
||||||
|
|
||||||
switch (normStatus) {
|
switch (normStatus) {
|
||||||
case 1:
|
case 1:
|
||||||
return AnalysisResultLevel.WARNING;
|
return AnalysisResultLevel.WARNING;
|
||||||
@@ -82,17 +103,24 @@ const Analysis = ({
|
|||||||
default:
|
default:
|
||||||
return AnalysisResultLevel.NORMAL;
|
return AnalysisResultLevel.NORMAL;
|
||||||
}
|
}
|
||||||
}, [results]);
|
}, [normStatus]);
|
||||||
|
|
||||||
const isCancelled = Number(results?.status) === 5;
|
const isCancelled = Number(results?.status) === 5;
|
||||||
const hasNestedElements = results?.nestedElements.length > 0;
|
const nestedElements = results?.nestedElements ?? null;
|
||||||
|
const hasNestedElements = Array.isArray(nestedElements) && nestedElements.length > 0;
|
||||||
|
|
||||||
const normRangeText = normLower !== null ? `${normLower} - ${normUpper || ''}` : null;
|
const normRangeText = (() => {
|
||||||
|
if (normLower === null && normUpper === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${normLower ?? '...'} - ${normUpper ?? '...'}`;
|
||||||
|
})();
|
||||||
|
const hasTextualResponse = hasIsNegative || hasIsWithinNorm;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-border rounded-lg border px-5">
|
<div className={cn("border-border rounded-lg border px-5", { 'ml-8': isNestedElement })}>
|
||||||
<div className="flex flex-col items-center justify-between gap-2 pt-3 pb-6 sm:py-3 sm:h-[65px] sm:flex-row sm:gap-0">
|
<div className="flex flex-col items-center justify-between gap-2 pt-3 pb-6 sm:py-3 sm:h-[65px] sm:flex-row sm:gap-0">
|
||||||
<div className="flex items-center gap-2 font-semibold">
|
<div className={cn("flex items-center gap-2 font-semibold", { 'font-bold': isNestedElement })}>
|
||||||
{name}
|
{name}
|
||||||
{results?.responseTime && (
|
{results?.responseTime && (
|
||||||
<div
|
<div
|
||||||
@@ -127,10 +155,18 @@ const Analysis = ({
|
|||||||
{isCancelled || !results || hasNestedElements ? null : (
|
{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={cn('font-semibold', {
|
||||||
|
'text-yellow-600': hasTextualResponse && analysisResultLevel === AnalysisResultLevel.WARNING,
|
||||||
|
'text-red-600': hasTextualResponse && analysisResultLevel === AnalysisResultLevel.CRITICAL,
|
||||||
|
'text-green-600': hasTextualResponse && analysisResultLevel === AnalysisResultLevel.NORMAL,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
<div className="text-muted-foreground text-sm">{unit}</div>
|
<div className="text-muted-foreground text-sm">{unit}</div>
|
||||||
</div>
|
</div>
|
||||||
{!(hasIsNegative || hasIsWithinNorm) && (
|
{!hasTextualResponse && (
|
||||||
<>
|
<>
|
||||||
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
|
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
|
||||||
{normRangeText}
|
{normRangeText}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AnalysisResultDetailsMapped } from "@/packages/features/accounts/src/types/analysis-results";
|
import type { AnalysisResultDetailsMapped } from "@/packages/features/user-analyses/src/types/analysis-results";
|
||||||
|
|
||||||
type AnalysisTestResponse = Omit<AnalysisResultDetailsMapped, 'order' | 'orderedAnalysisElementIds' | 'summary' | 'elements'>;
|
type AnalysisTestResponse = Omit<AnalysisResultDetailsMapped, 'order' | 'orderedAnalysisElementIds' | 'summary' | 'elements'>;
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ const big1: AnalysisTestResponse = {
|
|||||||
"unit": null,
|
"unit": null,
|
||||||
"normLower": null,
|
"normLower": null,
|
||||||
"normUpper": 2,
|
"normUpper": 2,
|
||||||
"normStatus": 0,
|
"normStatus": 2,
|
||||||
"responseTime": "2024-02-29T10:13:01+00:00",
|
"responseTime": "2024-02-29T10:13:01+00:00",
|
||||||
"responseValue": null,
|
"responseValue": null,
|
||||||
"responseValueIsNegative": null,
|
"responseValueIsNegative": null,
|
||||||
@@ -150,6 +150,26 @@ const big1: AnalysisTestResponse = {
|
|||||||
"analysisElementOriginalId": "59156-0"
|
"analysisElementOriginalId": "59156-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": true,
|
||||||
|
"normLowerIncluded": false,
|
||||||
|
"normUpperIncluded": false,
|
||||||
|
"status": "4",
|
||||||
|
"analysisElementOriginalId": "59156-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"analysisIdOriginal": "13955-0",
|
"analysisIdOriginal": "13955-0",
|
||||||
"isWaitingForResults": false,
|
"isWaitingForResults": false,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
|
|
||||||
import { AnalysisResultDetailsMapped } from '@kit/accounts/types/analysis-results';
|
import type { AnalysisResultDetailsMapped } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
||||||
|
|
||||||
|
|||||||
@@ -18,16 +18,32 @@ export async function getExistingAnalysisResponseElements({
|
|||||||
return data as AnalysisResponseElement[];
|
return data as AnalysisResponseElement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createAnalysisResponseElement({
|
export async function upsertAnalysisResponseElement({
|
||||||
element,
|
element,
|
||||||
}: {
|
}: {
|
||||||
element: Omit<AnalysisResponseElement, 'created_at' | 'updated_at' | 'id'>;
|
element: Omit<AnalysisResponseElement, 'created_at' | 'updated_at' | 'id'>;
|
||||||
}) {
|
}) {
|
||||||
await getSupabaseServerAdminClient()
|
const { data } = await getSupabaseServerAdminClient()
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('analysis_response_elements')
|
.from('analysis_response_elements')
|
||||||
.insert(element)
|
.upsert(
|
||||||
|
element,
|
||||||
|
{
|
||||||
|
onConflict: 'analysis_response_id,analysis_element_original_id',
|
||||||
|
ignoreDuplicates: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.select('id')
|
||||||
.throwOnError();
|
.throwOnError();
|
||||||
|
|
||||||
|
const analysisResponseElementId = data?.[0]?.id;
|
||||||
|
if (!analysisResponseElementId) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to insert or update analysis response element (response id: ${element.analysis_response_id}, element id: ${element.analysis_element_original_id})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { analysisResponseElementId };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function upsertAnalysisResponse({
|
export async function upsertAnalysisResponse({
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { toArray } from '@/lib/utils';
|
|
||||||
|
|
||||||
import { getMailer } from '@kit/mailers';
|
import { getMailer } from '@kit/mailers';
|
||||||
import { enhanceAction } from '@kit/next/actions';
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
|
import { toArray } from '@kit/shared/utils';
|
||||||
|
|
||||||
import { emailSchema } from '~/lib/validations/email.schema';
|
import { emailSchema } from '~/lib/validations/email.schema';
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export async function getLatestMessage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createMedipostActionLog({
|
export async function upsertMedipostActionLog({
|
||||||
action,
|
action,
|
||||||
xml,
|
xml,
|
||||||
hasAnalysisResults = false,
|
hasAnalysisResults = false,
|
||||||
@@ -40,8 +40,7 @@ export async function createMedipostActionLog({
|
|||||||
action:
|
action:
|
||||||
| 'send_order_to_medipost'
|
| 'send_order_to_medipost'
|
||||||
| 'sync_analysis_results_from_medipost'
|
| 'sync_analysis_results_from_medipost'
|
||||||
| 'send_fake_analysis_results_to_medipost'
|
| 'send_fake_analysis_results_to_medipost';
|
||||||
| 'send_analysis_results_to_medipost';
|
|
||||||
xml: string;
|
xml: string;
|
||||||
hasAnalysisResults?: boolean;
|
hasAnalysisResults?: boolean;
|
||||||
medusaOrderId?: string | null;
|
medusaOrderId?: string | null;
|
||||||
@@ -50,19 +49,34 @@ export async function createMedipostActionLog({
|
|||||||
medipostExternalOrderId?: string | null;
|
medipostExternalOrderId?: string | null;
|
||||||
medipostPrivateMessageId?: string | null;
|
medipostPrivateMessageId?: string | null;
|
||||||
}) {
|
}) {
|
||||||
await getSupabaseServerAdminClient()
|
const { data } = await getSupabaseServerAdminClient()
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('medipost_actions')
|
.from('medipost_actions')
|
||||||
.insert({
|
.upsert(
|
||||||
action,
|
{
|
||||||
xml,
|
action,
|
||||||
has_analysis_results: hasAnalysisResults,
|
xml,
|
||||||
medusa_order_id: medusaOrderId,
|
has_analysis_results: hasAnalysisResults,
|
||||||
response_xml: responseXml,
|
medusa_order_id: medusaOrderId,
|
||||||
has_error: hasError,
|
response_xml: responseXml,
|
||||||
medipost_external_order_id: medipostExternalOrderId,
|
has_error: hasError,
|
||||||
medipost_private_message_id: medipostPrivateMessageId,
|
medipost_external_order_id: medipostExternalOrderId,
|
||||||
})
|
medipost_private_message_id: medipostPrivateMessageId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onConflict: 'medipost_private_message_id',
|
||||||
|
ignoreDuplicates: false
|
||||||
|
}
|
||||||
|
)
|
||||||
.select('id')
|
.select('id')
|
||||||
.throwOnError();
|
.throwOnError();
|
||||||
|
|
||||||
|
const medipostActionId = data?.[0]?.id;
|
||||||
|
if (!medipostActionId) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to insert or update medipost action (private message id: ${medipostPrivateMessageId})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { medipostActionId };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,15 +13,16 @@ import type {
|
|||||||
MedipostOrderResponse,
|
MedipostOrderResponse,
|
||||||
UuringElement,
|
UuringElement,
|
||||||
} from '@/packages/shared/src/types/medipost-analysis';
|
} from '@/packages/shared/src/types/medipost-analysis';
|
||||||
import { toArray } from '@/lib/utils';
|
import { toArray } from '@kit/shared/utils';
|
||||||
import type { AnalysisOrder } from '~/lib/types/analysis-order';
|
import type { AnalysisOrder } from '~/lib/types/analysis-order';
|
||||||
import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element';
|
import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element';
|
||||||
|
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
||||||
|
|
||||||
import { Tables } from '@kit/supabase/database';
|
import { Tables } from '@kit/supabase/database';
|
||||||
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
||||||
import { getAnalysisElementsAdmin } from '../analysis-element.service';
|
import { getAnalysisElementsAdmin } from '../analysis-element.service';
|
||||||
import { getAnalyses } from '../analyses.service';
|
import { getAnalyses } from '../analyses.service';
|
||||||
import { createMedipostActionLog, getLatestMessage } from './medipostMessageBase.service';
|
import { upsertMedipostActionLog, getLatestMessage } from './medipostMessageBase.service';
|
||||||
import { validateMedipostResponse } from './medipostValidate.service';
|
import { validateMedipostResponse } from './medipostValidate.service';
|
||||||
import { getAnalysisOrder, updateAnalysisOrderStatus } from '../order.service';
|
import { getAnalysisOrder, updateAnalysisOrderStatus } from '../order.service';
|
||||||
import { parseXML } from '../util/xml.service';
|
import { parseXML } from '../util/xml.service';
|
||||||
@@ -29,7 +30,7 @@ import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service';
|
|||||||
import { getAccountAdmin } from '../account.service';
|
import { getAccountAdmin } from '../account.service';
|
||||||
import { logMedipostDispatch } from '../audit.service';
|
import { logMedipostDispatch } from '../audit.service';
|
||||||
import { MedipostValidationError } from './MedipostValidationError';
|
import { MedipostValidationError } from './MedipostValidationError';
|
||||||
import { createAnalysisResponseElement, getExistingAnalysisResponseElements, upsertAnalysisResponse } from '../analysis-order.service';
|
import { upsertAnalysisResponseElement, getExistingAnalysisResponseElements, upsertAnalysisResponse } from '../analysis-order.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!;
|
||||||
@@ -138,28 +139,25 @@ export async function getAnalysisResponseElementsForGroup({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseValueIsNumeric = responseValue !== null;
|
const mappedResponse = createUserAnalysesApi(getSupabaseServerAdminClient())
|
||||||
const responseValueIsNegative = vastuseVaartus === 'Negatiivne';
|
.mapUuringVastus({ uuringVastus: response });
|
||||||
const responseValueIsWithinNorm = vastuseVaartus === 'Normi piires';
|
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
analysis_element_original_id: analysisElementOriginalId,
|
analysis_element_original_id: analysisElementOriginalId,
|
||||||
norm_lower: response.NormAlum?.['#text'] ?? null,
|
norm_lower: mappedResponse.normLower,
|
||||||
norm_lower_included:
|
norm_lower_included: mappedResponse.normLowerIncluded,
|
||||||
response.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah',
|
norm_status: mappedResponse.normStatus,
|
||||||
norm_status: response.NormiStaatus,
|
norm_upper: mappedResponse.normUpper,
|
||||||
norm_upper: response.NormYlem?.['#text'] ?? null,
|
norm_upper_included: mappedResponse.normUpperIncluded,
|
||||||
norm_upper_included:
|
response_time: mappedResponse.responseTime,
|
||||||
response.NormYlem?.['@_kaasaarvatud'].toLowerCase() === 'jah',
|
response_value: mappedResponse.responseValue,
|
||||||
response_time: response.VastuseAeg ?? null,
|
|
||||||
response_value: responseValue,
|
|
||||||
unit: groupUuringElement.Mootyhik ?? null,
|
unit: groupUuringElement.Mootyhik ?? null,
|
||||||
original_response_element: groupUuringElement,
|
original_response_element: groupUuringElement,
|
||||||
analysis_name: groupUuringElement.UuringNimi || groupUuringElement.KNimetus,
|
analysis_name: groupUuringElement.UuringNimi || groupUuringElement.KNimetus,
|
||||||
comment: groupUuringElement.UuringuKommentaar ?? null,
|
comment: groupUuringElement.UuringuKommentaar ?? null,
|
||||||
status: status.toString(),
|
status: status.toString(),
|
||||||
response_value_is_within_norm: responseValueIsNumeric ? null : responseValueIsWithinNorm,
|
response_value_is_within_norm: mappedResponse.responseValueIsWithinNorm,
|
||||||
response_value_is_negative: responseValueIsNumeric ? null : responseValueIsNegative,
|
response_value_is_negative: mappedResponse.responseValueIsNegative,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,7 +196,7 @@ async function hasAllAnalysisResponseElements({
|
|||||||
}) {
|
}) {
|
||||||
const allOrderResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId });
|
const allOrderResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId });
|
||||||
const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0;
|
const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0;
|
||||||
return allOrderResponseElements.length === expectedOrderResponseElements;
|
return allOrderResponseElements.length >= expectedOrderResponseElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncPrivateMessage({
|
export async function syncPrivateMessage({
|
||||||
@@ -242,7 +240,7 @@ export async function syncPrivateMessage({
|
|||||||
|
|
||||||
for (const element of newElements) {
|
for (const element of newElements) {
|
||||||
try {
|
try {
|
||||||
await createAnalysisResponseElement({
|
await upsertAnalysisResponseElement({
|
||||||
element: {
|
element: {
|
||||||
...element,
|
...element,
|
||||||
analysis_response_id: analysisResponseId,
|
analysis_response_id: analysisResponseId,
|
||||||
@@ -305,7 +303,7 @@ export async function readPrivateMessageResponse({
|
|||||||
const hasInvalidOrderId = isNaN(analysisOrderId);
|
const hasInvalidOrderId = isNaN(analysisOrderId);
|
||||||
|
|
||||||
if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) {
|
if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) {
|
||||||
await createMedipostActionLog({
|
await upsertMedipostActionLog({
|
||||||
action: 'sync_analysis_results_from_medipost',
|
action: 'sync_analysis_results_from_medipost',
|
||||||
xml: privateMessageXml,
|
xml: privateMessageXml,
|
||||||
hasAnalysisResults: false,
|
hasAnalysisResults: false,
|
||||||
@@ -342,7 +340,7 @@ export async function readPrivateMessageResponse({
|
|||||||
|
|
||||||
const status = await syncPrivateMessage({ messageResponse, order: analysisOrder });
|
const status = await syncPrivateMessage({ messageResponse, order: analysisOrder });
|
||||||
|
|
||||||
await createMedipostActionLog({
|
await upsertMedipostActionLog({
|
||||||
action: 'sync_analysis_results_from_medipost',
|
action: 'sync_analysis_results_from_medipost',
|
||||||
xml: privateMessageXml,
|
xml: privateMessageXml,
|
||||||
hasAnalysisResults: true,
|
hasAnalysisResults: true,
|
||||||
@@ -475,7 +473,7 @@ export async function sendOrderToMedipost({
|
|||||||
isMedipostError,
|
isMedipostError,
|
||||||
errorMessage: e.response,
|
errorMessage: e.response,
|
||||||
});
|
});
|
||||||
await createMedipostActionLog({
|
await upsertMedipostActionLog({
|
||||||
action: 'send_order_to_medipost',
|
action: 'send_order_to_medipost',
|
||||||
xml: orderXml,
|
xml: orderXml,
|
||||||
hasAnalysisResults: false,
|
hasAnalysisResults: false,
|
||||||
@@ -489,7 +487,7 @@ export async function sendOrderToMedipost({
|
|||||||
isSuccess: false,
|
isSuccess: false,
|
||||||
isMedipostError,
|
isMedipostError,
|
||||||
});
|
});
|
||||||
await createMedipostActionLog({
|
await upsertMedipostActionLog({
|
||||||
action: 'send_order_to_medipost',
|
action: 'send_order_to_medipost',
|
||||||
xml: orderXml,
|
xml: orderXml,
|
||||||
hasAnalysisResults: false,
|
hasAnalysisResults: false,
|
||||||
@@ -505,7 +503,7 @@ export async function sendOrderToMedipost({
|
|||||||
isSuccess: true,
|
isSuccess: true,
|
||||||
isMedipostError: false,
|
isMedipostError: false,
|
||||||
});
|
});
|
||||||
await createMedipostActionLog({
|
await upsertMedipostActionLog({
|
||||||
action: 'send_order_to_medipost',
|
action: 'send_order_to_medipost',
|
||||||
xml: orderXml,
|
xml: orderXml,
|
||||||
hasAnalysisResults: false,
|
hasAnalysisResults: false,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
MaterjalideGrupp,
|
MaterjalideGrupp,
|
||||||
} from '@/lib/types/medipost';
|
} from '@/lib/types/medipost';
|
||||||
import { toArray } from '@/lib/utils';
|
import { toArray } from '@kit/shared/utils';
|
||||||
import { uniqBy } from 'lodash';
|
import { uniqBy } from 'lodash';
|
||||||
|
|
||||||
import { Tables } from '@kit/supabase/database';
|
import { Tables } from '@kit/supabase/database';
|
||||||
|
|||||||
@@ -9,11 +9,6 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toArray<T>(input?: T | T[] | null): T[] {
|
|
||||||
if (!input) return [];
|
|
||||||
return Array.isArray(input) ? input : [input];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toTitleCase(str?: string) {
|
export function toTitleCase(str?: string) {
|
||||||
return (
|
return (
|
||||||
str
|
str
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ const noop = (event: string) => {
|
|||||||
// do nothing - this is to prevent errors when the analytics service is not initialized
|
// do nothing - this is to prevent errors when the analytics service is not initialized
|
||||||
|
|
||||||
return async (...args: unknown[]) => {
|
return async (...args: unknown[]) => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.debug(
|
console.debug(
|
||||||
`Noop analytics service called with event: ${event}`,
|
`Noop analytics service called with event: ${event}`,
|
||||||
...args.filter(Boolean),
|
...args.filter(Boolean),
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import { Tables } from '@kit/supabase/database';
|
|
||||||
|
|
||||||
export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import * as z from 'zod';
|
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
|
||||||
|
|
||||||
export type UserAnalysisElement =
|
|
||||||
Database['medreport']['Tables']['analysis_response_elements']['Row'];
|
|
||||||
export type UserAnalysisResponse =
|
|
||||||
Database['medreport']['Tables']['analysis_responses']['Row'] & {
|
|
||||||
elements: UserAnalysisElement[];
|
|
||||||
};
|
|
||||||
export type UserAnalysis = UserAnalysisResponse[];
|
|
||||||
|
|
||||||
const ElementSchema = z.object({
|
|
||||||
unit: z.string(),
|
|
||||||
norm_lower: z.number(),
|
|
||||||
norm_upper: z.number(),
|
|
||||||
norm_status: z.number(),
|
|
||||||
analysis_name: z.string(),
|
|
||||||
response_time: z.string(),
|
|
||||||
response_value: z.number(),
|
|
||||||
response_value_is_negative: z.boolean(),
|
|
||||||
norm_lower_included: z.boolean(),
|
|
||||||
norm_upper_included: z.boolean(),
|
|
||||||
status: z.string(),
|
|
||||||
analysis_element_original_id: z.string(),
|
|
||||||
original_response_element: z.object({
|
|
||||||
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const OrderSchema = z.object({
|
|
||||||
status: z.string(),
|
|
||||||
medusa_order_id: z.string(),
|
|
||||||
created_at: z.coerce.date(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const DoctorAnalysisFeedbackSchema = z.object({
|
|
||||||
id: z.number(),
|
|
||||||
status: z.string(),
|
|
||||||
user_id: z.string(),
|
|
||||||
created_at: z.coerce.date(),
|
|
||||||
created_by: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const SummarySchema = z.object({
|
|
||||||
id: z.number(),
|
|
||||||
value: z.string(),
|
|
||||||
status: z.string(),
|
|
||||||
user_id: z.string(),
|
|
||||||
created_at: z.coerce.date(),
|
|
||||||
created_by: z.string(),
|
|
||||||
updated_at: z.coerce.date().nullable(),
|
|
||||||
updated_by: z.string(),
|
|
||||||
doctor_user_id: z.string().nullable(),
|
|
||||||
analysis_order_id: z.number(),
|
|
||||||
doctor_analysis_feedback: z.array(DoctorAnalysisFeedbackSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const AnalysisResultDetailsSchema = z.object({
|
|
||||||
id: z.number(),
|
|
||||||
analysis_order_id: z.number(),
|
|
||||||
order_number: z.string(),
|
|
||||||
order_status: z.string(),
|
|
||||||
user_id: z.string(),
|
|
||||||
created_at: z.coerce.date(),
|
|
||||||
updated_at: z.coerce.date().nullable(),
|
|
||||||
elements: z.array(ElementSchema),
|
|
||||||
order: OrderSchema,
|
|
||||||
summary: SummarySchema.nullable(),
|
|
||||||
});
|
|
||||||
export type AnalysisResultDetails = z.infer<typeof AnalysisResultDetailsSchema>;
|
|
||||||
|
|
||||||
export type AnalysisResultDetailsElementResults = {
|
|
||||||
unit: string | null;
|
|
||||||
normLower: number | null;
|
|
||||||
normUpper: number | null;
|
|
||||||
normStatus: number | null;
|
|
||||||
responseTime: string | null;
|
|
||||||
responseValue: number | null;
|
|
||||||
responseValueIsNegative: boolean | null;
|
|
||||||
responseValueIsWithinNorm: boolean | null;
|
|
||||||
normLowerIncluded: boolean;
|
|
||||||
normUpperIncluded: boolean;
|
|
||||||
status: string;
|
|
||||||
analysisElementOriginalId: string;
|
|
||||||
nestedElements: {
|
|
||||||
analysisElementOriginalId: string;
|
|
||||||
normLower?: number | null;
|
|
||||||
normLowerIncluded: boolean;
|
|
||||||
normStatus: number;
|
|
||||||
normUpper?: number | null;
|
|
||||||
normUpperIncluded: boolean;
|
|
||||||
responseTime: string;
|
|
||||||
responseValue: number;
|
|
||||||
status: number;
|
|
||||||
unit: string;
|
|
||||||
}[];
|
|
||||||
labComment?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnalysisResultDetailsElement = {
|
|
||||||
analysisIdOriginal: string;
|
|
||||||
isWaitingForResults: boolean;
|
|
||||||
analysisName: string;
|
|
||||||
results: AnalysisResultDetailsElementResults;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnalysisResultDetailsMapped = {
|
|
||||||
id: number;
|
|
||||||
order: {
|
|
||||||
status: string;
|
|
||||||
medusaOrderId: string;
|
|
||||||
createdAt: Date | string;
|
|
||||||
};
|
|
||||||
elements: {
|
|
||||||
id: string;
|
|
||||||
unit: string;
|
|
||||||
norm_lower: number;
|
|
||||||
norm_upper: number;
|
|
||||||
norm_status: number;
|
|
||||||
analysis_name: string;
|
|
||||||
response_time: string;
|
|
||||||
response_value: number;
|
|
||||||
norm_lower_included: boolean;
|
|
||||||
norm_upper_included: boolean;
|
|
||||||
status: string;
|
|
||||||
analysis_element_original_id: string;
|
|
||||||
}[];
|
|
||||||
orderedAnalysisElementIds: number[];
|
|
||||||
orderedAnalysisElements: AnalysisResultDetailsElement[];
|
|
||||||
summary: {
|
|
||||||
id: number;
|
|
||||||
status: string;
|
|
||||||
user_id: string;
|
|
||||||
created_at: Date;
|
|
||||||
created_by: string;
|
|
||||||
value?: string;
|
|
||||||
} | null;
|
|
||||||
};
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
import type { UuringElement, UuringuVastus } from '@kit/shared/types/medipost-analysis';
|
import { toArray } from '@kit/shared/utils';
|
||||||
|
import type { UuringuVastus } from '@kit/shared/types/medipost-analysis';
|
||||||
|
|
||||||
import type { AnalysisResultDetails, AnalysisResultDetailsMapped, UserAnalysis } from '../types/analysis-results';
|
import type { AnalysisResultsQuery, AnalysisResultDetailsElement, AnalysisResultDetailsMapped, AnalysisResultLevel, AnalysisResultsDetailsElementNested, AnalysisStatus, UserAnalysis } from '../types/analysis-results';
|
||||||
import type { AnalysisOrder } from '../types/analysis-orders';
|
import type { AnalysisOrder } from '../types/analysis-orders';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,31 +44,13 @@ class UserAnalysesApi {
|
|||||||
async getUserAnalysis(
|
async getUserAnalysis(
|
||||||
analysisOrderId: number,
|
analysisOrderId: number,
|
||||||
): Promise<AnalysisResultDetailsMapped | null> {
|
): Promise<AnalysisResultDetailsMapped | null> {
|
||||||
const authUser = await this.client.auth.getUser();
|
|
||||||
const { data, error: userError } = authUser;
|
|
||||||
|
|
||||||
if (userError) {
|
|
||||||
console.error('Failed to get user', userError);
|
|
||||||
throw userError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { user } = data;
|
|
||||||
|
|
||||||
const analysisOrder = await this.getAnalysisOrder({ analysisOrderId });
|
const analysisOrder = await this.getAnalysisOrder({ analysisOrderId });
|
||||||
const orderedAnalysisElementIds = analysisOrder.analysis_element_ids ?? [];
|
const orderedAnalysisElementIds = analysisOrder.analysis_element_ids ?? [];
|
||||||
if (orderedAnalysisElementIds.length === 0) {
|
if (orderedAnalysisElementIds.length === 0) {
|
||||||
console.error('No ordered analysis element ids found for analysis order id=', analysisOrderId);
|
console.error('No ordered analysis element ids found for analysis order id=', analysisOrderId);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { data: orderedAnalysisElements, error: orderedAnalysisElementsError } = await this.client
|
const orderedAnalysisElements = await this.getOrderedAnalysisElements({ analysisOrderId, orderedAnalysisElementIds });
|
||||||
.schema('medreport')
|
|
||||||
.from('analysis_elements')
|
|
||||||
.select('analysis_id_original,analysis_name_lab')
|
|
||||||
.in('id', orderedAnalysisElementIds);
|
|
||||||
if (orderedAnalysisElementsError) {
|
|
||||||
console.error('Failed to get ordered analysis elements for analysis order id=', analysisOrderId, orderedAnalysisElementsError);
|
|
||||||
throw orderedAnalysisElementsError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderedAnalysisElementOriginalIds = orderedAnalysisElements.map(({ analysis_id_original }) => analysis_id_original);
|
const orderedAnalysisElementOriginalIds = orderedAnalysisElements.map(({ analysis_id_original }) => analysis_id_original);
|
||||||
if (orderedAnalysisElementOriginalIds.length === 0) {
|
if (orderedAnalysisElementOriginalIds.length === 0) {
|
||||||
@@ -75,6 +58,43 @@ class UserAnalysesApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const responseWithElements = await this.getAnalysisResponseWithElements({ analysisOrderId });
|
||||||
|
if (!responseWithElements) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedOrderedAnalysisElements = await this.getMappedOrderedAnalysisElements({
|
||||||
|
analysisResponseElements: responseWithElements.elements,
|
||||||
|
orderedAnalysisElements,
|
||||||
|
});
|
||||||
|
|
||||||
|
const feedback = responseWithElements.summary?.doctor_analysis_feedback?.[0];
|
||||||
|
return {
|
||||||
|
id: analysisOrderId,
|
||||||
|
order: {
|
||||||
|
status: analysisOrder.status,
|
||||||
|
medusaOrderId: analysisOrder.medusa_order_id,
|
||||||
|
createdAt: analysisOrder.created_at,
|
||||||
|
},
|
||||||
|
orderedAnalysisElements: mappedOrderedAnalysisElements,
|
||||||
|
summary:
|
||||||
|
feedback?.status === 'COMPLETED'
|
||||||
|
? (responseWithElements.summary?.doctor_analysis_feedback?.[0] ?? null)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAnalysisResponseWithElements({
|
||||||
|
analysisOrderId,
|
||||||
|
}: {
|
||||||
|
analysisOrderId: number;
|
||||||
|
}) {
|
||||||
|
const { data, error: userError } = await this.client.auth.getUser();
|
||||||
|
if (userError) {
|
||||||
|
throw userError;
|
||||||
|
}
|
||||||
|
const { user } = data;
|
||||||
|
|
||||||
const { data: analysisResponse } = await this.client
|
const { data: analysisResponse } = await this.client
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('analysis_responses')
|
.from('analysis_responses')
|
||||||
@@ -87,29 +107,52 @@ class UserAnalysesApi {
|
|||||||
.eq('analysis_order_id', analysisOrderId)
|
.eq('analysis_order_id', analysisOrderId)
|
||||||
.throwOnError();
|
.throwOnError();
|
||||||
|
|
||||||
const responseWithElements = analysisResponse?.[0] as AnalysisResultDetails | null;
|
return analysisResponse?.[0] as AnalysisResultsQuery | null;
|
||||||
if (!responseWithElements) {
|
}
|
||||||
return null;
|
|
||||||
|
async getOrderedAnalysisElements({
|
||||||
|
analysisOrderId,
|
||||||
|
orderedAnalysisElementIds,
|
||||||
|
}: {
|
||||||
|
analysisOrderId: number;
|
||||||
|
orderedAnalysisElementIds: number[];
|
||||||
|
}) {
|
||||||
|
const { data: orderedAnalysisElements, error: orderedAnalysisElementsError } = await this.client
|
||||||
|
.schema('medreport')
|
||||||
|
.from('analysis_elements')
|
||||||
|
.select('analysis_id_original,analysis_name_lab')
|
||||||
|
.in('id', orderedAnalysisElementIds);
|
||||||
|
if (orderedAnalysisElementsError) {
|
||||||
|
console.error(`Failed to get ordered analysis elements for analysis order id=${analysisOrderId}`, orderedAnalysisElementsError);
|
||||||
|
throw orderedAnalysisElementsError;
|
||||||
}
|
}
|
||||||
|
return orderedAnalysisElements;
|
||||||
|
}
|
||||||
|
|
||||||
const analysisResponseElements = responseWithElements.elements;
|
async getMappedOrderedAnalysisElements({
|
||||||
|
analysisResponseElements,
|
||||||
const feedback = responseWithElements.summary?.doctor_analysis_feedback?.[0];
|
orderedAnalysisElements,
|
||||||
|
}: {
|
||||||
|
analysisResponseElements: AnalysisResultsQuery['elements'];
|
||||||
|
orderedAnalysisElements: { analysis_id_original: string; analysis_name_lab: string }[];
|
||||||
|
}): Promise<AnalysisResultDetailsElement[]> {
|
||||||
const mappedOrderedAnalysisElements = orderedAnalysisElements.map(({ analysis_id_original, analysis_name_lab }) => {
|
const mappedOrderedAnalysisElements = orderedAnalysisElements.map(({ analysis_id_original, analysis_name_lab }) => {
|
||||||
return this.getOrderedAnalysisElements({
|
return this.getOrderedAnalysisElement({
|
||||||
analysisIdOriginal: analysis_id_original,
|
analysisIdOriginal: analysis_id_original,
|
||||||
analysisNameLab: analysis_name_lab,
|
analysisNameLab: analysis_name_lab,
|
||||||
analysisResponseElements,
|
analysisResponseElements,
|
||||||
});
|
});
|
||||||
}).sort((a, b) => a.analysisName.localeCompare(b.analysisName));
|
}).sort((a, b) => a.analysisName.localeCompare(b.analysisName));
|
||||||
const nestedAnalysisElementIds = mappedOrderedAnalysisElements.map(({ results }) => results?.nestedElements.map(({ analysisElementOriginalId }) => analysisElementOriginalId)).flat().filter(Boolean);
|
|
||||||
|
const nestedAnalysisElementIds = mappedOrderedAnalysisElements
|
||||||
|
.map(({ results }) => results?.nestedElements.map(({ analysisElementOriginalId }) => analysisElementOriginalId))
|
||||||
|
.flat().filter(Boolean);
|
||||||
if (nestedAnalysisElementIds.length > 0) {
|
if (nestedAnalysisElementIds.length > 0) {
|
||||||
const { data: nestedAnalysisElements, error: nestedAnalysisElementsError } = await this.client
|
const { data: nestedAnalysisElements, error: nestedAnalysisElementsError } = await this.client
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('analysis_elements')
|
.from('analysis_elements')
|
||||||
.select('*')
|
.select('*')
|
||||||
.in('id', nestedAnalysisElementIds);
|
.in('analysis_id_original', nestedAnalysisElementIds);
|
||||||
if (!nestedAnalysisElementsError && nestedAnalysisElements) {
|
if (!nestedAnalysisElementsError && nestedAnalysisElements) {
|
||||||
for (const mappedOrderedAnalysisElement of mappedOrderedAnalysisElements) {
|
for (const mappedOrderedAnalysisElement of mappedOrderedAnalysisElements) {
|
||||||
const { results } = mappedOrderedAnalysisElement;
|
const { results } = mappedOrderedAnalysisElement;
|
||||||
@@ -118,58 +161,33 @@ class UserAnalysesApi {
|
|||||||
}
|
}
|
||||||
for (const nestedElement of results.nestedElements) {
|
for (const nestedElement of results.nestedElements) {
|
||||||
const { analysisElementOriginalId } = nestedElement;
|
const { analysisElementOriginalId } = nestedElement;
|
||||||
const nestedAnalysisElement = nestedAnalysisElements.find(({ id }) => id === analysisElementOriginalId);
|
const nestedAnalysisElement = nestedAnalysisElements.find(({ analysis_id_original }) => analysis_id_original === analysisElementOriginalId);
|
||||||
if (!nestedAnalysisElement) {
|
if (!nestedAnalysisElement) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
results.nestedElements.push({
|
|
||||||
...nestedAnalysisElement,
|
|
||||||
analysisElementOriginalId,
|
|
||||||
analysisName: nestedAnalysisElement.analysis_name_lab,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mappedOrderedAnalysisElements.forEach(({ results }) => {
|
nestedElement.analysisElementOriginalId = analysisElementOriginalId;
|
||||||
results?.nestedElements.forEach(({ analysisElementOriginalId }) => {
|
nestedElement.analysisName = nestedAnalysisElement.analysis_name_lab as string | undefined;
|
||||||
const nestedAnalysisElement = nestedAnalysisElements.find(({ id }) => id === analysisElementOriginalId);
|
}
|
||||||
if (nestedAnalysisElement) {
|
results.nestedElements = results.nestedElements.sort((a, b) => a.analysisName?.localeCompare(b.analysisName ?? '') ?? 0);
|
||||||
results?.nestedElements.push({
|
}
|
||||||
...nestedAnalysisElement,
|
} else {
|
||||||
analysisElementOriginalId,
|
console.error('Failed to get nested analysis elements by ids=', nestedAnalysisElementIds, nestedAnalysisElementsError);
|
||||||
analysisName: nestedAnalysisElement.analysis_name_lab,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return mappedOrderedAnalysisElements;
|
||||||
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({
|
getOrderedAnalysisElement({
|
||||||
analysisIdOriginal,
|
analysisIdOriginal,
|
||||||
analysisNameLab,
|
analysisNameLab,
|
||||||
analysisResponseElements,
|
analysisResponseElements,
|
||||||
}: {
|
}: {
|
||||||
analysisIdOriginal: string;
|
analysisIdOriginal: string;
|
||||||
analysisNameLab: string;
|
analysisNameLab: string;
|
||||||
analysisResponseElements: AnalysisResultDetails['elements'];
|
analysisResponseElements: AnalysisResultsQuery['elements'];
|
||||||
}) {
|
}): AnalysisResultDetailsElement {
|
||||||
const elementResponse = analysisResponseElements.find((element) => element.analysis_element_original_id === analysisIdOriginal);
|
const elementResponse = analysisResponseElements.find((element) => element.analysis_element_original_id === analysisIdOriginal);
|
||||||
if (!elementResponse) {
|
if (!elementResponse) {
|
||||||
return {
|
return {
|
||||||
@@ -184,51 +202,74 @@ class UserAnalysesApi {
|
|||||||
isWaitingForResults: false,
|
isWaitingForResults: false,
|
||||||
analysisName: analysisNameLab,
|
analysisName: analysisNameLab,
|
||||||
results: {
|
results: {
|
||||||
nestedElements: (() => {
|
nestedElements: ((): AnalysisResultsDetailsElementNested[] => {
|
||||||
const nestedElements = elementResponse.original_response_element?.UuringuElement as UuringElement[] | undefined;
|
const nestedElements = toArray(elementResponse.original_response_element?.UuringuElement)
|
||||||
if (!nestedElements) {
|
return nestedElements.map<AnalysisResultsDetailsElementNested>((element) => {
|
||||||
return [];
|
const mappedResponse = this.mapUuringVastus({
|
||||||
}
|
uuringVastus: element.UuringuVastus as UuringuVastus | undefined,
|
||||||
return nestedElements.map((element) => {
|
});
|
||||||
const elementVastus = element.UuringuVastus as UuringuVastus | undefined;
|
|
||||||
const responseValue = elementVastus?.VastuseVaartus;
|
|
||||||
const responseValueIsNumeric = !isNaN(Number(responseValue));
|
|
||||||
const responseValueIsNegative = responseValue === 'Negatiivne';
|
|
||||||
const responseValueIsWithinNorm = responseValue === 'Normi piires';
|
|
||||||
return {
|
return {
|
||||||
status: element.UuringOlek,
|
unit: element.Mootyhik ?? null,
|
||||||
unit: element.Mootyhik,
|
normLower: mappedResponse.normLower,
|
||||||
normLower: elementVastus?.NormAlum?.['#text'],
|
normUpper: mappedResponse.normUpper,
|
||||||
normUpper: elementVastus?.NormYlem?.['#text'],
|
normStatus: mappedResponse.normStatus,
|
||||||
normStatus: elementVastus?.NormiStaatus,
|
responseTime: mappedResponse.responseTime,
|
||||||
responseTime: elementVastus?.VastuseAeg,
|
responseValue: mappedResponse.responseValue,
|
||||||
response_value: responseValueIsNegative || !responseValueIsNumeric ? null : (responseValue ?? null),
|
responseValueIsNegative: mappedResponse.responseValueIsNegative,
|
||||||
response_value_is_negative: responseValueIsNumeric ? null : responseValueIsNegative,
|
responseValueIsWithinNorm: mappedResponse.responseValueIsWithinNorm,
|
||||||
response_value_is_within_norm: responseValueIsNumeric ? null : responseValueIsWithinNorm,
|
normLowerIncluded: mappedResponse.normLowerIncluded,
|
||||||
normLowerIncluded: elementVastus?.NormAlum?.['@_kaasaarvatud'] === 'JAH',
|
normUpperIncluded: mappedResponse.normUpperIncluded,
|
||||||
normUpperIncluded: elementVastus?.NormYlem?.['@_kaasaarvatud'] === 'JAH',
|
|
||||||
analysisElementOriginalId: element.UuringId,
|
analysisElementOriginalId: element.UuringId,
|
||||||
|
status: Number(elementResponse.status) as AnalysisStatus,
|
||||||
|
analysisName: undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
})(),
|
})(),
|
||||||
labComment,
|
labComment,
|
||||||
//originalResponseElement: elementResponse.original_response_element ?? null,
|
|
||||||
unit: elementResponse.unit,
|
unit: elementResponse.unit,
|
||||||
normLower: elementResponse.norm_lower,
|
normLower: elementResponse.norm_lower,
|
||||||
normUpper: elementResponse.norm_upper,
|
normUpper: elementResponse.norm_upper,
|
||||||
normStatus: elementResponse.norm_status,
|
normStatus: elementResponse.norm_status,
|
||||||
responseTime: elementResponse.response_time,
|
responseTime: elementResponse.response_time,
|
||||||
responseValue: elementResponse.response_value,
|
responseValue: elementResponse.response_value,
|
||||||
responseValueIsNegative: elementResponse.response_value_is_negative === true,
|
responseValueIsNegative: elementResponse.response_value_is_negative === null ? null : elementResponse.response_value_is_negative === true,
|
||||||
responseValueIsWithinNorm: elementResponse.response_value_is_within_norm === true,
|
responseValueIsWithinNorm: elementResponse.response_value_is_within_norm === null ? null : elementResponse.response_value_is_within_norm === true,
|
||||||
normLowerIncluded: elementResponse.norm_lower_included,
|
normLowerIncluded: elementResponse.norm_lower_included,
|
||||||
normUpperIncluded: elementResponse.norm_upper_included,
|
normUpperIncluded: elementResponse.norm_upper_included,
|
||||||
status: elementResponse.status,
|
status: Number(elementResponse.status) as AnalysisStatus,
|
||||||
analysisElementOriginalId: elementResponse.analysis_element_original_id,
|
analysisElementOriginalId: elementResponse.analysis_element_original_id,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mapUuringVastus({ uuringVastus }: { uuringVastus?: UuringuVastus }) {
|
||||||
|
const vastuseVaartus = uuringVastus?.VastuseVaartus;
|
||||||
|
const responseValue = (() => {
|
||||||
|
const valueAsNumber = Number(vastuseVaartus);
|
||||||
|
if (isNaN(valueAsNumber)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return valueAsNumber;
|
||||||
|
})();
|
||||||
|
const responseValueNumber = Number(responseValue);
|
||||||
|
const responseValueIsNumeric = !isNaN(responseValueNumber);
|
||||||
|
const responseValueIsNegative = vastuseVaartus === 'Negatiivne';
|
||||||
|
const responseValueIsWithinNorm = vastuseVaartus === 'Normi piires';
|
||||||
|
return {
|
||||||
|
normLower: uuringVastus?.NormAlum?.['#text'] ?? null,
|
||||||
|
normUpper: uuringVastus?.NormYlem?.['#text'] ?? null,
|
||||||
|
normStatus: (uuringVastus?.NormiStaatus ?? null) as AnalysisResultLevel | null,
|
||||||
|
responseTime: uuringVastus?.VastuseAeg ?? null,
|
||||||
|
responseValue: responseValueIsNegative || !responseValueIsNumeric ? null : (responseValueNumber ?? null),
|
||||||
|
responseValueIsNegative: responseValueIsNumeric ? null : responseValueIsNegative,
|
||||||
|
responseValueIsWithinNorm: responseValueIsNumeric ? null : responseValueIsWithinNorm,
|
||||||
|
normLowerIncluded:
|
||||||
|
uuringVastus?.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah',
|
||||||
|
normUpperIncluded:
|
||||||
|
uuringVastus?.NormYlem?.['@_kaasaarvatud'].toLowerCase() === 'jah',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// @TODO unused currently
|
// @TODO unused currently
|
||||||
async getUserAnalyses(): Promise<UserAnalysis | null> {
|
async getUserAnalyses(): Promise<UserAnalysis | null> {
|
||||||
const authUser = await this.client.auth.getUser();
|
const authUser = await this.client.auth.getUser();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as z from 'zod';
|
import type { Database } from '@kit/supabase/database';
|
||||||
|
import type { AnalysisOrderStatus, NormStatus } from '@kit/shared/types/medipost-analysis';
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
import type { AnalysisOrder } from './analysis-orders';
|
||||||
|
|
||||||
export type UserAnalysisElement =
|
export type UserAnalysisElement =
|
||||||
Database['medreport']['Tables']['analysis_response_elements']['Row'];
|
Database['medreport']['Tables']['analysis_response_elements']['Row'];
|
||||||
@@ -10,92 +11,131 @@ export type UserAnalysisResponse =
|
|||||||
};
|
};
|
||||||
export type UserAnalysis = UserAnalysisResponse[];
|
export type UserAnalysis = UserAnalysisResponse[];
|
||||||
|
|
||||||
const ElementSchema = z.object({
|
export type AnalysisResultsQuery = {
|
||||||
unit: z.string(),
|
id: number,
|
||||||
norm_lower: z.number(),
|
analysis_order_id: number,
|
||||||
norm_upper: z.number(),
|
order_number: string,
|
||||||
norm_status: z.number(),
|
order_status: string,
|
||||||
analysis_name: z.string(),
|
user_id: string,
|
||||||
response_time: z.string(),
|
created_at: string,
|
||||||
response_value: z.number(),
|
updated_at: string | null,
|
||||||
response_value_is_negative: z.boolean(),
|
elements: {
|
||||||
response_value_is_within_norm: z.boolean(),
|
unit: string,
|
||||||
norm_lower_included: z.boolean(),
|
norm_lower: number,
|
||||||
norm_upper_included: z.boolean(),
|
norm_upper: number,
|
||||||
status: z.string(),
|
norm_status: number,
|
||||||
analysis_element_original_id: z.string(),
|
analysis_name: string,
|
||||||
original_response_element: z.object({
|
response_time: string,
|
||||||
|
response_value: number,
|
||||||
|
response_value_is_negative: boolean,
|
||||||
|
response_value_is_within_norm: boolean,
|
||||||
|
norm_lower_included: boolean,
|
||||||
|
norm_upper_included: boolean,
|
||||||
|
status: string,
|
||||||
|
analysis_element_original_id: string,
|
||||||
|
original_response_element: {
|
||||||
|
UuringuElement: {
|
||||||
|
UuringIdOID: string,
|
||||||
|
UuringId: string,
|
||||||
|
TLyhend: string,
|
||||||
|
KNimetus: string,
|
||||||
|
UuringNimi: string,
|
||||||
|
UuringuKommentaar: string | null,
|
||||||
|
TellijaUuringId: number,
|
||||||
|
TeostajaUuringId: string,
|
||||||
|
UuringOlek: keyof typeof AnalysisOrderStatus,
|
||||||
|
Mootyhik: string | null,
|
||||||
|
Kood: {
|
||||||
|
HkKood: number,
|
||||||
|
HkKoodiKordaja: number,
|
||||||
|
Koefitsient: number,
|
||||||
|
Hind: number,
|
||||||
|
},
|
||||||
|
UuringuVastus: {
|
||||||
|
VastuseVaartus: string,
|
||||||
|
VastuseAeg: string,
|
||||||
|
NormiStaatus: keyof typeof NormStatus,
|
||||||
|
ProoviJarjenumber: number,
|
||||||
|
},
|
||||||
|
UuringuTaitjaAsutuseJnr: number,
|
||||||
|
},
|
||||||
|
UuringuKommentaar: string | null,
|
||||||
|
},
|
||||||
|
}[],
|
||||||
|
order: {
|
||||||
|
status: string,
|
||||||
|
medusa_order_id: string,
|
||||||
|
created_at: string,
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
id: number,
|
||||||
|
value: string,
|
||||||
|
status: string,
|
||||||
|
user_id: string,
|
||||||
|
created_at: string,
|
||||||
|
created_by: string,
|
||||||
|
updated_at: string | null,
|
||||||
|
updated_by: string,
|
||||||
|
doctor_user_id: string | null,
|
||||||
|
analysis_order_id: number,
|
||||||
|
doctor_analysis_feedback: {
|
||||||
|
id: number,
|
||||||
|
status: string,
|
||||||
|
user_id: string,
|
||||||
|
created_at: string,
|
||||||
|
created_by: string,
|
||||||
|
}[],
|
||||||
|
} | null,
|
||||||
|
};
|
||||||
|
|
||||||
}),
|
export type AnalysisResultsDetailsElementNested = {
|
||||||
});
|
analysisElementOriginalId: string;
|
||||||
|
analysisName?: string;
|
||||||
|
} & Pick<
|
||||||
|
AnalysisResultDetailsElementResults,
|
||||||
|
'unit' |
|
||||||
|
'normLower' |
|
||||||
|
'normUpper' |
|
||||||
|
'normStatus' |
|
||||||
|
'responseTime' |
|
||||||
|
'responseValue' |
|
||||||
|
'responseValueIsNegative' |
|
||||||
|
'responseValueIsWithinNorm' |
|
||||||
|
'normLowerIncluded' |
|
||||||
|
'normUpperIncluded' |
|
||||||
|
'status' |
|
||||||
|
'analysisElementOriginalId'
|
||||||
|
>;
|
||||||
|
|
||||||
const OrderSchema = z.object({
|
export enum AnalysisResultLevel {
|
||||||
status: z.string(),
|
NORMAL = 0,
|
||||||
medusa_order_id: z.string(),
|
WARNING = 1,
|
||||||
created_at: z.coerce.date(),
|
CRITICAL = 2,
|
||||||
});
|
}
|
||||||
|
|
||||||
const DoctorAnalysisFeedbackSchema = z.object({
|
export enum AnalysisStatus {
|
||||||
id: z.number(),
|
QUEUED = 1,
|
||||||
status: z.string(),
|
PENDING = 2,
|
||||||
user_id: z.string(),
|
ONGOING = 3,
|
||||||
created_at: z.coerce.date(),
|
COMPLETED = 4,
|
||||||
created_by: z.string(),
|
REFUSED = 5,
|
||||||
});
|
CANCELLED = 6,
|
||||||
|
}
|
||||||
const SummarySchema = z.object({
|
|
||||||
id: z.number(),
|
|
||||||
value: z.string(),
|
|
||||||
status: z.string(),
|
|
||||||
user_id: z.string(),
|
|
||||||
created_at: z.coerce.date(),
|
|
||||||
created_by: z.string(),
|
|
||||||
updated_at: z.coerce.date().nullable(),
|
|
||||||
updated_by: z.string(),
|
|
||||||
doctor_user_id: z.string().nullable(),
|
|
||||||
analysis_order_id: z.number(),
|
|
||||||
doctor_analysis_feedback: z.array(DoctorAnalysisFeedbackSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const AnalysisResultDetailsSchema = z.object({
|
|
||||||
id: z.number(),
|
|
||||||
analysis_order_id: z.number(),
|
|
||||||
order_number: z.string(),
|
|
||||||
order_status: z.string(),
|
|
||||||
user_id: z.string(),
|
|
||||||
created_at: z.coerce.date(),
|
|
||||||
updated_at: z.coerce.date().nullable(),
|
|
||||||
elements: z.array(ElementSchema),
|
|
||||||
order: OrderSchema,
|
|
||||||
summary: SummarySchema.nullable(),
|
|
||||||
});
|
|
||||||
export type AnalysisResultDetails = z.infer<typeof AnalysisResultDetailsSchema>;
|
|
||||||
|
|
||||||
export type AnalysisResultDetailsElementResults = {
|
export type AnalysisResultDetailsElementResults = {
|
||||||
unit: string | null;
|
unit: string | null;
|
||||||
normLower: number | null;
|
normLower: number | null;
|
||||||
normUpper: number | null;
|
normUpper: number | null;
|
||||||
normStatus: number | null;
|
normStatus: AnalysisResultLevel | null;
|
||||||
responseTime: string | null;
|
responseTime: string | null;
|
||||||
responseValue: number | null;
|
responseValue: number | null;
|
||||||
responseValueIsNegative: boolean | null;
|
responseValueIsNegative: boolean | null;
|
||||||
responseValueIsWithinNorm: boolean | null;
|
responseValueIsWithinNorm: boolean | null;
|
||||||
normLowerIncluded: boolean;
|
normLowerIncluded: boolean;
|
||||||
normUpperIncluded: boolean;
|
normUpperIncluded: boolean;
|
||||||
status: string;
|
status: AnalysisStatus;
|
||||||
analysisElementOriginalId: string;
|
analysisElementOriginalId: string;
|
||||||
nestedElements: {
|
nestedElements: AnalysisResultsDetailsElementNested[];
|
||||||
analysisElementOriginalId: string;
|
|
||||||
normLower?: number | null;
|
|
||||||
normLowerIncluded: boolean;
|
|
||||||
normStatus: number;
|
|
||||||
normUpper?: number | null;
|
|
||||||
normUpperIncluded: boolean;
|
|
||||||
responseTime: string;
|
|
||||||
responseValue: number;
|
|
||||||
status: number;
|
|
||||||
unit: string;
|
|
||||||
}[];
|
|
||||||
labComment?: string | null;
|
labComment?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,37 +143,21 @@ export type AnalysisResultDetailsElement = {
|
|||||||
analysisIdOriginal: string;
|
analysisIdOriginal: string;
|
||||||
isWaitingForResults: boolean;
|
isWaitingForResults: boolean;
|
||||||
analysisName: string;
|
analysisName: string;
|
||||||
results: AnalysisResultDetailsElementResults;
|
results?: AnalysisResultDetailsElementResults;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AnalysisResultDetailsMapped = {
|
export type AnalysisResultDetailsMapped = {
|
||||||
id: number;
|
id: number;
|
||||||
order: {
|
order: {
|
||||||
status: string;
|
|
||||||
medusaOrderId: string;
|
medusaOrderId: string;
|
||||||
createdAt: Date | string;
|
createdAt: string;
|
||||||
};
|
} & Pick<AnalysisOrder, 'status'>;
|
||||||
elements: {
|
|
||||||
id: string;
|
|
||||||
unit: string;
|
|
||||||
norm_lower: number;
|
|
||||||
norm_upper: number;
|
|
||||||
norm_status: number;
|
|
||||||
analysis_name: string;
|
|
||||||
response_time: string;
|
|
||||||
response_value: number;
|
|
||||||
norm_lower_included: boolean;
|
|
||||||
norm_upper_included: boolean;
|
|
||||||
status: string;
|
|
||||||
analysis_element_original_id: string;
|
|
||||||
}[];
|
|
||||||
orderedAnalysisElementIds: number[];
|
|
||||||
orderedAnalysisElements: AnalysisResultDetailsElement[];
|
orderedAnalysisElements: AnalysisResultDetailsElement[];
|
||||||
summary: {
|
summary: {
|
||||||
id: number;
|
id: number;
|
||||||
status: string;
|
status: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
created_at: Date;
|
created_at: string;
|
||||||
created_by: string;
|
created_by: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { MonitoringService } from '@kit/monitoring-core';
|
|||||||
|
|
||||||
export class ConsoleMonitoringService implements MonitoringService {
|
export class ConsoleMonitoringService implements MonitoringService {
|
||||||
identifyUser(data: { id: string }) {
|
identifyUser(data: { id: string }) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log(`[Console Monitoring] Identified user`, data);
|
console.log(`[Console Monitoring] Identified user`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,6 +15,9 @@ export class ConsoleMonitoringService implements MonitoringService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
captureEvent(event: string) {
|
captureEvent(event: string) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log(`[Console Monitoring] Captured event: ${event}`);
|
console.log(`[Console Monitoring] Captured event: ${event}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,3 +57,8 @@ export const getPersonParameters = (personalCode: string) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function toArray<T>(input?: T | T[] | null): T[] {
|
||||||
|
if (!input) return [];
|
||||||
|
return Array.isArray(input) ? input : [input];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1401,6 +1401,7 @@ export type Database = {
|
|||||||
medipost_private_message_id: string | null
|
medipost_private_message_id: string | null
|
||||||
medusa_order_id: string | null
|
medusa_order_id: string | null
|
||||||
response_xml: string | null
|
response_xml: string | null
|
||||||
|
updated_at: string | null
|
||||||
xml: string | null
|
xml: string | null
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
@@ -1413,6 +1414,7 @@ export type Database = {
|
|||||||
medipost_private_message_id?: string | null
|
medipost_private_message_id?: string | null
|
||||||
medusa_order_id?: string | null
|
medusa_order_id?: string | null
|
||||||
response_xml?: string | null
|
response_xml?: string | null
|
||||||
|
updated_at?: string | null
|
||||||
xml?: string | null
|
xml?: string | null
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
@@ -1425,6 +1427,7 @@ export type Database = {
|
|||||||
medipost_private_message_id?: string | null
|
medipost_private_message_id?: string | null
|
||||||
medusa_order_id?: string | null
|
medusa_order_id?: string | null
|
||||||
response_xml?: string | null
|
response_xml?: string | null
|
||||||
|
updated_at?: string | null
|
||||||
xml?: string | null
|
xml?: string | null
|
||||||
}
|
}
|
||||||
Relationships: []
|
Relationships: []
|
||||||
|
|||||||
@@ -158,11 +158,11 @@ export function PageHeader({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-between py-5',
|
'flex py-5 flex-col sm:flex-row items-start sm:items-center sm:justify-between',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={'flex flex-col gap-y-2'}>
|
<div className={'flex flex-col gap-y-4 sm:gap-y-2'}>
|
||||||
<If condition={title}>
|
<If condition={title}>
|
||||||
<PageTitle>{title}</PageTitle>
|
<PageTitle>{title}</PageTitle>
|
||||||
</If>
|
</If>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"pageTitle": "My analysis results",
|
"pageTitle": "My analysis results",
|
||||||
"description": "All analysis results will appear here within 1-3 business days after they have been done.",
|
"description": "",
|
||||||
|
"descriptionPartial": "All analysis results will appear here within 1-3 business days after they have been done.",
|
||||||
"descriptionEmpty": "If you've already done your analysis, your results will appear here soon.",
|
"descriptionEmpty": "If you've already done your analysis, your results will appear here soon.",
|
||||||
"orderNewAnalysis": "Order new analyses",
|
"orderNewAnalysis": "Order new analyses",
|
||||||
"waitingForResults": "Waiting for results",
|
"waitingForResults": "Waiting for results",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"pageTitle": "Minu analüüside vastused",
|
"pageTitle": "Minu analüüside vastused",
|
||||||
"description": "Kõikide analüüside tulemused ilmuvad 1-3 tööpäeva jooksul peale nende andmist.",
|
"description": "",
|
||||||
|
"descriptionPartial": "Kõikide analüüside tulemused ilmuvad 1-3 tööpäeva jooksul peale nende andmist.",
|
||||||
"descriptionEmpty": "Kui oled juba käinud analüüse andmas, siis varsti jõuavad siia sinu analüüside vastused.",
|
"descriptionEmpty": "Kui oled juba käinud analüüse andmas, siis varsti jõuavad siia sinu analüüside vastused.",
|
||||||
"orderNewAnalysis": "Telli uued analüüsid",
|
"orderNewAnalysis": "Telli uued analüüsid",
|
||||||
"waitingForResults": "Tulemuse ootel",
|
"waitingForResults": "Tulemuse ootel",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"pageTitle": "Мои результаты анализов",
|
"pageTitle": "Мои результаты анализов",
|
||||||
"description": "Все результаты анализов появляются в течение 1-3 рабочих дней после их сдачи.",
|
"description": "",
|
||||||
|
"descriptionPartial": "Все результаты анализов появляются в течение 1-3 рабочих дней после их сдачи.",
|
||||||
"descriptionEmpty": "Если вы уже сдали анализы, то вскоре здесь появятся ваши результаты.",
|
"descriptionEmpty": "Если вы уже сдали анализы, то вскоре здесь появятся ваши результаты.",
|
||||||
"orderNewAnalysis": "Заказать новые анализы",
|
"orderNewAnalysis": "Заказать новые анализы",
|
||||||
"waitingForResults": "Ожидание результатов",
|
"waitingForResults": "Ожидание результатов",
|
||||||
|
|||||||
@@ -75,11 +75,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
@apply text-base;
|
@apply text-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
h6 {
|
h6 {
|
||||||
@apply text-lg;
|
@apply text-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lucide {
|
.lucide {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- Add unique constraint on analysis_response_elements by analysis_response_id + analysis_element_original_id
|
||||||
|
CREATE UNIQUE INDEX analysis_response_elements_unique_by_response_and_element
|
||||||
|
ON "medreport"."analysis_response_elements"
|
||||||
|
USING btree (analysis_response_id, analysis_element_original_id);
|
||||||
|
|
||||||
|
ALTER TABLE "medreport"."analysis_response_elements"
|
||||||
|
ADD CONSTRAINT "analysis_response_elements_unique_by_response_and_element"
|
||||||
|
UNIQUE USING INDEX "analysis_response_elements_unique_by_response_and_element";
|
||||||
|
|
||||||
|
-- Add updated_at column to medipost_actions table
|
||||||
|
ALTER TABLE "medreport"."medipost_actions" ADD COLUMN "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now();
|
||||||
|
|
||||||
|
-- Add unique constraint on medipost_actions by medipost_private_message_id
|
||||||
|
-- Using partial index to allow multiple NULL values but enforce uniqueness for non-NULL values
|
||||||
|
CREATE UNIQUE INDEX medipost_actions_unique_by_private_message_id
|
||||||
|
ON "medreport"."medipost_actions"
|
||||||
|
USING btree (medipost_private_message_id)
|
||||||
|
WHERE medipost_private_message_id IS NOT NULL;
|
||||||
Reference in New Issue
Block a user