diff --git a/package-lock.json b/package-lock.json
index 050c917..08e7085 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -37,6 +37,7 @@
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-query": "^5.83.0",
+ "@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -2820,6 +2821,39 @@
"react": "^18 || ^19"
}
},
+ "node_modules/@tanstack/react-table": {
+ "version": "8.21.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
+ "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/table-core": "8.21.3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/@tanstack/table-core": {
+ "version": "8.21.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
+ "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
@@ -6844,6 +6878,7 @@
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@remix-run/router": "1.23.0"
},
diff --git a/package.json b/package.json
index 7331472..89170c6 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-query": "^5.83.0",
+ "@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
diff --git a/src/features/users/components/ActionButtons.tsx b/src/features/users/components/ActionButtons.tsx
new file mode 100644
index 0000000..cacb875
--- /dev/null
+++ b/src/features/users/components/ActionButtons.tsx
@@ -0,0 +1,213 @@
+'use client';
+
+import { useTransition } from 'react';
+import {
+ KeyRound,
+ Eye,
+ Ticket,
+ Ban,
+ Mail,
+ ShieldAlert,
+ LogOut,
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { toast } from 'sonner';
+import {
+ resetPassword,
+ impersonateUser,
+ terminateSessions,
+ createSupportTicket,
+} from '@/lib/actions/user-management';
+
+interface ActionButtonsProps {
+ userId: string;
+ userName: string;
+ onSuspend?: () => void; // Provided by parent to open modal
+ onSendNotification?: () => void; // Provided by parent
+}
+
+export function ActionButtons({
+ userId,
+ userName,
+ onSuspend,
+ onSendNotification,
+}: ActionButtonsProps) {
+ const [isPending, startTransition] = useTransition();
+
+ const handleResetPassword = () => {
+ startTransition(async () => {
+ const result = await resetPassword(userId);
+ if (result.success) {
+ toast.success('Password Reset Initiated', {
+ description: result.message,
+ });
+ } else {
+ toast.error('Action Failed', {
+ description: result.message,
+ });
+ }
+ });
+ };
+
+ const handleImpersonate = () => {
+ startTransition(async () => {
+ const result = await impersonateUser(userId);
+ if (result.success) {
+ toast.success(`Impersonating ${userName}`, {
+ description: 'Redirecting to user dashboard...',
+ });
+ // Simulate redirect delay
+ setTimeout(() => {
+ if (result.redirectUrl) window.location.href = result.redirectUrl;
+ }, 1000);
+ } else {
+ toast.error('Impersonation Failed', {
+ description: result.message,
+ });
+ }
+ });
+ };
+
+ const handleTerminateSessions = () => {
+ startTransition(async () => {
+ const result = await terminateSessions(userId);
+ if (result.success) {
+ toast.success('Sessions Terminated', {
+ description: 'User has been logged out from all devices.',
+ });
+ } else {
+ toast.error('Action Failed', {
+ description: result.message,
+ });
+ }
+ });
+ };
+
+ const handleCreateTicket = () => {
+ // Quick action: create a generic ticket for now
+ // In reality, this might open a dialog
+ startTransition(async () => {
+ const result = await createSupportTicket(userId, "User requires manual review");
+ if (result.success) {
+ toast.success('Support Ticket Created', {
+ description: result.message,
+ });
+ } else {
+ toast.error('Failed to create ticket', {
+ description: result.message,
+ });
+ }
+ });
+ };
+
+ return (
+
+
+
+
+
+
+ Reset Password
+
+
+
+
+
+
+
+
+ Impersonate User
+
+
+
+
+
+
+
+
+ Email / Notify User
+
+
+
+
+
+
+
+
+ Create Support Ticket
+
+
+
+
+
+
+
+
+
+
+
+
+ Terminate Sessions
+
+
+
+
+ Suspend / Ban User
+
+
+
+
+ );
+}
diff --git a/src/features/users/components/BanModal.tsx b/src/features/users/components/BanModal.tsx
new file mode 100644
index 0000000..e3f6979
--- /dev/null
+++ b/src/features/users/components/BanModal.tsx
@@ -0,0 +1,218 @@
+// BanModal - Ban workflow with ban types and evidence
+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 { Textarea } from '@/components/ui/textarea';
+import { Button } from '@/components/ui/button';
+import { Checkbox } from '@/components/ui/checkbox';
+import { Ban, Loader2, AlertOctagon, Shield, AtSign, Smartphone } from 'lucide-react';
+import { banUserSchema, type BanUserInput } from '@/lib/validations/user';
+import type { User, BanType } from '@/lib/types/user';
+import { toast } from 'sonner';
+import { cn } from '@/lib/utils';
+
+interface BanModalProps {
+ user: User | null;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onBanned?: () => void;
+}
+
+const banTypeConfig: Record = {
+ 'ip_ban': { label: 'IP Ban', description: 'Block logins from current IP address', icon: Shield },
+ 'device_ban': { label: 'Device Ban', description: 'Block logins from current device', icon: Smartphone },
+ 'email_blacklist': { label: 'Email Blacklist', description: 'Block signups from this email domain', icon: AtSign },
+};
+
+export function BanModal({ user, open, onOpenChange, onBanned }: BanModalProps) {
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const form = useForm({
+ resolver: zodResolver(banUserSchema),
+ defaultValues: {
+ banTypes: ['email_blacklist'],
+ publicReason: '',
+ internalEvidence: '',
+ },
+ });
+
+ const watchBanTypes = form.watch('banTypes');
+
+ const handleBanTypeToggle = (type: BanType) => {
+ const current = form.getValues('banTypes');
+ const updated = current.includes(type)
+ ? current.filter((t) => t !== type)
+ : [...current, type];
+ form.setValue('banTypes', updated as BanType[], { shouldValidate: true });
+ };
+
+ const onSubmit = async (data: BanUserInput) => {
+ if (!user) return;
+
+ setIsSubmitting(true);
+ try {
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ toast.success(`User "${user.name}" has been permanently banned`, {
+ description: `Ban types: ${data.banTypes.join(', ')}`,
+ });
+
+ onOpenChange(false);
+ onBanned?.();
+ form.reset();
+ } catch (error) {
+ toast.error('Failed to ban user');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleClose = () => {
+ if (!isSubmitting) {
+ onOpenChange(false);
+ form.reset();
+ }
+ };
+
+ if (!user) return null;
+
+ return (
+
+ );
+}
diff --git a/src/features/users/components/BulkActionsDropdown.tsx b/src/features/users/components/BulkActionsDropdown.tsx
new file mode 100644
index 0000000..8111114
--- /dev/null
+++ b/src/features/users/components/BulkActionsDropdown.tsx
@@ -0,0 +1,271 @@
+// BulkActionsDropdown - Bulk operations menu for selected users
+import { useState } from 'react';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+} from '@/components/ui/dropdown-menu';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import {
+ ChevronDown,
+ Bell,
+ Tag,
+ Shield,
+ Ban,
+ Trash2,
+ Download,
+ CheckCircle2,
+ XCircle,
+ Users,
+ Loader2,
+} from 'lucide-react';
+import { mockTags } from '../data/mockUserCrmData';
+import type { User } from '@/lib/types/user';
+import { toast } from 'sonner';
+import { cn } from '@/lib/utils';
+
+interface BulkActionsDropdownProps {
+ selectedUsers: User[];
+ onClearSelection: () => void;
+ onSendNotification: (users: User[]) => void;
+ onSuspendUsers: (users: User[]) => void;
+ onBanUsers: (users: User[]) => void;
+}
+
+export function BulkActionsDropdown({
+ selectedUsers,
+ onClearSelection,
+ onSendNotification,
+ onSuspendUsers,
+ onBanUsers,
+}: BulkActionsDropdownProps) {
+ const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
+ const [actionType, setActionType] = useState<'suspend' | 'ban' | 'delete' | 'export' | 'verify'>('suspend');
+ const [isProcessing, setIsProcessing] = useState(false);
+
+ const count = selectedUsers.length;
+
+ const handleAction = (action: 'suspend' | 'ban' | 'delete' | 'export' | 'verify') => {
+ setActionType(action);
+ setConfirmDialogOpen(true);
+ };
+
+ const handleConfirm = async () => {
+ setIsProcessing(true);
+ try {
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1500));
+
+ switch (actionType) {
+ case 'suspend':
+ toast.success(`Suspended ${count} user${count > 1 ? 's' : ''}`);
+ break;
+ case 'ban':
+ toast.success(`Banned ${count} user${count > 1 ? 's' : ''}`);
+ break;
+ case 'delete':
+ toast.success(`Deleted ${count} user${count > 1 ? 's' : ''}`);
+ break;
+ case 'export':
+ toast.success(`Exported ${count} user${count > 1 ? 's' : ''} to CSV`);
+ break;
+ case 'verify':
+ toast.success(`Verified ${count} user${count > 1 ? 's' : ''}`);
+ break;
+ }
+
+ setConfirmDialogOpen(false);
+ onClearSelection();
+ } catch (error) {
+ toast.error('Operation failed');
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ const handleTagUsers = async (tagId: string) => {
+ const tag = mockTags.find((t) => t.id === tagId);
+ if (!tag) return;
+
+ toast.success(`Added tag "${tag.name}" to ${count} user${count > 1 ? 's' : ''}`);
+ onClearSelection();
+ };
+
+ const actionConfig = {
+ suspend: {
+ title: 'Suspend Users',
+ description: `Are you sure you want to suspend ${count} user${count > 1 ? 's' : ''}? They will be unable to login.`,
+ icon: Shield,
+ buttonText: 'Suspend All',
+ buttonClass: 'bg-orange-600 hover:bg-orange-700',
+ },
+ ban: {
+ title: 'Ban Users',
+ description: `Are you sure you want to permanently ban ${count} user${count > 1 ? 's' : ''}? This action is severe.`,
+ icon: Ban,
+ buttonText: 'Ban All',
+ buttonClass: 'bg-red-600 hover:bg-red-700',
+ },
+ delete: {
+ title: 'Delete Users',
+ description: `Are you sure you want to permanently delete ${count} user${count > 1 ? 's' : ''}? This cannot be undone.`,
+ icon: Trash2,
+ buttonText: 'Delete All',
+ buttonClass: 'bg-red-600 hover:bg-red-700',
+ },
+ export: {
+ title: 'Export Users',
+ description: `Export ${count} user${count > 1 ? 's' : ''} to CSV file?`,
+ icon: Download,
+ buttonText: 'Export',
+ buttonClass: 'bg-primary hover:bg-primary/90',
+ },
+ verify: {
+ title: 'Verify Users',
+ description: `Mark ${count} user${count > 1 ? 's' : ''} as verified?`,
+ icon: CheckCircle2,
+ buttonText: 'Verify All',
+ buttonClass: 'bg-emerald-600 hover:bg-emerald-700',
+ },
+ };
+
+ const config = actionConfig[actionType];
+
+ if (count === 0) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+ onSendNotification(selectedUsers)}>
+ Send Notification
+
+
+
+
+ Add Tag
+
+
+ {mockTags.map((tag) => (
+ handleTagUsers(tag.id)}>
+
+ {tag.name}
+
+
+ ))}
+
+
+
+ handleAction('verify')}>
+ Verify Users
+
+
+ handleAction('export')}>
+ Export to CSV
+
+
+
+
+ handleAction('suspend')} className="text-orange-600">
+ Suspend Users
+
+
+ handleAction('ban')} className="text-red-600">
+ Ban Users
+
+
+
+
+ handleAction('delete')} className="text-red-600">
+ Delete Users
+
+
+
+
+
+ Clear Selection
+
+
+
+
+ {/* Confirmation Dialog */}
+
+ >
+ );
+}
diff --git a/src/features/users/components/CreateUserDialog.tsx b/src/features/users/components/CreateUserDialog.tsx
new file mode 100644
index 0000000..a40e217
--- /dev/null
+++ b/src/features/users/components/CreateUserDialog.tsx
@@ -0,0 +1,477 @@
+// 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([]);
+
+ const form = useForm({
+ 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 (
+
+ );
+}
diff --git a/src/features/users/components/DeleteConfirmDialog.tsx b/src/features/users/components/DeleteConfirmDialog.tsx
new file mode 100644
index 0000000..21b1e75
--- /dev/null
+++ b/src/features/users/components/DeleteConfirmDialog.tsx
@@ -0,0 +1,151 @@
+// DeleteConfirmDialog - Hard delete with type confirmation safeguard
+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,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import { Trash2, Loader2, AlertTriangle } from 'lucide-react';
+import { deleteConfirmSchema, type DeleteConfirmInput } from '@/lib/validations/user';
+import type { User } from '@/lib/types/user';
+import { toast } from 'sonner';
+
+interface DeleteConfirmDialogProps {
+ user: User | null;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onDeleted?: () => void;
+}
+
+export function DeleteConfirmDialog({ user, open, onOpenChange, onDeleted }: DeleteConfirmDialogProps) {
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const form = useForm({
+ resolver: zodResolver(deleteConfirmSchema),
+ defaultValues: {
+ confirmText: '' as 'DELETE',
+ },
+ });
+
+ const watchConfirmText = form.watch('confirmText');
+ const isConfirmValid = watchConfirmText === 'DELETE';
+
+ const onSubmit = async () => {
+ if (!user) return;
+
+ setIsSubmitting(true);
+ try {
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1500));
+
+ toast.success(`User "${user.name}" has been permanently deleted`);
+
+ onOpenChange(false);
+ onDeleted?.();
+ form.reset();
+ } catch (error) {
+ toast.error('Failed to delete user');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleClose = () => {
+ if (!isSubmitting) {
+ onOpenChange(false);
+ form.reset();
+ }
+ };
+
+ if (!user) return null;
+
+ return (
+
+ );
+}
diff --git a/src/features/users/components/NotificationComposer.tsx b/src/features/users/components/NotificationComposer.tsx
new file mode 100644
index 0000000..949aefd
--- /dev/null
+++ b/src/features/users/components/NotificationComposer.tsx
@@ -0,0 +1,365 @@
+// NotificationComposer - Custom push notification with templates and preview
+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 { Textarea } from '@/components/ui/textarea';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Switch } from '@/components/ui/switch';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import {
+ Bell,
+ Send,
+ Eye,
+ Clock,
+ Mail,
+ Smartphone,
+ Loader2,
+ Sparkles,
+ Zap,
+ CalendarDays,
+} from 'lucide-react';
+import { notificationSchema, type NotificationInput } from '@/lib/validations/user';
+import type { User } from '@/lib/types/user';
+
+// Local type for notification channels (not part of form schema but used for UI)
+type NotificationChannel = 'push' | 'email' | 'sms';
+import { toast } from 'sonner';
+import { cn } from '@/lib/utils';
+
+interface NotificationComposerProps {
+ users: User[];
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onSent?: () => void;
+}
+
+const templates = [
+ {
+ id: 'welcome',
+ name: 'Welcome',
+ icon: Sparkles,
+ title: 'Welcome to Eventify! 🎉',
+ body: 'Thank you for joining us. Explore amazing events happening near you!',
+ },
+ {
+ id: 'promo',
+ name: 'Flash Sale',
+ icon: Zap,
+ title: 'Flash Sale! 50% Off 🔥',
+ body: 'Limited time offer on premium events. Book now before it\'s gone!',
+ },
+ {
+ id: 'event',
+ name: 'Event Reminder',
+ icon: CalendarDays,
+ title: 'Don\'t Miss Out! 📅',
+ body: 'Your event is coming up soon. Make sure you\'re ready!',
+ },
+];
+
+const channelConfig: Record = {
+ push: { icon: Bell, label: 'Push' },
+ email: { icon: Mail, label: 'Email' },
+ sms: { icon: Smartphone, label: 'SMS' },
+};
+
+export function NotificationComposer({ users, open, onOpenChange, onSent }: NotificationComposerProps) {
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [activeTab, setActiveTab] = useState<'compose' | 'preview'>('compose');
+ const [selectedChannels, setSelectedChannels] = useState(['push']);
+
+ const form = useForm({
+ resolver: zodResolver(notificationSchema),
+ defaultValues: {
+ title: '',
+ body: '',
+ actionUrl: '',
+ priority: 'normal',
+ },
+ });
+
+ const watchTitle = form.watch('title');
+ const watchBody = form.watch('body');
+
+ const handleTemplateSelect = (template: typeof templates[0]) => {
+ form.setValue('title', template.title);
+ form.setValue('body', template.body);
+ };
+
+ const handleChannelToggle = (channel: NotificationChannel) => {
+ const updated = selectedChannels.includes(channel)
+ ? selectedChannels.filter((c) => c !== channel)
+ : [...selectedChannels, channel];
+ setSelectedChannels(updated);
+ // Note: channels are managed via local state, not form
+ };
+
+ const onSubmit = async (data: NotificationInput) => {
+ setIsSubmitting(true);
+ try {
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1500));
+
+ toast.success(`Notification sent to ${users.length} user${users.length > 1 ? 's' : ''}!`, {
+ description: data.title,
+ });
+
+ onOpenChange(false);
+ onSent?.();
+ form.reset();
+ setSelectedChannels(['push']);
+ } catch (error) {
+ toast.error('Failed to send notification');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleClose = () => {
+ if (!isSubmitting) {
+ onOpenChange(false);
+ form.reset();
+ setActiveTab('compose');
+ setSelectedChannels(['push']);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/features/users/components/SuspensionModal.tsx b/src/features/users/components/SuspensionModal.tsx
new file mode 100644
index 0000000..b73c85d
--- /dev/null
+++ b/src/features/users/components/SuspensionModal.tsx
@@ -0,0 +1,215 @@
+// SuspensionModal - Suspension workflow with reason, duration, and notification
+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 { Textarea } from '@/components/ui/textarea';
+import { Button } from '@/components/ui/button';
+import { Switch } from '@/components/ui/switch';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { AlertTriangle, Loader2 } from 'lucide-react';
+import { suspendUserSchema, type SuspendUserInput } from '@/lib/validations/user';
+import type { User } from '@/lib/types/user';
+import { toast } from 'sonner';
+
+interface SuspensionModalProps {
+ user: User | null;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onSuspended?: () => void;
+}
+
+const durationLabels = {
+ '7_days': '7 Days',
+ '30_days': '30 Days',
+ '90_days': '90 Days',
+ 'permanent': 'Permanent',
+};
+
+export function SuspensionModal({ user, open, onOpenChange, onSuspended }: SuspensionModalProps) {
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const form = useForm({
+ resolver: zodResolver(suspendUserSchema),
+ defaultValues: {
+ reason: 'Policy Violation',
+ customNote: '',
+ duration: '7_days',
+ sendEmail: true,
+ },
+ });
+
+ const onSubmit = async (data: SuspendUserInput) => {
+ if (!user) return;
+
+ setIsSubmitting(true);
+ try {
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ toast.success(`User "${user.name}" has been suspended`, {
+ description: `Duration: ${durationLabels[data.duration]}`,
+ });
+
+ onOpenChange(false);
+ onSuspended?.();
+ form.reset();
+ } catch (error) {
+ toast.error('Failed to suspend user');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleClose = () => {
+ if (!isSubmitting) {
+ onOpenChange(false);
+ form.reset();
+ }
+ };
+
+ if (!user) return null;
+
+ return (
+
+ );
+}
diff --git a/src/features/users/components/UserFilters.tsx b/src/features/users/components/UserFilters.tsx
new file mode 100644
index 0000000..4bcca8e
--- /dev/null
+++ b/src/features/users/components/UserFilters.tsx
@@ -0,0 +1,382 @@
+// UserFilters - Advanced filter sidebar with segments
+import { useState } from 'react';
+import {
+ Filter,
+ X,
+ ChevronDown,
+ ChevronUp,
+ Save,
+ RotateCcw,
+ Search,
+ Bookmark,
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Badge } from '@/components/ui/badge';
+import { Slider } from '@/components/ui/slider';
+import { Checkbox } from '@/components/ui/checkbox';
+import { Label } from '@/components/ui/label';
+import { Separator } from '@/components/ui/separator';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from '@/components/ui/collapsible';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import { mockTags, mockSegments } from '../data/mockUserCrmData';
+import type { UserStatus, UserRole, UserTier, SegmentFilters, UserSegment } from '@/lib/types/user';
+import { toast } from 'sonner';
+import { cn } from '@/lib/utils';
+
+interface UserFiltersProps {
+ filters: SegmentFilters;
+ onFiltersChange: (filters: SegmentFilters) => void;
+ isOpen: boolean;
+ onToggle: () => void;
+}
+
+const statusOptions: UserStatus[] = ['Active', 'Pending Verification', 'Suspended', 'Banned', 'Archived'];
+const roleOptions: UserRole[] = ['Super Admin', 'Admin', 'Support Agent', 'Partner', 'User'];
+const tierOptions: UserTier[] = ['Platinum', 'Gold', 'Silver', 'Bronze'];
+
+export function UserFilters({ filters, onFiltersChange, isOpen, onToggle }: UserFiltersProps) {
+ const [statusOpen, setStatusOpen] = useState(true);
+ const [roleOpen, setRoleOpen] = useState(false);
+ const [tierOpen, setTierOpen] = useState(false);
+ const [spentOpen, setSpentOpen] = useState(false);
+ const [tagsOpen, setTagsOpen] = useState(false);
+ const [segmentDialogOpen, setSegmentDialogOpen] = useState(false);
+ const [segmentName, setSegmentName] = useState('');
+
+ const activeFiltersCount = [
+ filters.search ? 1 : 0,
+ filters.status?.length ? 1 : 0,
+ filters.role?.length ? 1 : 0,
+ filters.tier?.length ? 1 : 0,
+ filters.tags?.length ? 1 : 0,
+ filters.minSpent ? 1 : 0,
+ filters.maxSpent ? 1 : 0,
+ ].reduce((a, b) => a + b, 0);
+
+ const handleStatusToggle = (status: UserStatus) => {
+ const current = filters.status || [];
+ const updated = current.includes(status)
+ ? current.filter((s) => s !== status)
+ : [...current, status];
+ onFiltersChange({ ...filters, status: updated.length ? updated : undefined });
+ };
+
+ const handleRoleToggle = (role: UserRole) => {
+ const current = filters.role || [];
+ const updated = current.includes(role)
+ ? current.filter((r) => r !== role)
+ : [...current, role];
+ onFiltersChange({ ...filters, role: updated.length ? updated : undefined });
+ };
+
+ const handleTierToggle = (tier: UserTier) => {
+ const current = filters.tier || [];
+ const updated = current.includes(tier)
+ ? current.filter((t) => t !== tier)
+ : [...current, tier];
+ onFiltersChange({ ...filters, tier: updated.length ? updated : undefined });
+ };
+
+ const handleTagToggle = (tagId: string) => {
+ const current = filters.tags || [];
+ const updated = current.includes(tagId)
+ ? current.filter((t) => t !== tagId)
+ : [...current, tagId];
+ onFiltersChange({ ...filters, tags: updated.length ? updated : undefined });
+ };
+
+ const handleSpentChange = (values: number[]) => {
+ onFiltersChange({
+ ...filters,
+ minSpent: values[0] > 0 ? values[0] : undefined,
+ maxSpent: values[1] < 200000 ? values[1] : undefined,
+ });
+ };
+
+ const handleSegmentApply = (segment: UserSegment) => {
+ onFiltersChange(segment.filters);
+ toast.success(`Applied segment: ${segment.name}`);
+ };
+
+ const handleSaveSegment = () => {
+ if (!segmentName.trim()) {
+ toast.error('Please enter a segment name');
+ return;
+ }
+ // In real app, this would save to backend
+ toast.success(`Segment "${segmentName}" saved!`);
+ setSegmentDialogOpen(false);
+ setSegmentName('');
+ };
+
+ const handleClearFilters = () => {
+ onFiltersChange({});
+ toast.info('Filters cleared');
+ };
+
+ if (!isOpen) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
Filters
+ {activeFiltersCount > 0 && (
+
+ {activeFiltersCount}
+
+ )}
+
+
+
+
+
+
+ {/* Search */}
+
+
+
+
+ onFiltersChange({ ...filters, search: e.target.value || undefined })}
+ className="pl-9 bg-white/50 border-white/50"
+ />
+
+
+
+
+
+ {/* Saved Segments */}
+
+
+
+
+
+
+
+ {/* Status Filter */}
+
+
+
+ {statusOpen ? : }
+
+
+ {statusOptions.map((status) => (
+
+ handleStatusToggle(status)}
+ />
+
+
+ ))}
+
+
+
+
+
+ {/* Role Filter */}
+
+
+
+ {roleOpen ? : }
+
+
+ {roleOptions.map((role) => (
+
+ handleRoleToggle(role)}
+ />
+
+
+ ))}
+
+
+
+
+
+ {/* Tier Filter */}
+
+
+
+ {tierOpen ? : }
+
+
+ {tierOptions.map((tier) => (
+
+ handleTierToggle(tier)}
+ />
+
+
+ ))}
+
+
+
+
+
+ {/* Total Spent Filter */}
+
+
+
+ {spentOpen ? : }
+
+
+
+
+ ₹{((filters.minSpent || 0) / 1000).toFixed(0)}K
+ ₹{((filters.maxSpent || 200000) / 1000).toFixed(0)}K
+
+
+
+
+
+
+ {/* Tags Filter */}
+
+
+
+ {tagsOpen ? : }
+
+
+
+ {mockTags.map((tag) => (
+ handleTagToggle(tag.id)}
+ className={cn(
+ 'cursor-pointer transition-all',
+ filters.tags?.includes(tag.id)
+ ? tag.color
+ : 'bg-white/50 text-muted-foreground hover:bg-white/80'
+ )}
+ >
+ {tag.name}
+
+ ))}
+
+
+
+
+
+
+ {/* Footer Actions */}
+
+
+
+
+
+
+ );
+}
diff --git a/src/features/users/components/UserInspectorSheet.tsx b/src/features/users/components/UserInspectorSheet.tsx
new file mode 100644
index 0000000..83de0c6
--- /dev/null
+++ b/src/features/users/components/UserInspectorSheet.tsx
@@ -0,0 +1,357 @@
+import { useState } from 'react';
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+} from '@/components/ui/sheet';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Textarea } from '@/components/ui/textarea';
+import { Separator } from '@/components/ui/separator';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+import {
+ User,
+ Mail,
+ Phone,
+ MapPin,
+ Shield,
+ Ticket,
+ CheckCircle2,
+ KeyRound,
+ Eye,
+ MessageSquare,
+ Ban,
+ Copy,
+ MoreHorizontal,
+ MoreVertical,
+ Clock,
+ AlertTriangle,
+ Plus,
+ Tag,
+} from 'lucide-react';
+import type { User as UserType } from '@/lib/types/user';
+import {
+ getUserBookings,
+ getUserNotes,
+} from '../data/mockUserCrmData';
+import { formatCurrency } from '@/data/mockData';
+import { ActionButtons } from './ActionButtons';
+import { toast } from 'sonner';
+import { cn } from '@/lib/utils';
+
+interface UserInspectorSheetProps {
+ user: UserType | null;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onEditUser: (user: UserType) => void;
+ onSendNotification: (user: UserType) => void;
+}
+
+export function UserInspectorSheet({
+ user,
+ open,
+ onOpenChange,
+ onEditUser,
+ onSendNotification,
+}: UserInspectorSheetProps) {
+ const [activeTab, setActiveTab] = useState('overview');
+ const [noteContent, setNoteContent] = useState('');
+
+ if (!user) return null;
+
+ // Fetch related data
+ const bookings = getUserBookings(user.id);
+ const notes = getUserNotes(user.id);
+
+ // Derived Metrics
+ const successBookings = bookings.filter(b => b.status === 'Attended' || b.status === 'Confirmed').length;
+ const cancelledBookings = bookings.filter(b => b.status === 'Cancelled' || b.status === 'Refunded').length;
+ const isHighRisk = user.refundRate > 5;
+
+ const copyToClipboard = (text: string) => {
+ navigator.clipboard.writeText(text);
+ toast.success('Copied to clipboard');
+ };
+
+ const handleAction = (label: string) => {
+ toast.success(`${label} action triggered`);
+ };
+
+ return (
+
+
+ {/* Section A: The Header (Compact) */}
+
+
+
+
+
+
+ {user.name.charAt(0)}
+
+
+
+
+
{user.name}
+ {user.isVerified && (
+
+ )}
+
+
+ copyToClipboard(user.id)}
+ title="Click to copy ID"
+ >
+ {user.id}
+
+ •
+ Joined {new Date(user.createdAt).toLocaleDateString('en-IN', { month: 'short', year: 'numeric' })}
+
+ {user.role}
+
+
+
+
+
+
+
+ {user.status}
+
+
+
+
+
+
+ onEditUser(user)}>Edit Profile
+ handleAction('Archive')}>Archive User
+ handleAction('Delete')}>Delete User
+
+
+
+
+
+
+ {/* Section B: Key Metrics Grid */}
+
+
+
LTV
+
50000 && "text-amber-600")}>
+ {formatCurrency(user.totalSpent)}
+
+
+
+
Bookings
+
+
+
+
+ {user.bookingsCount}
+
+
+
+
+ {successBookings} Success • {cancelledBookings} Cancelled
+
+
+
+
+
+
+
Avg. Ticket
+
+ {formatCurrency(user.averageOrderValue)}
+
+
+
+
Refund Risk
+
+ {user.refundRate}% {isHighRisk ? 'High' : 'Low'}
+
+
+
+
+ {/* Section C: Action Toolbar */}
+
+
handleAction('Suspend')}
+ onSendNotification={() => onSendNotification(user)}
+ />
+
+
+ {/* Section D: Tabbed Content */}
+
+
+
+ Overview
+ Orders
+ Admin Notes
+
+
+
+
+
+ {/* Tab 1: Overview */}
+
+ {/* Contact Info */}
+
+
Contact Information
+
+
+
+ {user.email}
+
+
+
+
{user.countryCode} {user.phone}
+
+ {user.lastDevice && (
+
+
+ {user.lastDevice.location}
+
+ )}
+
+
+
+
+
+ {/* Last Activity */}
+
+
Last Activity
+
+
+
+
+
+
Scanned at Tech Summit 2026
+
2 hours ago • Verified by Staff
+
+
+
+
+
+
+ {/* Tags */}
+
+
Tags
+
+ {user.tags.map((tag) => (
+
+ {tag.name}
+
+ ))}
+
+
+
+
+
+ {/* Tab 2: Orders */}
+
+
+
+
+ Event
+ Date
+ Amount
+ Status
+
+
+
+ {bookings.slice(0, 5).map((booking) => (
+
+
+ {booking.eventName}
+ {booking.ticketType} x{booking.quantity}
+
+
+ {new Date(booking.eventDate).toLocaleDateString('en-IN', { day: 'numeric', month: 'short' })}
+
+
+ {formatCurrency(booking.amount)}
+
+
+
+ {booking.status}
+
+
+
+ ))}
+
+
+ {bookings.length === 0 && (
+ No orders found.
+ )}
+
+
+ {/* Tab 3: Admin Notes */}
+
+
+
+
+
+
+ {notes.map((note) => (
+
+
{note.content}
+
+ {note.authorName}
+ • {new Date(note.createdAt).toLocaleDateString()}
+
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/features/users/components/UserMetricsBar.tsx b/src/features/users/components/UserMetricsBar.tsx
new file mode 100644
index 0000000..3f91eaf
--- /dev/null
+++ b/src/features/users/components/UserMetricsBar.tsx
@@ -0,0 +1,101 @@
+// UserMetricsBar - Analytics dashboard strip above table
+import {
+ Users,
+ UserCheck,
+ UserPlus,
+ AlertTriangle,
+ TrendingUp,
+ TrendingDown,
+ DollarSign,
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
+import type { UserMetrics } from '@/lib/types/user';
+import { formatCurrency } from '@/data/mockData';
+
+interface UserMetricsBarProps {
+ metrics: UserMetrics;
+}
+
+interface MetricCardProps {
+ title: string;
+ value: string | number;
+ trend?: number;
+ icon: React.ElementType;
+ iconColor: string;
+ iconBg: string;
+}
+
+function MetricCard({ title, value, trend, icon: Icon, iconColor, iconBg }: MetricCardProps) {
+ return (
+
+
+
+
+
+
{title}
+
+
{value}
+ {trend !== undefined && (
+
= 0 ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'
+ )}>
+ {trend >= 0 ? (
+
+ ) : (
+
+ )}
+ {Math.abs(trend).toFixed(1)}%
+
+ )}
+
+
+
+ );
+}
+
+export function UserMetricsBar({ metrics }: UserMetricsBarProps) {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/features/users/components/UsersTable.tsx b/src/features/users/components/UsersTable.tsx
new file mode 100644
index 0000000..bd80152
--- /dev/null
+++ b/src/features/users/components/UsersTable.tsx
@@ -0,0 +1,459 @@
+// UsersTable - TanStack Table implementation with sorting, selection, and actions
+import { useState, useMemo } from 'react';
+import {
+ ColumnDef,
+ flexRender,
+ getCoreRowModel,
+ getSortedRowModel,
+ SortingState,
+ useReactTable,
+ RowSelectionState,
+} from '@tanstack/react-table';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import { Checkbox } from '@/components/ui/checkbox';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+import {
+ MoreHorizontal,
+ Copy,
+ Mail,
+ Shield,
+ Ban,
+ Trash2,
+ Edit,
+ Bell,
+ Eye,
+ CheckCircle2,
+ Clock,
+ XCircle,
+ AlertCircle,
+ Archive,
+ ArrowUpDown,
+ ChevronUp,
+ ChevronDown,
+} from 'lucide-react';
+import { toast } from 'sonner';
+import type { User, UserStatus, UserRole } from '@/lib/types/user';
+import { formatCurrency } from '@/data/mockData';
+import { cn } from '@/lib/utils';
+
+interface UsersTableProps {
+ users: User[];
+ onSelectUser: (user: User) => void;
+ onEditUser: (user: User) => void;
+ onSuspendUser: (user: User) => void;
+ onBanUser: (user: User) => void;
+ onDeleteUser: (user: User) => void;
+ onSendNotification: (user: User) => void;
+ selectedUserIds: string[];
+ onSelectionChange: (userIds: string[]) => void;
+}
+
+// Status configuration
+const statusConfig: Record = {
+ 'Active': { icon: CheckCircle2, color: 'text-emerald-600', dotColor: 'bg-emerald-500' },
+ 'Pending Verification': { icon: Clock, color: 'text-amber-600', dotColor: 'bg-amber-500' },
+ 'Suspended': { icon: AlertCircle, color: 'text-orange-600', dotColor: 'bg-orange-500' },
+ 'Banned': { icon: XCircle, color: 'text-red-600', dotColor: 'bg-red-500' },
+ 'Archived': { icon: Archive, color: 'text-slate-500', dotColor: 'bg-slate-400' },
+};
+
+// Role badge colors
+const roleColors: Record = {
+ 'Super Admin': 'bg-purple-500/20 text-purple-700 border-purple-300',
+ 'Admin': 'bg-purple-400/20 text-purple-600 border-purple-200',
+ 'Support Agent': 'bg-blue-500/20 text-blue-700 border-blue-300',
+ 'Partner': 'bg-cyan-500/20 text-cyan-700 border-cyan-300',
+ 'User': 'bg-green-500/20 text-green-700 border-green-300',
+};
+
+// Health score badges
+const healthBadges = {
+ hot: { emoji: '🔥', label: 'Hot', color: 'bg-orange-100 text-orange-700' },
+ warm: { emoji: '😐', label: 'Warm', color: 'bg-yellow-100 text-yellow-700' },
+ cold: { emoji: '🧊', label: 'Cold', color: 'bg-blue-100 text-blue-700' },
+};
+
+// Responsive column visibility
+const columnVisibilityClasses: Record = {
+ email: 'hidden md:table-cell',
+ role: 'hidden lg:table-cell',
+ status: 'hidden sm:table-cell',
+ bookingsCount: 'hidden xl:table-cell',
+ totalSpent: 'hidden lg:table-cell',
+ lastActivityAt: 'hidden xl:table-cell',
+};
+
+function formatRelativeTime(dateString: string): string {
+ const date = new Date(dateString);
+ const now = new Date();
+ const diffMs = now.getTime() - date.getTime();
+ const diffMins = Math.floor(diffMs / (1000 * 60));
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
+
+ if (diffMins < 1) return 'Just now';
+ if (diffMins < 60) return `${diffMins}m ago`;
+ if (diffHours < 24) return `${diffHours}h ago`;
+ if (diffDays < 7) return `${diffDays}d ago`;
+ return date.toLocaleDateString('en-IN', { day: 'numeric', month: 'short' });
+}
+
+export function UsersTable({
+ users,
+ onSelectUser,
+ onEditUser,
+ onSuspendUser,
+ onBanUser,
+ onDeleteUser,
+ onSendNotification,
+ selectedUserIds,
+ onSelectionChange,
+}: UsersTableProps) {
+ const [sorting, setSorting] = useState([]);
+
+ const rowSelection = useMemo(() => {
+ const selection: RowSelectionState = {};
+ selectedUserIds.forEach((id) => {
+ const index = users.findIndex((u) => u.id === id);
+ if (index !== -1) selection[index] = true;
+ });
+ return selection;
+ }, [selectedUserIds, users]);
+
+ const handleCopyEmail = (email: string, e: React.MouseEvent) => {
+ e.stopPropagation();
+ navigator.clipboard.writeText(email);
+ toast.success('Email copied to clipboard');
+ };
+
+ const columns: ColumnDef[] = useMemo(
+ () => [
+ {
+ id: 'select',
+ header: ({ table }) => (
+ table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ className="translate-y-[2px]"
+ />
+ ),
+ cell: ({ row }) => (
+ row.toggleSelected(!!value)}
+ onClick={(e) => e.stopPropagation()}
+ aria-label="Select row"
+ className="translate-y-[2px]"
+ />
+ ),
+ enableSorting: false,
+ size: 40,
+ },
+ {
+ accessorKey: 'name',
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const user = row.original;
+ const health = healthBadges[user.healthScore];
+ return (
+
+
+
+
+ {user.name.charAt(0)}
+
+
+
+
+ {user.name}
+ {user.isVerified && (
+
+
+
+
+ Verified User
+
+ )}
+ {health.emoji}
+
+
{user.id}
+
+
+ );
+ },
+ size: 250,
+ },
+ {
+ accessorKey: 'email',
+ header: 'Email',
+ cell: ({ row }) => {
+ const email = row.original.email;
+ return (
+
+ {email}
+
+
+ );
+ },
+ size: 220,
+ },
+ {
+ accessorKey: 'role',
+ header: 'Role',
+ cell: ({ row }) => {
+ const role = row.original.role;
+ return (
+
+ {role}
+
+ );
+ },
+ size: 130,
+ },
+ {
+ accessorKey: 'status',
+ header: 'Status',
+ cell: ({ row }) => {
+ const status = row.original.status;
+ const config = statusConfig[status];
+ return (
+
+
+ {status}
+
+ );
+ },
+ size: 150,
+ },
+ {
+ accessorKey: 'bookingsCount',
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => (
+ {row.original.bookingsCount}
+ ),
+ size: 100,
+ },
+ {
+ accessorKey: 'totalSpent',
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => (
+ {formatCurrency(row.original.totalSpent)}
+ ),
+ size: 100,
+ },
+ {
+ accessorKey: 'lastActivityAt',
+ header: 'Last Active',
+ cell: ({ row }) => (
+
+ {formatRelativeTime(row.original.lastActivityAt)}
+
+ ),
+ size: 110,
+ },
+ {
+ id: 'actions',
+ cell: ({ row }) => {
+ const user = row.original;
+ return (
+
+
+
+
+
+ onSelectUser(user)}>
+ View Details
+
+ onEditUser(user)}>
+ Edit User
+
+ onSendNotification(user)}>
+ Send Notification
+
+
+ {user.status === 'Active' && (
+ onSuspendUser(user)} className="text-orange-600">
+ Suspend User
+
+ )}
+ {user.status !== 'Banned' && (
+ onBanUser(user)} className="text-red-600">
+ Ban User
+
+ )}
+
+ onDeleteUser(user)} className="text-red-600">
+ Delete User
+
+
+
+ );
+ },
+ size: 50,
+ },
+ ],
+ [onSelectUser, onEditUser, onSuspendUser, onBanUser, onDeleteUser, onSendNotification]
+ );
+
+ const table = useReactTable({
+ data: users,
+ columns,
+ state: { sorting, rowSelection },
+ onSortingChange: setSorting,
+ onRowSelectionChange: (updater) => {
+ const newSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
+ const newIds = Object.keys(newSelection)
+ .filter((key) => newSelection[key])
+ .map((key) => users[parseInt(key)]?.id)
+ .filter(Boolean) as string[];
+ onSelectionChange(newIds);
+ },
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ });
+
+ return (
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(header.column.columnDef.header, header.getContext())}
+
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => (
+ onSelectUser(row.original)}
+ >
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
+
+ ))
+ ) : (
+
+
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/features/users/data/mockUserCrmData.ts b/src/features/users/data/mockUserCrmData.ts
new file mode 100644
index 0000000..89ac1be
--- /dev/null
+++ b/src/features/users/data/mockUserCrmData.ts
@@ -0,0 +1,711 @@
+// User Command Center - Extended Mock Data
+// Comprehensive dataset for CRM, support, and moderation features
+
+import type {
+ User,
+ UserTag,
+ UserSegment,
+ UserBooking,
+ UserSession,
+ SuspiciousActivity,
+ SupportTicket,
+ UserNote,
+ EmailLog,
+ Notification,
+ AuditLog,
+ UserMetrics,
+ TimelineEvent,
+ NotificationTemplate,
+} from '@/lib/types/user';
+
+// ============================================
+// TAGS
+// ============================================
+
+export const mockTags: UserTag[] = [
+ { id: 'tag-vip', name: 'VIP', color: 'bg-purple-500/20 text-purple-600 border-purple-300' },
+ { id: 'tag-influencer', name: 'Influencer', color: 'bg-pink-500/20 text-pink-600 border-pink-300' },
+ { id: 'tag-press', name: 'Press', color: 'bg-blue-500/20 text-blue-600 border-blue-300' },
+ { id: 'tag-early-adopter', name: 'Early Adopter', color: 'bg-green-500/20 text-green-600 border-green-300' },
+ { id: 'tag-big-spender', name: 'Big Spender', color: 'bg-yellow-500/20 text-yellow-700 border-yellow-400' },
+ { id: 'tag-problem', name: 'Problem User', color: 'bg-red-500/20 text-red-600 border-red-300' },
+ { id: 'tag-new', name: 'New User', color: 'bg-cyan-500/20 text-cyan-600 border-cyan-300' },
+ { id: 'tag-corporate', name: 'Corporate', color: 'bg-slate-500/20 text-slate-600 border-slate-300' },
+];
+
+// ============================================
+// USERS - 20+ comprehensive entries
+// ============================================
+
+export const mockCrmUsers: User[] = [
+ {
+ id: 'usr-001',
+ name: 'Priya Sharma',
+ email: 'priya.sharma@gmail.com',
+ phone: '9876543210',
+ countryCode: '+91',
+ avatarUrl: 'https://i.pravatar.cc/150?u=priya',
+ role: 'User',
+ status: 'Active',
+ tier: 'Platinum',
+ healthScore: 'hot',
+ isVerified: true,
+ is2FAEnabled: true,
+ totalSpent: 125000,
+ bookingsCount: 45,
+ refundRate: 2.2,
+ averageOrderValue: 2778,
+ language: 'en',
+ timezone: 'Asia/Kolkata',
+ currency: 'INR',
+ emailNotifications: true,
+ pushNotifications: true,
+ smsNotifications: false,
+ tags: [mockTags[0], mockTags[4]], // VIP, Big Spender
+ pinnedNote: 'Top customer - Priority support always',
+ createdAt: '2023-01-15T10:30:00Z',
+ updatedAt: '2026-02-08T14:20:00Z',
+ lastLoginAt: '2026-02-09T08:45:00Z',
+ lastActivityAt: '2026-02-09T09:30:00Z',
+ lastDevice: { os: 'iOS 17', browser: 'Safari', ip: '103.42.156.78', location: 'Mumbai, IN' },
+ },
+ {
+ id: 'usr-002',
+ name: 'Rahul Verma',
+ email: 'rahul.verma@outlook.com',
+ phone: '8765432109',
+ countryCode: '+91',
+ avatarUrl: 'https://i.pravatar.cc/150?u=rahul',
+ role: 'User',
+ status: 'Active',
+ tier: 'Gold',
+ healthScore: 'warm',
+ isVerified: true,
+ is2FAEnabled: false,
+ totalSpent: 45000,
+ bookingsCount: 18,
+ refundRate: 5.5,
+ averageOrderValue: 2500,
+ language: 'hi',
+ timezone: 'Asia/Kolkata',
+ currency: 'INR',
+ emailNotifications: true,
+ pushNotifications: true,
+ smsNotifications: true,
+ tags: [mockTags[3]], // Early Adopter
+ createdAt: '2023-06-20T15:45:00Z',
+ updatedAt: '2026-02-05T11:00:00Z',
+ lastLoginAt: '2026-02-07T16:30:00Z',
+ lastActivityAt: '2026-02-07T17:15:00Z',
+ lastDevice: { os: 'Android 14', browser: 'Chrome', ip: '49.207.183.42', location: 'Delhi, IN' },
+ },
+ {
+ id: 'usr-003',
+ name: 'Ananya Patel',
+ email: 'ananya.p@yahoo.com',
+ phone: '7654321098',
+ countryCode: '+91',
+ avatarUrl: 'https://i.pravatar.cc/150?u=ananya',
+ role: 'User',
+ status: 'Pending Verification',
+ tier: 'Bronze',
+ healthScore: 'cold',
+ isVerified: false,
+ is2FAEnabled: false,
+ totalSpent: 0,
+ bookingsCount: 0,
+ refundRate: 0,
+ averageOrderValue: 0,
+ language: 'en',
+ timezone: 'Asia/Kolkata',
+ currency: 'INR',
+ emailNotifications: true,
+ pushNotifications: false,
+ smsNotifications: false,
+ tags: [mockTags[6]], // New User
+ createdAt: '2026-02-08T09:00:00Z',
+ updatedAt: '2026-02-08T09:00:00Z',
+ lastLoginAt: '2026-02-08T09:00:00Z',
+ lastActivityAt: '2026-02-08T09:05:00Z',
+ lastDevice: { os: 'Windows 11', browser: 'Edge', ip: '122.161.52.19', location: 'Ahmedabad, IN' },
+ },
+ {
+ id: 'usr-004',
+ name: 'Vikram Singh',
+ email: 'vikram.s@proton.me',
+ phone: '6543210987',
+ countryCode: '+91',
+ avatarUrl: 'https://i.pravatar.cc/150?u=vikram',
+ role: 'User',
+ status: 'Suspended',
+ tier: 'Silver',
+ healthScore: 'cold',
+ isVerified: true,
+ is2FAEnabled: true,
+ totalSpent: 15000,
+ bookingsCount: 8,
+ refundRate: 25,
+ averageOrderValue: 1875,
+ language: 'en',
+ timezone: 'Asia/Kolkata',
+ currency: 'INR',
+ emailNotifications: false,
+ pushNotifications: false,
+ smsNotifications: false,
+ tags: [mockTags[5]], // Problem User
+ pinnedNote: 'Suspended for multiple chargebacks. Review before reinstatement.',
+ createdAt: '2024-03-10T12:00:00Z',
+ updatedAt: '2026-02-01T09:30:00Z',
+ lastLoginAt: '2026-01-28T11:20:00Z',
+ lastActivityAt: '2026-01-28T11:45:00Z',
+ lastDevice: { os: 'macOS 14', browser: 'Chrome', ip: '157.33.48.92', location: 'Bangalore, IN' },
+ },
+ {
+ id: 'usr-005',
+ name: 'Meera Reddy',
+ email: 'meera.reddy@gmail.com',
+ phone: '9988776655',
+ countryCode: '+91',
+ avatarUrl: 'https://i.pravatar.cc/150?u=meera',
+ role: 'User',
+ status: 'Banned',
+ tier: 'Bronze',
+ healthScore: 'cold',
+ isVerified: true,
+ is2FAEnabled: false,
+ totalSpent: 2500,
+ bookingsCount: 2,
+ refundRate: 100,
+ averageOrderValue: 1250,
+ language: 'en',
+ timezone: 'Asia/Kolkata',
+ currency: 'INR',
+ emailNotifications: false,
+ pushNotifications: false,
+ smsNotifications: false,
+ tags: [mockTags[5]], // Problem User
+ pinnedNote: 'BANNED: Fraud - Multiple stolen credit cards used',
+ createdAt: '2025-08-15T08:00:00Z',
+ updatedAt: '2025-10-20T14:00:00Z',
+ lastLoginAt: '2025-10-18T10:30:00Z',
+ lastActivityAt: '2025-10-18T10:35:00Z',
+ lastDevice: { os: 'Android 13', browser: 'Chrome', ip: '103.87.141.52', location: 'Hyderabad, IN' },
+ },
+ {
+ id: 'usr-006',
+ name: 'Arjun Nair',
+ email: 'arjun.nair@techcorp.com',
+ phone: '8899776655',
+ countryCode: '+91',
+ avatarUrl: 'https://i.pravatar.cc/150?u=arjun',
+ role: 'Partner',
+ status: 'Active',
+ tier: 'Gold',
+ healthScore: 'hot',
+ isVerified: true,
+ is2FAEnabled: true,
+ totalSpent: 250000,
+ bookingsCount: 60,
+ refundRate: 1.5,
+ averageOrderValue: 4167,
+ language: 'en',
+ timezone: 'Asia/Kolkata',
+ currency: 'INR',
+ emailNotifications: true,
+ pushNotifications: true,
+ smsNotifications: true,
+ tags: [mockTags[0], mockTags[7]], // VIP, Corporate
+ pinnedNote: 'Corporate account - TechCorp bulk bookings',
+ createdAt: '2023-02-28T10:00:00Z',
+ updatedAt: '2026-02-09T08:00:00Z',
+ lastLoginAt: '2026-02-09T10:15:00Z',
+ lastActivityAt: '2026-02-09T10:45:00Z',
+ lastDevice: { os: 'Windows 11', browser: 'Chrome', ip: '14.139.42.15', location: 'Chennai, IN' },
+ },
+ {
+ id: 'usr-007',
+ name: 'Sneha Kapoor',
+ email: 'sneha.kapoor@instagram.fam',
+ phone: '7788996655',
+ countryCode: '+91',
+ avatarUrl: 'https://i.pravatar.cc/150?u=sneha',
+ role: 'User',
+ status: 'Active',
+ tier: 'Gold',
+ healthScore: 'hot',
+ isVerified: true,
+ is2FAEnabled: false,
+ totalSpent: 78000,
+ bookingsCount: 32,
+ refundRate: 3.1,
+ averageOrderValue: 2438,
+ language: 'en',
+ timezone: 'Asia/Kolkata',
+ currency: 'INR',
+ emailNotifications: true,
+ pushNotifications: true,
+ smsNotifications: false,
+ tags: [mockTags[1], mockTags[0]], // Influencer, VIP
+ pinnedNote: 'Instagram influencer 500K followers. Comp tickets for fashion events.',
+ createdAt: '2023-09-05T14:30:00Z',
+ updatedAt: '2026-02-08T16:00:00Z',
+ lastLoginAt: '2026-02-09T07:30:00Z',
+ lastActivityAt: '2026-02-09T08:00:00Z',
+ lastDevice: { os: 'iOS 17', browser: 'Safari', ip: '103.219.75.33', location: 'Mumbai, IN' },
+ },
+ {
+ id: 'usr-008',
+ name: 'Karan Malhotra',
+ email: 'karan.m@gmail.com',
+ phone: '6677889944',
+ countryCode: '+91',
+ avatarUrl: 'https://i.pravatar.cc/150?u=karan',
+ role: 'User',
+ status: 'Active',
+ tier: 'Silver',
+ healthScore: 'warm',
+ isVerified: true,
+ is2FAEnabled: false,
+ totalSpent: 22000,
+ bookingsCount: 12,
+ refundRate: 8.3,
+ averageOrderValue: 1833,
+ language: 'en',
+ timezone: 'Asia/Kolkata',
+ currency: 'INR',
+ emailNotifications: true,
+ pushNotifications: false,
+ smsNotifications: false,
+ tags: [],
+ createdAt: '2024-05-12T11:00:00Z',
+ updatedAt: '2026-02-04T15:30:00Z',
+ lastLoginAt: '2026-02-06T19:45:00Z',
+ lastActivityAt: '2026-02-06T20:10:00Z',
+ lastDevice: { os: 'Android 14', browser: 'Chrome', ip: '223.226.187.44', location: 'Pune, IN' },
+ },
+ {
+ id: 'usr-009',
+ name: 'Divya Iyer',
+ email: 'divya.iyer@journalism.in',
+ phone: '5566778899',
+ countryCode: '+91',
+ avatarUrl: 'https://i.pravatar.cc/150?u=divya',
+ role: 'User',
+ status: 'Active',
+ tier: 'Silver',
+ healthScore: 'warm',
+ isVerified: true,
+ is2FAEnabled: true,
+ totalSpent: 18500,
+ bookingsCount: 15,
+ refundRate: 0,
+ averageOrderValue: 1233,
+ language: 'en',
+ timezone: 'Asia/Kolkata',
+ currency: 'INR',
+ emailNotifications: true,
+ pushNotifications: true,
+ smsNotifications: true,
+ tags: [mockTags[2]], // Press
+ pinnedNote: 'JOURNALIST - Times of India. Handle with care, provide press passes.',
+ createdAt: '2024-01-20T09:00:00Z',
+ updatedAt: '2026-02-07T12:00:00Z',
+ lastLoginAt: '2026-02-08T14:30:00Z',
+ lastActivityAt: '2026-02-08T15:00:00Z',
+ lastDevice: { os: 'macOS 14', browser: 'Safari', ip: '182.73.156.89', location: 'Mumbai, IN' },
+ },
+ {
+ id: 'usr-010',
+ name: 'Rohan Chatterjee',
+ email: 'rohan.c@yahoo.co.in',
+ phone: '4455667788',
+ countryCode: '+91',
+ avatarUrl: 'https://i.pravatar.cc/150?u=rohan',
+ role: 'User',
+ status: 'Archived',
+ tier: 'Bronze',
+ healthScore: 'cold',
+ isVerified: true,
+ is2FAEnabled: false,
+ totalSpent: 5000,
+ bookingsCount: 4,
+ refundRate: 25,
+ averageOrderValue: 1250,
+ language: 'en',
+ timezone: 'Asia/Kolkata',
+ currency: 'INR',
+ emailNotifications: false,
+ pushNotifications: false,
+ smsNotifications: false,
+ tags: [],
+ createdAt: '2024-07-08T16:00:00Z',
+ updatedAt: '2025-12-15T10:00:00Z',
+ lastLoginAt: '2025-11-20T09:00:00Z',
+ lastActivityAt: '2025-11-20T09:30:00Z',
+ lastDevice: { os: 'Windows 10', browser: 'Firefox', ip: '117.197.45.88', location: 'Kolkata, IN' },
+ },
+ {
+ id: 'usr-011',
+ name: 'Aisha Khan',
+ email: 'aisha.khan@corporate.io',
+ phone: '9123456780',
+ countryCode: '+91',
+ avatarUrl: 'https://i.pravatar.cc/150?u=aisha',
+ role: 'Admin',
+ status: 'Active',
+ tier: 'Platinum',
+ healthScore: 'hot',
+ isVerified: true,
+ is2FAEnabled: true,
+ totalSpent: 0,
+ bookingsCount: 0,
+ refundRate: 0,
+ averageOrderValue: 0,
+ language: 'en',
+ timezone: 'Asia/Kolkata',
+ currency: 'INR',
+ emailNotifications: true,
+ pushNotifications: true,
+ smsNotifications: true,
+ tags: [],
+ createdAt: '2022-06-01T08:00:00Z',
+ updatedAt: '2026-02-09T09:00:00Z',
+ lastLoginAt: '2026-02-09T09:00:00Z',
+ lastActivityAt: '2026-02-09T10:30:00Z',
+ lastDevice: { os: 'macOS 14', browser: 'Chrome', ip: '14.98.172.55', location: 'Delhi, IN' },
+ },
+ {
+ id: 'usr-012',
+ name: 'Nikhil Joshi',
+ email: 'nikhil.joshi@hotmail.com',
+ phone: '8234567891',
+ countryCode: '+91',
+ avatarUrl: 'https://i.pravatar.cc/150?u=nikhil',
+ role: 'User',
+ status: 'Active',
+ tier: 'Bronze',
+ healthScore: 'cold',
+ isVerified: true,
+ is2FAEnabled: false,
+ totalSpent: 3500,
+ bookingsCount: 3,
+ refundRate: 0,
+ averageOrderValue: 1167,
+ language: 'en',
+ timezone: 'Asia/Kolkata',
+ currency: 'INR',
+ emailNotifications: true,
+ pushNotifications: false,
+ smsNotifications: false,
+ tags: [],
+ createdAt: '2025-03-15T14:00:00Z',
+ updatedAt: '2026-01-20T11:00:00Z',
+ lastLoginAt: '2026-01-15T18:00:00Z',
+ lastActivityAt: '2026-01-15T18:30:00Z',
+ lastDevice: { os: 'Android 13', browser: 'Chrome', ip: '203.122.45.67', location: 'Jaipur, IN' },
+ },
+ {
+ id: 'usr-013',
+ name: 'Pooja Menon',
+ email: 'pooja.menon@gmail.com',
+ phone: '7345678902',
+ countryCode: '+91',
+ avatarUrl: 'https://i.pravatar.cc/150?u=pooja',
+ role: 'User',
+ status: 'Active',
+ tier: 'Gold',
+ healthScore: 'hot',
+ isVerified: true,
+ is2FAEnabled: true,
+ totalSpent: 68000,
+ bookingsCount: 28,
+ refundRate: 3.6,
+ averageOrderValue: 2429,
+ language: 'en',
+ timezone: 'Asia/Kolkata',
+ currency: 'INR',
+ emailNotifications: true,
+ pushNotifications: true,
+ smsNotifications: true,
+ tags: [mockTags[3], mockTags[4]], // Early Adopter, Big Spender
+ createdAt: '2023-04-10T10:00:00Z',
+ updatedAt: '2026-02-08T20:00:00Z',
+ lastLoginAt: '2026-02-09T06:30:00Z',
+ lastActivityAt: '2026-02-09T07:00:00Z',
+ lastDevice: { os: 'iOS 17', browser: 'Safari', ip: '59.94.178.23', location: 'Bangalore, IN' },
+ },
+ {
+ id: 'usr-014',
+ name: 'Amit Saxena',
+ email: 'amit.saxena@business.org',
+ phone: '6456789013',
+ countryCode: '+91',
+ avatarUrl: 'https://i.pravatar.cc/150?u=amit',
+ role: 'Support Agent',
+ status: 'Active',
+ tier: 'Silver',
+ healthScore: 'warm',
+ isVerified: true,
+ is2FAEnabled: true,
+ totalSpent: 0,
+ bookingsCount: 0,
+ refundRate: 0,
+ averageOrderValue: 0,
+ language: 'en',
+ timezone: 'Asia/Kolkata',
+ currency: 'INR',
+ emailNotifications: true,
+ pushNotifications: true,
+ smsNotifications: false,
+ tags: [],
+ createdAt: '2024-02-01T09:00:00Z',
+ updatedAt: '2026-02-09T08:30:00Z',
+ lastLoginAt: '2026-02-09T08:30:00Z',
+ lastActivityAt: '2026-02-09T10:00:00Z',
+ lastDevice: { os: 'Windows 11', browser: 'Chrome', ip: '14.139.82.44', location: 'Gurgaon, IN' },
+ },
+ {
+ id: 'usr-015',
+ name: 'Kavitha Rajan',
+ email: 'kavitha.r@startup.co',
+ phone: '5567890124',
+ countryCode: '+91',
+ avatarUrl: 'https://i.pravatar.cc/150?u=kavitha',
+ role: 'User',
+ status: 'Active',
+ tier: 'Silver',
+ healthScore: 'warm',
+ isVerified: true,
+ is2FAEnabled: false,
+ totalSpent: 28000,
+ bookingsCount: 14,
+ refundRate: 7.1,
+ averageOrderValue: 2000,
+ language: 'en',
+ timezone: 'Asia/Kolkata',
+ currency: 'INR',
+ emailNotifications: true,
+ pushNotifications: true,
+ smsNotifications: false,
+ tags: [mockTags[7]], // Corporate
+ createdAt: '2024-08-20T12:00:00Z',
+ updatedAt: '2026-02-03T16:00:00Z',
+ lastLoginAt: '2026-02-05T11:00:00Z',
+ lastActivityAt: '2026-02-05T11:45:00Z',
+ lastDevice: { os: 'macOS 14', browser: 'Chrome', ip: '182.68.45.91', location: 'Chennai, IN' },
+ },
+];
+
+// ============================================
+// BOOKINGS
+// ============================================
+
+export const mockBookings: UserBooking[] = [
+ { id: 'book-001', userId: 'usr-001', eventId: 'evt-001', eventName: 'Tech Summit 2026', eventDate: '2026-03-15', ticketType: 'VIP Pass', quantity: 2, amount: 15000, status: 'Confirmed', createdAt: '2026-02-08T10:00:00Z' },
+ { id: 'book-002', userId: 'usr-001', eventId: 'evt-002', eventName: 'Mumbai Music Festival', eventDate: '2026-02-20', ticketType: 'General Admission', quantity: 4, amount: 8000, status: 'Attended', createdAt: '2026-01-15T14:00:00Z' },
+ { id: 'book-003', userId: 'usr-001', eventId: 'evt-003', eventName: 'Comedy Night Live', eventDate: '2026-01-28', ticketType: 'Premium', quantity: 2, amount: 5000, status: 'Attended', createdAt: '2026-01-10T09:00:00Z' },
+ { id: 'book-004', userId: 'usr-002', eventId: 'evt-001', eventName: 'Tech Summit 2026', eventDate: '2026-03-15', ticketType: 'Standard', quantity: 1, amount: 5000, status: 'Confirmed', createdAt: '2026-02-05T11:30:00Z' },
+ { id: 'book-005', userId: 'usr-002', eventId: 'evt-004', eventName: 'Startup Mixer', eventDate: '2026-01-20', ticketType: 'Entry', quantity: 1, amount: 1500, status: 'No-Show', createdAt: '2026-01-12T16:00:00Z' },
+ { id: 'book-006', userId: 'usr-007', eventId: 'evt-005', eventName: 'Fashion Week Preview', eventDate: '2026-02-25', ticketType: 'Front Row', quantity: 1, amount: 25000, status: 'Confirmed', createdAt: '2026-02-01T10:00:00Z' },
+];
+
+// ============================================
+// SESSIONS
+// ============================================
+
+export const mockSessions: UserSession[] = [
+ { id: 'sess-001', userId: 'usr-001', device: { os: 'iOS 17', browser: 'Safari', ip: '103.42.156.78', location: 'Mumbai, IN' }, isCurrentSession: true, createdAt: '2026-02-09T08:45:00Z', lastActiveAt: '2026-02-09T09:30:00Z' },
+ { id: 'sess-002', userId: 'usr-001', device: { os: 'macOS 14', browser: 'Chrome', ip: '103.42.156.80', location: 'Mumbai, IN' }, isCurrentSession: false, createdAt: '2026-02-07T10:00:00Z', lastActiveAt: '2026-02-07T18:00:00Z' },
+ { id: 'sess-003', userId: 'usr-001', device: { os: 'Windows 11', browser: 'Edge', ip: '14.139.42.15', location: 'Office - Delhi, IN' }, isCurrentSession: false, createdAt: '2026-02-05T09:00:00Z', lastActiveAt: '2026-02-05T17:30:00Z' },
+];
+
+// ============================================
+// SUSPICIOUS ACTIVITY
+// ============================================
+
+export const mockSuspiciousActivity: SuspiciousActivity[] = [
+ { id: 'susp-001', userId: 'usr-004', type: 'failed_login', description: '5 failed login attempts in 10 minutes', severity: 'high', timestamp: '2026-01-28T11:15:00Z', resolved: false },
+ { id: 'susp-002', userId: 'usr-005', type: 'geolocation_jump', description: 'Login from Mumbai then London within 1 hour', severity: 'high', timestamp: '2025-10-17T14:00:00Z', resolved: true },
+ { id: 'susp-003', userId: 'usr-002', type: 'multiple_devices', description: '3 new devices logged in within 24 hours', severity: 'medium', timestamp: '2026-02-06T08:00:00Z', resolved: false },
+];
+
+// ============================================
+// SUPPORT TICKETS
+// ============================================
+
+export const mockTickets: SupportTicket[] = [
+ {
+ id: 'tkt-001',
+ userId: 'usr-001',
+ subject: 'VIP Pass upgrade not reflecting',
+ type: 'Technical',
+ priority: 'High',
+ status: 'In Progress',
+ assignedTo: 'usr-014',
+ assignedToName: 'Amit Saxena',
+ messages: [
+ { id: 'msg-001', ticketId: 'tkt-001', authorId: 'usr-001', authorName: 'Priya Sharma', authorType: 'user', content: 'I upgraded to VIP but my app still shows General Admission.', createdAt: '2026-02-08T15:00:00Z' },
+ { id: 'msg-002', ticketId: 'tkt-001', authorId: 'usr-014', authorName: 'Amit Saxena', authorType: 'agent', content: 'Hi Priya, I can see the upgrade. Please try logging out and back in. It should reflect now.', createdAt: '2026-02-08T15:30:00Z' },
+ ],
+ createdAt: '2026-02-08T15:00:00Z',
+ updatedAt: '2026-02-08T15:30:00Z',
+ },
+ {
+ id: 'tkt-002',
+ userId: 'usr-002',
+ subject: 'Refund request for Startup Mixer',
+ type: 'Refund',
+ priority: 'Normal',
+ status: 'Waiting on User',
+ assignedTo: 'usr-014',
+ assignedToName: 'Amit Saxena',
+ messages: [
+ { id: 'msg-003', ticketId: 'tkt-002', authorId: 'usr-002', authorName: 'Rahul Verma', authorType: 'user', content: 'I couldn\'t attend due to work emergency. Can I get a refund?', createdAt: '2026-01-22T10:00:00Z' },
+ { id: 'msg-004', ticketId: 'tkt-002', authorId: 'usr-014', authorName: 'Amit Saxena', authorType: 'agent', content: 'We can offer a 50% refund or full credit for a future event. Which would you prefer?', createdAt: '2026-01-22T14:00:00Z' },
+ ],
+ createdAt: '2026-01-22T10:00:00Z',
+ updatedAt: '2026-01-22T14:00:00Z',
+ },
+];
+
+// ============================================
+// USER NOTES
+// ============================================
+
+export const mockNotes: UserNote[] = [
+ { id: 'note-001', userId: 'usr-001', authorId: 'usr-011', authorName: 'Aisha Khan', content: 'Top customer - Priority support always. Birthday: March 15.', isPinned: true, createdAt: '2023-06-10T10:00:00Z', updatedAt: '2024-01-15T14:00:00Z' },
+ { id: 'note-002', userId: 'usr-001', authorId: 'usr-014', authorName: 'Amit Saxena', content: 'Prefers WhatsApp for communication. Respond within 2 hours.', isPinned: false, createdAt: '2025-08-20T11:00:00Z', updatedAt: '2025-08-20T11:00:00Z' },
+ { id: 'note-003', userId: 'usr-007', authorId: 'usr-011', authorName: 'Aisha Khan', content: 'Instagram influencer 500K followers. Comp tickets for fashion events. @snehakapoor', isPinned: true, createdAt: '2024-02-14T09:00:00Z', updatedAt: '2024-02-14T09:00:00Z' },
+ { id: 'note-004', userId: 'usr-009', authorId: 'usr-011', authorName: 'Aisha Khan', content: 'JOURNALIST - Times of India. Handle with care, provide press passes. Article contact: editor@toi.com', isPinned: true, createdAt: '2024-03-01T10:00:00Z', updatedAt: '2024-03-01T10:00:00Z' },
+ { id: 'note-005', userId: 'usr-004', authorId: 'usr-011', authorName: 'Aisha Khan', content: 'Suspended for multiple chargebacks. 3 chargebacks in 2 months. Review before reinstatement. Evidence: #CHG-2026-001, #CHG-2026-002, #CHG-2026-003', isPinned: true, createdAt: '2026-02-01T09:30:00Z', updatedAt: '2026-02-01T09:30:00Z' },
+];
+
+// ============================================
+// EMAIL LOGS
+// ============================================
+
+export const mockEmailLogs: EmailLog[] = [
+ { id: 'email-001', userId: 'usr-001', templateName: 'Welcome Email', subject: 'Welcome to Eventify!', sentAt: '2023-01-15T10:35:00Z', status: 'Delivered' },
+ { id: 'email-002', userId: 'usr-001', templateName: 'Booking Confirmation', subject: 'Your tickets for Tech Summit 2026', sentAt: '2026-02-08T10:05:00Z', status: 'Delivered' },
+ { id: 'email-003', userId: 'usr-002', templateName: 'Booking Confirmation', subject: 'Your tickets for Tech Summit 2026', sentAt: '2026-02-05T11:35:00Z', status: 'Delivered' },
+ { id: 'email-004', userId: 'usr-003', templateName: 'Email Verification', subject: 'Verify your email address', sentAt: '2026-02-08T09:05:00Z', status: 'Sent' },
+ { id: 'email-005', userId: 'usr-004', templateName: 'Account Suspended', subject: 'Your account has been suspended', sentAt: '2026-02-01T09:35:00Z', status: 'Delivered' },
+];
+
+// ============================================
+// NOTIFICATIONS
+// ============================================
+
+export const mockNotifications: Notification[] = [
+ { id: 'notif-001', userId: 'usr-001', title: 'VIP Early Access', body: 'Get exclusive early bird tickets for Tech Summit 2026!', actionUrl: '/events/tech-summit-2026', priority: 'high', status: 'Clicked', sentAt: '2026-01-20T10:00:00Z', deliveredAt: '2026-01-20T10:00:30Z', clickedAt: '2026-01-20T10:05:00Z', createdAt: '2026-01-20T09:55:00Z', createdBy: 'usr-011' },
+ { id: 'notif-002', userId: 'usr-001', title: 'Event Reminder', body: 'Mumbai Music Festival starts tomorrow!', actionUrl: '/events/mumbai-music-festival', priority: 'normal', status: 'Delivered', sentAt: '2026-02-19T10:00:00Z', deliveredAt: '2026-02-19T10:00:15Z', createdAt: '2026-02-19T09:55:00Z', createdBy: 'system' },
+ { id: 'notif-003', userId: 'usr-007', title: 'Special Invite', body: 'You\'re invited to Fashion Week Preview - Front Row!', actionUrl: '/events/fashion-week-2026', priority: 'high', status: 'Clicked', sentAt: '2026-01-25T14:00:00Z', deliveredAt: '2026-01-25T14:00:20Z', clickedAt: '2026-01-25T14:10:00Z', createdAt: '2026-01-25T13:55:00Z', createdBy: 'usr-011' },
+];
+
+// ============================================
+// AUDIT LOGS
+// ============================================
+
+export const mockAuditLogs: AuditLog[] = [
+ { id: 'audit-001', actorId: 'usr-011', actorName: 'Aisha Khan', actorRole: 'Admin', action: 'user_created', targetUserId: 'usr-003', targetUserName: 'Ananya Patel', ipAddress: '14.98.172.55', timestamp: '2026-02-08T09:00:00Z' },
+ { id: 'audit-002', actorId: 'usr-011', actorName: 'Aisha Khan', actorRole: 'Admin', action: 'suspended', targetUserId: 'usr-004', targetUserName: 'Vikram Singh', changes: { before: { status: 'Active' }, after: { status: 'Suspended' } }, metadata: { reason: 'Payment Issue', note: 'Multiple chargebacks' }, ipAddress: '14.98.172.55', timestamp: '2026-02-01T09:30:00Z' },
+ { id: 'audit-003', actorId: 'usr-011', actorName: 'Aisha Khan', actorRole: 'Admin', action: 'banned', targetUserId: 'usr-005', targetUserName: 'Meera Reddy', changes: { before: { status: 'Suspended' }, after: { status: 'Banned' } }, metadata: { banTypes: ['ip_ban', 'email_blacklist'], reason: 'Fraud - stolen credit cards' }, ipAddress: '14.98.172.55', timestamp: '2025-10-20T14:00:00Z' },
+ { id: 'audit-004', actorId: 'usr-014', actorName: 'Amit Saxena', actorRole: 'Support Agent', action: 'note_added', targetUserId: 'usr-001', targetUserName: 'Priya Sharma', metadata: { noteId: 'note-002' }, ipAddress: '14.139.82.44', timestamp: '2025-08-20T11:00:00Z' },
+ { id: 'audit-005', actorId: 'usr-011', actorName: 'Aisha Khan', actorRole: 'Admin', action: 'tag_added', targetUserId: 'usr-007', targetUserName: 'Sneha Kapoor', changes: { before: { tags: ['VIP'] }, after: { tags: ['VIP', 'Influencer'] } }, ipAddress: '14.98.172.55', timestamp: '2024-02-14T09:00:00Z' },
+ { id: 'audit-006', actorId: 'usr-011', actorName: 'Aisha Khan', actorRole: 'Admin', action: 'notification_sent', targetUserId: 'usr-001', targetUserName: 'Priya Sharma', metadata: { notificationId: 'notif-001', title: 'VIP Early Access' }, ipAddress: '14.98.172.55', timestamp: '2026-01-20T09:55:00Z' },
+];
+
+// ============================================
+// TIMELINE EVENTS
+// ============================================
+
+export const mockTimeline: TimelineEvent[] = [
+ { id: 'tl-001', userId: 'usr-001', type: 'booking', title: 'Booked Tech Summit 2026', description: '2x VIP Pass - ₹15,000', timestamp: '2026-02-08T10:00:00Z' },
+ { id: 'tl-002', userId: 'usr-001', type: 'notification_sent', title: 'Push Notification Sent', description: 'VIP Early Access campaign', timestamp: '2026-01-20T10:00:00Z' },
+ { id: 'tl-003', userId: 'usr-001', type: 'booking', title: 'Attended Mumbai Music Festival', description: '4x General Admission - ₹8,000', timestamp: '2026-02-20T18:00:00Z' },
+ { id: 'tl-004', userId: 'usr-001', type: 'support_ticket', title: 'Support Ticket Created', description: 'VIP Pass upgrade not reflecting', timestamp: '2026-02-08T15:00:00Z' },
+ { id: 'tl-005', userId: 'usr-001', type: 'login', title: 'Logged In', description: 'iOS 17, Safari - Mumbai, IN', timestamp: '2026-02-09T08:45:00Z' },
+];
+
+// ============================================
+// SEGMENTS
+// ============================================
+
+export const mockSegments: UserSegment[] = [
+ { id: 'seg-001', name: 'VIP Customers', description: 'Users who spent more than ₹50,000', filters: { minSpent: 50000 }, userCount: 4, createdAt: '2024-01-01T10:00:00Z', createdBy: 'usr-011' },
+ { id: 'seg-002', name: 'At Risk', description: 'Haven\'t logged in for 90+ days', filters: { lastActiveBefore: '2025-11-09T00:00:00Z' }, userCount: 2, createdAt: '2024-06-15T14:00:00Z', createdBy: 'usr-011' },
+ { id: 'seg-003', name: 'Influencers', description: 'Users tagged as Influencer or Press', filters: { tags: ['tag-influencer', 'tag-press'] }, userCount: 2, createdAt: '2024-03-01T10:00:00Z', createdBy: 'usr-011' },
+ { id: 'seg-004', name: 'New This Week', description: 'Users who joined in the last 7 days', filters: { lastActiveAfter: '2026-02-02T00:00:00Z' }, userCount: 1, createdAt: '2026-02-01T10:00:00Z', createdBy: 'usr-011' },
+];
+
+// ============================================
+// NOTIFICATION TEMPLATES
+// ============================================
+
+export const mockNotificationTemplates: NotificationTemplate[] = [
+ { id: 'tpl-001', name: 'Birthday Greeting', title: '🎂 Happy Birthday!', body: 'Wishing you an amazing birthday! Here\'s a special 20% discount on your next booking.', actionUrl: '/profile/offers' },
+ { id: 'tpl-002', name: 'Event Reminder', title: '🎫 Event Tomorrow!', body: '{{event_name}} starts tomorrow. Don\'t forget your tickets!', actionUrl: '/my-tickets' },
+ { id: 'tpl-003', name: 'VIP Upgrade Offer', title: '⭐ Upgrade to VIP', body: 'Exclusive offer: Upgrade to VIP for just ₹2,000 more and get front-row access!', actionUrl: '/upgrade' },
+ { id: 'tpl-004', name: 'Win-Back Campaign', title: '👋 We Miss You!', body: 'It\'s been a while! Check out the amazing events happening near you.', actionUrl: '/events' },
+];
+
+// ============================================
+// METRICS
+// ============================================
+
+export const mockUserMetrics: UserMetrics = {
+ totalUsers: 15420,
+ totalUsersTrend: 5.2,
+ activeUsers: 8750,
+ activeUsersTrend: 3.8,
+ newToday: 47,
+ newTodayTrend: 12.5,
+ suspendedCount: 23,
+ bannedCount: 8,
+ averageLifetimeValue: 4250,
+ averageLifetimeValueTrend: 7.1,
+};
+
+// ============================================
+// HELPER FUNCTIONS
+// ============================================
+
+export function getUserById(id: string): User | undefined {
+ return mockCrmUsers.find(u => u.id === id);
+}
+
+export function getUserBookings(userId: string): UserBooking[] {
+ return mockBookings.filter(b => b.userId === userId);
+}
+
+export function getUserSessions(userId: string): UserSession[] {
+ return mockSessions.filter(s => s.userId === userId);
+}
+
+export function getUserTickets(userId: string): SupportTicket[] {
+ return mockTickets.filter(t => t.userId === userId);
+}
+
+export function getUserNotes(userId: string): UserNote[] {
+ return mockNotes.filter(n => n.userId === userId);
+}
+
+export function getUserEmailLogs(userId: string): EmailLog[] {
+ return mockEmailLogs.filter(e => e.userId === userId);
+}
+
+export function getUserNotifications(userId: string): Notification[] {
+ return mockNotifications.filter(n => n.userId === userId);
+}
+
+export function getUserAuditLogs(userId: string): AuditLog[] {
+ return mockAuditLogs.filter(a => a.targetUserId === userId);
+}
+
+export function getUserTimeline(userId: string): TimelineEvent[] {
+ return mockTimeline.filter(t => t.userId === userId);
+}
+
+export function getUserSuspiciousActivity(userId: string): SuspiciousActivity[] {
+ return mockSuspiciousActivity.filter(a => a.userId === userId);
+}
diff --git a/src/lib/actions/user-management.ts b/src/lib/actions/user-management.ts
new file mode 100644
index 0000000..9ecb8f8
--- /dev/null
+++ b/src/lib/actions/user-management.ts
@@ -0,0 +1,146 @@
+'use server';
+
+import { z } from 'zod';
+import { logAdminAction } from '@/lib/audit-logger';
+
+// --- Validation Schemas ---
+
+const suspendSchema = z.object({
+ userId: z.string(),
+ reason: z.string().min(10, "Reason must be at least 10 characters"),
+ duration: z.enum(['24h', '7d', '30d', 'indefinite']),
+});
+
+const banSchema = z.object({
+ userId: z.string(),
+ reason: z.string().min(10),
+});
+
+const ticketSchema = z.object({
+ userId: z.string(),
+ summary: z.string().min(5),
+});
+
+// --- Authorization Helper ---
+
+async function verifyAdmin() {
+ // TODO: Integrate with Auth.js session check
+ // const session = await auth();
+ // if (session?.user?.role !== 'admin') throw new Error("Unauthorized");
+
+ // Mock successful admin check
+ return { id: 'admin-1', role: 'admin' };
+}
+
+// --- Server Actions ---
+
+export async function resetPassword(userId: string) {
+ try {
+ const admin = await verifyAdmin();
+
+ // Simulate DB call
+ await new Promise(resolve => setTimeout(resolve, 800));
+
+ await logAdminAction({
+ actorId: admin.id,
+ action: 'RESET_PASSWORD',
+ targetId: userId,
+ });
+
+ return { success: true, message: "Recovery email sent to user." };
+ } catch (error) {
+ return { success: false, message: "Failed to reset password." };
+ }
+}
+
+export async function impersonateUser(userId: string) {
+ try {
+ const admin = await verifyAdmin();
+
+ await logAdminAction({
+ actorId: admin.id,
+ action: 'IMPERSONATE_USER',
+ targetId: userId,
+ });
+
+ // In a real app, you'd set a cookie here
+ // cookies().set('impersonate_id', userId);
+
+ return { success: true, redirectUrl: '/dashboard' };
+ } catch (error) {
+ return { success: false, message: "Failed to start impersonation session." };
+ }
+}
+
+export async function terminateSessions(userId: string) {
+ try {
+ const admin = await verifyAdmin();
+
+ // Simulate DB call to delete sessions
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ await logAdminAction({
+ actorId: admin.id,
+ action: 'TERMINATE_SESSIONS',
+ targetId: userId,
+ });
+
+ return { success: true, message: "All active sessions terminated." };
+ } catch (error) {
+ return { success: false, message: "Failed to terminate sessions." };
+ }
+}
+
+export async function toggleSuspension(formData: FormData) {
+ try {
+ const admin = await verifyAdmin();
+
+ const rawData = {
+ userId: formData.get('userId'),
+ reason: formData.get('reason'),
+ duration: formData.get('duration'),
+ };
+
+ const validated = suspendSchema.safeParse(rawData);
+ if (!validated.success) {
+ return { success: false, message: validated.error.errors[0].message };
+ }
+
+ // Simulate DB update
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ await logAdminAction({
+ actorId: admin.id,
+ action: 'SUSPEND_USER',
+ targetId: validated.data.userId,
+ details: { reason: validated.data.reason, duration: validated.data.duration },
+ });
+
+ return { success: true, message: `User suspended for ${validated.data.duration}.` };
+ } catch (error) {
+ return { success: false, message: "Failed to suspend user." };
+ }
+}
+
+export async function createSupportTicket(userId: string, summary: string) {
+ try {
+ const admin = await verifyAdmin();
+
+ const validated = ticketSchema.safeParse({ userId, summary });
+ if (!validated.success) return { success: false, message: "Invalid input" };
+
+ // Simulate DB insert
+ await new Promise(resolve => setTimeout(resolve, 600));
+
+ await logAdminAction({
+ actorId: admin.id,
+ action: 'CREATE_TICKET',
+ targetId: userId,
+ details: { summary },
+ });
+
+ return { success: true, message: "Support ticket #T-1234 created." };
+ } catch (error) {
+ return { success: false, message: "Failed to create ticket." };
+ }
+}
diff --git a/src/lib/audit-logger.ts b/src/lib/audit-logger.ts
new file mode 100644
index 0000000..7b09fca
--- /dev/null
+++ b/src/lib/audit-logger.ts
@@ -0,0 +1,26 @@
+export interface AuditLogEntry {
+ actorId: string;
+ action: string;
+ targetId: string;
+ details?: Record;
+ timestamp: Date;
+}
+
+/**
+ * Logs an administrative action to the audit/console.
+ * In a real app, this would write to a database table or external logging service.
+ */
+export async function logAdminAction(entry: Omit) {
+ const logEntry: AuditLogEntry = {
+ ...entry,
+ timestamp: new Date(),
+ };
+
+ // Simulate database latency
+ // await new Promise(resolve => setTimeout(resolve, 100));
+
+ console.log(`[AUDIT] Admin ${entry.actorId} performed ${entry.action} on ${entry.targetId}`, entry.details || '');
+
+ // TODO: Connect to real audit log database
+ return { success: true };
+}
diff --git a/src/lib/types/user.ts b/src/lib/types/user.ts
new file mode 100644
index 0000000..850683c
--- /dev/null
+++ b/src/lib/types/user.ts
@@ -0,0 +1,365 @@
+// User Command Center - Complete Type Definitions
+// All types for CRM, moderation, notifications, and support
+
+// ============================================
+// CORE USER ENTITY
+// ============================================
+
+export type UserStatus = 'Active' | 'Pending Verification' | 'Suspended' | 'Banned' | 'Archived';
+export type UserRole = 'Super Admin' | 'Admin' | 'Support Agent' | 'Partner' | 'User';
+export type UserTier = 'Platinum' | 'Gold' | 'Silver' | 'Bronze';
+export type UserHealthScore = 'hot' | 'warm' | 'cold';
+
+export interface User {
+ id: string;
+ name: string;
+ email: string;
+ phone: string;
+ countryCode: string;
+ avatarUrl?: string;
+
+ // Role & Status
+ role: UserRole;
+ status: UserStatus;
+ tier: UserTier;
+ healthScore: UserHealthScore;
+ isVerified: boolean;
+ is2FAEnabled: boolean;
+
+ // Metrics
+ totalSpent: number;
+ bookingsCount: number;
+ refundRate: number;
+ averageOrderValue: number;
+
+ // Preferences
+ language: string;
+ timezone: string;
+ currency: string;
+
+ // Notification Preferences
+ emailNotifications: boolean;
+ pushNotifications: boolean;
+ smsNotifications: boolean;
+
+ // Tags & Notes
+ tags: UserTag[];
+ pinnedNote?: string;
+
+ // Timestamps
+ createdAt: string;
+ updatedAt: string;
+ lastLoginAt: string;
+ lastActivityAt: string;
+
+ // Device Info (from last login)
+ lastDevice?: DeviceInfo;
+}
+
+export interface DeviceInfo {
+ os: string;
+ browser: string;
+ ip: string;
+ location: string;
+}
+
+// ============================================
+// TAGS & SEGMENTATION
+// ============================================
+
+export interface UserTag {
+ id: string;
+ name: string;
+ color: string; // Tailwind color class
+}
+
+export interface UserSegment {
+ id: string;
+ name: string;
+ description?: string;
+ filters: SegmentFilters;
+ userCount: number;
+ createdAt: string;
+ createdBy: string;
+}
+
+export interface SegmentFilters {
+ search?: string;
+ status?: UserStatus[];
+ role?: UserRole[];
+ tier?: UserTier[];
+ tags?: string[];
+ minSpent?: number;
+ maxSpent?: number;
+ minBookings?: number;
+ maxBookings?: number;
+ lastActiveAfter?: string;
+ lastActiveBefore?: string;
+}
+
+// ============================================
+// BOOKINGS & TRANSACTIONS
+// ============================================
+
+export type BookingStatus = 'Confirmed' | 'Attended' | 'No-Show' | 'Cancelled' | 'Refunded';
+
+export interface UserBooking {
+ id: string;
+ userId: string;
+ eventId: string;
+ eventName: string;
+ eventDate: string;
+ ticketType: string;
+ quantity: number;
+ amount: number;
+ status: BookingStatus;
+ createdAt: string;
+}
+
+// ============================================
+// SECURITY & SESSIONS
+// ============================================
+
+export interface UserSession {
+ id: string;
+ userId: string;
+ device: DeviceInfo;
+ isCurrentSession: boolean;
+ createdAt: string;
+ lastActiveAt: string;
+}
+
+export interface SuspiciousActivity {
+ id: string;
+ userId: string;
+ type: 'failed_login' | 'geolocation_jump' | 'multiple_devices' | 'password_change';
+ description: string;
+ severity: 'low' | 'medium' | 'high';
+ timestamp: string;
+ resolved: boolean;
+}
+
+// ============================================
+// SUPPORT & COMMUNICATION
+// ============================================
+
+export type TicketStatus = 'Open' | 'In Progress' | 'Waiting on User' | 'Resolved' | 'Closed';
+export type TicketPriority = 'Low' | 'Normal' | 'High' | 'Urgent';
+export type TicketType = 'Refund' | 'Technical' | 'Inquiry' | 'Complaint' | 'Feedback';
+
+export interface SupportTicket {
+ id: string;
+ userId: string;
+ subject: string;
+ type: TicketType;
+ priority: TicketPriority;
+ status: TicketStatus;
+ assignedTo?: string;
+ assignedToName?: string;
+ messages: TicketMessage[];
+ createdAt: string;
+ updatedAt: string;
+ resolvedAt?: string;
+}
+
+export interface TicketMessage {
+ id: string;
+ ticketId: string;
+ authorId: string;
+ authorName: string;
+ authorType: 'user' | 'agent';
+ content: string;
+ createdAt: string;
+}
+
+export interface UserNote {
+ id: string;
+ userId: string;
+ authorId: string;
+ authorName: string;
+ content: string;
+ isPinned: boolean;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface EmailLog {
+ id: string;
+ userId: string;
+ templateName: string;
+ subject: string;
+ sentAt: string;
+ status: 'Sent' | 'Delivered' | 'Bounced' | 'Failed';
+}
+
+// ============================================
+// NOTIFICATIONS
+// ============================================
+
+export type NotificationPriority = 'low' | 'normal' | 'high';
+export type NotificationStatus = 'Pending' | 'Sent' | 'Delivered' | 'Failed' | 'Clicked';
+
+export interface Notification {
+ id: string;
+ userId: string;
+ title: string;
+ body: string;
+ actionUrl?: string;
+ priority: NotificationPriority;
+ status: NotificationStatus;
+ scheduledFor?: string;
+ sentAt?: string;
+ deliveredAt?: string;
+ clickedAt?: string;
+ createdAt: string;
+ createdBy: string;
+}
+
+export interface NotificationTemplate {
+ id: string;
+ name: string;
+ title: string;
+ body: string;
+ actionUrl?: string;
+}
+
+// ============================================
+// AUDIT LOGGING
+// ============================================
+
+export type AuditAction =
+ | 'user_created'
+ | 'user_updated'
+ | 'user_deleted'
+ | 'user_archived'
+ | 'status_changed'
+ | 'role_changed'
+ | 'tag_added'
+ | 'tag_removed'
+ | 'note_added'
+ | 'note_updated'
+ | 'note_deleted'
+ | 'password_reset'
+ | '2fa_enabled'
+ | '2fa_disabled'
+ | '2fa_reset'
+ | 'session_terminated'
+ | 'suspended'
+ | 'suspension_lifted'
+ | 'banned'
+ | 'notification_sent'
+ | 'ticket_created'
+ | 'ticket_assigned'
+ | 'ticket_resolved'
+ | 'data_exported'
+ | 'data_anonymized';
+
+export interface AuditLog {
+ id: string;
+ actorId: string;
+ actorName: string;
+ actorRole: UserRole;
+ action: AuditAction;
+ targetUserId: string;
+ targetUserName: string;
+ changes?: {
+ before: Record;
+ after: Record;
+ };
+ metadata?: Record;
+ ipAddress: string;
+ timestamp: string;
+}
+
+// ============================================
+// MODERATION
+// ============================================
+
+export type SuspensionReason = 'Payment Issue' | 'Policy Violation' | 'Spam' | 'Abuse' | 'Other';
+export type SuspensionDuration = '7_days' | '30_days' | '90_days' | 'permanent';
+export type BanType = 'ip_ban' | 'device_ban' | 'email_blacklist';
+
+export interface Suspension {
+ userId: string;
+ reason: SuspensionReason;
+ customNote?: string;
+ duration: SuspensionDuration;
+ sendEmail: boolean;
+ suspendedAt: string;
+ suspendedBy: string;
+ expiresAt?: string;
+ liftedAt?: string;
+ liftedBy?: string;
+ liftNote?: string;
+}
+
+export interface Ban {
+ userId: string;
+ banTypes: BanType[];
+ publicReason: string;
+ internalEvidence?: string;
+ bannedAt: string;
+ bannedBy: string;
+}
+
+// ============================================
+// METRICS & ANALYTICS
+// ============================================
+
+export interface UserMetrics {
+ totalUsers: number;
+ totalUsersTrend: number; // percentage change
+ activeUsers: number; // logged in last 30 days
+ activeUsersTrend: number;
+ newToday: number;
+ newTodayTrend: number;
+ suspendedCount: number;
+ bannedCount: number;
+ averageLifetimeValue: number;
+ averageLifetimeValueTrend: number;
+}
+
+// ============================================
+// BULK OPERATIONS
+// ============================================
+
+export type BulkAction =
+ | 'change_status'
+ | 'assign_role'
+ | 'add_tag'
+ | 'remove_tag'
+ | 'send_notification'
+ | 'export_csv'
+ | 'delete';
+
+export interface BulkActionResult {
+ action: BulkAction;
+ totalSelected: number;
+ successCount: number;
+ failureCount: number;
+ errors?: { userId: string; error: string }[];
+}
+
+// ============================================
+// INTERACTION TIMELINE
+// ============================================
+
+export type TimelineEventType =
+ | 'booking'
+ | 'login'
+ | 'support_ticket'
+ | 'email_sent'
+ | 'note_added'
+ | 'status_changed'
+ | 'notification_sent'
+ | 'payment';
+
+export interface TimelineEvent {
+ id: string;
+ userId: string;
+ type: TimelineEventType;
+ title: string;
+ description: string;
+ metadata?: Record;
+ timestamp: string;
+}
diff --git a/src/lib/validations/user.ts b/src/lib/validations/user.ts
new file mode 100644
index 0000000..4a223cb
--- /dev/null
+++ b/src/lib/validations/user.ts
@@ -0,0 +1,274 @@
+// User Command Center - Zod Validation Schemas
+import { z } from 'zod';
+
+// ============================================
+// ENUMS AS ZOD SCHEMAS
+// ============================================
+
+export const userStatusSchema = z.enum([
+ 'Active',
+ 'Pending Verification',
+ 'Suspended',
+ 'Banned',
+ 'Archived'
+]);
+
+export const userRoleSchema = z.enum([
+ 'Super Admin',
+ 'Admin',
+ 'Support Agent',
+ 'Partner',
+ 'User'
+]);
+
+export const userTierSchema = z.enum(['Platinum', 'Gold', 'Silver', 'Bronze']);
+
+export const ticketTypeSchema = z.enum([
+ 'Refund',
+ 'Technical',
+ 'Inquiry',
+ 'Complaint',
+ 'Feedback'
+]);
+
+export const ticketPrioritySchema = z.enum(['Low', 'Normal', 'High', 'Urgent']);
+
+export const notificationPrioritySchema = z.enum(['low', 'normal', 'high']);
+
+export const suspensionReasonSchema = z.enum([
+ 'Payment Issue',
+ 'Policy Violation',
+ 'Spam',
+ 'Abuse',
+ 'Other'
+]);
+
+export const suspensionDurationSchema = z.enum([
+ '7_days',
+ '30_days',
+ '90_days',
+ 'permanent'
+]);
+
+export const banTypeSchema = z.enum(['ip_ban', 'device_ban', 'email_blacklist']);
+
+// ============================================
+// CREATE USER SCHEMA
+// ============================================
+
+export const createUserSchema = z.object({
+ // Basic Info
+ name: z
+ .string()
+ .min(2, 'Name must be at least 2 characters')
+ .max(100, 'Name must be less than 100 characters'),
+ email: z
+ .string()
+ .email('Invalid email address')
+ .min(1, 'Email is required'),
+ phone: z
+ .string()
+ .min(10, 'Phone number must be at least 10 digits')
+ .max(15, 'Phone number must be less than 15 digits')
+ .regex(/^[0-9+\-\s()]+$/, 'Invalid phone number format'),
+ countryCode: z.string().min(1, 'Country code is required').default('+91'),
+
+ // Security
+ password: z
+ .string()
+ .min(8, 'Password must be at least 8 characters')
+ .regex(
+ /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
+ 'Password must contain uppercase, lowercase, and number'
+ )
+ .optional(),
+ role: userRoleSchema.default('User'),
+
+ // Preferences
+ language: z.string().default('en'),
+ timezone: z.string().default('Asia/Kolkata'),
+ currency: z.string().default('INR'),
+
+ // Tags (array of tag IDs)
+ tagIds: z.array(z.string()).optional().default([]),
+});
+
+export type CreateUserInput = z.infer;
+
+// ============================================
+// UPDATE USER SCHEMA
+// ============================================
+
+export const updateUserSchema = z.object({
+ name: z
+ .string()
+ .min(2, 'Name must be at least 2 characters')
+ .max(100, 'Name must be less than 100 characters')
+ .optional(),
+ phone: z
+ .string()
+ .min(10, 'Phone number must be at least 10 digits')
+ .max(15, 'Phone number must be less than 15 digits')
+ .regex(/^[0-9+\-\s()]+$/, 'Invalid phone number format')
+ .optional(),
+ countryCode: z.string().optional(),
+ avatarUrl: z.string().url('Invalid URL').optional().nullable(),
+
+ // Role & Status (requires elevated permissions)
+ role: userRoleSchema.optional(),
+ status: userStatusSchema.optional(),
+ tier: userTierSchema.optional(),
+
+ // Security
+ is2FAEnabled: z.boolean().optional(),
+
+ // Preferences
+ language: z.string().optional(),
+ timezone: z.string().optional(),
+ currency: z.string().optional(),
+
+ // Notification Preferences
+ emailNotifications: z.boolean().optional(),
+ pushNotifications: z.boolean().optional(),
+ smsNotifications: z.boolean().optional(),
+});
+
+export type UpdateUserInput = z.infer;
+
+// ============================================
+// SUSPEND USER SCHEMA
+// ============================================
+
+export const suspendUserSchema = z.object({
+ reason: suspensionReasonSchema,
+ customNote: z
+ .string()
+ .max(500, 'Note must be less than 500 characters')
+ .optional(),
+ duration: suspensionDurationSchema,
+ sendEmail: z.boolean().default(true),
+});
+
+export type SuspendUserInput = z.infer;
+
+// ============================================
+// BAN USER SCHEMA
+// ============================================
+
+export const banUserSchema = z.object({
+ banTypes: z
+ .array(banTypeSchema)
+ .min(1, 'Select at least one ban type'),
+ publicReason: z
+ .string()
+ .min(10, 'Reason must be at least 10 characters')
+ .max(200, 'Reason must be less than 200 characters'),
+ internalEvidence: z.string().optional(),
+});
+
+export type BanUserInput = z.infer;
+
+// ============================================
+// NOTIFICATION SCHEMA
+// ============================================
+
+export const notificationSchema = z.object({
+ title: z
+ .string()
+ .min(3, 'Title must be at least 3 characters')
+ .max(100, 'Title must be less than 100 characters'),
+ body: z
+ .string()
+ .min(10, 'Body must be at least 10 characters')
+ .max(500, 'Body must be less than 500 characters'),
+ actionUrl: z
+ .string()
+ .url('Invalid URL')
+ .optional()
+ .or(z.literal('')),
+ priority: notificationPrioritySchema.default('normal'),
+ scheduleFor: z.string().datetime().optional(),
+});
+
+export type NotificationInput = z.infer;
+
+// ============================================
+// SUPPORT TICKET SCHEMA
+// ============================================
+
+export const createTicketSchema = z.object({
+ subject: z
+ .string()
+ .min(5, 'Subject must be at least 5 characters')
+ .max(200, 'Subject must be less than 200 characters'),
+ type: ticketTypeSchema,
+ priority: ticketPrioritySchema.default('Normal'),
+ initialMessage: z
+ .string()
+ .min(10, 'Message must be at least 10 characters')
+ .max(2000, 'Message must be less than 2000 characters')
+ .optional(),
+});
+
+export type CreateTicketInput = z.infer;
+
+// ============================================
+// USER NOTE SCHEMA
+// ============================================
+
+export const userNoteSchema = z.object({
+ content: z
+ .string()
+ .min(1, 'Note cannot be empty')
+ .max(2000, 'Note must be less than 2000 characters'),
+ isPinned: z.boolean().default(false),
+});
+
+export type UserNoteInput = z.infer;
+
+// ============================================
+// FILTER SCHEMA
+// ============================================
+
+export const userFiltersSchema = z.object({
+ search: z.string().optional(),
+ status: z.array(userStatusSchema).optional(),
+ role: z.array(userRoleSchema).optional(),
+ tier: z.array(userTierSchema).optional(),
+ tags: z.array(z.string()).optional(),
+ minSpent: z.number().min(0).optional(),
+ maxSpent: z.number().min(0).optional(),
+ minBookings: z.number().min(0).optional(),
+ maxBookings: z.number().min(0).optional(),
+ lastActiveAfter: z.string().optional(),
+ lastActiveBefore: z.string().optional(),
+});
+
+export type UserFiltersInput = z.infer;
+
+// ============================================
+// SEGMENT SCHEMA
+// ============================================
+
+export const createSegmentSchema = z.object({
+ name: z
+ .string()
+ .min(3, 'Name must be at least 3 characters')
+ .max(50, 'Name must be less than 50 characters'),
+ description: z.string().max(200).optional(),
+ filters: userFiltersSchema,
+});
+
+export type CreateSegmentInput = z.infer;
+
+// ============================================
+// DELETE CONFIRMATION SCHEMA
+// ============================================
+
+export const deleteConfirmSchema = z.object({
+ confirmText: z.literal('DELETE', {
+ errorMap: () => ({ message: 'Type DELETE to confirm' }),
+ }),
+});
+
+export type DeleteConfirmInput = z.infer;
diff --git a/src/pages/Users.tsx b/src/pages/Users.tsx
index fbf8a97..4b1237f 100644
--- a/src/pages/Users.tsx
+++ b/src/pages/Users.tsx
@@ -1,86 +1,436 @@
+// Users.tsx - User Command Center - Complete rewrite
+import { useState, useMemo } from 'react';
+import { useQueryState, parseAsString, parseAsInteger, parseAsArrayOf } from 'nuqs';
+import { AppLayout } from '@/components/layout/AppLayout';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Badge } from '@/components/ui/badge';
+import { TooltipProvider } from '@/components/ui/tooltip';
+import {
+ Search,
+ Plus,
+ LayoutGrid,
+ LayoutList,
+ RefreshCw,
+ Download,
+ Filter,
+} from 'lucide-react';
-import {
- Shield,
- Users,
- BriefcaseBusiness,
- Settings2,
- ListFilter
-} from "lucide-react";
-import { AppLayout } from "@/components/layout/AppLayout";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { InternalTeamTab } from "@/features/users/components/InternalTeamTab";
-import { PartnerAdminTab } from "@/features/users/components/PartnerAdminTab";
-import { UserBaseTab } from "@/features/users/components/UserBaseTab";
-import { RoleMatrix } from "@/features/users/components/RoleMatrix";
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetHeader,
- SheetTitle,
- SheetTrigger
-} from "@/components/ui/sheet";
-import { Button } from "@/components/ui/button";
+// Components
+import { UsersTable } from '@/features/users/components/UsersTable';
+import { UserFilters } from '@/features/users/components/UserFilters';
+import { UserMetricsBar } from '@/features/users/components/UserMetricsBar';
+import { UserInspectorSheet } from '@/features/users/components/UserInspectorSheet';
+import { CreateUserDialog } from '@/features/users/components/CreateUserDialog';
+import { SuspensionModal } from '@/features/users/components/SuspensionModal';
+import { BanModal } from '@/features/users/components/BanModal';
+import { DeleteConfirmDialog } from '@/features/users/components/DeleteConfirmDialog';
+import { NotificationComposer } from '@/features/users/components/NotificationComposer';
+import { BulkActionsDropdown } from '@/features/users/components/BulkActionsDropdown';
+
+// Data & Types
+import { mockCrmUsers, mockUserMetrics } from '@/features/users/data/mockUserCrmData';
+import type { User, SegmentFilters } from '@/lib/types/user';
+import { toast } from 'sonner';
+import { cn } from '@/lib/utils';
+
+type ViewMode = 'table' | 'grid';
export default function Users() {
+ // URL State with nuqs
+ const [searchQuery, setSearchQuery] = useQueryState('q', parseAsString.withDefault(''));
+ const [minSpent, setMinSpent] = useQueryState('minSpent', parseAsInteger);
+ const [maxSpent, setMaxSpent] = useQueryState('maxSpent', parseAsInteger);
+
+ // Local State
+ const [viewMode, setViewMode] = useState('table');
+ const [filtersOpen, setFiltersOpen] = useState(false);
+ const [filters, setFilters] = useState({});
+ const [selectedUserIds, setSelectedUserIds] = useState([]);
+
+ // Modal States
+ const [selectedUser, setSelectedUser] = useState(null);
+ const [inspectorOpen, setInspectorOpen] = useState(false);
+ const [createDialogOpen, setCreateDialogOpen] = useState(false);
+ const [suspensionModalOpen, setSuspensionModalOpen] = useState(false);
+ const [banModalOpen, setBanModalOpen] = useState(false);
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [notificationComposerOpen, setNotificationComposerOpen] = useState(false);
+ const [notificationTargetUsers, setNotificationTargetUsers] = useState([]);
+
+ // Sync URL params to filters
+ const mergedFilters: SegmentFilters = useMemo(() => ({
+ ...filters,
+ search: searchQuery || filters.search,
+ minSpent: minSpent ?? filters.minSpent,
+ maxSpent: maxSpent ?? filters.maxSpent,
+ }), [filters, searchQuery, minSpent, maxSpent]);
+
+ // Active filters count
+ const activeFiltersCount = useMemo(() => {
+ let count = 0;
+ if (mergedFilters.search) count++;
+ if (mergedFilters.status?.length) count++;
+ if (mergedFilters.role?.length) count++;
+ if (mergedFilters.tier?.length) count++;
+ if (mergedFilters.tags?.length) count++;
+ if (mergedFilters.minSpent) count++;
+ if (mergedFilters.maxSpent) count++;
+ return count;
+ }, [mergedFilters]);
+
+ // Filter users
+ const filteredUsers = useMemo(() => {
+ return mockCrmUsers.filter((user) => {
+ // Search filter
+ if (mergedFilters.search) {
+ const query = mergedFilters.search.toLowerCase();
+ if (
+ !user.name.toLowerCase().includes(query) &&
+ !user.email.toLowerCase().includes(query) &&
+ !user.phone.includes(query) &&
+ !user.id.toLowerCase().includes(query)
+ ) {
+ return false;
+ }
+ }
+
+ // Status filter
+ if (mergedFilters.status?.length && !mergedFilters.status.includes(user.status)) {
+ return false;
+ }
+
+ // Role filter
+ if (mergedFilters.role?.length && !mergedFilters.role.includes(user.role)) {
+ return false;
+ }
+
+ // Tier filter
+ if (mergedFilters.tier?.length && !mergedFilters.tier.includes(user.tier)) {
+ return false;
+ }
+
+ // Spent range filter
+ if (mergedFilters.minSpent && user.totalSpent < mergedFilters.minSpent) {
+ return false;
+ }
+ if (mergedFilters.maxSpent && user.totalSpent > mergedFilters.maxSpent) {
+ return false;
+ }
+
+ // Tags filter
+ if (mergedFilters.tags?.length) {
+ const userTagIds = user.tags.map((t) => t.id);
+ if (!mergedFilters.tags.some((tagId) => userTagIds.includes(tagId))) {
+ return false;
+ }
+ }
+
+ return true;
+ });
+ }, [mergedFilters]);
+
+ // Selected users array
+ const selectedUsers = useMemo(
+ () => mockCrmUsers.filter((u) => selectedUserIds.includes(u.id)),
+ [selectedUserIds]
+ );
+
+ // Handlers
+ const handleSelectUser = (user: User) => {
+ setSelectedUser(user);
+ setInspectorOpen(true);
+ };
+
+ const handleEditUser = (user: User) => {
+ setSelectedUser(user);
+ toast.info(`Edit user: ${user.name}`);
+ // In real app, this would open an edit modal
+ };
+
+ const handleSuspendUser = (user: User) => {
+ setSelectedUser(user);
+ setSuspensionModalOpen(true);
+ };
+
+ const handleBanUser = (user: User) => {
+ setSelectedUser(user);
+ setBanModalOpen(true);
+ };
+
+ const handleDeleteUser = (user: User) => {
+ setSelectedUser(user);
+ setDeleteDialogOpen(true);
+ };
+
+ const handleSendNotification = (user: User) => {
+ setNotificationTargetUsers([user]);
+ setNotificationComposerOpen(true);
+ };
+
+ const handleBulkNotification = (users: User[]) => {
+ setNotificationTargetUsers(users);
+ setNotificationComposerOpen(true);
+ };
+
+ const handleRefresh = () => {
+ toast.success('Data refreshed');
+ };
+
+ const handleExport = () => {
+ toast.success(`Exported ${filteredUsers.length} users to CSV`);
+ };
+
+ const handleFiltersChange = (newFilters: SegmentFilters) => {
+ setFilters(newFilters);
+ // Sync to URL
+ if (newFilters.search !== mergedFilters.search) {
+ setSearchQuery(newFilters.search || null);
+ }
+ if (newFilters.minSpent !== mergedFilters.minSpent) {
+ setMinSpent(newFilters.minSpent || null);
+ }
+ if (newFilters.maxSpent !== mergedFilters.maxSpent) {
+ setMaxSpent(newFilters.maxSpent || null);
+ }
+ };
+
return (
-
-
+
+
+
+ {/* Metrics Bar */}
+
-
-
+ {/* Toolbar */}
+
+ {/* Left: Search */}
+
+
+
+ setSearchQuery(e.target.value || null)}
+ className="pl-9 bg-white/50 border-white/50"
+ />
+
+
+
- {/* Main Navigation Tabs */}
-
-
-
- Internal Team
-
-
-
- Partners
-
-
-
- User Base
-
-
+ {/* Right: Actions */}
+
+ {/* Bulk Actions (only when selected) */}
+
setSelectedUserIds([])}
+ onSendNotification={handleBulkNotification}
+ onSuspendUsers={(users) => {
+ if (users.length === 1) {
+ handleSuspendUser(users[0]);
+ } else {
+ toast.info(`Suspend ${users.length} users`);
+ }
+ }}
+ onBanUsers={(users) => {
+ if (users.length === 1) {
+ handleBanUser(users[0]);
+ } else {
+ toast.info(`Ban ${users.length} users`);
+ }
+ }}
+ />
- {/* Quick Actions (Role Manager) */}
-
-
-
-
-
-
-
-
+ {/* Refresh */}
+
+
+
-
-
-
+ {/* Export */}
+
+
+
-
-
-
+ {/* Create User */}
+
setCreateDialogOpen(true)} className="gap-2">
+ Add User
+
-
-
-
+
+ {/* Content Area */}
+
+ {/* Filter Sidebar (collapsible) */}
+ {filtersOpen && (
+
setFiltersOpen(false)}
+ />
+ )}
+
+ {/* Main Content */}
+
+ {/* Results Count */}
+
+
+ Showing {filteredUsers.length} of{' '}
+ {mockCrmUsers.length} users
+ {selectedUserIds.length > 0 && (
+
+ ({selectedUserIds.length} selected)
+
+ )}
+
+
+
+ {/* Table View */}
+ {viewMode === 'table' && (
+
+ )}
+
+ {/* Grid View */}
+ {viewMode === 'grid' && (
+
+ {filteredUsers.map((user) => (
+
handleSelectUser(user)}
+ className="group relative flex flex-col gap-4 rounded-xl border border-white/40 bg-white/30 p-5 shadow-neu-sm hover:shadow-neu transition-all duration-300 cursor-pointer backdrop-blur-md overflow-hidden"
+ >
+
+

+
+
+ {user.name}
+
+
{user.email}
+
+
+
+
+ {user.status}
+
+
+ ₹{(user.totalSpent / 1000).toFixed(0)}K
+
+
+
+ {user.tags.slice(0, 2).map((tag) => (
+
+ {tag.name}
+
+ ))}
+ {user.tags.length > 2 && (
+
+ +{user.tags.length - 2}
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+
+
+ {/* Modals & Sheets */}
+
+
+
+
+
+
+
+
+
+
+ setNotificationTargetUsers([])}
+ />
+
+
);
}