Files
vkv/resources/js/components/admin/rulesets/AdminRulesetForm.tsx
Zdeněk Burda 41e3ce6f25 Initial commit
2026-01-09 21:26:40 +01:00

1326 lines
58 KiB
TypeScript

import React from "react";
import {
Accordion,
AccordionItem,
Button,
Card,
CardBody,
CardHeader,
Input,
Switch,
Textarea,
} from "@heroui/react";
import type { TFunction } from "i18next";
import type { RuleSetForm, RuleSetFormMode } from "./adminRulesetTypes";
type RuleSetValidation = {
name?: string | null;
code?: string | null;
points_per_qso?: string | null;
points_per_km?: string | null;
min_distance_km?: string | null;
time_tolerance_sec?: string | null;
out_of_window_dq_threshold?: string | null;
time_diff_dq_threshold_percent?: string | null;
time_diff_dq_threshold_sec?: string | null;
bad_qso_dq_threshold_percent?: string | null;
penalty_dup_points?: string | null;
penalty_nil_points?: string | null;
penalty_busted_call_points?: string | null;
penalty_busted_rst_points?: string | null;
penalty_busted_exchange_points?: string | null;
penalty_busted_serial_points?: string | null;
penalty_busted_locator_points?: string | null;
penalty_out_of_window_points?: string | null;
callsign_suffix_max_len?: string | null;
callsign_levenshtein_max?: string | null;
time_shift_seconds?: string | null;
time_mismatch_max_sec?: string | null;
};
type AdminRulesetFormProps = {
formMode: Exclude<RuleSetFormMode, "none">;
form: RuleSetForm;
setForm: React.Dispatch<React.SetStateAction<RuleSetForm>>;
validation: RuleSetValidation;
formError: string | null;
formSuccess: string | null;
reportRequirementWarning: boolean;
submitting: boolean;
hasValidationErrors: boolean;
label: (key: string, fallback: string) => string;
helpLabel: (label: string, key: string) => React.ReactNode;
tRules: TFunction;
onSubmit: (event: React.FormEvent) => void;
onClose: () => void;
};
export default function AdminRulesetForm({
formMode,
form,
setForm,
validation,
formError,
formSuccess,
reportRequirementWarning,
submitting,
hasValidationErrors,
label,
helpLabel,
tRules,
onSubmit,
onClose,
}: AdminRulesetFormProps) {
return (
<form onSubmit={onSubmit} className="space-y-6">
<h2 className="text-lg font-semibold">
{formMode === "create"
? label("admin_rulesets_form_create_title", "Nová sada pravidel")
: label("admin_rulesets_form_edit_title", "Upravit sadu pravidel")}
</h2>
<Card>
<CardHeader className="flex flex-col items-start gap-1">
<h3 className="text-lg font-semibold">
{label("admin_rulesets_section_base_title", "Základ")}
</h3>
<p className="text-sm text-foreground-500">
{label(
"admin_rulesets_section_base_desc",
"Identita rulesetu a krátký popis pro rozhodčí."
)}
</p>
</CardHeader>
<CardBody className="space-y-4">
<div className="grid gap-3 md:grid-cols-2">
<Input
label={helpLabel(label("admin_rulesets_label_name", "Název"), "ruleset_help_name")}
title={tRules("ruleset_help_name")}
value={form.name}
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
isInvalid={!!validation.name}
errorMessage={validation.name ?? undefined}
/>
<Input
label={helpLabel(label("admin_rulesets_label_code", "Kód"), "ruleset_help_code")}
title={tRules("ruleset_help_code")}
value={form.code}
onChange={(e) => setForm((prev) => ({ ...prev, code: e.target.value }))}
isInvalid={!!validation.code}
errorMessage={validation.code ?? undefined}
/>
</div>
<Textarea
label={helpLabel(label("admin_rulesets_label_description", "Popis"), "ruleset_help_description")}
title={tRules("ruleset_help_description")}
value={form.description}
onChange={(e) => setForm((prev) => ({ ...prev, description: e.target.value }))}
minRows={2}
/>
</CardBody>
</Card>
<Card>
<CardHeader className="flex flex-col items-start gap-1">
<h3 className="text-lg font-semibold">
{label("admin_rulesets_section_scoring_title", "Bodování")}
</h3>
<p className="text-sm text-foreground-500">
{label(
"admin_rulesets_section_scoring_desc",
"Jak se počítají body a jak se zaokrouhluje vzdálenost."
)}
</p>
</CardHeader>
<CardBody className="space-y-4">
<div className="grid gap-3 md:grid-cols-3">
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_scoring_mode", "Scoring mode"),
"ruleset_help_scoring_mode"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_scoring_mode")}
value={form.scoring_mode}
onChange={(e) => setForm((prev) => ({ ...prev, scoring_mode: e.target.value }))}
>
<option value="DISTANCE">DISTANCE</option>
<option value="FIXED_POINTS">FIXED_POINTS</option>
</select>
</label>
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_points_per_qso", "Points / QSO"),
"ruleset_help_points_per_qso"
)}
title={tRules("ruleset_help_points_per_qso")}
value={form.points_per_qso}
onChange={(e) => setForm((prev) => ({ ...prev, points_per_qso: e.target.value }))}
isInvalid={!!validation.points_per_qso}
errorMessage={validation.points_per_qso ?? undefined}
/>
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_points_per_km", "Points / km"),
"ruleset_help_points_per_km"
)}
title={tRules("ruleset_help_points_per_km")}
value={form.points_per_km}
onChange={(e) => setForm((prev) => ({ ...prev, points_per_km: e.target.value }))}
isInvalid={!!validation.points_per_km}
errorMessage={validation.points_per_km ?? undefined}
/>
</div>
<div className="grid gap-3 md:grid-cols-3 items-center">
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_distance_rounding", "Distance rounding"),
"ruleset_help_distance_rounding"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_distance_rounding")}
value={form.distance_rounding}
onChange={(e) => setForm((prev) => ({ ...prev, distance_rounding: e.target.value }))}
>
<option value="FLOOR">FLOOR</option>
<option value="ROUND">ROUND</option>
<option value="CEIL">CEIL</option>
</select>
</label>
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_min_distance_km", "Min. distance (km)"),
"ruleset_help_min_distance_km"
)}
title={tRules("ruleset_help_min_distance_km")}
value={form.min_distance_km}
onChange={(e) => setForm((prev) => ({ ...prev, min_distance_km: e.target.value }))}
isInvalid={!!validation.min_distance_km}
errorMessage={validation.min_distance_km ?? undefined}
/>
</div>
</CardBody>
</Card>
<Card>
<CardHeader className="flex flex-col items-start gap-1">
<h3 className="text-lg font-semibold">
{label("admin_rulesets_section_operating_window_title", "Operating window")}
</h3>
<p className="text-sm text-foreground-500">
{label(
"admin_rulesets_section_operating_window_desc",
"Nastavení výběru nejlepšího souvislého okna pro 6H."
)}
</p>
</CardHeader>
<CardBody className="space-y-4">
<div className="grid gap-3 md:grid-cols-3 items-center">
<Switch
isSelected={form.operating_window_mode === "BEST_CONTIGUOUS"}
onValueChange={(value) =>
setForm((prev) => ({
...prev,
operating_window_mode: value ? "BEST_CONTIGUOUS" : "NONE",
operating_window_hours: value ? "6" : "",
}))
}
title={tRules("ruleset_help_operating_window_mode")}
>
{helpLabel(
label("admin_rulesets_label_operating_window_enabled", "6H operating window"),
"ruleset_help_operating_window_mode"
)}
</Switch>
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_operating_window_hours", "Operating window (hours)"),
"ruleset_help_operating_window_hours"
)}
title={tRules("ruleset_help_operating_window_hours")}
value={form.operating_window_hours || (form.operating_window_mode === "BEST_CONTIGUOUS" ? "6" : "")}
onChange={(e) =>
setForm((prev) => ({
...prev,
operating_window_hours: e.target.value,
}))
}
isDisabled
/>
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_sixhr_ranking_mode", "6H ranking mode"),
"ruleset_help_sixhr_ranking_mode"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_sixhr_ranking_mode")}
value={form.sixhr_ranking_mode}
onChange={(e) => setForm((prev) => ({ ...prev, sixhr_ranking_mode: e.target.value }))}
>
<option value="IARU">IARU</option>
<option value="CRK">CRK</option>
</select>
</label>
</div>
</CardBody>
</Card>
<Card>
<CardHeader className="flex flex-col items-start gap-1">
<h3 className="text-lg font-semibold">
{label("admin_rulesets_section_matching_title", "Matching")}
</h3>
<p className="text-sm text-foreground-500">
{label(
"admin_rulesets_section_matching_desc",
"Pravidla pro párování a normalizaci volacích znaků."
)}
</p>
</CardHeader>
<CardBody className="space-y-4">
<div className="grid gap-3 md:grid-cols-3 items-center">
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_time_tolerance_sec", "Time tolerance (s)"),
"ruleset_help_time_tolerance_sec"
)}
title={tRules("ruleset_help_time_tolerance_sec")}
value={form.time_tolerance_sec}
onChange={(e) => setForm((prev) => ({ ...prev, time_tolerance_sec: e.target.value }))}
isInvalid={!!validation.time_tolerance_sec}
errorMessage={validation.time_tolerance_sec ?? undefined}
/>
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_callsign_normalization", "Callsign normalization"),
"ruleset_help_callsign_normalization"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_callsign_normalization")}
value={form.callsign_normalization}
onChange={(e) => setForm((prev) => ({ ...prev, callsign_normalization: e.target.value }))}
>
<option value="STRICT">STRICT</option>
<option value="IGNORE_SUFFIX">IGNORE_SUFFIX</option>
</select>
</label>
<Switch
isSelected={form.checklog_matching}
onValueChange={(value) => setForm((prev) => ({ ...prev, checklog_matching: value }))}
title={tRules("ruleset_help_checklog_matching")}
>
{helpLabel(
label("admin_rulesets_label_checklog_matching", "CHECK logy v matchingu"),
"ruleset_help_checklog_matching"
)}
</Switch>
</div>
<div className="grid gap-3 md:grid-cols-3 items-center">
<Switch
isSelected={form.allow_time_shift_one_hour}
onValueChange={(value) => setForm((prev) => ({ ...prev, allow_time_shift_one_hour: value }))}
title={tRules("ruleset_help_allow_time_shift_one_hour")}
>
{helpLabel(
label("admin_rulesets_label_allow_time_shift", "Povolit posun času"),
"ruleset_help_allow_time_shift_one_hour"
)}
</Switch>
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_time_shift_seconds", "Time shift (s)"),
"ruleset_help_time_shift_seconds"
)}
title={tRules("ruleset_help_time_shift_seconds")}
value={form.time_shift_seconds}
onChange={(e) => setForm((prev) => ({ ...prev, time_shift_seconds: e.target.value }))}
isInvalid={!!validation.time_shift_seconds}
errorMessage={validation.time_shift_seconds ?? undefined}
/>
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_time_mismatch_policy", "Time mismatch policy"),
"ruleset_help_time_mismatch_policy"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_time_mismatch_policy")}
value={form.time_mismatch_policy}
onChange={(e) => setForm((prev) => ({ ...prev, time_mismatch_policy: e.target.value }))}
>
<option value="INVALID">INVALID</option>
<option value="ZERO_POINTS">ZERO_POINTS</option>
<option value="FLAG_ONLY">FLAG_ONLY</option>
<option value="PENALTY">PENALTY</option>
</select>
</label>
</div>
<div className="grid gap-3 md:grid-cols-3 items-center">
<Switch
isSelected={form.allow_time_mismatch_pairing}
onValueChange={(value) =>
setForm((prev) => ({ ...prev, allow_time_mismatch_pairing: value }))
}
title={tRules("ruleset_help_allow_time_mismatch_pairing")}
>
{helpLabel(
label("admin_rulesets_label_allow_time_mismatch_pairing", "Párovat mimo toleranci"),
"ruleset_help_allow_time_mismatch_pairing"
)}
</Switch>
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_time_mismatch_max_sec", "Time mismatch max (s)"),
"ruleset_help_time_mismatch_max_sec"
)}
title={tRules("ruleset_help_time_mismatch_max_sec")}
value={form.time_mismatch_max_sec}
onChange={(e) => setForm((prev) => ({ ...prev, time_mismatch_max_sec: e.target.value }))}
isInvalid={!!validation.time_mismatch_max_sec}
errorMessage={validation.time_mismatch_max_sec ?? undefined}
/>
<div />
</div>
<div className="grid gap-3 md:grid-cols-3 items-center">
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_callsign_suffix_len", "Max suffix délka"),
"ruleset_help_callsign_suffix_max_len"
)}
title={tRules("ruleset_help_callsign_suffix_max_len")}
value={form.callsign_suffix_max_len}
onChange={(e) => setForm((prev) => ({ ...prev, callsign_suffix_max_len: e.target.value }))}
isInvalid={!!validation.callsign_suffix_max_len}
errorMessage={validation.callsign_suffix_max_len ?? undefined}
/>
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_callsign_levenshtein", "Levenshtein max"),
"ruleset_help_callsign_levenshtein_max"
)}
title={tRules("ruleset_help_callsign_levenshtein_max")}
value={form.callsign_levenshtein_max}
onChange={(e) => setForm((prev) => ({ ...prev, callsign_levenshtein_max: e.target.value }))}
isInvalid={!!validation.callsign_levenshtein_max}
errorMessage={validation.callsign_levenshtein_max ?? undefined}
/>
<div />
</div>
<div className="grid gap-3 md:grid-cols-3 items-center">
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_time_diff_dq_percent", "Time diff DQ %"),
"ruleset_help_time_diff_dq_threshold_percent"
)}
title={tRules("ruleset_help_time_diff_dq_threshold_percent")}
value={form.time_diff_dq_threshold_percent}
onChange={(e) =>
setForm((prev) => ({ ...prev, time_diff_dq_threshold_percent: e.target.value }))
}
isInvalid={!!validation.time_diff_dq_threshold_percent}
errorMessage={validation.time_diff_dq_threshold_percent ?? undefined}
/>
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_time_diff_dq_sec", "Time diff DQ (s)"),
"ruleset_help_time_diff_dq_threshold_sec"
)}
title={tRules("ruleset_help_time_diff_dq_threshold_sec")}
value={form.time_diff_dq_threshold_sec}
onChange={(e) =>
setForm((prev) => ({ ...prev, time_diff_dq_threshold_sec: e.target.value }))
}
isInvalid={!!validation.time_diff_dq_threshold_sec}
errorMessage={validation.time_diff_dq_threshold_sec ?? undefined}
/>
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_bad_qso_dq_percent", "Bad QSO DQ %"),
"ruleset_help_bad_qso_dq_threshold_percent"
)}
title={tRules("ruleset_help_bad_qso_dq_threshold_percent")}
value={form.bad_qso_dq_threshold_percent}
onChange={(e) =>
setForm((prev) => ({ ...prev, bad_qso_dq_threshold_percent: e.target.value }))
}
isInvalid={!!validation.bad_qso_dq_threshold_percent}
errorMessage={validation.bad_qso_dq_threshold_percent ?? undefined}
/>
</div>
<div className="grid gap-3 md:grid-cols-3 items-center">
<Switch
isSelected={form.match_require_locator_match}
onValueChange={(value) =>
setForm((prev) => ({ ...prev, match_require_locator_match: value }))
}
title={tRules("ruleset_help_match_require_locator_match")}
>
{helpLabel(
label("admin_rulesets_label_match_require_locator", "Matching vyžaduje lokátor"),
"ruleset_help_match_require_locator_match"
)}
</Switch>
<Switch
isSelected={form.match_require_exchange_match}
onValueChange={(value) =>
setForm((prev) => ({ ...prev, match_require_exchange_match: value }))
}
title={tRules("ruleset_help_match_require_exchange_match")}
>
{helpLabel(
label("admin_rulesets_label_match_require_exchange", "Matching vyžaduje exchange"),
"ruleset_help_match_require_exchange_match"
)}
</Switch>
<Input
label={helpLabel(
label("admin_rulesets_label_tiebreak_order", "Tiebreak order"),
"ruleset_help_match_tiebreak_order"
)}
title={tRules("ruleset_help_match_tiebreak_order")}
value={form.match_tiebreak_order}
onChange={(e) => setForm((prev) => ({ ...prev, match_tiebreak_order: e.target.value }))}
placeholder={
label(
"admin_rulesets_label_tiebreak_placeholder",
"time_diff, exchange_match, locator_match"
)
}
/>
</div>
<div className="grid gap-3 md:grid-cols-3 items-center">
<Switch
isSelected={form.ignore_slash_part}
onValueChange={(value) => setForm((prev) => ({ ...prev, ignore_slash_part: value }))}
title={tRules("ruleset_help_ignore_slash_part")}
>
{helpLabel(
label("admin_rulesets_label_ignore_suffix", "Ignorovat suffix v call"),
"ruleset_help_ignore_slash_part"
)}
</Switch>
<Switch
isSelected={form.ignore_third_part}
onValueChange={(value) => setForm((prev) => ({ ...prev, ignore_third_part: value }))}
title={tRules("ruleset_help_ignore_third_part")}
>
{helpLabel(
label("admin_rulesets_label_ignore_third_part", "Ignorovat 3. část call"),
"ruleset_help_ignore_third_part"
)}
</Switch>
<div />
</div>
<div className="grid gap-3 md:grid-cols-3 items-center">
<Switch
isSelected={form.rst_ignore_third_char}
onValueChange={(value) => setForm((prev) => ({ ...prev, rst_ignore_third_char: value }))}
title={tRules("ruleset_help_rst_ignore_third_char")}
>
{helpLabel(
label("admin_rulesets_label_rst_ignore_third_char", "Ignorovat 3. znak RST"),
"ruleset_help_rst_ignore_third_char"
)}
</Switch>
<Switch
isSelected={form.letters_in_rst}
onValueChange={(value) => setForm((prev) => ({ ...prev, letters_in_rst: value }))}
title={tRules("ruleset_help_letters_in_rst")}
>
{helpLabel(
label("admin_rulesets_label_letters_in_rst", "RST s písmeny"),
"ruleset_help_letters_in_rst"
)}
</Switch>
<div />
</div>
</CardBody>
</Card>
<Card>
<CardHeader className="flex flex-col items-start gap-1">
<h3 className="text-lg font-semibold">
{label("admin_rulesets_section_exchange_title", "Výměna")}
</h3>
<p className="text-sm text-foreground-500">
{label(
"admin_rulesets_section_exchange_desc",
"Nastavení typu exchange a povinných částí výměny."
)}
</p>
</CardHeader>
<CardBody className="space-y-4">
<div className="grid gap-3 md:grid-cols-3">
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_exchange_type", "Exchange type"),
"ruleset_help_exchange_type"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_exchange_type")}
value={form.exchange_type}
onChange={(e) => setForm((prev) => ({ ...prev, exchange_type: e.target.value }))}
>
<option value="SERIAL">SERIAL</option>
<option value="WWL">WWL</option>
<option value="SERIAL_WWL">SERIAL_WWL</option>
<option value="CUSTOM">CUSTOM</option>
</select>
</label>
<Switch
isSelected={form.exchange_requires_wwl}
onValueChange={(value) => setForm((prev) => ({ ...prev, exchange_requires_wwl: value }))}
title={tRules("ruleset_help_exchange_requires_wwl")}
>
{helpLabel(
label("admin_rulesets_label_exchange_requires_wwl", "Vyžadovat WWL"),
"ruleset_help_exchange_requires_wwl"
)}
</Switch>
<Switch
isSelected={form.exchange_requires_serial}
onValueChange={(value) => setForm((prev) => ({ ...prev, exchange_requires_serial: value }))}
title={tRules("ruleset_help_exchange_requires_serial")}
>
{helpLabel(
label("admin_rulesets_label_exchange_requires_serial", "Vyžadovat serial"),
"ruleset_help_exchange_requires_serial"
)}
</Switch>
</div>
<div className="grid gap-3 md:grid-cols-3 items-center">
<Switch
isSelected={form.exchange_requires_report}
onValueChange={(value) => setForm((prev) => ({ ...prev, exchange_requires_report: value }))}
title={tRules("ruleset_help_exchange_requires_report")}
>
{helpLabel(
label("admin_rulesets_label_exchange_requires_report", "Report součástí výměny"),
"ruleset_help_exchange_requires_report"
)}
</Switch>
<Input
label={helpLabel(
label("admin_rulesets_label_exchange_pattern", "Exchange regex"),
"ruleset_help_exchange_pattern"
)}
title={tRules("ruleset_help_exchange_pattern")}
value={form.exchange_pattern}
onChange={(e) => setForm((prev) => ({ ...prev, exchange_pattern: e.target.value }))}
/>
</div>
{reportRequirementWarning && (
<div className="text-xs text-amber-600">
{label(
"admin_rulesets_warning_report_required",
"Busted RST se vyhodnocuje jen pokud je zapnuté „Report součástí výměny“."
)}
</div>
)}
</CardBody>
</Card>
<Card>
<CardHeader className="flex flex-col items-start gap-1">
<h3 className="text-lg font-semibold">
{label("admin_rulesets_section_errors_title", "Duplicity a busted")}
</h3>
<p className="text-sm text-foreground-500">
{label(
"admin_rulesets_section_errors_desc",
"Jak se označují a bodují DUP/NIL/BUSTED situace."
)}
</p>
</CardHeader>
<CardBody className="space-y-4">
<div className="grid gap-3 md:grid-cols-3">
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_dupe_scope", "Dupe scope"),
"ruleset_help_dupe_scope"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_dupe_scope")}
value={form.dupe_scope}
onChange={(e) => setForm((prev) => ({ ...prev, dupe_scope: e.target.value }))}
>
<option value="BAND">BAND</option>
<option value="BAND_MODE">BAND_MODE</option>
</select>
</label>
<Switch
isSelected={form.require_unique_qso}
onValueChange={(value) => setForm((prev) => ({ ...prev, require_unique_qso: value }))}
title={tRules("ruleset_help_require_unique_qso")}
>
{helpLabel(
label("admin_rulesets_label_unique_qso", "Unikátní QSO"),
"ruleset_help_require_unique_qso"
)}
</Switch>
</div>
<div className="grid gap-3 md:grid-cols-3 items-center">
<Switch
isSelected={form.discard_qso_rec_diff_call}
onValueChange={(value) => setForm((prev) => ({ ...prev, discard_qso_rec_diff_call: value }))}
title={tRules("ruleset_help_discard_qso_rec_diff_call")}
>
{helpLabel(
label("admin_rulesets_label_busted_call_rx", "Busted call (RX)"),
"ruleset_help_discard_qso_rec_diff_call"
)}
</Switch>
<Switch
isSelected={form.discard_qso_sent_diff_call}
onValueChange={(value) => setForm((prev) => ({ ...prev, discard_qso_sent_diff_call: value }))}
title={tRules("ruleset_help_discard_qso_sent_diff_call")}
>
{helpLabel(
label("admin_rulesets_label_busted_call_tx", "Busted call (TX)"),
"ruleset_help_discard_qso_sent_diff_call"
)}
</Switch>
<Switch
isSelected={form.discard_qso_rec_diff_rst}
onValueChange={(value) => setForm((prev) => ({ ...prev, discard_qso_rec_diff_rst: value }))}
title={tRules("ruleset_help_discard_qso_rec_diff_rst")}
>
{helpLabel(
label("admin_rulesets_label_busted_rst_rx", "Busted RST (RX)"),
"ruleset_help_discard_qso_rec_diff_rst"
)}
</Switch>
</div>
<div className="grid gap-3 md:grid-cols-3 items-center">
<Switch
isSelected={form.discard_qso_sent_diff_rst}
onValueChange={(value) => setForm((prev) => ({ ...prev, discard_qso_sent_diff_rst: value }))}
title={tRules("ruleset_help_discard_qso_sent_diff_rst")}
>
{helpLabel(
label("admin_rulesets_label_busted_rst_tx", "Busted RST (TX)"),
"ruleset_help_discard_qso_sent_diff_rst"
)}
</Switch>
<Switch
isSelected={form.discard_qso_rec_diff_serial}
onValueChange={(value) =>
setForm((prev) => ({ ...prev, discard_qso_rec_diff_serial: value }))
}
title={tRules("ruleset_help_discard_qso_rec_diff_serial")}
>
{helpLabel(
label("admin_rulesets_label_busted_serial_rx", "Busted serial (RX)"),
"ruleset_help_discard_qso_rec_diff_serial"
)}
</Switch>
<Switch
isSelected={form.discard_qso_sent_diff_serial}
onValueChange={(value) =>
setForm((prev) => ({ ...prev, discard_qso_sent_diff_serial: value }))
}
title={tRules("ruleset_help_discard_qso_sent_diff_serial")}
>
{helpLabel(
label("admin_rulesets_label_busted_serial_tx", "Busted serial (TX)"),
"ruleset_help_discard_qso_sent_diff_serial"
)}
</Switch>
</div>
<div className="grid gap-3 md:grid-cols-3 items-center">
<Switch
isSelected={form.discard_qso_rec_diff_wwl}
onValueChange={(value) =>
setForm((prev) => ({ ...prev, discard_qso_rec_diff_wwl: value }))
}
title={tRules("ruleset_help_discard_qso_rec_diff_wwl")}
>
{helpLabel(
label("admin_rulesets_label_busted_wwl_rx", "Busted WWL (RX)"),
"ruleset_help_discard_qso_rec_diff_wwl"
)}
</Switch>
<Switch
isSelected={form.discard_qso_sent_diff_wwl}
onValueChange={(value) =>
setForm((prev) => ({ ...prev, discard_qso_sent_diff_wwl: value }))
}
title={tRules("ruleset_help_discard_qso_sent_diff_wwl")}
>
{helpLabel(
label("admin_rulesets_label_busted_wwl_tx", "Busted WWL (TX)"),
"ruleset_help_discard_qso_sent_diff_wwl"
)}
</Switch>
<Switch
isSelected={form.discard_qso_rec_diff_code}
onValueChange={(value) => setForm((prev) => ({ ...prev, discard_qso_rec_diff_code: value }))}
title={tRules("ruleset_help_discard_qso_rec_diff_code")}
>
{helpLabel(
label("admin_rulesets_label_busted_exchange_rx", "Busted exchange (RX)"),
"ruleset_help_discard_qso_rec_diff_code"
)}
</Switch>
<Switch
isSelected={form.discard_qso_sent_diff_code}
onValueChange={(value) => setForm((prev) => ({ ...prev, discard_qso_sent_diff_code: value }))}
title={tRules("ruleset_help_discard_qso_sent_diff_code")}
>
{helpLabel(
label("admin_rulesets_label_busted_exchange_tx", "Busted exchange (TX)"),
"ruleset_help_discard_qso_sent_diff_code"
)}
</Switch>
</div>
<div className="grid gap-3 md:grid-cols-3">
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_dup_policy", "DUP policy"),
"ruleset_help_dup_qso_policy"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_dup_qso_policy")}
value={form.dup_qso_policy}
onChange={(e) => setForm((prev) => ({ ...prev, dup_qso_policy: e.target.value }))}
>
<option value="ZERO_POINTS">ZERO_POINTS</option>
<option value="PENALTY">PENALTY</option>
</select>
</label>
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_nil_policy", "NIL policy"),
"ruleset_help_nil_qso_policy"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_nil_qso_policy")}
value={form.nil_qso_policy}
onChange={(e) => setForm((prev) => ({ ...prev, nil_qso_policy: e.target.value }))}
>
<option value="ZERO_POINTS">ZERO_POINTS</option>
<option value="PENALTY">PENALTY</option>
</select>
</label>
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_busted_call_policy", "Busted call policy"),
"ruleset_help_busted_call_policy"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_busted_call_policy")}
value={form.busted_call_policy}
onChange={(e) => setForm((prev) => ({ ...prev, busted_call_policy: e.target.value }))}
>
<option value="ZERO_POINTS">ZERO_POINTS</option>
<option value="PENALTY">PENALTY</option>
</select>
</label>
</div>
<div className="grid gap-3 md:grid-cols-3">
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_no_counterpart_policy", "No counterpart policy"),
"ruleset_help_no_counterpart_log_policy"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_no_counterpart_log_policy")}
value={form.no_counterpart_log_policy}
onChange={(e) =>
setForm((prev) => ({ ...prev, no_counterpart_log_policy: e.target.value }))
}
>
<option value="INVALID">INVALID</option>
<option value="ZERO_POINTS">ZERO_POINTS</option>
<option value="FLAG_ONLY">FLAG_ONLY</option>
<option value="PENALTY">PENALTY</option>
</select>
</label>
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_not_in_counterpart_policy", "Not in counterpart policy"),
"ruleset_help_not_in_counterpart_log_policy"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_not_in_counterpart_log_policy")}
value={form.not_in_counterpart_log_policy}
onChange={(e) =>
setForm((prev) => ({ ...prev, not_in_counterpart_log_policy: e.target.value }))
}
>
<option value="INVALID">INVALID</option>
<option value="ZERO_POINTS">ZERO_POINTS</option>
<option value="FLAG_ONLY">FLAG_ONLY</option>
<option value="PENALTY">PENALTY</option>
</select>
</label>
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_unique_policy", "Unique policy"),
"ruleset_help_unique_qso_policy"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_unique_qso_policy")}
value={form.unique_qso_policy}
onChange={(e) =>
setForm((prev) => ({ ...prev, unique_qso_policy: e.target.value }))
}
>
<option value="INVALID">INVALID</option>
<option value="ZERO_POINTS">ZERO_POINTS</option>
<option value="FLAG_ONLY">FLAG_ONLY</option>
</select>
</label>
</div>
<div className="grid gap-3 md:grid-cols-2">
<Input
label={helpLabel(
label("admin_rulesets_label_dup_resolution_strategy", "Dup resolution strategy"),
"ruleset_help_dup_resolution_strategy"
)}
title={tRules("ruleset_help_dup_resolution_strategy")}
value={form.dup_resolution_strategy}
onChange={(e) => setForm((prev) => ({ ...prev, dup_resolution_strategy: e.target.value }))}
placeholder={label(
"admin_rulesets_label_dup_resolution_placeholder",
"paired_first, ok_first, earlier_time, lower_id"
)}
/>
</div>
<div className="grid gap-3 md:grid-cols-3">
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_busted_rst_policy", "Busted RST policy"),
"ruleset_help_busted_rst_policy"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_busted_rst_policy")}
value={form.busted_rst_policy}
onChange={(e) => setForm((prev) => ({ ...prev, busted_rst_policy: e.target.value }))}
>
<option value="ZERO_POINTS">ZERO_POINTS</option>
<option value="PENALTY">PENALTY</option>
</select>
</label>
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_busted_exchange_policy", "Busted exchange policy"),
"ruleset_help_busted_exchange_policy"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_busted_exchange_policy")}
value={form.busted_exchange_policy}
onChange={(e) => setForm((prev) => ({ ...prev, busted_exchange_policy: e.target.value }))}
>
<option value="ZERO_POINTS">ZERO_POINTS</option>
<option value="PENALTY">PENALTY</option>
</select>
</label>
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_busted_serial_policy", "Busted serial policy"),
"ruleset_help_busted_serial_policy"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_busted_serial_policy")}
value={form.busted_serial_policy}
onChange={(e) => setForm((prev) => ({ ...prev, busted_serial_policy: e.target.value }))}
>
<option value="ZERO_POINTS">ZERO_POINTS</option>
<option value="PENALTY">PENALTY</option>
</select>
</label>
</div>
<div className="grid gap-3 md:grid-cols-3">
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_busted_locator_policy", "Busted locator policy"),
"ruleset_help_busted_locator_policy"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_busted_locator_policy")}
value={form.busted_locator_policy}
onChange={(e) => setForm((prev) => ({ ...prev, busted_locator_policy: e.target.value }))}
>
<option value="ZERO_POINTS">ZERO_POINTS</option>
<option value="PENALTY">PENALTY</option>
</select>
</label>
</div>
</CardBody>
</Card>
<Card>
<CardHeader className="flex flex-col items-start gap-1">
<h3 className="text-lg font-semibold">
{label("admin_rulesets_section_out_of_window_title", "Out-of-window")}
</h3>
<p className="text-sm text-foreground-500">
{label(
"admin_rulesets_section_out_of_window_desc",
"Chování pro QSO mimo časové okno kola."
)}
</p>
</CardHeader>
<CardBody className="space-y-4">
<div className="grid gap-3 md:grid-cols-3 items-center">
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_out_of_window_policy", "Out-of-window policy"),
"ruleset_help_out_of_window_policy"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_out_of_window_policy")}
value={form.out_of_window_policy}
onChange={(e) => setForm((prev) => ({ ...prev, out_of_window_policy: e.target.value }))}
>
<option value="IGNORE">IGNORE</option>
<option value="ZERO_POINTS">ZERO_POINTS</option>
<option value="PENALTY">PENALTY</option>
<option value="INVALID">INVALID</option>
</select>
</label>
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_out_of_window_dq_threshold", "DQ threshold (out-of-window)"),
"ruleset_help_out_of_window_dq_threshold"
)}
title={tRules("ruleset_help_out_of_window_dq_threshold")}
value={form.out_of_window_dq_threshold}
onChange={(e) => setForm((prev) => ({ ...prev, out_of_window_dq_threshold: e.target.value }))}
isInvalid={!!validation.out_of_window_dq_threshold}
errorMessage={validation.out_of_window_dq_threshold ?? undefined}
/>
<Switch
isSelected={form.require_locators}
onValueChange={(value) => setForm((prev) => ({ ...prev, require_locators: value }))}
title={tRules("ruleset_help_require_locators")}
>
{helpLabel(
label("admin_rulesets_label_require_locators", "Požadovat lokátory"),
"ruleset_help_require_locators"
)}
</Switch>
</div>
</CardBody>
</Card>
<Card>
<CardHeader className="flex flex-col items-start gap-1">
<h3 className="text-lg font-semibold">
{label("admin_rulesets_section_multipliers_title", "Multiplikátory")}
</h3>
<p className="text-sm text-foreground-500">
{label(
"admin_rulesets_section_multipliers_desc",
"Nastavení typu a rozsahu multiplikátorů."
)}
</p>
</CardHeader>
<CardBody className="space-y-4">
<div className="grid gap-3 md:grid-cols-3 items-center">
<Switch
isSelected={form.use_multipliers}
onValueChange={(value) => setForm((prev) => ({ ...prev, use_multipliers: value }))}
title={tRules("ruleset_help_use_multipliers")}
>
{helpLabel(
label("admin_rulesets_label_use_multipliers", "Používat multiplikátory"),
"ruleset_help_use_multipliers"
)}
</Switch>
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_multiplier_type", "Multiplier type"),
"ruleset_help_multiplier_type"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_multiplier_type")}
value={form.multiplier_type}
onChange={(e) => setForm((prev) => ({ ...prev, multiplier_type: e.target.value }))}
>
<option value="NONE">NONE</option>
<option value="WWL">WWL</option>
<option value="DXCC">DXCC</option>
<option value="SECTION">SECTION</option>
<option value="COUNTRY">COUNTRY</option>
</select>
</label>
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_multiplier_scope", "Multiplier scope"),
"ruleset_help_multiplier_scope"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_multiplier_scope")}
value={form.multiplier_scope}
onChange={(e) => setForm((prev) => ({ ...prev, multiplier_scope: e.target.value }))}
>
<option value="PER_BAND">PER_BAND</option>
<option value="OVERALL">OVERALL</option>
</select>
</label>
</div>
<div className="grid gap-3 md:grid-cols-3">
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_multiplier_source", "Multiplier source"),
"ruleset_help_multiplier_source"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_multiplier_source")}
value={form.multiplier_source}
onChange={(e) => setForm((prev) => ({ ...prev, multiplier_source: e.target.value }))}
>
<option value="VALID_ONLY">VALID_ONLY</option>
<option value="ALL_MATCHED">ALL_MATCHED</option>
</select>
</label>
<label className="flex flex-col gap-1 text-sm">
<span className="text-foreground-500">
{helpLabel(
label("admin_rulesets_label_wwl_level", "WWL level"),
"ruleset_help_wwl_multiplier_level"
)}
</span>
<select
className="border border-divider rounded px-2 py-1 bg-background"
title={tRules("ruleset_help_wwl_multiplier_level")}
value={form.wwl_multiplier_level}
onChange={(e) => setForm((prev) => ({ ...prev, wwl_multiplier_level: e.target.value }))}
>
<option value="LOCATOR_2">LOCATOR_2</option>
<option value="LOCATOR_4">LOCATOR_4</option>
<option value="LOCATOR_6">LOCATOR_6</option>
</select>
</label>
</div>
</CardBody>
</Card>
<Accordion>
<AccordionItem
key="penalties"
aria-label={label("admin_rulesets_section_penalties_title", "Penalizace")}
title={label("admin_rulesets_section_penalties_title", "Penalizace")}
>
<Card>
<CardHeader className="flex flex-col items-start gap-1">
<h3 className="text-lg font-semibold">
{label("admin_rulesets_section_penalties_title", "Penalizace")}
</h3>
<p className="text-sm text-foreground-500">
{label(
"admin_rulesets_section_penalties_desc",
"Výše penalizací pro jednotlivé typy chyb."
)}
</p>
</CardHeader>
<CardBody className="space-y-3">
<div className="grid gap-3 md:grid-cols-3">
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_penalty_dup", "Penalty DUP"),
"ruleset_help_penalty_dup_points"
)}
title={tRules("ruleset_help_penalty_dup_points")}
value={form.penalty_dup_points}
onChange={(e) => setForm((prev) => ({ ...prev, penalty_dup_points: e.target.value }))}
isInvalid={!!validation.penalty_dup_points}
errorMessage={validation.penalty_dup_points ?? undefined}
/>
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_penalty_nil", "Penalty NIL"),
"ruleset_help_penalty_nil_points"
)}
title={tRules("ruleset_help_penalty_nil_points")}
value={form.penalty_nil_points}
onChange={(e) => setForm((prev) => ({ ...prev, penalty_nil_points: e.target.value }))}
isInvalid={!!validation.penalty_nil_points}
errorMessage={validation.penalty_nil_points ?? undefined}
/>
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_penalty_busted_call", "Penalty busted call"),
"ruleset_help_penalty_busted_call_points"
)}
title={tRules("ruleset_help_penalty_busted_call_points")}
value={form.penalty_busted_call_points}
onChange={(e) =>
setForm((prev) => ({ ...prev, penalty_busted_call_points: e.target.value }))
}
isInvalid={!!validation.penalty_busted_call_points}
errorMessage={validation.penalty_busted_call_points ?? undefined}
/>
</div>
<div className="grid gap-3 md:grid-cols-3">
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_penalty_busted_rst", "Penalty busted RST"),
"ruleset_help_penalty_busted_rst_points"
)}
title={tRules("ruleset_help_penalty_busted_rst_points")}
value={form.penalty_busted_rst_points}
onChange={(e) => setForm((prev) => ({ ...prev, penalty_busted_rst_points: e.target.value }))}
isInvalid={!!validation.penalty_busted_rst_points}
errorMessage={validation.penalty_busted_rst_points ?? undefined}
/>
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_penalty_busted_exchange", "Penalty busted exchange"),
"ruleset_help_penalty_busted_exchange_points"
)}
title={tRules("ruleset_help_penalty_busted_exchange_points")}
value={form.penalty_busted_exchange_points}
onChange={(e) =>
setForm((prev) => ({ ...prev, penalty_busted_exchange_points: e.target.value }))
}
isInvalid={!!validation.penalty_busted_exchange_points}
errorMessage={validation.penalty_busted_exchange_points ?? undefined}
/>
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_penalty_out_of_window", "Penalty out-of-window"),
"ruleset_help_penalty_out_of_window_points"
)}
title={tRules("ruleset_help_penalty_out_of_window_points")}
value={form.penalty_out_of_window_points}
onChange={(e) =>
setForm((prev) => ({ ...prev, penalty_out_of_window_points: e.target.value }))
}
isInvalid={!!validation.penalty_out_of_window_points}
errorMessage={validation.penalty_out_of_window_points ?? undefined}
/>
</div>
<div className="grid gap-3 md:grid-cols-3">
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_penalty_busted_serial", "Penalty busted serial"),
"ruleset_help_penalty_busted_serial_points"
)}
title={tRules("ruleset_help_penalty_busted_serial_points")}
value={form.penalty_busted_serial_points}
onChange={(e) =>
setForm((prev) => ({ ...prev, penalty_busted_serial_points: e.target.value }))
}
isInvalid={!!validation.penalty_busted_serial_points}
errorMessage={validation.penalty_busted_serial_points ?? undefined}
/>
<Input
type="number"
label={helpLabel(
label("admin_rulesets_label_penalty_busted_locator", "Penalty busted locator"),
"ruleset_help_penalty_busted_locator_points"
)}
title={tRules("ruleset_help_penalty_busted_locator_points")}
value={form.penalty_busted_locator_points}
onChange={(e) =>
setForm((prev) => ({ ...prev, penalty_busted_locator_points: e.target.value }))
}
isInvalid={!!validation.penalty_busted_locator_points}
errorMessage={validation.penalty_busted_locator_points ?? undefined}
/>
<div />
</div>
</CardBody>
</Card>
</AccordionItem>
</Accordion>
{formError && <div className="text-sm text-red-600">{formError}</div>}
{formSuccess && <div className="text-sm text-green-600">{formSuccess}</div>}
<div className="flex flex-wrap gap-3">
<Button type="submit" color="primary" isLoading={submitting} isDisabled={hasValidationErrors}>
{label("admin_rulesets_save", "Uložit změny")}
</Button>
<Button type="button" variant="bordered" onClick={onClose}>
{label("admin_form_close", "Zavřít formulář")}
</Button>
</div>
</form>
);
}