add health benefit form

fix super admin
This commit is contained in:
Danel Kungla
2025-07-23 16:33:24 +03:00
parent 2db67b7f20
commit 86b86c6752
43 changed files with 1329 additions and 561 deletions

View File

@@ -2,6 +2,7 @@ import { use } from 'react';
import { cookies } from 'next/headers';
import { retrieveCart } from '@lib/data';
import { z } from 'zod';
import { UserWorkspaceContextProvider } from '@kit/accounts/components';
@@ -17,7 +18,6 @@ import { HomeMenuNavigation } from '../_components/home-menu-navigation';
import { HomeMobileNavigation } from '../_components/home-mobile-navigation';
import { HomeSidebar } from '../_components/home-sidebar';
import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
import { retrieveCart } from '@lib/data';
function UserHomeLayout({ children }: React.PropsWithChildren) {
const state = use(getLayoutState());

View File

@@ -0,0 +1,83 @@
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { Trans } from '@kit/ui/trans';
const HealthBenefitFields = () => {
return (
<div className="flex flex-col gap-3">
<FormField
name="occurance"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey="billing:healthBenefitForm.title" />
</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue
placeholder={<Trans i18nKey="common:formField:occurance" />}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="yearly">
<Trans i18nKey="billing:occurance.yearly" />
</SelectItem>
<SelectItem value="quarterly">
<Trans i18nKey="billing:occurance.quarterly" />
</SelectItem>
<SelectItem value="monthly">
<Trans i18nKey="billing:occurance.monthly" />
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="amount"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:amount'} />
</FormLabel>
<FormControl>
<Input
{...field}
type="number"
onChange={(e) =>
field.onChange(
e.target.value === '' ? null : Number(e.target.value),
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
);
};
export default HealthBenefitFields;

View File

@@ -0,0 +1,111 @@
'use client';
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 YearlyExpensesOverview from './yearly-expenses-overview';
const HealthBenefitForm = ({
account,
companyParams,
}: {
account: Database['medreport']['Tables']['accounts']['Row'];
companyParams: Database['medreport']['Tables']['company_params']['Row'];
}) => {
const form = useForm({
resolver: zodResolver(UpdateHealthBenefitSchema),
mode: 'onChange',
defaultValues: {
occurance: companyParams.benefit_occurance || 'yearly',
amount: companyParams.benefit_amount || 0,
},
});
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',
},
);
})}
>
<div className="mt-8 flex items-center justify-between">
<div>
<h4>
<Trans
i18nKey="billing:pageTitle"
values={{ companyName: account.slug }}
/>
</h4>
<p className="text-muted-foreground text-sm">
<Trans i18nKey="billing:description" />
</p>
</div>
<Button type="submit" className="relative">
{form.formState.isSubmitting && (
<div className="absolute inset-0 flex items-center justify-center">
<Spinner />
</div>
)}
<div className={cn({ invisible: form.formState.isSubmitting })}>
<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">
{companyParams.benefit_amount || 0}
</p>
</div>
<Separator />
<div className="p-6">
<HealthBenefitFields />
</div>
</div>
<div className="flex-1">
<YearlyExpensesOverview
employeeCount={22}
companyParams={companyParams}
/>
<p className="text-muted-foreground mt-2 text-sm">
<Trans i18nKey="billing:healthBenefitForm.info" />
</p>
</div>
</div>
</form>
</Form>
);
};
export default HealthBenefitForm;

View File

@@ -0,0 +1,90 @@
import { useMemo } from 'react';
import { Database } from '@/packages/supabase/src/database.types';
import { Trans } from '@kit/ui/makerkit/trans';
import { Separator } from '@kit/ui/separator';
const YearlyExpensesOverview = ({
employeeCount = 0,
companyParams,
}: {
employeeCount?: number;
companyParams: Database['medreport']['Tables']['company_params']['Row'];
}) => {
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 / 4).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 * 4).toFixed(2);
case 'monthly':
return (companyParams.benefit_amount * 12).toFixed(2);
default:
return '0.00';
}
}, [companyParams]);
return (
<div className="border-border rounded-lg border p-6">
<h5>
<Trans i18nKey="billing:expensesOverview.title" />
</h5>
<div className="mt-5 flex justify-between">
<p className="text-sm font-medium">
<Trans i18nKey="billing:expensesOverview.monthly" />
</p>
<span className="text-primary text-sm font-bold">
{monthlyExpensePerEmployee}
</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 }}
/>
</p>
<span className="text-sm font-medium">
{(Number(maxYearlyExpensePerEmployee) * employeeCount).toFixed(2)}
</span>
</div>
<Separator />
<div className="mt-4 flex justify-between">
<p className="font-semibold">Kokku</p>
<span className="font-semibold">13 200,00 </span>
</div>
</div>
);
};
export default YearlyExpensesOverview;

View File

@@ -2,6 +2,8 @@
import { redirect } from 'next/navigation';
import { UpdateHealthBenefitData } from '@/packages/features/team-accounts/src/server/types';
import { enhanceAction } from '@kit/next/actions';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -63,3 +65,13 @@ export const createBillingPortalSession = enhanceAction(
},
{},
);
export const updateHealthBenefit = enhanceAction(
async (data: UpdateHealthBenefitData) => {
const client = getSupabaseServerClient();
const service = createTeamBillingService(client);
await service.updateHealthBenefit(data);
},
{},
);

View File

