diff --git a/app/home/(user)/(dashboard)/page.tsx b/app/home/(user)/(dashboard)/page.tsx
index 14d8bff..7eba847 100644
--- a/app/home/(user)/(dashboard)/page.tsx
+++ b/app/home/(user)/(dashboard)/page.tsx
@@ -1,14 +1,18 @@
import { redirect } from 'next/navigation';
+import { toTitleCase } from '@/lib/utils';
+import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
+import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
+
+import { PageBody, PageHeader } from '@kit/ui/page';
+import { Trans } from '@kit/ui/trans';
+
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
-import { PageBody, PageHeader } from '@kit/ui/page';
-import { Trans } from '@kit/ui/trans';
-import { toTitleCase } from '@/lib/utils';
import Dashboard from '../_components/dashboard';
-import { loadCurrentUserAccount } from '../_lib/server/load-user-account';
import DashboardCards from '../_components/dashboard-cards';
+import { loadCurrentUserAccount } from '../_lib/server/load-user-account';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
@@ -20,7 +24,12 @@ export const generateMetadata = async () => {
};
async function UserHomePage() {
+ const client = getSupabaseServerClient();
+
const account = await loadCurrentUserAccount();
+ const api = await createAccountsApi(client);
+ const bmiThresholds = await api.fetchBmiThresholds();
+
if (!account) {
redirect('/');
}
@@ -28,18 +37,17 @@ async function UserHomePage() {
return (
<>
-
-
- {account.name ? `, ${toTitleCase(account.name)}` : ''}
- >
- }
- description={
-
+
+
+ {account.name ? `, ${toTitleCase(account.name)}` : ''}
+ >
}
+ description={}
/>
-
+
>
);
diff --git a/app/home/(user)/_components/dashboard.tsx b/app/home/(user)/_components/dashboard.tsx
index f223b1c..181129d 100644
--- a/app/home/(user)/_components/dashboard.tsx
+++ b/app/home/(user)/_components/dashboard.tsx
@@ -4,14 +4,13 @@ import Link from 'next/link';
import { InfoTooltip } from '@/components/ui/info-tooltip';
import type { AccountWithParams } from '@/packages/features/accounts/src/server/api';
+import { Database } from '@/packages/supabase/src/database.types';
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
import Isikukood from 'isikukood';
import {
Activity,
- ChevronRight,
Clock9,
Droplets,
- LineChart,
Pill,
Scale,
TrendingUp,
@@ -25,95 +24,96 @@ import {
CardDescription,
CardFooter,
CardHeader,
- CardProps,
} from '@kit/ui/card';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
+import { BmiCategory } from '~/lib/types/bmi';
+import {
+ bmiFromMetric,
+ getBmiBackgroundColor,
+ getBmiStatus,
+} from '~/lib/utils';
+
const cards = ({
gender,
age,
height,
weight,
+ bmiStatus,
}: {
gender?: string;
age?: number;
height?: number | null;
weight?: number | null;
-}) => {
- const heightInMeters = height ? height / 100 : null;
- const bmi =
- heightInMeters && weight
- ? (weight / (heightInMeters * heightInMeters)).toFixed(1)
- : null;
- return [
- {
- title: 'dashboard:gender',
- description: gender ?? 'dashboard:male',
- icon: ,
- iconBg: 'bg-success',
- },
- {
- title: 'dashboard:age',
- description: age ? `${age}` : '-',
- icon: ,
- iconBg: 'bg-success',
- },
- {
- title: 'dashboard:height',
- description: height ? `${height}cm` : '-',
- icon: ,
- iconBg: 'bg-success',
- },
- {
- title: 'dashboard:weight',
- description: weight ? `${weight}kg` : '-',
- icon: ,
- iconBg: 'bg-success',
- },
- {
- title: 'dashboard:bmi',
- description: bmi,
- icon: ,
- iconBg: 'bg-success',
- },
- {
- title: 'dashboard:bloodPressure',
- description: '-',
- icon: ,
- iconBg: 'bg-warning',
- },
- {
- title: 'dashboard:cholesterol',
- description: '-',
- icon: ,
- iconBg: 'bg-destructive',
- },
- {
- title: 'dashboard:ldlCholesterol',
- description: '-',
- icon: ,
- iconBg: 'bg-warning',
- },
- // {
- // title: 'Score 2',
- // description: 'Normis',
- // icon: ,
- // iconBg: 'bg-success',
- // },
- // {
- // title: 'dashboard:smoking',
- // description: 'dashboard:respondToQuestion',
- // descriptionColor: 'text-primary',
- // icon: (
- //
- // ),
- // cardVariant: 'gradient-success' as CardProps['variant'],
- // },
- ];
-};
+ bmiStatus: BmiCategory | null;
+}) => [
+ {
+ title: 'dashboard:gender',
+ description: gender ?? 'dashboard:male',
+ icon: ,
+ iconBg: 'bg-success',
+ },
+ {
+ title: 'dashboard:age',
+ description: age ? `${age}` : '-',
+ icon: ,
+ iconBg: 'bg-success',
+ },
+ {
+ title: 'dashboard:height',
+ description: height ? `${height}cm` : '-',
+ icon: ,
+ iconBg: 'bg-success',
+ },
+ {
+ title: 'dashboard:weight',
+ description: weight ? `${weight}kg` : '-',
+ icon: ,
+ iconBg: 'bg-success',
+ },
+ {
+ title: 'dashboard:bmi',
+ description: bmiFromMetric(weight || 0, height || 0).toString(),
+ icon: ,
+ iconBg: getBmiBackgroundColor(bmiStatus),
+ },
+ {
+ title: 'dashboard:bloodPressure',
+ description: '-',
+ icon: ,
+ iconBg: 'bg-warning',
+ },
+ {
+ title: 'dashboard:cholesterol',
+ description: '-',
+ icon: ,
+ iconBg: 'bg-destructive',
+ },
+ {
+ title: 'dashboard:ldlCholesterol',
+ description: '-',
+ icon: ,
+ iconBg: 'bg-warning',
+ },
+ // {
+ // title: 'Score 2',
+ // description: 'Normis',
+ // icon: ,
+ // iconBg: 'bg-success',
+ // },
+ // {
+ // title: 'dashboard:smoking',
+ // description: 'dashboard:respondToQuestion',
+ // descriptionColor: 'text-primary',
+ // icon: (
+ //
+ // ),
+ // cardVariant: 'gradient-success' as CardProps['variant'],
+ // },
+];
const dummyRecommendations = [
{
@@ -160,8 +160,22 @@ const getPersonParameters = (personalCode: string) => {
}
};
-export default function Dashboard({ account }: { account: AccountWithParams }) {
+export default function Dashboard({
+ account,
+ bmiThresholds,
+}: {
+ account: AccountWithParams;
+ bmiThresholds: Omit<
+ Database['medreport']['Tables']['bmi_thresholds']['Row'],
+ 'id'
+ >[];
+}) {
const params = getPersonParameters(account.personal_code!);
+ const bmiStatus = getBmiStatus(bmiThresholds, {
+ age: params?.age || 0,
+ height: account.account_params?.[0]?.height || 0,
+ weight: account.account_params?.[0]?.weight || 0,
+ });
return (
<>
@@ -171,18 +185,19 @@ export default function Dashboard({ account }: { account: AccountWithParams }) {
age: params?.age,
height: account.account_params?.[0]?.height,
weight: account.account_params?.[0]?.weight,
+ bmiStatus,
}).map(
({
title,
description,
icon,
iconBg,
- cardVariant,
- descriptionColor,
+ // cardVariant,
+ // descriptionColor,
}) => (
@@ -199,7 +214,9 @@ export default function Dashboard({ account }: { account: AccountWithParams }) {
-
+
diff --git a/app/home/[account]/_lib/server/load-team-account-health-details.ts b/app/home/[account]/_lib/server/load-team-account-health-details.ts
index 6aeaa3c..e723846 100644
--- a/app/home/[account]/_lib/server/load-team-account-health-details.ts
+++ b/app/home/[account]/_lib/server/load-team-account-health-details.ts
@@ -2,6 +2,8 @@ import React from 'react';
import { Clock, TrendingUp, User } from 'lucide-react';
+import { bmiFromMetric } from '~/lib/utils';
+
import { TeamAccountStatisticsProps } from '../../_components/team-account-statistics';
interface AccountHealthDetailsField {
@@ -26,9 +28,7 @@ export const getAccountHealthDetailsFields = (
): AccountHealthDetailsField[] => {
const averageBMI = (
memberParams.reduce((sum, { height, weight }) => {
- const hMeters = height! / 100;
- const bmi = weight! / (hMeters * hMeters);
- return sum + bmi;
+ return bmiFromMetric(weight ?? 0, height ?? 0) + sum;
}, 0) / memberParams.length
).toFixed(0);
diff --git a/lib/types/bmi.ts b/lib/types/bmi.ts
new file mode 100644
index 0000000..f5080b2
--- /dev/null
+++ b/lib/types/bmi.ts
@@ -0,0 +1,7 @@
+export enum BmiCategory {
+ UNDER_WEIGHT = 'under_weight',
+ NORMAL = 'normal',
+ OVER_WEIGHT = 'over_weight',
+ VERY_OVERWEIGHT = 'very_overweight',
+ OBESE = 'obese',
+}
diff --git a/lib/utils.ts b/lib/utils.ts
index a9c34c4..29bf93e 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -1,6 +1,9 @@
+import { Database } from '@/packages/supabase/src/database.types';
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
+import { BmiCategory } from './types/bmi';
+
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
@@ -19,11 +22,63 @@ export function toTitleCase(str?: string) {
);
}
-export function sortByDate(a: T[] | undefined, key: keyof T): T[] | undefined {
+export function sortByDate(
+ a: T[] | undefined,
+ key: keyof T,
+): T[] | undefined {
return a?.sort((a, b) => {
if (!a[key] || !b[key]) {
return 0;
}
- return new Date(b[key] as string).getTime() - new Date(a[key] as string).getTime();
+ return (
+ new Date(b[key] as string).getTime() -
+ new Date(a[key] as string).getTime()
+ );
});
}
+
+export const bmiFromMetric = (kg: number, cm: number) => {
+ const m = cm / 100;
+ return kg / (m * m);
+};
+
+export function getBmiStatus(
+ thresholds: Omit<
+ Database['medreport']['Tables']['bmi_thresholds']['Row'],
+ 'id'
+ >[],
+ params: { age: number; height: number; weight: number },
+): BmiCategory | null {
+ const age = params.age;
+ const thresholdByAge =
+ thresholds.find(
+ (b) => age >= b.age_min && (b.age_max == null || age <= b.age_max),
+ ) || null;
+ const bmi = bmiFromMetric(params.weight, params.height);
+
+ if (!thresholdByAge || Number.isNaN(bmi)) return null;
+
+ if (bmi > thresholdByAge.obesity_min) return BmiCategory.OBESE;
+ if (bmi > thresholdByAge.strong_min) return BmiCategory.VERY_OVERWEIGHT;
+ if (bmi > thresholdByAge.overweight_min) return BmiCategory.OVER_WEIGHT;
+ if (bmi >= thresholdByAge.normal_min && bmi <= thresholdByAge.normal_max)
+ return BmiCategory.NORMAL;
+ return BmiCategory.UNDER_WEIGHT;
+}
+
+export function getBmiBackgroundColor(bmiStatus: BmiCategory | null): string {
+ switch (bmiStatus) {
+ case BmiCategory.UNDER_WEIGHT:
+ return 'bg-warning';
+ case BmiCategory.NORMAL:
+ return 'bg-success';
+ case BmiCategory.OVER_WEIGHT:
+ return 'bg-warning';
+ case BmiCategory.VERY_OVERWEIGHT:
+ return 'bg-destructive';
+ case BmiCategory.OBESE:
+ return 'bg-error';
+ default:
+ return 'bg-success';
+ }
+}
diff --git a/packages/features/accounts/src/server/api.ts b/packages/features/accounts/src/server/api.ts
index 7bebe76..0844138 100644
--- a/packages/features/accounts/src/server/api.ts
+++ b/packages/features/accounts/src/server/api.ts
@@ -242,6 +242,23 @@ class AccountsApi {
return (count ?? 0) > 0;
}
+
+ async fetchBmiThresholds() {
+ // Fetch BMI
+ const { data, error } = await this.client
+ .schema('medreport')
+ .from('bmi_thresholds')
+ .select(
+ 'age_min,age_max,underweight_max,normal_min,normal_max,overweight_min,strong_min,obesity_min',
+ )
+ .order('age_min', { ascending: true });
+
+ if (error) {
+ console.error('Error fetching BMI thresholds:', error);
+ throw error;
+ }
+ return data;
+ }
}
export function createAccountsApi(client: SupabaseClient) {
diff --git a/packages/features/accounts/src/types/accounts.ts b/packages/features/accounts/src/types/accounts.ts
index eb9cf8e..2d54306 100644
--- a/packages/features/accounts/src/types/accounts.ts
+++ b/packages/features/accounts/src/types/accounts.ts
@@ -14,4 +14,4 @@ export enum ApplicationRoleEnum {
User = 'user',
Doctor = 'doctor',
SuperAdmin = 'super_admin',
-}
\ No newline at end of file
+}
diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts
index 8142777..9093fe4 100644
--- a/packages/supabase/src/database.types.ts
+++ b/packages/supabase/src/database.types.ts
@@ -690,6 +690,42 @@ export type Database = {
},
]
}
+ bmi_thresholds: {
+ Row: {
+ age_max: number | null
+ age_min: number
+ id: number
+ normal_max: number
+ normal_min: number
+ obesity_min: number
+ overweight_min: number
+ strong_min: number
+ underweight_max: number
+ }
+ Insert: {
+ age_max?: number | null
+ age_min: number
+ id?: number
+ normal_max: number
+ normal_min: number
+ obesity_min: number
+ overweight_min: number
+ strong_min: number
+ underweight_max: number
+ }
+ Update: {
+ age_max?: number | null
+ age_min?: number
+ id?: number
+ normal_max?: number
+ normal_min?: number
+ obesity_min?: number
+ overweight_min?: number
+ strong_min?: number
+ underweight_max?: number
+ }
+ Relationships: []
+ }
codes: {
Row: {
analysis_element_id: number | null
@@ -1807,6 +1843,7 @@ export type Database = {
primary_owner_user_id: string
subscription_status: Database["medreport"]["Enums"]["subscription_status"]
permissions: Database["medreport"]["Enums"]["app_permissions"][]
+ account_role: string
application_role: Database["medreport"]["Enums"]["application_role"]
}[]
}
diff --git a/supabase/migrations/20250821213200_bmi_config.sql b/supabase/migrations/20250821213200_bmi_config.sql
new file mode 100644
index 0000000..72fb723
--- /dev/null
+++ b/supabase/migrations/20250821213200_bmi_config.sql
@@ -0,0 +1,27 @@
+create table medreport.bmi_thresholds (
+ id bigserial primary key,
+ age_min int not null,
+ age_max int,
+ underweight_max numeric not null,
+ normal_min numeric not null,
+ normal_max numeric not null,
+ overweight_min numeric not null,
+ strong_min numeric not null,
+ obesity_min numeric not null
+);
+
+insert into medreport.bmi_thresholds
+(age_min, age_max, underweight_max, normal_min, normal_max, overweight_min, strong_min, obesity_min)
+values
+(19, 24, 19, 19, 24, 24, 30, 35),
+(25, 34, 20, 20, 25, 25, 30, 35),
+(35, 44, 21, 21, 26, 26, 30, 35),
+(45, 54, 22, 22, 27, 27, 30, 35),
+(55, 64, 23, 23, 28, 28, 30, 35),
+(65, null, 24, 24, 29, 29, 30, 35);
+
+alter table medreport.bmi_thresholds enable row level security;
+
+grant select, insert, update, delete on table medreport.bmi_thresholds to authenticated;
+
+create policy "bmi_thresholds_select" on "medreport"."bmi_thresholds" for select to service_role using (true);
\ No newline at end of file