mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 18:52:56 +00:00
upload
This commit is contained in:
@@ -0,0 +1,508 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
Badge,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
Input,
|
||||
HStack,
|
||||
useColorModeValue,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalCloseButton,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
VStack
|
||||
} from '@chakra-ui/react';
|
||||
import { AddIcon, DeleteIcon, EmailIcon, Search2Icon, StarIcon, ArrowForwardIcon } from '@chakra-ui/icons';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { format } from 'date-fns';
|
||||
import { cs } from 'date-fns/locale';
|
||||
import AdminLayout from '../../layouts/AdminLayout';
|
||||
import {
|
||||
getContactMessages,
|
||||
markAsRead,
|
||||
deleteMessage,
|
||||
deleteMultipleMessages,
|
||||
forwardAllMessages,
|
||||
ContactMessage
|
||||
} from '../../services/admin/contactMessages';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import MessageDetailModal from '../../components/admin/MessageDetailModal';
|
||||
import ConfirmationDialog from '../../components/common/ConfirmationDialog';
|
||||
|
||||
export default function MessagesAdminPage() {
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||||
const inputBg = useColorModeValue('white', 'gray.700');
|
||||
const [selectedMessages, setSelectedMessages] = useState<string[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<'all' | 'read' | 'unread'>('all');
|
||||
const [sortBy, setSortBy] = useState<{ field: string; order: 'asc' | 'desc' }>({
|
||||
field: 'createdAt',
|
||||
order: 'desc',
|
||||
});
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
const {
|
||||
isOpen: isDetailOpen,
|
||||
onOpen: onDetailOpen,
|
||||
onClose: onDetailClose
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isDeleteOpen,
|
||||
onOpen: onDeleteOpen,
|
||||
onClose: onDeleteClose
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isForwardAllOpen,
|
||||
onOpen: onForwardAllOpen,
|
||||
onClose: onForwardAllClose
|
||||
} = useDisclosure();
|
||||
|
||||
const [forwardAllEmail, setForwardAllEmail] = useState('');
|
||||
|
||||
const [selectedMessage, setSelectedMessage] = useState<ContactMessage | null>(null);
|
||||
const toast = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['admin', 'contact-messages', { ...pagination, searchTerm, statusFilter, sortBy }],
|
||||
queryFn: () =>
|
||||
getContactMessages({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
search: searchTerm,
|
||||
isRead: statusFilter === 'all' ? undefined : statusFilter === 'read',
|
||||
sortBy: sortBy.field,
|
||||
sortOrder: sortBy.order,
|
||||
}),
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
const markAsReadMutation = useMutation({
|
||||
mutationFn: markAsRead,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'contact-messages'] });
|
||||
toast({
|
||||
title: 'Zpráva označena jako přečtená',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMessageMutation = useMutation({
|
||||
mutationFn: deleteMessage,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'contact-messages'] });
|
||||
toast({
|
||||
title: 'Zpráva smazána',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMultipleMutation = useMutation({
|
||||
mutationFn: deleteMultipleMessages,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'contact-messages'] });
|
||||
setSelectedMessages([]);
|
||||
toast({
|
||||
title: 'Vybrané zprávy byly smazány',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const forwardAllMutation = useMutation({
|
||||
mutationFn: forwardAllMessages,
|
||||
onSuccess: (data) => {
|
||||
toast({
|
||||
title: 'Zprávy se přeposílají',
|
||||
description: data.message || 'Všechny zprávy budou přeposlány na zadaný e-mail',
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
setForwardAllEmail('');
|
||||
onForwardAllClose();
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: 'Chyba',
|
||||
description: 'Nepodařilo se přeposlat zprávy',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleViewMessage = (message: ContactMessage) => {
|
||||
setSelectedMessage(message);
|
||||
if (!message.isRead) {
|
||||
markAsReadMutation.mutate(message.id);
|
||||
}
|
||||
onDetailOpen();
|
||||
};
|
||||
|
||||
const handleDeleteClick = (message: ContactMessage) => {
|
||||
setSelectedMessage(message);
|
||||
onDeleteOpen();
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = () => {
|
||||
if (selectedMessage) {
|
||||
deleteMessageMutation.mutate(selectedMessage.id);
|
||||
}
|
||||
onDeleteClose();
|
||||
};
|
||||
|
||||
const handleBulkDelete = () => {
|
||||
if (selectedMessages.length > 0) {
|
||||
deleteMultipleMutation.mutate(selectedMessages);
|
||||
}
|
||||
};
|
||||
|
||||
const handleForwardAll = () => {
|
||||
if (!forwardAllEmail || !forwardAllEmail.includes('@')) {
|
||||
toast({
|
||||
title: 'Chyba',
|
||||
description: 'Zadejte platnou e-mailovou adresu',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
forwardAllMutation.mutate(forwardAllEmail);
|
||||
};
|
||||
|
||||
const handleSelectAll = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedMessages(data?.data.map((msg) => msg.id) || []);
|
||||
} else {
|
||||
setSelectedMessages([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectMessage = (id: string, isSelected: boolean) => {
|
||||
if (isSelected) {
|
||||
setSelectedMessages((prev) => [...prev, id]);
|
||||
} else {
|
||||
setSelectedMessages((prev) => prev.filter((msgId) => msgId !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSort = (field: string) => {
|
||||
setSortBy((prev) => ({
|
||||
field,
|
||||
order: prev.field === field && prev.order === 'asc' ? 'desc' : 'asc',
|
||||
}));
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return format(new Date(dateString), 'd. M. yyyy HH:mm', { locale: cs });
|
||||
};
|
||||
|
||||
const getSortIcon = (field: string) => {
|
||||
if (sortBy.field !== field) return null;
|
||||
return sortBy.order === 'asc' ? '↑' : '↓';
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<Box p={6}>
|
||||
<Flex justify="space-between" align="center" mb={6}>
|
||||
<Box>
|
||||
<Heading size="lg" mb={2}>
|
||||
Příchozí zprávy
|
||||
</Heading>
|
||||
<Text color="gray.600">
|
||||
Spravujte příchozí zprávy z kontaktního formuláře
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<HStack spacing={3} flexWrap="wrap">
|
||||
<Button
|
||||
colorScheme="teal"
|
||||
variant="outline"
|
||||
leftIcon={<ArrowForwardIcon />}
|
||||
onClick={onForwardAllOpen}
|
||||
size={{ base: "sm", md: "md" }}
|
||||
>
|
||||
Přeposlat vše
|
||||
</Button>
|
||||
{selectedMessages.length > 0 && (
|
||||
<Button
|
||||
colorScheme="red"
|
||||
variant="outline"
|
||||
leftIcon={<DeleteIcon />}
|
||||
onClick={handleBulkDelete}
|
||||
isLoading={deleteMultipleMutation.isLoading}
|
||||
size={{ base: "sm", md: "md" }}
|
||||
>
|
||||
Smazat vybrané ({selectedMessages.length})
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
<Box bg={cardBg} borderRadius="lg" boxShadow="sm" p={4} mb={6}>
|
||||
<Flex mb={4} gap={4} flexWrap="wrap">
|
||||
<InputGroup maxW="md">
|
||||
<InputLeftElement pointerEvents="none">
|
||||
<Search2Icon color="gray.400" />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
placeholder="Hledat v zprávách..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
<Select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value as any)}
|
||||
maxW="200px"
|
||||
>
|
||||
<option value="all">Všechny zprávy</option>
|
||||
<option value="unread">Nepřečtené</option>
|
||||
<option value="read">Přečtené</option>
|
||||
</Select>
|
||||
</Flex>
|
||||
|
||||
<Box overflowX="auto">
|
||||
<Table variant="simple">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th w="40px">
|
||||
<Checkbox
|
||||
isChecked={selectedMessages.length > 0 && selectedMessages.length === data?.data.length}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
</Th>
|
||||
<Th
|
||||
cursor="pointer"
|
||||
onClick={() => handleSort('name')}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
>
|
||||
Jméno {getSortIcon('name')}
|
||||
</Th>
|
||||
<Th
|
||||
cursor="pointer"
|
||||
onClick={() => handleSort('email')}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
>
|
||||
E-mail {getSortIcon('email')}
|
||||
</Th>
|
||||
<Th>Předmět</Th>
|
||||
<Th>Zdroj</Th>
|
||||
<Th
|
||||
cursor="pointer"
|
||||
onClick={() => handleSort('createdAt')}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
>
|
||||
Datum {getSortIcon('createdAt')}
|
||||
</Th>
|
||||
<Th>Stav</Th>
|
||||
<Th w="120px">Akce</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{isLoading ? (
|
||||
<Tr>
|
||||
<Td colSpan={8} textAlign="center" py={8}>
|
||||
Načítání...
|
||||
</Td>
|
||||
</Tr>
|
||||
) : isError ? (
|
||||
<Tr>
|
||||
<Td colSpan={8} textAlign="center" py={8} color="red.500">
|
||||
Chyba při načítání zpráv
|
||||
</Td>
|
||||
</Tr>
|
||||
) : data?.data.length === 0 ? (
|
||||
<Tr>
|
||||
<Td colSpan={8} textAlign="center" py={8} color="gray.500">
|
||||
Žádné zprávy nenalezeny
|
||||
</Td>
|
||||
</Tr>
|
||||
) : (
|
||||
data?.data.map((message) => (
|
||||
<Tr
|
||||
key={message.id}
|
||||
bg={!message.isRead ? 'blue.50' : 'transparent'}
|
||||
_hover={{ bg: !message.isRead ? 'blue.50' : 'gray.50' }}
|
||||
>
|
||||
<Td>
|
||||
<Checkbox
|
||||
isChecked={selectedMessages.includes(message.id)}
|
||||
onChange={(e) => handleSelectMessage(message.id, e.target.checked)}
|
||||
/>
|
||||
</Td>
|
||||
<Td fontWeight={!message.isRead ? 'semibold' : 'normal'}>
|
||||
{message.name}
|
||||
</Td>
|
||||
<Td>{message.email}</Td>
|
||||
<Td maxW="200px" isTruncated title={message.subject || 'Bez předmětu'}>
|
||||
{message.subject || '—'}
|
||||
</Td>
|
||||
<Td>
|
||||
{message.source === 'sponsor' ? (
|
||||
<Badge colorScheme="purple">Sponzor</Badge>
|
||||
) : (
|
||||
<Badge colorScheme="gray">Kontakt</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td whiteSpace="nowrap">
|
||||
{formatDate(message.createdAt)}
|
||||
</Td>
|
||||
<Td>
|
||||
{message.isRead ? (
|
||||
<Badge colorScheme="green">Přečteno</Badge>
|
||||
) : (
|
||||
<Badge colorScheme="blue">Nová zpráva</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<HStack spacing={2}>
|
||||
<IconButton
|
||||
aria-label="Zobrazit zprávu"
|
||||
icon={<EmailIcon />}
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
variant="ghost"
|
||||
onClick={() => handleViewMessage(message)}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="Smazat zprávu"
|
||||
icon={<DeleteIcon />}
|
||||
size="sm"
|
||||
colorScheme="red"
|
||||
variant="ghost"
|
||||
onClick={() => handleDeleteClick(message)}
|
||||
isLoading={
|
||||
deleteMessageMutation.isLoading &&
|
||||
deleteMessageMutation.variables === message.id
|
||||
}
|
||||
/>
|
||||
</HStack>
|
||||
</Td>
|
||||
</Tr>
|
||||
))
|
||||
)}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{data && data.total > 0 && (
|
||||
<Flex justify="space-between" mt={4} alignItems="center">
|
||||
<Text color="gray.600" fontSize="sm">
|
||||
Zobrazeno {data.data.length} z {data.total} zpráv
|
||||
</Text>
|
||||
<Pagination
|
||||
currentPage={pagination.page}
|
||||
totalPages={data.totalPages || 1}
|
||||
onPageChange={(page) => setPagination((p) => ({ ...p, page }))}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{selectedMessage && (
|
||||
<MessageDetailModal
|
||||
isOpen={isDetailOpen}
|
||||
onClose={onDetailClose}
|
||||
message={selectedMessage}
|
||||
onDelete={() => {
|
||||
onDetailClose();
|
||||
handleDeleteClick(selectedMessage);
|
||||
}}
|
||||
onMarkAsRead={() => markAsReadMutation.mutate(selectedMessage.id)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfirmationDialog
|
||||
isOpen={isDeleteOpen}
|
||||
onClose={onDeleteClose}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
title="Smazat zprávu"
|
||||
message="Opravdu chcete smazat tuto zprávu? Tato akce je nevratná."
|
||||
confirmText="Smazat"
|
||||
cancelText="Zrušit"
|
||||
isDanger
|
||||
isLoading={deleteMessageMutation.isLoading}
|
||||
/>
|
||||
|
||||
<Modal isOpen={isForwardAllOpen} onClose={onForwardAllClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Přeposlat všechny zprávy</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<VStack spacing={4}>
|
||||
<Text>
|
||||
Všechny příchozí zprávy budou přeposlány na zadanou e-mailovou adresu.
|
||||
</Text>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>E-mailová adresa</FormLabel>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="prijemce@email.cz"
|
||||
value={forwardAllEmail}
|
||||
onChange={(e) => setForwardAllEmail(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="ghost" mr={3} onClick={onForwardAllClose}>
|
||||
Zrušit
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme="teal"
|
||||
onClick={handleForwardAll}
|
||||
isLoading={forwardAllMutation.isLoading}
|
||||
>
|
||||
Přeposlat
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user