fixed user manage panel and updated support tickets to be deleted

This commit is contained in:
nvrl committed 2026-02-18 03:20:10 +01:00
1 parent 1624988727
commit 20e9d8ba1f
4 files changed
+129 -62

No files matched your search

+5
View File
@@ -598,6 +598,11 @@ class TicketViewSet(viewsets.ModelViewSet):
return Response({'status': 'updated'})
return Response({'error': 'Invalid status'}, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, *args, **kwargs):
if not request.user.is_superuser:
raise permissions.PermissionDenied("Only admins can delete tickets.")
return super().destroy(request, *args, **kwargs)
class AdminViewSet(viewsets.ViewSet):
permission_classes = [permissions.IsAdminUser]
+105 -61
View File
@@ -1,12 +1,14 @@
import React, { useState, useEffect } from 'react';
import {
Container, Typography, Box, Card, CardContent, Button,
Stack, alpha, CircularProgress, Chip, Paper, List, ListItem, ListItemText,
Avatar, Grid, TextField
Stack, alpha, useTheme, CircularProgress, Chip, Paper, List, ListItem, ListItemText,
Avatar, Grid, TextField, Tabs, Tab, Divider
} from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import DeleteIcon from '@mui/icons-material/Delete';
import SecurityIcon from '@mui/icons-material/Security';
import HistoryIcon from '@mui/icons-material/History';
import RateReviewIcon from '@mui/icons-material/RateReview';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom';
import api from '../api';
@@ -30,9 +32,11 @@ interface AdminUser {
const AdminUserDetail: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const theme = useTheme();
const { id } = useParams<{ id: string }>();
const [user, setUser] = useState<AdminUser | null>(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState(0);
// Edit state for Ratings
const [editingRatingId, setEditingRatingId] = useState<number | null>(null);
@@ -49,7 +53,7 @@ const AdminUserDetail: React.FC = () => {
setUser(res.data);
} catch (err) {
console.error(err);
navigate('/admin');
navigate('/admin-panel');
} finally {
setLoading(false);
}
@@ -105,7 +109,7 @@ const AdminUserDetail: React.FC = () => {
if (!window.confirm('PERMANENTLY DELETE THIS USER AND ALL DATA? This cannot be undone.')) return;
try {
await api.delete(`admin-custom/${id}/user_detail/`);
navigate('/admin');
navigate('/admin-panel');
} catch (err) { alert('Delete failed'); }
};
@@ -117,81 +121,67 @@ const AdminUserDetail: React.FC = () => {
<Button
variant="outlined"
startIcon={<ArrowBackIcon />}
onClick={() => navigate('/admin')}
onClick={() => navigate('/admin-panel')}
sx={{ mb: 4, borderRadius: 2 }}
>
{t('common.back')}
</Button>
{/* Profile Header */}
<Card elevation={0} sx={{ borderRadius: 4, border: '1px solid', borderColor: 'divider', mb: 4 }}>
<CardContent sx={{ p: 4 }}>
<Stack direction="row" spacing={3} alignItems="center">
<Avatar sx={{ width: 80, height: 80, fontSize: '2rem', bgcolor: 'primary.main' }}>
<CardContent sx={{ p: { xs: 2, md: 4 } }}>
<Grid container spacing={3} alignItems="center">
<Grid size={{ xs: 12, sm: 'auto' }}>
<Avatar sx={{ width: 80, height: 80, fontSize: '2rem', bgcolor: 'primary.main', mx: 'auto' }}>
{user.username.charAt(0).toUpperCase()}
</Avatar>
<Box sx={{ flex: 1 }}>
</Grid>
<Grid size={{ xs: 12, sm: 8, md: 9 }}>
<Box sx={{ textAlign: { xs: 'center', sm: 'left' } }}>
<Typography variant="h4" sx={{ fontWeight: 'bold' }}>{user.username}</Typography>
<Typography variant="body1" color="text.secondary">{user.email}</Typography>
<Typography variant="caption" color="text.secondary" display="block">
<Typography variant="caption" color="text.secondary" display="block" sx={{ mt: 0.5 }}>
Joined: {formatDate(user.date_joined)} • Last Login: {formatDate(user.last_login)}
</Typography>
<Stack direction="row" spacing={1} sx={{ mt: 1 }}>
<Stack direction="row" spacing={1} sx={{ mt: 1.5, justifyContent: { xs: 'center', sm: 'flex-start' } }}>
<Chip label={user.is_active ? 'Active' : 'Inactive'} color={user.is_active ? 'success' : 'default'} size="small" />
{user.is_superuser && <Chip label="Superuser" color="secondary" size="small" />}
</Stack>
</Box>
</Grid>
<Grid size={{ xs: 12, sm: 'auto' }}>
<Button
variant="outlined"
fullWidth
variant="contained"
color={user.is_active ? "warning" : "success"}
onClick={handleToggleActive}
sx={{ borderRadius: 2, textTransform: 'none', fontWeight: 'bold' }}
>
{user.is_active ? t('admin.deactivate') : t('admin.activate')}
</Button>
</Stack>
</Grid>
</Grid>
</CardContent>
</Card>
<Grid container spacing={4}>
<Grid size={{ xs: 12, md: 4 }}>
<Stack spacing={4}>
<Paper sx={{ p: 3, borderRadius: 3 }}>
<Typography variant="h6" gutterBottom sx={{ fontWeight: 'bold', display: 'flex', alignItems: 'center', gap: 1 }}>
<SecurityIcon /> {t('admin.ipHistory')}
</Typography>
<List dense>
{user.ips.map((ip, idx) => (
<ListItem key={idx}>
<ListItemText primary={ip} />
</ListItem>
))}
{user.ips.length === 0 && <Typography variant="caption">No IP history available.</Typography>}
</List>
</Paper>
{/* Content Tabs */}
<Box sx={{ mb: 4 }}>
<Tabs value={activeTab} onChange={(_, v) => setActiveTab(v)} variant="fullWidth">
<Tab icon={<RateReviewIcon />} label={`Content (${user.ratings.length})`} sx={{ fontWeight: 'bold' }} />
<Tab icon={<HistoryIcon />} label="Security & IP" sx={{ fontWeight: 'bold' }} />
<Tab icon={<SecurityIcon />} label="Danger Zone" sx={{ fontWeight: 'bold', color: 'error.main' }} />
</Tabs>
<Divider />
</Box>
<Paper sx={{ p: 3, borderRadius: 3, border: '1px solid', borderColor: 'error.main', bgcolor: (theme) => alpha(theme.palette.error.main, 0.05) }}>
<Typography variant="h6" color="error" gutterBottom sx={{ fontWeight: 'bold' }}>{t('admin.dangerZone')}</Typography>
<Typography variant="body2" sx={{ mb: 2 }}>
Deleting this user will permanently remove their profile, ratings, replies, and all associated metadata.
</Typography>
<Button
variant="contained"
color="error"
startIcon={<DeleteIcon />}
onClick={handleDelete}
fullWidth
>
{t('admin.deleteUser')}
</Button>
</Paper>
</Stack>
</Grid>
<Grid size={{ xs: 12, md: 8 }}>
<Typography variant="h5" sx={{ mb: 3, fontWeight: 'bold' }}>User Content</Typography>
{/* TAB 0: USER CONTENT (Ratings & Replies) */}
{activeTab === 0 && (
<Box>
<Typography variant="h5" sx={{ mb: 3, fontWeight: 'bold' }}>User Ratings & Discussion</Typography>
<Stack spacing={2}>
{user.ratings.map(rating => (
<Card key={rating.id} variant="outlined" sx={{ borderRadius: 3 }}>
<CardContent>
<Card key={rating.id} variant="outlined" sx={{ borderRadius: 3, bgcolor: alpha(theme.palette.background.paper, 0.4) }}>
<CardContent sx={{ p: { xs: 2, md: 3 } }}>
{editingRatingId === rating.id ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Stack direction="row" spacing={2} alignItems="center">
@@ -204,7 +194,8 @@ const AdminUserDetail: React.FC = () => {
/>
</Stack>
<MentionTextField
multiline rows={3} value={editComment}
multiline rows={3}
value={editComment}
onChange={setEditComment}
/>
<Stack direction="row" spacing={1}>
@@ -217,17 +208,23 @@ const AdminUserDetail: React.FC = () => {
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" sx={{ mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold' }}>{rating.flavor_name}</Typography>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<Chip label={`${rating.score}/10`} size="small" color="primary" />
<Chip label={`${rating.score}/10`} size="small" color="primary" sx={{ fontWeight: '900' }} />
<Button size="small" onClick={() => { setEditingRatingId(rating.id); setEditScore(rating.score); setEditComment(rating.comment || ''); }}>Edit</Button>
<Button size="small" color="error" onClick={() => handleDeleteRating(rating.id)}>Delete</Button>
</Box>
</Stack>
<Typography variant="body2" color="text.primary" paragraph sx={{ bgcolor: 'action.hover', p: 1.5, borderRadius: 2 }}>
{rating.comment ? <RichText text={rating.comment} /> : <i>No comment</i>}
<Box sx={{ bgcolor: 'action.hover', p: 1.5, borderRadius: 2, mb: 2 }}>
{rating.comment ? (
<Typography variant="body2" color="text.primary">
<RichText text={rating.comment} />
</Typography>
) : (
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>No comment</Typography>
)}
</Box>
{rating.replies.length > 0 && (
<Box sx={{ mt: 2, pl: 2, borderLeft: '2px solid', borderColor: 'divider' }}>
<Box sx={{ pl: 2, borderLeft: '2px solid', borderColor: 'divider' }}>
{rating.replies.map((reply: any) => (
<Box key={reply.id} sx={{ mb: 1.5 }}>
{editingReplyId === reply.id ? (
@@ -241,13 +238,15 @@ const AdminUserDetail: React.FC = () => {
) : (
<>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="caption" sx={{ fontWeight: 'bold' }}>{reply.user}</Typography>
<Typography variant="caption" sx={{ fontWeight: 'bold', color: reply.user === user.username ? 'primary.main' : 'text.secondary' }}>
{reply.user}
</Typography>
<Box sx={{ display: 'flex', gap: 0.5 }}>
<Button size="small" sx={{ minWidth: 0, p: 0, fontSize: '0.7rem' }} onClick={() => { setEditingReplyId(reply.id); setEditReplyText(reply.text); }}>Edit</Button>
<Button size="small" color="error" sx={{ minWidth: 0, p: 0, fontSize: '0.7rem' }} onClick={() => handleDeleteReply(reply.id)}>Delete</Button>
</Box>
</Stack>
<Typography variant="caption" display="block"><RichText text={reply.text} /></Typography>
<Typography variant="caption" display="block" sx={{ mt: 0.2 }}><RichText text={reply.text} /></Typography>
</>
)}
</Box>
@@ -259,10 +258,55 @@ const AdminUserDetail: React.FC = () => {
</CardContent>
</Card>
))}
{user.ratings.length === 0 && <Typography color="text.secondary">No ratings yet.</Typography>}
{user.ratings.length === 0 && (
<Box sx={{ py: 6, textAlign: 'center', bgcolor: 'action.hover', borderRadius: 3, border: '1px dashed', borderColor: 'divider' }}>
<Typography color="text.secondary">No ratings yet.</Typography>
</Box>
)}
</Stack>
</Grid>
</Grid>
</Box>
)}
{/* TAB 1: SECURITY & IP */}
{activeTab === 1 && (
<Paper sx={{ p: 3, borderRadius: 3, border: '1px solid', borderColor: 'divider' }}>
<Typography variant="h6" gutterBottom sx={{ fontWeight: 'bold', display: 'flex', alignItems: 'center', gap: 1 }}>
<SecurityIcon /> {t('admin.ipHistory')}
</Typography>
<List dense>
{user.ips.map((ip, idx) => (
<ListItem key={idx} divider={idx < user.ips.length - 1}>
<ListItemText
primary={ip}
primaryTypographyProps={{ sx: { fontWeight: 'bold', fontFamily: 'monospace' } }}
/>
</ListItem>
))}
{user.ips.length === 0 && <Typography variant="caption">No IP history available.</Typography>}
</List>
</Paper>
)}
{/* TAB 2: DANGER ZONE */}
{activeTab === 2 && (
<Paper sx={{ p: 4, borderRadius: 3, border: '1px solid', borderColor: 'error.main', bgcolor: (theme) => alpha(theme.palette.error.main, 0.05) }}>
<Typography variant="h5" color="error" gutterBottom sx={{ fontWeight: 'bold' }}>{t('admin.dangerZone')}</Typography>
<Typography variant="body1" sx={{ mb: 4, maxWidth: 600 }}>
Deleting this user will permanently remove their profile, all their ratings, replies, and IP history.
<strong> This action is irreversible.</strong>
</Typography>
<Button
variant="contained"
color="error"
size="large"
startIcon={<DeleteIcon />}
onClick={handleDelete}
sx={{ borderRadius: 2, fontWeight: 'bold', px: 4 }}
>
{t('admin.deleteUser')}
</Button>
</Paper>
)}
</Container>
);
};
+1 -1
View File
@@ -177,7 +177,7 @@ const PublicProfile: React.FC<PublicProfileProps> = ({ adminMode }) => {
<Button
variant="outlined"
color="secondary"
onClick={() => navigate(`/admin/user/${data.id}`)}
onClick={() => navigate(`/admin-panel/user/${data.id}`)}
sx={{ borderRadius: 4, px: 4 }}
>
{t('admin.manageUser')}
+18
View File
@@ -6,6 +6,7 @@ import {
} from '@mui/material';
import SendIcon from '@mui/icons-material/Send';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import { useTranslation } from 'react-i18next';
import api from '../api';
import { useTitle } from '../hooks/useTitle';
@@ -98,6 +99,16 @@ const Support: React.FC = () => {
}
};
const handleDeleteTicket = async (ticketId: number) => {
if (!confirm('Delete this ticket?')) return;
try {
await api.delete(`tickets/${ticketId}/`);
fetchTickets();
} catch (err) {
alert('Failed to delete ticket');
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'open': return 'error';
@@ -169,7 +180,14 @@ const Support: React.FC = () => {
</Typography>
<Typography variant="caption" color="text.secondary">{formatDate(ticket.created_at)}</Typography>
</Box>
<Stack direction="row" spacing={1} alignItems="center">
<Chip label={t(`support.${ticket.status}`)} color={getStatusColor(ticket.status) as any} size="small" sx={{ fontWeight: 'bold' }} />
{currentUser?.is_superuser && (
<IconButton size="small" color="error" onClick={(e) => { e.stopPropagation(); handleDeleteTicket(ticket.id); }}>
<DeleteIcon fontSize="small" />
</IconButton>
)}
</Stack>
</Box>
<Collapse in={expandedId === ticket.id}>