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'; 'use client';
import { useState } from 'react';
import { UpdateHealthBenefitSchema } from '@/packages/billing/core/src/schema'; import { UpdateHealthBenefitSchema } from '@/packages/billing/core/src/schema';
import { Database } from '@/packages/supabase/src/database.types'; import { Database } from '@/packages/supabase/src/database.types';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
@@ -22,32 +24,52 @@ import YearlyExpensesOverview from './yearly-expenses-overview';
const HealthBenefitForm = ({ const HealthBenefitForm = ({
account, account,
companyParams, companyParams,
employeeCount,
}: { }: {
account: Database['medreport']['Tables']['accounts']['Row']; account: Database['medreport']['Tables']['accounts']['Row'];
companyParams: Database['medreport']['Tables']['company_params']['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({ const form = useForm({
resolver: zodResolver(UpdateHealthBenefitSchema), resolver: zodResolver(UpdateHealthBenefitSchema),
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
occurance: companyParams.benefit_occurance || 'yearly', occurance: currentCompanyParams.benefit_occurance || 'yearly',
amount: companyParams.benefit_amount || 0, 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 ( return (
<Form {...form}> <Form {...form}>
<form <form
className="flex flex-col gap-6 px-6 text-left" className="flex flex-col gap-6 px-6 text-left"
onSubmit={form.handleSubmit((data) => { onSubmit={form.handleSubmit(onSubmit)}
toast.promise(
() => updateHealthBenefit({ ...data, accountId: account.id }),
{
success: 'success',
error: 'error',
},
);
})}
> >
<div className="mt-8 flex items-center justify-between"> <div className="mt-8 flex items-center justify-between">
<div> <div>
@@ -61,13 +83,13 @@ const HealthBenefitForm = ({
<Trans i18nKey="billing:description" /> <Trans i18nKey="billing:description" />
</p> </p>
</div> </div>
<Button type="submit" className="relative"> <Button type="submit" className="relative" disabled={isLoading}>
{form.formState.isSubmitting && ( {isLoading && (
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<Spinner /> <Spinner />
</div> </div>
)} )}
<div className={cn({ invisible: form.formState.isSubmitting })}> <div className={cn({ invisible: isLoading })}>
<Trans i18nKey="account:saveChanges" /> <Trans i18nKey="account:saveChanges" />
</div> </div>
</Button> </Button>
@@ -82,7 +104,7 @@ const HealthBenefitForm = ({
<Trans i18nKey="billing:healthBenefitForm.description" /> <Trans i18nKey="billing:healthBenefitForm.description" />
</p> </p>
<p className="pt-2 text-2xl font-semibold"> <p className="pt-2 text-2xl font-semibold">
{companyParams.benefit_amount || 0} {currentCompanyParams.benefit_amount || 0}
</p> </p>
</div> </div>
@@ -95,8 +117,8 @@ const HealthBenefitForm = ({
<div className="flex-1"> <div className="flex-1">
<YearlyExpensesOverview <YearlyExpensesOverview
employeeCount={22} employeeCount={employeeCount}
companyParams={companyParams} companyParams={currentCompanyParams}
/> />
<p className="text-muted-foreground mt-2 text-sm"> <p className="text-muted-foreground mt-2 text-sm">
<Trans i18nKey="billing:healthBenefitForm.info" /> <Trans i18nKey="billing:healthBenefitForm.info" />

View File

@@ -21,7 +21,7 @@ const YearlyExpensesOverview = ({
case 'yearly': case 'yearly':
return (companyParams.benefit_amount / 12).toFixed(2); return (companyParams.benefit_amount / 12).toFixed(2);
case 'quarterly': case 'quarterly':
return (companyParams.benefit_amount / 4).toFixed(2); return (companyParams.benefit_amount / 3).toFixed(2);
case 'monthly': case 'monthly':
return companyParams.benefit_amount.toFixed(2); return companyParams.benefit_amount.toFixed(2);
default: default:
@@ -38,7 +38,7 @@ const YearlyExpensesOverview = ({
case 'yearly': case 'yearly':
return companyParams.benefit_amount.toFixed(2); return companyParams.benefit_amount.toFixed(2);
case 'quarterly': case 'quarterly':
return (companyParams.benefit_amount * 4).toFixed(2); return (companyParams.benefit_amount * 3).toFixed(2);
case 'monthly': case 'monthly':
return (companyParams.benefit_amount * 12).toFixed(2); return (companyParams.benefit_amount * 12).toFixed(2);
default: default:
@@ -80,8 +80,15 @@ const YearlyExpensesOverview = ({
</div> </div>
<Separator /> <Separator />
<div className="mt-4 flex justify-between"> <div className="mt-4 flex justify-between">
<p className="font-semibold">Kokku</p> <p className="font-semibold">
<span className="font-semibold">13 200,00 </span> <Trans i18nKey="billing:expensesOverview.sum" />
</p>
<span className="font-semibold">
{companyParams.benefit_amount
? companyParams.benefit_amount * employeeCount
: 0}{' '}
</span>
</div> </div>
</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 { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
@@ -23,13 +24,20 @@ export const generateMetadata = async () => {
async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) { async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
const accountSlug = (await params).account; const accountSlug = (await params).account;
const api = createTeamAccountsApi(getSupabaseServerClient()); const client = getSupabaseServerClient();
const api = createTeamAccountsApi(client);
const account = await api.getTeamAccount(accountSlug); const account = await api.getTeamAccount(accountSlug);
const companyParams = await api.getTeamAccountParams(account.id); const companyParams = await api.getTeamAccountParams(account.id);
const accounts = await api.getMembers(accountSlug);
return ( return (
<PageBody> <PageBody>
<HealthBenefitForm account={account} companyParams={companyParams} /> <HealthBenefitForm
account={account}
companyParams={companyParams}
employeeCount={accounts.length}
/>
</PageBody> </PageBody>
); );
} }

View File

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

View File

@@ -137,6 +137,7 @@
"title": "Kulude ülevaade 2025 aasta raames", "title": "Kulude ülevaade 2025 aasta raames",
"monthly": "Kulu töötaja kohta kuus *", "monthly": "Kulu töötaja kohta kuus *",
"yearly": "Maksimaalne kulu inimese kohta kokku aastas *", "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"
} }
} }