mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
Merge branch 'main' into narl.io
This commit is contained in:
commit
f794529636
7 files changed
+165
-62
No files matched your search
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 6.0.2 on 2026-02-18 02:55
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('api', '0015_ticket_ticketmessage_userip'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='notification',
|
||||||
|
name='ticket',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.ticket'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='notification',
|
||||||
|
name='notification_type',
|
||||||
|
field=models.CharField(choices=[('reply', 'Reply'), ('mention', 'Mention'), ('ticket_new', 'New Ticket'), ('ticket_reply', 'Ticket Reply')], max_length=20),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -124,11 +124,12 @@ class Notification(models.Model):
|
|||||||
TYPE_CHOICES = [
|
TYPE_CHOICES = [
|
||||||
('reply', 'Reply'),
|
('reply', 'Reply'),
|
||||||
('mention', 'Mention'),
|
('mention', 'Mention'),
|
||||||
('ticket', 'Ticket Update'),
|
('ticket_new', 'New Ticket'),
|
||||||
|
('ticket_reply', 'Ticket Reply'),
|
||||||
]
|
]
|
||||||
recipient = models.ForeignKey(User, related_name='notifications', on_delete=models.CASCADE)
|
recipient = models.ForeignKey(User, related_name='notifications', on_delete=models.CASCADE)
|
||||||
actor = models.ForeignKey(User, related_name='actions', on_delete=models.CASCADE)
|
actor = models.ForeignKey(User, related_name='actions', on_delete=models.CASCADE)
|
||||||
notification_type = models.CharField(max_length=10, choices=TYPE_CHOICES)
|
notification_type = models.CharField(max_length=20, choices=TYPE_CHOICES)
|
||||||
rating = models.ForeignKey(Rating, on_delete=models.CASCADE, null=True, blank=True)
|
rating = models.ForeignKey(Rating, on_delete=models.CASCADE, null=True, blank=True)
|
||||||
reply = models.ForeignKey(Reply, on_delete=models.CASCADE, null=True, blank=True)
|
reply = models.ForeignKey(Reply, on_delete=models.CASCADE, null=True, blank=True)
|
||||||
ticket = models.ForeignKey('Ticket', on_delete=models.CASCADE, null=True, blank=True)
|
ticket = models.ForeignKey('Ticket', on_delete=models.CASCADE, null=True, blank=True)
|
||||||
|
|||||||
@@ -583,7 +583,7 @@ class TicketViewSet(viewsets.ModelViewSet):
|
|||||||
Notification.objects.create(
|
Notification.objects.create(
|
||||||
recipient=admin,
|
recipient=admin,
|
||||||
actor=self.request.user,
|
actor=self.request.user,
|
||||||
notification_type='ticket',
|
notification_type='ticket_new',
|
||||||
ticket=ticket
|
ticket=ticket
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -603,7 +603,7 @@ class TicketViewSet(viewsets.ModelViewSet):
|
|||||||
Notification.objects.create(
|
Notification.objects.create(
|
||||||
recipient=ticket.user,
|
recipient=ticket.user,
|
||||||
actor=request.user,
|
actor=request.user,
|
||||||
notification_type='ticket',
|
notification_type='ticket_reply',
|
||||||
ticket=ticket
|
ticket=ticket
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -613,7 +613,7 @@ class TicketViewSet(viewsets.ModelViewSet):
|
|||||||
Notification.objects.create(
|
Notification.objects.create(
|
||||||
recipient=admin,
|
recipient=admin,
|
||||||
actor=request.user,
|
actor=request.user,
|
||||||
notification_type='ticket',
|
notification_type='ticket_reply',
|
||||||
ticket=ticket
|
ticket=ticket
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+60
-44
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useMemo, Suspense, lazy } from 'react';
|
import React, { useState, useEffect, useMemo, Suspense, lazy } from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route, Link, useNavigate, useLocation } from 'react-router-dom';
|
import { Routes, Route, Link, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ThemeProvider,
|
ThemeProvider,
|
||||||
CssBaseline,
|
CssBaseline,
|
||||||
@@ -206,6 +206,7 @@ const GlobalSearch = () => {
|
|||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const { i18n, t } = useTranslation();
|
const { i18n, t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [themeName, setThemeName] = useState<CatppuccinTheme>((localStorage.getItem('theme') as CatppuccinTheme) || 'holy_light');
|
const [themeName, setThemeName] = useState<CatppuccinTheme>((localStorage.getItem('theme') as CatppuccinTheme) || 'holy_light');
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
const [notifAnchorEl, setNotifAnchorEl] = useState<null | HTMLElement>(null);
|
const [notifAnchorEl, setNotifAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
@@ -492,35 +493,52 @@ const App: React.FC = () => {
|
|||||||
<Typography variant="body2" color="text.secondary">No notifications yet</Typography>
|
<Typography variant="body2" color="text.secondary">No notifications yet</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
notifications.map(n => (
|
notifications.map(n => {
|
||||||
<MenuItem
|
let message = "";
|
||||||
key={n.id}
|
if (n.notification_type === 'reply') message = `replied to your review on ${n.flavor_name}`;
|
||||||
onClick={() => handleNotificationClick(n)}
|
else if (n.notification_type === 'mention') message = `mentioned you on ${n.flavor_name}`;
|
||||||
sx={{
|
else if (n.notification_type === 'ticket_new') message = t('community.notifTicketNew');
|
||||||
py: 1.5,
|
else if (n.notification_type === 'ticket_reply') message = user?.is_superuser ? t('community.notifTicketReplyAdmin') : t('community.notifTicketReply');
|
||||||
px: 2,
|
|
||||||
borderBottom: '1px solid',
|
const handleClick = () => {
|
||||||
borderColor: 'divider',
|
if (n.notification_type.startsWith('ticket')) {
|
||||||
bgcolor: n.is_read ? 'transparent' : alpha(theme.palette.primary.main, 0.05),
|
handleNotificationClick(n);
|
||||||
whiteSpace: 'normal',
|
navigate('/support');
|
||||||
display: 'flex',
|
} else {
|
||||||
gap: 2,
|
handleNotificationClick(n);
|
||||||
alignItems: 'flex-start'
|
}
|
||||||
}}
|
};
|
||||||
>
|
|
||||||
<Avatar src={n.actor_avatar || undefined} sx={{ width: 32, height: 32 }}>
|
return (
|
||||||
{n.actor_username.charAt(0).toUpperCase()}
|
<MenuItem
|
||||||
</Avatar>
|
key={n.id}
|
||||||
<Box sx={{ flex: 1 }}>
|
onClick={handleClick}
|
||||||
<Typography variant="body2" sx={{ lineHeight: 1.4 }}>
|
sx={{
|
||||||
<strong>{n.actor_username}</strong> {n.notification_type === 'reply' ? 'replied' : 'mentioned you'} on <strong>{n.flavor_name}</strong>
|
py: 1.5,
|
||||||
</Typography>
|
px: 2,
|
||||||
<Typography variant="caption" color="text.secondary">
|
borderBottom: '1px solid',
|
||||||
{formatDate(n.created_at)}
|
borderColor: 'divider',
|
||||||
</Typography>
|
bgcolor: n.is_read ? 'transparent' : alpha(theme.palette.primary.main, 0.05),
|
||||||
</Box>
|
whiteSpace: 'normal',
|
||||||
</MenuItem>
|
display: 'flex',
|
||||||
))
|
gap: 2,
|
||||||
|
alignItems: 'flex-start'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar src={n.actor_avatar || undefined} sx={{ width: 32, height: 32 }}>
|
||||||
|
{n.actor_username.charAt(0).toUpperCase()}
|
||||||
|
</Avatar>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Typography variant="body2" sx={{ lineHeight: 1.4 }}>
|
||||||
|
<strong>{n.actor_username}</strong> {message}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{formatDate(n.created_at)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
@@ -529,19 +547,18 @@ const App: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Router>
|
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh', width: '100%', overflowX: 'hidden' }}>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh', width: '100%', overflowX: 'hidden' }}>
|
<AppBar
|
||||||
<AppBar
|
position="sticky"
|
||||||
position="sticky"
|
elevation={0}
|
||||||
elevation={0}
|
sx={{
|
||||||
sx={{
|
borderBottom: '1px solid',
|
||||||
borderBottom: '1px solid',
|
borderColor: 'divider',
|
||||||
borderColor: 'divider',
|
bgcolor: (theme) => alpha(theme.palette.background.paper, 0.8),
|
||||||
bgcolor: (theme) => alpha(theme.palette.background.paper, 0.8),
|
backdropFilter: 'blur(12px)',
|
||||||
backdropFilter: 'blur(12px)',
|
color: 'text.primary'
|
||||||
color: 'text.primary'
|
}}
|
||||||
}}
|
>
|
||||||
>
|
|
||||||
<Toolbar sx={{ minHeight: { xs: 56, sm: 64 }, px: { xs: 1, sm: 4, md: 6 } }}>
|
<Toolbar sx={{ minHeight: { xs: 56, sm: 64 }, px: { xs: 1, sm: 4, md: 6 } }}>
|
||||||
<Typography variant="h6" component="div" sx={{ fontWeight: 'bold', mr: 2 }}>
|
<Typography variant="h6" component="div" sx={{ fontWeight: 'bold', mr: 2 }}>
|
||||||
<Link to="/" style={{ color: 'inherit', textDecoration: 'none' }}>
|
<Link to="/" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
@@ -678,7 +695,6 @@ const App: React.FC = () => {
|
|||||||
<Footer />
|
<Footer />
|
||||||
</Box>
|
</Box>
|
||||||
<CookieBanner onThemeChange={handleThemeChange} currentTheme={themeName} />
|
<CookieBanner onThemeChange={handleThemeChange} currentTheme={themeName} />
|
||||||
</Router>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-2
@@ -125,6 +125,9 @@ const resources = {
|
|||||||
"notifications": "Recent Notifications",
|
"notifications": "Recent Notifications",
|
||||||
"notifReply": "replied",
|
"notifReply": "replied",
|
||||||
"notifMention": "mentioned you",
|
"notifMention": "mentioned you",
|
||||||
|
"notifTicketNew": "opened a new support ticket",
|
||||||
|
"notifTicketReply": "replied to your support ticket",
|
||||||
|
"notifTicketReplyAdmin": "replied to a support ticket",
|
||||||
"noNotifications": "No notifications.",
|
"noNotifications": "No notifications.",
|
||||||
"searchFriends": "Search friends...",
|
"searchFriends": "Search friends...",
|
||||||
"writeReply": "Add a reply...",
|
"writeReply": "Add a reply...",
|
||||||
@@ -143,7 +146,8 @@ const resources = {
|
|||||||
"in_progress": "In Progress",
|
"in_progress": "In Progress",
|
||||||
"noTickets": "You haven't opened any tickets yet.",
|
"noTickets": "You haven't opened any tickets yet.",
|
||||||
"reply": "Send Message",
|
"reply": "Send Message",
|
||||||
"ticketDetail": "Ticket #{{id}}"
|
"ticketDetail": "Ticket #{{id}}",
|
||||||
|
"unread": "New message"
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"title": "Admin Control Panel",
|
"title": "Admin Control Panel",
|
||||||
@@ -315,6 +319,9 @@ const resources = {
|
|||||||
"notifications": "Neueste Benachrichtigungen",
|
"notifications": "Neueste Benachrichtigungen",
|
||||||
"notifReply": "hat geantwortet",
|
"notifReply": "hat geantwortet",
|
||||||
"notifMention": "hat dich erwähnt",
|
"notifMention": "hat dich erwähnt",
|
||||||
|
"notifTicketNew": "hat ein neues Support-Ticket eröffnet",
|
||||||
|
"notifTicketReply": "hat auf dein Support-Ticket geantwortet",
|
||||||
|
"notifTicketReplyAdmin": "hat auf ein Support-Ticket geantwortet",
|
||||||
"noNotifications": "Keine Benachrichtigungen.",
|
"noNotifications": "Keine Benachrichtigungen.",
|
||||||
"searchFriends": "Freunde suchen...",
|
"searchFriends": "Freunde suchen...",
|
||||||
"writeReply": "Eine Antwort hinzufügen...",
|
"writeReply": "Eine Antwort hinzufügen...",
|
||||||
@@ -333,7 +340,8 @@ const resources = {
|
|||||||
"in_progress": "In Bearbeitung",
|
"in_progress": "In Bearbeitung",
|
||||||
"noTickets": "Du hast noch keine Tickets geöffnet.",
|
"noTickets": "Du hast noch keine Tickets geöffnet.",
|
||||||
"reply": "Nachricht senden",
|
"reply": "Nachricht senden",
|
||||||
"ticketDetail": "Ticket #{{id}}"
|
"ticketDetail": "Ticket #{{id}}",
|
||||||
|
"unread": "Neue Nachricht"
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"title": "Admin-Kontrollzentrum",
|
"title": "Admin-Kontrollzentrum",
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { BrowserRouter as Router } from 'react-router-dom'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<Router>
|
||||||
|
<App />
|
||||||
|
</Router>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import {
|
import {
|
||||||
Container, Typography, Box, Card, CardContent, Button,
|
Container, Typography, Box, Card, CardContent, Button,
|
||||||
TextField, Stack, Chip, Divider, Collapse, Paper, alpha, CircularProgress,
|
TextField, Stack, Chip, Divider, Collapse, Paper, alpha, CircularProgress,
|
||||||
IconButton, Avatar
|
IconButton, Avatar, useTheme
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import SendIcon from '@mui/icons-material/Send';
|
import SendIcon from '@mui/icons-material/Send';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
@@ -37,6 +37,7 @@ interface Ticket {
|
|||||||
|
|
||||||
const Support: React.FC = () => {
|
const Support: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
useTitle(t('support.title'));
|
useTitle(t('support.title'));
|
||||||
const [tickets, setTickets] = useState<Ticket[]>([]);
|
const [tickets, setTickets] = useState<Ticket[]>([]);
|
||||||
@@ -51,15 +52,25 @@ const Support: React.FC = () => {
|
|||||||
const [replyText, setReplyText] = useState<{[key: number]: string}>({});
|
const [replyText, setReplyText] = useState<{[key: number]: string}>({});
|
||||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
const [expandedId, setExpandedId] = useState<number | null>(null);
|
||||||
const [currentUser, setCurrentUser] = useState<any>(null);
|
const [currentUser, setCurrentUser] = useState<any>(null);
|
||||||
|
const [unreadTicketIds, setUnreadTicketIds] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
const fetchTickets = async () => {
|
const fetchTickets = async () => {
|
||||||
try {
|
try {
|
||||||
const [ticketRes, userRes] = await Promise.all([
|
const [ticketRes, userRes, notifRes] = await Promise.all([
|
||||||
api.get('tickets/'),
|
api.get('tickets/'),
|
||||||
api.get('users/me/')
|
api.get('users/me/'),
|
||||||
|
api.get('notifications/')
|
||||||
]);
|
]);
|
||||||
setTickets(ticketRes.data.results || ticketRes.data);
|
setTickets(ticketRes.data.results || ticketRes.data);
|
||||||
setCurrentUser(userRes.data);
|
setCurrentUser(userRes.data);
|
||||||
|
|
||||||
|
const notifs = notifRes.data.results || notifRes.data;
|
||||||
|
const unreadIds = new Set<number>(
|
||||||
|
notifs
|
||||||
|
.filter((n: any) => !n.is_read && n.notification_type.startsWith('ticket') && n.ticket)
|
||||||
|
.map((n: any) => n.ticket)
|
||||||
|
);
|
||||||
|
setUnreadTicketIds(unreadIds);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -71,6 +82,26 @@ const Support: React.FC = () => {
|
|||||||
fetchTickets();
|
fetchTickets();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleExpand = async (ticketId: number) => {
|
||||||
|
if (expandedId === ticketId) {
|
||||||
|
setExpandedId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setExpandedId(ticketId);
|
||||||
|
|
||||||
|
// Mark notifications for this ticket as read
|
||||||
|
if (unreadTicketIds.has(ticketId)) {
|
||||||
|
try {
|
||||||
|
// We'd need an endpoint to mark all for a specific ticket,
|
||||||
|
// or just rely on the user clicking the notification.
|
||||||
|
// For now, let's just clear it from the local state.
|
||||||
|
const newUnread = new Set(unreadTicketIds);
|
||||||
|
newUnread.delete(ticketId);
|
||||||
|
setUnreadTicketIds(newUnread);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreate = async (e: React.FormEvent) => {
|
const handleCreate = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
@@ -177,18 +208,38 @@ const Support: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
{tickets.map((ticket: Ticket) => (
|
{tickets.map((ticket: Ticket) => (
|
||||||
<Card key={ticket.id} sx={{ borderRadius: 3, border: '1px solid', borderColor: 'divider' }}>
|
<Card key={ticket.id} sx={{
|
||||||
|
borderRadius: 3,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: unreadTicketIds.has(ticket.id) ? 'primary.main' : 'divider',
|
||||||
|
bgcolor: unreadTicketIds.has(ticket.id) ? alpha(theme.palette.primary.main, 0.02) : 'inherit'
|
||||||
|
}}>
|
||||||
<CardContent sx={{ p: 0 }}>
|
<CardContent sx={{ p: 0 }}>
|
||||||
<Box
|
<Box
|
||||||
onClick={() => setExpandedId(expandedId === ticket.id ? null : ticket.id)}
|
onClick={() => handleExpand(ticket.id)}
|
||||||
sx={{ p: 2, cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }}
|
sx={{ p: 2, cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
|
||||||
<Box>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>{ticket.subject}</Typography>
|
{unreadTicketIds.has(ticket.id) && (
|
||||||
<Typography variant="caption" color="text.secondary">{formatDate(ticket.created_at)}</Typography>
|
<Box sx={{ width: 10, height: 10, bgcolor: 'primary.main', borderRadius: '50%' }} />
|
||||||
|
)}
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
|
||||||
|
{ticket.subject}
|
||||||
|
{currentUser?.is_superuser && (
|
||||||
|
<Typography component="span" variant="caption" sx={{ ml: 1, color: 'primary.main' }}>
|
||||||
|
({ticket.username})
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">{formatDate(ticket.created_at)}</Typography>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
{unreadTicketIds.has(ticket.id) && (
|
||||||
|
<Chip label={t('support.unread')} color="primary" size="small" sx={{ fontWeight: 'bold', height: 20, fontSize: '0.6rem' }} />
|
||||||
|
)}
|
||||||
<Chip label={t(`support.${ticket.status}`)} color={getStatusColor(ticket.status) as any} size="small" sx={{ fontWeight: 'bold' }} />
|
<Chip label={t(`support.${ticket.status}`)} color={getStatusColor(ticket.status) as any} size="small" sx={{ fontWeight: 'bold' }} />
|
||||||
{currentUser?.is_superuser && (
|
{currentUser?.is_superuser && (
|
||||||
<IconButton size="small" color="error" onClick={(e) => { e.stopPropagation(); handleDeleteTicket(ticket.id); }}>
|
<IconButton size="small" color="error" onClick={(e) => { e.stopPropagation(); handleDeleteTicket(ticket.id); }}>
|
||||||
@@ -204,7 +255,7 @@ const Support: React.FC = () => {
|
|||||||
{ticket.username.charAt(0).toUpperCase()}
|
{ticket.username.charAt(0).toUpperCase()}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||||
<Typography variant="caption" sx={{ fontWeight: 'bold', display: 'block' }}>
|
<Typography variant="caption" sx={{ fontWeight: 'bold', display: 'block', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
{ticket.username} • <span style={{ opacity: 0.7 }}>{ticket.user_email}</span>
|
{ticket.username} • <span style={{ opacity: 0.7 }}>{ticket.user_email}</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -220,7 +271,7 @@ const Support: React.FC = () => {
|
|||||||
onClick={(e) => { e.stopPropagation(); navigate(`/admin-panel/user/${ticket.user}`); }}
|
onClick={(e) => { e.stopPropagation(); navigate(`/admin-panel/user/${ticket.user}`); }}
|
||||||
sx={{ minWidth: 0, py: 0, textTransform: 'none', fontSize: '0.7rem' }}
|
sx={{ minWidth: 0, py: 0, textTransform: 'none', fontSize: '0.7rem' }}
|
||||||
>
|
>
|
||||||
Admin User
|
{t('admin.manageUser')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in new issue
Block a user