import { useEffect, useState } from "react"; import axios from "axios"; import { useTranslation } from "react-i18next"; import { useLanguageStore } from "@/stores/languageStore"; import { useNavigate } from "react-router-dom"; import { useContestStore, type RoundSummary } from "@/stores/contestStore"; import { useContestRefreshStore } from "@/stores/contestRefreshStore"; import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, Pagination, type Selection, } from "@heroui/react"; type TranslatedField = string | Record; export type Round = { id: number; contest_id: number; name: TranslatedField; description?: TranslatedField | null; bands?: { id: number; name: string }[]; categories?: { id: number; name: string }[]; power_categories?: { id: number; name: string }[]; is_active: boolean; is_test: boolean; is_sixhr: boolean; is_mcr?: boolean; start_time: string | null; end_time: string | null; logs_deadline: string | null; preliminary_evaluation_run_id?: number | null; official_evaluation_run_id?: number | null; test_evaluation_run_id?: number | null; contest?: { id: number; name: TranslatedField; is_active?: boolean; } | null; }; type PaginatedResponse = { data: T[]; current_page?: number; last_page?: number; total?: number; per_page?: number; }; type RoundsTableProps = { onSelectRound?: (round: Round) => void; /** Show/hide the edit (pencil) actions column. Default: true */ enableEdit?: boolean; /** Filter the list to only active rounds (is_active === true). Default: false */ onlyActive?: boolean; /** Show/hide the is_active column. Default: true */ showActiveColumn?: boolean; /** Show/hide contest column. Default: true */ showContestColumn?: boolean; /** Filter rounds by contest id (round.contest_id). If undefined/null, no contest filter is applied. */ contestId?: number | null; /** Zahrnout testovací kola. Default: false */ showTests?: boolean; /** Skrýt neaktivní kola (nebo neaktivní závody) pro nepřihlášené. */ hideInactiveForGuests?: boolean; /** Indikuje nepřihlášeného uživatele. */ isGuest?: boolean; /** Kliknutí na řádek přenaviguje na detail kola. Default: false */ enableRowNavigation?: boolean; /** Volitelný builder URL pro navigaci na detail kola. */ roundLinkBuilder?: (round: Round) => string; /** Pokud máš data kol už ve store, můžeš je sem předat místo fetchování. */ roundsFromStore?: RoundSummary[] | null; /** Volitelný nadpis tabulky. */ title?: string; }; function resolveTranslation( field: TranslatedField | null | undefined, locale: string ): string { if (!field) return ""; if (typeof field === "string") { return field; } if (field[locale]) return field[locale]; if (field["en"]) return field["en"]; const first = Object.values(field)[0]; return first ?? ""; } function formatDateTime(value: string | null, locale: string): string { if (!value) return "—"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString(locale); } type ResultBadge = { label: string; className: string; }; function getResultsBadge(round: Round, t: (key: string) => string): ResultBadge | null { if (round.preliminary_evaluation_run_id) { return { label: t("results_type_preliminary") ?? "Předběžné výsledky", className: "bg-emerald-100 text-emerald-800", }; } if (round.official_evaluation_run_id) { return { label: t("results_type_final") ?? "Finální výsledky", className: "bg-emerald-600 text-white", }; } const now = Date.now(); const startTime = round.start_time ? Date.parse(round.start_time) : null; const logsDeadline = round.logs_deadline ? Date.parse(round.logs_deadline) : null; if (startTime && !Number.isNaN(startTime) && now < startTime) { return { label: t("results_type_not_started") ?? "Závod ještě nezačal", className: "bg-gray-100 text-gray-700", }; } if ( logsDeadline && !Number.isNaN(logsDeadline) && now <= logsDeadline ) { return { label: t("results_type_log_collection_open") ?? "Otevřeno pro sběr logů", className: "bg-sky-100 text-sky-800", }; } return { label: t("results_type_declared") ?? "Deklarované výsledky", className: "bg-amber-100 text-amber-800", }; } export default function RoundsTable({ onSelectRound, enableEdit = true, onlyActive = false, showActiveColumn = true, showContestColumn = true, contestId = null, showTests = false, hideInactiveForGuests = false, isGuest = false, enableRowNavigation = false, roundLinkBuilder, roundsFromStore = null, title, }: RoundsTableProps) { const { t } = useTranslation("common"); const locale = useLanguageStore((s) => s.locale); const navigate = useNavigate(); const storeRounds = useContestStore((s) => s.selectedContestRounds); const refreshKey = useContestRefreshStore((s) => s.refreshKey); const [items, setItems] = useState(roundsFromStore ?? []); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [page, setPage] = useState(1); const [lastPage, setLastPage] = useState(1); const [useLocalPaging, setUseLocalPaging] = useState(false); const perPage = 20; const [selectedKeys, setSelectedKeys] = useState(new Set([])); const filterByTests = (list: Round[]): Round[] => showTests ? list : list.filter((r) => !r.is_test); const filterForGuests = (list: Round[]): Round[] => { if (!hideInactiveForGuests || !isGuest) return list; return list.filter((r) => r.is_active && (r.contest?.is_active ?? true)); }; useEffect(() => { const providedRounds = roundsFromStore ?? (storeRounds.length > 0 ? storeRounds : null); if (providedRounds && refreshKey === 0) { const filtered = filterForGuests(filterByTests(providedRounds as Round[])); setUseLocalPaging(true); setItems(filtered); setLastPage(Math.max(1, Math.ceil(filtered.length / perPage))); setLoading(false); setError(null); return; } let active = true; (async () => { try { setLoading(true); setError(null); setUseLocalPaging(false); const res = await axios.get | Round[]>( "/api/rounds", { withCredentials: true, headers: { Accept: "application/json" }, params: { lang: locale, contest_id: contestId ?? undefined, only_active: onlyActive ? 1 : 0, include_tests: showTests ? 1 : 0, per_page: perPage, page, }, } ); if (!active) return; const data = Array.isArray(res.data) ? res.data : (res.data as PaginatedResponse).data; const filtered = filterForGuests(filterByTests(data)); setItems(filtered); if (!Array.isArray(res.data)) { const meta = res.data as PaginatedResponse; const nextLastPage = meta.last_page ?? 1; setLastPage(nextLastPage > 0 ? nextLastPage : 1); } else { setLastPage(1); } } catch { if (!active) return; setError(t("unable_to_load_rounds") ?? "Nepodařilo se načíst seznam kol."); } finally { if (active) setLoading(false); } })(); return () => { active = false; }; }, [locale, t, roundsFromStore, storeRounds, contestId, onlyActive, showTests, refreshKey, page, perPage]); useEffect(() => { if (page > lastPage) { setPage(lastPage); } }, [page, lastPage]); const canEdit = Boolean(enableEdit && onSelectRound); const visibleItems = useLocalPaging ? filterForGuests(items).slice((page - 1) * perPage, page * perPage) : filterForGuests(items); if (loading) return
{t("rounds_loading") ?? "Načítám kola…"}
; if (error) return
{error}
; if (visibleItems.length === 0) { return
{t("rounds_empty") ?? "Žádná kola nejsou k dispozici."}
; } const columns = [ { key: "name", label: t("round_name") ?? "Kolo" }, ...(showContestColumn ? [{ key: "contest", label: t("round_contest") ?? "Závod" }] : []), { key: "schedule", label: t("round_schedule") ?? "Termín" }, ...(showActiveColumn ? [{ key: "is_active", label: t("round_active") ?? "Aktivní" }] : []), ...(canEdit ? [{ key: "actions", label: "" }] : []), ]; return (
{title &&

{title}

} {(column) => ( {column.label} )} {(item) => ( { if (!enableRowNavigation) return; const target = roundLinkBuilder ? roundLinkBuilder(item) : `/contests/${item.contest_id}/rounds/${item.id}`; navigate(target); }} className={enableRowNavigation ? "cursor-pointer hover:bg-default-100" : undefined} > {(columnKey) => { switch (columnKey) { case "name": { const name = resolveTranslation(item.name, locale); const description = resolveTranslation(item.description ?? null, locale); const resultsBadge = getResultsBadge(item, t); return (
{name} {description && ( {description} )}
{resultsBadge && ( {resultsBadge.label} )} {item.is_test && ( {t("round_test") ?? "Test"} )} {item.is_sixhr && ( 6h )} {item.is_mcr && ( MČR )}
); } case "contest": { const contestName = resolveTranslation( item.contest?.name ?? null, locale ); return ( {contestName || "—"} ); } case "schedule": return (
{formatDateTime(item.start_time, locale)} {formatDateTime(item.end_time, locale)} {item.logs_deadline && ( {(t("round_logs_deadline") ?? "Logy do") + ": "} {formatDateTime(item.logs_deadline, locale)} )}
); case "is_active": return ( {item.is_active ? t("yes") ?? "Ano" : t("no") ?? "Ne"} ); case "actions": return ( {canEdit && (
{onSelectRound && ( )}
)}
); default: return ; } }}
)}
{lastPage > 1 && (
)}
); }