Files
MyClub/frontend/src/pages/admin/StandingsAdminPage.tsx
T
Tomáš Dvořák 12cba639b9 upload
2025-10-16 13:32:05 +02:00

159 lines
5.7 KiB
TypeScript

import { Heading, Text, Box, Spinner, Alert, AlertIcon, Table, Thead, Tbody, Tr, Th, Td, VStack, Select, HStack, Badge } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useMemo, useState } from 'react';
import AdminLayout from '../../layouts/AdminLayout';
import { assetUrl } from '../../utils/url';
import { TeamLogo } from '../../components/common/TeamLogo';
type TableRow = {
rank?: string;
team?: string;
team_id?: string;
team_logo_url?: string;
played?: string;
wins?: string;
draws?: string;
losses?: string;
score?: string;
points?: string;
};
const StandingsAdminPage: React.FC = () => {
const { data, isLoading, error } = useQuery<any>({
queryKey: ['facr-tables-cache'],
queryFn: async () => {
const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1';
const origin = new URL(apiUrl).origin;
const url = `${origin}/cache/prefetch/facr_tables.json`;
const res = await fetch(url, { headers: { 'Cache-Control': 'no-cache' } });
if (!res.ok) throw new Error(`Failed to load cache: ${res.status}`);
return res.json();
},
staleTime: 5 * 60 * 1000,
});
const competitions: any[] = Array.isArray(data?.competitions) ? data!.competitions : [];
// Optional category/code switcher based on competition code
const [code, setCode] = useState<string>('');
const options = useMemo(() => {
const items = competitions.map((c) => ({ code: c.code, name: c.name }));
const unique = new Map(items.map((i) => [i.code, i]));
return Array.from(unique.values());
}, [competitions]);
const filtered = code ? competitions.filter((c) => c.code === code) : competitions;
return (
<AdminLayout>
<Heading size="lg" mb={2}>Standings (FAČR)</Heading>
<Text mb={4}>Read-only view of standings from FACR cache. Fast and offline-friendly.</Text>
{isLoading && (
<VStack align="start" spacing={3} mb={6}>
<Spinner />
<Text>Načítám tabulky</Text>
</VStack>
)}
{Boolean(error) && (
<Alert status="error" variant="left-accent" mb={4}>
<AlertIcon />
Nepodařilo se načíst data z cache.
</Alert>
)}
{!isLoading && !error && (
<HStack mb={4} spacing={3} align="center">
<Text color="gray.600">Soutěž:</Text>
<Select value={code} onChange={(e) => setCode(e.target.value)} maxW="260px" size="sm">
<option value="">Vše</option>
{options.map((o) => (
<option key={o.code} value={o.code}>{o.code} {o.name}</option>
))}
</Select>
<Badge colorScheme="gray" variant="subtle">{filtered.length} soutěží</Badge>
</HStack>
)}
{!isLoading && !error && filtered.map((comp) => {
const rows: TableRow[] = comp?.table?.overall || [];
return (
<Box key={comp.id} mb={8}>
<Heading size="md" mb={3}>{comp.name}</Heading>
<Box
overflowX="auto"
borderWidth="1px"
borderRadius="md"
w="full"
maxW="100%"
sx={{
WebkitOverflowScrolling: 'touch',
'th, td': { whiteSpace: 'nowrap' },
}}
>
<Table size="sm" sx={{ width: 'max-content' }}>
<Thead>
<Tr>
<Th>#</Th>
<Th>Tým</Th>
<Th isNumeric>Z</Th>
<Th isNumeric>V</Th>
<Th isNumeric>R</Th>
<Th isNumeric>P</Th>
<Th isNumeric>Skóre</Th>
<Th isNumeric>Body</Th>
</Tr>
</Thead>
<Tbody>
{rows.map((r, idx) => (
<Tr key={`${comp.id}-${idx}`}>
<Td width="40px">{r.rank}</Td>
<Td>
<HStack spacing={2} align="center">
<TeamLogo
teamId={r.team_id}
teamName={r.team}
facrLogo={r.team_logo_url}
size="small"
alt={r.team}
objectFit="contain"
fallbackIcon={
<Box
w="20px"
h="20px"
bg="gray.200"
borderRadius="md"
display="flex"
alignItems="center"
justifyContent="center"
color="gray.400"
fontSize="xs"
fontWeight="bold"
>
{r.team?.substring(0, 2).toUpperCase() || '??'}
</Box>
}
/>
<Text as="span">{r.team}</Text>
</HStack>
</Td>
<Td isNumeric>{r.played}</Td>
<Td isNumeric>{r.wins}</Td>
<Td isNumeric>{r.draws}</Td>
<Td isNumeric>{r.losses}</Td>
<Td isNumeric>{r.score}</Td>
<Td isNumeric fontWeight="bold">{r.points}</Td>
</Tr>
))}
</Tbody>
</Table>
</Box>
</Box>
);
})}
</AdminLayout>
);
};
export default StandingsAdminPage;