405 lines
No EOL
12 KiB
JavaScript
405 lines
No EOL
12 KiB
JavaScript
import React, { useState } from 'react';
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Paper,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
TablePagination,
|
|
IconButton,
|
|
TextField,
|
|
InputAdornment,
|
|
Chip,
|
|
CircularProgress,
|
|
Alert,
|
|
Dialog,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogContentText,
|
|
DialogTitle,
|
|
Button,
|
|
Switch,
|
|
FormControlLabel,
|
|
Tooltip
|
|
} from '@mui/material';
|
|
import {
|
|
Search as SearchIcon,
|
|
Edit as EditIcon,
|
|
Clear as ClearIcon,
|
|
Mail as MailIcon,
|
|
CheckCircle as ActiveIcon,
|
|
Cancel as DisabledIcon
|
|
} from '@mui/icons-material';
|
|
import { useAdminUsers, useUpdateUser } from '@hooks/adminHooks';
|
|
import { format } from 'date-fns';
|
|
import EmailDialog from '@components/EmailDialog';
|
|
import { useAuth } from '@hooks/reduxHooks';
|
|
const AdminCustomersPage = () => {
|
|
const { user, userData, isAuthenticated } = useAuth();
|
|
const [page, setPage] = useState(0);
|
|
const [rowsPerPage, setRowsPerPage] = useState(10);
|
|
const [search, setSearch] = useState('');
|
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
|
const [emailDialogOpen, setEmailDialogOpen] = useState(false);
|
|
const [currentUser, setCurrentUser] = useState(null);
|
|
const [emailRecipient, setEmailRecipient] = useState(null);
|
|
const [formData, setFormData] = useState({
|
|
is_disabled: false,
|
|
is_admin: false,
|
|
internal_notes: ''
|
|
});
|
|
|
|
// React Query hooks
|
|
const { data: users, isLoading, error } = useAdminUsers();
|
|
const updateUserMutation = useUpdateUser();
|
|
|
|
// Filter users by search term
|
|
const filteredUsers = users ? users.filter(user => {
|
|
const searchTerm = search.toLowerCase();
|
|
return (
|
|
user.email.toLowerCase().includes(searchTerm) ||
|
|
(user.first_name && user.first_name.toLowerCase().includes(searchTerm)) ||
|
|
(user.last_name && user.last_name.toLowerCase().includes(searchTerm))
|
|
);
|
|
}) : [];
|
|
|
|
// Paginated users
|
|
const paginatedUsers = filteredUsers.slice(
|
|
page * rowsPerPage,
|
|
page * rowsPerPage + rowsPerPage
|
|
);
|
|
|
|
// Handle search change
|
|
const handleSearchChange = (event) => {
|
|
setSearch(event.target.value);
|
|
setPage(0);
|
|
};
|
|
|
|
// Clear search
|
|
const handleClearSearch = () => {
|
|
setSearch('');
|
|
setPage(0);
|
|
};
|
|
|
|
// Handle page change
|
|
const handleChangePage = (event, newPage) => {
|
|
setPage(newPage);
|
|
};
|
|
|
|
// Handle rows per page change
|
|
const handleChangeRowsPerPage = (event) => {
|
|
setRowsPerPage(parseInt(event.target.value, 10));
|
|
setPage(0);
|
|
};
|
|
|
|
// Handle edit dialog open
|
|
const handleOpenEditDialog = (user) => {
|
|
setCurrentUser(user);
|
|
setFormData({
|
|
is_disabled: user.is_disabled,
|
|
is_admin: user.is_admin,
|
|
internal_notes: user.internal_notes || ''
|
|
});
|
|
setEditDialogOpen(true);
|
|
};
|
|
|
|
// Handle edit dialog close
|
|
const handleCloseEditDialog = () => {
|
|
setCurrentUser(null);
|
|
setEditDialogOpen(false);
|
|
};
|
|
|
|
// Handle email dialog open
|
|
const handleOpenEmailDialog = (user) => {
|
|
setEmailRecipient(user);
|
|
setEmailDialogOpen(true);
|
|
};
|
|
|
|
// Handle email dialog close
|
|
const handleCloseEmailDialog = () => {
|
|
setEmailRecipient(null);
|
|
setEmailDialogOpen(false);
|
|
};
|
|
|
|
// Handle form changes
|
|
const handleFormChange = (e) => {
|
|
const { name, value, checked } = e.target;
|
|
setFormData(prev => ({
|
|
...prev,
|
|
[name]: name === 'is_disabled' || name === 'is_admin' ? checked : value
|
|
}));
|
|
};
|
|
|
|
// Handle save user
|
|
const handleSaveUser = () => {
|
|
if (currentUser) {
|
|
updateUserMutation.mutate({
|
|
id: currentUser.id,
|
|
data: formData
|
|
});
|
|
}
|
|
};
|
|
|
|
// Format date
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return 'Never';
|
|
try {
|
|
return format(new Date(dateString), 'MMM d, yyyy h:mm a');
|
|
} catch (error) {
|
|
return dateString;
|
|
}
|
|
};
|
|
|
|
// Loading state
|
|
if (isLoading) {
|
|
return (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// Error state
|
|
if (error) {
|
|
return (
|
|
<Alert severity="error" sx={{ my: 2 }}>
|
|
Error loading customers: {error.message}
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box>
|
|
<Typography variant="h4" component="h1" gutterBottom>
|
|
Customers
|
|
</Typography>
|
|
|
|
{/* Search Box */}
|
|
<Paper sx={{ p: 2, mb: 3 }}>
|
|
<TextField
|
|
fullWidth
|
|
placeholder="Search by name or email..."
|
|
value={search}
|
|
onChange={handleSearchChange}
|
|
InputProps={{
|
|
startAdornment: (
|
|
<InputAdornment position="start">
|
|
<SearchIcon />
|
|
</InputAdornment>
|
|
),
|
|
endAdornment: search && (
|
|
<InputAdornment position="end">
|
|
<IconButton size="small" onClick={handleClearSearch}>
|
|
<ClearIcon />
|
|
</IconButton>
|
|
</InputAdornment>
|
|
)
|
|
}}
|
|
/>
|
|
</Paper>
|
|
|
|
{/* Users Table */}
|
|
<Paper>
|
|
<TableContainer>
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>Name</TableCell>
|
|
<TableCell>Email</TableCell>
|
|
<TableCell>Status</TableCell>
|
|
<TableCell>Last Login</TableCell>
|
|
<TableCell>Joined</TableCell>
|
|
<TableCell>Notes</TableCell>
|
|
<TableCell align="right">Actions</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{paginatedUsers.length > 0 ? (
|
|
paginatedUsers.map((user) => (
|
|
<TableRow key={user.id}>
|
|
<TableCell>
|
|
{user.first_name} {user.last_name}
|
|
{user.is_admin && (
|
|
<Chip
|
|
size="small"
|
|
label="Admin"
|
|
color="secondary"
|
|
sx={{ ml: 1 }}
|
|
/>
|
|
)}
|
|
{user.is_super_admin && (
|
|
<Chip
|
|
size="small"
|
|
label="Super Admin"
|
|
color="primary"
|
|
sx={{ ml: 1 }}
|
|
/>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>{user.email}</TableCell>
|
|
<TableCell>
|
|
{user.is_disabled ? (
|
|
<Chip
|
|
icon={<DisabledIcon />}
|
|
label="Disabled"
|
|
color="error"
|
|
size="small"
|
|
/>
|
|
) : (
|
|
<Chip
|
|
icon={<ActiveIcon />}
|
|
label="Active"
|
|
color="success"
|
|
size="small"
|
|
/>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>{formatDate(user.last_login)}</TableCell>
|
|
<TableCell>{formatDate(user.created_at)}</TableCell>
|
|
<TableCell>
|
|
{user.internal_notes ? (
|
|
<Tooltip title={user.internal_notes}>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{
|
|
maxWidth: 150,
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap'
|
|
}}
|
|
>
|
|
{user.internal_notes}
|
|
</Typography>
|
|
</Tooltip>
|
|
) : (
|
|
<Typography variant="body2" color="text.secondary">
|
|
No notes
|
|
</Typography>
|
|
)}
|
|
</TableCell>
|
|
<TableCell align="right">
|
|
<IconButton
|
|
onClick={() => handleOpenEditDialog(user)}
|
|
color="primary"
|
|
>
|
|
<EditIcon />
|
|
</IconButton>
|
|
<IconButton
|
|
onClick={() => handleOpenEmailDialog(user)}
|
|
color="primary"
|
|
>
|
|
<MailIcon />
|
|
</IconButton>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell colSpan={7} align="center">
|
|
<Typography variant="body1" py={2}>
|
|
{search ? 'No customers match your search.' : 'No customers found.'}
|
|
</Typography>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
<TablePagination
|
|
rowsPerPageOptions={[5, 10, 25, 50]}
|
|
component="div"
|
|
count={filteredUsers.length}
|
|
rowsPerPage={rowsPerPage}
|
|
page={page}
|
|
onPageChange={handleChangePage}
|
|
onRowsPerPageChange={handleChangeRowsPerPage}
|
|
/>
|
|
</Paper>
|
|
|
|
{/* Edit User Dialog */}
|
|
<Dialog open={editDialogOpen} onClose={handleCloseEditDialog} maxWidth="sm" fullWidth>
|
|
<DialogTitle>
|
|
{currentUser && `Edit Customer: ${currentUser.first_name} ${currentUser.last_name}`}
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
{currentUser && (
|
|
<Box sx={{ pt: 1 }}>
|
|
<Typography variant="subtitle1" gutterBottom>
|
|
Email: {currentUser.email}
|
|
</Typography>
|
|
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={formData.is_disabled}
|
|
onChange={handleFormChange}
|
|
disabled={userData?.id === currentUser.id}
|
|
name="is_disabled"
|
|
color="error"
|
|
/>
|
|
}
|
|
label={`${formData.is_disabled ? "Account is disabled" : "Account is active"}` + `${userData?.id === currentUser.id && formData.is_admin? " (Current user can\'t disabled themselves)" : "" }`}
|
|
sx={{ my: 2, display: 'block' }}
|
|
/>
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={formData.is_admin}
|
|
onChange={handleFormChange}
|
|
disabled={userData?.id === currentUser.id && formData.is_admin}
|
|
name="is_admin"
|
|
color="error"
|
|
/>
|
|
}
|
|
label={`${formData.is_admin ? "Account is Admin" : "Account is not Admin"}` + `${userData?.id === currentUser.id && formData.is_admin? " (Admin can't downgrade themselves)" : "" }`}
|
|
sx={{ my: 2, display: 'block' }}
|
|
/>
|
|
|
|
|
|
<TextField
|
|
autoFocus
|
|
name="internal_notes"
|
|
label="Internal Notes"
|
|
fullWidth
|
|
multiline
|
|
rows={4}
|
|
value={formData.internal_notes}
|
|
onChange={handleFormChange}
|
|
placeholder="Add internal notes about this customer (not visible to the customer)"
|
|
variant="outlined"
|
|
sx={{ mt: 2 }}
|
|
/>
|
|
|
|
<DialogContentText variant="caption" sx={{ mt: 1 }}>
|
|
Last login: {formatDate(currentUser.last_login)}
|
|
</DialogContentText>
|
|
</Box>
|
|
)}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={handleCloseEditDialog}>Cancel</Button>
|
|
<Button
|
|
onClick={handleSaveUser}
|
|
variant="contained"
|
|
disabled={updateUserMutation.isLoading}
|
|
>
|
|
{updateUserMutation.isLoading ? <CircularProgress size={24} /> : 'Save Changes'}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
{/* Email Dialog */}
|
|
<EmailDialog
|
|
open={emailDialogOpen}
|
|
onClose={handleCloseEmailDialog}
|
|
recipient={emailRecipient}
|
|
/>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default AdminCustomersPage; |