1326 lines
58 KiB
TypeScript
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>
|
|
);
|
|
}
|