add health benefit form
fix super admin
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
|
||||
@@ -29,7 +30,11 @@ const features = {
|
||||
enableThemeToggle: featuresFlagConfig.enableThemeToggle,
|
||||
};
|
||||
|
||||
export function SiteHeaderAccountSection() {
|
||||
export function SiteHeaderAccountSection({
|
||||
accounts,
|
||||
}: {
|
||||
accounts: UserWorkspace['accounts'];
|
||||
}) {
|
||||
const session = useSession();
|
||||
const signOut = useSignOut();
|
||||
|
||||
@@ -41,6 +46,7 @@ export function SiteHeaderAccountSection() {
|
||||
features={features}
|
||||
user={session.data.user}
|
||||
signOutRequested={() => signOut.mutateAsync()}
|
||||
accounts={accounts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace';
|
||||
|
||||
import { Header } from '@kit/ui/marketing';
|
||||
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
@@ -5,12 +7,16 @@ import { AppLogo } from '~/components/app-logo';
|
||||
import { SiteHeaderAccountSection } from './site-header-account-section';
|
||||
import { SiteNavigation } from './site-navigation';
|
||||
|
||||
export function SiteHeader() {
|
||||
export function SiteHeader({
|
||||
accounts,
|
||||
}: {
|
||||
accounts: UserWorkspace['accounts'];
|
||||
}) {
|
||||
return (
|
||||
<Header
|
||||
logo={<AppLogo />}
|
||||
navigation={<SiteNavigation />}
|
||||
actions={<SiteHeaderAccountSection />}
|
||||
actions={<SiteHeaderAccountSection accounts={accounts} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { use } from 'react';
|
||||
|
||||
import { SiteFooter } from '~/(marketing)/_components/site-footer';
|
||||
import { SiteHeader } from '~/(marketing)/_components/site-header';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { loadUserWorkspace } from '../home/(user)/_lib/server/load-user-workspace';
|
||||
|
||||
function SiteLayout(props: React.PropsWithChildren) {
|
||||
const workspace = use(loadUserWorkspace());
|
||||
|
||||
return (
|
||||
<div className={'flex min-h-[100vh] flex-col'}>
|
||||
<SiteHeader />
|
||||
<SiteHeader accounts={workspace.accounts} />
|
||||
|
||||
{props.children}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace';
|
||||
import { LayoutDashboard, Users } from 'lucide-react';
|
||||
|
||||
import {
|
||||
@@ -21,10 +22,13 @@ import {
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
|
||||
|
||||
export function AdminSidebar() {
|
||||
export function AdminSidebar({
|
||||
accounts,
|
||||
}: {
|
||||
accounts: UserWorkspace['accounts'];
|
||||
}) {
|
||||
const path = usePathname();
|
||||
const { open } = useSidebar();
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader className={'m-2'}>
|
||||
@@ -62,8 +66,8 @@ export function AdminSidebar() {
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>
|
||||
<ProfileAccountDropdownContainer />
|
||||
<ProfileAccountDropdownContainer accounts={accounts} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs';
|
||||
|
||||
import { AdminAccountsTable } from '@kit/admin/components/admin-accounts-table';
|
||||
import { AdminCreateUserDialog } from '@kit/admin/components/admin-create-user-dialog';
|
||||
import { AdminCreateCompanyDialog } from '@kit/admin/components/admin-create-company-dialog';
|
||||
import { AdminCreateUserDialog } from '@kit/admin/components/admin-create-user-dialog';
|
||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
||||
@@ -24,7 +24,7 @@ export const metadata = {
|
||||
};
|
||||
|
||||
async function AccountsPage(props: AdminAccountsPageProps) {
|
||||
const client = getSupabaseServerClient();
|
||||
const client = getSupabaseServerClient().schema('medreport');
|
||||
const searchParams = await props.searchParams;
|
||||
const page = searchParams.page ? parseInt(searchParams.page) : 1;
|
||||
|
||||
@@ -33,18 +33,19 @@ async function AccountsPage(props: AdminAccountsPageProps) {
|
||||
<PageHeader description={<AppBreadcrumbs />}>
|
||||
<div className="flex justify-end gap-2">
|
||||
<AdminCreateUserDialog>
|
||||
<Button data-test="admin-create-user-button">Create Personal Account</Button>
|
||||
<Button data-test="admin-create-user-button">
|
||||
Create Personal Account
|
||||
</Button>
|
||||
</AdminCreateUserDialog>
|
||||
<AdminCreateCompanyDialog>
|
||||
<Button>Create Company Account</Button>
|
||||
</AdminCreateCompanyDialog>
|
||||
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<PageBody>
|
||||
<ServerDataLoader
|
||||
table={'accounts'}
|
||||
table="accounts"
|
||||
client={client}
|
||||
page={page}
|
||||
where={(queryBuilder) => {
|
||||
@@ -55,7 +56,9 @@ async function AccountsPage(props: AdminAccountsPageProps) {
|
||||
}
|
||||
|
||||
if (query) {
|
||||
queryBuilder.or(`name.ilike.%${query}%,email.ilike.%${query}%,personal_code.ilike.%${query}%`);
|
||||
queryBuilder.or(
|
||||
`name.ilike.%${query}%,email.ilike.%${query}%,personal_code.ilike.%${query}%`,
|
||||
);
|
||||
}
|
||||
|
||||
return queryBuilder;
|
||||
|
||||
@@ -8,6 +8,8 @@ import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||
import { AdminSidebar } from '~/admin/_components/admin-sidebar';
|
||||
import { AdminMobileNavigation } from '~/admin/_components/mobile-navigation';
|
||||
|
||||
import { loadUserWorkspace } from '../home/(user)/_lib/server/load-user-workspace';
|
||||
|
||||
export const metadata = {
|
||||
title: `Super Admin`,
|
||||
};
|
||||
@@ -16,12 +18,13 @@ export const dynamic = 'force-dynamic';
|
||||
|
||||
export default function AdminLayout(props: React.PropsWithChildren) {
|
||||
const state = use(getLayoutState());
|
||||
const workspace = use(loadUserWorkspace());
|
||||
|
||||
return (
|
||||
<SidebarProvider defaultOpen={state.open}>
|
||||
<Page style={'sidebar'}>
|
||||
<PageNavigation>
|
||||
<AdminSidebar />
|
||||
<AdminSidebar accounts={workspace.accounts} />
|
||||
</PageNavigation>
|
||||
|
||||
<PageMobileNavigation>
|
||||
|
||||
@@ -8,6 +8,8 @@ import { ExternalLink } from '@/public/assets/external-link';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { onUpdateAccount } from '@kit/auth/actions/update-account-actions';
|
||||
import { UpdateAccountSchema } from '@kit/auth/schemas/update-account.schema';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Checkbox } from '@kit/ui/checkbox';
|
||||
import {
|
||||
@@ -21,9 +23,6 @@ import {
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { UpdateAccountSchema } from '../_lib/schemas/update-account.schema';
|
||||
import { onUpdateAccount } from '../_lib/server/update-account';
|
||||
|
||||
export function UpdateAccountForm({ user }: { user: User }) {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(UpdateAccountSchema),
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const UpdateAccountSchema = z.object({
|
||||
firstName: z
|
||||
.string({
|
||||
required_error: 'First name is required',
|
||||
})
|
||||
.nonempty(),
|
||||
lastName: z
|
||||
.string({
|
||||
required_error: 'Last name is required',
|
||||
})
|
||||
.nonempty(),
|
||||
personalCode: z
|
||||
.string({
|
||||
required_error: 'Personal code is required',
|
||||
})
|
||||
.nonempty(),
|
||||
email: z.string().email({
|
||||
message: 'Email is required',
|
||||
}),
|
||||
phone: z
|
||||
.string({
|
||||
required_error: 'Phone number is required',
|
||||
})
|
||||
.nonempty(),
|
||||
city: z.string().optional(),
|
||||
weight: z
|
||||
.number({
|
||||
required_error: 'Weight is required',
|
||||
invalid_type_error: 'Weight must be a number',
|
||||
})
|
||||
.gt(0, { message: 'Weight must be greater than 0' }),
|
||||
|
||||
height: z
|
||||
.number({
|
||||
required_error: 'Height is required',
|
||||
invalid_type_error: 'Height must be a number',
|
||||
})
|
||||
.gt(0, { message: 'Height must be greater than 0' }),
|
||||
userConsent: z.boolean().refine((val) => val === true, {
|
||||
message: 'Must be true',
|
||||
}),
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { createAuthApi } from '@kit/auth/api';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { updateCustomer } from '@lib/data/customer';
|
||||
|
||||
import { UpdateAccountSchema } from '../schemas/update-account.schema';
|
||||
|
||||
export interface AccountSubmitData {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
personalCode: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
city?: string;
|
||||
weight: number | null;
|
||||
height: number | null;
|
||||
userConsent: boolean;
|
||||
}
|
||||
|
||||
export const onUpdateAccount = enhanceAction(
|
||||
async (params: AccountSubmitData) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const api = createAuthApi(client);
|
||||
|
||||
try {
|
||||
await api.updateAccount(params);
|
||||
console.log('SUCCESS', pathsConfig.auth.updateAccountSuccess);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
console.warn('On update account error: ' + err.message);
|
||||
}
|
||||
console.warn('On update account error: ', err);
|
||||
}
|
||||
|
||||
await updateCustomer({
|
||||
first_name: params.firstName,
|
||||
last_name: params.lastName,
|
||||
phone: params.phone,
|
||||
});
|
||||
|
||||
const hasUnseenMembershipConfirmation =
|
||||
await api.hasUnseenMembershipConfirmation();
|
||||
|
||||
if (hasUnseenMembershipConfirmation) {
|
||||
redirect(pathsConfig.auth.membershipConfirmation);
|
||||
} else {
|
||||
redirect(pathsConfig.app.selectPackage);
|
||||
}
|
||||
},
|
||||
{
|
||||
schema: UpdateAccountSchema,
|
||||
},
|
||||
);
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
111
app/home/[account]/billing/_components/health-benefit-form.tsx
Normal file
111
app/home/[account]/billing/_components/health-benefit-form.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user