E-Commerce-Module/frontend/src/pages/Admin/CustomersPage.jsx

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;