221 lines
5.5 KiB
TypeScript
221 lines
5.5 KiB
TypeScript
import { useMemo } from 'react';
|
|
|
|
import { ArrowDown } from 'lucide-react';
|
|
|
|
import { cn } from '@kit/ui/utils';
|
|
|
|
import { AnalysisResultForDisplay } from './analysis-doctor';
|
|
|
|
export enum AnalysisResultLevel {
|
|
NORMAL = 'NORMAL',
|
|
WARNING = 'WARNING',
|
|
CRITICAL = 'CRITICAL',
|
|
}
|
|
|
|
const Level = ({
|
|
isActive = false,
|
|
color,
|
|
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-60': !isActive,
|
|
'rounded-l-lg': isFirst,
|
|
'rounded-r-lg': isLast,
|
|
})}
|
|
>
|
|
{isActive && (
|
|
<div
|
|
className="absolute top-[-14px] left-1/2 -translate-x-1/2 rounded-[10px] bg-white p-[2px]"
|
|
{...(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>
|
|
);
|
|
};
|
|
|
|
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" isFirst isLast />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const AnalysisLevelBar = ({
|
|
level,
|
|
results,
|
|
normRangeText,
|
|
}: {
|
|
level: AnalysisResultLevel | null;
|
|
results: AnalysisResultForDisplay;
|
|
normRangeText: string | null;
|
|
}) => {
|
|
const {
|
|
norm_lower: lower,
|
|
norm_upper: upper,
|
|
response_value: value,
|
|
} = results;
|
|
|
|
// 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;
|
|
}
|
|
|
|
// If no normal ranges defined, center the arrow
|
|
if (lower === null && upper === null) {
|
|
return 50;
|
|
}
|
|
|
|
// 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]);
|
|
|
|
// 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;
|
|
|
|
// 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] = [
|
|
{
|
|
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,
|
|
]);
|
|
|
|
// 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(
|
|
'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>
|
|
);
|
|
};
|
|
|
|
export default AnalysisLevelBar;
|