From 56ffd7591eb1f6163e4daabb7cfc3e1b38e33ee3 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Mon, 25 Aug 2025 12:24:27 +0300 Subject: [PATCH] feat(team-accounts): enhance team account statistics and health details components with new data and improved calculations --- .../team-account-benefit-statistics.tsx | 70 ++++--- .../team-account-health-details.tsx | 31 +-- .../_components/team-account-statistics.tsx | 193 ++++++++++++------ .../load-team-account-health-details.ts | 81 +++++--- app/home/[account]/page.tsx | 44 ++-- lib/services/audit/pageView.service.ts | 1 + lib/utils.ts | 6 +- .../features/team-accounts/src/server/api.ts | 1 + packages/supabase/src/database.types.ts | 1 + public/locales/et/common.json | 3 +- public/locales/et/teams.json | 7 +- ...20250825110100_improve_members_queries.sql | 48 +++++ 12 files changed, 336 insertions(+), 150 deletions(-) create mode 100644 supabase/migrations/20250825110100_improve_members_queries.sql diff --git a/app/home/[account]/_components/team-account-benefit-statistics.tsx b/app/home/[account]/_components/team-account-benefit-statistics.tsx index 95f35a6..36a2426 100644 --- a/app/home/[account]/_components/team-account-benefit-statistics.tsx +++ b/app/home/[account]/_components/team-account-benefit-statistics.tsx @@ -3,10 +3,12 @@ import React, { use } from 'react'; import { redirect } from 'next/navigation'; import { formatCurrency } from '@/packages/shared/src/utils'; +import { Database } from '@/packages/supabase/src/database.types'; import { PiggyBankIcon, Settings } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Card, CardTitle } from '@kit/ui/card'; +import { cn } from '@kit/ui/lib/utils'; import { Button } from '@kit/ui/shadcn/button'; import { Trans } from '@kit/ui/trans'; @@ -14,15 +16,27 @@ import pathsConfig from '~/config/paths.config'; import { createPath } from '~/config/team-account-navigation.config'; interface TeamAccountBenefitStatisticsProps { + employeeCount: number; accountSlug: string; + companyParams: Database['medreport']['Tables']['company_params']['Row']; } const StatisticsCard = ({ children }: { children: React.ReactNode }) => { return {children}; }; -const StatisticsCardTitle = ({ children }: { children: React.ReactNode }) => { - return {children}; +const StatisticsCardTitle = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => { + return ( + + {children} + + ); }; const StatisticsDescription = ({ children }: { children: React.ReactNode }) => { @@ -34,7 +48,9 @@ const StatisticsValue = ({ children }: { children: React.ReactNode }) => { }; const TeamAccountBenefitStatistics = ({ + employeeCount, accountSlug, + companyParams, }: TeamAccountBenefitStatisticsProps) => { const { i18n: { language }, @@ -76,7 +92,8 @@ const TeamAccountBenefitStatistics = ({ i18nKey="teams:benefitStatistics.budget.volume" values={{ volume: formatCurrency({ - value: 15000, + value: + (Number(companyParams.benefit_amount) || 0) * employeeCount, locale: language, currencyCode: 'EUR', }), @@ -87,11 +104,17 @@ const TeamAccountBenefitStatistics = ({
+ + + + + 1800 € + - 18 % + 200 € - 22 % + 200 € - 20 % + 200 € - E-konsultatsioon - 17 % + + + + 200 € - + + -
-
- 23 % - - - -
-
- 1800 € - - - -
-
-
+ 200 € + + + +
); diff --git a/app/home/[account]/_components/team-account-health-details.tsx b/app/home/[account]/_components/team-account-health-details.tsx index 904a2bc..666a029 100644 --- a/app/home/[account]/_components/team-account-health-details.tsx +++ b/app/home/[account]/_components/team-account-health-details.tsx @@ -1,33 +1,36 @@ import React from 'react'; +import { Database } from '@/packages/supabase/src/database.types'; + import { Card } from '@kit/ui/card'; import { Trans } from '@kit/ui/trans'; import { cn } from '@kit/ui/utils'; -import { - NormStatus, - getAccountHealthDetailsFields, -} from '../_lib/server/load-team-account-health-details'; +import { getAccountHealthDetailsFields } from '../_lib/server/load-team-account-health-details'; import { TeamAccountStatisticsProps } from './team-account-statistics'; const TeamAccountHealthDetails = ({ memberParams, + bmiThresholds, + members, }: { memberParams: TeamAccountStatisticsProps['memberParams']; + bmiThresholds: Omit< + Database['medreport']['Tables']['bmi_thresholds']['Row'], + 'id' + >[]; + members: Database['medreport']['Functions']['get_account_members']['Returns']; }) => { - const accountHealthDetailsFields = - getAccountHealthDetailsFields(memberParams); + const accountHealthDetailsFields = getAccountHealthDetailsFields( + memberParams, + bmiThresholds, + members, + ); return (
- {accountHealthDetailsFields.map(({ title, Icon, value, normStatus }) => ( + {accountHealthDetailsFields.map(({ title, Icon, value, iconBg }) => ( -
+
diff --git a/app/home/[account]/_components/team-account-statistics.tsx b/app/home/[account]/_components/team-account-statistics.tsx index 98b0b83..d4cf88c 100644 --- a/app/home/[account]/_components/team-account-statistics.tsx +++ b/app/home/[account]/_components/team-account-statistics.tsx @@ -1,12 +1,24 @@ 'use client'; +import { useState } from 'react'; + import { redirect } from 'next/navigation'; import { Database } from '@/packages/supabase/src/database.types'; -import { ChevronRight, Euro, User } from 'lucide-react'; +import { format } from 'date-fns'; +import { enGB, et } from 'date-fns/locale'; +import { CalendarIcon, ChevronRight, Euro, User } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import { Card } from '@kit/ui/card'; import { Trans } from '@kit/ui/makerkit/trans'; +import { Button } from '@kit/ui/shadcn/button'; +import { Calendar, DateRange } from '@kit/ui/shadcn/calendar'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@kit/ui/shadcn/popover'; import pathsConfig from '~/config/paths.config'; import { createPath } from '~/config/team-account-navigation.config'; @@ -20,82 +32,145 @@ export interface TeamAccountStatisticsProps { Database['medreport']['Tables']['account_params']['Row'], 'weight' | 'height' >[]; + bmiThresholds: Omit< + Database['medreport']['Tables']['bmi_thresholds']['Row'], + 'id' + >[]; + members: Database['medreport']['Functions']['get_account_members']['Returns']; + companyParams: Database['medreport']['Tables']['company_params']['Row']; } export default function TeamAccountStatistics({ teamAccount, memberParams, + bmiThresholds, + members, + companyParams, }: TeamAccountStatisticsProps) { + const [date, setDate] = useState({ + from: new Date(), + to: new Date(), + }); + const { + i18n: { language }, + } = useTranslation(); + const dateFormatOptions = { + locale: language === 'et' ? et : enGB, + }; + return ( -
- + <> +
+

+ +

-
- -
-
- + + + + + + + + +
-
- - redirect( - createPath( - pathsConfig.app.accountMembers, - teamAccount.slug || '', - ), - ) - } - > -
+
+ + +
+ +
+ +
+ + +
+ + redirect( + createPath( + pathsConfig.app.accountMembers, + teamAccount.slug || '', + ), + ) + } + > +
+
+ +
+
+ +
+ + + +

+ +

+
+
+ + + redirect( + createPath( + pathsConfig.app.accountBilling, + teamAccount.slug || '', + ), + ) + } + >
-
- +
+
- +

- +

-
-
- - - redirect( - createPath( - pathsConfig.app.accountBilling, - teamAccount.slug || '', - ), - ) - } - > -
- -
-
- -
- - - -

- -

-
+ +
-
+ ); } 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 e723846..0c4ff72 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 @@ -1,8 +1,14 @@ import React from 'react'; +import { Database } from '@/packages/supabase/src/database.types'; +import Isikukood from 'isikukood'; import { Clock, TrendingUp, User } from 'lucide-react'; -import { bmiFromMetric } from '~/lib/utils'; +import { + bmiFromMetric, + getBmiBackgroundColor, + getBmiStatus, +} from '~/lib/utils'; import { TeamAccountStatisticsProps } from '../../_components/team-account-statistics'; @@ -14,66 +20,89 @@ interface AccountHealthDetailsField { color?: string; className?: string; }>; - normStatus: NormStatus; -} - -export enum NormStatus { - CRITICAL = 'CRITICAL', - WARNING = 'WARNING', - NORMAL = 'NORMAL', + iconBg: string; } export const getAccountHealthDetailsFields = ( memberParams: TeamAccountStatisticsProps['memberParams'], + bmiThresholds: Omit< + Database['medreport']['Tables']['bmi_thresholds']['Row'], + 'id' + >[], + members: Database['medreport']['Functions']['get_account_members']['Returns'], ): AccountHealthDetailsField[] => { - const averageBMI = ( - memberParams.reduce((sum, { height, weight }) => { - return bmiFromMetric(weight ?? 0, height ?? 0) + sum; - }, 0) / memberParams.length - ).toFixed(0); + const avarageWeight = + memberParams.reduce((sum, r) => sum + r.weight!, 0) / memberParams.length; + const avarageHeight = + memberParams.reduce((sum, r) => sum + r.height!, 0) / memberParams.length; + const avarageAge = + members.reduce((sum, r) => { + const person = new Isikukood(r.personal_code); + return sum + person.getAge(); + }, 0) / members.length; + const numberOfMaleMembers = members.filter((r) => { + const person = new Isikukood(r.personal_code); + return person.getGender() === 'male'; + }).length; + const numberOfFemaleMembers = members.filter((r) => { + const person = new Isikukood(r.personal_code); + return person.getGender() === 'female'; + }).length; + const averageBMI = bmiFromMetric(avarageWeight, avarageHeight); + const bmiStatus = getBmiStatus(bmiThresholds, { + age: avarageAge, + height: avarageHeight, + weight: avarageWeight, + }); + const malePercentage = members.length + ? (numberOfMaleMembers / members.length) * 100 + : 0; + const femalePercentage = members.length + ? (numberOfFemaleMembers / members.length) * 100 + : 0; return [ { title: 'teams:healthDetails.women', - value: `50% (${memberParams.length})`, + value: `${femalePercentage}% (${numberOfFemaleMembers})`, Icon: User, - normStatus: NormStatus.NORMAL, + iconBg: 'bg-success', }, { title: 'teams:healthDetails.men', - value: `50% (${memberParams.length})`, + value: `${malePercentage}% (${numberOfMaleMembers})`, Icon: User, - normStatus: NormStatus.NORMAL, + iconBg: 'bg-success', }, { title: 'teams:healthDetails.avgAge', - value: '56', + value: avarageAge.toFixed(0), Icon: Clock, - normStatus: NormStatus.NORMAL, + iconBg: 'bg-success', }, { title: 'teams:healthDetails.bmi', value: averageBMI, Icon: TrendingUp, - normStatus: NormStatus.WARNING, + iconBg: getBmiBackgroundColor(bmiStatus), }, { title: 'teams:healthDetails.cholesterol', - value: '6.1', + value: '-', Icon: TrendingUp, - normStatus: NormStatus.WARNING, + iconBg: 'bg-warning', }, { title: 'teams:healthDetails.vitaminD', - value: '76', + value: '-', Icon: TrendingUp, - normStatus: NormStatus.NORMAL, + iconBg: 'bg-warning', }, { title: 'teams:healthDetails.smokers', - value: '22%', + value: '-', Icon: TrendingUp, - normStatus: NormStatus.CRITICAL, + iconBg: 'bg-warning', }, ]; }; diff --git a/app/home/[account]/page.tsx b/app/home/[account]/page.tsx index 46a0c2b..262a35c 100644 --- a/app/home/[account]/page.tsx +++ b/app/home/[account]/page.tsx @@ -2,6 +2,7 @@ import { use } from 'react'; +import { createAccountsApi } from '@/packages/features/accounts/src/server/api'; import { CompanyGuard } from '@/packages/features/team-accounts/src/components'; import { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api'; import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; @@ -11,6 +12,10 @@ import { Trans } from '@kit/ui/trans'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; +import { + PAGE_VIEW_ACTION, + createPageViewLog, +} from '~/lib/services/audit/pageView.service'; import { Dashboard } from './_components/dashboard'; import { TeamAccountLayoutPageHeader } from './_components/team-account-layout-page-header'; @@ -31,25 +36,32 @@ export const generateMetadata = async () => { function TeamAccountHomePage({ params }: TeamAccountHomePageProps) { const account = use(params).account; const client = getSupabaseServerClient(); - const api = createTeamAccountsApi(client); - const teamAccount = use(api.getTeamAccount(account)); - const { memberParams } = use(api.getMembers(account)); + const teamAccountsApi = createTeamAccountsApi(client); + const accountsApi = createAccountsApi(client); + const teamAccount = use(teamAccountsApi.getTeamAccount(account)); + const { memberParams, members } = use(teamAccountsApi.getMembers(account)); + const bmiThresholds = use(accountsApi.fetchBmiThresholds()); + const companyParams = use( + teamAccountsApi.getTeamAccountParams(teamAccount.id), + ); + + use( + createPageViewLog({ + accountId: teamAccount.id, + action: PAGE_VIEW_ACTION.VIEW_TEAM_ACCOUNT_DASHBOARD, + }), + ); return ( - <> - - } + + - - - - - + ); } diff --git a/lib/services/audit/pageView.service.ts b/lib/services/audit/pageView.service.ts index 28fc458..92ce10e 100644 --- a/lib/services/audit/pageView.service.ts +++ b/lib/services/audit/pageView.service.ts @@ -4,6 +4,7 @@ export enum PAGE_VIEW_ACTION { VIEW_ANALYSIS_RESULTS = 'VIEW_ANALYSIS_RESULTS', REGISTRATION_SUCCESS = 'REGISTRATION_SUCCESS', VIEW_ORDER_ANALYSIS = 'VIEW_ORDER_ANALYSIS', + VIEW_TEAM_ACCOUNT_DASHBOARD = 'VIEW_TEAM_ACCOUNT_DASHBOARD', } export const createPageViewLog = async ({ diff --git a/lib/utils.ts b/lib/utils.ts index 592732f..e056699 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -70,15 +70,13 @@ export function getBmiStatus( export function getBmiBackgroundColor(bmiStatus: BmiCategory | null): string { switch (bmiStatus) { case BmiCategory.UNDER_WEIGHT: + case BmiCategory.OVER_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'; + return 'bg-destructive'; default: return 'bg-success'; } diff --git a/packages/features/team-accounts/src/server/api.ts b/packages/features/team-accounts/src/server/api.ts index 44b0379..de706f1 100644 --- a/packages/features/team-accounts/src/server/api.ts +++ b/packages/features/team-accounts/src/server/api.ts @@ -291,6 +291,7 @@ export class TeamAccountsApi { .single(); if (error) { + console.warn('Error fetching company params', error); throw error; } diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index 9093fe4..14db6a3 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -1726,6 +1726,7 @@ export type Database = { primary_owner_user_id: string name: string email: string + personal_code: string picture_url: string created_at: string updated_at: string diff --git a/public/locales/et/common.json b/public/locales/et/common.json index fd3a5e9..2faf3e8 100644 --- a/public/locales/et/common.json +++ b/public/locales/et/common.json @@ -122,7 +122,8 @@ "weight": "Kaal", "height": "Pikkus", "occurance": "Toetuse sagedus", - "amount": "Summa" + "amount": "Summa", + "date": "Vali kuupäev" }, "wallet": { "balance": "Sinu MedReporti konto seis", diff --git a/public/locales/et/teams.json b/public/locales/et/teams.json index 515d538..f262a7a 100644 --- a/public/locales/et/teams.json +++ b/public/locales/et/teams.json @@ -1,7 +1,7 @@ { "home": { "pageTitle": "Ülevaade", - "headerTitle": "{{companyName}} tervise ülevaade", + "headerTitle": "{{companyName}} Tervisekassa kokkuvõte", "healthDetails": "Ettevõtte terviseandmed", "membersSettingsButtonTitle": "Halda töötajaid", "membersSettingsButtonDescription": "Lisa, muuda või eemalda töötajaid.", @@ -31,13 +31,14 @@ "volume": "Eelarve maht {{volume}}" }, "data": { - "reservations": "{{value}} broneeringut", + "reservations": "{{value}} teenust", "analysis": "Analüüsid", "doctorsAndSpecialists": "Eriarstid ja spetsialistid", "researches": "Uuringud", "healthResearchPlans": "Terviseuuringute paketid", "serviceUsage": "{{value}} teenuse kasutust", - "serviceSum": "Teenuste summa" + "serviceSum": "Teenuste summa", + "eclinic": "Digikliinik" } }, "healthDetails": { diff --git a/supabase/migrations/20250825110100_improve_members_queries.sql b/supabase/migrations/20250825110100_improve_members_queries.sql new file mode 100644 index 0000000..2674dd5 --- /dev/null +++ b/supabase/migrations/20250825110100_improve_members_queries.sql @@ -0,0 +1,48 @@ +DROP FUNCTION IF EXISTS medreport.get_account_members(text); + +CREATE OR REPLACE FUNCTION medreport.get_account_members(account_slug text) + RETURNS TABLE( + id uuid, + user_id uuid, + account_id uuid, + role character varying, + role_hierarchy_level integer, + primary_owner_user_id uuid, + name character varying, + email character varying, + personal_code text, + picture_url character varying, + created_at timestamp with time zone, + updated_at timestamp with time zone +) + LANGUAGE plpgsql + SET search_path TO '' +AS $function$begin + return QUERY + select + acc.id, + am.user_id, + am.account_id, + am.account_role, + r.hierarchy_level, + a.primary_owner_user_id, + acc.name, + acc.email, + acc.personal_code, + acc.picture_url, + am.created_at, + am.updated_at + from + medreport.accounts_memberships am + join medreport.accounts a on a.id = am.account_id + join medreport.accounts acc on acc.id = am.user_id + join medreport.roles r on r.name = am.account_role + where + a.slug = account_slug; + +end;$function$ +; + +grant + execute on function medreport.get_account_members (text) to authenticated, + service_role; \ No newline at end of file