mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
upload
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Button,
|
||||
Badge,
|
||||
Text,
|
||||
useToast,
|
||||
Select,
|
||||
Spinner,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
IconButton,
|
||||
useColorModeValue,
|
||||
Collapse,
|
||||
Divider,
|
||||
} from '@chakra-ui/react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { AddIcon, DeleteIcon, ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons';
|
||||
import { getPolls, createPoll, updatePoll, Poll, CreatePollRequest } from '../../services/polls';
|
||||
|
||||
interface PollLinkerProps {
|
||||
articleId?: number;
|
||||
eventId?: number;
|
||||
onPollsChanged?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* PollLinker - Component to manage poll associations with articles/events
|
||||
* Can be embedded in article and activity admin pages
|
||||
*/
|
||||
const PollLinker: React.FC<PollLinkerProps> = ({ articleId, eventId, onPollsChanged }) => {
|
||||
const toast = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [selectedPollId, setSelectedPollId] = useState<string>('');
|
||||
|
||||
const bgBox = useColorModeValue('gray.50', 'gray.700');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
||||
|
||||
// Query for existing polls
|
||||
const queryParams = articleId ? { article_id: articleId } : eventId ? { event_id: eventId } : {};
|
||||
|
||||
const { data: linkedPolls, isLoading: isLoadingLinked } = useQuery({
|
||||
queryKey: ['linked-polls', queryParams],
|
||||
queryFn: () => getPolls(queryParams),
|
||||
enabled: !!(articleId || eventId),
|
||||
});
|
||||
|
||||
// Query for all available polls
|
||||
const { data: allPolls, isLoading: isLoadingAll } = useQuery({
|
||||
queryKey: ['all-admin-polls'],
|
||||
queryFn: () => getPolls({ status: 'active' }),
|
||||
});
|
||||
|
||||
// Mutation to link existing poll
|
||||
const linkPollMutation = useMutation({
|
||||
mutationFn: async (pollId: number) => {
|
||||
const updateData: any = {};
|
||||
if (articleId) updateData.related_article_id = articleId;
|
||||
if (eventId) updateData.related_event_id = eventId;
|
||||
|
||||
return updatePoll(pollId, updateData);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['linked-polls'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['all-admin-polls'] });
|
||||
toast({
|
||||
title: 'Anketa propojena',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
setSelectedPollId('');
|
||||
if (onPollsChanged) onPollsChanged();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
title: 'Chyba',
|
||||
description: error.response?.data?.error || 'Nepodařilo se propojit anketu',
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation to unlink poll
|
||||
const unlinkPollMutation = useMutation({
|
||||
mutationFn: async (pollId: number) => {
|
||||
const updateData: any = {};
|
||||
if (articleId) updateData.related_article_id = null;
|
||||
if (eventId) updateData.related_event_id = null;
|
||||
|
||||
return updatePoll(pollId, updateData);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['linked-polls'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['all-admin-polls'] });
|
||||
toast({
|
||||
title: 'Anketa odpojena',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
if (onPollsChanged) onPollsChanged();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
title: 'Chyba',
|
||||
description: error.response?.data?.error || 'Nepodařilo se odpojit anketu',
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleLinkPoll = () => {
|
||||
if (!selectedPollId) {
|
||||
toast({
|
||||
title: 'Vyberte anketu',
|
||||
description: 'Prosím vyberte anketu ze seznamu',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
linkPollMutation.mutate(parseInt(selectedPollId));
|
||||
};
|
||||
|
||||
const handleUnlinkPoll = (pollId: number) => {
|
||||
if (window.confirm('Opravdu chcete odpojit tuto anketu?')) {
|
||||
unlinkPollMutation.mutate(pollId);
|
||||
}
|
||||
};
|
||||
|
||||
// Filter out polls that are already linked
|
||||
const linkedPollIds = new Set(linkedPolls?.map(p => p.id) || []);
|
||||
const availablePolls = allPolls?.filter(p => !linkedPollIds.has(p.id)) || [];
|
||||
|
||||
if (!articleId && !eventId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="md"
|
||||
p={4}
|
||||
bg={bgBox}
|
||||
>
|
||||
<VStack align="stretch" spacing={3}>
|
||||
<HStack justify="space-between">
|
||||
<HStack>
|
||||
<Text fontWeight="bold" fontSize="sm">
|
||||
Ankety ({linkedPolls?.length || 0})
|
||||
</Text>
|
||||
{(linkedPolls?.length || 0) > 0 && (
|
||||
<Badge colorScheme="blue">{linkedPolls!.length} připojeno</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
<IconButton
|
||||
aria-label={isExpanded ? 'Skrýt' : 'Zobrazit'}
|
||||
icon={isExpanded ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
<Collapse in={isExpanded}>
|
||||
<VStack spacing={4} align="stretch">
|
||||
{isLoadingLinked ? (
|
||||
<HStack justify="center" py={4}>
|
||||
<Spinner size="sm" />
|
||||
<Text fontSize="sm">Načítání anket...</Text>
|
||||
</HStack>
|
||||
) : linkedPolls && linkedPolls.length > 0 ? (
|
||||
<VStack spacing={2} align="stretch">
|
||||
<Text fontSize="xs" fontWeight="bold" color="gray.500">
|
||||
Připojené ankety:
|
||||
</Text>
|
||||
{linkedPolls.map((poll) => (
|
||||
<HStack
|
||||
key={poll.id}
|
||||
p={2}
|
||||
borderWidth="1px"
|
||||
borderRadius="md"
|
||||
justify="space-between"
|
||||
bg="white"
|
||||
_dark={{ bg: 'gray.800' }}
|
||||
>
|
||||
<VStack align="start" spacing={0} flex={1}>
|
||||
<Text fontSize="sm" fontWeight="medium">
|
||||
{poll.title}
|
||||
</Text>
|
||||
<HStack spacing={2}>
|
||||
<Badge size="sm" colorScheme={poll.status === 'active' ? 'green' : 'gray'}>
|
||||
{poll.status}
|
||||
</Badge>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{poll.total_votes} hlasů
|
||||
</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<IconButton
|
||||
aria-label="Odpojit anketu"
|
||||
icon={<DeleteIcon />}
|
||||
size="sm"
|
||||
colorScheme="red"
|
||||
variant="ghost"
|
||||
onClick={() => handleUnlinkPoll(poll.id)}
|
||||
isLoading={unlinkPollMutation.isPending}
|
||||
/>
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
) : (
|
||||
<Alert status="info" size="sm">
|
||||
<AlertIcon />
|
||||
<Text fontSize="sm">Žádné ankety nejsou připojeny</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
{isLoadingAll ? (
|
||||
<HStack justify="center" py={4}>
|
||||
<Spinner size="sm" />
|
||||
</HStack>
|
||||
) : availablePolls.length > 0 ? (
|
||||
<VStack spacing={3} align="stretch">
|
||||
<Text fontSize="xs" fontWeight="bold" color="gray.500">
|
||||
Připojit existující anketu:
|
||||
</Text>
|
||||
<HStack>
|
||||
<Select
|
||||
value={selectedPollId}
|
||||
onChange={(e) => setSelectedPollId(e.target.value)}
|
||||
placeholder="Vyberte anketu..."
|
||||
size="sm"
|
||||
flex={1}
|
||||
>
|
||||
{availablePolls.map((poll) => (
|
||||
<option key={poll.id} value={poll.id}>
|
||||
{poll.title} ({poll.status}) - {poll.total_votes} hlasů
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Button
|
||||
leftIcon={<AddIcon />}
|
||||
onClick={handleLinkPoll}
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
isLoading={linkPollMutation.isPending}
|
||||
isDisabled={!selectedPollId}
|
||||
>
|
||||
Připojit
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
) : (
|
||||
<Alert status="warning" size="sm">
|
||||
<AlertIcon />
|
||||
<Text fontSize="sm">
|
||||
Žádné dostupné ankety. Vytvořte novou v{' '}
|
||||
<Button
|
||||
as="a"
|
||||
href="/admin/ankety"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
>
|
||||
správě anket
|
||||
</Button>
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
💡 Tip: Ankety se zobrazí automaticky na konci {articleId ? 'článku' : 'aktivity'}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Collapse>
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PollLinker;
|
||||
Reference in New Issue
Block a user