Initial commit
This commit is contained in:
223
resources/js/pages/RoundDetailPage.tsx
Normal file
223
resources/js/pages/RoundDetailPage.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
import RoundDetail from "@/components/RoundDetail";
|
||||
import RoundFileUpload from "@/components/RoundFileUpload";
|
||||
import LogsTable from "@/components/LogsTable";
|
||||
import ResultsTables from "@/components/ResultsTables";
|
||||
import RoundEvaluationPanel from "@/components/RoundEvaluationPanel";
|
||||
import { Button, Card, CardHeader, CardBody, Divider, Tabs, Tab } from "@heroui/react";
|
||||
import { useContestStore } from "@/stores/contestStore";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
import axios from "axios";
|
||||
import useRoundEvaluationRun from "@/hooks/useRoundEvaluationRun";
|
||||
|
||||
export default function RoundDetailPage() {
|
||||
const { contestId, roundId } = useParams();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const cId = contestId ? Number(contestId) : null;
|
||||
const rId = roundId ? Number(roundId) : null;
|
||||
const selectedRound = useContestStore((s) => s.selectedRound);
|
||||
const user = useUserStore((s) => s.user);
|
||||
const [logsRefreshKey, setLogsRefreshKey] = useState(0);
|
||||
const selectedTab = searchParams.get("tab") ?? "verified";
|
||||
const [declaredFilter, setDeclaredFilter] = useState<"ALL" | "OK">("ALL");
|
||||
const [finalFilter, setFinalFilter] = useState<"ALL" | "OK">("ALL");
|
||||
const [finalResultLabel, setFinalResultLabel] = useState<string | null>(null);
|
||||
const [finalResultClass, setFinalResultClass] = useState<string>("");
|
||||
const [finalResultType, setFinalResultType] = useState<string | null>(null);
|
||||
const [recalcLoading, setRecalcLoading] = useState(false);
|
||||
const [recalcMessage, setRecalcMessage] = useState<string | null>(null);
|
||||
const [recalcError, setRecalcError] = useState<string | null>(null);
|
||||
const { run } = useRoundEvaluationRun(rId);
|
||||
const { t } = useTranslation("common");
|
||||
const roundDeadline =
|
||||
selectedRound?.id === rId ? selectedRound?.logs_deadline ?? null : null;
|
||||
const anonymousUploadClosed = () => {
|
||||
if (user) return false;
|
||||
if (!roundDeadline) return false;
|
||||
const deadline = new Date(roundDeadline);
|
||||
if (Number.isNaN(deadline.getTime())) return false;
|
||||
return new Date() > deadline;
|
||||
};
|
||||
const publicResultType = finalResultType ?? run?.result_type ?? null;
|
||||
const shouldShowEvaluatingMessage =
|
||||
!user &&
|
||||
((run && run.status !== "SUCCEEDED") || publicResultType === "TEST");
|
||||
const evaluatingLabel = t("results_evaluating") ?? "Vyhodnocuje se";
|
||||
const filterAllLabel = t("results_filter_all") ?? "Všechny výsledky";
|
||||
const filterOkLabel = t("results_filter_ok_ol") ?? "OK/OL závodníci";
|
||||
|
||||
const handleTabChange = (key: any) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("tab", String(key));
|
||||
setSearchParams(params, { replace: true });
|
||||
};
|
||||
|
||||
const handleRecalculate = async () => {
|
||||
if (!rId) return;
|
||||
try {
|
||||
setRecalcLoading(true);
|
||||
setRecalcMessage(null);
|
||||
setRecalcError(null);
|
||||
await axios.post(`/api/rounds/${rId}/recalculate-claimed`, null, {
|
||||
headers: { Accept: "application/json" },
|
||||
withCredentials: true,
|
||||
});
|
||||
setRecalcMessage(
|
||||
(t("declared_recalculate_started") as string) || "Přepočet byl spuštěn."
|
||||
);
|
||||
} catch (e: any) {
|
||||
const fallback =
|
||||
(t("declared_recalculate_failed") as string) ||
|
||||
"Nepodařilo se spustit přepočet.";
|
||||
const msg = e?.response?.data?.message || fallback;
|
||||
setRecalcError(msg);
|
||||
} finally {
|
||||
setRecalcLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<span className="text-md font-semibold">
|
||||
{t("round_detail_title") ?? "Detail kola"}
|
||||
</span>
|
||||
</CardHeader>
|
||||
<Divider />
|
||||
<CardBody>
|
||||
<RoundDetail roundId={rId} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
{user && <RoundEvaluationPanel roundId={rId} />}
|
||||
|
||||
{anonymousUploadClosed && (
|
||||
<RoundFileUpload
|
||||
roundId={rId}
|
||||
startTime={selectedRound?.start_time ?? null}
|
||||
logsDeadline={roundDeadline}
|
||||
onUploaded={() => setLogsRefreshKey((k) => k + 1)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card className="mt-4">
|
||||
<Tabs
|
||||
aria-label={t("round_detail_tabs_aria") ?? "Round detail tabs"}
|
||||
selectedKey={selectedTab}
|
||||
onSelectionChange={handleTabChange}
|
||||
>
|
||||
<Tab key="verified" title={t("results_tab") ?? "Výsledky"}>
|
||||
<CardBody>
|
||||
{shouldShowEvaluatingMessage ? (
|
||||
<div className="text-sm text-foreground-600">{evaluatingLabel}</div>
|
||||
) : (
|
||||
<>
|
||||
{finalResultLabel && (
|
||||
<div
|
||||
className={[
|
||||
"mb-2 inline-flex items-center rounded px-2 py-1 text-xs font-semibold",
|
||||
finalResultClass,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
{finalResultLabel}
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3 flex items-center gap-3 text-sm">
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="finalFilter"
|
||||
value="ALL"
|
||||
checked={finalFilter === "ALL"}
|
||||
onChange={() => setFinalFilter("ALL")}
|
||||
/>
|
||||
<span>{filterAllLabel}</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="finalFilter"
|
||||
value="OK"
|
||||
checked={finalFilter === "OK"}
|
||||
onChange={() => setFinalFilter("OK")}
|
||||
/>
|
||||
<span>{filterOkLabel}</span>
|
||||
</label>
|
||||
</div>
|
||||
<ResultsTables
|
||||
roundId={rId}
|
||||
contestId={cId}
|
||||
filter={finalFilter}
|
||||
mode="final"
|
||||
showResultTypeLabel={false}
|
||||
onResultTypeChange={(label, className, resultType) => {
|
||||
setFinalResultLabel(label);
|
||||
setFinalResultClass(className);
|
||||
setFinalResultType(resultType);
|
||||
}}
|
||||
refreshKey={run?.result_type ?? ""}
|
||||
evaluationRunId={run?.id ?? null}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CardBody>
|
||||
</Tab>
|
||||
<Tab key="declared" title={t("declared_results_tab") ?? "Deklarované výsledky"}>
|
||||
<CardBody>
|
||||
<div className="mb-3 text-sm text-foreground-600 space-y-1">
|
||||
<p>{t("declared_note_line1") ?? "Deklarované výsledky jsou předběžné výsledky OK a OL stanic."}</p>
|
||||
<p>{t("declared_note_line2") ?? "Zobrazené mezinárodní výsledky nejsou oficiální výsledky a slouží pouze pro porovnání."}</p>
|
||||
<p>{t("declared_note_line3") ?? "Deklarované výsledky jsou uspořádány na základě údajů v hlavičce EDI souborů v řádce CQSOP=. Další sloupce rovněž zobrazují data z deníku, které jsou kontrolovány pouze na správnost formátu zápisu."}</p>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center gap-3 text-sm">
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="declaredFilter"
|
||||
value="ALL"
|
||||
checked={declaredFilter === "ALL"}
|
||||
onChange={() => setDeclaredFilter("ALL")}
|
||||
/>
|
||||
<span>{filterAllLabel}</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="declaredFilter"
|
||||
value="OK"
|
||||
checked={declaredFilter === "OK"}
|
||||
onChange={() => setDeclaredFilter("OK")}
|
||||
/>
|
||||
<span>{filterOkLabel}</span>
|
||||
</label>
|
||||
{user && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={handleRecalculate}
|
||||
isLoading={recalcLoading}
|
||||
isDisabled={!rId}
|
||||
>
|
||||
{t("declared_recalculate") ?? "Přepočítat"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{recalcMessage && <div className="mb-2 text-sm text-green-600">{recalcMessage}</div>}
|
||||
{recalcError && <div className="mb-2 text-sm text-red-600">{recalcError}</div>}
|
||||
<ResultsTables roundId={rId} contestId={cId} filter={declaredFilter} />
|
||||
</CardBody>
|
||||
</Tab>
|
||||
<Tab key="logs" title={t("uploaded_logs_tab") ?? "Nahrané logy"}>
|
||||
<CardBody>
|
||||
<LogsTable roundId={rId} contestId={cId} refreshKey={logsRefreshKey} />
|
||||
</CardBody>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user