Files
eventify_command_center/src/features/users/components/CreateUserDialog.tsx

478 lines
22 KiB
TypeScript

// CreateUserDialog - Multi-step form for creating new users
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { Progress } from '@/components/ui/progress';
import {
User,
Mail,
Phone,
KeyRound,
Globe,
Tag,
ChevronLeft,
ChevronRight,
Loader2,
} from 'lucide-react';
import { createUserSchema, type CreateUserInput } from '@/lib/validations/user';
import { mockTags } from '../data/mockUserCrmData';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
interface CreateUserDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onUserCreated?: () => void;
}
const steps = [
{ id: 1, title: 'Basic Info', icon: User },
{ id: 2, title: 'Security', icon: KeyRound },
{ id: 3, title: 'Preferences', icon: Globe },
{ id: 4, title: 'Tags', icon: Tag },
];
const countryCodes = [
{ code: '+91', country: 'India' },
{ code: '+1', country: 'USA' },
{ code: '+44', country: 'UK' },
{ code: '+971', country: 'UAE' },
{ code: '+65', country: 'Singapore' },
];
const languages = [
{ code: 'en', name: 'English' },
{ code: 'hi', name: 'Hindi' },
{ code: 'ta', name: 'Tamil' },
{ code: 'te', name: 'Telugu' },
{ code: 'mr', name: 'Marathi' },
];
const timezones = [
{ code: 'Asia/Kolkata', name: 'India (IST)' },
{ code: 'America/New_York', name: 'Eastern Time (ET)' },
{ code: 'Europe/London', name: 'London (GMT)' },
{ code: 'Asia/Dubai', name: 'Dubai (GST)' },
{ code: 'Asia/Singapore', name: 'Singapore (SGT)' },
];
const currencies = [
{ code: 'INR', symbol: '₹', name: 'Indian Rupee' },
{ code: 'USD', symbol: '$', name: 'US Dollar' },
{ code: 'GBP', symbol: '£', name: 'British Pound' },
{ code: 'AED', symbol: 'د.إ', name: 'UAE Dirham' },
{ code: 'SGD', symbol: 'S$', name: 'Singapore Dollar' },
];
export function CreateUserDialog({ open, onOpenChange, onUserCreated }: CreateUserDialogProps) {
const [currentStep, setCurrentStep] = useState(1);
const [isSubmitting, setIsSubmitting] = useState(false);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const form = useForm<CreateUserInput>({
resolver: zodResolver(createUserSchema),
defaultValues: {
name: '',
email: '',
phone: '',
countryCode: '+91',
password: '',
role: 'User',
language: 'en',
timezone: 'Asia/Kolkata',
currency: 'INR',
tagIds: [],
},
});
const handleNext = async () => {
let isValid = true;
if (currentStep === 1) {
isValid = await form.trigger(['name', 'email', 'phone', 'countryCode']);
} else if (currentStep === 2) {
isValid = await form.trigger(['password', 'role']);
} else if (currentStep === 3) {
isValid = await form.trigger(['language', 'timezone', 'currency']);
}
if (isValid && currentStep < 4) {
setCurrentStep(currentStep + 1);
}
};
const handleBack = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
const handleTagToggle = (tagId: string) => {
const updated = selectedTags.includes(tagId)
? selectedTags.filter((t) => t !== tagId)
: [...selectedTags, tagId];
setSelectedTags(updated);
form.setValue('tagIds', updated);
};
const onSubmit = async (data: CreateUserInput) => {
setIsSubmitting(true);
try {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500));
toast.success(`User "${data.name}" created successfully!`);
onOpenChange(false);
onUserCreated?.();
form.reset();
setCurrentStep(1);
setSelectedTags([]);
} catch (error) {
toast.error('Failed to create user');
} finally {
setIsSubmitting(false);
}
};
const handleClose = () => {
if (!isSubmitting) {
onOpenChange(false);
form.reset();
setCurrentStep(1);
setSelectedTags([]);
}
};
const progress = (currentStep / 4) * 100;
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<User className="h-5 w-5 text-primary" />
Create New User
</DialogTitle>
<DialogDescription>
Add a new user to the platform. Step {currentStep} of 4.
</DialogDescription>
</DialogHeader>
{/* Progress Bar */}
<div className="space-y-2">
<Progress value={progress} className="h-2" />
<div className="flex justify-between">
{steps.map((step) => (
<div
key={step.id}
className={cn(
'flex items-center gap-1 text-xs',
currentStep >= step.id ? 'text-primary' : 'text-muted-foreground'
)}
>
<step.icon className="h-3 w-3" />
<span className="hidden sm:inline">{step.title}</span>
</div>
))}
</div>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* Step 1: Basic Info */}
{currentStep === 1 && (
<div className="space-y-4 animate-in fade-in-50 duration-300">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Full Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email Address</FormLabel>
<FormControl>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input placeholder="john@example.com" className="pl-9" {...field} />
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-3 gap-3">
<FormField
control={form.control}
name="countryCode"
render={({ field }) => (
<FormItem>
<FormLabel>Code</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{countryCodes.map((cc) => (
<SelectItem key={cc.code} value={cc.code}>
{cc.code} {cc.country}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Phone Number</FormLabel>
<FormControl>
<div className="relative">
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input placeholder="9876543210" className="pl-9" {...field} />
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
)}
{/* Step 2: Security */}
{currentStep === 2 && (
<div className="space-y-4 animate-in fade-in-50 duration-300">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password (Optional)</FormLabel>
<FormControl>
<div className="relative">
<KeyRound className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input type="password" placeholder="Leave blank for magic link" className="pl-9" {...field} />
</div>
</FormControl>
<FormDescription>
If left blank, user will receive a magic link to set password.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="User">User</SelectItem>
<SelectItem value="Partner">Partner</SelectItem>
<SelectItem value="Support Agent">Support Agent</SelectItem>
<SelectItem value="Admin">Admin</SelectItem>
<SelectItem value="Super Admin">Super Admin</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
{/* Step 3: Preferences */}
{currentStep === 3 && (
<div className="space-y-4 animate-in fade-in-50 duration-300">
<FormField
control={form.control}
name="language"
render={({ field }) => (
<FormItem>
<FormLabel>Language</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{languages.map((lang) => (
<SelectItem key={lang.code} value={lang.code}>
{lang.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="timezone"
render={({ field }) => (
<FormItem>
<FormLabel>Timezone</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{timezones.map((tz) => (
<SelectItem key={tz.code} value={tz.code}>
{tz.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="currency"
render={({ field }) => (
<FormItem>
<FormLabel>Currency</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{currencies.map((cur) => (
<SelectItem key={cur.code} value={cur.code}>
{cur.symbol} {cur.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
{/* Step 4: Tags */}
{currentStep === 4 && (
<div className="space-y-4 animate-in fade-in-50 duration-300">
<FormItem>
<FormLabel>Tags (Optional)</FormLabel>
<FormDescription>Add labels to help segment this user.</FormDescription>
<div className="flex flex-wrap gap-2 mt-3">
{mockTags.map((tag) => (
<Badge
key={tag.id}
variant="outline"
onClick={() => handleTagToggle(tag.id)}
className={cn(
'cursor-pointer transition-all',
selectedTags.includes(tag.id)
? tag.color
: 'bg-secondary/50 text-muted-foreground hover:bg-secondary'
)}
>
{tag.name}
</Badge>
))}
</div>
</FormItem>
</div>
)}
<DialogFooter className="flex justify-between sm:justify-between">
<Button
type="button"
variant="outline"
onClick={handleBack}
disabled={currentStep === 1 || isSubmitting}
>
<ChevronLeft className="h-4 w-4 mr-1" /> Back
</Button>
{currentStep < 4 ? (
<Button type="button" onClick={handleNext}>
Next <ChevronRight className="h-4 w-4 ml-1" />
</Button>
) : (
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> Creating...
</>
) : (
'Create User'
)}
</Button>
)}
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}