B2B-30: add button to create company from super admin page
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
useSidebar,
|
||||
} from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
@@ -22,11 +23,12 @@ import { ProfileAccountDropdownContainer } from '~/components/personal-account-d
|
||||
|
||||
export function AdminSidebar() {
|
||||
const path = usePathname();
|
||||
const { open } = useSidebar();
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader className={'m-2'}>
|
||||
<AppLogo href={'/admin'} className="max-w-full" />
|
||||
<AppLogo href={'/admin'} className="max-w-full" compact={!open} />
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
||||
@@ -30,10 +31,14 @@ async function AccountsPage(props: AdminAccountsPageProps) {
|
||||
return (
|
||||
<>
|
||||
<PageHeader description={<AppBreadcrumbs />}>
|
||||
<div className="flex justify-end">
|
||||
<div className="flex justify-end gap-2">
|
||||
<AdminCreateUserDialog>
|
||||
<Button data-test="admin-create-user-button">Create User</Button>
|
||||
<Button data-test="admin-create-user-button">Create Personal Account</Button>
|
||||
</AdminCreateUserDialog>
|
||||
<AdminCreateCompanyDialog>
|
||||
<Button>Create Company Account</Button>
|
||||
</AdminCreateCompanyDialog>
|
||||
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
|
||||
@@ -4,29 +4,33 @@ import { MedReportLogo } from './med-report-title';
|
||||
|
||||
function LogoImage({
|
||||
className,
|
||||
compact = false,
|
||||
}: {
|
||||
className?: string;
|
||||
width?: number;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
return <MedReportLogo className={className} />;
|
||||
return <MedReportLogo compact={compact} className={className} />;
|
||||
}
|
||||
|
||||
export function AppLogo({
|
||||
href,
|
||||
label,
|
||||
className,
|
||||
compact = false,
|
||||
}: {
|
||||
href?: string | null;
|
||||
className?: string;
|
||||
label?: string;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
if (href === null) {
|
||||
return <LogoImage className={className} />;
|
||||
return <LogoImage className={className} compact={compact} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link aria-label={label ?? 'Home Page'} href={href ?? '/'} prefetch={true}>
|
||||
<LogoImage className={className} />
|
||||
<LogoImage className={className} compact={compact} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MedReportSmallLogo } from "@/public/assets/med-report-small-logo";
|
||||
|
||||
export const MedReportLogo = ({ className }: { className?: string }) => (
|
||||
export const MedReportLogo = ({ className, compact = false }: { className?: string, compact?: boolean }) => (
|
||||
<div className={cn('flex gap-2 justify-center', className)}>
|
||||
<MedReportSmallLogo />
|
||||
<span className="text-foreground text-lg font-semibold tracking-tighter">
|
||||
{!compact && <span className="text-foreground text-lg font-semibold tracking-tighter">
|
||||
MedReport
|
||||
</span>
|
||||
</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@kit/ui/alert-dialog';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { createTeamAccountAction } from '../lib/server/admin-server-actions';
|
||||
import { CreateCompanySchema, CreateCompanySchemaType } from '../lib/server/schema/create-company.schema';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export function AdminCreateCompanyDialog(props: React.PropsWithChildren) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const form = useForm<CreateCompanySchemaType>({
|
||||
resolver: zodResolver(CreateCompanySchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
const onSubmit = (data: CreateCompanySchemaType) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const error = await createTeamAccountAction(data);
|
||||
|
||||
if (!error) {
|
||||
toast.success('Company creates successfully');
|
||||
form.reset();
|
||||
|
||||
setOpen(false);
|
||||
setError(null);
|
||||
|
||||
|
||||
} else {
|
||||
setError('Something went wrong with company creation');
|
||||
|
||||
}
|
||||
|
||||
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Error');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Create New Company Account</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
Complete the form below to create a new company account.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-test={'admin-create-user-form'}
|
||||
className={'flex flex-col space-y-4'}
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<If condition={!!error}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
|
||||
<FormField
|
||||
name={'name'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'teams:teamNameLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
data-test={'create-team-name-input'}
|
||||
required
|
||||
minLength={2}
|
||||
maxLength={50}
|
||||
placeholder={''}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'teams:teamNameDescription'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
<Button disabled={pending} type={'submit'}>
|
||||
{pending ? 'Creating...' : 'Create Company'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,8 @@ import { ResetPasswordSchema } from './schema/reset-password.schema';
|
||||
import { createAdminAccountsService } from './services/admin-accounts.service';
|
||||
import { createAdminAuthUserService } from './services/admin-auth-user.service';
|
||||
import { adminAction } from './utils/admin-action';
|
||||
import { CreateCompanySchema } from './schema/create-company.schema';
|
||||
import { createCreateTeamAccountService } from './services/create-team-account.service';
|
||||
|
||||
/**
|
||||
* @name banUserAction
|
||||
@@ -222,6 +224,42 @@ export const resetPasswordAction = adminAction(
|
||||
),
|
||||
);
|
||||
|
||||
export const createTeamAccountAction = enhanceAction(
|
||||
async ({ name }, user) => {
|
||||
const logger = await getLogger();
|
||||
const client = getSupabaseServerClient();
|
||||
const service = createCreateTeamAccountService(client);
|
||||
|
||||
const ctx = {
|
||||
name: 'team-accounts.create',
|
||||
userId: user.id,
|
||||
accountName: name,
|
||||
};
|
||||
|
||||
logger.info(ctx, `Creating company account...`);
|
||||
|
||||
const { data, error } = await service.createNewOrganizationAccount({
|
||||
name,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
logger.error({ ...ctx, error }, `Failed to create company account`);
|
||||
|
||||
return {
|
||||
error: true,
|
||||
};
|
||||
}
|
||||
|
||||
logger.info(ctx, `Company account created`);
|
||||
|
||||
redirect(`/home/${data.slug}/settings`);
|
||||
},
|
||||
{
|
||||
schema: CreateCompanySchema,
|
||||
},
|
||||
);
|
||||
|
||||
function revalidateAdmin() {
|
||||
revalidatePath('/admin', 'layout');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* @name RESERVED_NAMES_ARRAY
|
||||
* @description Array of reserved names for team accounts
|
||||
* This is a list of names that cannot be used for team accounts as they are reserved for other purposes.
|
||||
* Please include any new reserved names here.
|
||||
*/
|
||||
const RESERVED_NAMES_ARRAY = [
|
||||
'settings',
|
||||
'billing',
|
||||
// please add more reserved names here
|
||||
];
|
||||
|
||||
const SPECIAL_CHARACTERS_REGEX = /[!@#$%^&*()+=[\]{};':"\\|,.<>/?]/;
|
||||
|
||||
/**
|
||||
* @name CompanyNameSchema
|
||||
*/
|
||||
export const CompanyNameSchema = z
|
||||
.string({
|
||||
description: 'The name of the company account',
|
||||
})
|
||||
.min(2)
|
||||
.max(50)
|
||||
.refine(
|
||||
(name) => {
|
||||
console.log(name);
|
||||
return !SPECIAL_CHARACTERS_REGEX.test(name);
|
||||
},
|
||||
{
|
||||
message: 'teams:specialCharactersError',
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(name) => {
|
||||
return !RESERVED_NAMES_ARRAY.includes(name.toLowerCase());
|
||||
},
|
||||
{
|
||||
message: 'teams:reservedNameError',
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @name CreateCompanySchema
|
||||
* @description Schema for creating a team account
|
||||
*/
|
||||
export const CreateCompanySchema = z.object({
|
||||
name: CompanyNameSchema,
|
||||
});
|
||||
|
||||
export type CreateCompanySchemaType = z.infer<typeof CreateCompanySchema>;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'server-only';
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
export function createCreateTeamAccountService(
|
||||
client: SupabaseClient<Database>,
|
||||
) {
|
||||
return new CreateTeamAccountService(client);
|
||||
}
|
||||
|
||||
class CreateTeamAccountService {
|
||||
private readonly namespace = 'accounts.create-team-account';
|
||||
|
||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||
|
||||
async createNewOrganizationAccount(params: { name: string; userId: string }) {
|
||||
const logger = await getLogger();
|
||||
const ctx = { ...params, namespace: this.namespace };
|
||||
|
||||
logger.info(ctx, `Creating new company account...`);
|
||||
|
||||
const { error, data } = await this.client.rpc('create_team_account', {
|
||||
account_name: params.name,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
logger.error(
|
||||
{
|
||||
error,
|
||||
...ctx,
|
||||
},
|
||||
`Error creating company account`,
|
||||
);
|
||||
|
||||
throw new Error('Error creating company account');
|
||||
}
|
||||
|
||||
logger.info(ctx, `Company account created successfully`);
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user