From 1aeee0bc30960f8208fd32beb6768c975d2e3bc6 Mon Sep 17 00:00:00 2001 From: Karli Date: Fri, 26 Sep 2025 13:47:32 +0300 Subject: [PATCH] feat(MED-97): update benefit stats view in dashboards --- .../(user)/_components/dashboard-cards.tsx | 55 +++++- app/home/(user)/_components/dashboard.tsx | 8 +- .../team-account-benefit-statistics.tsx | 133 ++++---------- .../team-account-health-details.tsx | 6 +- .../_components/team-account-statistics.tsx | 70 +++----- ...-team-account-benefit-expenses-overview.ts | 75 ++++++++ .../load-team-account-benefit-statistics.ts | 96 ++++++++++ .../load-team-account-health-details.ts | 8 +- .../health-benefit-form-client.tsx | 97 ++++++++++ .../_components/health-benefit-form.tsx | 166 ++++++------------ .../_components/yearly-expenses-overview.tsx | 96 +++++----- app/home/[account]/billing/page.tsx | 10 +- app/home/[account]/members/page.tsx | 2 +- app/home/[account]/page.tsx | 7 +- lib/types/account-balance-entry.ts | 3 - lib/utils.ts | 7 +- .../features/accounts/src/types/accounts.ts | 32 ++-- packages/features/admin/package.json | 1 + .../src/components/admin-accounts-table.tsx | 4 +- .../lib/server/schema/admin-actions.schema.ts | 6 +- .../server/services/admin-accounts.service.ts | 3 +- .../webhooks/account-webhooks.service.ts | 4 +- pnpm-lock.yaml | 3 + 23 files changed, 518 insertions(+), 374 deletions(-) create mode 100644 app/home/[account]/_lib/server/load-team-account-benefit-expenses-overview.ts create mode 100644 app/home/[account]/_lib/server/load-team-account-benefit-statistics.ts create mode 100644 app/home/[account]/billing/_components/health-benefit-form-client.tsx delete mode 100644 lib/types/account-balance-entry.ts diff --git a/app/home/(user)/_components/dashboard-cards.tsx b/app/home/(user)/_components/dashboard-cards.tsx index 686afe9..f8adcb0 100644 --- a/app/home/(user)/_components/dashboard-cards.tsx +++ b/app/home/(user)/_components/dashboard-cards.tsx @@ -3,12 +3,33 @@ import Link from 'next/link'; import { ChevronRight, HeartPulse } from 'lucide-react'; import { Button } from '@kit/ui/button'; -import { Card, CardDescription, CardFooter, CardHeader } from '@kit/ui/card'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@kit/ui/card'; import { Trans } from '@kit/ui/trans'; +import { formatCurrency } from '@/packages/shared/src/utils'; +import { createI18nServerInstance } from '@/lib/i18n/i18n.server'; +import { cn } from '@kit/ui/lib/utils'; +import { loadCurrentUserAccount } from '../_lib/server/load-user-account'; +import { getAccountBalanceSummary } from '../_lib/server/balance-actions'; + +export default async function DashboardCards() { + const { language } = await createI18nServerInstance(); + + const { account } = await loadCurrentUserAccount(); + const balanceSummary = account ? await getAccountBalanceSummary(account.id) : null; -export default function DashboardCards() { return ( -
+
+ + + + + + + + + +
+ {formatCurrency({ + value: balanceSummary?.totalBalance || 0, + locale: language, + currencyCode: 'EUR', + })} +
+ + + +
+
); } + +function Figure(props: React.PropsWithChildren) { + return
{props.children}
; +} diff --git a/app/home/(user)/_components/dashboard.tsx b/app/home/(user)/_components/dashboard.tsx index 53e93ad..e0fbf7e 100644 --- a/app/home/(user)/_components/dashboard.tsx +++ b/app/home/(user)/_components/dashboard.tsx @@ -2,7 +2,6 @@ import Link from 'next/link'; -import { Database } from '@/packages/supabase/src/database.types'; import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons'; import { isNil } from 'lodash'; import { @@ -15,7 +14,7 @@ import { User, } from 'lucide-react'; -import type { AccountWithParams } from '@kit/accounts/types/accounts'; +import type { AccountWithParams, BmiThresholds } from '@kit/accounts/types/accounts'; import { pathsConfig } from '@kit/shared/config'; import { Button } from '@kit/ui/button'; import { @@ -138,10 +137,7 @@ export default function Dashboard({ bmiThresholds, }: { account: AccountWithParams; - bmiThresholds: Omit< - Database['medreport']['Tables']['bmi_thresholds']['Row'], - 'id' - >[]; + bmiThresholds: Omit[]; }) { const height = account.accountParams?.height || 0; const weight = account.accountParams?.weight || 0; diff --git a/app/home/[account]/_components/team-account-benefit-statistics.tsx b/app/home/[account]/_components/team-account-benefit-statistics.tsx index dd4d983..035ff9d 100644 --- a/app/home/[account]/_components/team-account-benefit-statistics.tsx +++ b/app/home/[account]/_components/team-account-benefit-statistics.tsx @@ -1,23 +1,14 @@ import React 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 { PiggyBankIcon } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { createPath, pathsConfig } from '@kit/shared/config'; 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'; -interface TeamAccountBenefitStatisticsProps { - employeeCount: number; - accountSlug: string; - companyParams: Database['medreport']['Tables']['company_params']['Row']; -} +import { AccountBenefitStatistics } from '../_lib/server/load-team-account-benefit-statistics'; const StatisticsCard = ({ children }: { children: React.ReactNode }) => { return {children}; @@ -46,10 +37,10 @@ const StatisticsValue = ({ children }: { children: React.ReactNode }) => { }; const TeamAccountBenefitStatistics = ({ - employeeCount, - accountSlug, - companyParams, -}: TeamAccountBenefitStatisticsProps) => { + accountBenefitStatistics, +}: { + accountBenefitStatistics: AccountBenefitStatistics; +}) => { const { i18n: { language }, } = useTranslation(); @@ -58,114 +49,64 @@ const TeamAccountBenefitStatistics = ({
-
-

- -

-

- -

- - - + + + + + {formatCurrency({ + value: accountBenefitStatistics.periodTotal, + locale: language, + currencyCode: 'EUR', + })} +
- + - 1800 € + + {formatCurrency({ + value: accountBenefitStatistics.orders.totalSum, + locale: language, + currencyCode: 'EUR', + })} + + - 200 € + {accountBenefitStatistics.orders.analysesSum} € - - - - - - - 200 € - - - - - - - - - 200 € - - - - - - - - - 200 € - - - + - 200 € + + {formatCurrency({ + value: accountBenefitStatistics.orders.analysisPackagesSum, + locale: language, + currencyCode: 'EUR', + })} + diff --git a/app/home/[account]/_components/team-account-health-details.tsx b/app/home/[account]/_components/team-account-health-details.tsx index 666a029..547a8a8 100644 --- a/app/home/[account]/_components/team-account-health-details.tsx +++ b/app/home/[account]/_components/team-account-health-details.tsx @@ -5,6 +5,7 @@ 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 type { BmiThresholds } from '@kit/accounts/types/accounts'; import { getAccountHealthDetailsFields } from '../_lib/server/load-team-account-health-details'; import { TeamAccountStatisticsProps } from './team-account-statistics'; @@ -15,10 +16,7 @@ const TeamAccountHealthDetails = ({ members, }: { memberParams: TeamAccountStatisticsProps['memberParams']; - bmiThresholds: Omit< - Database['medreport']['Tables']['bmi_thresholds']['Row'], - 'id' - >[]; + bmiThresholds: Omit[]; members: Database['medreport']['Functions']['get_account_members']['Returns']; }) => { const accountHealthDetailsFields = getAccountHealthDetailsFields( diff --git a/app/home/[account]/_components/team-account-statistics.tsx b/app/home/[account]/_components/team-account-statistics.tsx index fe8af56..bae53a4 100644 --- a/app/home/[account]/_components/team-account-statistics.tsx +++ b/app/home/[account]/_components/team-account-statistics.tsx @@ -14,28 +14,19 @@ import { createPath, pathsConfig } from '@kit/shared/config'; 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 { DateRange } from '@kit/ui/shadcn/calendar'; +import { AccountBenefitStatistics } from '../_lib/server/load-team-account-benefit-statistics'; import TeamAccountBenefitStatistics from './team-account-benefit-statistics'; import TeamAccountHealthDetails from './team-account-health-details'; +import type { Account, AccountParams, BmiThresholds } from '@/packages/features/accounts/src/types/accounts'; export interface TeamAccountStatisticsProps { - teamAccount: Database['medreport']['Tables']['accounts']['Row']; - memberParams: Pick< - Database['medreport']['Tables']['account_params']['Row'], - 'weight' | 'height' - >[]; - bmiThresholds: Omit< - Database['medreport']['Tables']['bmi_thresholds']['Row'], - 'id' - >[]; + teamAccount: Account; + memberParams: Pick[]; + bmiThresholds: Omit[]; members: Database['medreport']['Functions']['get_account_members']['Returns']; - companyParams: Database['medreport']['Tables']['company_params']['Row']; + accountBenefitStatistics: AccountBenefitStatistics; } export default function TeamAccountStatistics({ @@ -43,11 +34,12 @@ export default function TeamAccountStatistics({ memberParams, bmiThresholds, members, - companyParams, + accountBenefitStatistics, }: TeamAccountStatisticsProps) { + const currentDate = new Date(); const [date, setDate] = useState({ - from: new Date(), - to: new Date(), + from: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1), + to: new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0), }); const { i18n: { language }, @@ -66,28 +58,16 @@ export default function TeamAccountStatistics({ /> - - - - - - - - +
- +
@@ -148,7 +124,7 @@ export default function TeamAccountStatistics({ redirect( createPath( pathsConfig.app.accountBilling, - teamAccount.slug || '', + teamAccount.slug!, ), ) } diff --git a/app/home/[account]/_lib/server/load-team-account-benefit-expenses-overview.ts b/app/home/[account]/_lib/server/load-team-account-benefit-expenses-overview.ts new file mode 100644 index 0000000..93548d7 --- /dev/null +++ b/app/home/[account]/_lib/server/load-team-account-benefit-expenses-overview.ts @@ -0,0 +1,75 @@ +import { getSupabaseServerClient } from "@/packages/supabase/src/clients/server-client"; +import { loadAccountBenefitStatistics, loadCompanyPersonalAccountsBalanceEntries } from "./load-team-account-benefit-statistics"; + +export interface TeamAccountBenefitExpensesOverview { + benefitAmount: number | null; + benefitOccurrence: 'yearly' | 'monthly' | 'quarterly' | null; + currentMonthUsageTotal: number; + managementFee: number; + managementFeeTotal: number; + total: number; +} + +const MANAGEMENT_FEE = 5.50; + +const MONTHS = 12; +const QUARTERS = 4; + +export async function loadTeamAccountBenefitExpensesOverview({ + companyId, + employeeCount, +}: { + companyId: string; + employeeCount: number; +}): Promise { + const supabase = getSupabaseServerClient(); + const { data, error } = await supabase + .schema('medreport') + .from('benefit_distribution_schedule') + .select('*') + .eq('company_id', companyId) + .eq('is_active', true) + .single(); + + let benefitAmount: TeamAccountBenefitExpensesOverview['benefitAmount'] = null; + let benefitOccurrence: TeamAccountBenefitExpensesOverview['benefitOccurrence'] = null; + if (error) { + console.warn('Failed to load team account benefit expenses overview'); + } else { + benefitAmount = data.benefit_amount as TeamAccountBenefitExpensesOverview['benefitAmount']; + benefitOccurrence = data.benefit_occurrence as TeamAccountBenefitExpensesOverview['benefitOccurrence']; + } + + const { purchaseEntriesTotal } = await loadCompanyPersonalAccountsBalanceEntries({ accountId: companyId }); + + return { + benefitAmount, + benefitOccurrence, + currentMonthUsageTotal: purchaseEntriesTotal, + managementFee: MANAGEMENT_FEE, + managementFeeTotal: MANAGEMENT_FEE * employeeCount, + total: (() => { + if (typeof benefitAmount !== 'number') { + return 0; + } + + const currentDate = new Date(); + const createdAt = new Date(data.created_at); + const isCreatedThisYear = createdAt.getFullYear() === currentDate.getFullYear(); + if (benefitOccurrence === 'yearly') { + return benefitAmount * employeeCount; + } else if (benefitOccurrence === 'monthly') { + const monthsLeft = isCreatedThisYear + ? MONTHS - createdAt.getMonth() + : MONTHS; + return benefitAmount * employeeCount * monthsLeft; + } else if (benefitOccurrence === 'quarterly') { + const quartersLeft = isCreatedThisYear + ? QUARTERS - (createdAt.getMonth() / 3) + : QUARTERS; + return benefitAmount * employeeCount * quartersLeft; + } + return 0; + })(), + } +} diff --git a/app/home/[account]/_lib/server/load-team-account-benefit-statistics.ts b/app/home/[account]/_lib/server/load-team-account-benefit-statistics.ts new file mode 100644 index 0000000..05f51f0 --- /dev/null +++ b/app/home/[account]/_lib/server/load-team-account-benefit-statistics.ts @@ -0,0 +1,96 @@ +'use server'; + +import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; + +export interface AccountBenefitStatistics { + benefitDistributionSchedule: { + amount: number; + }; + companyAccountsCount: number; + periodTotal: number; + orders: { + totalSum: number; + + analysesCount: number; + analysesSum: number; + + analysisPackagesCount: number; + analysisPackagesSum: number; + } +} + +export const loadCompanyPersonalAccountsBalanceEntries = async ({ + accountId, +}: { + accountId: string; +}) => { + const supabase = getSupabaseServerAdminClient(); + + const { count, data: accountMemberships } = await supabase + .schema('medreport') + .from('accounts_memberships') + .select('user_id') + .eq('account_id', accountId) + .throwOnError(); + + const { data: accountBalanceEntries } = await supabase + .schema('medreport') + .from('account_balance_entries') + .select('*') + .eq('is_active', true) + .in('account_id', accountMemberships.map(({ user_id }) => user_id)) + .throwOnError(); + + const purchaseEntries = accountBalanceEntries.filter(({ entry_type }) => entry_type === 'purchase'); + const analysesEntries = purchaseEntries.filter(({ is_analysis_order }) => is_analysis_order); + const analysisPackagesEntries = purchaseEntries.filter(({ is_analysis_package_order }) => is_analysis_package_order); + + return { + accountBalanceEntries, + analysesEntries, + analysisPackagesEntries, + companyAccountsCount: count || 0, + purchaseEntries, + purchaseEntriesTotal: purchaseEntries.reduce((acc, { amount }) => acc + Math.abs(amount || 0), 0), + }; +} + +export const loadAccountBenefitStatistics = async ( + accountId: string, +): Promise => { + const supabase = getSupabaseServerAdminClient(); + + const { + analysesEntries, + analysisPackagesEntries, + companyAccountsCount, + purchaseEntriesTotal, + } = await loadCompanyPersonalAccountsBalanceEntries({ accountId }); + + const { data: benefitDistributionSchedule } = await supabase + .schema('medreport') + .from('benefit_distribution_schedule') + .select('*') + .eq('company_id', accountId) + .eq('is_active', true) + .single() + .throwOnError(); + + const scheduleAmount = benefitDistributionSchedule?.benefit_amount || 0; + return { + companyAccountsCount, + benefitDistributionSchedule: { + amount: benefitDistributionSchedule?.benefit_amount || 0, + }, + periodTotal: scheduleAmount * companyAccountsCount, + orders: { + totalSum: purchaseEntriesTotal, + + analysesCount: analysesEntries.length, + analysesSum: analysesEntries.reduce((acc, { amount }) => acc + Math.abs(amount || 0), 0), + + analysisPackagesCount: analysisPackagesEntries.length, + analysisPackagesSum: analysisPackagesEntries.reduce((acc, { amount }) => acc + Math.abs(amount || 0), 0), + }, + }; +}; 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 1705770..9568dda 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 @@ -11,6 +11,7 @@ import { } from '~/lib/utils'; import { TeamAccountStatisticsProps } from '../../_components/team-account-statistics'; +import type { BmiThresholds } from '@kit/accounts/types/accounts'; interface AccountHealthDetailsField { title: string; @@ -25,10 +26,7 @@ interface AccountHealthDetailsField { export const getAccountHealthDetailsFields = ( memberParams: TeamAccountStatisticsProps['memberParams'], - bmiThresholds: Omit< - Database['medreport']['Tables']['bmi_thresholds']['Row'], - 'id' - >[], + bmiThresholds: Omit[], members: Database['medreport']['Functions']['get_account_members']['Returns'], ): AccountHealthDetailsField[] => { const averageWeight = @@ -82,7 +80,7 @@ export const getAccountHealthDetailsFields = ( }, { title: 'teams:healthDetails.bmi', - value: averageBMI, + value: averageBMI!, Icon: TrendingUp, iconBg: getBmiBackgroundColor(bmiStatus), }, diff --git a/app/home/[account]/billing/_components/health-benefit-form-client.tsx b/app/home/[account]/billing/_components/health-benefit-form-client.tsx new file mode 100644 index 0000000..8812683 --- /dev/null +++ b/app/home/[account]/billing/_components/health-benefit-form-client.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { useState } from 'react'; + +import { UpdateHealthBenefitSchema } from '@/packages/billing/core/src/schema'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; + +import { Button } from '@kit/ui/button'; +import { Form } from '@kit/ui/form'; +import { Spinner } from '@kit/ui/makerkit/spinner'; +import { toast } from '@kit/ui/shadcn/sonner'; +import { Trans } from '@kit/ui/trans'; + +import { cn } from '~/lib/utils'; + +import { updateHealthBenefit } from '../_lib/server/server-actions'; +import HealthBenefitFields from './health-benefit-fields'; +import { Account, CompanyParams } from '@/packages/features/accounts/src/types/accounts'; +import { useTranslation } from 'react-i18next'; + +const HealthBenefitFormClient = ({ + account, + companyParams, +}: { + account: Account; + companyParams: CompanyParams; +}) => { + const { t } = useTranslation('account'); + + const [currentCompanyParams, setCurrentCompanyParams] = + useState(companyParams); + const [isLoading, setIsLoading] = useState(false); + + const form = useForm({ + resolver: zodResolver(UpdateHealthBenefitSchema), + mode: 'onChange', + defaultValues: { + occurrence: currentCompanyParams.benefit_occurance || 'yearly', + amount: currentCompanyParams.benefit_amount || 0, + }, + }); + + const isDirty = form.formState.isDirty; + + const onSubmit = (data: { occurrence: string; amount: number }) => { + const promise = async () => { + setIsLoading(true); + try { + await updateHealthBenefit({ ...data, accountId: account.id }); + setCurrentCompanyParams((prev) => ({ + ...prev, + benefit_amount: data.amount, + benefit_occurance: data.occurrence, + })); + } finally { + form.reset(data); + setIsLoading(false); + } + }; + + toast.promise(promise, { + success: t('account:healthBenefitForm.updateSuccess'), + error: 'error', + }); + }; + + return ( +
+ + + + + + + ); +}; + +export default HealthBenefitFormClient; + + diff --git a/app/home/[account]/billing/_components/health-benefit-form.tsx b/app/home/[account]/billing/_components/health-benefit-form.tsx index a21a72e..d49ccd2 100644 --- a/app/home/[account]/billing/_components/health-benefit-form.tsx +++ b/app/home/[account]/billing/_components/health-benefit-form.tsx @@ -1,138 +1,70 @@ -'use client'; - -import { useState } from 'react'; - -import { UpdateHealthBenefitSchema } from '@/packages/billing/core/src/schema'; -import { Database } from '@/packages/supabase/src/database.types'; -import { zodResolver } from '@hookform/resolvers/zod'; import { PiggyBankIcon } from 'lucide-react'; -import { useForm } from 'react-hook-form'; -import { Button } from '@kit/ui/button'; -import { Form } from '@kit/ui/form'; -import { Spinner } from '@kit/ui/makerkit/spinner'; import { Separator } from '@kit/ui/shadcn/separator'; -import { toast } from '@kit/ui/shadcn/sonner'; import { Trans } from '@kit/ui/trans'; -import { cn } from '~/lib/utils'; - -import { updateHealthBenefit } from '../_lib/server/server-actions'; -import HealthBenefitFields from './health-benefit-fields'; +import HealthBenefitFormClient from './health-benefit-form-client'; import YearlyExpensesOverview from './yearly-expenses-overview'; +import { TeamAccountBenefitExpensesOverview } from '../../_lib/server/load-team-account-benefit-expenses-overview'; +import { Account, CompanyParams } from '@/packages/features/accounts/src/types/accounts'; -const HealthBenefitForm = ({ +const HealthBenefitForm = async ({ account, companyParams, employeeCount, + expensesOverview, }: { - account: Database['medreport']['Tables']['accounts']['Row']; - companyParams: Database['medreport']['Tables']['company_params']['Row']; + account: Account; + companyParams: CompanyParams; employeeCount: number; + expensesOverview: TeamAccountBenefitExpensesOverview; }) => { - const [currentCompanyParams, setCurrentCompanyParams] = - useState( - companyParams, - ); - const [isLoading, setIsLoading] = useState(false); - const form = useForm({ - resolver: zodResolver(UpdateHealthBenefitSchema), - mode: 'onChange', - defaultValues: { - occurrence: currentCompanyParams.benefit_occurance || 'yearly', - amount: currentCompanyParams.benefit_amount || 0, - }, - }); - const isDirty = form.formState.isDirty; - - const onSubmit = (data: { occurrence: string; amount: number }) => { - const promise = async () => { - setIsLoading(true); - try { - await updateHealthBenefit({ ...data, accountId: account.id }); - setCurrentCompanyParams((prev) => ({ - ...prev, - benefit_amount: data.amount, - benefit_occurance: data.occurrence, - })); - } finally { - form.reset(data); - setIsLoading(false); - } - }; - - toast.promise(promise, { - success: 'Andmed uuendatud', - error: 'error', - }); - }; - return ( -
- -
-
-

- -

-

- -

-
- -
-
-
-
-
- -
-

- -

-

- {currentCompanyParams.benefit_amount || 0} € -

-
- - - -
- -
-
- -
- +
+
+

+ -

- +

+
+
+ +
+
+
+
+ +
+

+

+ + + +
+ +
- - + +
+ + +

+ +

+
+
+
); }; diff --git a/app/home/[account]/billing/_components/yearly-expenses-overview.tsx b/app/home/[account]/billing/_components/yearly-expenses-overview.tsx index 36a42be..afd3d21 100644 --- a/app/home/[account]/billing/_components/yearly-expenses-overview.tsx +++ b/app/home/[account]/billing/_components/yearly-expenses-overview.tsx @@ -1,50 +1,19 @@ -import { useMemo } from 'react'; - -import { Database } from '@/packages/supabase/src/database.types'; +'use client'; import { Trans } from '@kit/ui/makerkit/trans'; import { Separator } from '@kit/ui/separator'; +import { formatCurrency } from '@/packages/shared/src/utils'; +import { useTranslation } from 'react-i18next'; +import { TeamAccountBenefitExpensesOverview } from '../../_lib/server/load-team-account-benefit-expenses-overview'; const YearlyExpensesOverview = ({ employeeCount = 0, - companyParams, + expensesOverview, }: { employeeCount?: number; - companyParams: Database['medreport']['Tables']['company_params']['Row']; + expensesOverview: TeamAccountBenefitExpensesOverview; }) => { - const monthlyExpensePerEmployee = useMemo(() => { - if (!companyParams.benefit_amount) { - return '0.00'; - } - - switch (companyParams.benefit_occurance) { - case 'yearly': - return (companyParams.benefit_amount / 12).toFixed(2); - case 'quarterly': - return (companyParams.benefit_amount / 3).toFixed(2); - case 'monthly': - return companyParams.benefit_amount.toFixed(2); - default: - return '0.00'; - } - }, [companyParams]); - - const maxYearlyExpensePerEmployee = useMemo(() => { - if (!companyParams.benefit_amount) { - return '0.00'; - } - - switch (companyParams.benefit_occurance) { - case 'yearly': - return companyParams.benefit_amount.toFixed(2); - case 'quarterly': - return (companyParams.benefit_amount * 3).toFixed(2); - case 'monthly': - return (companyParams.benefit_amount * 12).toFixed(2); - default: - return '0.00'; - } - }, [companyParams]); + const { i18n: { language } } = useTranslation(); return (
@@ -53,41 +22,56 @@ const YearlyExpensesOverview = ({

- +

- {monthlyExpensePerEmployee} € + {employeeCount}
-

- -

- - {maxYearlyExpensePerEmployee} € - -
-

- {(Number(maxYearlyExpensePerEmployee) * employeeCount).toFixed(2)} € + {formatCurrency({ + value: expensesOverview.managementFeeTotal, + locale: language, + currencyCode: 'EUR', + })} + +
+
+

+ +

+ + {formatCurrency({ + value: expensesOverview.currentMonthUsageTotal, + locale: language, + currencyCode: 'EUR', + })}

- +

- {companyParams.benefit_amount - ? companyParams.benefit_amount * employeeCount - : 0}{' '} - € + {formatCurrency({ + value: expensesOverview.total, + locale: language, + currencyCode: 'EUR', + })}
diff --git a/app/home/[account]/billing/page.tsx b/app/home/[account]/billing/page.tsx index bc8288b..ed06890 100644 --- a/app/home/[account]/billing/page.tsx +++ b/app/home/[account]/billing/page.tsx @@ -7,6 +7,7 @@ import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; import HealthBenefitForm from './_components/health-benefit-form'; +import { loadTeamAccountBenefitExpensesOverview } from '../_lib/server/load-team-account-benefit-expenses-overview'; interface TeamAccountBillingPageProps { params: Promise<{ account: string }>; @@ -27,8 +28,14 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) { const api = createTeamAccountsApi(client); const account = await api.getTeamAccount(accountSlug); - const companyParams = await api.getTeamAccountParams(account.id); const { members } = await api.getMembers(accountSlug); + const [expensesOverview, companyParams] = await Promise.all([ + loadTeamAccountBenefitExpensesOverview({ + companyId: account.id, + employeeCount: members.length, + }), + api.getTeamAccountParams(account.id), + ]); return ( @@ -36,6 +43,7 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) { account={account} companyParams={companyParams} employeeCount={members.length} + expensesOverview={expensesOverview} /> ); diff --git a/app/home/[account]/members/page.tsx b/app/home/[account]/members/page.tsx index 324e6b0..fc11cea 100644 --- a/app/home/[account]/members/page.tsx +++ b/app/home/[account]/members/page.tsx @@ -54,7 +54,7 @@ async function TeamAccountMembersPage({ params }: TeamAccountMembersPageProps) { return ( <> } + title={} description={} /> diff --git a/app/home/[account]/page.tsx b/app/home/[account]/page.tsx index 7283798..5cff5d3 100644 --- a/app/home/[account]/page.tsx +++ b/app/home/[account]/page.tsx @@ -17,6 +17,7 @@ import { } from '~/lib/services/audit/pageView.service'; import { Dashboard } from './_components/dashboard'; +import { loadAccountBenefitStatistics } from './_lib/server/load-team-account-benefit-statistics'; interface TeamAccountHomePageProps { params: Promise<{ account: string }>; @@ -39,9 +40,7 @@ function TeamAccountHomePage({ params }: TeamAccountHomePageProps) { const teamAccount = use(teamAccountsApi.getTeamAccount(account)); const { memberParams, members } = use(teamAccountsApi.getMembers(account)); const bmiThresholds = use(userAnalysesApi.fetchBmiThresholds()); - const companyParams = use( - teamAccountsApi.getTeamAccountParams(teamAccount.id), - ); + const accountBenefitStatistics = use(loadAccountBenefitStatistics(teamAccount.id)); use( createPageViewLog({ @@ -57,7 +56,7 @@ function TeamAccountHomePage({ params }: TeamAccountHomePageProps) { memberParams={memberParams} bmiThresholds={bmiThresholds} members={members} - companyParams={companyParams} + accountBenefitStatistics={accountBenefitStatistics} /> ); diff --git a/lib/types/account-balance-entry.ts b/lib/types/account-balance-entry.ts deleted file mode 100644 index 434e5e6..0000000 --- a/lib/types/account-balance-entry.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { Database } from "@/packages/supabase/src/database.types"; - -export type AccountBalanceEntry = Database['medreport']['Tables']['account_balance_entries']['Row']; diff --git a/lib/utils.ts b/lib/utils.ts index d9d0f96..7d2f9ad 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,9 +1,9 @@ -import { Database } from '@/packages/supabase/src/database.types'; import { type ClassValue, clsx } from 'clsx'; import Isikukood, { Gender } from 'isikukood'; import { twMerge } from 'tailwind-merge'; import { BmiCategory } from './types/bmi'; +import type { BmiThresholds } from '@kit/accounts/types/accounts'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -45,10 +45,7 @@ export const bmiFromMetric = (kg: number, cm: number) => { }; export function getBmiStatus( - thresholds: Omit< - Database['medreport']['Tables']['bmi_thresholds']['Row'], - 'id' - >[], + thresholds: Omit[], params: { age: number; height: number; weight: number }, ): BmiCategory | null { const age = params.age; diff --git a/packages/features/accounts/src/types/accounts.ts b/packages/features/accounts/src/types/accounts.ts index bc305c0..430c898 100644 --- a/packages/features/accounts/src/types/accounts.ts +++ b/packages/features/accounts/src/types/accounts.ts @@ -1,23 +1,25 @@ import { Database } from '@kit/supabase/database'; -export type ApplicationRole = - Database['medreport']['Tables']['accounts']['Row']['application_role']; +export type ApplicationRole = Account['application_role']; export enum ApplicationRoleEnum { User = 'user', Doctor = 'doctor', SuperAdmin = 'super_admin', } -export type AccountWithParams = - Database['medreport']['Tables']['accounts']['Row'] & { - accountParams: - | (Pick< - Database['medreport']['Tables']['account_params']['Row'], - 'weight' | 'height' - > & { - isSmoker: - | Database['medreport']['Tables']['account_params']['Row']['is_smoker'] - | null; - }) - | null; - }; +export type AccountParams = + Database['medreport']['Tables']['account_params']['Row']; + +export type Account = Database['medreport']['Tables']['accounts']['Row']; +export type AccountWithParams = Account & { + accountParams: + | (Pick & { + isSmoker: AccountParams['is_smoker'] | null; + }) + | null; +}; + +export type CompanyParams = + Database['medreport']['Tables']['company_params']['Row']; + +export type BmiThresholds = Database['medreport']['Tables']['bmi_thresholds']['Row']; diff --git a/packages/features/admin/package.json b/packages/features/admin/package.json index 72da143..d07673d 100644 --- a/packages/features/admin/package.json +++ b/packages/features/admin/package.json @@ -9,6 +9,7 @@ "devDependencies": { "@hookform/resolvers": "^5.0.1", "@kit/next": "workspace:*", + "@kit/accounts": "workspace:*", "@kit/shared": "workspace:*", "@kit/supabase": "workspace:*", "@kit/tsconfig": "workspace:*", diff --git a/packages/features/admin/src/components/admin-accounts-table.tsx b/packages/features/admin/src/components/admin-accounts-table.tsx index ab7f0c1..d993ab7 100644 --- a/packages/features/admin/src/components/admin-accounts-table.tsx +++ b/packages/features/admin/src/components/admin-accounts-table.tsx @@ -11,7 +11,7 @@ import { EllipsisVertical } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; -import { Database } from '@kit/supabase/database'; +import type { Account } from '@kit/accounts/types/accounts'; import { Button } from '@kit/ui/button'; import { Checkbox } from '@kit/ui/checkbox'; import { @@ -44,8 +44,6 @@ import { AdminDeleteUserDialog } from './admin-delete-user-dialog'; import { AdminImpersonateUserDialog } from './admin-impersonate-user-dialog'; import { AdminResetPasswordDialog } from './admin-reset-password-dialog'; -type Account = Database['medreport']['Tables']['accounts']['Row']; - const FiltersSchema = z.object({ type: z.enum(['all', 'team', 'personal']), query: z.string().optional(), diff --git a/packages/features/admin/src/lib/server/schema/admin-actions.schema.ts b/packages/features/admin/src/lib/server/schema/admin-actions.schema.ts index 8edd356..aa5d86d 100644 --- a/packages/features/admin/src/lib/server/schema/admin-actions.schema.ts +++ b/packages/features/admin/src/lib/server/schema/admin-actions.schema.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { Database } from '@kit/supabase/database'; +import { ApplicationRole } from '@kit/accounts/types/accounts'; const ConfirmationSchema = z.object({ confirmation: z.custom((value) => value === 'CONFIRM'), @@ -19,9 +19,7 @@ export const DeleteAccountSchema = ConfirmationSchema.extend({ accountId: z.string().uuid(), }); -type ApplicationRoleType = - Database['medreport']['Tables']['accounts']['Row']['application_role']; export const UpdateAccountRoleSchema = z.object({ accountId: z.string().uuid(), - role: z.string() as z.ZodType, + role: z.string() as z.ZodType, }); diff --git a/packages/features/admin/src/lib/server/services/admin-accounts.service.ts b/packages/features/admin/src/lib/server/services/admin-accounts.service.ts index c46bc03..6f26b58 100644 --- a/packages/features/admin/src/lib/server/services/admin-accounts.service.ts +++ b/packages/features/admin/src/lib/server/services/admin-accounts.service.ts @@ -3,6 +3,7 @@ import 'server-only'; import { SupabaseClient } from '@supabase/supabase-js'; import { Database } from '@kit/supabase/database'; +import type { ApplicationRole } from '@kit/accounts/types/accounts'; export function createAdminAccountsService(client: SupabaseClient) { return new AdminAccountsService(client); @@ -25,7 +26,7 @@ class AdminAccountsService { async updateRole( accountId: string, - role: Database['medreport']['Tables']['accounts']['Row']['application_role'], + role: ApplicationRole, ) { const { error } = await this.adminClient .schema('medreport') diff --git a/packages/features/team-accounts/src/server/services/webhooks/account-webhooks.service.ts b/packages/features/team-accounts/src/server/services/webhooks/account-webhooks.service.ts index 9bdc5a3..d1d6aa4 100644 --- a/packages/features/team-accounts/src/server/services/webhooks/account-webhooks.service.ts +++ b/packages/features/team-accounts/src/server/services/webhooks/account-webhooks.service.ts @@ -1,9 +1,7 @@ import { z } from 'zod'; import { getLogger } from '@kit/shared/logger'; -import { Database } from '@kit/supabase/database'; - -type Account = Database['medreport']['Tables']['accounts']['Row']; +import type { Account } from '@kit/accounts/types/accounts'; export function createAccountWebhooksService() { return new AccountWebhooksService(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c396f6..b000d26 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -647,6 +647,9 @@ importers: '@hookform/resolvers': specifier: ^5.0.1 version: 5.2.1(react-hook-form@7.62.0(react@19.1.0)) + '@kit/accounts': + specifier: workspace:* + version: link:../accounts '@kit/next': specifier: workspace:* version: link:../../next