Initial commit
This commit is contained in:
354
resources/js/components/RoundEvaluationOverrides.tsx
Normal file
354
resources/js/components/RoundEvaluationOverrides.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user