Files
reactShop/src/pages/UsersPage.js
2025-07-02 12:49:06 +02:00

719 lines
25 KiB
JavaScript

import React from 'react';
import {
Container,
Typography,
Paper,
Box,
Divider,
Grid,
Card,
CardContent,
List,
ListItem,
ListItemText,
Chip,
Avatar,
Tabs,
Tab,
Stack,
Button,
Snackbar,
Alert,
Link as MuiLink
} from '@mui/material';
import { Navigate, Link } from 'react-router-dom';
import PersonIcon from '@mui/icons-material/Person';
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
import GroupIcon from '@mui/icons-material/Group';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import { ADMIN_COLORS, getAdminStyles } from '../theme/adminColors.js';
class UsersPage extends React.Component {
constructor(props) {
super(props);
this.state = {
user: null,
users: [],
totalCount: 0,
totalOrders: 0,
loading: true,
redirect: false,
switchingUser: false,
notification: {
open: false,
message: '',
severity: 'success'
},
currentlyImpersonating: null
};
}
checkUserLoggedIn = () => {
const storedUser = sessionStorage.getItem('user');
if (!storedUser) {
this.setState({ redirect: true, user: null });
return;
}
try {
const userData = JSON.parse(storedUser);
if (!userData) {
this.setState({ redirect: true, user: null });
} else if (!this.state.user) {
// Only update user if it's not already set
this.setState({ user: userData, loading: false });
}
} catch (error) {
console.error('Error parsing user from sessionStorage:', error);
this.setState({ redirect: true, user: null });
}
// Once loading is complete
if (this.state.loading) {
this.setState({ loading: false });
}
}
handleStorageChange = (e) => {
if (e.key === 'user' && !e.newValue) {
// User was removed from sessionStorage in another tab
this.setState({ redirect: true, user: null });
}
}
componentDidMount() {
this.loadInitialData();
this.checkUserLoggedIn();
// Set up interval to regularly check login status
this.checkLoginInterval = setInterval(this.checkUserLoggedIn, 1000);
// Add storage event listener to detect when user logs out in other tabs
window.addEventListener('storage', this.handleStorageChange);
}
componentDidUpdate(prevProps) {
// Handle socket connection changes
const wasConnected = prevProps.socket && prevProps.socket.connected;
const isNowConnected = this.props.socket && this.props.socket.connected;
if (!wasConnected && isNowConnected) {
// Socket just connected, reload data
this.loadInitialData();
}
}
componentWillUnmount() {
// Clear interval and remove event listeners
if (this.checkLoginInterval) {
clearInterval(this.checkLoginInterval);
}
window.removeEventListener('storage', this.handleStorageChange);
}
loadInitialData = () => {
if (this.props.socket && this.props.socket.connected) {
this.props.socket.emit('getUsers', (response) => {
if (response.success) {
console.log('Users:', response.data.users);
console.log('Total count:', response.data.totalCount);
console.log('Total orders:', response.data.totalOrders);
this.setState({
users: response.data.users,
totalCount: response.data.totalCount,
totalOrders: response.data.totalOrders
});
} else {
console.error('Error:', response.error);
}
});
}
}
formatDate = (dateString) => {
try {
const date = new Date(dateString);
// Check if date is valid
if (isNaN(date.getTime())) {
return dateString; // Return original string if date is invalid
}
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch (error) {
console.error('Error formatting date:', error);
return dateString; // Return original string if formatting fails
}
}
getOrderStatusColor = (status) => {
switch (status) {
case 'completed':
case 'delivered':
return 'success';
case 'pending':
return 'warning';
case 'processing':
case 'shipped':
return 'info';
case 'cancelled':
return 'error';
default:
return 'default';
}
}
getOrderStatusChipColor = (status) => {
switch (status) {
case 'completed':
case 'delivered':
return ADMIN_COLORS.primary;
case 'pending':
return ADMIN_COLORS.warning;
case 'processing':
case 'shipped':
return ADMIN_COLORS.secondary;
case 'cancelled':
return ADMIN_COLORS.error;
default:
return ADMIN_COLORS.secondaryText;
}
}
formatPrice = (price) => {
return typeof price === 'number'
? `${price.toFixed(2)}`
: price;
}
handleSwitchUser = (email) => {
if (!this.props.socket || !this.props.socket.connected) {
this.showNotification('Socket not connected', 'error');
return;
}
this.setState({ switchingUser: true });
this.props.socket.emit('switchUser', { email }, (response) => {
console.log('Switch user response:', response);
this.setState({ switchingUser: false });
if (response.success) {
this.setState({ currentlyImpersonating: response.data.targetUser });
this.showNotification(`Successfully switched to user: ${email}`, 'success');
// Update sessionStorage with the switched user info
const currentUser = JSON.parse(sessionStorage.getItem('user') || '{}');
const switchedUser = {
...currentUser,
id: response.data.targetUser.id,
email: response.data.targetUser.email,
admin: true, // Admin privileges are preserved
originalAdmin: response.data.originalAdmin
};
sessionStorage.setItem('user', JSON.stringify(switchedUser));
// Trigger userLoggedIn event to refresh other components
window.dispatchEvent(new Event('userLoggedIn'));
} else {
this.showNotification(`Failed to switch user: ${response.error}`, 'error');
}
});
}
handleSwitchBackToAdmin = () => {
if (!this.props.socket || !this.props.socket.connected) {
this.showNotification('Socket not connected', 'error');
return;
}
this.setState({ switchingUser: true });
this.props.socket.emit('switchBackToAdmin', (response) => {
console.log('Switch back to admin response:', response);
this.setState({ switchingUser: false });
if (response.success) {
this.setState({ currentlyImpersonating: null });
this.showNotification(`Switched back to admin`, 'success');
// Restore original admin info in sessionStorage
const currentUser = JSON.parse(sessionStorage.getItem('user') || '{}');
if (currentUser.originalAdmin) {
const restoredAdmin = {
...currentUser,
id: currentUser.originalAdmin.id,
email: currentUser.originalAdmin.email,
admin: true
};
delete restoredAdmin.originalAdmin;
sessionStorage.setItem('user', JSON.stringify(restoredAdmin));
}
// Trigger userLoggedIn event to refresh other components
window.dispatchEvent(new Event('userLoggedIn'));
} else {
this.showNotification(`Failed to switch back: ${response.error}`, 'error');
}
});
}
showNotification = (message, severity = 'success') => {
console.log('Showing notification:', message, severity);
this.setState({
notification: {
open: true,
message,
severity
}
});
}
handleCloseNotification = () => {
this.setState({
notification: {
...this.state.notification,
open: false
}
});
}
render() {
const { users, totalCount, totalOrders } = this.state;
if (this.state.redirect || (!this.state.loading && !this.state.user)) {
return <Navigate to="/" />;
}
// Check if current user is admin
if (this.state.user && !this.state.user.admin) {
return <Navigate to="/" />;
}
const hasUsers = users && users.length > 0;
const styles = getAdminStyles();
return (
<Box sx={styles.pageContainer}>
<Container
maxWidth="lg"
sx={{
py: 6
}}
>
{/* Admin Navigation Tabs */}
<Paper
elevation={1}
sx={{
mb: 3,
...styles.tabBar
}}
>
<Tabs
value={1}
sx={{
px: 2,
'& .MuiTabs-indicator': {
backgroundColor: ADMIN_COLORS.primary
}
}}
>
<Tab
label="Dashboard"
component={Link}
to="/admin"
sx={{
textTransform: 'none',
color: ADMIN_COLORS.primaryText,
fontFamily: ADMIN_COLORS.fontFamily,
'&:hover': {
color: ADMIN_COLORS.primaryBright
}
}}
/>
<Tab
label="Users"
component={Link}
to="/admin/users"
sx={{
textTransform: 'none',
fontWeight: 'bold',
color: ADMIN_COLORS.primary,
fontFamily: ADMIN_COLORS.fontFamily
}}
/>
<Tab
label="Server Logs"
component={Link}
to="/admin/logs"
sx={{
textTransform: 'none',
color: ADMIN_COLORS.primaryText,
fontFamily: ADMIN_COLORS.fontFamily,
'&:hover': {
color: ADMIN_COLORS.primaryBright
}
}}
/>
</Tabs>
</Paper>
<Paper
elevation={3}
sx={{
p: 3,
mb: 4,
...styles.contentPaper
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography
variant="h5"
sx={{
display: 'flex',
alignItems: 'center',
...styles.primaryHeading
}}
>
<GroupIcon sx={{ mr: 1, color: ADMIN_COLORS.primary }} />
User Management
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{this.state.currentlyImpersonating && (
<>
<Chip
label={`Impersonating: ${this.state.currentlyImpersonating.email}`}
size="small"
sx={{
fontWeight: 'medium',
backgroundColor: ADMIN_COLORS.magenta,
color: ADMIN_COLORS.hoverBackground,
fontFamily: ADMIN_COLORS.fontFamily
}}
/>
<Button
variant="outlined"
size="small"
onClick={this.handleSwitchBackToAdmin}
disabled={this.state.switchingUser}
sx={{
textTransform: 'none',
color: ADMIN_COLORS.primaryText,
borderColor: ADMIN_COLORS.border,
fontFamily: ADMIN_COLORS.fontFamily,
'&:hover': {
borderColor: ADMIN_COLORS.primary,
backgroundColor: 'rgba(80, 250, 123, 0.1)'
}
}}
>
Switch Back to Admin
</Button>
</>
)}
</Box>
</Box>
<Stack direction="row" spacing={4} sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<GroupIcon sx={{ mr: 1, color: ADMIN_COLORS.secondary }} />
<Typography
variant="subtitle1"
sx={{
...styles.primaryText
}}
>
Total Users: <strong style={{ color: ADMIN_COLORS.warning }}>{totalCount}</strong>
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<ShoppingCartIcon sx={{ mr: 1, color: ADMIN_COLORS.secondary }} />
<Typography
variant="subtitle1"
sx={{
...styles.primaryText
}}
>
Total Orders: <strong style={{ color: ADMIN_COLORS.warning }}>{totalOrders}</strong>
</Typography>
</Box>
</Stack>
{!hasUsers && (
<Typography
variant="body1"
sx={{
mt: 2,
...styles.secondaryText
}}
>
No users found.
</Typography>
)}
{hasUsers && (
<Grid container spacing={3} sx={{ mt: 1 }}>
{users.map((user, i) => (
<Grid size={{ xs: 12, md: 6, lg: 4 }} key={user.id || i}>
<Card
variant="outlined"
sx={{
height: '100%',
...styles.card
}}
>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2, width: '100%' }}>
<Avatar sx={{
mr: 2,
bgcolor: user.admin ? ADMIN_COLORS.magenta : ADMIN_COLORS.secondary,
color: ADMIN_COLORS.hoverBackground
}}>
{user.admin ? <AdminPanelSettingsIcon /> : <PersonIcon />}
</Avatar>
<Box sx={{ flexGrow: 1, minWidth: 0, overflow: 'hidden' }}>
<Typography
variant="h6"
component="div"
noWrap
sx={{
...styles.primaryHeading
}}
>
User #{user.id}
</Typography>
<MuiLink
component="button"
onClick={() => this.handleSwitchUser(user.email)}
disabled={this.state.switchingUser}
sx={{
color: ADMIN_COLORS.secondary,
textDecoration: 'underline',
textDecorationColor: 'transparent',
cursor: 'pointer',
fontSize: '0.875rem',
fontWeight: 'medium',
border: 'none',
background: 'none',
padding: 0,
textAlign: 'left',
display: 'block',
width: '100%',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
fontFamily: ADMIN_COLORS.fontFamily,
'&:hover': {
textDecorationColor: ADMIN_COLORS.secondary,
color: ADMIN_COLORS.primaryBright
},
'&:disabled': {
color: ADMIN_COLORS.secondaryText,
cursor: 'not-allowed'
}
}}
title="Click to switch to this user"
>
{user.email}
</MuiLink>
</Box>
{user.admin == true&& (
<Box sx={{ flexShrink: 0, ml: 1 }}>
<Chip
label="Admin"
size="small"
sx={{
backgroundColor: ADMIN_COLORS.magenta,
color: ADMIN_COLORS.hoverBackground,
fontFamily: ADMIN_COLORS.fontFamily,
fontWeight: 'bold'
}}
/>
</Box>
)}
</Box>
<Divider sx={{ mb: 2, borderColor: ADMIN_COLORS.border }} />
<List disablePadding>
<ListItem sx={{ py: 0.5, px: 0 }}>
<ListItemText
primary="Status"
secondary={user.admin ? "Administrator" : "User"}
primaryTypographyProps={{
fontSize: '0.875rem',
fontWeight: 'medium',
color: ADMIN_COLORS.primaryText,
fontFamily: ADMIN_COLORS.fontFamily
}}
secondaryTypographyProps={{
color: user.admin ? ADMIN_COLORS.magenta : ADMIN_COLORS.secondary,
fontWeight: 'medium',
fontFamily: ADMIN_COLORS.fontFamily
}}
/>
</ListItem>
<ListItem sx={{ py: 0.5, px: 0 }}>
<ListItemText
primary="Created"
secondary={this.formatDate(user.created_at)}
primaryTypographyProps={{
fontSize: '0.875rem',
fontWeight: 'medium',
color: ADMIN_COLORS.primaryText,
fontFamily: ADMIN_COLORS.fontFamily
}}
secondaryTypographyProps={{
color: ADMIN_COLORS.warning,
fontFamily: ADMIN_COLORS.fontFamily
}}
/>
</ListItem>
<ListItem sx={{ py: 0.5, px: 0 }}>
<ListItemText
primary="Orders"
secondary={`${user.orderCount || 0} total`}
primaryTypographyProps={{
fontSize: '0.875rem',
fontWeight: 'medium',
color: ADMIN_COLORS.primaryText,
fontFamily: ADMIN_COLORS.fontFamily
}}
secondaryTypographyProps={{
color: ADMIN_COLORS.warning,
fontFamily: ADMIN_COLORS.fontFamily
}}
/>
</ListItem>
</List>
{/* All Orders */}
{user.orders && user.orders.length > 0 && (
<>
<Divider sx={{ my: 2, borderColor: ADMIN_COLORS.border }} />
<Typography
variant="subtitle2"
sx={{
mb: 1,
...styles.primaryHeading
}}
>
Orders
</Typography>
<List disablePadding>
{user.orders
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)) // Sort by newest first
.map((order, orderIndex) => (
<ListItem key={order.orderId || orderIndex} sx={{ py: 0.5, px: 0 }}>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography
variant="caption"
sx={{
fontWeight: 'medium',
color: ADMIN_COLORS.primaryText,
fontFamily: ADMIN_COLORS.fontFamily
}}
>
{order.orderId || 'N/A'}
</Typography>
<Chip
label={order.status || 'unknown'}
size="small"
sx={{
fontSize: '0.65rem',
height: 'auto',
py: 0.25,
backgroundColor: this.getOrderStatusChipColor(order.status),
color: ADMIN_COLORS.hoverBackground,
fontFamily: ADMIN_COLORS.fontFamily,
fontWeight: 'bold'
}}
/>
</Box>
}
secondary={
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 0.5 }}>
<Typography
variant="caption"
sx={{
color: ADMIN_COLORS.warning,
fontFamily: ADMIN_COLORS.fontFamily
}}
>
{this.formatDate(order.created_at)}
</Typography>
<Typography
variant="caption"
sx={{
color: ADMIN_COLORS.primaryBright,
fontWeight: 'medium',
fontFamily: ADMIN_COLORS.fontFamily
}}
>
{order.totalCost ? this.formatPrice(order.totalCost) : 'N/A'}
</Typography>
</Box>
}
/>
</ListItem>
))}
</List>
</>
)}
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
</Paper>
{/* Notification Snackbar */}
<Snackbar
open={this.state.notification.open}
autoHideDuration={6000}
onClose={this.handleCloseNotification}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
>
<Alert
onClose={this.handleCloseNotification}
severity={this.state.notification.severity}
sx={{
width: '100%',
backgroundColor: ADMIN_COLORS.surfaceBackground,
border: `1px solid ${ADMIN_COLORS.border}`,
color: ADMIN_COLORS.primaryText,
fontFamily: ADMIN_COLORS.fontFamily,
'& .MuiAlert-icon': {
color: this.state.notification.severity === 'success'
? ADMIN_COLORS.primary
: this.state.notification.severity === 'error'
? ADMIN_COLORS.error
: ADMIN_COLORS.warning
},
'& .MuiAlert-action': {
color: ADMIN_COLORS.primaryText
}
}}
>
{this.state.notification.message}
</Alert>
</Snackbar>
</Container>
</Box>
);
}
}
export default UsersPage;