diff --git a/app/admin/_components/admin-sidebar.tsx b/app/admin/_components/admin-sidebar.tsx index d7d655c..5b6c230 100644 --- a/app/admin/_components/admin-sidebar.tsx +++ b/app/admin/_components/admin-sidebar.tsx @@ -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 ( - + @@ -64,4 +66,4 @@ export function AdminSidebar() { ); -} +} \ No newline at end of file diff --git a/app/admin/accounts/page.tsx b/app/admin/accounts/page.tsx index d83191b..21c1104 100644 --- a/app/admin/accounts/page.tsx +++ b/app/admin/accounts/page.tsx @@ -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 ( <> }> -
+
- + + + + +
diff --git a/components/app-logo.tsx b/components/app-logo.tsx index c2fe283..d11f922 100644 --- a/components/app-logo.tsx +++ b/components/app-logo.tsx @@ -4,29 +4,33 @@ import { MedReportLogo } from './med-report-title'; function LogoImage({ className, + compact = false, }: { className?: string; width?: number; + compact?: boolean; }) { - return ; + return ; } export function AppLogo({ href, label, className, + compact = false, }: { href?: string | null; className?: string; label?: string; + compact?: boolean; }) { if (href === null) { - return ; + return ; } return ( - + ); } diff --git a/components/med-report-title.tsx b/components/med-report-title.tsx index bf809f8..efbea64 100644 --- a/components/med-report-title.tsx +++ b/components/med-report-title.tsx @@ -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 }) => (
- + {!compact && MedReport - + }
); diff --git a/packages/features/admin/src/components/admin-create-company-dialog.tsx b/packages/features/admin/src/components/admin-create-company-dialog.tsx new file mode 100644 index 0000000..3dacfed --- /dev/null +++ b/packages/features/admin/src/components/admin-create-company-dialog.tsx @@ -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(null); + const [open, setOpen] = useState(false); + + const form = useForm({ + 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 ( + + {props.children} + + + + Create New Company Account + + + Complete the form below to create a new company account. + + + +
+ + + + Error + + {error} + + + + { + return ( + + + + + + + + + + + + + + + + ); + }} + /> + + + Cancel + + + + + +
+
+ ); +} diff --git a/packages/features/admin/src/lib/server/admin-server-actions.ts b/packages/features/admin/src/lib/server/admin-server-actions.ts index 260724f..060980d 100644 --- a/packages/features/admin/src/lib/server/admin-server-actions.ts +++ b/packages/features/admin/src/lib/server/admin-server-actions.ts @@ -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'); } diff --git a/packages/features/admin/src/lib/server/schema/create-company.schema.ts b/packages/features/admin/src/lib/server/schema/create-company.schema.ts new file mode 100644 index 0000000..26daa01 --- /dev/null +++ b/packages/features/admin/src/lib/server/schema/create-company.schema.ts @@ -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; + diff --git a/packages/features/admin/src/lib/server/services/create-team-account.service.ts b/packages/features/admin/src/lib/server/services/create-team-account.service.ts new file mode 100644 index 0000000..0a0edea --- /dev/null +++ b/packages/features/admin/src/lib/server/services/create-team-account.service.ts @@ -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, +) { + return new CreateTeamAccountService(client); +} + +class CreateTeamAccountService { + private readonly namespace = 'accounts.create-team-account'; + + constructor(private readonly client: SupabaseClient) {} + + 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 }; + } +}