Files
motovaultpro/frontend/src/pages/admin/AdminUsersPage.tsx
Eric Gullickson 754639c86d
Some checks failed
Deploy to Staging / Build Images (pull_request) Successful in 6m41s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 52s
Deploy to Staging / Verify Staging (pull_request) Failing after 4m7s
Deploy to Staging / Notify Staging Ready (pull_request) Has been skipped
Deploy to Staging / Notify Staging Failure (pull_request) Successful in 9s
chore: update test fixtures and frontend for UUID identity (refs #217)
Backend test fixtures:
- Replace auth0|xxx format with UUID in all test userId values
- Update admin tests for new id/userProfileId schema
- Add missing deletionRequestedAt/deletionScheduledFor to auth test mocks
- Fix admin integration test supertest usage (app.server)

Frontend:
- AdminUser type: auth0Sub -> id + userProfileId
- admin.api.ts: all user management methods use userId (UUID) params
- useUsers/useAdmins hooks: auth0Sub -> userId/id in mutations
- AdminUsersPage + AdminUsersMobileScreen: user.auth0Sub -> user.id
- Remove encodeURIComponent (UUIDs don't need encoding)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:21:18 -06:00

844 lines
28 KiB
TypeScript

/**
* @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 (
<TableRow>
<TableCell colSpan={9} sx={{ py: 0, bgcolor: 'action.hover' }}>
<Collapse in={isOpen} timeout="auto" unmountOnExit>
<Box sx={{ py: 2, px: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
<DirectionsCar fontSize="small" />
Vehicles
</Typography>
{isLoading ? (
<CircularProgress size={20} />
) : error ? (
<Typography variant="body2" color="error">
Failed to load vehicles
</Typography>
) : !data?.vehicles?.length ? (
<Typography variant="body2" color="text.secondary">
No vehicles registered
</Typography>
) : (
<Table size="small" sx={{ maxWidth: 500 }}>
<TableHead>
<TableRow>
<TableCell sx={{ fontWeight: 600 }}>Year</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Make</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Model</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.vehicles.map((vehicle, idx) => (
<TableRow key={idx}>
<TableCell>{vehicle.year}</TableCell>
<TableCell>{vehicle.make}</TableCell>
<TableCell>{vehicle.model}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</Box>
</Collapse>
</TableCell>
</TableRow>
);
};
export const AdminUsersPage: React.FC = () => {
const { isAdmin, loading: adminLoading } = useAdminAccess();
// Filter state
const [params, setParams] = useState<ListUsersParams>({
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 | HTMLElement>(null);
const [selectedUser, setSelectedUser] = useState<ManagedUser | null>(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<string | null>(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<HTMLInputElement>) => {
setParams(prev => ({ ...prev, pageSize: parseInt(event.target.value, 10), page: 1 }));
},
[]
);
const handleTierFilterChange = useCallback((event: SelectChangeEvent<string>) => {
const value = event.target.value;
setParams(prev => ({
...prev,
tier: value ? (value as SubscriptionTier) : undefined,
page: 1,
}));
}, []);
const handleStatusFilterChange = useCallback((event: SelectChangeEvent<string>) => {
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<HTMLElement>, 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 (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
<CircularProgress />
</Box>
);
}
// Not admin redirect
if (!isAdmin) {
return <Navigate to="/garage/settings" replace />;
}
const users = data?.users || [];
const total = data?.total || 0;
return (
<Box sx={{ py: 2, display: 'flex', flexDirection: 'column', gap: 3 }}>
<AdminSectionHeader
title="User Management"
stats={[
{ label: 'Total Users', value: statsData?.totalUsers ?? total },
{ label: 'Total Vehicles', value: statsData?.totalVehicles ?? 0 },
]}
/>
{/* Filters Bar */}
<Paper elevation={1} sx={{ p: 2, borderRadius: 1.5 }}>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
gap: 2,
alignItems: { xs: 'stretch', md: 'center' },
}}
>
{/* Search Input */}
<TextField
placeholder="Search by email or name..."
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
onKeyPress={handleSearchKeyPress}
size="small"
sx={{ flex: 1, minWidth: 250 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search color="action" />
</InputAdornment>
),
endAdornment: searchInput && (
<InputAdornment position="end">
<IconButton size="small" onClick={handleClearSearch}>
<Clear />
</IconButton>
</InputAdornment>
),
}}
/>
{/* Search Button */}
<Button
variant="contained"
onClick={handleSearch}
disabled={isLoading}
sx={{ textTransform: 'none', minWidth: 100 }}
>
{isLoading ? <CircularProgress size={20} /> : 'Search'}
</Button>
{/* Tier Filter */}
<FormControl size="small" sx={{ minWidth: 120 }}>
<InputLabel>Tier</InputLabel>
<Select
value={params.tier || ''}
label="Tier"
onChange={handleTierFilterChange}
>
<MenuItem value="">All</MenuItem>
<MenuItem value="free">Free</MenuItem>
<MenuItem value="pro">Pro</MenuItem>
<MenuItem value="enterprise">Enterprise</MenuItem>
</Select>
</FormControl>
{/* Status Filter */}
<FormControl size="small" sx={{ minWidth: 140 }}>
<InputLabel>Status</InputLabel>
<Select
value={params.status || 'all'}
label="Status"
onChange={handleStatusFilterChange}
>
<MenuItem value="all">All</MenuItem>
<MenuItem value="active">Active</MenuItem>
<MenuItem value="deactivated">Deactivated</MenuItem>
</Select>
</FormControl>
</Box>
</Paper>
{/* Users Table */}
<Paper elevation={1} sx={{ borderRadius: 1.5 }}>
{isLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress />
</Box>
) : error ? (
<Box sx={{ p: 4, textAlign: 'center' }}>
<Typography color="error">Failed to load users. Please try again.</Typography>
</Box>
) : users.length === 0 ? (
<Box sx={{ p: 4, textAlign: 'center' }}>
<Typography color="text.secondary">No users found matching your criteria.</Typography>
</Box>
) : (
<>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Email</TableCell>
<TableCell>Display Name</TableCell>
<TableCell>Tier</TableCell>
<TableCell>Vehicles</TableCell>
<TableCell>Status</TableCell>
<TableCell>Admin</TableCell>
<TableCell>Created</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users.map((user) => (
<React.Fragment key={user.id}>
<TableRow
sx={{
opacity: user.deactivatedAt ? 0.6 : 1,
'& > *': { borderBottom: expandedRow === user.id ? 'unset' : undefined },
}}
>
<TableCell>{user.email}</TableCell>
<TableCell>{user.displayName || '-'}</TableCell>
<TableCell>
<FormControl size="small" sx={{ minWidth: 100 }}>
<Select
value={user.subscriptionTier}
onChange={(e) =>
handleTierChange(user.id, e.target.value as SubscriptionTier)
}
disabled={!!user.deactivatedAt || updateTierMutation.isPending}
size="small"
>
<MenuItem value="free">Free</MenuItem>
<MenuItem value="pro">Pro</MenuItem>
<MenuItem value="enterprise">Enterprise</MenuItem>
</Select>
</FormControl>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
{user.vehicleCount > 0 && (
<IconButton
size="small"
onClick={() => setExpandedRow(
expandedRow === user.id ? null : user.id
)}
aria-label="show vehicles"
sx={{ minWidth: 44, minHeight: 44 }}
>
{expandedRow === user.id ? (
<KeyboardArrowUp fontSize="small" />
) : (
<KeyboardArrowDown fontSize="small" />
)}
</IconButton>
)}
<Typography variant="body2">{user.vehicleCount}</Typography>
</Box>
</TableCell>
<TableCell>
<Chip
label={user.deactivatedAt ? 'Deactivated' : 'Active'}
color={user.deactivatedAt ? 'error' : 'success'}
size="small"
/>
</TableCell>
<TableCell>
{user.isAdmin && (
<Tooltip title={`Admin (${user.adminRole})`}>
<AdminPanelSettings color="primary" />
</Tooltip>
)}
</TableCell>
<TableCell>
{new Date(user.createdAt).toLocaleDateString()}
</TableCell>
<TableCell align="right">
<IconButton
size="small"
onClick={(e) => handleMenuOpen(e, user)}
>
<MoreVert />
</IconButton>
</TableCell>
</TableRow>
<UserVehiclesRow
userId={user.id}
isOpen={expandedRow === user.id}
/>
</React.Fragment>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={total}
page={(params.page || 1) - 1}
rowsPerPage={params.pageSize || 20}
onPageChange={handlePageChange}
onRowsPerPageChange={handleRowsPerPageChange}
rowsPerPageOptions={PAGE_SIZE_OPTIONS}
/>
</>
)}
</Paper>
{/* Action Menu */}
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
<MenuItem onClick={handleEditClick}>
<Edit sx={{ mr: 1 }} fontSize="small" />
Edit User
</MenuItem>
{!selectedUser?.isAdmin && (
<MenuItem onClick={handlePromoteClick}>
<Security sx={{ mr: 1 }} fontSize="small" />
Promote to Admin
</MenuItem>
)}
{selectedUser?.deactivatedAt ? (
<MenuItem onClick={handleReactivate}>
<PersonAdd sx={{ mr: 1 }} fontSize="small" />
Reactivate User
</MenuItem>
) : (
<MenuItem onClick={handleDeactivateClick} sx={{ color: 'error.main' }}>
<PersonOff sx={{ mr: 1 }} fontSize="small" />
Deactivate User
</MenuItem>
)}
{!selectedUser?.isAdmin && (
<MenuItem onClick={handleHardDeleteClick} sx={{ color: 'error.main' }}>
<DeleteForever sx={{ mr: 1 }} fontSize="small" />
Delete Permanently
</MenuItem>
)}
</Menu>
{/* Deactivate Confirmation Dialog */}
<Dialog
open={deactivateDialogOpen}
onClose={() => !deactivateMutation.isPending && setDeactivateDialogOpen(false)}
>
<DialogTitle>Deactivate User</DialogTitle>
<DialogContent>
<Typography sx={{ mb: 2 }}>
Are you sure you want to deactivate{' '}
<strong>{selectedUser?.email}</strong>?
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
The user will no longer be able to log in, but their data will be preserved.
</Typography>
<TextField
label="Reason (optional)"
value={deactivateReason}
onChange={(e) => setDeactivateReason(e.target.value)}
fullWidth
multiline
rows={2}
placeholder="Enter a reason for deactivation..."
/>
</DialogContent>
<DialogActions>
<Button
onClick={() => setDeactivateDialogOpen(false)}
disabled={deactivateMutation.isPending}
sx={{ textTransform: 'none' }}
>
Cancel
</Button>
<Button
onClick={handleDeactivateConfirm}
disabled={deactivateMutation.isPending}
color="error"
variant="contained"
sx={{ textTransform: 'none' }}
>
{deactivateMutation.isPending ? <CircularProgress size={20} /> : 'Deactivate'}
</Button>
</DialogActions>
</Dialog>
{/* Edit User Dialog */}
<Dialog
open={editDialogOpen}
onClose={() => !updateProfileMutation.isPending && handleEditCancel()}
maxWidth="sm"
fullWidth
>
<DialogTitle>Edit User</DialogTitle>
<DialogContent>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
<TextField
label="Email"
type="email"
value={editEmail}
onChange={(e) => setEditEmail(e.target.value)}
fullWidth
/>
<TextField
label="Display Name"
value={editDisplayName}
onChange={(e) => setEditDisplayName(e.target.value)}
fullWidth
placeholder="Enter display name..."
/>
</Box>
</DialogContent>
<DialogActions>
<Button
onClick={handleEditCancel}
disabled={updateProfileMutation.isPending}
sx={{ textTransform: 'none' }}
>
Cancel
</Button>
<Button
onClick={handleEditConfirm}
disabled={updateProfileMutation.isPending || (editEmail === selectedUser?.email && editDisplayName === (selectedUser?.displayName || ''))}
color="primary"
variant="contained"
sx={{ textTransform: 'none' }}
>
{updateProfileMutation.isPending ? <CircularProgress size={20} /> : 'Save'}
</Button>
</DialogActions>
</Dialog>
{/* Promote to Admin Dialog */}
<Dialog
open={promoteDialogOpen}
onClose={() => !promoteToAdminMutation.isPending && handlePromoteCancel()}
maxWidth="sm"
fullWidth
>
<DialogTitle>Promote to Admin</DialogTitle>
<DialogContent>
<Typography sx={{ mb: 3 }}>
Promote <strong>{selectedUser?.email}</strong> to an administrator role.
</Typography>
<FormControl fullWidth>
<InputLabel>Admin Role</InputLabel>
<Select
value={promoteRole}
label="Admin Role"
onChange={(e) => setPromoteRole(e.target.value as 'admin' | 'super_admin')}
>
<MenuItem value="admin">Admin</MenuItem>
<MenuItem value="super_admin">Super Admin</MenuItem>
</Select>
</FormControl>
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
Admins can manage users, catalog data, and view audit logs.
Super Admins have additional permissions to manage other administrators.
</Typography>
</DialogContent>
<DialogActions>
<Button
onClick={handlePromoteCancel}
disabled={promoteToAdminMutation.isPending}
sx={{ textTransform: 'none' }}
>
Cancel
</Button>
<Button
onClick={handlePromoteConfirm}
disabled={promoteToAdminMutation.isPending}
color="primary"
variant="contained"
sx={{ textTransform: 'none' }}
>
{promoteToAdminMutation.isPending ? <CircularProgress size={20} /> : 'Promote'}
</Button>
</DialogActions>
</Dialog>
{/* Hard Delete Confirmation Dialog */}
<Dialog
open={hardDeleteDialogOpen}
onClose={() => !hardDeleteMutation.isPending && handleHardDeleteCancel()}
maxWidth="sm"
fullWidth
>
<DialogTitle sx={{ color: 'error.main' }}>
Permanently Delete User
</DialogTitle>
<DialogContent>
<Box sx={{ bgcolor: 'error.light', color: 'error.contrastText', p: 2, borderRadius: 1, mb: 3 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
Warning: This action cannot be undone!
</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>
All user data will be permanently deleted, including vehicles, fuel logs,
maintenance records, and documents. The user's Auth0 account will also be deleted.
</Typography>
</Box>
<Typography sx={{ mb: 2 }}>
Are you sure you want to permanently delete{' '}
<strong>{selectedUser?.email}</strong>?
</Typography>
<TextField
label="Reason for deletion"
value={hardDeleteReason}
onChange={(e) => setHardDeleteReason(e.target.value)}
fullWidth
multiline
rows={2}
placeholder="GDPR request, user request, etc..."
sx={{ mb: 3 }}
/>
<Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Type <strong>DELETE</strong> to confirm:
</Typography>
<TextField
value={hardDeleteConfirmText}
onChange={(e) => 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'
: ''
}
/>
</Box>
</DialogContent>
<DialogActions>
<Button
onClick={handleHardDeleteCancel}
disabled={hardDeleteMutation.isPending}
sx={{ textTransform: 'none' }}
>
Cancel
</Button>
<Button
onClick={handleHardDeleteConfirm}
disabled={hardDeleteMutation.isPending || hardDeleteConfirmText !== 'DELETE'}
color="error"
variant="contained"
sx={{ textTransform: 'none' }}
>
{hardDeleteMutation.isPending ? <CircularProgress size={20} /> : 'Delete Permanently'}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};