feat(MED-161): separate doctor analysis view
This commit is contained in:
149
app/doctor/_components/analysis-doctor.tsx
Normal file
149
app/doctor/_components/analysis-doctor.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
|
import { AnalysisElement } from '~/lib/services/analysis-element.service';
|
||||||
|
|
||||||
|
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 enum AnalysisStatus {
|
||||||
|
NORMAL = 0,
|
||||||
|
MEDIUM = 1,
|
||||||
|
HIGH = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalysisDoctor = ({
|
||||||
|
analysisElement,
|
||||||
|
results,
|
||||||
|
startIcon,
|
||||||
|
endIcon,
|
||||||
|
isCancelled,
|
||||||
|
}: {
|
||||||
|
analysisElement: Pick<AnalysisElement, 'analysis_name_lab'>;
|
||||||
|
results?: AnalysisResultForDisplay;
|
||||||
|
isCancelled?: boolean;
|
||||||
|
startIcon?: ReactElement | null;
|
||||||
|
endIcon?: ReactNode | null;
|
||||||
|
}) => {
|
||||||
|
const name = analysisElement.analysis_name_lab || '';
|
||||||
|
const status = results?.norm_status || AnalysisStatus.NORMAL;
|
||||||
|
const value = results?.response_value || 0;
|
||||||
|
const unit = results?.unit || '';
|
||||||
|
const normLowerIncluded = results?.norm_lower_included || false;
|
||||||
|
const normUpperIncluded = results?.norm_upper_included || false;
|
||||||
|
const normLower = results?.norm_lower || 0;
|
||||||
|
const normUpper = results?.norm_upper || 0;
|
||||||
|
|
||||||
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
const analysisResultLevel = useMemo(() => {
|
||||||
|
if (!results) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUnderNorm = value < normLower;
|
||||||
|
if (isUnderNorm) {
|
||||||
|
switch (status) {
|
||||||
|
case AnalysisStatus.MEDIUM:
|
||||||
|
return AnalysisResultLevel.LOW;
|
||||||
|
default:
|
||||||
|
return AnalysisResultLevel.VERY_LOW;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (status) {
|
||||||
|
case AnalysisStatus.MEDIUM:
|
||||||
|
return AnalysisResultLevel.HIGH;
|
||||||
|
case AnalysisStatus.HIGH:
|
||||||
|
return AnalysisResultLevel.VERY_HIGH;
|
||||||
|
default:
|
||||||
|
return AnalysisResultLevel.NORMAL;
|
||||||
|
}
|
||||||
|
}, [results, value, normLower]);
|
||||||
|
|
||||||
|
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">
|
||||||
|
<div className="flex items-center gap-2 font-semibold">
|
||||||
|
{startIcon || <div className="w-4" />}
|
||||||
|
{name}
|
||||||
|
{results?.response_time && (
|
||||||
|
<div
|
||||||
|
className="group/tooltip relative"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowTooltip(!showTooltip);
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => setShowTooltip(false)}
|
||||||
|
>
|
||||||
|
<Info className="hover" />{' '}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute bottom-full left-1/2 z-10 mb-2 hidden -translate-x-1/2 rounded border bg-white p-4 text-sm whitespace-nowrap group-hover/tooltip:block',
|
||||||
|
{ block: showTooltip },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Trans i18nKey="analysis-results:analysisDate" />
|
||||||
|
{': '}
|
||||||
|
{format(new Date(results.response_time), 'dd.MM.yyyy HH:mm')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{results ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-3 sm:ml-auto">
|
||||||
|
<div className="font-semibold">{value}</div>
|
||||||
|
<div className="text-muted-foreground text-sm">{unit}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
|
||||||
|
{normLower} - {normUpper}
|
||||||
|
<div>
|
||||||
|
<Trans i18nKey="analysis-results:results.range.normal" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AnalysisLevelBar
|
||||||
|
results={results}
|
||||||
|
normLowerIncluded={normLowerIncluded}
|
||||||
|
normUpperIncluded={normUpperIncluded}
|
||||||
|
level={analysisResultLevel!}
|
||||||
|
/>
|
||||||
|
{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 />
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnalysisDoctor;
|
||||||
134
app/doctor/_components/analysis-level-bar.tsx
Normal file
134
app/doctor/_components/analysis-level-bar.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { ArrowDown } from 'lucide-react';
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Level = ({
|
||||||
|
isActive = false,
|
||||||
|
color,
|
||||||
|
isFirst = false,
|
||||||
|
isLast = false,
|
||||||
|
arrowLocation,
|
||||||
|
}: {
|
||||||
|
isActive?: boolean;
|
||||||
|
color: 'destructive' | 'success' | 'warning' | 'gray-200';
|
||||||
|
isFirst?: boolean;
|
||||||
|
isLast?: boolean;
|
||||||
|
arrowLocation?: number;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(`bg-${color} relative h-3 flex-1`, {
|
||||||
|
'opacity-20': !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]"
|
||||||
|
style={{ left: `${arrowLocation}%` }}
|
||||||
|
>
|
||||||
|
<ArrowDown strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AnalysisLevelBarSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="mt-4 flex h-3 w-[35%] max-w-[360px] gap-1 sm:mt-0">
|
||||||
|
<Level color="gray-200" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AnalysisLevelBar = ({
|
||||||
|
normLowerIncluded = true,
|
||||||
|
normUpperIncluded = true,
|
||||||
|
level,
|
||||||
|
results,
|
||||||
|
}: {
|
||||||
|
normLowerIncluded?: boolean;
|
||||||
|
normUpperIncluded?: boolean;
|
||||||
|
level: AnalysisResultLevel;
|
||||||
|
results: AnalysisResultForDisplay;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const { norm_lower: lower, norm_upper: upper, response_value: value } = results;
|
||||||
|
const arrowLocation = useMemo(() => {
|
||||||
|
if (value < lower!) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normLowerIncluded || normUpperIncluded) {
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculated = ((value - lower!) / (upper! - lower!)) * 100;
|
||||||
|
|
||||||
|
if (calculated > 100) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return calculated;
|
||||||
|
}, [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]);
|
||||||
|
|
||||||
|
const hasAbnormalLevel = isVeryLow || isLow || isHigh || isVeryHigh;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4 flex h-3 w-[35%] max-w-[360px] gap-1 sm:mt-0">
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnalysisLevelBar;
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import Analysis from '~/home/(user)/(dashboard)/analysis-results/_components/analysis';
|
import Analysis from '~/home/(user)/(dashboard)/analysis-results/_components/analysis';
|
||||||
|
import AnalysisDoctor from './analysis-doctor';
|
||||||
|
|
||||||
export default function DoctorAnalysisWrapper({
|
export default function DoctorAnalysisWrapper({
|
||||||
analysisData,
|
analysisData,
|
||||||
@@ -65,7 +66,7 @@ export default function DoctorAnalysisWrapper({
|
|||||||
{analysisData.latestPreviousAnalysis && (
|
{analysisData.latestPreviousAnalysis && (
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="my-1 flex flex-col">
|
<div className="my-1 flex flex-col">
|
||||||
<Analysis
|
<AnalysisDoctor
|
||||||
endIcon={
|
endIcon={
|
||||||
analysisData.latestPreviousAnalysis.comment && (
|
analysisData.latestPreviousAnalysis.comment && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user