355 lines
13 KiB
TypeScript
355 lines
13 KiB
TypeScript
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>
|
|
);
|
|
}
|