342 lines
11 KiB
TypeScript
342 lines
11 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import axios from "axios";
|
|
import { Spinner, useDisclosure } from "@heroui/react";
|
|
import RoundEvaluationLogOverridesTable from "@/components/RoundEvaluationLogOverridesTable";
|
|
import RoundEvaluationLogOverridesModal from "@/components/RoundEvaluationLogOverridesModal";
|
|
import RoundEvaluationLogOverridesSearch from "@/components/RoundEvaluationLogOverridesSearch";
|
|
import type {
|
|
LogOverride,
|
|
LogResult,
|
|
OverrideForm,
|
|
} from "@/components/RoundEvaluationLogOverrides.types";
|
|
|
|
type RoundDetail = {
|
|
id: number;
|
|
bands?: { id: number; name: string }[];
|
|
categories?: { id: number; name: string }[];
|
|
power_categories?: { id: number; name: string }[];
|
|
powerCategories?: { id: number; name: string }[];
|
|
};
|
|
|
|
type PaginatedResponse<T> = {
|
|
data: T[];
|
|
current_page: number;
|
|
last_page: number;
|
|
total: number;
|
|
};
|
|
|
|
type Props = {
|
|
roundId: number | null;
|
|
evaluationRunId: number;
|
|
};
|
|
|
|
export default function RoundEvaluationLogOverrides({ roundId, evaluationRunId }: Props) {
|
|
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
|
|
const [items, setItems] = useState<LogResult[]>([]);
|
|
const [overrides, setOverrides] = useState<Record<number, LogOverride>>({});
|
|
const [forms, setForms] = useState<Record<number, OverrideForm>>({});
|
|
const [loading, setLoading] = useState(false);
|
|
const [search, setSearch] = useState("");
|
|
const [roundDetail, setRoundDetail] = useState<RoundDetail | null>(null);
|
|
const [activeLogId, setActiveLogId] = useState<number | null>(null);
|
|
const perPage = 5000;
|
|
|
|
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]
|
|
);
|
|
|
|
const resolveName = (list: { id: number; name: string }[], id?: number | null) => {
|
|
if (!id) return "AUTO";
|
|
return list.find((item) => item.id === id)?.name ?? `#${id}`;
|
|
};
|
|
|
|
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]);
|
|
|
|
const fetchOverrides = useCallback(async (active?: { value: boolean }) => {
|
|
try {
|
|
const res = await axios.get<PaginatedResponse<LogOverride>>("/api/log-overrides", {
|
|
headers: { Accept: "application/json" },
|
|
params: { evaluation_run_id: evaluationRunId, per_page: 5000 },
|
|
withCredentials: true,
|
|
});
|
|
if (active && !active.value) return;
|
|
const map: Record<number, LogOverride> = {};
|
|
res.data.data.forEach((item) => {
|
|
map[item.log_id] = item;
|
|
});
|
|
setOverrides(map);
|
|
} catch {
|
|
if (active && !active.value) return;
|
|
setOverrides({});
|
|
}
|
|
}, [evaluationRunId]);
|
|
|
|
useEffect(() => {
|
|
const active = { value: true };
|
|
fetchOverrides(active);
|
|
return () => {
|
|
active.value = false;
|
|
};
|
|
}, [fetchOverrides]);
|
|
|
|
const fetchItems = useCallback(async (active?: { value: boolean }) => {
|
|
try {
|
|
setLoading(true);
|
|
const res = await axios.get<PaginatedResponse<LogResult>>("/api/log-results", {
|
|
headers: { Accept: "application/json" },
|
|
params: { evaluation_run_id: evaluationRunId, per_page: perPage },
|
|
withCredentials: true,
|
|
});
|
|
if (active && !active.value) return;
|
|
setItems(res.data.data);
|
|
} catch {
|
|
if (active && !active.value) return;
|
|
setItems([]);
|
|
} finally {
|
|
if (!active || active.value) setLoading(false);
|
|
}
|
|
}, [evaluationRunId, perPage]);
|
|
|
|
useEffect(() => {
|
|
const active = { value: true };
|
|
fetchItems(active);
|
|
return () => {
|
|
active.value = false;
|
|
};
|
|
}, [fetchItems]);
|
|
|
|
useEffect(() => {
|
|
if (!items.length) return;
|
|
setForms((prev) => {
|
|
const next = { ...prev };
|
|
for (const item of items) {
|
|
const override = overrides[item.log_id];
|
|
if (!next[item.log_id]) {
|
|
next[item.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) : "",
|
|
reason: override?.reason ?? "",
|
|
saving: false,
|
|
error: null,
|
|
success: null,
|
|
};
|
|
}
|
|
}
|
|
return next;
|
|
});
|
|
}, [items, overrides]);
|
|
|
|
const filteredItems = useMemo(() => {
|
|
const query = search.trim().toLowerCase();
|
|
if (!query) return items;
|
|
return items.filter((item) => {
|
|
const log = item.log;
|
|
const parts = [
|
|
String(item.log_id),
|
|
log?.pcall ?? "",
|
|
log?.pband ?? "",
|
|
log?.psect ?? "",
|
|
]
|
|
.join(" ")
|
|
.toLowerCase();
|
|
return parts.includes(query);
|
|
});
|
|
}, [items, search]);
|
|
|
|
const activeItem = useMemo(
|
|
() => (activeLogId ? items.find((item) => item.log_id === activeLogId) ?? null : null),
|
|
[items, activeLogId]
|
|
);
|
|
|
|
const openEditor = useCallback(
|
|
(logId: number) => {
|
|
setActiveLogId(logId);
|
|
onOpen();
|
|
},
|
|
[onOpen]
|
|
);
|
|
|
|
const handleFieldChange = (logId: number, field: keyof OverrideForm, value: string) => {
|
|
const emptyForm: OverrideForm = {
|
|
status: "AUTO",
|
|
bandId: "",
|
|
categoryId: "",
|
|
powerCategoryId: "",
|
|
reason: "",
|
|
saving: false,
|
|
error: null,
|
|
success: null,
|
|
};
|
|
setForms((prev) => ({
|
|
...prev,
|
|
[logId]: {
|
|
...(prev[logId] ?? emptyForm),
|
|
[field]: value,
|
|
error: null,
|
|
success: null,
|
|
},
|
|
}));
|
|
};
|
|
|
|
const saveOverride = async (logId: number) => {
|
|
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 hasAny = hasStatus || hasBand || hasCategory || hasPower;
|
|
const reason = 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) : "",
|
|
};
|
|
const hasChanges =
|
|
form.status !== baseline.status ||
|
|
form.bandId !== baseline.bandId ||
|
|
form.categoryId !== baseline.categoryId ||
|
|
form.powerCategoryId !== baseline.powerCategoryId;
|
|
|
|
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: "Bez změn." },
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (!hasChanges && override) {
|
|
setForms((prev) => ({
|
|
...prev,
|
|
[logId]: { ...prev[logId], saving: false, success: "Bez změn." },
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (!reason) {
|
|
setForms((prev) => ({
|
|
...prev,
|
|
[logId]: { ...prev[logId], saving: false, error: "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,
|
|
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: "Uloženo." },
|
|
}));
|
|
await Promise.all([fetchOverrides(), fetchItems()]);
|
|
onClose();
|
|
} catch (e: any) {
|
|
const msg = e?.response?.data?.message || "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">Ruční zásahy po agregaci</div>
|
|
<div className="text-xs text-foreground-500 mb-2">
|
|
Slouží pro korekce klasifikace a statusu logu před publikací.
|
|
</div>
|
|
<RoundEvaluationLogOverridesSearch search={search} onChange={setSearch} />
|
|
{loading && (
|
|
<div className="flex items-center gap-2 text-xs text-foreground-600">
|
|
<Spinner size="sm" /> Načítám výsledky…
|
|
</div>
|
|
)}
|
|
{!loading && filteredItems.length === 0 && (
|
|
<div className="text-xs text-foreground-600">Žádné položky k úpravě.</div>
|
|
)}
|
|
{filteredItems.length > 0 && (
|
|
<div className="space-y-4">
|
|
<RoundEvaluationLogOverridesTable
|
|
items={filteredItems}
|
|
overrides={overrides}
|
|
onEdit={openEditor}
|
|
/>
|
|
</div>
|
|
)}
|
|
<RoundEvaluationLogOverridesModal
|
|
isOpen={isOpen}
|
|
onOpenChange={onOpenChange}
|
|
onClose={onClose}
|
|
activeItem={activeItem}
|
|
override={activeItem ? overrides[activeItem.log_id] : undefined}
|
|
form={activeItem ? forms[activeItem.log_id] : undefined}
|
|
bands={bands}
|
|
categories={categories}
|
|
powerCategories={powerCategories}
|
|
onFieldChange={handleFieldChange}
|
|
onSave={saveOverride}
|
|
resolveName={resolveName}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|