Initial commit

This commit is contained in:
Zdeněk Burda
2026-01-09 21:26:40 +01:00
parent e83aec6dca
commit 41e3ce6f25
404 changed files with 61250 additions and 28 deletions

View File

@@ -0,0 +1,354 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import axios from "axios";
import { useDisclosure } from "@heroui/react";
import { useTranslation } from "react-i18next";
import RoundEvaluationOverrideRow from "@/components/RoundEvaluationOverrideRow";
import RoundEvaluationOverrideDetailModal from "@/components/RoundEvaluationOverrideDetailModal";
import RoundEvaluationOverridesPagination from "@/components/RoundEvaluationOverridesPagination";
import type {
LogItem,
LogOverride,
OverrideForm,
RoundDetail,
} from "@/components/RoundEvaluationOverrides.types";
type PaginatedResponse<T> = {
data: T[];
current_page: number;
last_page: number;
total: number;
};
type Props = {
roundId: number | null;
evaluationRunId: number;
};
export default function RoundEvaluationOverrides({ roundId, evaluationRunId }: Props) {
const [logs, setLogs] = useState<LogItem[]>([]);
const [overrides, setOverrides] = useState<Record<number, LogOverride>>({});
const [forms, setForms] = useState<Record<number, OverrideForm>>({});
const [page, setPage] = useState(1);
const [lastPage, setLastPage] = useState(1);
const [roundDetail, setRoundDetail] = useState<RoundDetail | null>(null);
const [loading, setLoading] = useState(false);
const [activeLogId, setActiveLogId] = useState<number | null>(null);
const perPage = 30;
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const { t } = useTranslation("common");
const bands = useMemo(() => roundDetail?.bands ?? [], [roundDetail?.bands]);
const categories = useMemo(() => roundDetail?.categories ?? [], [roundDetail?.categories]);
const powerCategories = useMemo(
() => roundDetail?.powerCategories ?? roundDetail?.power_categories ?? [],
[roundDetail?.powerCategories, roundDetail?.power_categories]
);
useEffect(() => {
if (!roundId) return;
let active = true;
(async () => {
try {
const res = await axios.get<RoundDetail>(`/api/rounds/${roundId}`, {
headers: { Accept: "application/json" },
withCredentials: true,
});
if (!active) return;
setRoundDetail(res.data);
} catch {
if (!active) return;
setRoundDetail(null);
}
})();
return () => {
active = false;
};
}, [roundId]);
useEffect(() => {
if (!roundId) return;
let active = true;
(async () => {
try {
setLoading(true);
const res = await axios.get<PaginatedResponse<LogItem>>("/api/logs", {
headers: { Accept: "application/json" },
params: { round_id: roundId, per_page: perPage, page },
withCredentials: true,
});
if (!active) return;
setLogs(res.data.data);
setLastPage(res.data.last_page ?? 1);
} catch {
if (!active) return;
setLogs([]);
} finally {
if (active) setLoading(false);
}
})();
return () => {
active = false;
};
}, [roundId, page]);
useEffect(() => {
let active = true;
(async () => {
try {
const res = await axios.get<PaginatedResponse<LogOverride>>("/api/log-overrides", {
headers: { Accept: "application/json" },
params: { evaluation_run_id: evaluationRunId, per_page: 500 },
withCredentials: true,
});
if (!active) return;
const map: Record<number, LogOverride> = {};
res.data.data.forEach((item) => {
map[item.log_id] = item;
});
setOverrides(map);
} catch {
if (!active) return;
setOverrides({});
}
})();
return () => {
active = false;
};
}, [evaluationRunId]);
useEffect(() => {
if (!logs.length) return;
setForms((prev) => {
const next = { ...prev };
for (const log of logs) {
const override = overrides[log.id];
if (!next[log.id]) {
next[log.id] = {
status: override?.forced_log_status ?? "AUTO",
bandId: override?.forced_band_id ? String(override.forced_band_id) : "",
categoryId: override?.forced_category_id ? String(override.forced_category_id) : "",
powerCategoryId: override?.forced_power_category_id ? String(override.forced_power_category_id) : "",
sixhrCategory:
override?.forced_sixhr_category === null || override?.forced_sixhr_category === undefined
? ""
: override.forced_sixhr_category
? "1"
: "0",
reason: override?.reason ?? "",
saving: false,
error: null,
success: null,
};
}
}
return next;
});
}, [logs, overrides]);
useEffect(() => {
if (!isOpen) {
setActiveLogId(null);
}
}, [isOpen]);
const openDetail = useCallback(
(logId: number) => {
setActiveLogId(logId);
onOpen();
},
[onOpen]
);
const handleFieldChange = useCallback(
(logId: number, field: keyof OverrideForm, value: string) => {
setForms((prev) => ({
...prev,
[logId]: {
...prev[logId],
[field]: value,
error: null,
success: null,
},
}));
},
[]
);
const commitReason = useCallback((logId: number, reason: string) => {
setForms((prev) => ({
...prev,
[logId]: {
...prev[logId],
reason,
error: null,
success: null,
},
}));
}, []);
const saveOverride = async (logId: number, reasonOverride?: string) => {
const form = forms[logId];
if (!form) return;
const override = overrides[logId];
const hasStatus = form.status && form.status !== "AUTO";
const hasBand = form.bandId !== "";
const hasCategory = form.categoryId !== "";
const hasPower = form.powerCategoryId !== "";
const hasSixhr = form.sixhrCategory !== "";
const hasAny = hasStatus || hasBand || hasCategory || hasPower || hasSixhr;
if (reasonOverride !== undefined) {
commitReason(logId, reasonOverride);
}
const reason = (reasonOverride ?? form.reason).trim();
const baseline = {
status: override?.forced_log_status ?? "AUTO",
bandId: override?.forced_band_id ? String(override.forced_band_id) : "",
categoryId: override?.forced_category_id ? String(override.forced_category_id) : "",
powerCategoryId: override?.forced_power_category_id ? String(override.forced_power_category_id) : "",
sixhrCategory:
override?.forced_sixhr_category === null || override?.forced_sixhr_category === undefined
? ""
: override.forced_sixhr_category
? "1"
: "0",
};
const hasChanges =
form.status !== baseline.status ||
form.bandId !== baseline.bandId ||
form.categoryId !== baseline.categoryId ||
form.powerCategoryId !== baseline.powerCategoryId ||
form.sixhrCategory !== baseline.sixhrCategory;
setForms((prev) => ({
...prev,
[logId]: { ...prev[logId], saving: true, error: null, success: null },
}));
try {
if (!hasChanges && !override) {
setForms((prev) => ({
...prev,
[logId]: { ...prev[logId], saving: false, success: t("override_no_changes") ?? "Bez změn." },
}));
return;
}
if (!hasChanges && override) {
setForms((prev) => ({
...prev,
[logId]: { ...prev[logId], saving: false, success: t("override_no_changes") ?? "Bez změn." },
}));
return;
}
if (!reason) {
setForms((prev) => ({
...prev,
[logId]: {
...prev[logId],
saving: false,
error: t("override_reason_required") ?? "Doplň důvod změny.",
},
}));
return;
}
const payload = {
evaluation_run_id: evaluationRunId,
log_id: logId,
forced_log_status: form.status || "AUTO",
forced_band_id: form.bandId ? Number(form.bandId) : null,
forced_category_id: form.categoryId ? Number(form.categoryId) : null,
forced_power_category_id: form.powerCategoryId ? Number(form.powerCategoryId) : null,
forced_sixhr_category: form.sixhrCategory === "" ? null : form.sixhrCategory === "1",
reason,
};
if (override) {
const res = await axios.put<LogOverride>(`/api/log-overrides/${override.id}`, payload, {
headers: { Accept: "application/json" },
withCredentials: true,
withXSRFToken: true,
});
setOverrides((prev) => ({ ...prev, [logId]: res.data }));
} else {
const res = await axios.post<LogOverride>("/api/log-overrides", payload, {
headers: { Accept: "application/json" },
withCredentials: true,
withXSRFToken: true,
});
setOverrides((prev) => ({ ...prev, [logId]: res.data }));
}
setForms((prev) => ({
...prev,
[logId]: { ...prev[logId], saving: false, success: t("override_saved") ?? "Uloženo." },
}));
} catch (e: any) {
const msg =
e?.response?.data?.message ||
(t("override_save_failed") ?? "Nepodařilo se uložit override.");
setForms((prev) => ({
...prev,
[logId]: { ...prev[logId], saving: false, error: msg },
}));
}
};
if (!roundId) return null;
return (
<div className="pt-2 border-t border-divider">
<div className="font-semibold text-sm mb-2">
{t("override_pre_match_title") ?? "Ruční zásahy před matchingem"}
</div>
<div className="text-xs text-foreground-500 mb-2">
{t("override_pre_match_hint") ??
"Změny se projeví po kliknutí na „Pokračovat“. IGNORED vyřadí log z matchingu."}
</div>
{loading && (
<div className="text-xs text-foreground-600">
{t("override_loading_logs") ?? "Načítám logy…"}
</div>
)}
{!loading && logs.length === 0 && (
<div className="text-xs text-foreground-600">
{t("override_no_logs") ?? "Žádné logy k úpravě."}
</div>
)}
{logs.length > 0 && (
<div className="space-y-3">
<div className="grid gap-2">
{logs.map((log) => (
<RoundEvaluationOverrideRow
key={log.id}
log={log}
form={forms[log.id]}
override={overrides[log.id]}
bands={bands}
categories={categories}
powerCategories={powerCategories}
onFieldChange={handleFieldChange}
onReasonCommit={commitReason}
onSave={saveOverride}
onOpenDetail={openDetail}
/>
))}
</div>
<RoundEvaluationOverridesPagination
page={page}
lastPage={lastPage}
onPrev={() => setPage((p) => Math.max(1, p - 1))}
onNext={() => setPage((p) => Math.min(lastPage, p + 1))}
/>
</div>
)}
<RoundEvaluationOverrideDetailModal
isOpen={isOpen}
logId={activeLogId}
onOpenChange={onOpenChange}
/>
</div>
);
}