feat(MED-97): update benefit stats view in dashboards

This commit is contained in:
2025-09-26 13:47:32 +03:00
parent fdc2e3e064
commit 1aeee0bc30
23 changed files with 518 additions and 374 deletions

View File

@@ -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>(companyParams);
const [isLoading, setIsLoading] = useState<boolean>(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 (
<Form {...form}>
<form
className="flex flex-col gap-6"
onSubmit={form.handleSubmit(onSubmit)}
>
<HealthBenefitFields />
<Button
type="submit"
className="relative"
disabled={!isDirty || isLoading}
>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center">
<Spinner />
</div>
)}
<div className={cn({ invisible: isLoading })}>
<Trans i18nKey="account:saveChanges" />
</div>
</Button>
</form>
</Form>
);
};
export default HealthBenefitFormClient;

View File

@@ -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<Database['medreport']['Tables']['company_params']['Row']>(
companyParams,
);
const [isLoading, setIsLoading] = useState<boolean>(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 (
<Form {...form}>
<form
className="flex flex-col gap-6 px-6 text-left"
onSubmit={form.handleSubmit(onSubmit)}
>
<div className="mt-8 flex items-center justify-between">
<div>
<h4>
<Trans
i18nKey="billing:pageTitle"
values={{ companyName: account.name }}
/>
</h4>
<p className="text-muted-foreground text-sm">
<Trans i18nKey="billing:description" />
</p>
</div>
<Button
type="submit"
className="relative"
disabled={!isDirty || isLoading}
>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center">
<Spinner />
</div>
)}
<div className={cn({ invisible: isLoading })}>
<Trans i18nKey="account:saveChanges" />
</div>
</Button>
</div>
<div className="flex flex-row gap-6">
<div className="border-border w-1/3 rounded-lg border">
<div className="p-6">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-orange-100">
<PiggyBankIcon className="h-[32px] w-[32px] stroke-orange-400 stroke-2" />
</div>
<p className="mt-4 text-sm font-medium">
<Trans i18nKey="billing:healthBenefitForm.description" />
</p>
<p className="pt-2 text-2xl font-semibold">
{currentCompanyParams.benefit_amount || 0}
</p>
</div>
<Separator />
<div className="p-6">
<HealthBenefitFields />
</div>
</div>
<div className="flex-1">
<YearlyExpensesOverview
employeeCount={employeeCount}
companyParams={currentCompanyParams}
<div className="flex flex-col gap-6 px-6 text-left">
<div className="mt-8 flex items-center justify-between">
<div>
<h4>
<Trans
i18nKey="billing:pageTitle"
values={{ companyName: account.name }}
/>
<p className="text-muted-foreground mt-2 text-sm">
<Trans i18nKey="billing:healthBenefitForm.info" />
</h4>
</div>
</div>
<div className="flex flex-row gap-6">
<div className="border-border w-1/3 rounded-lg border">
<div className="p-6">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-orange-100">
<PiggyBankIcon className="h-[32px] w-[32px] stroke-orange-400 stroke-2" />
</div>
<p className="mt-4 text-sm font-medium">
<Trans i18nKey="billing:healthBenefitForm.description" />
</p>
</div>
<Separator />
<div className="p-6">
<HealthBenefitFormClient
account={account}
companyParams={companyParams}
/>
</div>
</div>
</form>
</Form>
<div className="flex-1 space-y-6">
<YearlyExpensesOverview
employeeCount={employeeCount}
expensesOverview={expensesOverview}
/>
<p className="text-muted-foreground text-sm">
<Trans i18nKey="billing:healthBenefitForm.info" />
</p>
</div>
</div>
</div>
);
};

View File

@@ -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 (
<div className="border-border rounded-lg border p-6">
@@ -53,41 +22,56 @@ const YearlyExpensesOverview = ({
</h5>
<div className="mt-5 flex justify-between">
<p className="text-sm font-medium">
<Trans i18nKey="billing:expensesOverview.monthly" />
<Trans i18nKey="billing:expensesOverview.employeeCount" />
</p>
<span className="text-primary text-sm font-bold">
{monthlyExpensePerEmployee}
{employeeCount}
</span>
</div>
<div className="mt-3 flex justify-between">
<p className="text-sm font-medium">
<Trans i18nKey="billing:expensesOverview.yearly" />
</p>
<span className="text-sm font-medium">
{maxYearlyExpensePerEmployee}
</span>
</div>
<div className="mt-5 mb-3 flex justify-between">
<p className="text-sm font-medium">
<Trans
i18nKey="billing:expensesOverview.total"
values={{ employeeCount: employeeCount || 0 }}
i18nKey="billing:expensesOverview.managementFeeTotal"
values={{
managementFee: formatCurrency({
value: expensesOverview.managementFee,
locale: language,
currencyCode: 'EUR',
}),
}}
/>
</p>
<span className="text-sm font-medium">
{(Number(maxYearlyExpensePerEmployee) * employeeCount).toFixed(2)}
{formatCurrency({
value: expensesOverview.managementFeeTotal,
locale: language,
currencyCode: 'EUR',
})}
</span>
</div>
<div className="mt-3 mb-4 flex justify-between">
<p className="text-sm font-medium">
<Trans i18nKey="billing:expensesOverview.currentMonthUsageTotal" />
</p>
<span className="text-sm font-medium">
{formatCurrency({
value: expensesOverview.currentMonthUsageTotal,
locale: language,
currencyCode: 'EUR',
})}
</span>
</div>
<Separator />
<div className="mt-4 flex justify-between">
<p className="font-semibold">
<Trans i18nKey="billing:expensesOverview.sum" />
<Trans i18nKey="billing:expensesOverview.total" />
</p>
<span className="font-semibold">
{companyParams.benefit_amount
? companyParams.benefit_amount * employeeCount
: 0}{' '}
{formatCurrency({
value: expensesOverview.total,
locale: language,
currencyCode: 'EUR',
})}
</span>
</div>
</div>