ifixkart-admin/app/(admin)/users/page.tsx

1542 lines
68 KiB
TypeScript

'use client';
import React, { useEffect, useState, useMemo, useRef } from 'react';
import {
Users, Plus, Search, Trash2, Edit, Eye, Loader2, X, EyeOff, CheckCircle2,
AlertTriangle, UserCheck, ChevronDown, Settings,
Filter, FileSpreadsheet, Download, RefreshCw, CheckSquare, Square,
Mail, Phone
} from 'lucide-react';
import { userService, UserResponse, UserCreate, UserUpdate } from '@/services/api/userService';
import { rolePermissionService } from '@/services/api/rolePermissionService';
import { format } from 'date-fns';
import { toast } from 'sonner';
import { SlideOver } from '@/components/ui/SlideOver';
import { ViewModeToggle } from '@/components/ui/ViewModeToggle';
import { StatsSparklineCard, datesToSparkline, weekOverWeekChange } from '@/components/ui/StatsSparklineCard';
import { CustomSelect } from '@/components/ui/CustomSelect';
import { TablePagination, useClientPagination } from '@/components/ui/TablePagination';
import { PhoneInput } from '@/components/ui/PhoneInput';
import { validatePhone, validateEmail } from '@/lib/validation';
// Master data lookup mapped to seeded backend database IDs
const ROLES = [
{ id: '01KXRF5VQ1GQ58RDRX45BY9X1D', name: 'Super Admin' },
{ id: '01KXRF5VQ2X83NF2950KTMZT45', name: 'Admin' },
{ id: '01KXRF5VQ2X83NF2950KTMZT46', name: 'Technician' },
{ id: '01KXRF5VQ3QPM1CJZ679R152CQ', name: 'Sales' },
{ id: '01KXRF5VQ4Z7YXKKX83VSR532Q', name: 'Customer Support' },
];
const DEPARTMENTS = [
{ id: '01KXRF5VQ5NB1GN907TGZZ3C9Q', name: 'Administration' },
{ id: '01KXRF5VQ6D0NJC1RT6422QT0Y', name: 'Technical Repair' },
{ id: '01KXRF5VQ6D0NJC1RT6422QT0Z', name: 'Sales & Billing' },
{ id: '01KXRF5VQ74CRWS1951WETKC38', name: 'Support Desk' },
];
const DESIGNATIONS = [
{ id: '01KXRF5VQ8GXRR9JEMNW6VS215', name: 'General Administrator' },
{ id: '01KXRF5VQ92Q2DA0VED445M6PJ', name: 'Senior Technician' },
{ id: '01KXRF5VQ92Q2DA0VED445M6PK', name: 'Junior Technician' },
{ id: '01KXRF5VQ92Q2DA0VED445M6PM', name: 'Billing Representative' },
];
const GENDERS = ['Male', 'Female', 'Other', 'Prefer not to say'];
// Highlight match function
const highlightMatch = (text: string, query: string) => {
if (!query) return text;
const parts = text.split(new RegExp(`(${query})`, 'gi'));
return (
<>
{parts.map((part, index) =>
part.toLowerCase() === query.toLowerCase() ? (
<mark key={index} className="bg-yellow-100 text-yellow-900 rounded-sm px-0.5 font-semibold">{part}</mark>
) : (
part
)
)}
</>
);
};
export default function UsersPage() {
const [users, setUsers] = useState<UserResponse[]>([]);
const [loading, setLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
// Search and Sort
const [search, setSearch] = useState('');
const [sortOption, setSortOption] = useState('recently_added'); // ascending, descending, recently_added
// Filter States
const [showFilters, setShowFilters] = useState(false);
const [filterRole, setFilterRole] = useState<string>('');
const [filterDept, setFilterDept] = useState<string>('');
const [filterStatus, setFilterStatus] = useState<string>('all'); // all, active, inactive
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [viewMode, setViewMode] = useState<'table' | 'grid'>('table');
const [draftRole, setDraftRole] = useState('');
const [draftDept, setDraftDept] = useState('');
const [draftStatus, setDraftStatus] = useState('all');
const [draftStartDate, setDraftStartDate] = useState('');
const [draftEndDate, setDraftEndDate] = useState('');
const filterRef = useRef<HTMLDivElement>(null);
// Row Selection
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [mockSubscriptions, setMockSubscriptions] = useState([
{ id: 'SUB-RAZOR-9082', name: 'Premium ERP Support Plan' },
{ id: 'SUB-RAZOR-1249', name: 'Standard Technical SLA Plan' },
{ id: 'SUB-RAZOR-8821', name: 'Basic Helpdesk SLA Plan' }
]);
const [selectedSubId, setSelectedSubId] = useState('SUB-RAZOR-9082');
// Column Visibility
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>({
index: true,
employee: true,
roleDept: true,
emailPhone: true,
designation: true,
status: true,
verified: true,
createdAt: true,
actions: true,
});
// Modals state
const [showAddModal, setShowAddModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showViewModal, setShowViewModal] = useState(false);
const [deleteId, setDeleteId] = useState<string | null>(null);
const [showBulkDeleteModal, setShowBulkDeleteModal] = useState(false);
const [isSoftDelete, setIsSoftDelete] = useState(true);
// Selected user for View or Edit
const [selectedUser, setSelectedUser] = useState<UserResponse | null>(null);
// Form states
const [addForm, setAddForm] = useState<UserCreate>({
first_name: '',
last_name: '',
display_name: '',
email: '',
phone: '',
password: '',
gender: 'Prefer not to say',
dob: '',
role_id: ROLES[1].id,
department_id: DEPARTMENTS[0].id,
designation_id: DESIGNATIONS[0].id,
manager_id: '',
});
const [editForm, setEditForm] = useState<UserUpdate & { password?: string }>({
first_name: '',
last_name: '',
display_name: '',
email: '',
phone: '',
password: '',
gender: 'Prefer not to say',
dob: '',
role_id: '',
department_id: '',
designation_id: '',
manager_id: '',
});
const [showPassword, setShowPassword] = useState(false);
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
const [availableRoles, setAvailableRoles] = useState<{ id: string; name: string }[]>(ROLES);
const fetchUsers = async () => {
setLoading(true);
try {
const data = await userService.getAll(0, 100);
setUsers(data);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to fetch users';
toast.error(message);
}
try {
const dynRoles = await rolePermissionService.getRoles();
if (dynRoles && dynRoles.length > 0) {
setAvailableRoles(dynRoles.map(r => ({ id: r.role_id, name: r.role_name })));
}
} catch {}
setLoading(false);
};
useEffect(() => {
fetchUsers();
}, []);
useEffect(() => {
if (!showFilters) return;
const onPointerDown = (event: MouseEvent) => {
if (!filterRef.current?.contains(event.target as Node)) {
setShowFilters(false);
}
};
document.addEventListener('mousedown', onPointerDown);
return () => document.removeEventListener('mousedown', onPointerDown);
}, [showFilters]);
const getRoleName = (id: string) => availableRoles.find(r => r.id === id || r.name.toLowerCase() === id.toLowerCase())?.name || id || 'Unknown';
const getDeptName = (id: string) => DEPARTMENTS.find(d => d.id === id)?.name || 'Unknown';
const getDesigName = (id: string) => DESIGNATIONS.find(d => d.id === id)?.name || 'Unknown';
// Validation Form Checks
const validateForm = (data: Partial<UserCreate>, isEdit = false): boolean => {
const errors: Record<string, string> = {};
if (!data.first_name || data.first_name.trim().length < 2) {
errors.first_name = 'First name must be at least 2 characters';
}
const emailErr = validateEmail(data.email || '', true);
if (emailErr) errors.email = emailErr;
const phoneErr = validatePhone(data.phone || '', true);
if (phoneErr) errors.phone = phoneErr;
if (!isEdit) {
const password = data.password || '';
if (password.length < 6) {
errors.password = 'Password must be at least 6 characters';
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(password)) {
errors.password = 'Password must contain uppercase, lowercase, and a number';
}
}
if (!data.role_id) errors.role_id = 'Role is required';
if (!data.department_id) errors.department_id = 'Department is required';
if (!data.designation_id) errors.designation_id = 'Designation is required';
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
// Submit Handlers
const handleAddSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm(addForm)) return;
try {
setIsSubmitting(true);
const payload = { ...addForm };
if (!payload.display_name) payload.display_name = `${payload.first_name}`;
payload.last_name = payload.display_name; // Mapped to backend User Name requirements
if (!payload.dob) delete payload.dob;
if (!payload.manager_id) delete payload.manager_id;
await userService.create(payload);
toast.success(payload.display_name ? `User "${payload.display_name}" created` : 'User created');
setShowAddModal(false);
// Reset form
setAddForm({
first_name: '',
last_name: '',
display_name: '',
email: '',
phone: '',
password: '',
gender: 'Prefer not to say',
dob: '',
role_id: ROLES[1].id,
department_id: DEPARTMENTS[0].id,
designation_id: DESIGNATIONS[0].id,
manager_id: '',
});
fetchUsers();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to create user';
toast.error(message);
} finally {
setIsSubmitting(false);
}
};
const handleEditClick = (user: UserResponse) => {
setSelectedUser(user);
setEditForm({
first_name: user.first_name,
last_name: user.last_name,
display_name: user.display_name || '',
email: user.email,
phone: user.phone,
password: '',
gender: user.gender || 'Prefer not to say',
dob: user.dob || '',
role_id: user.role_id,
department_id: user.department_id,
designation_id: user.designation_id,
manager_id: user.manager_id || '',
});
setFormErrors({});
setShowEditModal(true);
};
const handleEditSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedUser) return;
if (!validateForm(editForm, true)) return;
try {
setIsSubmitting(true);
const payload: UserUpdate = {
first_name: editForm.first_name,
last_name: editForm.first_name, // Sync display/last name requirements
display_name: editForm.display_name || undefined,
email: editForm.email,
phone: editForm.phone,
gender: editForm.gender,
dob: editForm.dob || undefined,
role_id: editForm.role_id,
department_id: editForm.department_id,
designation_id: editForm.designation_id,
manager_id: editForm.manager_id || undefined,
};
if (editForm.password && editForm.password.trim().length > 0) {
payload.password = editForm.password;
}
await userService.update(selectedUser.user_id, payload);
toast.success('User details saved');
setShowEditModal(false);
fetchUsers();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to update user';
toast.error(message);
} finally {
setIsSubmitting(false);
}
};
const handleViewClick = (user: UserResponse) => {
setSelectedUser(user);
setShowViewModal(true);
};
const handleDelete = async (userId: string) => {
try {
await userService.delete(userId);
toast.success('User moved to deleted');
setDeleteId(null);
fetchUsers();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to delete user';
toast.error(message);
}
};
// Bulk deletion
const handleBulkDelete = async () => {
if (selectedIds.length === 0) return;
try {
setLoading(true);
await Promise.all(selectedIds.map(id => userService.delete(id)));
toast.success(`Deleted ${selectedIds.length} ${selectedIds.length === 1 ? 'user' : 'users'}`);
setSelectedIds([]);
setShowBulkDeleteModal(false);
fetchUsers();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to delete selected users';
toast.error(message);
setLoading(false);
}
};
// Bulk assign mock subscription
const handleBulkAssignSubscription = () => {
if (selectedIds.length === 0) return;
const subName = mockSubscriptions.find(s => s.id === selectedSubId)?.name;
toast.success(`Assigned ${selectedIds.length} ${selectedIds.length === 1 ? 'user' : 'users'} to ${subName || 'subscription'}`);
setSelectedIds([]);
};
// Bulk remove mock subscription
const handleBulkRemoveSubscription = () => {
if (selectedIds.length === 0) return;
const subName = mockSubscriptions.find(s => s.id === selectedSubId)?.name || 'subscription';
toast.success(`Removed ${subName} from ${selectedIds.length} ${selectedIds.length === 1 ? 'user' : 'users'}`);
setSelectedIds([]);
};
// Toggle selection
const handleSelectRow = (id: string) => {
setSelectedIds(prev =>
prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]
);
};
const handleSelectAll = (filteredList: UserResponse[]) => {
const filteredUserIds = filteredList.map(u => u.user_id);
const allSelected = filteredUserIds.every(id => selectedIds.includes(id));
if (allSelected) {
setSelectedIds(prev => prev.filter(id => !filteredUserIds.includes(id)));
} else {
setSelectedIds(prev => Array.from(new Set([...prev, ...filteredUserIds])));
}
};
// Client-side Export Methods
const handleExportCSV = (selectedOnly = false) => {
const listToExport = selectedOnly
? users.filter(u => selectedIds.includes(u.user_id))
: users;
if (listToExport.length === 0) {
toast.warning(selectedOnly ? 'No selected users to export' : 'No users to export');
return;
}
const headers = ['S.No', 'Employee Code', 'First Name', 'Last Name', 'Email', 'Phone', 'Role', 'Department', 'Designation', 'Status', 'Verified', 'Created At'];
const rows = listToExport.map((u, i) => [
i + 1,
u.employee_code || '—',
u.first_name,
u.last_name,
u.email,
u.phone,
getRoleName(u.role_id),
getDeptName(u.department_id),
getDesigName(u.designation_id),
u.is_active ? 'Active' : 'Inactive',
`Email:${u.email_verified ? 'Yes' : 'No'} Phone:${u.phone_verified ? 'Yes' : 'No'}`,
format(new Date(u.created_at), 'yyyy-MM-dd')
]);
const csvContent = [headers.join(','), ...rows.map(e => e.map(val => `"${val}"`).join(','))].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', `ifixkart_users_${selectedOnly ? 'selected_' : 'all_'}${format(new Date(), 'yyyyMMdd')}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.success(`Downloaded ${listToExport.length} ${listToExport.length === 1 ? 'user' : 'users'} as CSV`);
};
const handleExportPDF = (selectedOnly = false) => {
const listToExport = selectedOnly
? users.filter(u => selectedIds.includes(u.user_id))
: users;
if (listToExport.length === 0) {
toast.warning(selectedOnly ? 'No selected users to export' : 'No users to export');
return;
}
const printWindow = window.open('', '_blank');
if (!printWindow) {
toast.error('Allow pop-ups to open the users print preview');
return;
}
const html = `
<html>
<head>
<title>iFixKart Staff Directory PDF Export</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; padding: 20px; color: #1e293b; }
h2 { color: #0f172a; border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 11px; }
th { background-color: #0f172a; color: white; padding: 10px; text-align: left; }
td { padding: 10px; border-bottom: 1px solid #e2e8f0; }
tr:nth-child(even) { background-color: #f8fafc; }
</style>
</head>
<body>
<h2>iFixKart Staff Directory (${listToExport.length} Records)</h2>
<p>Exported on: ${format(new Date(), 'yyyy-MM-dd HH:mm:ss')}</p>
<table>
<thead>
<tr>
<th>S.No</th>
<th>Employee Code</th>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Role & Dept</th>
<th>Designation</th>
<th>Status</th>
</tr>
</thead>
<tbody>
${listToExport.map((u, i) => `
<tr>
<td>${i + 1}</td>
<td><b>${u.employee_code || '—'}</b></td>
<td>${u.first_name} ${u.last_name}</td>
<td>${u.email}</td>
<td>${u.phone}</td>
<td>${getRoleName(u.role_id)} (${getDeptName(u.department_id)})</td>
<td>${getDesigName(u.designation_id)}</td>
<td>${u.is_active ? 'Active' : 'Inactive'}</td>
</tr>
`).join('')}
</tbody>
</table>
<script>
window.onload = function() { window.print(); }
</script>
</body>
</html>
`;
printWindow.document.write(html);
printWindow.document.close();
toast.success(`Print preview opened for ${listToExport.length} ${listToExport.length === 1 ? 'user' : 'users'}`);
};
// Filter & Search Logic
const filteredUsers = useMemo(() => {
return users.filter((u) => {
// Search
const q = search.toLowerCase();
const searchMatch = !search || (
u.first_name.toLowerCase().includes(q) ||
u.last_name.toLowerCase().includes(q) ||
`${u.first_name} ${u.last_name}`.toLowerCase().includes(q) ||
(u.display_name || '').toLowerCase().includes(q) ||
u.email.toLowerCase().includes(q) ||
u.phone.toLowerCase().includes(q) ||
(u.employee_code || '').toLowerCase().includes(q) ||
getRoleName(u.role_id).toLowerCase().includes(q) ||
getDeptName(u.department_id).toLowerCase().includes(q)
);
// Dropdown/Accordion Filters
const roleMatch = !filterRole || u.role_id === filterRole;
const deptMatch = !filterDept || u.department_id === filterDept;
const statusMatch = filterStatus === 'all' ||
(filterStatus === 'active' && u.is_active) ||
(filterStatus === 'inactive' && !u.is_active);
// Date Range Match
let dateMatch = true;
if (startDate || endDate) {
const createDate = new Date(u.created_at);
if (startDate) {
dateMatch = dateMatch && createDate >= new Date(startDate);
}
if (endDate) {
dateMatch = dateMatch && createDate <= new Date(endDate);
}
}
return searchMatch && roleMatch && deptMatch && statusMatch && dateMatch;
}).sort((a, b) => {
if (sortOption === 'ascending') {
return a.first_name.localeCompare(b.first_name);
}
if (sortOption === 'descending') {
return b.first_name.localeCompare(a.first_name);
}
// Default: recently added
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});
}, [users, search, sortOption, filterRole, filterDept, filterStatus, startDate, endDate]);
const pager = useClientPagination(filteredUsers);
// Status Metrics
const totalCount = users.length;
const activeCount = users.filter(u => u.is_active).length;
const inactiveCount = users.filter(u => !u.is_active).length;
const filtersActive = Boolean(filterRole || filterDept || filterStatus !== 'all' || startDate || endDate);
const allUserDates = useMemo(() => users.map((u) => u.created_at), [users]);
const activeUserDates = useMemo(
() => users.filter((u) => u.is_active).map((u) => u.created_at),
[users]
);
const inactiveUserDates = useMemo(
() => users.filter((u) => !u.is_active).map((u) => u.created_at),
[users]
);
const dataCardShell =
'crm-radius-section border border-border bg-card shadow-[0_1px_3px_rgba(16,24,40,0.06)] overflow-hidden min-w-0';
const inputClass =
'w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary';
const labelClass = 'block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5';
const primaryButton =
'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[13px] font-semibold cursor-pointer disabled:opacity-50';
const secondaryButton =
'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control border border-border bg-card text-foreground text-[13px] font-medium hover:bg-muted cursor-pointer disabled:opacity-50';
const renderStatusBadge = (active: boolean) => (
<span
className={`inline-flex items-center px-2.5 py-1 crm-radius-badge text-[11px] font-semibold ${
active ? 'bg-success text-white' : 'bg-destructive text-white'
}`}
>
{active ? 'Active' : 'Inactive'}
</span>
);
return (
<div className="flex flex-col gap-5 min-w-0">
<div className="flex flex-wrap items-start justify-between gap-3 min-w-0">
<div className="min-w-0">
<div className="flex items-center gap-2">
<h1 className="text-lg font-semibold text-foreground">Manage Users</h1>
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 crm-radius-toggle bg-primary/10 text-primary text-[11px] font-semibold leading-none">
{totalCount}
</span>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<div className="relative group">
<button type="button" className={secondaryButton}>
<Download className="w-3.5 h-3.5" />
Export
<ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />
</button>
<div className="absolute right-0 top-full mt-1 z-50 w-52 crm-radius-section border border-border bg-card shadow-[0_8px_24px_rgba(16,24,40,0.12)] hidden group-hover:block p-1.5 overflow-hidden">
<div className="px-3 py-2 text-[11px] text-muted-foreground font-semibold">All records</div>
<button type="button" onClick={() => handleExportCSV(false)} className="w-full text-left px-3 py-2 crm-radius-section text-[13px] text-[#6b7280] hover:bg-[#eef0f4] hover:text-[#3d4654] cursor-pointer flex items-center gap-1.5">
<FileSpreadsheet className="w-4 h-4" /> Excel (.csv)
</button>
<button type="button" onClick={() => handleExportPDF(false)} className="w-full text-left px-3 py-2 crm-radius-section text-[13px] text-[#6b7280] hover:bg-[#eef0f4] hover:text-[#3d4654] cursor-pointer flex items-center gap-1.5">
<FileSpreadsheet className="w-4 h-4" /> Printable PDF
</button>
{selectedIds.length > 0 && (
<>
<div className="px-3 py-2 text-[11px] text-muted-foreground font-semibold">Selected ({selectedIds.length})</div>
<button type="button" onClick={() => handleExportCSV(true)} className="w-full text-left px-3 py-2 crm-radius-section text-[13px] text-[#6b7280] hover:bg-[#eef0f4] hover:text-[#3d4654] cursor-pointer flex items-center gap-1.5">
<FileSpreadsheet className="w-4 h-4" /> Selected CSV
</button>
<button type="button" onClick={() => handleExportPDF(true)} className="w-full text-left px-3 py-2 crm-radius-section text-[13px] text-[#6b7280] hover:bg-[#eef0f4] hover:text-[#3d4654] cursor-pointer flex items-center gap-1.5">
<FileSpreadsheet className="w-4 h-4" /> Selected PDF
</button>
</>
)}
</div>
</div>
<button
type="button"
onClick={fetchUsers}
className="w-8 h-8 crm-radius-control border border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer transition-colors flex items-center justify-center"
title="Reload"
>
<RefreshCw className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => {
setFormErrors({});
setShowAddModal(true);
}}
className={primaryButton}
>
<span className="w-[18px] h-[18px] rounded-full bg-white text-primary flex items-center justify-center">
<Plus className="w-3 h-3" strokeWidth={3} />
</span>
Add user
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<StatsSparklineCard
title="Total users"
value={totalCount}
icon={Users}
tone="green"
series={datesToSparkline(allUserDates)}
deltaPercent={weekOverWeekChange(allUserDates)}
active={filterStatus === 'all'}
onClick={() => setFilterStatus('all')}
/>
<StatsSparklineCard
title="Active users"
value={activeCount}
icon={UserCheck}
tone="violet"
series={datesToSparkline(activeUserDates)}
deltaPercent={weekOverWeekChange(activeUserDates)}
active={filterStatus === 'active'}
onClick={() => setFilterStatus('active')}
/>
<StatsSparklineCard
title="Inactive users"
value={inactiveCount}
icon={AlertTriangle}
tone="orange"
series={datesToSparkline(inactiveUserDates)}
deltaPercent={weekOverWeekChange(inactiveUserDates)}
active={filterStatus === 'inactive'}
onClick={() => setFilterStatus('inactive')}
/>
</div>
{selectedIds.length > 0 && (
<div className={`${dataCardShell} p-3.5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3`}>
<div className="flex items-center gap-2 text-[13px] text-foreground font-semibold">
<CheckCircle2 className="w-4 h-4 shrink-0 text-primary" />
<span>{selectedIds.length} selected</span>
</div>
<div className="flex flex-wrap items-center gap-2 w-full sm:w-auto">
<CustomSelect
value={selectedSubId}
onChange={setSelectedSubId}
aria-label="Subscription plan"
className="min-w-[220px]"
options={mockSubscriptions.map((sub) => ({ value: sub.id, label: sub.name }))}
/>
<button type="button" onClick={handleBulkAssignSubscription} className={primaryButton}>
Assign
</button>
<button type="button" onClick={handleBulkRemoveSubscription} className={secondaryButton}>
Remove license
</button>
<button
type="button"
onClick={() => setShowBulkDeleteModal(true)}
className="inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control bg-destructive hover:bg-destructive/90 text-white text-[13px] font-semibold cursor-pointer"
>
<Trash2 className="w-3.5 h-3.5" />
Delete selected
</button>
</div>
</div>
)}
<div className={dataCardShell}>
<div className="p-4 space-y-3">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2.5 min-w-0">
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 min-w-0">
<div className="relative" ref={filterRef}>
<button
type="button"
onClick={() => {
setDraftRole(filterRole);
setDraftDept(filterDept);
setDraftStatus(filterStatus);
setDraftStartDate(startDate);
setDraftEndDate(endDate);
setShowFilters((open) => !open);
}}
className={`inline-flex items-center gap-1.5 h-9 px-3 crm-radius-control border bg-card text-[13px] font-medium cursor-pointer transition-colors ${
filtersActive || showFilters
? 'border-primary text-primary bg-primary/5'
: 'border-border text-foreground hover:bg-muted'
}`}
>
<Filter className="w-3.5 h-3.5" />
Filter
{filtersActive && <span className="w-1.5 h-1.5 rounded-full bg-primary" />}
<ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />
</button>
{showFilters && (
<div className="absolute left-0 top-full mt-1.5 z-30 w-[260px] crm-radius-section border border-border bg-card shadow-lg p-3 space-y-3">
<div>
<label className={labelClass}>Role</label>
<CustomSelect
value={draftRole}
onChange={setDraftRole}
aria-label="Role"
options={[
{ value: '', label: 'All Roles' },
...availableRoles.map((r) => ({ value: r.id, label: r.name })),
]}
/>
</div>
<div>
<label className={labelClass}>Department</label>
<CustomSelect
value={draftDept}
onChange={setDraftDept}
aria-label="Department"
options={[
{ value: '', label: 'All Departments' },
...DEPARTMENTS.map((d) => ({ value: d.id, label: d.name })),
]}
/>
</div>
<div>
<label className={labelClass}>Status</label>
<CustomSelect
value={draftStatus}
onChange={setDraftStatus}
aria-label="Status"
options={[
{ value: 'all', label: 'All Statuses' },
{ value: 'active', label: 'Active only' },
{ value: 'inactive', label: 'Inactive only' },
]}
/>
</div>
<div>
<label className={labelClass}>Created</label>
<div className="flex gap-2">
<input
type="date"
value={draftStartDate}
onChange={(e) => setDraftStartDate(e.target.value)}
className="w-1/2 h-9 px-2 crm-radius-control border border-border bg-card text-[12px] text-foreground"
/>
<input
type="date"
value={draftEndDate}
onChange={(e) => setDraftEndDate(e.target.value)}
className="w-1/2 h-9 px-2 crm-radius-control border border-border bg-card text-[12px] text-foreground"
/>
</div>
</div>
<div className="flex items-center justify-end gap-2 pt-1">
<button
type="button"
onClick={() => {
setDraftRole('');
setDraftDept('');
setDraftStatus('all');
setDraftStartDate('');
setDraftEndDate('');
setFilterRole('');
setFilterDept('');
setFilterStatus('all');
setStartDate('');
setEndDate('');
setShowFilters(false);
}}
className={secondaryButton}
>
Clear
</button>
<button
type="button"
onClick={() => {
setFilterRole(draftRole);
setFilterDept(draftDept);
setFilterStatus(draftStatus);
setStartDate(draftStartDate);
setEndDate(draftEndDate);
setShowFilters(false);
}}
className={primaryButton}
>
Apply
</button>
</div>
</div>
)}
</div>
<div className="relative w-full sm:w-[240px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground pointer-events-none" />
<input
type="text"
placeholder="Search users"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full h-9 pl-8 pr-3 crm-radius-control border border-border bg-card text-[13px] text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors"
/>
</div>
</div>
<div className="flex flex-wrap items-center gap-2 shrink-0">
<CustomSelect
value={sortOption}
onChange={setSortOption}
aria-label="Sort users"
className="w-[160px]"
options={[
{ value: 'ascending', label: 'Sort A-Z' },
{ value: 'descending', label: 'Sort Z-A' },
{ value: 'recently_added', label: 'Recently added' },
]}
/>
<ViewModeToggle value={viewMode} onChange={setViewMode} />
<div className="relative group">
<button type="button" className={secondaryButton}>
<Settings className="w-3.5 h-3.5" />
Columns
<ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />
</button>
<div className="absolute right-0 top-full mt-1.5 w-48 crm-radius-section border border-border bg-card shadow-lg hidden group-hover:block z-20 p-2 space-y-1">
{Object.keys(visibleColumns).map(colKey => (
<label key={colKey} className="flex items-center gap-2 px-2 py-1 hover:bg-muted crm-radius-control text-[13px] text-foreground cursor-pointer">
<input
type="checkbox"
checked={visibleColumns[colKey]}
onChange={() => setVisibleColumns(prev => ({ ...prev, [colKey]: !prev[colKey] }))}
className="rounded-sm border-border text-primary focus:ring-primary w-3.5 h-3.5"
/>
<span className="capitalize">{colKey.replace(/([A-Z])/g, ' $1')}</span>
</label>
))}
</div>
</div>
</div>
</div>
</div>
{viewMode === 'grid' ? (
loading ? (
<div className="py-12 flex justify-center border-t border-border">
<Loader2 className="w-6 h-6 animate-spin text-primary" />
</div>
) : filteredUsers.length === 0 ? (
<div className="py-12 text-center text-[13px] text-muted-foreground border-t border-border">
{search ? 'No users match your search query' : 'No users created yet'}
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3 p-4 border-t border-border">
{pager.items.map((user) => (
<div key={user.user_id} className="crm-radius-section border border-border bg-card p-3.5 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2.5 min-w-0">
<div className="w-9 h-9 crm-radius-icon flex items-center justify-center text-[12px] font-bold text-primary shrink-0 bg-primary/10">
{user.first_name.charAt(0).toUpperCase()}{user.last_name.charAt(0).toUpperCase()}
</div>
<div className="min-w-0">
<p className="text-[13px] font-semibold text-foreground truncate">
{user.first_name} {user.last_name}
</p>
<p className="text-[11px] text-muted-foreground truncate">{getRoleName(user.role_id)}</p>
</div>
</div>
{renderStatusBadge(user.is_active)}
</div>
<div className="mt-3 space-y-1.5 text-[12px] text-muted-foreground">
<p className="flex items-center gap-1.5 truncate"><Mail className="w-3.5 h-3.5 shrink-0" />{user.email}</p>
<p className="flex items-center gap-1.5 truncate"><Phone className="w-3.5 h-3.5 shrink-0" />{user.phone || '—'}</p>
<p className="truncate">{getDeptName(user.department_id)} · {getDesigName(user.designation_id)}</p>
</div>
<div className="mt-3 flex items-center justify-end gap-1">
<button type="button" onClick={() => handleViewClick(user)} className="w-8 h-8 crm-radius-control hover:bg-muted cursor-pointer flex items-center justify-center" title="View details">
<Eye className="w-3.5 h-3.5 text-muted-foreground" />
</button>
<button type="button" onClick={() => handleEditClick(user)} className="w-8 h-8 crm-radius-control hover:bg-muted cursor-pointer flex items-center justify-center" title="Edit user">
<Edit className="w-3.5 h-3.5 text-muted-foreground" />
</button>
<button type="button" onClick={() => setDeleteId(user.user_id)} className="w-8 h-8 crm-radius-control hover:bg-destructive/10 cursor-pointer flex items-center justify-center" title="Delete user">
<Trash2 className="w-3.5 h-3.5 text-destructive" />
</button>
</div>
</div>
))}
</div>
)
) : (
<div className="overflow-x-auto border-t border-border">
<table className="crm-data-table min-w-[980px]">
<thead>
<tr className="border-b border-border bg-gray-50">
<th className="px-5 py-3 w-10 text-center">
<button
type="button"
onClick={() => handleSelectAll(filteredUsers)}
className="text-muted-foreground hover:text-foreground cursor-pointer inline-flex items-center"
>
{filteredUsers.length > 0 && filteredUsers.every(u => selectedIds.includes(u.user_id)) ? (
<CheckSquare className="w-4 h-4 text-primary" />
) : (
<Square className="w-4 h-4" />
)}
</button>
</th>
{visibleColumns.index && <th className="px-5 py-3 text-[13px] font-semibold text-gray-700">S.No</th>}
{visibleColumns.employee && <th className="px-5 py-3 text-[13px] font-semibold text-gray-700">Employee</th>}
{visibleColumns.roleDept && <th className="px-5 py-3 text-[13px] font-semibold text-gray-700">Role &amp; dept</th>}
{visibleColumns.emailPhone && <th className="px-5 py-3 text-[13px] font-semibold text-gray-700">Email &amp; phone</th>}
{visibleColumns.designation && <th className="px-5 py-3 text-[13px] font-semibold text-gray-700">Designation</th>}
{visibleColumns.status && <th className="px-5 py-3 text-[13px] font-semibold text-gray-700">Status</th>}
{visibleColumns.verified && <th className="px-5 py-3 text-[13px] font-semibold text-gray-700">Verified</th>}
{visibleColumns.createdAt && <th className="px-5 py-3 text-[13px] font-semibold text-gray-700">Created</th>}
{visibleColumns.actions && <th className="px-5 py-3 text-[13px] font-semibold text-gray-700 text-right">Actions</th>}
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={10} className="text-center py-12">
<Loader2 className="w-6 h-6 animate-spin text-primary mx-auto" />
<p className="text-[13px] text-muted-foreground mt-2">Loading users...</p>
</td>
</tr>
) : filteredUsers.length === 0 ? (
<tr>
<td colSpan={10} className="text-center py-12">
<Users className="w-8 h-8 text-muted-foreground/30 mx-auto" />
<p className="text-[13px] text-muted-foreground mt-2">
{search ? 'No users match your search query' : 'No users created yet'}
</p>
</td>
</tr>
) : (
pager.items.map((user, index) => {
const isSelected = selectedIds.includes(user.user_id);
return (
<tr
key={user.user_id}
className={`border-b border-border hover:bg-muted/10 transition-colors ${ isSelected ? 'bg-primary/5' : '' }`}
>
<td className="px-5 py-3.5 text-center">
<button
type="button"
onClick={() => handleSelectRow(user.user_id)}
className="text-muted-foreground hover:text-foreground cursor-pointer inline-flex items-center"
>
{isSelected ? (
<CheckSquare className="w-4 h-4 text-primary" />
) : (
<Square className="w-4 h-4" />
)}
</button>
</td>
{visibleColumns.index && (
<td className="px-5 py-3.5 text-[13px] text-muted-foreground">
{(pager.page - 1) * pager.pageSize + index + 1}
</td>
)}
{visibleColumns.employee && (
<td className="px-5 py-3.5">
<div className="flex items-center gap-3">
<div className="w-9 h-9 crm-radius-icon flex items-center justify-center text-xs font-bold text-primary shrink-0 bg-primary/10">
{user.first_name.charAt(0).toUpperCase()}{user.last_name.charAt(0).toUpperCase()}
</div>
<div>
<p className="text-[13px] font-semibold text-foreground">
{highlightMatch(`${user.first_name} ${user.last_name}`, search)}
</p>
<p className="text-[11px] text-muted-foreground font-mono">{user.employee_code || '—'}</p>
</div>
</div>
</td>
)}
{visibleColumns.roleDept && (
<td className="px-5 py-3.5">
<span className="text-[13px] font-semibold text-foreground block">
{getRoleName(user.role_id)}
</span>
<span className="text-[11px] text-muted-foreground block">
{getDeptName(user.department_id)}
</span>
</td>
)}
{visibleColumns.emailPhone && (
<td className="px-5 py-3.5">
<span className="text-[13px] text-foreground block font-medium">
{highlightMatch(user.email, search)}
</span>
<span className="text-[11px] text-muted-foreground block font-mono">
{highlightMatch(user.phone, search)}
</span>
</td>
)}
{visibleColumns.designation && (
<td className="px-5 py-3.5 text-[13px] text-muted-foreground">
{getDesigName(user.designation_id)}
</td>
)}
{visibleColumns.status && (
<td className="px-5 py-3.5">
{renderStatusBadge(user.is_active)}
</td>
)}
{visibleColumns.verified && (
<td className="px-5 py-3.5">
<div className="flex flex-col gap-1">
<span className={`inline-flex items-center px-2 py-0.5 crm-radius-badge text-[11px] font-semibold w-max ${ user.email_verified ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground' }`}>
Email: {user.email_verified ? 'Verified' : 'Pending'}
</span>
<span className={`inline-flex items-center px-2 py-0.5 crm-radius-badge text-[11px] font-semibold w-max ${ user.phone_verified ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground' }`}>
Phone: {user.phone_verified ? 'Verified' : 'Pending'}
</span>
</div>
</td>
)}
{visibleColumns.createdAt && (
<td className="px-5 py-3.5 text-[13px] text-muted-foreground">
{format(new Date(user.created_at), 'yyyy-MM-dd')}
</td>
)}
{visibleColumns.actions && (
<td className="px-5 py-3.5 text-right">
<div className="flex gap-1 justify-end">
<button
type="button"
onClick={() => handleViewClick(user)}
className="w-8 h-8 crm-radius-control flex items-center justify-center hover:bg-muted cursor-pointer transition-colors"
title="View details"
>
<Eye className="w-4 h-4 text-muted-foreground" />
</button>
<button
type="button"
onClick={() => handleEditClick(user)}
className="w-8 h-8 crm-radius-control flex items-center justify-center hover:bg-muted cursor-pointer transition-colors"
title="Edit user"
>
<Edit className="w-4 h-4 text-muted-foreground" />
</button>
<button
type="button"
onClick={() => setDeleteId(user.user_id)}
className="w-8 h-8 crm-radius-control flex items-center justify-center hover:bg-destructive/10 cursor-pointer transition-colors"
title="Delete user"
>
<Trash2 className="w-4 h-4 text-destructive" />
</button>
</div>
</td>
)}
</tr>
);
})
)}
</tbody>
</table>
</div>
)}
<TablePagination {...pager} />
</div>
<SlideOver
open={showAddModal}
onClose={() => !isSubmitting && setShowAddModal(false)}
title="Add user"
icon={<Users className="w-4 h-4 text-primary shrink-0" />}
>
<form onSubmit={handleAddSubmit} className="flex min-h-0 flex-1 flex-col">
<div className="flex-1 min-h-0 overflow-y-auto px-5 py-4 space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className={labelClass}>First name <span className="text-primary">*</span></label>
<input
type="text"
required
value={addForm.first_name}
onChange={(e) => setAddForm({ ...addForm, first_name: e.target.value })}
className={inputClass}
/>
{formErrors.first_name && <p className="text-[11px] text-destructive mt-1">{formErrors.first_name}</p>}
</div>
<div>
<label className={labelClass}>User name <span className="text-primary">*</span></label>
<input
type="text"
required
placeholder="e.g. jdoe"
value={addForm.display_name}
onChange={(e) => setAddForm({ ...addForm, display_name: e.target.value })}
className={inputClass}
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className={labelClass}>Email <span className="text-primary">*</span></label>
<input
type="email"
required
value={addForm.email}
onChange={(e) => setAddForm({ ...addForm, email: e.target.value })}
className={inputClass}
/>
{formErrors.email && <p className="text-[11px] text-destructive mt-1">{formErrors.email}</p>}
</div>
<div>
<PhoneInput
label="Phone"
required
value={addForm.phone}
onChange={(val) => setAddForm({ ...addForm, phone: val })}
error={formErrors.phone}
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className={labelClass}>Password <span className="text-primary">*</span></label>
<input
type="password"
value={addForm.password}
onChange={(e) => setAddForm({ ...addForm, password: e.target.value })}
className={inputClass}
/>
{formErrors.password && <p className="text-[11px] text-destructive mt-1">{formErrors.password}</p>}
</div>
<div>
<label className={labelClass}>Role <span className="text-primary">*</span></label>
<CustomSelect
value={addForm.role_id}
onChange={(role_id) => setAddForm({ ...addForm, role_id })}
aria-label="Role"
options={availableRoles.map((r) => ({ value: r.id, label: r.name }))}
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className={labelClass}>Department <span className="text-primary">*</span></label>
<CustomSelect
value={addForm.department_id}
onChange={(department_id) => setAddForm({ ...addForm, department_id })}
aria-label="Department"
options={DEPARTMENTS.map((d) => ({ value: d.id, label: d.name }))}
/>
</div>
<div>
<label className={labelClass}>Designation <span className="text-primary">*</span></label>
<CustomSelect
value={addForm.designation_id}
onChange={(designation_id) => setAddForm({ ...addForm, designation_id })}
aria-label="Designation"
options={DESIGNATIONS.map((d) => ({ value: d.id, label: d.name }))}
/>
</div>
</div>
</div>
<div className="shrink-0 border-t border-border px-5 py-3 flex items-center justify-end gap-2 bg-card">
<button
type="button"
onClick={() => setShowAddModal(false)}
disabled={isSubmitting}
className={secondaryButton}
>
Cancel
</button>
<button type="submit" disabled={isSubmitting} className={primaryButton}>
{isSubmitting ? 'Creating...' : 'Create user'}
</button>
</div>
</form>
</SlideOver>
<SlideOver
open={showEditModal && !!selectedUser}
onClose={() => !isSubmitting && setShowEditModal(false)}
title="Edit user"
icon={<Edit className="w-4 h-4 text-primary shrink-0" />}
>
<form onSubmit={handleEditSubmit} className="flex min-h-0 flex-1 flex-col">
<div className="flex-1 min-h-0 overflow-y-auto px-5 py-4 space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className={labelClass}>First name <span className="text-primary">*</span></label>
<input
type="text"
required
value={editForm.first_name}
onChange={(e) => setEditForm({ ...editForm, first_name: e.target.value })}
className={inputClass}
/>
{formErrors.first_name && <p className="text-[11px] text-destructive mt-1">{formErrors.first_name}</p>}
</div>
<div>
<label className={labelClass}>User name</label>
<input
type="text"
value={editForm.display_name}
onChange={(e) => setEditForm({ ...editForm, display_name: e.target.value })}
className={inputClass}
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className={labelClass}>Email <span className="text-primary">*</span></label>
<input
type="email"
required
value={editForm.email}
onChange={(e) => setEditForm({ ...editForm, email: e.target.value })}
className={inputClass}
/>
{formErrors.email && <p className="text-[11px] text-destructive mt-1">{formErrors.email}</p>}
</div>
<div>
<PhoneInput
label="Phone"
required
value={editForm.phone}
onChange={(val) => setEditForm({ ...editForm, phone: val })}
error={formErrors.phone}
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className={labelClass}>Reset password</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
placeholder="Leave blank to keep current"
value={editForm.password}
onChange={(e) => setEditForm({ ...editForm, password: e.target.value })}
className={`${inputClass} pr-10`}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer text-muted-foreground hover:text-foreground"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
{formErrors.password && <p className="text-[11px] text-destructive mt-1">{formErrors.password}</p>}
</div>
<div>
<label className={labelClass}>Date of birth</label>
<input
type="date"
value={editForm.dob}
onChange={(e) => setEditForm({ ...editForm, dob: e.target.value })}
className={inputClass}
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label className={labelClass}>Role <span className="text-primary">*</span></label>
<CustomSelect
value={editForm.role_id || ''}
onChange={(role_id) => setEditForm({ ...editForm, role_id })}
aria-label="Role"
options={availableRoles.map((r) => ({ value: r.id, label: r.name }))}
/>
</div>
<div>
<label className={labelClass}>Department <span className="text-primary">*</span></label>
<CustomSelect
value={editForm.department_id || ''}
onChange={(department_id) => setEditForm({ ...editForm, department_id })}
aria-label="Department"
options={DEPARTMENTS.map((d) => ({ value: d.id, label: d.name }))}
/>
</div>
<div>
<label className={labelClass}>Designation <span className="text-primary">*</span></label>
<CustomSelect
value={editForm.designation_id || ''}
onChange={(designation_id) => setEditForm({ ...editForm, designation_id })}
aria-label="Designation"
options={DESIGNATIONS.map((d) => ({ value: d.id, label: d.name }))}
/>
</div>
</div>
</div>
<div className="shrink-0 border-t border-border px-5 py-3 flex items-center justify-end gap-2 bg-card">
<button
type="button"
onClick={() => setShowEditModal(false)}
disabled={isSubmitting}
className={secondaryButton}
>
Cancel
</button>
<button type="submit" disabled={isSubmitting} className={primaryButton}>
{isSubmitting ? 'Saving...' : 'Save changes'}
</button>
</div>
</form>
</SlideOver>
<SlideOver
open={showViewModal && !!selectedUser}
onClose={() => setShowViewModal(false)}
title="User details"
icon={<Users className="w-4 h-4 text-primary shrink-0" />}
>
{selectedUser && (
<div className="flex min-h-0 flex-1 flex-col">
<div className="flex-1 min-h-0 overflow-y-auto px-5 py-4 space-y-5">
<div className="flex items-center gap-3">
<div className="w-12 h-12 crm-radius-icon flex items-center justify-center text-[15px] font-bold text-primary bg-primary/10 shrink-0">
{selectedUser.first_name.charAt(0).toUpperCase()}{selectedUser.last_name.charAt(0).toUpperCase()}
</div>
<div className="min-w-0">
<h4 className="text-[16px] font-semibold text-foreground">
{selectedUser.first_name} {selectedUser.last_name}
</h4>
{selectedUser.display_name && (
<p className="text-[13px] text-muted-foreground">Alias: {selectedUser.display_name}</p>
)}
<p className="text-[12px] text-muted-foreground font-mono mt-0.5">
Code: {selectedUser.employee_code || '—'}
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className={labelClass}>Role</p>
<p className="text-[13px] font-semibold text-foreground">{getRoleName(selectedUser.role_id)}</p>
</div>
<div>
<p className={labelClass}>Department</p>
<p className="text-[13px] font-semibold text-foreground">{getDeptName(selectedUser.department_id)}</p>
</div>
<div>
<p className={labelClass}>Designation</p>
<p className="text-[13px] font-semibold text-foreground">{getDesigName(selectedUser.designation_id)}</p>
</div>
<div>
<p className={labelClass}>Status</p>
{renderStatusBadge(selectedUser.is_active)}
</div>
</div>
<div className="p-4 crm-radius-section border border-border bg-muted/30 space-y-3">
<div>
<p className={labelClass}>Email</p>
<p className="text-[13px] text-foreground">{selectedUser.email}</p>
</div>
<div>
<p className={labelClass}>Phone</p>
<p className="text-[13px] text-foreground">{selectedUser.phone}</p>
</div>
</div>
</div>
<div className="shrink-0 border-t border-border px-5 py-3 flex justify-end bg-card">
<button type="button" onClick={() => setShowViewModal(false)} className={secondaryButton}>
Close
</button>
</div>
</div>
)}
</SlideOver>
{deleteId && (
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 p-4"
onClick={() => setDeleteId(null)}
>
<div
role="dialog"
aria-modal="true"
aria-labelledby="delete-user-title"
className="bg-card border border-border shadow-2xl w-full max-w-sm overflow-hidden crm-radius-none"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-start justify-between gap-3 px-5 pt-5 pb-2">
<div className="flex items-start gap-3 min-w-0">
<div className="w-10 h-10 rounded-full bg-destructive/10 text-destructive flex items-center justify-center shrink-0">
<AlertTriangle className="w-5 h-5" />
</div>
<div className="min-w-0">
<h3 id="delete-user-title" className="text-[15px] font-semibold text-foreground">
Delete user
</h3>
<p className="text-[13px] text-muted-foreground mt-1 leading-relaxed">
This will soft-delete the staff profile and disable system access.
</p>
</div>
</div>
<button
type="button"
aria-label="Close delete dialog"
onClick={() => setDeleteId(null)}
className="w-8 h-8 rounded-full border border-border text-muted-foreground hover:bg-muted cursor-pointer transition-colors flex items-center justify-center shrink-0"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="flex items-center justify-end gap-2 px-5 pb-5 pt-3">
<button type="button" onClick={() => setDeleteId(null)} className={secondaryButton}>
Cancel
</button>
<button
type="button"
onClick={() => handleDelete(deleteId)}
className="inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control bg-destructive hover:bg-destructive/90 text-white text-[13px] font-semibold cursor-pointer"
>
Delete
</button>
</div>
</div>
</div>
)}
{showBulkDeleteModal && (
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 p-4"
onClick={() => setShowBulkDeleteModal(false)}
>
<div
role="dialog"
aria-modal="true"
aria-labelledby="bulk-delete-title"
className="bg-card border border-border shadow-2xl w-full max-w-sm overflow-hidden crm-radius-none"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-start justify-between gap-3 px-5 pt-5 pb-2">
<div className="flex items-start gap-3 min-w-0">
<div className="w-10 h-10 rounded-full bg-destructive/10 text-destructive flex items-center justify-center shrink-0">
<AlertTriangle className="w-5 h-5" />
</div>
<div className="min-w-0">
<h3 id="bulk-delete-title" className="text-[15px] font-semibold text-foreground">
Delete selected users
</h3>
<p className="text-[13px] text-muted-foreground mt-1 leading-relaxed">
Delete <span className="font-semibold text-foreground">{selectedIds.length}</span> selected staff profiles?
</p>
</div>
</div>
<button
type="button"
aria-label="Close bulk delete dialog"
onClick={() => setShowBulkDeleteModal(false)}
className="w-8 h-8 rounded-full border border-border text-muted-foreground hover:bg-muted cursor-pointer transition-colors flex items-center justify-center shrink-0"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="px-5 pb-2">
<label className="flex items-center justify-between gap-3 p-3 crm-radius-section border border-border bg-muted/30 cursor-pointer">
<div>
<p className="text-[13px] font-semibold text-foreground">Soft delete</p>
<p className="text-[11px] text-muted-foreground">Keep the profile archived in the database</p>
</div>
<input
type="checkbox"
checked={isSoftDelete}
onChange={() => setIsSoftDelete(!isSoftDelete)}
className="rounded-sm border-border text-primary focus:ring-primary w-4 h-4 cursor-pointer"
/>
</label>
</div>
<div className="flex items-center justify-end gap-2 px-5 pb-5 pt-3">
<button type="button" onClick={() => setShowBulkDeleteModal(false)} className={secondaryButton}>
Cancel
</button>
<button
type="button"
onClick={handleBulkDelete}
className="inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control bg-destructive hover:bg-destructive/90 text-white text-[13px] font-semibold cursor-pointer"
>
Delete
</button>
</div>
</div>
</div>
)}
</div>
);
}