mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
204 lines
9.9 KiB
TypeScript
204 lines
9.9 KiB
TypeScript
import React from 'react';
|
||
import AdminLayout from '../../layouts/AdminLayout';
|
||
import { Box, Heading, HStack, VStack, Button, Select, Input, Table, Thead, Tbody, Tr, Th, Td, Text, Badge, IconButton, useToast, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, ModalCloseButton, useDisclosure, FormControl, FormLabel, NumberInput, NumberInputField, Switch } from '@chakra-ui/react';
|
||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||
import { adminListComments, adminUpdateCommentStatus, adminBanUser, adminListUnbanRequests, adminResolveUnban } from '../../services/admin/comments';
|
||
import { deleteComment } from '../../services/comments';
|
||
import { FiTrash2 } from 'react-icons/fi';
|
||
|
||
const CommentsAdminPage: React.FC = () => {
|
||
const [status, setStatus] = React.useState<string>('');
|
||
const [targetType, setTargetType] = React.useState<string>('');
|
||
const [targetId, setTargetId] = React.useState<string>('');
|
||
const [userId, setUserId] = React.useState<string>('');
|
||
const [page, setPage] = React.useState<number>(1);
|
||
const [reportedOnly, setReportedOnly] = React.useState<boolean>(false);
|
||
const toast = useToast();
|
||
const qc = useQueryClient();
|
||
|
||
const listQ = useQuery({
|
||
queryKey: ['admin-comments', { status, targetType, targetId, userId, page }],
|
||
queryFn: () => adminListComments({ status: status as any, target_type: targetType, target_id: targetId, user_id: userId, page, page_size: 50 }),
|
||
keepPreviousData: true,
|
||
});
|
||
|
||
const unbanQ = useQuery({
|
||
queryKey: ['admin-unban-requests'],
|
||
queryFn: adminListUnbanRequests,
|
||
});
|
||
|
||
const updateStatusMut = useMutation({
|
||
mutationFn: (args: { id: number; s: 'visible'|'hidden' }) => adminUpdateCommentStatus(args.id, args.s),
|
||
onSuccess: async () => { await qc.invalidateQueries({ queryKey: ['admin-comments'] }); },
|
||
});
|
||
const deleteMut = useMutation({
|
||
mutationFn: (id: number) => deleteComment(id),
|
||
onSuccess: async () => { await qc.invalidateQueries({ queryKey: ['admin-comments'] }); toast({ status: 'success', title: 'Smazáno' }); },
|
||
});
|
||
|
||
const [banUserId, setBanUserId] = React.useState<number | null>(null);
|
||
const banModal = useDisclosure();
|
||
const [banReason, setBanReason] = React.useState<string>('Porušení pravidel diskuse');
|
||
const [banHours, setBanHours] = React.useState<number>(0);
|
||
const banMut = useMutation({
|
||
mutationFn: () => adminBanUser(banUserId || 0, banReason, banHours),
|
||
onSuccess: async () => { banModal.onClose(); setBanUserId(null); toast({ status: 'success', title: 'Uživatel zablokován' }); },
|
||
});
|
||
|
||
const resolveUnbanMut = useMutation({
|
||
mutationFn: (args: { id: number; action: 'approve'|'reject' }) => adminResolveUnban(args.id, args.action),
|
||
onSuccess: async () => { await qc.invalidateQueries({ queryKey: ['admin-unban-requests'] }); toast({ status: 'success', title: 'Vyřízeno' }); },
|
||
});
|
||
|
||
const itemsAll = listQ.data?.items || [];
|
||
const items = React.useMemo(() => {
|
||
if (!reportedOnly) return itemsAll;
|
||
return itemsAll.filter((c: any) => (c as any).reports && (c as any).reports > 0);
|
||
}, [itemsAll, reportedOnly]);
|
||
|
||
return (
|
||
<AdminLayout>
|
||
<Box>
|
||
<Heading size="md" mb={4}>Komentáře (moderace)</Heading>
|
||
<VStack align="stretch" spacing={3} mb={4}>
|
||
<HStack>
|
||
<Select placeholder="Status" value={status} onChange={(e) => { setStatus(e.target.value); setPage(1); }} maxW="200px">
|
||
<option value="visible">Viditelné</option>
|
||
<option value="hidden">Skryté</option>
|
||
</Select>
|
||
<Select placeholder="Typ cíle" value={targetType} onChange={(e) => { setTargetType(e.target.value); setPage(1); }} maxW="220px">
|
||
<option value="article">Článek</option>
|
||
<option value="event">Aktivita</option>
|
||
<option value="gallery_album">Galerie</option>
|
||
<option value="youtube_video">YouTube video</option>
|
||
</Select>
|
||
<Input placeholder="Target ID" value={targetId} onChange={(e) => { setTargetId(e.target.value); setPage(1); }} maxW="200px" />
|
||
<Input placeholder="User ID" value={userId} onChange={(e) => { setUserId(e.target.value); setPage(1); }} maxW="200px" />
|
||
<HStack>
|
||
<Text fontSize="sm" color="gray.500">Jen nahlášené</Text>
|
||
<Switch isChecked={reportedOnly} onChange={(e)=>setReportedOnly(e.target.checked)} />
|
||
</HStack>
|
||
</HStack>
|
||
</VStack>
|
||
|
||
<Box borderWidth="1px" borderRadius="md" overflowX="auto">
|
||
<Table size="sm">
|
||
<Thead>
|
||
<Tr>
|
||
<Th>ID</Th>
|
||
<Th>Uživatel</Th>
|
||
<Th>Cíl</Th>
|
||
<Th>Obsah</Th>
|
||
<Th>Spam</Th>
|
||
<Th>Hlášení</Th>
|
||
<Th>Status</Th>
|
||
<Th>Akce</Th>
|
||
</Tr>
|
||
</Thead>
|
||
<Tbody>
|
||
{items.map((c) => (
|
||
<Tr key={c.id}>
|
||
<Td>#{c.id}</Td>
|
||
<Td>#{c.user?.id} {c.user?.first_name} {c.user?.last_name}</Td>
|
||
<Td><Badge>{c.target_type}</Badge> <Text as="span">{c.target_id}</Text></Td>
|
||
<Td maxW="420px"><Text noOfLines={2}>{c.content}</Text></Td>
|
||
<Td>{(c as any).spam_score ? <Badge colorScheme={(c as any).spam_score > 0.5 ? 'orange' : 'green'}>{(c as any).spam_score.toFixed(2)}</Badge> : '-'}</Td>
|
||
<Td>{(c as any).reports ? <Badge colorScheme={(c as any).reports > 2 ? 'red' : 'yellow'}>{(c as any).reports}</Badge> : '-'}</Td>
|
||
<Td>
|
||
<HStack>
|
||
<Button size="xs" variant={c.status === 'visible' ? 'solid' : 'outline'} onClick={() => updateStatusMut.mutate({ id: c.id, s: 'visible' })}>Viditelné</Button>
|
||
<Button size="xs" variant={c.status === 'hidden' ? 'solid' : 'outline'} onClick={() => updateStatusMut.mutate({ id: c.id, s: 'hidden' })}>Skryté</Button>
|
||
</HStack>
|
||
</Td>
|
||
<Td>
|
||
<HStack>
|
||
<IconButton aria-label="Smazat" size="xs" icon={<FiTrash2 />} onClick={() => deleteMut.mutate(c.id)} />
|
||
<Button size="xs" variant="outline" onClick={() => { setBanUserId(c.user?.id as any); banModal.onOpen(); }}>Ban</Button>
|
||
</HStack>
|
||
</Td>
|
||
</Tr>
|
||
))}
|
||
</Tbody>
|
||
</Table>
|
||
</Box>
|
||
|
||
<HStack mt={3} justify="space-between">
|
||
<Text fontSize="sm" color="gray.500">Stránka {page} • {listQ.data?.total || 0} komentářů</Text>
|
||
<HStack>
|
||
<Button size="sm" variant="outline" onClick={() => setPage(p => Math.max(1, p - 1))} isDisabled={page <= 1}>Předchozí</Button>
|
||
<Button size="sm" variant="outline" onClick={() => setPage(p => p + 1)} isDisabled={(itemsAll.length === 0) || ((itemsAll.length < 50) && (listQ.data?.total || 0) <= (page * 50))}>Další</Button>
|
||
</HStack>
|
||
</HStack>
|
||
|
||
<Heading size="sm" mt={6} mb={2}>Žádosti o odblokování</Heading>
|
||
<Box borderWidth="1px" borderRadius="md" overflowX="auto">
|
||
<Table size="sm">
|
||
<Thead>
|
||
<Tr>
|
||
<Th>ID</Th>
|
||
<Th>Uživatel</Th>
|
||
<Th>Text</Th>
|
||
<Th>Status</Th>
|
||
<Th>Akce</Th>
|
||
</Tr>
|
||
</Thead>
|
||
<Tbody>
|
||
{(unbanQ.data?.items || []).map((r) => (
|
||
<Tr key={r.id}>
|
||
<Td>#{r.id}</Td>
|
||
<Td>#{r.user_id}</Td>
|
||
<Td maxW="480px"><Text noOfLines={2}>{r.message}</Text></Td>
|
||
<Td><Badge>{r.status}</Badge></Td>
|
||
<Td>
|
||
<HStack>
|
||
<Button size="xs" colorScheme="green" variant="outline" onClick={() => resolveUnbanMut.mutate({ id: r.id, action: 'approve' })}>Povolit</Button>
|
||
<Button size="xs" colorScheme="red" variant="outline" onClick={() => resolveUnbanMut.mutate({ id: r.id, action: 'reject' })}>Zamítnout</Button>
|
||
</HStack>
|
||
</Td>
|
||
</Tr>
|
||
))}
|
||
</Tbody>
|
||
</Table>
|
||
</Box>
|
||
|
||
{/* Ban modal */}
|
||
<Modal isOpen={banModal.isOpen} onClose={banModal.onClose} isCentered>
|
||
<ModalOverlay />
|
||
<ModalContent>
|
||
<ModalHeader>Zablokovat uživatele #{banUserId}</ModalHeader>
|
||
<ModalCloseButton />
|
||
<ModalBody>
|
||
<VStack align="stretch" spacing={3}>
|
||
<FormControl>
|
||
<FormLabel>Důvod</FormLabel>
|
||
<Input value={banReason} onChange={(e) => setBanReason(e.target.value)} />
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>Doba (hodiny) – 0 = trvale</FormLabel>
|
||
<NumberInput min={0} value={banHours} onChange={(v) => setBanHours(Number(v) || 0)}>
|
||
<NumberInputField />
|
||
</NumberInput>
|
||
</FormControl>
|
||
<HStack>
|
||
<Text fontSize="sm" color="gray.500">Rychlá volba:</Text>
|
||
<Button size="xs" variant="outline" onClick={()=>setBanHours(24)}>24h</Button>
|
||
<Button size="xs" variant="outline" onClick={()=>setBanHours(24*7)}>7 dní</Button>
|
||
<Button size="xs" variant="outline" onClick={()=>setBanHours(0)}>Trvale</Button>
|
||
</HStack>
|
||
</VStack>
|
||
</ModalBody>
|
||
<ModalFooter>
|
||
<HStack>
|
||
<Button onClick={banModal.onClose}>Zrušit</Button>
|
||
<Button colorScheme="red" isLoading={banMut.isPending} onClick={() => banMut.mutate()}>Zablokovat</Button>
|
||
</HStack>
|
||
</ModalFooter>
|
||
</ModalContent>
|
||
</Modal>
|
||
</Box>
|
||
</AdminLayout>
|
||
);
|
||
};
|
||
|
||
export default CommentsAdminPage;
|