Update analysis level bars in doctor view based on user view
This commit is contained in:
@@ -7,11 +7,9 @@ import { cn } from '@kit/ui/utils';
|
||||
import { AnalysisResultForDisplay } from './analysis-doctor';
|
||||
|
||||
export enum AnalysisResultLevel {
|
||||
VERY_LOW = 0,
|
||||
LOW = 1,
|
||||
NORMAL = 2,
|
||||
HIGH = 3,
|
||||
VERY_HIGH = 4,
|
||||
NORMAL = 'NORMAL',
|
||||
WARNING = 'WARNING',
|
||||
CRITICAL = 'CRITICAL',
|
||||
}
|
||||
|
||||
const Level = ({
|
||||
@@ -20,17 +18,19 @@ const Level = ({
|
||||
isFirst = false,
|
||||
isLast = false,
|
||||
arrowLocation,
|
||||
normRangeText,
|
||||
}: {
|
||||
isActive?: boolean;
|
||||
color: 'destructive' | 'success' | 'warning' | 'gray-200';
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
arrowLocation?: number;
|
||||
normRangeText?: string | null;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(`bg-${color} relative h-3 flex-1`, {
|
||||
'opacity-20': !isActive,
|
||||
'opacity-60': !isActive,
|
||||
'rounded-l-lg': isFirst,
|
||||
'rounded-r-lg': isLast,
|
||||
})}
|
||||
@@ -38,11 +38,32 @@ const Level = ({
|
||||
{isActive && (
|
||||
<div
|
||||
className="absolute top-[-14px] left-1/2 -translate-x-1/2 rounded-[10px] bg-white p-[2px]"
|
||||
style={{ left: `${arrowLocation}%` }}
|
||||
{...(arrowLocation
|
||||
? {
|
||||
style: {
|
||||
left: `${arrowLocation}%`,
|
||||
...(arrowLocation > 92.5 && { left: '92.5%' }),
|
||||
...(arrowLocation < 7.5 && { left: '7.5%' }),
|
||||
},
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<ArrowDown strokeWidth={2} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{color === 'success' && typeof normRangeText === 'string' && (
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground absolute bottom-[-18px] left-3/8 text-xs font-bold whitespace-nowrap',
|
||||
{
|
||||
'opacity-60': isActive,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{normRangeText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -50,81 +71,144 @@ const Level = ({
|
||||
export const AnalysisLevelBarSkeleton = () => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
const AnalysisLevelBar = ({
|
||||
normLowerIncluded = true,
|
||||
normUpperIncluded = true,
|
||||
level,
|
||||
results,
|
||||
normRangeText,
|
||||
}: {
|
||||
normLowerIncluded?: boolean;
|
||||
normUpperIncluded?: boolean;
|
||||
level: AnalysisResultLevel;
|
||||
level: AnalysisResultLevel | null;
|
||||
results: AnalysisResultForDisplay;
|
||||
normRangeText: string | null;
|
||||
}) => {
|
||||
const {
|
||||
norm_lower: lower,
|
||||
norm_upper: upper,
|
||||
response_value: value,
|
||||
} = 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;
|
||||
}
|
||||
|
||||
const calculated = ((value - lower!) / (upper! - lower!)) * 100;
|
||||
|
||||
if (calculated > 100) {
|
||||
return 100;
|
||||
// If no normal ranges defined, center the arrow
|
||||
if (lower === null && upper === null) {
|
||||
return 50;
|
||||
}
|
||||
|
||||
return calculated;
|
||||
// If only upper bound exists
|
||||
if (lower === null && upper !== null) {
|
||||
if (value <= upper) {
|
||||
return Math.min(75, (value / upper) * 75); // Show in left 75% of normal range
|
||||
}
|
||||
return 100; // Beyond upper bound
|
||||
}
|
||||
|
||||
// If only lower bound exists
|
||||
if (upper === null && lower !== null) {
|
||||
if (value >= lower) {
|
||||
// Value is in normal range (above lower bound)
|
||||
// Position proportionally in the normal range section
|
||||
const normalizedPosition = Math.min((value - lower) / (lower * 0.5), 1); // Use 50% of lower as scale
|
||||
return normalizedPosition * 100;
|
||||
}
|
||||
// Value is below lower bound - position in the "below normal" section
|
||||
const belowPosition = Math.max(0, Math.min(1, value / lower));
|
||||
return belowPosition * 100;
|
||||
}
|
||||
|
||||
// Both bounds exist
|
||||
if (lower !== null && upper !== null) {
|
||||
if (value < lower) {
|
||||
return 0; // Below normal range
|
||||
}
|
||||
if (value > upper) {
|
||||
return 100; // Above normal range
|
||||
}
|
||||
// Within normal range
|
||||
return ((value - lower) / (upper - lower)) * 100;
|
||||
}
|
||||
|
||||
return 50; // Fallback
|
||||
}, [value, upper, lower]);
|
||||
|
||||
const [isVeryLow, isLow, isHigh, isVeryHigh] = useMemo(
|
||||
() => [
|
||||
level === AnalysisResultLevel.VERY_LOW,
|
||||
level === AnalysisResultLevel.LOW,
|
||||
level === AnalysisResultLevel.HIGH,
|
||||
level === AnalysisResultLevel.VERY_HIGH,
|
||||
],
|
||||
[level, value, upper, lower],
|
||||
);
|
||||
// Determine level states based on normStatus
|
||||
const isNormal = level === AnalysisResultLevel.NORMAL;
|
||||
const isWarning = level === AnalysisResultLevel.WARNING;
|
||||
const isCritical = level === AnalysisResultLevel.CRITICAL;
|
||||
const isPending = level === null;
|
||||
|
||||
const hasAbnormalLevel = isVeryLow || isLow || isHigh || isVeryHigh;
|
||||
// If pending results, show gray bar
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="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 (
|
||||
<div className="mt-4 flex h-3 w-[60%] max-w-[360px] gap-1 sm:mt-0 sm:w-[35%]">
|
||||
{normLowerIncluded && (
|
||||
<>
|
||||
<Level isActive={isVeryLow} color="destructive" isFirst />
|
||||
<Level isActive={isLow} color="warning" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Level
|
||||
isFirst={!normLowerIncluded}
|
||||
isLast={!normUpperIncluded}
|
||||
{...(hasAbnormalLevel
|
||||
? { color: 'warning', isActive: false }
|
||||
: { color: 'success', isActive: true })}
|
||||
arrowLocation={arrowLocation}
|
||||
/>
|
||||
|
||||
{normUpperIncluded && (
|
||||
<>
|
||||
<Level isActive={isHigh} color="warning" />
|
||||
<Level isActive={isVeryHigh} color="destructive" isLast />
|
||||
</>
|
||||
<div
|
||||
className={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 {...second} />
|
||||
<Level {...third} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user