478 lines
22 KiB
TypeScript
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>
|
|
);
|
|
}
|