515 lines
22 KiB
TypeScript
515 lines
22 KiB
TypeScript
import React, { useEffect, useMemo, useState } from "react";
|
||
import axios from "axios";
|
||
import { Button, Modal, ModalBody, ModalContent, ModalHeader, useDisclosure } from "@heroui/react";
|
||
import Markdown from "react-markdown";
|
||
import { useLanguageStore } from "@/stores/languageStore";
|
||
import { useTranslation } from "react-i18next";
|
||
import AdminRulesetForm from "@/components/admin/rulesets/AdminRulesetForm";
|
||
import AdminRulesetsTable from "@/components/admin/rulesets/AdminRulesetsTable";
|
||
import {
|
||
emptyForm,
|
||
isBlank,
|
||
isIntegerLike,
|
||
isNumberLike,
|
||
numberValue,
|
||
toIntOrNull,
|
||
toNumberOrNull,
|
||
type EvaluationRuleSet,
|
||
type RuleSetForm,
|
||
type RuleSetFormMode,
|
||
} from "@/components/admin/rulesets/adminRulesetTypes";
|
||
import rulesetDoc from "../../docs/EvaluationRuleSet.md?raw";
|
||
|
||
const RULESET_LOADERS = {
|
||
en: () => import("../locales/en/ruleset.json"),
|
||
cs: () => import("../locales/cs/ruleset.json"),
|
||
} as const;
|
||
|
||
type PaginatedResponse<T> = { data: T[] };
|
||
|
||
export default function AdminEvaluationPage() {
|
||
const locale = useLanguageStore((s) => s.locale);
|
||
const { t } = useTranslation("common");
|
||
const { t: tRules, i18n } = useTranslation("ruleset");
|
||
const valuePlaceholder = t("value_na") ?? "—";
|
||
const label = (key: string, fallback: string) => (t(key) as string) ?? fallback;
|
||
const [items, setItems] = useState<EvaluationRuleSet[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [refreshKey, setRefreshKey] = useState(0);
|
||
const [formMode, setFormMode] = useState<RuleSetFormMode>("none");
|
||
const [editing, setEditing] = useState<EvaluationRuleSet | null>(null);
|
||
const [form, setForm] = useState<RuleSetForm>(emptyForm);
|
||
const [initialForm, setInitialForm] = useState<RuleSetForm>(emptyForm);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [formError, setFormError] = useState<string | null>(null);
|
||
const [formSuccess, setFormSuccess] = useState<string | null>(null);
|
||
const { isOpen: helpOpen, onOpen: openHelp, onOpenChange: onHelpChange } = useDisclosure();
|
||
|
||
useEffect(() => {
|
||
let active = true;
|
||
const loadNamespace = async (lang: "cs" | "en") => {
|
||
if (i18n.hasResourceBundle(lang, "ruleset")) return;
|
||
const loader = RULESET_LOADERS[lang];
|
||
if (!loader) return;
|
||
const module = await loader();
|
||
if (!active) return;
|
||
const data = module.default ?? module;
|
||
i18n.addResourceBundle(lang, "ruleset", data, true, true);
|
||
};
|
||
|
||
const lang = i18n.language?.startsWith("cs") ? "cs" : "en";
|
||
loadNamespace(lang);
|
||
if (lang !== "en") {
|
||
loadNamespace("en");
|
||
}
|
||
|
||
return () => {
|
||
active = false;
|
||
};
|
||
}, [i18n, i18n.language]);
|
||
|
||
useEffect(() => {
|
||
let active = true;
|
||
(async () => {
|
||
try {
|
||
setLoading(true);
|
||
setError(null);
|
||
const res = await axios.get<PaginatedResponse<EvaluationRuleSet> | EvaluationRuleSet[]>(
|
||
"/api/evaluation-rule-sets",
|
||
{
|
||
headers: { Accept: "application/json" },
|
||
params: { per_page: 200 },
|
||
withCredentials: true,
|
||
}
|
||
);
|
||
if (!active) return;
|
||
const data = Array.isArray(res.data) ? res.data : res.data.data;
|
||
setItems(data);
|
||
} catch {
|
||
if (!active) return;
|
||
setError(t("admin_rulesets_load_failed") ?? "Nepodařilo se načíst rule sety.");
|
||
} finally {
|
||
if (active) setLoading(false);
|
||
}
|
||
})();
|
||
return () => {
|
||
active = false;
|
||
};
|
||
}, [locale, refreshKey]);
|
||
|
||
const resetForm = () => {
|
||
setForm(emptyForm);
|
||
setInitialForm(emptyForm);
|
||
setEditing(null);
|
||
setFormError(null);
|
||
setFormSuccess(null);
|
||
};
|
||
|
||
const helpLabel = (labelText: string, key: string) => {
|
||
const fieldKey = key.startsWith("ruleset_help_") ? key.slice("ruleset_help_".length) : key;
|
||
return (
|
||
<span className="inline-flex flex-col leading-tight" title={tRules(key)}>
|
||
<span>{labelText}</span>
|
||
<span className="text-[0.8em] text-foreground-400 font-mono">{fieldKey}</span>
|
||
</span>
|
||
);
|
||
};
|
||
|
||
const formatDate = (value: string | null | undefined, lang: string) => {
|
||
if (!value) return valuePlaceholder;
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return valuePlaceholder;
|
||
return new Intl.DateTimeFormat(lang, {
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric",
|
||
}).format(date);
|
||
};
|
||
|
||
const openEdit = (item: EvaluationRuleSet) => {
|
||
resetForm();
|
||
setEditing(item);
|
||
setFormMode("edit");
|
||
const nextForm: RuleSetForm = {
|
||
name: item.name ?? "",
|
||
code: item.code ?? "",
|
||
description: item.description ?? "",
|
||
scoring_mode: item.scoring_mode ?? "DISTANCE",
|
||
points_per_qso: numberValue(item.points_per_qso),
|
||
points_per_km: numberValue(item.points_per_km),
|
||
use_multipliers: !!item.use_multipliers,
|
||
multiplier_type: item.multiplier_type ?? "WWL",
|
||
dup_qso_policy: item.dup_qso_policy === "COUNT_ONCE" ? "ZERO_POINTS" : item.dup_qso_policy ?? "ZERO_POINTS",
|
||
nil_qso_policy: item.nil_qso_policy ?? "PENALTY",
|
||
no_counterpart_log_policy: item.no_counterpart_log_policy ?? item.nil_qso_policy ?? "PENALTY",
|
||
not_in_counterpart_log_policy:
|
||
item.not_in_counterpart_log_policy ?? item.nil_qso_policy ?? "PENALTY",
|
||
unique_qso_policy: item.unique_qso_policy ?? "ZERO_POINTS",
|
||
busted_call_policy: item.busted_call_policy ?? "PENALTY",
|
||
busted_rst_policy: item.busted_rst_policy ?? "ZERO_POINTS",
|
||
busted_exchange_policy: item.busted_exchange_policy ?? "ZERO_POINTS",
|
||
busted_serial_policy: item.busted_serial_policy ?? item.busted_exchange_policy ?? "ZERO_POINTS",
|
||
busted_locator_policy: item.busted_locator_policy ?? item.busted_exchange_policy ?? "ZERO_POINTS",
|
||
penalty_dup_points: numberValue(item.penalty_dup_points),
|
||
penalty_nil_points: numberValue(item.penalty_nil_points),
|
||
penalty_busted_call_points: numberValue(item.penalty_busted_call_points),
|
||
penalty_busted_rst_points: numberValue(item.penalty_busted_rst_points),
|
||
penalty_busted_exchange_points: numberValue(item.penalty_busted_exchange_points),
|
||
penalty_busted_serial_points: numberValue(item.penalty_busted_serial_points),
|
||
penalty_busted_locator_points: numberValue(item.penalty_busted_locator_points),
|
||
penalty_out_of_window_points: numberValue(item.penalty_out_of_window_points),
|
||
dupe_scope: item.dupe_scope ?? "BAND",
|
||
callsign_normalization: item.callsign_normalization ?? "IGNORE_SUFFIX",
|
||
distance_rounding: item.distance_rounding ?? "FLOOR",
|
||
min_distance_km: numberValue(item.min_distance_km),
|
||
require_locators: item.require_locators ?? true,
|
||
out_of_window_policy: item.out_of_window_policy ?? "INVALID",
|
||
exchange_type: item.exchange_type ?? "SERIAL_WWL",
|
||
exchange_requires_wwl: item.exchange_requires_wwl ?? true,
|
||
exchange_requires_serial: item.exchange_requires_serial ?? true,
|
||
exchange_requires_report: item.exchange_requires_report ?? false,
|
||
exchange_pattern: item.exchange_pattern ?? "",
|
||
match_tiebreak_order: Array.isArray(item.match_tiebreak_order)
|
||
? item.match_tiebreak_order.join(", ")
|
||
: "",
|
||
match_require_locator_match: item.match_require_locator_match ?? false,
|
||
match_require_exchange_match: item.match_require_exchange_match ?? false,
|
||
multiplier_scope: item.multiplier_scope ?? "PER_BAND",
|
||
multiplier_source: item.multiplier_source ?? "VALID_ONLY",
|
||
wwl_multiplier_level: item.wwl_multiplier_level ?? "LOCATOR_6",
|
||
checklog_matching: item.checklog_matching ?? true,
|
||
out_of_window_dq_threshold: numberValue(item.out_of_window_dq_threshold),
|
||
time_diff_dq_threshold_percent: numberValue(item.time_diff_dq_threshold_percent),
|
||
time_diff_dq_threshold_sec: numberValue(item.time_diff_dq_threshold_sec),
|
||
bad_qso_dq_threshold_percent: numberValue(item.bad_qso_dq_threshold_percent),
|
||
time_tolerance_sec: numberValue(item.time_tolerance_sec),
|
||
allow_time_shift_one_hour: item.allow_time_shift_one_hour ?? true,
|
||
time_shift_seconds: numberValue(item.time_shift_seconds),
|
||
time_mismatch_policy: item.time_mismatch_policy ?? "FLAG_ONLY",
|
||
allow_time_mismatch_pairing: item.allow_time_mismatch_pairing ?? true,
|
||
time_mismatch_max_sec: numberValue(item.time_mismatch_max_sec),
|
||
require_unique_qso: item.require_unique_qso ?? true,
|
||
ignore_slash_part: item.ignore_slash_part ?? true,
|
||
ignore_third_part: item.ignore_third_part ?? true,
|
||
rst_ignore_third_char: item.rst_ignore_third_char ?? true,
|
||
callsign_suffix_max_len: numberValue(item.callsign_suffix_max_len),
|
||
callsign_levenshtein_max: numberValue(item.callsign_levenshtein_max),
|
||
letters_in_rst: item.letters_in_rst ?? true,
|
||
discard_qso_rec_diff_call: item.discard_qso_rec_diff_call ?? true,
|
||
discard_qso_sent_diff_call: item.discard_qso_sent_diff_call ?? false,
|
||
discard_qso_rec_diff_rst: item.discard_qso_rec_diff_rst ?? true,
|
||
discard_qso_sent_diff_rst: item.discard_qso_sent_diff_rst ?? false,
|
||
discard_qso_rec_diff_serial: item.discard_qso_rec_diff_serial ?? true,
|
||
discard_qso_sent_diff_serial: item.discard_qso_sent_diff_serial ?? false,
|
||
discard_qso_rec_diff_wwl: item.discard_qso_rec_diff_wwl ?? true,
|
||
discard_qso_sent_diff_wwl: item.discard_qso_sent_diff_wwl ?? false,
|
||
discard_qso_rec_diff_code: item.discard_qso_rec_diff_code ?? true,
|
||
discard_qso_sent_diff_code: item.discard_qso_sent_diff_code ?? false,
|
||
dup_resolution_strategy: Array.isArray(item.dup_resolution_strategy)
|
||
? item.dup_resolution_strategy.join(", ")
|
||
: "",
|
||
operating_window_mode: item.operating_window_mode ?? "NONE",
|
||
operating_window_hours: numberValue(item.operating_window_hours),
|
||
sixhr_ranking_mode: item.sixhr_ranking_mode ?? "IARU",
|
||
};
|
||
setForm(nextForm);
|
||
setInitialForm(nextForm);
|
||
};
|
||
|
||
const handleSubmit = async (event: React.FormEvent) => {
|
||
event.preventDefault();
|
||
setFormError(null);
|
||
setFormSuccess(null);
|
||
if (hasValidationErrors) {
|
||
setFormError(t("admin_rulesets_fix_errors") ?? "Opravte chyby ve formuláři.");
|
||
return;
|
||
}
|
||
|
||
|
||
const payload: Record<string, unknown> = {
|
||
name: form.name.trim(),
|
||
code: form.code.trim(),
|
||
description: form.description.trim() || null,
|
||
scoring_mode: form.scoring_mode,
|
||
points_per_qso: toNumberOrNull(form.points_per_qso),
|
||
points_per_km: toNumberOrNull(form.points_per_km),
|
||
use_multipliers: form.use_multipliers,
|
||
multiplier_type: form.multiplier_type,
|
||
dup_qso_policy: form.dup_qso_policy,
|
||
nil_qso_policy: form.nil_qso_policy,
|
||
no_counterpart_log_policy: form.no_counterpart_log_policy,
|
||
not_in_counterpart_log_policy: form.not_in_counterpart_log_policy,
|
||
unique_qso_policy: form.unique_qso_policy,
|
||
busted_call_policy: form.busted_call_policy,
|
||
busted_rst_policy: form.busted_rst_policy,
|
||
busted_exchange_policy: form.busted_exchange_policy,
|
||
busted_serial_policy: form.busted_serial_policy,
|
||
busted_locator_policy: form.busted_locator_policy,
|
||
penalty_dup_points: toIntOrNull(form.penalty_dup_points),
|
||
penalty_nil_points: toIntOrNull(form.penalty_nil_points),
|
||
penalty_busted_call_points: toIntOrNull(form.penalty_busted_call_points),
|
||
penalty_busted_rst_points: toIntOrNull(form.penalty_busted_rst_points),
|
||
penalty_busted_exchange_points: toIntOrNull(form.penalty_busted_exchange_points),
|
||
penalty_busted_serial_points: toIntOrNull(form.penalty_busted_serial_points),
|
||
penalty_busted_locator_points: toIntOrNull(form.penalty_busted_locator_points),
|
||
penalty_out_of_window_points: toIntOrNull(form.penalty_out_of_window_points),
|
||
dupe_scope: form.dupe_scope,
|
||
callsign_normalization: form.callsign_normalization,
|
||
distance_rounding: form.distance_rounding,
|
||
min_distance_km: toNumberOrNull(form.min_distance_km),
|
||
require_locators: form.require_locators,
|
||
out_of_window_policy: form.out_of_window_policy,
|
||
exchange_type: form.exchange_type,
|
||
exchange_requires_wwl: form.exchange_requires_wwl,
|
||
exchange_requires_serial: form.exchange_requires_serial,
|
||
exchange_requires_report: form.exchange_requires_report,
|
||
exchange_pattern: form.exchange_pattern.trim() || null,
|
||
match_tiebreak_order: form.match_tiebreak_order.trim()
|
||
? form.match_tiebreak_order.split(",").map((v) => v.trim()).filter(Boolean)
|
||
: null,
|
||
match_require_locator_match: form.match_require_locator_match,
|
||
match_require_exchange_match: form.match_require_exchange_match,
|
||
multiplier_scope: form.multiplier_scope,
|
||
multiplier_source: form.multiplier_source,
|
||
wwl_multiplier_level: form.wwl_multiplier_level,
|
||
checklog_matching: form.checklog_matching,
|
||
out_of_window_dq_threshold: toNumberOrNull(form.out_of_window_dq_threshold),
|
||
time_diff_dq_threshold_percent: toIntOrNull(form.time_diff_dq_threshold_percent),
|
||
time_diff_dq_threshold_sec: toIntOrNull(form.time_diff_dq_threshold_sec),
|
||
bad_qso_dq_threshold_percent: toIntOrNull(form.bad_qso_dq_threshold_percent),
|
||
time_tolerance_sec: toNumberOrNull(form.time_tolerance_sec),
|
||
allow_time_shift_one_hour: form.allow_time_shift_one_hour,
|
||
time_shift_seconds: toIntOrNull(form.time_shift_seconds),
|
||
time_mismatch_policy: form.time_mismatch_policy,
|
||
allow_time_mismatch_pairing: form.allow_time_mismatch_pairing,
|
||
time_mismatch_max_sec: toIntOrNull(form.time_mismatch_max_sec),
|
||
require_unique_qso: form.require_unique_qso,
|
||
ignore_slash_part: form.ignore_slash_part,
|
||
ignore_third_part: form.ignore_third_part,
|
||
rst_ignore_third_char: form.rst_ignore_third_char,
|
||
callsign_suffix_max_len: toIntOrNull(form.callsign_suffix_max_len),
|
||
callsign_levenshtein_max: toIntOrNull(form.callsign_levenshtein_max),
|
||
letters_in_rst: form.letters_in_rst,
|
||
discard_qso_rec_diff_call: form.discard_qso_rec_diff_call,
|
||
discard_qso_sent_diff_call: form.discard_qso_sent_diff_call,
|
||
discard_qso_rec_diff_rst: form.discard_qso_rec_diff_rst,
|
||
discard_qso_sent_diff_rst: form.discard_qso_sent_diff_rst,
|
||
discard_qso_rec_diff_serial: form.discard_qso_rec_diff_serial,
|
||
discard_qso_sent_diff_serial: form.discard_qso_sent_diff_serial,
|
||
discard_qso_rec_diff_wwl: form.discard_qso_rec_diff_wwl,
|
||
discard_qso_sent_diff_wwl: form.discard_qso_sent_diff_wwl,
|
||
discard_qso_rec_diff_code: form.discard_qso_rec_diff_code,
|
||
discard_qso_sent_diff_code: form.discard_qso_sent_diff_code,
|
||
dup_resolution_strategy: form.dup_resolution_strategy.trim()
|
||
? form.dup_resolution_strategy.split(",").map((v) => v.trim()).filter(Boolean)
|
||
: null,
|
||
operating_window_mode: form.operating_window_mode,
|
||
operating_window_hours: toIntOrNull(form.operating_window_hours),
|
||
sixhr_ranking_mode: form.sixhr_ranking_mode,
|
||
options: null,
|
||
};
|
||
|
||
try {
|
||
setSubmitting(true);
|
||
await axios.get("/sanctum/csrf-cookie", { withCredentials: true });
|
||
if (formMode === "edit" && editing) {
|
||
await axios.put(`/api/evaluation-rule-sets/${editing.id}`, payload, {
|
||
headers: { Accept: "application/json" },
|
||
withCredentials: true,
|
||
withXSRFToken: true,
|
||
});
|
||
setFormSuccess(t("admin_rulesets_updated") ?? "Rule set byl upraven.");
|
||
} else {
|
||
await axios.post(`/api/evaluation-rule-sets`, payload, {
|
||
headers: { Accept: "application/json" },
|
||
withCredentials: true,
|
||
withXSRFToken: true,
|
||
});
|
||
setFormSuccess(t("admin_rulesets_created") ?? "Rule set byl vytvořen.");
|
||
}
|
||
setFormMode("none");
|
||
resetForm();
|
||
setRefreshKey((k) => k + 1);
|
||
} catch (e: any) {
|
||
setFormError(
|
||
e?.response?.data?.message ?? (t("admin_rulesets_save_failed") ?? "Chyba při ukládání rule setu.")
|
||
);
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const visibleItems = useMemo(() => items, [items]);
|
||
const validation = useMemo(() => {
|
||
const numberError = (value: string) =>
|
||
value.trim() && !isNumberLike(value)
|
||
? t("validation_number") ?? "Musí být číslo."
|
||
: null;
|
||
const intError = (value: string) =>
|
||
value.trim() && !isIntegerLike(value)
|
||
? t("validation_integer") ?? "Musí být celé číslo."
|
||
: null;
|
||
return {
|
||
name: isBlank(form.name)
|
||
? t("validation_name_required") ?? "Název je povinný."
|
||
: null,
|
||
code: isBlank(form.code)
|
||
? t("validation_code_required") ?? "Kód je povinný."
|
||
: null,
|
||
points_per_qso: numberError(form.points_per_qso),
|
||
points_per_km: numberError(form.points_per_km),
|
||
min_distance_km: numberError(form.min_distance_km),
|
||
time_tolerance_sec: intError(form.time_tolerance_sec),
|
||
out_of_window_dq_threshold: intError(form.out_of_window_dq_threshold),
|
||
time_diff_dq_threshold_percent: intError(form.time_diff_dq_threshold_percent) ||
|
||
(form.time_diff_dq_threshold_percent.trim() &&
|
||
(Number(form.time_diff_dq_threshold_percent) < 1 || Number(form.time_diff_dq_threshold_percent) > 100)
|
||
? t("validation_range_1_100") ?? "Rozsah 1–100."
|
||
: null),
|
||
time_diff_dq_threshold_sec: intError(form.time_diff_dq_threshold_sec) ||
|
||
(form.time_diff_dq_threshold_sec.trim() && Number(form.time_diff_dq_threshold_sec) < 1
|
||
? t("validation_min_one") ?? "Musí být alespoň 1."
|
||
: null),
|
||
bad_qso_dq_threshold_percent: intError(form.bad_qso_dq_threshold_percent) ||
|
||
(form.bad_qso_dq_threshold_percent.trim() &&
|
||
(Number(form.bad_qso_dq_threshold_percent) < 1 || Number(form.bad_qso_dq_threshold_percent) > 100)
|
||
? t("validation_range_1_100") ?? "Rozsah 1–100."
|
||
: null),
|
||
callsign_suffix_max_len: intError(form.callsign_suffix_max_len) ||
|
||
(form.callsign_suffix_max_len.trim() && Number(form.callsign_suffix_max_len) < 0
|
||
? t("validation_min_zero") ?? "Musí být alespoň 0."
|
||
: null),
|
||
callsign_levenshtein_max: intError(form.callsign_levenshtein_max) ||
|
||
(form.callsign_levenshtein_max.trim() &&
|
||
(Number(form.callsign_levenshtein_max) < 0 || Number(form.callsign_levenshtein_max) > 2)
|
||
? t("validation_range_0_2") ?? "Rozsah 0–2."
|
||
: null),
|
||
time_shift_seconds: intError(form.time_shift_seconds) ||
|
||
(form.time_shift_seconds.trim() && Number(form.time_shift_seconds) < 0
|
||
? t("validation_min_zero") ?? "Musí být alespoň 0."
|
||
: null),
|
||
time_mismatch_max_sec: intError(form.time_mismatch_max_sec) ||
|
||
(form.time_mismatch_max_sec.trim() && Number(form.time_mismatch_max_sec) < 0
|
||
? t("validation_min_zero") ?? "Musí být alespoň 0."
|
||
: null),
|
||
penalty_dup_points: intError(form.penalty_dup_points),
|
||
penalty_nil_points: intError(form.penalty_nil_points),
|
||
penalty_busted_call_points: intError(form.penalty_busted_call_points),
|
||
penalty_busted_rst_points: intError(form.penalty_busted_rst_points),
|
||
penalty_busted_exchange_points: intError(form.penalty_busted_exchange_points),
|
||
penalty_busted_serial_points: intError(form.penalty_busted_serial_points),
|
||
penalty_busted_locator_points: intError(form.penalty_busted_locator_points),
|
||
penalty_out_of_window_points: intError(form.penalty_out_of_window_points),
|
||
};
|
||
}, [form]);
|
||
|
||
const hasValidationErrors = useMemo(
|
||
() => Object.values(validation).some(Boolean),
|
||
[validation]
|
||
);
|
||
|
||
const isFormDirty = useMemo(
|
||
() => JSON.stringify(form) !== JSON.stringify(initialForm),
|
||
[form, initialForm]
|
||
);
|
||
|
||
const reportRequirementWarning =
|
||
!form.exchange_requires_report &&
|
||
(form.discard_qso_rec_diff_rst || form.discard_qso_sent_diff_rst);
|
||
|
||
const closeForm = () => {
|
||
if (isFormDirty) {
|
||
const confirmed = window.confirm(
|
||
t("admin_form_confirm_close") ??
|
||
"Formulář obsahuje neuložené změny. Opravdu ho chcete zavřít?"
|
||
);
|
||
if (!confirmed) return;
|
||
}
|
||
setFormMode("none");
|
||
resetForm();
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="flex gap-3 items-center">
|
||
<h1 className="text-xl font-semibold">{t("admin_rulesets_title") ?? "Evaluation rules"}</h1>
|
||
{formMode !== "none" && (
|
||
<Button variant="light" onPress={closeForm}>
|
||
{t("admin_form_close") ?? "Zavřít formulář"}
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
{loading ? (
|
||
<div>{t("admin_rulesets_loading") ?? "Načítám rule sety…"}</div>
|
||
) : error ? (
|
||
<div className="text-sm text-red-600">{error}</div>
|
||
) : (
|
||
<AdminRulesetsTable
|
||
items={visibleItems}
|
||
locale={locale}
|
||
valuePlaceholder={valuePlaceholder}
|
||
formatDate={formatDate}
|
||
onEdit={openEdit}
|
||
t={(key) => t(key) as string}
|
||
/>
|
||
)}
|
||
|
||
{!loading && !error && (
|
||
<div className="flex flex-wrap gap-3">
|
||
<Button
|
||
type="button"
|
||
color="primary"
|
||
onPress={() => {
|
||
resetForm();
|
||
setFormMode("create");
|
||
}}
|
||
>
|
||
{label("admin_rulesets_create", "Nová sada pravidel")}
|
||
</Button>
|
||
<Button type="button" variant="bordered" onPress={openHelp}>
|
||
{label("admin_rulesets_help", "Nápověda")}
|
||
</Button>
|
||
{formMode !== "none" && (
|
||
<Button type="button" variant="bordered" onPress={closeForm}>
|
||
{label("admin_form_close", "Zavřít formulář")}
|
||
</Button>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{formMode !== "none" && (
|
||
<AdminRulesetForm
|
||
formMode={formMode === "create" ? "create" : "edit"}
|
||
form={form}
|
||
setForm={setForm}
|
||
validation={validation}
|
||
formError={formError}
|
||
formSuccess={formSuccess}
|
||
reportRequirementWarning={reportRequirementWarning}
|
||
submitting={submitting}
|
||
hasValidationErrors={hasValidationErrors}
|
||
label={label}
|
||
helpLabel={helpLabel}
|
||
tRules={tRules}
|
||
onSubmit={handleSubmit}
|
||
onClose={closeForm}
|
||
/>
|
||
)}
|
||
<Modal isOpen={helpOpen} onOpenChange={onHelpChange} size="5xl" scrollBehavior="inside">
|
||
<ModalContent>
|
||
<ModalHeader className="flex flex-col gap-1">
|
||
{label("admin_rulesets_help_title", "Dokumentace rulesetu")}
|
||
</ModalHeader>
|
||
<ModalBody>
|
||
<div className="prose max-w-none text-sm">
|
||
<Markdown>{rulesetDoc}</Markdown>
|
||
</div>
|
||
</ModalBody>
|
||
</ModalContent>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
}
|