"use client"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useUser } from "@clerk/nextjs"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { toast } from "@/lib/toast"; import { roleSelectionSchema, basicInfoSchema, clientInfoSchema, invitationOptionsSchema, mergeUserCreationData, type RoleSelectionData, type BasicInfoData, type ClientInfoData, type InvitationOptionsData, } from "@/lib/validation/user-schemas"; import { ChevronLeft, ChevronRight, Check } from "lucide-react"; import { useGyms } from "@/hooks/use-api"; import { getInvitableRoles } from "@/lib/auth/permissions"; import type { UserRole } from "@fitai/shared"; interface CreateUserModalProps { open: boolean; onOpenChange: (open: boolean) => void; onSuccess: () => void; } type Step = 1 | 2 | 3 | 4; export function CreateUserModal({ open, onOpenChange, onSuccess, }: CreateUserModalProps) { const [currentStep, setCurrentStep] = useState(1); const [roleData, setRoleData] = useState(null); const [basicInfo, setBasicInfo] = useState(null); const [clientInfo, setClientInfo] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const { data: gyms = [] } = useGyms(); const { user } = useUser(); // Get current user's role and gym from Clerk metadata const currentUserRole = (user?.publicMetadata?.role as UserRole) || "client"; const currentUserGymId = user?.publicMetadata?.gymId as string | null; // Determine which roles the current user can create const invitableRoles = getInvitableRoles(currentUserRole); // Step 1: Role Selection Form const roleForm = useForm({ resolver: zodResolver(roleSelectionSchema), defaultValues: { role: "client" }, }); // Step 2: Basic Info Form const basicForm = useForm({ resolver: zodResolver(basicInfoSchema), defaultValues: { firstName: "", lastName: "", phone: "", email: "", }, }); // Step 3: Client Info Form const clientForm = useForm({ resolver: zodResolver(clientInfoSchema), defaultValues: { membershipType: "basic", membershipStatus: "active", sendWelcomeEmail: false, }, }); // Step 4: Invitation Options Form const invitationForm = useForm({ resolver: zodResolver(invitationOptionsSchema), defaultValues: { sendInvitation: true, gymId: "", }, }); const handleRoleNext = async () => { const isValid = await roleForm.trigger(); if (isValid) { const data = roleForm.getValues(); setRoleData(data); setCurrentStep(2); } }; const handleBasicNext = async () => { const isValid = await basicForm.trigger(); if (isValid) { const data = basicForm.getValues(); setBasicInfo(data); // Determine next step based on role if (roleData?.role === "client") { setCurrentStep(3); } else { // For admin/trainer, skip client info setCurrentStep(4); } } }; const handleClientNext = async () => { const isValid = await clientForm.trigger(); if (isValid) { const data = clientForm.getValues(); setClientInfo(data); // If email provided, show invitation options if (basicInfo?.email) { setCurrentStep(4); } else { // No email, submit directly await handleSubmit(); } } }; const handleSubmit = async (invitationData?: InvitationOptionsData) => { if (!roleData || !basicInfo) return; try { setIsSubmitting(true); const payload = mergeUserCreationData( roleData, basicInfo, clientInfo || undefined, invitationData, ); // Auto-assign gym for non-superAdmins // SuperAdmins can select gym manually, others use their own gym const finalPayload = { ...payload, gymId: currentUserRole === "superAdmin" ? payload.gymId : currentUserGymId, }; const response = await fetch("/api/users/create", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(finalPayload), }); if (!response.ok) { const error = await response.json(); toast.error(error.error || "Failed to create user"); return; } const result = await response.json(); toast.success(result.message || "User created successfully!"); // Reset form resetModal(); // Add delay to allow Clerk API to propagate invitation await new Promise((resolve) => setTimeout(resolve, 1000)); onSuccess(); onOpenChange(false); } catch (error) { console.error("Create user error:", error); toast.error("An unexpected error occurred"); } finally { setIsSubmitting(false); } }; const handleInvitationSubmit = async () => { const isValid = await invitationForm.trigger(); if (isValid) { const data = invitationForm.getValues(); await handleSubmit(data); } }; const resetModal = () => { setCurrentStep(1); setRoleData(null); setBasicInfo(null); setClientInfo(null); roleForm.reset(); basicForm.reset(); clientForm.reset(); invitationForm.reset(); }; const handleBack = () => { if (currentStep === 4 && roleData?.role === "client") { setCurrentStep(3); } else if (currentStep === 4 || currentStep === 3) { setCurrentStep(2); } else if (currentStep === 2) { setCurrentStep(1); } }; const getStepTitle = () => { switch (currentStep) { case 1: return "Select Role"; case 2: return "Basic Information"; case 3: return "Membership Details"; case 4: return "Invitation Options"; default: return ""; } }; const getStepDescription = () => { switch (currentStep) { case 1: return "Choose the role for this user"; case 2: return "Enter user's basic information"; case 3: return "Configure client membership"; case 4: return basicInfo?.email ? "Choose how to notify the user" : "Review and create"; default: return ""; } }; return ( {getStepTitle()} {getStepDescription()} {/* Step Progress Indicator */}
{[1, 2, 3, 4].map((step) => (
{step < currentStep ? : step}
))}
{/* Step 1: Role Selection */} {currentStep === 1 && (
{invitableRoles.map((role) => ( ))}
{roleForm.formState.errors.role && (

{roleForm.formState.errors.role.message}

)}
)} {/* Step 2: Basic Information */} {currentStep === 2 && (
{basicForm.formState.errors.firstName && (

{basicForm.formState.errors.firstName.message}

)}
{basicForm.formState.errors.lastName && (

{basicForm.formState.errors.lastName.message}

)}
{basicForm.formState.errors.phone && (

{basicForm.formState.errors.phone.message}

)}

Email is currently required in the database schema

{basicForm.formState.errors.email && (

{basicForm.formState.errors.email.message}

)}
)} {/* Step 3: Client Information (only for clients) */} {currentStep === 3 && roleData?.role === "client" && (
{clientForm.formState.errors.membershipType && (

{clientForm.formState.errors.membershipType.message}

)}
{basicInfo?.email && (
)}
)} {/* Step 4: Invitation Options */} {currentStep === 4 && (
{basicInfo?.email && ( <>

Email provided: {basicInfo.email}

{invitationForm.watch("sendInvitation") ? "User will receive an email to create their account" : "User record will be created without a Clerk account"}

)} {!basicInfo?.email && (

No email provided. User will be created in the database only.

)} {/* Gym Selector - Only for SuperAdmins */} {currentUserRole === "superAdmin" && (

If not selected, user will not be assigned to any gym

)} {/* Info message for non-superAdmins */} {currentUserRole !== "superAdmin" && currentUserGymId && (

User will be automatically assigned to your gym

)}
)}
); }