feat(MED-105): update analysis results page

This commit is contained in:
2025-08-11 09:21:06 +03:00
parent aba4596edd
commit 49fc75b17b
13 changed files with 147 additions and 87 deletions

View File

@@ -19,7 +19,7 @@ const Level = ({
isLast = false, isLast = false,
}: { }: {
isActive?: boolean; isActive?: boolean;
color: 'destructive' | 'success' | 'warning'; color: 'destructive' | 'success' | 'warning' | 'gray-200';
isFirst?: boolean; isFirst?: boolean;
isLast?: boolean; isLast?: boolean;
}) => { }) => {
@@ -40,6 +40,14 @@ const Level = ({
); );
}; };
export const AnalysisLevelBarSkeleton = () => {
return (
<div className="mt-4 flex h-3 max-w-[360px] w-[35%] gap-1 sm:mt-0">
<Level color="gray-200" />
</div>
);
};
const AnalysisLevelBar = ({ const AnalysisLevelBar = ({
normLowerIncluded = true, normLowerIncluded = true,
normUpperIncluded = true, normUpperIncluded = true,
@@ -50,7 +58,7 @@ const AnalysisLevelBar = ({
level: AnalysisResultLevel; level: AnalysisResultLevel;
}) => { }) => {
return ( return (
<div className="mt-4 flex h-3 w-full max-w-[360px] gap-1 sm:mt-0"> <div className="mt-4 flex h-3 max-w-[360px] w-[35%] gap-1 sm:mt-0">
{normLowerIncluded && ( {normLowerIncluded && (
<> <>
<Level <Level

View File

@@ -6,7 +6,10 @@ import { Info } from 'lucide-react';
import { cn } from '@kit/ui/utils'; import { cn } from '@kit/ui/utils';
import AnalysisLevelBar, { AnalysisResultLevel } from './analysis-level-bar'; import AnalysisLevelBar, { AnalysisLevelBarSkeleton, AnalysisResultLevel } from './analysis-level-bar';
import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts';
import { AnalysisElement } from '~/lib/services/analysis-element.service';
import { Trans } from '@kit/ui/trans';
export enum AnalysisStatus { export enum AnalysisStatus {
NORMAL = 0, NORMAL = 0,
@@ -15,31 +18,27 @@ export enum AnalysisStatus {
} }
const Analysis = ({ const Analysis = ({
analysis: { analysisElement,
name, results,
status,
unit,
value,
normLowerIncluded,
normUpperIncluded,
normLower,
normUpper,
},
}: { }: {
analysis: { analysisElement: AnalysisElement;
name: string; results?: UserAnalysisElement;
status: AnalysisStatus;
unit: string;
value: number;
normLowerIncluded: boolean;
normUpperIncluded: boolean;
normLower: number;
normUpper: number;
};
}) => { }) => {
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 [showTooltip, setShowTooltip] = useState(false);
const isUnderNorm = value < normLower; const isUnderNorm = value < normLower;
const getAnalysisResultLevel = () => { const getAnalysisResultLevel = () => {
if (!results) {
return null;
}
if (isUnderNorm) { if (isUnderNorm) {
switch (status) { switch (status) {
case AnalysisStatus.MEDIUM: case AnalysisStatus.MEDIUM:
@@ -59,7 +58,7 @@ const Analysis = ({
}; };
return ( return (
<div className="border-border grid grid-cols-2 items-center justify-between rounded-lg border px-5 py-3 sm:flex"> <div className="border-border items-center justify-between rounded-lg border px-5 py-3 sm:h-[65px] flex flex-col sm:flex-row px-12 gap-2 sm:gap-0">
<div className="flex items-center gap-2 font-semibold"> <div className="flex items-center gap-2 font-semibold">
{name} {name}
<div <div
@@ -78,19 +77,35 @@ const Analysis = ({
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> {results ? (
<div className="font-semibold">{value}</div> <>
<div className="text-muted-foreground text-sm">{unit}</div> <div className="flex items-center gap-3 sm:ml-auto">
</div> <div className="font-semibold">{value}</div>
<div className="text-muted-foreground mt-4 flex gap-2 text-center text-sm sm:mt-0 sm:block sm:gap-0"> <div className="text-muted-foreground text-sm">{unit}</div>
{normLower} - {normUpper} </div>
<div>Normaalne vahemik</div> <div className="text-muted-foreground flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0 mx-8">
</div> {normLower} - {normUpper}
<AnalysisLevelBar <div>
normLowerIncluded={normLowerIncluded} <Trans i18nKey="analysis-results:results.range.normal" />
normUpperIncluded={normUpperIncluded} </div>
level={getAnalysisResultLevel()} </div>
/> <AnalysisLevelBar
normLowerIncluded={normLowerIncluded}
normUpperIncluded={normUpperIncluded}
level={getAnalysisResultLevel()!}
/>
</>
) : (
<>
<div className="flex items-center gap-3 sm:ml-auto">
<div className="font-semibold">
<Trans i18nKey="analysis-results:waitingForResults" />
</div>
</div>
<div className="w-[60px] mx-8"></div>
<AnalysisLevelBarSkeleton />
</>
)}
</div> </div>
); );
}; };

View File

@@ -1,4 +1,5 @@
import { Fragment } from 'react'; import Link from 'next/link';
import { createI18nServerInstance } from '@/lib/i18n/i18n.server'; import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
import { withI18n } from '@/lib/i18n/with-i18n'; import { withI18n } from '@/lib/i18n/with-i18n';
@@ -7,11 +8,17 @@ import { PageBody } from '@kit/ui/page';
import { Button } from '@kit/ui/shadcn/button'; import { Button } from '@kit/ui/shadcn/button';
import { loadUserAnalysis } from '../../_lib/server/load-user-analysis'; import { loadUserAnalysis } from '../../_lib/server/load-user-analysis';
import Analysis, { AnalysisStatus } from './_components/analysis'; import Analysis from './_components/analysis';
import { listProductTypes } from '@lib/data/products';
import pathsConfig from '~/config/paths.config';
import { redirect } from 'next/navigation';
import { getOrders } from '~/lib/services/order.service';
import { AnalysisElement, getAnalysisElements } from '~/lib/services/analysis-element.service';
import type { UserAnalysisElement } from '@kit/accounts/types/accounts';
export const generateMetadata = async () => { export const generateMetadata = async () => {
const i18n = await createI18nServerInstance(); const i18n = await createI18nServerInstance();
const title = i18n.t('account:analysisResults.pageTitle'); const title = i18n.t('analysis-results:pageTitle');
return { return {
title, title,
@@ -21,45 +28,56 @@ export const generateMetadata = async () => {
async function AnalysisResultsPage() { async function AnalysisResultsPage() {
const analysisList = await loadUserAnalysis(); const analysisList = await loadUserAnalysis();
const orders = await getOrders().catch(() => null);
const { productTypes } = await listProductTypes();
if (!orders || !productTypes) {
redirect(pathsConfig.auth.signIn);
}
const analysisElementIds = [
...new Set(orders?.flatMap((order) => order.analysis_element_ids).filter(Boolean) as number[]),
];
const analysisElements = await getAnalysisElements({ ids: analysisElementIds });
const analysisElementsWithResults = analysisElements.reduce((acc, curr) => {
const analysisResponseWithElementResults = analysisList?.find((result) => result.elements.some((element) => element.analysis_element_original_id === curr.analysis_id_original));
const elementResults = analysisResponseWithElementResults?.elements.find((element) => element.analysis_element_original_id === curr.analysis_id_original) as UserAnalysisElement | undefined;
return {
...acc,
[curr.id]: {
analysisElement: curr,
results: elementResults,
},
}
}, {} as Record<number, { analysisElement: AnalysisElement; results: UserAnalysisElement | undefined }>);
return ( return (
<PageBody> <PageBody>
<div className="mt-8 flex items-center justify-between"> <div className="mt-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-0">
<div> <div>
<h4> <h4>
<Trans i18nKey="account:analysisResults.pageTitle" /> <Trans i18nKey="analysis-results:pageTitle" />
</h4> </h4>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
{analysisList && analysisList.length > 0 ? ( {analysisList && analysisList.length > 0 ? (
<Trans i18nKey="account:analysisResults.description" /> <Trans i18nKey="analysis-results:description" />
) : ( ) : (
<Trans i18nKey="account:analysisResults.descriptionEmpty" /> <Trans i18nKey="analysis-results:descriptionEmpty" />
)} )}
</p> </p>
</div> </div>
<Button> <Button asChild>
<Trans i18nKey="account:analysisResults.orderNewAnalysis" /> <Link href={pathsConfig.app.orderAnalysis}>
<Trans i18nKey="analysis-results:orderNewAnalysis" />
</Link>
</Button> </Button>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{analysisList?.map((analysis) => ( {Object.entries(analysisElementsWithResults).map(([id, { analysisElement, results }]) => {
<Fragment key={analysis.id}> return (
{analysis.elements.map((element) => ( <Analysis key={id} analysisElement={analysisElement} results={results} />
<Analysis );
key={element.id} })}
analysis={{
name: element.analysis_name || '',
status: element.norm_status as AnalysisStatus,
unit: element.unit || '',
value: element.response_value,
normLowerIncluded: !!element.norm_lower_included,
normUpperIncluded: !!element.norm_upper_included,
normLower: element.norm_lower || 0,
normUpper: element.norm_upper || 0,
}}
/>
))}
</Fragment>
))}
</div> </div>
</PageBody> </PageBody>
); );

View File

@@ -39,6 +39,7 @@ export const defaultI18nNamespaces = [
'order-analysis', 'order-analysis',
'cart', 'cart',
'orders', 'orders',
'analysis-results',
]; ];
/** /**

View File

@@ -1,4 +1,3 @@
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Json, Tables } from '@kit/supabase/database'; import { Json, Tables } from '@kit/supabase/database';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import type { IMaterialGroup, IUuringElement } from './medipost.types'; import type { IMaterialGroup, IUuringElement } from './medipost.types';
@@ -9,10 +8,12 @@ export type AnalysisElement = Tables<{ schema: 'medreport' }, 'analysis_elements
export async function getAnalysisElements({ export async function getAnalysisElements({
originalIds, originalIds,
ids,
}: { }: {
originalIds?: string[]; originalIds?: string[];
ids?: number[];
}): Promise<AnalysisElement[]> { }): Promise<AnalysisElement[]> {
const query = getSupabaseServerClient() const query = getSupabaseServerAdminClient()
.schema('medreport') .schema('medreport')
.from('analysis_elements') .from('analysis_elements')
.select(`*, analysis_groups(*)`) .select(`*, analysis_groups(*)`)
@@ -22,6 +23,10 @@ export async function getAnalysisElements({
query.in('analysis_id_original', [...new Set(originalIds)]); query.in('analysis_id_original', [...new Set(originalIds)]);
} }
if (Array.isArray(ids)) {
query.in('id', ids);
}
const { data: analysisElements, error } = await query; const { data: analysisElements, error } = await query;
if (error) { if (error) {

View File

@@ -1,6 +1,7 @@
import { Database } from '@kit/supabase/database'; import { Database } from '@kit/supabase/database';
export type UserAnalysis = export type UserAnalysisElement = Database['medreport']['Tables']['analysis_response_elements']['Row'];
(Database['medreport']['Tables']['analysis_responses']['Row'] & { export type UserAnalysisResponse = Database['medreport']['Tables']['analysis_responses']['Row'] & {
elements: Database['medreport']['Tables']['analysis_response_elements']['Row'][]; elements: UserAnalysisElement[];
})[]; };
export type UserAnalysis = UserAnalysisResponse[];

View File

@@ -42,7 +42,7 @@ function PageWithSidebar(props: PageProps) {
> >
{MobileNavigation} {MobileNavigation}
<div className={'bg-background flex flex-1 flex-col px-4 lg:px-0'}> <div className={'bg-background flex flex-1 flex-col px-4 lg:px-0 pb-8'}>
{Children} {Children}
</div> </div>
</div> </div>

View File

@@ -122,11 +122,5 @@
"consentToAnonymizedCompanyData": { "consentToAnonymizedCompanyData": {
"label": "Consent to be included in employer statistics", "label": "Consent to be included in employer statistics",
"description": "Consent to be included in anonymized company statistics" "description": "Consent to be included in anonymized company statistics"
},
"analysisResults": {
"pageTitle": "My analysis results",
"description": "Super, you've already done your analysis. Here are your important results:",
"descriptionEmpty": "If you've already done your analysis, your results will appear here soon.",
"orderNewAnalysis": "Order new analyses"
} }
} }

View File

@@ -0,0 +1,12 @@
{
"pageTitle": "My analysis results",
"description": "All analysis results will appear here within 1-3 business days after they have been done.",
"descriptionEmpty": "If you've already done your analysis, your results will appear here soon.",
"orderNewAnalysis": "Order new analyses",
"waitingForResults": "Waiting for results",
"results": {
"range": {
"normal": "Normal range"
}
}
}

View File

@@ -145,11 +145,5 @@
"successTitle": "Tere, {{firstName}} {{lastName}}", "successTitle": "Tere, {{firstName}} {{lastName}}",
"successDescription": "Teie tervisekonto on aktiveeritud ja kasutamiseks valmis!", "successDescription": "Teie tervisekonto on aktiveeritud ja kasutamiseks valmis!",
"successButton": "Jätka" "successButton": "Jätka"
},
"analysisResults": {
"pageTitle": "Minu analüüside vastused",
"description": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad:",
"descriptionEmpty": "Kui oled juba käinud analüüse andmas, siis varsti jõuavad siia sinu analüüside vastused.",
"orderNewAnalysis": "Telli uued analüüsid"
} }
} }

View File

@@ -0,0 +1,12 @@
{
"pageTitle": "Minu analüüside vastused",
"description": "Kõikide analüüside tulemused ilmuvad 1-3 tööpäeva jooksul peale nende andmist.",
"descriptionEmpty": "Kui oled juba käinud analüüse andmas, siis varsti jõuavad siia sinu analüüside vastused.",
"orderNewAnalysis": "Telli uued analüüsid",
"waitingForResults": "Tulemuse ootel",
"results": {
"range": {
"normal": "Normaalne vahemik"
}
}
}

View File

@@ -135,8 +135,8 @@
--animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out;
--breakpoint-xs: 30rem; --breakpoint-xs: 48rem;
--breakpoint-sm: 48rem; --breakpoint-sm: 64rem;
--breakpoint-md: 70rem; --breakpoint-md: 70rem;
--breakpoint-lg: 80rem; --breakpoint-lg: 80rem;
--breakpoint-xl: 96rem; --breakpoint-xl: 96rem;

View File

@@ -5,7 +5,7 @@ export const getAnalysisElementMedusaProductIds = (products: ({ metadata?: { ana
const mapped = products const mapped = products
.flatMap((product) => { .flatMap((product) => {
const value = product?.metadata?.analysisElementMedusaProductIds; const value = product?.metadata?.analysisElementMedusaProductIds?.replaceAll("'", '"');
try { try {
return JSON.parse(value as string); return JSON.parse(value as string);
} catch (e) { } catch (e) {