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