Initial commit
This commit is contained in:
326
resources/js/hooks/useRoundEvaluationRun.ts
Normal file
326
resources/js/hooks/useRoundEvaluationRun.ts
Normal file
@@ -0,0 +1,326 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type EvaluationRun = {
|
||||
id: number;
|
||||
round_id: number;
|
||||
name?: string | null;
|
||||
rules_version?: string | null;
|
||||
result_type?: string | null;
|
||||
status?: string | null;
|
||||
current_step?: string | null;
|
||||
created_at?: string | null;
|
||||
started_at?: string | null;
|
||||
finished_at?: string | null;
|
||||
progress_total?: number | null;
|
||||
progress_done?: number | null;
|
||||
};
|
||||
|
||||
export type EvaluationRunEvent = {
|
||||
id: number;
|
||||
level: string;
|
||||
message: string;
|
||||
context?: Record<string, unknown> | null;
|
||||
created_at?: string | null;
|
||||
};
|
||||
|
||||
type PaginatedResponse<T> = {
|
||||
data: T[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
const isActiveStatus = (status?: string | null) =>
|
||||
status === "PENDING" ||
|
||||
status === "RUNNING" ||
|
||||
status === "WAITING_REVIEW_INPUT" ||
|
||||
status === "WAITING_REVIEW_MATCH" ||
|
||||
status === "WAITING_REVIEW_SCORE";
|
||||
|
||||
const isOfficialRun = (run: EvaluationRun | null) =>
|
||||
!!run && run.rules_version !== "CLAIMED";
|
||||
|
||||
export const steps = [
|
||||
{ key: "prepare", label: "Prepare" },
|
||||
{ key: "parse_logs", label: "Parse" },
|
||||
{ key: "build_working_set", label: "Working set" },
|
||||
{ key: "waiting_review_input", label: "Review input" },
|
||||
{ key: "match", label: "Match" },
|
||||
{ key: "waiting_review_match", label: "Review match" },
|
||||
{ key: "score", label: "Score" },
|
||||
{ key: "aggregate", label: "Aggregate" },
|
||||
{ key: "waiting_review_score", label: "Review score" },
|
||||
{ key: "finalize", label: "Finalize" },
|
||||
];
|
||||
|
||||
export default function useRoundEvaluationRun(roundId: number | null) {
|
||||
const { i18n } = useTranslation();
|
||||
const [run, setRun] = useState<EvaluationRun | null>(null);
|
||||
const [runs, setRuns] = useState<EvaluationRun[]>([]);
|
||||
const [events, setEvents] = useState<EvaluationRunEvent[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [actionLoading, setActionLoading] = useState(false);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pendingMessageClear, setPendingMessageClear] = useState(false);
|
||||
const [lastKnownStep, setLastKnownStep] = useState<string | null>(null);
|
||||
const [hasLoaded, setHasLoaded] = useState(false);
|
||||
|
||||
const load = async (opts?: { silent?: boolean }) => {
|
||||
if (!roundId) return;
|
||||
try {
|
||||
if (!opts?.silent) {
|
||||
setLoading(true);
|
||||
}
|
||||
if (!opts?.silent) {
|
||||
setError(null);
|
||||
}
|
||||
const res = await axios.get<PaginatedResponse<EvaluationRun>>("/api/evaluation-runs", {
|
||||
headers: { Accept: "application/json" },
|
||||
params: { round_id: roundId, per_page: 20 },
|
||||
withCredentials: true,
|
||||
});
|
||||
const officialRuns = res.data.data.filter((item) => item.rules_version !== "CLAIMED");
|
||||
const limitedRuns = officialRuns.slice(0, 3);
|
||||
const latest = limitedRuns[0] ?? null;
|
||||
setRuns(limitedRuns);
|
||||
setRun(latest);
|
||||
setHasLoaded(true);
|
||||
if (pendingMessageClear) {
|
||||
setMessage(null);
|
||||
setPendingMessageClear(false);
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (!opts?.silent) {
|
||||
const msg = e?.response?.data?.message || "Nepodařilo se načíst stav vyhodnocení.";
|
||||
setError(msg);
|
||||
}
|
||||
} finally {
|
||||
if (!opts?.silent) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchEvents = async (runId: number) => {
|
||||
try {
|
||||
const res = await axios.get<EvaluationRunEvent[]>(
|
||||
`/api/evaluation-runs/${runId}/events`,
|
||||
{
|
||||
headers: { Accept: "application/json" },
|
||||
params: { limit: 8, min_level: "warning" },
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
setEvents(res.data);
|
||||
} catch {
|
||||
setEvents([]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [roundId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!run || !isOfficialRun(run)) return;
|
||||
fetchEvents(run.id);
|
||||
}, [run?.id]);
|
||||
|
||||
const canStart = useMemo(() => {
|
||||
if (!roundId) return false;
|
||||
if (!run) return true;
|
||||
return !isActiveStatus(run.status);
|
||||
}, [roundId, run]);
|
||||
|
||||
const canResume =
|
||||
run?.status === "WAITING_REVIEW_INPUT" ||
|
||||
run?.status === "WAITING_REVIEW_MATCH" ||
|
||||
run?.status === "WAITING_REVIEW_SCORE";
|
||||
const canCancel = run && isActiveStatus(run.status);
|
||||
const shouldPollRun =
|
||||
run && isOfficialRun(run) && (run.status === "RUNNING" || run.status === "PENDING");
|
||||
const shouldPollEvents = shouldPollRun;
|
||||
|
||||
const currentStepIndex = run
|
||||
? steps.findIndex((step) => step.key === (run.current_step ?? lastKnownStep))
|
||||
: -1;
|
||||
const isSucceeded = run?.status === "SUCCEEDED";
|
||||
const stepProgressPercent =
|
||||
run && run.progress_total && run.progress_total > 0
|
||||
? Math.round((run.progress_done / run.progress_total) * 100)
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!run || !shouldPollEvents) return;
|
||||
const interval = window.setInterval(() => {
|
||||
fetchEvents(run.id);
|
||||
}, 1000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [run?.id, shouldPollEvents]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!run || !shouldPollRun) return;
|
||||
const interval = window.setInterval(() => {
|
||||
load({ silent: true });
|
||||
}, 1000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [run?.id, shouldPollRun]);
|
||||
|
||||
useEffect(() => {
|
||||
if (run?.current_step) {
|
||||
setLastKnownStep(run.current_step);
|
||||
}
|
||||
}, [run?.current_step]);
|
||||
|
||||
const formatEventTime = (value?: string | null) => {
|
||||
if (!value) return null;
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
const locale = i18n.language?.startsWith("cs") ? "cs-CZ" : "en-US";
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
const handleStart = async () => {
|
||||
if (!roundId) return;
|
||||
try {
|
||||
setActionLoading(true);
|
||||
setMessage(null);
|
||||
setError(null);
|
||||
const res = await axios.post(`/api/rounds/${roundId}/evaluation-runs/start`, null, {
|
||||
headers: { Accept: "application/json" },
|
||||
withCredentials: true,
|
||||
withXSRFToken: true,
|
||||
});
|
||||
setRun(res.data);
|
||||
setMessage("Vyhodnocení bylo spuštěno.");
|
||||
setPendingMessageClear(true);
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data?.message || "Nepodařilo se spustit vyhodnocení.";
|
||||
setError(msg);
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartIncremental = async () => {
|
||||
if (!roundId) return;
|
||||
try {
|
||||
setActionLoading(true);
|
||||
setMessage(null);
|
||||
setError(null);
|
||||
const res = await axios.post(`/api/rounds/${roundId}/evaluation-runs/start-incremental`, null, {
|
||||
headers: { Accept: "application/json" },
|
||||
withCredentials: true,
|
||||
withXSRFToken: true,
|
||||
});
|
||||
setRun(res.data);
|
||||
setMessage("Vyhodnocení bylo spuštěno znovu.");
|
||||
setPendingMessageClear(true);
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data?.message || "Nepodařilo se spustit vyhodnocení znovu.";
|
||||
setError(msg);
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResume = async () => {
|
||||
if (!run) return;
|
||||
try {
|
||||
setActionLoading(true);
|
||||
setMessage(null);
|
||||
setError(null);
|
||||
await axios.post(`/api/evaluation-runs/${run.id}/resume`, null, {
|
||||
headers: { Accept: "application/json" },
|
||||
withCredentials: true,
|
||||
withXSRFToken: true,
|
||||
});
|
||||
setMessage("Pokračování bylo spuštěno.");
|
||||
setPendingMessageClear(true);
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data?.message || "Nepodařilo se pokračovat.";
|
||||
setError(msg);
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = async () => {
|
||||
if (!run) return;
|
||||
try {
|
||||
setActionLoading(true);
|
||||
setMessage(null);
|
||||
setError(null);
|
||||
await axios.post(`/api/evaluation-runs/${run.id}/cancel`, null, {
|
||||
headers: { Accept: "application/json" },
|
||||
withCredentials: true,
|
||||
withXSRFToken: true,
|
||||
});
|
||||
setMessage("Vyhodnocení bylo zrušeno.");
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data?.message || "Nepodařilo se zrušit běh.";
|
||||
setError(msg);
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetResultType = async (resultType: "PRELIMINARY" | "FINAL" | "TEST") => {
|
||||
if (!run) return;
|
||||
try {
|
||||
setActionLoading(true);
|
||||
setMessage(null);
|
||||
setError(null);
|
||||
const res = await axios.post(
|
||||
`/api/evaluation-runs/${run.id}/result-type`,
|
||||
{ result_type: resultType },
|
||||
{
|
||||
headers: { Accept: "application/json" },
|
||||
withCredentials: true,
|
||||
withXSRFToken: true,
|
||||
}
|
||||
);
|
||||
setRun(res.data);
|
||||
setMessage("Typ výsledků byl uložen.");
|
||||
setPendingMessageClear(true);
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data?.message || "Nepodařilo se uložit typ výsledků.";
|
||||
setError(msg);
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
run,
|
||||
runs,
|
||||
events,
|
||||
loading,
|
||||
actionLoading,
|
||||
message,
|
||||
error,
|
||||
hasLoaded,
|
||||
canStart,
|
||||
canResume,
|
||||
canCancel,
|
||||
shouldPollRun,
|
||||
isOfficialRun: isOfficialRun(run),
|
||||
currentStepIndex,
|
||||
isSucceeded,
|
||||
stepProgressPercent,
|
||||
formatEventTime,
|
||||
handleStart,
|
||||
handleStartIncremental,
|
||||
handleResume,
|
||||
handleCancel,
|
||||
handleSetResultType,
|
||||
};
|
||||
}
|
||||
151
resources/js/hooks/useRoundMeta.ts
Normal file
151
resources/js/hooks/useRoundMeta.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
export type BandOption = {
|
||||
id: number;
|
||||
name: string;
|
||||
order?: number | null;
|
||||
has_power_category?: boolean | null;
|
||||
};
|
||||
|
||||
export type EdiCategoryOption = {
|
||||
id: number;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type EdiBandOption = {
|
||||
id: number;
|
||||
value: string;
|
||||
bands?: { id: number; name: string }[];
|
||||
};
|
||||
|
||||
type SelectedRoundLike = {
|
||||
id?: number | null;
|
||||
bands?: BandOption[];
|
||||
} | null;
|
||||
|
||||
export const useRoundMeta = (roundId: number | null, selectedRound: SelectedRoundLike) => {
|
||||
const [bands, setBands] = useState<BandOption[]>([]);
|
||||
const [bandsError, setBandsError] = useState<string | null>(null);
|
||||
const [bandsLoading, setBandsLoading] = useState(false);
|
||||
const [ediCategories, setEdiCategories] = useState<EdiCategoryOption[]>([]);
|
||||
const [ediCategoriesError, setEdiCategoriesError] = useState<string | null>(null);
|
||||
const [ediCategoriesLoading, setEdiCategoriesLoading] = useState(false);
|
||||
const [ediBands, setEdiBands] = useState<EdiBandOption[]>([]);
|
||||
|
||||
// načti dostupné bandy pro select PBand
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
(async () => {
|
||||
try {
|
||||
setBandsLoading(true);
|
||||
setBandsError(null);
|
||||
|
||||
let bandList: BandOption[] = [];
|
||||
|
||||
if (roundId && selectedRound?.id === roundId && Array.isArray(selectedRound.bands)) {
|
||||
bandList = selectedRound.bands as BandOption[];
|
||||
}
|
||||
|
||||
if (roundId) {
|
||||
try {
|
||||
const roundRes = await axios.get<any>(`/api/rounds/${roundId}`, {
|
||||
headers: { Accept: "application/json" },
|
||||
withCredentials: true,
|
||||
});
|
||||
if (Array.isArray(roundRes.data?.bands) && roundRes.data.bands.length > 0) {
|
||||
bandList = roundRes.data.bands;
|
||||
}
|
||||
} catch {
|
||||
// fallback handled below
|
||||
}
|
||||
}
|
||||
|
||||
if (!bandList.length) {
|
||||
const res = await axios.get<BandOption[] | { data: BandOption[] }>("/api/bands", {
|
||||
headers: { Accept: "application/json" },
|
||||
withCredentials: true,
|
||||
});
|
||||
bandList = Array.isArray(res.data) ? res.data : (res.data as any)?.data ?? [];
|
||||
}
|
||||
|
||||
if (!active) return;
|
||||
const sorted = [...bandList].sort((a, b) => {
|
||||
const aOrder = a.order ?? 0;
|
||||
const bOrder = b.order ?? 0;
|
||||
if (aOrder !== bOrder) return aOrder - bOrder;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
setBands(sorted);
|
||||
} catch {
|
||||
if (!active) return;
|
||||
setBandsError("Nepodařilo se načíst seznam pásem.");
|
||||
} finally {
|
||||
if (active) setBandsLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [roundId, selectedRound]);
|
||||
|
||||
// načti dostupné EDI kategorie pro validaci PSect
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
(async () => {
|
||||
try {
|
||||
setEdiCategoriesLoading(true);
|
||||
setEdiCategoriesError(null);
|
||||
const res = await axios.get<EdiCategoryOption[] | { data: EdiCategoryOption[] }>("/api/edi-categories", {
|
||||
headers: { Accept: "application/json" },
|
||||
withCredentials: true,
|
||||
});
|
||||
if (!active) return;
|
||||
const raw = Array.isArray(res.data) ? res.data : (res.data as any)?.data ?? [];
|
||||
const sorted = [...raw].sort((a, b) => a.value.localeCompare(b.value));
|
||||
setEdiCategories(sorted);
|
||||
} catch {
|
||||
if (!active) return;
|
||||
setEdiCategoriesError("Nepodařilo se načíst seznam kategorií.");
|
||||
} finally {
|
||||
if (active) setEdiCategoriesLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// načti EDI bandy pro mapování PBand -> Band
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await axios.get<EdiBandOption[] | { data: EdiBandOption[] }>("/api/edi-bands", {
|
||||
headers: { Accept: "application/json" },
|
||||
withCredentials: true,
|
||||
params: { per_page: 500 },
|
||||
});
|
||||
if (!active) return;
|
||||
const raw = Array.isArray(res.data) ? res.data : (res.data as any)?.data ?? [];
|
||||
setEdiBands(raw);
|
||||
} catch {
|
||||
if (!active) return;
|
||||
// nechávám tiché selhání, mapování je nice-to-have
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
bands,
|
||||
bandsError,
|
||||
bandsLoading,
|
||||
ediCategories,
|
||||
ediCategoriesError,
|
||||
ediCategoriesLoading,
|
||||
ediBands,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user