@@ -2,6 +2,7 @@ import 'server-only';
import { SupabaseClient } from '@supabase/supabase-js';
import { UpdateHealthBenefitData } from '@/packages/features/team-accounts/src/server/types';
import { Database } from '@/packages/supabase/src/database.types';
import { z } from 'zod';
@@ -292,6 +293,25 @@ class TeamBillingService {
return Promise.reject(error as Error);
}
}
async updateHealthBenefit(data: UpdateHealthBenefitData): Promise<void> {
const api = createTeamAccountsApi(this.client);
const logger = await getLogger();
try {
await api.updateHealthBenefit(data);
} catch (error) {
logger.error(
{
accountId: data.accountId,
error,
name: `billing.updateHealthBenefit`,
},
`Encountered an error while updating the company health benefit`,
);
return Promise.reject(error as Error);
}
}
}
function getCheckoutSessionReturnUrl(accountSlug: string) {

View File

@@ -1,27 +1,12 @@
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
import { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import {
BillingPortalCard,
CurrentLifetimeOrderCard,
CurrentSubscriptionCard,
} from '@kit/billing-gateway/components';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { If } from '@kit/ui/if';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
import billingConfig from '~/config/billing.config';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
// local imports
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
import { loadTeamAccountBillingPage } from '../_lib/server/team-account-billing-page.loader';
import { loadTeamWorkspace } from '../_lib/server/team-account-workspace.loader';
import { TeamAccountCheckoutForm } from './_components/team-account-checkout-form';
import { createBillingPortalSession } from './_lib/server/server-actions';
import HealthBenefitForm from './_components/health-benefit-form';
interface TeamAccountBillingPageProps {
params: Promise<{ account: string }>;
@@ -37,99 +22,16 @@ export const generateMetadata = async () => {
};
async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
const account = (await params).account;
const workspace = await loadTeamWorkspace(account);
const accountId = workspace.account.id;
const [data, customerId] = await loadTeamAccountBillingPage(accountId);
const canManageBilling =
workspace.account.permissions.includes('billing.manage');
const Checkout = () => {
if (!canManageBilling) {
return <CannotManageBillingAlert />;
}
return (
<TeamAccountCheckoutForm customerId={customerId} accountId={accountId} />
);
};
const BillingPortal = () => {
if (!canManageBilling || !customerId) {
return null;
}
return (
<form action={createBillingPortalSession}>
<input type="hidden" name={'accountId'} value={accountId} />
<input type="hidden" name={'slug'} value={account} />
<BillingPortalCard />
</form>
);
};
const accountSlug = (await params).account;
const api = createTeamAccountsApi(getSupabaseServerClient());
const account = await api.getTeamAccount(accountSlug);
const companyParams = await api.getTeamAccountParams(account.id);
return (
<>
<TeamAccountLayoutPageHeader
account={account}
title={<Trans i18nKey={'common:routes.billing'} />}
description={<AppBreadcrumbs />}
/>
<PageBody>
<div
className={cn(`flex w-full flex-col space-y-4`, {
'max-w-2xl': data,
})}
>
<If
condition={data}
fallback={
<div>
<Checkout />
</div>
}
>
{(data) => {
if ('active' in data) {
return (
<CurrentSubscriptionCard
subscription={data}
config={billingConfig}
/>
);
}
return (
<CurrentLifetimeOrderCard order={data} config={billingConfig} />
);
}}
</If>
<BillingPortal />
</div>
</PageBody>
</>
<PageBody>
<HealthBenefitForm account={account} companyParams={companyParams} />
</PageBody>
);
}
export default withI18n(TeamAccountBillingPage);
function CannotManageBillingAlert() {
return (
<Alert variant={'warning'}>
<ExclamationTriangleIcon className={'h-4'} />
<AlertTitle>
<Trans i18nKey={'billing:cannotManageBillingAlertTitle'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'billing:cannotManageBillingAlertDescription'} />
</AlertDescription>
</Alert>
);
}

View File

@@ -4,7 +4,10 @@ import { cookies } from 'next/headers';
import { z } from 'zod';
import { CompanyGuard, TeamAccountWorkspaceContextProvider } from '@kit/team-accounts/components';
import {
CompanyGuard,
TeamAccountWorkspaceContextProvider,
} from '@kit/team-accounts/components';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
@@ -124,9 +127,10 @@ function HeaderLayout({
/>
</PageNavigation>
<PageMobileNavigation className={'flex items-center justify-between'}>
<PageMobileNavigation
className={'flex items-center justify-between'}
>
<AppLogo />
<div className={'flex space-x-4'}>
<TeamAccountLayoutMobileNavigation
userId={data.user.id}

View File

@@ -1,5 +1,7 @@
import { use } from 'react';
import { CompanyGuard } from '@/packages/features/team-accounts/src/components';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
@@ -9,7 +11,6 @@ import { withI18n } from '~/lib/i18n/with-i18n';
import { DashboardDemo } from './_components/dashboard-demo';
import { TeamAccountLayoutPageHeader } from './_components/team-account-layout-page-header';
import { CompanyGuard } from '@/packages/features/team-accounts/src/components';
interface TeamAccountHomePageProps {
params: Promise<{ account: string }>;
@@ -26,7 +27,7 @@ export const generateMetadata = async () => {
function TeamAccountHomePage({ params }: TeamAccountHomePageProps) {
const account = use(params).account;
console.log('TeamAccountHomePage account', account);
return (
<>
<TeamAccountLayoutPageHeader
@@ -34,7 +35,7 @@ function TeamAccountHomePage({ params }: TeamAccountHomePageProps) {
title={<Trans i18nKey={'common:routes.dashboard'} />}
description={<AppBreadcrumbs />}
/>
<PageBody>
<DashboardDemo />
</PageBody>
@@ -42,4 +43,4 @@ function TeamAccountHomePage({ params }: TeamAccountHomePageProps) {
);
}
export default withI18n(CompanyGuard(TeamAccountHomePage));
export default withI18n(CompanyGuard(TeamAccountHomePage));