feat(billing): enhance health benefit form and yearly expenses overview with employee count and loading state

This commit is contained in:
Danel Kungla
2025-08-11 15:04:16 +03:00
parent 6e63646944
commit 9298abe354
6 changed files with 79 additions and 39 deletions

View File

@@ -1,5 +1,7 @@
'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';
@@ -22,32 +24,52 @@ import YearlyExpensesOverview from './yearly-expenses-overview';
const HealthBenefitForm = ({
account,
companyParams,
employeeCount,
}: {
account: Database['medreport']['Tables']['accounts']['Row'];
companyParams: Database['medreport']['Tables']['company_params']['Row'];
employeeCount: number;
}) => {
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: {
occurance: companyParams.benefit_occurance || 'yearly',
amount: companyParams.benefit_amount || 0,
occurance: currentCompanyParams.benefit_occurance || 'yearly',
amount: currentCompanyParams.benefit_amount || 0,
},
});
const onSubmit = (data: { occurance: 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.occurance,
}));
} finally {
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((data) => {
toast.promise(
() => updateHealthBenefit({ ...data, accountId: account.id }),
{
success: 'success',
error: 'error',
},
);
})}
onSubmit={form.handleSubmit(onSubmit)}
>
<div className="mt-8 flex items-center justify-between">
<div>
@@ -61,13 +83,13 @@ const HealthBenefitForm = ({
<Trans i18nKey="billing:description" />
</p>
</div>
<Button type="submit" className="relative">
{form.formState.isSubmitting && (
<Button type="submit" className="relative" disabled={isLoading}>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center">
<Spinner />
</div>
)}
<div className={cn({ invisible: form.formState.isSubmitting })}>
<div className={cn({ invisible: isLoading })}>
<Trans i18nKey="account:saveChanges" />
</div>
</Button>
@@ -82,7 +104,7 @@ const HealthBenefitForm = ({
<Trans i18nKey="billing:healthBenefitForm.description" />
</p>
<p className="pt-2 text-2xl font-semibold">
{companyParams.benefit_amount || 0}
{currentCompanyParams.benefit_amount || 0}
</p>
</div>
@@ -95,8 +117,8 @@ const HealthBenefitForm = ({
<div className="flex-1">
<YearlyExpensesOverview
employeeCount={22}
companyParams={companyParams}
employeeCount={employeeCount}
companyParams={currentCompanyParams}
/>
<p className="text-muted-foreground mt-2 text-sm">
<Trans i18nKey="billing:healthBenefitForm.info" />

View File

@@ -21,7 +21,7 @@ const YearlyExpensesOverview = ({
case 'yearly':
return (companyParams.benefit_amount / 12).toFixed(2);
case 'quarterly':
return (companyParams.benefit_amount / 4).toFixed(2);
return (companyParams.benefit_amount / 3).toFixed(2);
case 'monthly':
return companyParams.benefit_amount.toFixed(2);
default:
@@ -38,7 +38,7 @@ const YearlyExpensesOverview = ({
case 'yearly':
return companyParams.benefit_amount.toFixed(2);
case 'quarterly':
return (companyParams.benefit_amount * 4).toFixed(2);
return (companyParams.benefit_amount * 3).toFixed(2);
case 'monthly':
return (companyParams.benefit_amount * 12).toFixed(2);
default:
@@ -80,8 +80,15 @@ const YearlyExpensesOverview = ({
</div>
<Separator />
<div className="mt-4 flex justify-between">
<p className="font-semibold">Kokku</p>
<span className="font-semibold">13 200,00 </span>
<p className="font-semibold">
<Trans i18nKey="billing:expensesOverview.sum" />
</p>
<span className="font-semibold">
{companyParams.benefit_amount
? companyParams.benefit_amount * employeeCount
: 0}{' '}
</span>
</div>
</div>
);

View File

@@ -1,3 +1,4 @@
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
import { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
@@ -23,13 +24,20 @@ export const generateMetadata = async () => {
async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
const accountSlug = (await params).account;
const api = createTeamAccountsApi(getSupabaseServerClient());
const client = getSupabaseServerClient();
const api = createTeamAccountsApi(client);
const account = await api.getTeamAccount(accountSlug);
const companyParams = await api.getTeamAccountParams(account.id);
const accounts = await api.getMembers(accountSlug);
return (
<PageBody>
<HealthBenefitForm account={account} companyParams={companyParams} />
<HealthBenefitForm
account={account}
companyParams={companyParams}
employeeCount={accounts.length}
/>
</PageBody>
);
}

View File

@@ -3,6 +3,7 @@ import { BadgeX, Ban, ShieldPlus, VenetianMask } from 'lucide-react';
import { Tables } from '@kit/supabase/database';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createTeamAccountsApi } from '@kit/team-accounts/api';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Badge } from '@kit/ui/badge';
@@ -162,7 +163,8 @@ async function PersonalAccountPage(props: { account: Account }) {
async function TeamAccountPage(props: {
account: Account & { memberships: Membership[] };
}) {
const members = await getMembers(props.account.slug ?? '');
const api = createTeamAccountsApi(getSupabaseServerClient());
const members = await api.getMembers(props.account.slug ?? '');
return (
<>
@@ -386,17 +388,3 @@ async function getMemberships(userId: string) {
return memberships.data;
}
async function getMembers(accountSlug: string) {
const client = getSupabaseServerClient();
const members = await client.schema('medreport').rpc('get_account_members', {
account_slug: accountSlug,
});
if (members.error) {
throw members.error;
}
return members.data;
}

View File

@@ -295,6 +295,20 @@ export class TeamAccountsApi {
return data;
}
async getMembers(accountSlug: string) {
const members = await this.client
.schema('medreport')
.rpc('get_account_members', {
account_slug: accountSlug,
});
if (members.error) {
throw members.error;
}
return members.data;
}
}
export function createTeamAccountsApi(client: SupabaseClient<Database>) {

View File

@@ -137,6 +137,7 @@
"title": "Kulude ülevaade 2025 aasta raames",
"monthly": "Kulu töötaja kohta kuus *",
"yearly": "Maksimaalne kulu inimese kohta kokku aastas *",
"total": "Maksimaalne kulu {{employeeCount}} töötaja kohta aastas *"
"total": "Maksimaalne kulu {{employeeCount}} töötaja kohta aastas *",
"sum": "Kokku"
}
}