Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 196 additions & 32 deletions src/app/_components/admincomponents/admin-dashboard.tsx
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this file, what is the distinction between the invite types of "user", "agency" and null?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using invite type to determine which modal to display. "user" for the user modal, "agency" for the agency modal, and null for the selection modal to select between inviting a user and agency.

Original file line number Diff line number Diff line change
Expand Up @@ -3,81 +3,245 @@
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<boolean>(false);
const [inviteType, setInviteType] = useState<InviteType>(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) => {
notify.error(error.message || "Failed to send invitation");
},
});

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();
},
Comment on lines +36 to +46
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent early return on null data — user gets no feedback.

The if (!data) return; guard silently swallows the case. The mutation's onSuccess fired, so the UI won't show a loading state, but the modal stays open with no notification. Consider at least a warning.

Proposed fix
    onSuccess: (data) => {
-     if (!data) return;
+     if (!data) {
+       notify.error("Unexpected error: no data returned");
+       return;
+     }
      notify.success(`Agency "${data.name}" created successfully`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const createAgencyMutation = api.organization.createOrganization.useMutation({
onSuccess: (data) => {
if (!data) return;
notify.success(`Agency "${data.name}" created successfully`);
agencyForm.reset();
setShowInviteModal(false);
void organizations.refetch();
},
const createAgencyMutation = api.organization.createOrganization.useMutation({
onSuccess: (data) => {
if (!data) {
notify.error("Unexpected error: no data returned");
return;
}
notify.success(`Agency "${data.name}" created successfully`);
agencyForm.reset();
setShowInviteModal(false);
void organizations.refetch();
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/_components/admincomponents/admin-dashboard.tsx` around lines 36 -
43, In createAgencyMutation's onSuccess handler, don't silently return when data
is falsy; instead notify the user (e.g., call notify.warn or notify.error with a
message like "No agency data returned from server") and optionally log the
unexpected response, and only perform agencyForm.reset(),
setShowInviteModal(false), and organizations.refetch() when data is present;
update the guard in onSuccess to show the warning and exit without closing the
modal or resetting the form so the user receives feedback.

✅ Confirmed as addressed by @promatty

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i mean i dont see this happening since we return the org object, but to be safe lets add error handling

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@promatty, that makes sense! Even though the API should always return the organization object, adding the error handling is good defensive programming. It ensures users get proper feedback in the unlikely edge case where something unexpected happens.

Thanks for addressing this! 🎉


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

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 (
<>
<Button onClick={() => setShowInviteModal(true)}>Invite New User</Button>
<Modal
opened={showInviteModal}
onClose={() => {
form.clearErrors();
setShowInviteModal(false);
}}
onConfirm={() => {
handleConfirm();
}}
title={
<Box fw={600} fz="xl">
Invite a new user
<Button onClick={() => setShowInviteModal(true)}>Send Invitation</Button>

{/* Invite Type Selection Modal */}
{inviteType === null && (
<Modal
opened={showInviteModal}
onClose={handleCloseModal}
onConfirm={() => {}}
title={
<Box fw={600} fz="xl">
Send an Invite to the Navigation Centre
</Box>
}
size="lg"
showDefaultFooter={false}
>
<Box
style={{
display: "flex",
flexDirection: "column",
gap: "1rem",
padding: "1rem 0",
}}
>
<Button
onClick={() => handleInviteTypeSelect("user")}
height="48px"
fontSize="18px"
icon={<User width="24" height="24" stroke="white" />}
>
Invite User
</Button>
<Button
onClick={() => handleInviteTypeSelect("agency")}
height="48px"
fontSize="18px"
icon={<Home width="24" height="24" stroke="white" />}
>
Create Agency
</Button>
</Box>
}
size="md"
showDefaultFooter
confirmText="Send Invitation"
loading={inviteUserMutation.isPending}
>
<InviteUserForm organizations={organizations.data ?? []} form={form} />
</Modal>
</Modal>
)}

{/* Invite User Form Modal */}
{inviteType === "user" && (
<Modal
opened={showInviteModal}
onClose={handleCloseModal}
onConfirm={handleUserConfirm}
title={
<Box fw={600} fz="xl">
Invite a User
</Box>
}
size="lg"
showDefaultFooter
confirmText="Invite to Navigation Centre"
cancelText="Cancel Invite"
loading={inviteUserMutation.isPending}
>
<InviteUserForm organizations={organizations.data ?? []} form={userForm} />
</Modal>
)}

{/* Create Agency Form Modal */}
{inviteType === "agency" && (
<Modal
opened={showInviteModal}
onClose={handleCloseModal}
onConfirm={handleAgencyConfirm}
title={
<Box fw={600} fz="xl">
Create an Agency
</Box>
}
size="lg"
showDefaultFooter
confirmText="Create Agency"
cancelText="Cancel"
loading={createAgencyMutation.isPending}
>
<InviteAgencyForm form={agencyForm} />
</Modal>
)}
</>
);
};
38 changes: 38 additions & 0 deletions src/app/_components/admincomponents/invite-agency-form.tsx
Original file line number Diff line number Diff line change
@@ -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<InviteAgencyForm>;
}

export const InviteAgencyForm = ({ form }: InviteAgencyFormProps) => {
return (
<Stack gap="xl">
<Stack gap="md">
<Box fw={600} fz="lg" c="var(--color-primary)">
Agency Information
</Box>
<TextInput
withAsterisk
label="Agency Name"
placeholder="Enter agency name"
key={form.key("name")}
{...form.getInputProps("name")}
/>
<TextInput
withAsterisk
label="Agency Slug (ex: my-org)"
placeholder="Enter agency slug"
key={form.key("slug")}
{...form.getInputProps("slug")}
/>
</Stack>
</Stack>
);
};
Loading