/** * @ai-summary Desktop admin page for user management * @ai-context List users, filter, search, change tiers, deactivate/reactivate */ import React, { useState, useCallback } from 'react'; import { Navigate } from 'react-router-dom'; import { Box, Button, Chip, CircularProgress, Collapse, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, IconButton, InputAdornment, InputLabel, Menu, MenuItem, Paper, Select, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TextField, Tooltip, Typography, SelectChangeEvent, } from '@mui/material'; import { Search, Clear, MoreVert, AdminPanelSettings, PersonOff, PersonAdd, Edit, Security, DeleteForever, DirectionsCar, KeyboardArrowDown, KeyboardArrowUp, } from '@mui/icons-material'; import { useAdminAccess } from '../../core/auth/useAdminAccess'; import { useUsers, useUpdateUserTier, useDeactivateUser, useReactivateUser, useUpdateUserProfile, usePromoteToAdmin, useHardDeleteUser, useAdminStats, useUserVehicles, } from '../../features/admin/hooks/useUsers'; import { ManagedUser, SubscriptionTier, ListUsersParams, } from '../../features/admin/types/admin.types'; import { AdminSectionHeader } from '../../features/admin/components'; const PAGE_SIZE_OPTIONS = [10, 20, 50, 100]; // Expandable vehicle row component const UserVehiclesRow: React.FC<{ userId: string; isOpen: boolean }> = ({ userId, isOpen }) => { const { data, isLoading, error } = useUserVehicles(userId); if (!isOpen) return null; return ( Vehicles {isLoading ? ( ) : error ? ( Failed to load vehicles ) : !data?.vehicles?.length ? ( No vehicles registered ) : ( Year Make Model {data.vehicles.map((vehicle, idx) => ( {vehicle.year} {vehicle.make} {vehicle.model} ))}
)}
); }; export const AdminUsersPage: React.FC = () => { const { isAdmin, loading: adminLoading } = useAdminAccess(); // Filter state const [params, setParams] = useState({ page: 1, pageSize: 20, status: 'all', sortBy: 'createdAt', sortOrder: 'desc', }); const [searchInput, setSearchInput] = useState(''); // Query const { data, isLoading, error } = useUsers(params); // Mutations const updateTierMutation = useUpdateUserTier(); const deactivateMutation = useDeactivateUser(); const reactivateMutation = useReactivateUser(); const updateProfileMutation = useUpdateUserProfile(); const promoteToAdminMutation = usePromoteToAdmin(); const hardDeleteMutation = useHardDeleteUser(); // Action menu state const [anchorEl, setAnchorEl] = useState(null); const [selectedUser, setSelectedUser] = useState(null); // Deactivate dialog state const [deactivateDialogOpen, setDeactivateDialogOpen] = useState(false); const [deactivateReason, setDeactivateReason] = useState(''); // Edit dialog state const [editDialogOpen, setEditDialogOpen] = useState(false); const [editEmail, setEditEmail] = useState(''); const [editDisplayName, setEditDisplayName] = useState(''); // Promote to admin dialog state const [promoteDialogOpen, setPromoteDialogOpen] = useState(false); const [promoteRole, setPromoteRole] = useState<'admin' | 'super_admin'>('admin'); // Hard delete dialog state const [hardDeleteDialogOpen, setHardDeleteDialogOpen] = useState(false); const [hardDeleteReason, setHardDeleteReason] = useState(''); const [hardDeleteConfirmText, setHardDeleteConfirmText] = useState(''); // Expanded row state for vehicle list const [expandedRow, setExpandedRow] = useState(null); // Admin stats query const { data: statsData } = useAdminStats(); // Handlers const handleSearch = useCallback(() => { setParams(prev => ({ ...prev, search: searchInput || undefined, page: 1 })); }, [searchInput]); const handleSearchKeyPress = useCallback( (event: React.KeyboardEvent) => { if (event.key === 'Enter') { handleSearch(); } }, [handleSearch] ); const handleClearSearch = useCallback(() => { setSearchInput(''); setParams(prev => ({ ...prev, search: undefined, page: 1 })); }, []); const handlePageChange = useCallback((_: unknown, newPage: number) => { setParams(prev => ({ ...prev, page: newPage + 1 })); }, []); const handleRowsPerPageChange = useCallback( (event: React.ChangeEvent) => { setParams(prev => ({ ...prev, pageSize: parseInt(event.target.value, 10), page: 1 })); }, [] ); const handleTierFilterChange = useCallback((event: SelectChangeEvent) => { const value = event.target.value; setParams(prev => ({ ...prev, tier: value ? (value as SubscriptionTier) : undefined, page: 1, })); }, []); const handleStatusFilterChange = useCallback((event: SelectChangeEvent) => { setParams(prev => ({ ...prev, status: event.target.value as 'active' | 'deactivated' | 'all', page: 1, })); }, []); const handleTierChange = useCallback( (userId: string, newTier: SubscriptionTier) => { updateTierMutation.mutate({ userId, data: { subscriptionTier: newTier } }); }, [updateTierMutation] ); const handleMenuOpen = useCallback((event: React.MouseEvent, user: ManagedUser) => { setAnchorEl(event.currentTarget); setSelectedUser(user); }, []); const handleMenuClose = useCallback(() => { setAnchorEl(null); setSelectedUser(null); }, []); const handleDeactivateClick = useCallback(() => { setDeactivateDialogOpen(true); setAnchorEl(null); }, []); const handleDeactivateConfirm = useCallback(() => { if (selectedUser) { deactivateMutation.mutate( { userId: selectedUser.id, data: { reason: deactivateReason || undefined } }, { onSuccess: () => { setDeactivateDialogOpen(false); setDeactivateReason(''); setSelectedUser(null); }, } ); } }, [selectedUser, deactivateReason, deactivateMutation]); const handleReactivate = useCallback(() => { if (selectedUser) { reactivateMutation.mutate(selectedUser.id); setAnchorEl(null); setSelectedUser(null); } }, [selectedUser, reactivateMutation]); const handleEditClick = useCallback(() => { if (selectedUser) { setEditEmail(selectedUser.email); setEditDisplayName(selectedUser.displayName || ''); setEditDialogOpen(true); setAnchorEl(null); } }, [selectedUser]); const handleEditConfirm = useCallback(() => { if (selectedUser) { const updates: { email?: string; displayName?: string } = {}; if (editEmail !== selectedUser.email) { updates.email = editEmail; } if (editDisplayName !== (selectedUser.displayName || '')) { updates.displayName = editDisplayName; } if (Object.keys(updates).length > 0) { updateProfileMutation.mutate( { userId: selectedUser.id, data: updates }, { onSuccess: () => { setEditDialogOpen(false); setEditEmail(''); setEditDisplayName(''); setSelectedUser(null); }, } ); } } }, [selectedUser, editEmail, editDisplayName, updateProfileMutation]); const handleEditCancel = useCallback(() => { setEditDialogOpen(false); setEditEmail(''); setEditDisplayName(''); setSelectedUser(null); }, []); const handlePromoteClick = useCallback(() => { setPromoteRole('admin'); setPromoteDialogOpen(true); setAnchorEl(null); }, []); const handlePromoteConfirm = useCallback(() => { if (selectedUser) { promoteToAdminMutation.mutate( { userId: selectedUser.id, data: { role: promoteRole } }, { onSuccess: () => { setPromoteDialogOpen(false); setPromoteRole('admin'); setSelectedUser(null); }, } ); } }, [selectedUser, promoteRole, promoteToAdminMutation]); const handlePromoteCancel = useCallback(() => { setPromoteDialogOpen(false); setPromoteRole('admin'); setSelectedUser(null); }, []); const handleHardDeleteClick = useCallback(() => { setHardDeleteDialogOpen(true); setAnchorEl(null); }, []); const handleHardDeleteConfirm = useCallback(() => { if (selectedUser && hardDeleteConfirmText === 'DELETE') { hardDeleteMutation.mutate( { userId: selectedUser.id, reason: hardDeleteReason || undefined }, { onSuccess: () => { setHardDeleteDialogOpen(false); setHardDeleteReason(''); setHardDeleteConfirmText(''); setSelectedUser(null); }, } ); } }, [selectedUser, hardDeleteReason, hardDeleteConfirmText, hardDeleteMutation]); const handleHardDeleteCancel = useCallback(() => { setHardDeleteDialogOpen(false); setHardDeleteReason(''); setHardDeleteConfirmText(''); setSelectedUser(null); }, []); // Loading state if (adminLoading) { return ( ); } // Not admin redirect if (!isAdmin) { return ; } const users = data?.users || []; const total = data?.total || 0; return ( {/* Filters Bar */} {/* Search Input */} setSearchInput(e.target.value)} onKeyPress={handleSearchKeyPress} size="small" sx={{ flex: 1, minWidth: 250 }} InputProps={{ startAdornment: ( ), endAdornment: searchInput && ( ), }} /> {/* Search Button */} {/* Tier Filter */} Tier {/* Status Filter */} Status {/* Users Table */} {isLoading ? ( ) : error ? ( Failed to load users. Please try again. ) : users.length === 0 ? ( No users found matching your criteria. ) : ( <> Email Display Name Tier Vehicles Status Admin Created Actions {users.map((user) => ( *': { borderBottom: expandedRow === user.id ? 'unset' : undefined }, }} > {user.email} {user.displayName || '-'} {user.vehicleCount > 0 && ( setExpandedRow( expandedRow === user.id ? null : user.id )} aria-label="show vehicles" sx={{ minWidth: 44, minHeight: 44 }} > {expandedRow === user.id ? ( ) : ( )} )} {user.vehicleCount} {user.isAdmin && ( )} {new Date(user.createdAt).toLocaleDateString()} handleMenuOpen(e, user)} > ))}
)}
{/* Action Menu */} Edit User {!selectedUser?.isAdmin && ( Promote to Admin )} {selectedUser?.deactivatedAt ? ( Reactivate User ) : ( Deactivate User )} {!selectedUser?.isAdmin && ( Delete Permanently )} {/* Deactivate Confirmation Dialog */} !deactivateMutation.isPending && setDeactivateDialogOpen(false)} > Deactivate User Are you sure you want to deactivate{' '} {selectedUser?.email}? The user will no longer be able to log in, but their data will be preserved. setDeactivateReason(e.target.value)} fullWidth multiline rows={2} placeholder="Enter a reason for deactivation..." /> {/* Edit User Dialog */} !updateProfileMutation.isPending && handleEditCancel()} maxWidth="sm" fullWidth > Edit User setEditEmail(e.target.value)} fullWidth /> setEditDisplayName(e.target.value)} fullWidth placeholder="Enter display name..." /> {/* Promote to Admin Dialog */} !promoteToAdminMutation.isPending && handlePromoteCancel()} maxWidth="sm" fullWidth > Promote to Admin Promote {selectedUser?.email} to an administrator role. Admin Role Admins can manage users, catalog data, and view audit logs. Super Admins have additional permissions to manage other administrators. {/* Hard Delete Confirmation Dialog */} !hardDeleteMutation.isPending && handleHardDeleteCancel()} maxWidth="sm" fullWidth > Permanently Delete User Warning: This action cannot be undone! All user data will be permanently deleted, including vehicles, fuel logs, maintenance records, and documents. The user's Auth0 account will also be deleted. Are you sure you want to permanently delete{' '} {selectedUser?.email}? setHardDeleteReason(e.target.value)} fullWidth multiline rows={2} placeholder="GDPR request, user request, etc..." sx={{ mb: 3 }} /> Type DELETE to confirm: setHardDeleteConfirmText(e.target.value.toUpperCase())} fullWidth placeholder="Type DELETE" error={hardDeleteConfirmText.length > 0 && hardDeleteConfirmText !== 'DELETE'} helperText={ hardDeleteConfirmText.length > 0 && hardDeleteConfirmText !== 'DELETE' ? 'Please type DELETE exactly' : '' } />
); };