Add user analysis view bars + nested element logic to doctor view also
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
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';
|
||||
|
||||
@@ -10,23 +9,24 @@ import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { AnalysisElement } from '~/lib/services/analysis-element.service';
|
||||
import { NestedAnalysisElement } from '@/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema';
|
||||
|
||||
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 type AnalysisResultForDisplay = {
|
||||
norm_status?: number | null;
|
||||
response_value?: number | null;
|
||||
unit?: string | null;
|
||||
norm_lower_included?: boolean | null;
|
||||
norm_upper_included?: boolean | null;
|
||||
norm_lower?: number | null;
|
||||
norm_upper?: number | null;
|
||||
response_time?: string | null;
|
||||
nestedElements?: NestedAnalysisElement[];
|
||||
};
|
||||
|
||||
const AnalysisDoctor = ({
|
||||
analysisElement,
|
||||
@@ -72,6 +72,12 @@ const AnalysisDoctor = ({
|
||||
return `${normLower ?? '...'} - ${normUpper ?? '...'}`;
|
||||
}, [normLower, normUpper]);
|
||||
|
||||
const nestedElements = results?.nestedElements ?? null;
|
||||
const hasNestedElements =
|
||||
Array.isArray(nestedElements) && nestedElements.length > 0;
|
||||
|
||||
const isAnalysisLevelBarHidden = isCancelled || !results || hasNestedElements;
|
||||
|
||||
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">
|
||||
@@ -101,7 +107,7 @@ const AnalysisDoctor = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{results ? (
|
||||
{isAnalysisLevelBarHidden ? null : (
|
||||
<>
|
||||
<div className="flex items-center gap-3 sm:ml-auto">
|
||||
<div className="font-semibold">{value}</div>
|
||||
@@ -120,17 +126,33 @@ const AnalysisDoctor = ({
|
||||
/>
|
||||
{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 />
|
||||
</>
|
||||
)}
|
||||
{(() => {
|
||||
// If parent has nested elements, don't show anything
|
||||
if (hasNestedElements) {
|
||||
return null;
|
||||
}
|
||||
// If we're showing the level bar, don't show waiting
|
||||
if (!isAnalysisLevelBarHidden) {
|
||||
return null;
|
||||
}
|
||||
// If cancelled, don't show waiting
|
||||
if (isCancelled) {
|
||||
return null;
|
||||
}
|
||||
// Otherwise, show waiting for results
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -105,35 +105,38 @@ const AnalysisLevelBar = ({
|
||||
|
||||
// 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
|
||||
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) {
|
||||
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
|
||||
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));
|
||||
const belowPosition = Math.max(0, Math.min(1, value / lower!));
|
||||
return belowPosition * 100;
|
||||
}
|
||||
|
||||
// Both bounds exist
|
||||
if (lower !== null && upper !== null) {
|
||||
if (value < lower) {
|
||||
if (value < lower!) {
|
||||
return 0; // Below normal range
|
||||
}
|
||||
if (value > upper) {
|
||||
if (value > upper!) {
|
||||
return 100; // Above normal range
|
||||
}
|
||||
// Within normal range
|
||||
return ((value - lower) / (upper - lower)) * 100;
|
||||
return ((value - lower!) / (upper! - lower!)) * 100;
|
||||
}
|
||||
|
||||
return 50; // Fallback
|
||||
@@ -145,18 +148,10 @@ const AnalysisLevelBar = ({
|
||||
const isCritical = level === AnalysisResultLevel.CRITICAL;
|
||||
const isPending = level === null;
|
||||
|
||||
// If pending results, show gray bar
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="w-60% mt-4 flex h-3 max-w-[360px] gap-1 sm:mt-0 sm:w-[35%]">
|
||||
<Level color="gray-200" isFirst isLast />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show appropriate levels based on available norm bounds
|
||||
const hasLowerBound = lower !== null;
|
||||
|
||||
// Calculate level configuration (must be called before any returns)
|
||||
const [first, second, third] = useMemo(() => {
|
||||
const [warning, normal, critical] = [
|
||||
{
|
||||
@@ -196,6 +191,15 @@ const AnalysisLevelBar = ({
|
||||
hasLowerBound,
|
||||
]);
|
||||
|
||||
// If pending results, show gray bar
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="w-60% mt-4 flex h-3 max-w-[360px] gap-1 sm:mt-0 sm:w-[35%]">
|
||||
<Level color="gray-200" isFirst isLast />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { CaretDownIcon, QuestionMarkCircledIcon } from '@radix-ui/react-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -23,55 +25,25 @@ export default function DoctorAnalysisWrapper({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Collapsible className="w-full" key={analysisData.id}>
|
||||
<CollapsibleTrigger
|
||||
disabled={!analysisData.latestPreviousAnalysis}
|
||||
asChild
|
||||
>
|
||||
<div className="[&[data-state=open]_.caret-icon]:rotate-180">
|
||||
<AnalysisDoctor
|
||||
startIcon={
|
||||
analysisData.latestPreviousAnalysis && (
|
||||
<CaretDownIcon className="caret-icon transition-transform duration-200" />
|
||||
)
|
||||
}
|
||||
endIcon={
|
||||
analysisData.comment && (
|
||||
<>
|
||||
<div className="xs:flex hidden">
|
||||
<InfoTooltip
|
||||
content={analysisData.comment}
|
||||
icon={
|
||||
<QuestionMarkCircledIcon className="mx-2 text-blue-800" />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p className="xs:hidden">
|
||||
<strong>
|
||||
<Trans i18nKey="doctor:labComment" />:
|
||||
</strong>{' '}
|
||||
{analysisData.comment}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
analysisElement={{
|
||||
analysis_name_lab: analysisData.analysis_name,
|
||||
}}
|
||||
results={analysisData}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
{analysisData.latestPreviousAnalysis && (
|
||||
<CollapsibleContent>
|
||||
<div className="my-1 flex flex-col">
|
||||
<>
|
||||
<Collapsible className="w-full" key={analysisData.id}>
|
||||
<CollapsibleTrigger
|
||||
disabled={!analysisData.latestPreviousAnalysis}
|
||||
asChild
|
||||
>
|
||||
<div className="[&[data-state=open]_.caret-icon]:rotate-180">
|
||||
<AnalysisDoctor
|
||||
startIcon={
|
||||
analysisData.latestPreviousAnalysis && (
|
||||
<CaretDownIcon className="caret-icon transition-transform duration-200" />
|
||||
)
|
||||
}
|
||||
endIcon={
|
||||
analysisData.latestPreviousAnalysis.comment && (
|
||||
analysisData.comment && (
|
||||
<>
|
||||
<div className="xs:flex hidden">
|
||||
<InfoTooltip
|
||||
content={analysisData.latestPreviousAnalysis.comment}
|
||||
content={analysisData.comment}
|
||||
icon={
|
||||
<QuestionMarkCircledIcon className="mx-2 text-blue-800" />
|
||||
}
|
||||
@@ -79,25 +51,103 @@ export default function DoctorAnalysisWrapper({
|
||||
</div>
|
||||
<p className="xs:hidden">
|
||||
<strong>
|
||||
<Trans i18nKey="doctor:labComment" />:{' '}
|
||||
</strong>
|
||||
{analysisData.latestPreviousAnalysis.comment}
|
||||
<Trans i18nKey="doctor:labComment" />:
|
||||
</strong>{' '}
|
||||
{analysisData.comment}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
analysisElement={{
|
||||
analysis_name_lab: t('doctor:previousResults', {
|
||||
date: formatDate(
|
||||
analysisData.latestPreviousAnalysis.response_time,
|
||||
),
|
||||
}),
|
||||
analysis_name_lab: analysisData.analysis_name,
|
||||
}}
|
||||
results={analysisData.latestPreviousAnalysis}
|
||||
results={analysisData}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
)}
|
||||
</Collapsible>
|
||||
</CollapsibleTrigger>
|
||||
{analysisData.latestPreviousAnalysis && (
|
||||
<CollapsibleContent>
|
||||
<div className="my-1 flex flex-col gap-2">
|
||||
<AnalysisDoctor
|
||||
endIcon={
|
||||
analysisData.latestPreviousAnalysis.comment && (
|
||||
<>
|
||||
<div className="xs:flex hidden">
|
||||
<InfoTooltip
|
||||
content={analysisData.latestPreviousAnalysis.comment}
|
||||
icon={
|
||||
<QuestionMarkCircledIcon className="mx-2 text-blue-800" />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p className="xs:hidden">
|
||||
<strong>
|
||||
<Trans i18nKey="doctor:labComment" />:{' '}
|
||||
</strong>
|
||||
{analysisData.latestPreviousAnalysis.comment}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
analysisElement={{
|
||||
analysis_name_lab: t('doctor:previousResults', {
|
||||
date: formatDate(
|
||||
analysisData.latestPreviousAnalysis.response_time!,
|
||||
),
|
||||
}),
|
||||
}}
|
||||
results={analysisData.latestPreviousAnalysis}
|
||||
/>
|
||||
{analysisData.latestPreviousAnalysis.nestedElements?.map(
|
||||
(nestedElement, nestedIndex) => (
|
||||
<div
|
||||
key={`prev-nested-${nestedElement.analysisElementOriginalId}-${nestedIndex}`}
|
||||
className="ml-8"
|
||||
>
|
||||
<AnalysisDoctor
|
||||
analysisElement={{
|
||||
analysis_name_lab: nestedElement.analysisNameLab ?? '',
|
||||
}}
|
||||
results={{
|
||||
norm_status: nestedElement.normStatus,
|
||||
response_value: nestedElement.responseValue,
|
||||
unit: nestedElement.unit,
|
||||
norm_lower: nestedElement.normLower,
|
||||
norm_upper: nestedElement.normUpper,
|
||||
norm_lower_included: nestedElement.normLowerIncluded,
|
||||
norm_upper_included: nestedElement.normUpperIncluded,
|
||||
response_time: nestedElement.responseTime,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
)}
|
||||
</Collapsible>
|
||||
{analysisData.nestedElements?.map((nestedElement, nestedIndex) => (
|
||||
<div
|
||||
key={`nested-${nestedElement.analysisElementOriginalId}-${nestedIndex}`}
|
||||
className="ml-8"
|
||||
>
|
||||
<AnalysisDoctor
|
||||
analysisElement={{
|
||||
analysis_name_lab: nestedElement.analysisNameLab ?? '',
|
||||
}}
|
||||
results={{
|
||||
norm_status: nestedElement.normStatus,
|
||||
response_value: nestedElement.responseValue,
|
||||
unit: nestedElement.unit,
|
||||
norm_lower: nestedElement.normLower,
|
||||
norm_upper: nestedElement.normUpper,
|
||||
norm_lower_included: nestedElement.normLowerIncluded,
|
||||
norm_upper_included: nestedElement.normUpperIncluded,
|
||||
response_time: nestedElement.responseTime,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user