Update analysis level bars in doctor view based on user view
This commit is contained in:
@@ -28,12 +28,6 @@ export type AnalysisResultForDisplay = Pick<
|
|||||||
| 'response_time'
|
| 'response_time'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export enum AnalysisStatus {
|
|
||||||
NORMAL = 0,
|
|
||||||
MEDIUM = 1,
|
|
||||||
HIGH = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
const AnalysisDoctor = ({
|
const AnalysisDoctor = ({
|
||||||
analysisElement,
|
analysisElement,
|
||||||
results,
|
results,
|
||||||
@@ -48,38 +42,35 @@ const AnalysisDoctor = ({
|
|||||||
endIcon?: ReactNode | null;
|
endIcon?: ReactNode | null;
|
||||||
}) => {
|
}) => {
|
||||||
const name = analysisElement.analysis_name_lab || '';
|
const name = analysisElement.analysis_name_lab || '';
|
||||||
const status = results?.norm_status || AnalysisStatus.NORMAL;
|
const status = results?.norm_status;
|
||||||
const value = results?.response_value || 0;
|
const value = results?.response_value || 0;
|
||||||
const unit = results?.unit || '';
|
const unit = results?.unit || '';
|
||||||
const normLowerIncluded = results?.norm_lower_included || false;
|
const normLower = results?.norm_lower ?? null;
|
||||||
const normUpperIncluded = results?.norm_upper_included || false;
|
const normUpper = results?.norm_upper ?? null;
|
||||||
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(() => {
|
||||||
if (!results) {
|
if (!results || status === null || status === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUnderNorm = value < normLower;
|
|
||||||
if (isUnderNorm) {
|
|
||||||
switch (status) {
|
|
||||||
case AnalysisStatus.MEDIUM:
|
|
||||||
return AnalysisResultLevel.LOW;
|
|
||||||
default:
|
|
||||||
return AnalysisResultLevel.VERY_LOW;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case AnalysisStatus.MEDIUM:
|
case 1:
|
||||||
return AnalysisResultLevel.HIGH;
|
return AnalysisResultLevel.WARNING;
|
||||||
case AnalysisStatus.HIGH:
|
case 2:
|
||||||
return AnalysisResultLevel.VERY_HIGH;
|
return AnalysisResultLevel.CRITICAL;
|
||||||
|
case 0:
|
||||||
default:
|
default:
|
||||||
return AnalysisResultLevel.NORMAL;
|
return AnalysisResultLevel.NORMAL;
|
||||||
}
|
}
|
||||||
}, [results, value, normLower]);
|
}, [results, status]);
|
||||||
|
|
||||||
|
const normRangeText = useMemo(() => {
|
||||||
|
if (normLower === null && normUpper === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${normLower ?? '...'} - ${normUpper ?? '...'}`;
|
||||||
|
}, [normLower, normUpper]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-border rounded-lg border px-5">
|
<div className="border-border rounded-lg border px-5">
|
||||||
@@ -117,16 +108,15 @@ const AnalysisDoctor = ({
|
|||||||
<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">
|
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
|
||||||
{normLower} - {normUpper}
|
{normRangeText}
|
||||||
<div>
|
<div>
|
||||||
<Trans i18nKey="analysis-results:results.range.normal" />
|
<Trans i18nKey="analysis-results:results.range.normal" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AnalysisLevelBar
|
<AnalysisLevelBar
|
||||||
results={results}
|
results={results}
|
||||||
normLowerIncluded={normLowerIncluded}
|
level={analysisResultLevel}
|
||||||
normUpperIncluded={normUpperIncluded}
|
normRangeText={normRangeText}
|
||||||
level={analysisResultLevel!}
|
|
||||||
/>
|
/>
|
||||||
{endIcon || <div className="mx-2 w-4" />}
|
{endIcon || <div className="mx-2 w-4" />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -7,11 +7,9 @@ import { cn } from '@kit/ui/utils';
|
|||||||
import { AnalysisResultForDisplay } from './analysis-doctor';
|
import { AnalysisResultForDisplay } from './analysis-doctor';
|
||||||
|
|
||||||
export enum AnalysisResultLevel {
|
export enum AnalysisResultLevel {
|
||||||
VERY_LOW = 0,
|
NORMAL = 'NORMAL',
|
||||||
LOW = 1,
|
WARNING = 'WARNING',
|
||||||
NORMAL = 2,
|
CRITICAL = 'CRITICAL',
|
||||||
HIGH = 3,
|
|
||||||
VERY_HIGH = 4,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Level = ({
|
const Level = ({
|
||||||
@@ -20,17 +18,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,
|
||||||
})}
|
})}
|
||||||
@@ -38,11 +38,32 @@ 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(
|
||||||
|
'text-muted-foreground absolute bottom-[-18px] left-3/8 text-xs font-bold whitespace-nowrap',
|
||||||
|
{
|
||||||
|
'opacity-60': isActive,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{normRangeText}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -50,81 +71,144 @@ const Level = ({
|
|||||||
export const AnalysisLevelBarSkeleton = () => {
|
export const AnalysisLevelBarSkeleton = () => {
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex h-3 w-[60%] max-w-[360px] gap-1 sm:mt-0 sm:w-[35%]">
|
<div className="mt-4 flex h-3 w-[60%] max-w-[360px] gap-1 sm:mt-0 sm:w-[35%]">
|
||||||
<Level color="gray-200" />
|
<Level color="gray-200" isFirst isLast />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AnalysisLevelBar = ({
|
const AnalysisLevelBar = ({
|
||||||
normLowerIncluded = true,
|
|
||||||
normUpperIncluded = true,
|
|
||||||
level,
|
level,
|
||||||
results,
|
results,
|
||||||
|
normRangeText,
|
||||||
}: {
|
}: {
|
||||||
normLowerIncluded?: boolean;
|
level: AnalysisResultLevel | null;
|
||||||
normUpperIncluded?: boolean;
|
|
||||||
level: AnalysisResultLevel;
|
|
||||||
results: AnalysisResultForDisplay;
|
results: AnalysisResultForDisplay;
|
||||||
|
normRangeText: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
norm_lower: lower,
|
norm_lower: lower,
|
||||||
norm_upper: upper,
|
norm_upper: upper,
|
||||||
response_value: value,
|
response_value: value,
|
||||||
} = results;
|
} = results;
|
||||||
const arrowLocation = useMemo(() => {
|
|
||||||
if (value < lower!) {
|
|
||||||
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
|
||||||
() => [
|
const isNormal = level === AnalysisResultLevel.NORMAL;
|
||||||
level === AnalysisResultLevel.VERY_LOW,
|
const isWarning = level === AnalysisResultLevel.WARNING;
|
||||||
level === AnalysisResultLevel.LOW,
|
const isCritical = level === AnalysisResultLevel.CRITICAL;
|
||||||
level === AnalysisResultLevel.HIGH,
|
const isPending = level === null;
|
||||||
level === AnalysisResultLevel.VERY_HIGH,
|
|
||||||
],
|
|
||||||
[level, value, upper, lower],
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasAbnormalLevel = isVeryLow || isLow || isHigh || isVeryHigh;
|
// 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;
|
||||||
|
|
||||||
|
const [first, second, third] = useMemo(() => {
|
||||||
|
const [warning, normal, critical] = [
|
||||||
|
{
|
||||||
|
isActive: isWarning,
|
||||||
|
color: 'warning',
|
||||||
|
...(isWarning ? { arrowLocation } : {}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isActive: isNormal,
|
||||||
|
color: 'success',
|
||||||
|
normRangeText,
|
||||||
|
...(isNormal ? { arrowLocation } : {}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isActive: isCritical,
|
||||||
|
color: 'destructive',
|
||||||
|
isLast: true,
|
||||||
|
...(isCritical ? { arrowLocation } : {}),
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
if (!hasLowerBound) {
|
||||||
|
return [{ ...normal, isFirst: true }, warning, critical] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ ...warning, isFirst: true },
|
||||||
|
normal,
|
||||||
|
{ ...critical, isLast: true },
|
||||||
|
] as const;
|
||||||
|
}, [
|
||||||
|
arrowLocation,
|
||||||
|
normRangeText,
|
||||||
|
isNormal,
|
||||||
|
isWarning,
|
||||||
|
isCritical,
|
||||||
|
hasLowerBound,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex h-3 w-[60%] max-w-[360px] gap-1 sm:mt-0 sm:w-[35%]">
|
<div
|
||||||
{normLowerIncluded && (
|
className={cn(
|
||||||
<>
|
'flex h-3 gap-1',
|
||||||
<Level isActive={isVeryLow} color="destructive" isFirst />
|
'mt-4 sm:mt-0',
|
||||||
<Level isActive={isLow} color="warning" />
|
'w-[60%] sm:w-[35%]',
|
||||||
</>
|
'min-w-[50vw] sm:min-w-auto',
|
||||||
)}
|
'max-w-[360px]',
|
||||||
|
|
||||||
<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 />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<Level {...first} />
|
||||||
|
<Level {...second} />
|
||||||
|
<Level {...third} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user