Initial commit
This commit is contained in:
341
resources/js/components/RoundEvaluationLogOverrides.tsx
Normal file
341
resources/js/components/RoundEvaluationLogOverrides.tsx
Normal file
@@ -0,0 +1,341 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user