Merge branch 'main' into feature/MED-100

This commit is contained in:
2025-07-21 11:38:47 +03:00
46 changed files with 6714 additions and 501 deletions

View File

@@ -0,0 +1,89 @@
import React from 'react';
import { ArrowDown } from 'lucide-react';
import { cn } from '@kit/ui/utils';
export enum AnalysisResultLevel {
VERY_LOW = 0,
LOW = 1,
NORMAL = 2,
HIGH = 3,
VERY_HIGH = 4,
}
const Level = ({
isActive = false,
color,
isFirst = false,
isLast = false,
}: {
isActive?: boolean;
color: 'destructive' | 'success' | 'warning';
isFirst?: boolean;
isLast?: boolean;
}) => {
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]">
<ArrowDown strokeWidth={2} />
</div>
)}
</div>
);
};
const AnalysisLevelBar = ({
normLowerIncluded = true,
normUpperIncluded = true,
level,
}: {
normLowerIncluded?: boolean;
normUpperIncluded?: boolean;
level: AnalysisResultLevel;
}) => {
return (
<div className="mt-4 flex h-3 w-full max-w-[360px] gap-1 sm:mt-0">
{normLowerIncluded && (
<>
<Level
isActive={level === AnalysisResultLevel.VERY_LOW}
color="destructive"
isFirst
/>
<Level isActive={level === AnalysisResultLevel.LOW} color="warning" />
</>
)}
<Level
isFirst={!normLowerIncluded}
isLast={!normUpperIncluded}
isActive={level === AnalysisResultLevel.NORMAL}
color="success"
/>
{normUpperIncluded && (
<>
<Level
isActive={level === AnalysisResultLevel.HIGH}
color="warning"
/>
<Level
isActive={level === AnalysisResultLevel.VERY_HIGH}
color="destructive"
isLast
/>
</>
)}
</div>
);
};
export default AnalysisLevelBar;

View File

@@ -0,0 +1,98 @@
'use client';
import React, { useState } from 'react';
import { Info } from 'lucide-react';
import { cn } from '@kit/ui/utils';
import AnalysisLevelBar, { AnalysisResultLevel } from './analysis-level-bar';
export enum AnalysisStatus {
NORMAL = 0,
MEDIUM = 1,
HIGH = 2,
}
const Analysis = ({
analysis: {
name,
status,
unit,
value,
normLowerIncluded,
normUpperIncluded,
normLower,
normUpper,
},
}: {
analysis: {
name: string;
status: AnalysisStatus;
unit: string;
value: number;
normLowerIncluded: boolean;
normUpperIncluded: boolean;
normLower: number;
normUpper: number;
};
}) => {
const [showTooltip, setShowTooltip] = useState(false);
const isUnderNorm = value < normLower;
const getAnalysisResultLevel = () => {
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;
}
};
return (
<div className="border-border grid grid-cols-2 items-center justify-between rounded-lg border px-5 py-3 sm:flex">
<div className="flex items-center gap-2 font-semibold">
{name}
<div
className="group/tooltip relative"
onClick={() => 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 },
)}
>
This text changes when you hover the box above.
</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="font-semibold">{value}</div>
<div className="text-muted-foreground text-sm">{unit}</div>
</div>
<div className="text-muted-foreground mt-4 flex gap-2 text-center text-sm sm:mt-0 sm:block sm:gap-0">
{normLower} - {normUpper}
<div>Normaalne vahemik</div>
</div>
<AnalysisLevelBar
normLowerIncluded={normLowerIncluded}
normUpperIncluded={normUpperIncluded}
level={getAnalysisResultLevel()}
/>
</div>
);
};
export default Analysis;

View File

@@ -0,0 +1,59 @@
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
import { withI18n } from '@/lib/i18n/with-i18n';
import { Trans } from '@kit/ui/makerkit/trans';
import { PageBody } from '@kit/ui/page';
import { Button } from '@kit/ui/shadcn/button';
import { loadUserAnalysis } from '../../_lib/server/load-user-analysis';
import Analysis, { AnalysisStatus } from './_components/analysis';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('account:analysisResults.pageTitle');
return {
title,
};
};
async function AnalysisResultsPage() {
const analysisList = await loadUserAnalysis();
return (
<PageBody>
<div className="mt-8 flex items-center justify-between">
<div>
<h4>
<Trans i18nKey="account:analysisResults.pageTitle" />
</h4>
<p className="text-muted-foreground text-sm">
<Trans i18nKey="account:analysisResults.description" />
</p>
</div>
<Button>
<Trans i18nKey="account:analysisResults.orderNewAnalysis" />
</Button>
</div>
<div className="flex flex-col gap-2">
{analysisList?.map((analysis, index) => (
<Analysis
key={index}
analysis={{
name: analysis.element.analysis_name || '',
status: analysis.element.norm_status as AnalysisStatus,
unit: analysis.element.unit || '',
value: analysis.element.response_value,
normLowerIncluded: !!analysis.element.norm_lower_included,
normUpperIncluded: !!analysis.element.norm_upper_included,
normLower: analysis.element.norm_lower || 0,
normUpper: analysis.element.norm_upper || 0,
}}
/>
))}
</div>
</PageBody>
);
}
export default withI18n(AnalysisResultsPage);

View File

@@ -1,6 +1,7 @@
'use client';
import Link from 'next/link';
import { InfoTooltip } from '@/components/ui/info-tooltip';
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
import {
@@ -130,7 +131,7 @@ const dummyRecommendations = [
export default function Dashboard() {
return (
<>
<div className="grid auto-rows-fr grid-cols-5 gap-3">
<div className="grid auto-rows-fr grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-5">
{dummyCards.map(
({
title,
@@ -190,7 +191,7 @@ export default function Dashboard() {
) => {
return (
<div className="flex justify-between" key={index}>
<div className="flex flex-row items-center gap-4">
<div className="mr-4 flex flex-row items-center gap-4">
<div
className={cn(
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',

View File

@@ -0,0 +1,22 @@
import { cache } from 'react';
import { createAccountsApi } from '@kit/accounts/api';
import { UserAnalysis } from '@kit/accounts/types/accounts';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export type UserAccount = Awaited<ReturnType<typeof loadUserAnalysis>>;
/**
* @name loadUserAccount
* @description
* Load the user account. It's a cached per-request function that fetches the user workspace data.
* It can be used across the server components to load the user workspace data.
*/
export const loadUserAnalysis = cache(analysisLoader);
async function analysisLoader(): Promise<UserAnalysis | null> {
const client = getSupabaseServerClient();
const api = createAccountsApi(client);
return api.getUserAnalysis();
}