diff --git a/src/app/_components/admincomponents/admin-dashboard.tsx b/src/app/_components/admincomponents/admin-dashboard.tsx index 98bd77e4..cd51a405 100644 --- a/src/app/_components/admincomponents/admin-dashboard.tsx +++ b/src/app/_components/admincomponents/admin-dashboard.tsx @@ -3,22 +3,29 @@ import { Box } from "@mantine/core"; import { useForm } from "@mantine/form"; import { useState } from "react"; +import { InviteAgencyForm } from "@/app/_components/admincomponents/invite-agency-form"; import { InviteUserForm } from "@/app/_components/admincomponents/invite-user-form"; import Button from "@/app/_components/common/button/Button"; import Modal from "@/app/_components/common/modal/modal"; +import Home from "@/assets/icons/home"; +import User from "@/assets/icons/user"; import { notify } from "@/lib/notifications"; import { api } from "@/trpc/react"; -import { OrganizationRole } from "@/types/types"; +import { OrganizationRole, Role } from "@/types/types"; +import { emailRegex } from "@/types/validation"; + +type InviteType = "user" | "agency" | null; export const AdminDashboard = () => { const [showInviteModal, setShowInviteModal] = useState(false); + const [inviteType, setInviteType] = useState(null); const organizations = api.organization.getAll.useQuery(); const inviteUserMutation = api.organization.inviteUser.useMutation({ onSuccess: (data) => { notify.success(`Invitation sent to ${data.email}`); - form.reset(); + userForm.reset(); setShowInviteModal(false); }, onError: (error) => { @@ -26,58 +33,215 @@ export const AdminDashboard = () => { }, }); - const form = useForm({ + const createAgencyMutation = api.organization.createOrganization.useMutation({ + onSuccess: (data) => { + if (!data) { + notify.error("Failed to create agency"); + return; + } + notify.success(`Agency "${data.name}" created successfully`); + agencyForm.reset(); + setShowInviteModal(false); + void organizations.refetch(); + }, + onError: (error) => { + notify.error(error.message || "Failed to create agency"); + }, + }); + + const userForm = useForm({ mode: "uncontrolled", initialValues: { email: "", + role: Role.DRIVER, organizationRole: OrganizationRole.MEMBER, organizationId: "", }, validate: { - email: (value) => (value.trim().length > 0 ? null : "Email is required"), - organizationRole: (value) => (value.trim().length > 0 ? null : "Role is required"), - organizationId: (value) => (value.trim().length > 0 ? null : "Organization is required"), + email: (value) => { + if (value.trim().length === 0) return "Email is required"; + if (!emailRegex.test(value)) return "Invalid email address"; + return null; + }, + role: (value) => (value.trim().length > 0 ? null : "Role is required"), + organizationId: (value, values) => { + if (values.role === Role.AGENCY) { + return value.trim().length > 0 ? null : "Organization is required"; + } + return null; + }, + }, + }); + + const agencyForm = useForm({ + mode: "uncontrolled", + initialValues: { + name: "", + slug: "", + }, + validate: { + name: (value) => (value.trim().length > 0 ? null : "Agency name is required"), + slug: (value) => { + if (value.trim().length === 0) return "Agency slug is required"; + if (!/^[a-z0-9-]+$/.test(value)) + return "Slug must only contain lowercase letters, numbers, and hyphens"; + return null; + }, }, }); - const handleConfirm = () => { - const validation = form.validate(); + const handleUserConfirm = () => { + const validation = userForm.validate(); + + if (validation.hasErrors) { + notify.error("Please fix the errors in the form before submitting"); + return; + } + + const formValues = userForm.getValues(); + + // Determine the organization ID based on application role + let organizationId: string; + const appRole = formValues.role; + + if (appRole === Role.ADMIN) { + const adminsOrg = organizations.data?.find((org) => org.slug === "admins"); + if (!adminsOrg) { + notify.error("Admins organization not found"); + return; + } + organizationId = adminsOrg.id; + } else if (appRole === Role.DRIVER) { + const driversOrg = organizations.data?.find((org) => org.slug === "drivers"); + if (!driversOrg) { + notify.error("Drivers organization not found"); + return; + } + organizationId = driversOrg.id; + } else { + organizationId = formValues.organizationId; + } + + const submitData = { + email: formValues.email, + organizationRole: formValues.organizationRole, + role: formValues.role, + organizationId, + }; + + inviteUserMutation.mutate(submitData); + }; + + const handleAgencyConfirm = () => { + const validation = agencyForm.validate(); if (validation.hasErrors) { notify.error("Please fix the errors in the form before submitting"); return; } - // todo: handle case where admin permissions are given within the organization - console.log("submit", form.values); + const formValues = agencyForm.getValues(); + createAgencyMutation.mutate(formValues); + }; + + const handleCloseModal = () => { + userForm.clearErrors(); + agencyForm.clearErrors(); + setShowInviteModal(false); + setInviteType(null); + }; - inviteUserMutation.mutate(form.values); + const handleInviteTypeSelect = (type: "user" | "agency") => { + setInviteType(type); }; return ( <> - - { - form.clearErrors(); - setShowInviteModal(false); - }} - onConfirm={() => { - handleConfirm(); - }} - title={ - - Invite a new user + + + {/* Invite Type Selection Modal */} + {inviteType === null && ( + {}} + title={ + + Send an Invite to the Navigation Centre + + } + size="lg" + showDefaultFooter={false} + > + + + - } - size="md" - showDefaultFooter - confirmText="Send Invitation" - loading={inviteUserMutation.isPending} - > - - + + )} + + {/* Invite User Form Modal */} + {inviteType === "user" && ( + + Invite a User + + } + size="lg" + showDefaultFooter + confirmText="Invite to Navigation Centre" + cancelText="Cancel Invite" + loading={inviteUserMutation.isPending} + > + + + )} + + {/* Create Agency Form Modal */} + {inviteType === "agency" && ( + + Create an Agency + + } + size="lg" + showDefaultFooter + confirmText="Create Agency" + cancelText="Cancel" + loading={createAgencyMutation.isPending} + > + + + )} ); }; diff --git a/src/app/_components/admincomponents/invite-agency-form.tsx b/src/app/_components/admincomponents/invite-agency-form.tsx new file mode 100644 index 00000000..db7cc563 --- /dev/null +++ b/src/app/_components/admincomponents/invite-agency-form.tsx @@ -0,0 +1,38 @@ +"use client"; +import { Box, Stack, TextInput } from "@mantine/core"; +import type { UseFormReturnType } from "@mantine/form"; + +interface InviteAgencyForm { + name: string; + slug: string; +} + +interface InviteAgencyFormProps { + form: UseFormReturnType; +} + +export const InviteAgencyForm = ({ form }: InviteAgencyFormProps) => { + return ( + + + + Agency Information + + + + + + ); +}; diff --git a/src/app/_components/admincomponents/invite-user-form.tsx b/src/app/_components/admincomponents/invite-user-form.tsx index 7615b796..848a616c 100644 --- a/src/app/_components/admincomponents/invite-user-form.tsx +++ b/src/app/_components/admincomponents/invite-user-form.tsx @@ -1,12 +1,15 @@ "use client"; -import { Box, Divider, Select, Stack, TextInput } from "@mantine/core"; +import { Box, Button, Group, Select, Stack, TextInput } from "@mantine/core"; import type { UseFormReturnType } from "@mantine/form"; import type { Organization } from "better-auth/plugins/organization"; -import { ALL_ORGANIZATION_ROLES, type OrganizationRole } from "@/types/types"; -import classes from "./invite-user-form.module.scss"; +import User from "@/assets/icons/user"; +import { ALL_ROLES, OrganizationRole, Role } from "@/types/types"; + +const SYSTEM_ORG_SLUGS = ["admins", "drivers"]; interface InviteUserForm { email: string; + role: Role; organizationRole: OrganizationRole; organizationId: string; } @@ -18,44 +21,100 @@ interface InviteUserFormProps { const NO_ORGS_DATA = [{ value: "", label: "no organizations available", disabled: true }]; +const ROLE_LABELS: Record = { + [Role.ADMIN]: "Administrator", + [Role.DRIVER]: "Driver", + [Role.AGENCY]: "Agency Member", +}; + export const InviteUserForm = ({ form, organizations }: InviteUserFormProps) => { + const selectedRole = form.values.role; + const showOrganizationSelect = selectedRole === Role.AGENCY; + + const agencyOrganizations = organizations.filter( + (org) => !SYSTEM_ORG_SLUGS.includes(org.slug ?? ""), + ); + + const handleRoleChange = (role: Role) => { + form.setFieldValue("role", role); + if (role !== Role.AGENCY) { + form.setFieldValue("organizationId", ""); + } + }; + return ( - + - - User Information + + Invitee Information -
- -
+
- -
+ + + + Role Information + + + + Invitee Role in the Navigation Centre + + + {ALL_ROLES.map((role) => ( + + ))} + + ({ value: org.id, label: org.name })) ?? NO_ORGS_DATA} - key={form.key("organizationId")} - {...form.getInputProps("organizationId")} - /> -
+ {showOrganizationSelect && ( +