Initial commit

This commit is contained in:
Zdeněk Burda
2026-01-09 21:26:40 +01:00
parent e83aec6dca
commit 41e3ce6f25
404 changed files with 61250 additions and 28 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,74 @@
import React from "react";
import {
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
} from "@heroui/react";
import type { EvaluationRuleSet } from "./adminRulesetTypes";
type AdminRulesetsTableProps = {
items: EvaluationRuleSet[];
locale: string;
valuePlaceholder: string;
formatDate: (value: string | null | undefined, lang: string) => string;
onEdit: (item: EvaluationRuleSet) => void;
t: (key: string) => string;
};
const PencilIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="h-4 w-4"
>
<path d="M13.586 3.586a2 2 0 0 1 2.828 2.828l-8.25 8.25a2 2 0 0 1-.878.518l-3.122.89a.75.75 0 0 1-.926-.926l.89-3.122a2 2 0 0 1 .518-.878l8.25-8.25Z" />
<path d="M12.475 4.697 4.75 12.422l-.69 2.422 2.422-.69 7.725-7.725-1.732-1.732Z" />
</svg>
);
export default function AdminRulesetsTable({
items,
locale,
valuePlaceholder,
formatDate,
onEdit,
t,
}: AdminRulesetsTableProps) {
return (
<Table aria-label={t("admin_rulesets_table_aria") ?? "Evaluation rulesets table"} selectionMode="none">
<TableHeader>
<TableColumn>{t("admin_rulesets_table_name") ?? "Název"}</TableColumn>
<TableColumn>{t("admin_rulesets_table_code") ?? "Kód"}</TableColumn>
<TableColumn>{t("admin_rulesets_table_scoring") ?? "Scoring"}</TableColumn>
<TableColumn>{t("admin_rulesets_table_multiplier") ?? "Multiplier"}</TableColumn>
<TableColumn>{t("admin_rulesets_table_updated") ?? "Aktualizace"}</TableColumn>
<TableColumn></TableColumn>
</TableHeader>
<TableBody items={items}>
{(item) => (
<TableRow key={item.id}>
<TableCell>{item.name}</TableCell>
<TableCell>{item.code}</TableCell>
<TableCell>{item.scoring_mode ?? valuePlaceholder}</TableCell>
<TableCell>{item.multiplier_type ?? valuePlaceholder}</TableCell>
<TableCell>{formatDate(item.updated_at ?? null, locale)}</TableCell>
<TableCell>
<button
type="button"
onClick={() => onEdit(item)}
className="inline-flex items-center p-1 rounded hover:bg-default-100 text-default-500 hover:text-default-700"
aria-label={t("admin_rulesets_edit_aria") ?? "Upravit rule set"}
>
<PencilIcon />
</button>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
);
}

View File

@@ -0,0 +1,259 @@
export type EvaluationRuleSet = {
id: number;
name: string;
code: string;
description?: string | null;
scoring_mode?: string | null;
points_per_qso?: number | null;
points_per_km?: number | null;
use_multipliers?: boolean | null;
multiplier_type?: string | null;
dup_qso_policy?: string | null;
nil_qso_policy?: string | null;
no_counterpart_log_policy?: string | null;
not_in_counterpart_log_policy?: string | null;
unique_qso_policy?: string | null;
busted_call_policy?: string | null;
busted_rst_policy?: string | null;
busted_exchange_policy?: string | null;
busted_serial_policy?: string | null;
busted_locator_policy?: string | null;
penalty_dup_points?: number | null;
penalty_nil_points?: number | null;
penalty_busted_call_points?: number | null;
penalty_busted_rst_points?: number | null;
penalty_busted_exchange_points?: number | null;
penalty_busted_serial_points?: number | null;
penalty_busted_locator_points?: number | null;
penalty_out_of_window_points?: number | null;
dupe_scope?: string | null;
callsign_normalization?: string | null;
distance_rounding?: string | null;
min_distance_km?: number | null;
require_locators?: boolean | null;
out_of_window_policy?: string | null;
exchange_type?: string | null;
exchange_requires_wwl?: boolean | null;
exchange_requires_serial?: boolean | null;
exchange_requires_report?: boolean | null;
exchange_pattern?: string | null;
match_tiebreak_order?: string[] | null;
match_require_locator_match?: boolean | null;
match_require_exchange_match?: boolean | null;
multiplier_scope?: string | null;
multiplier_source?: string | null;
wwl_multiplier_level?: string | null;
checklog_matching?: boolean | null;
out_of_window_dq_threshold?: number | null;
time_diff_dq_threshold_percent?: number | null;
time_diff_dq_threshold_sec?: number | null;
bad_qso_dq_threshold_percent?: number | null;
time_tolerance_sec?: number | null;
allow_time_shift_one_hour?: boolean | null;
time_shift_seconds?: number | null;
time_mismatch_policy?: string | null;
allow_time_mismatch_pairing?: boolean | null;
time_mismatch_max_sec?: number | null;
require_unique_qso?: boolean | null;
ignore_slash_part?: boolean | null;
ignore_third_part?: boolean | null;
rst_ignore_third_char?: boolean | null;
callsign_suffix_max_len?: number | null;
callsign_levenshtein_max?: number | null;
letters_in_rst?: boolean | null;
discard_qso_rec_diff_call?: boolean | null;
discard_qso_sent_diff_call?: boolean | null;
discard_qso_rec_diff_rst?: boolean | null;
discard_qso_sent_diff_rst?: boolean | null;
discard_qso_rec_diff_serial?: boolean | null;
discard_qso_sent_diff_serial?: boolean | null;
discard_qso_rec_diff_wwl?: boolean | null;
discard_qso_sent_diff_wwl?: boolean | null;
discard_qso_rec_diff_code?: boolean | null;
discard_qso_sent_diff_code?: boolean | null;
dup_resolution_strategy?: string[] | null;
operating_window_mode?: string | null;
operating_window_hours?: number | null;
sixhr_ranking_mode?: string | null;
options?: Record<string, unknown> | null;
updated_at?: string | null;
};
export type RuleSetForm = {
name: string;
code: string;
description: string;
scoring_mode: string;
points_per_qso: string;
points_per_km: string;
use_multipliers: boolean;
multiplier_type: string;
dup_qso_policy: string;
nil_qso_policy: string;
no_counterpart_log_policy: string;
not_in_counterpart_log_policy: string;
unique_qso_policy: string;
busted_call_policy: string;
busted_rst_policy: string;
busted_exchange_policy: string;
busted_serial_policy: string;
busted_locator_policy: string;
penalty_dup_points: string;
penalty_nil_points: string;
penalty_busted_call_points: string;
penalty_busted_rst_points: string;
penalty_busted_exchange_points: string;
penalty_busted_serial_points: string;
penalty_busted_locator_points: string;
penalty_out_of_window_points: string;
dupe_scope: string;
callsign_normalization: string;
distance_rounding: string;
min_distance_km: string;
require_locators: boolean;
out_of_window_policy: string;
exchange_type: string;
exchange_requires_wwl: boolean;
exchange_requires_serial: boolean;
exchange_requires_report: boolean;
exchange_pattern: string;
match_tiebreak_order: string;
match_require_locator_match: boolean;
match_require_exchange_match: boolean;
multiplier_scope: string;
multiplier_source: string;
wwl_multiplier_level: string;
checklog_matching: boolean;
out_of_window_dq_threshold: string;
time_diff_dq_threshold_percent: string;
time_diff_dq_threshold_sec: string;
bad_qso_dq_threshold_percent: string;
time_tolerance_sec: string;
allow_time_shift_one_hour: boolean;
time_shift_seconds: string;
time_mismatch_policy: string;
allow_time_mismatch_pairing: boolean;
time_mismatch_max_sec: string;
require_unique_qso: boolean;
ignore_slash_part: boolean;
ignore_third_part: boolean;
rst_ignore_third_char: boolean;
callsign_suffix_max_len: string;
callsign_levenshtein_max: string;
letters_in_rst: boolean;
discard_qso_rec_diff_call: boolean;
discard_qso_sent_diff_call: boolean;
discard_qso_rec_diff_rst: boolean;
discard_qso_sent_diff_rst: boolean;
discard_qso_rec_diff_serial: boolean;
discard_qso_sent_diff_serial: boolean;
discard_qso_rec_diff_wwl: boolean;
discard_qso_sent_diff_wwl: boolean;
discard_qso_rec_diff_code: boolean;
discard_qso_sent_diff_code: boolean;
dup_resolution_strategy: string;
operating_window_mode: string;
operating_window_hours: string;
sixhr_ranking_mode: string;
};
export type RuleSetFormMode = "none" | "create" | "edit";
export const emptyForm: RuleSetForm = {
name: "",
code: "",
description: "",
scoring_mode: "DISTANCE",
points_per_qso: "",
points_per_km: "",
use_multipliers: true,
multiplier_type: "WWL",
dup_qso_policy: "ZERO_POINTS",
nil_qso_policy: "PENALTY",
no_counterpart_log_policy: "PENALTY",
not_in_counterpart_log_policy: "PENALTY",
unique_qso_policy: "ZERO_POINTS",
busted_call_policy: "PENALTY",
busted_rst_policy: "ZERO_POINTS",
busted_exchange_policy: "ZERO_POINTS",
busted_serial_policy: "ZERO_POINTS",
busted_locator_policy: "ZERO_POINTS",
penalty_dup_points: "",
penalty_nil_points: "",
penalty_busted_call_points: "",
penalty_busted_rst_points: "",
penalty_busted_exchange_points: "",
penalty_busted_serial_points: "",
penalty_busted_locator_points: "",
penalty_out_of_window_points: "",
dupe_scope: "BAND",
callsign_normalization: "IGNORE_SUFFIX",
distance_rounding: "FLOOR",
min_distance_km: "",
require_locators: true,
out_of_window_policy: "INVALID",
exchange_type: "SERIAL_WWL",
exchange_requires_wwl: true,
exchange_requires_serial: true,
exchange_requires_report: false,
exchange_pattern: "",
match_tiebreak_order: "",
match_require_locator_match: false,
match_require_exchange_match: false,
multiplier_scope: "PER_BAND",
multiplier_source: "VALID_ONLY",
wwl_multiplier_level: "LOCATOR_6",
checklog_matching: true,
out_of_window_dq_threshold: "",
time_diff_dq_threshold_percent: "",
time_diff_dq_threshold_sec: "",
bad_qso_dq_threshold_percent: "",
time_tolerance_sec: "",
allow_time_shift_one_hour: true,
time_shift_seconds: "3600",
time_mismatch_policy: "FLAG_ONLY",
allow_time_mismatch_pairing: true,
time_mismatch_max_sec: "",
require_unique_qso: true,
ignore_slash_part: true,
ignore_third_part: true,
rst_ignore_third_char: true,
callsign_suffix_max_len: "4",
callsign_levenshtein_max: "2",
letters_in_rst: true,
discard_qso_rec_diff_call: true,
discard_qso_sent_diff_call: false,
discard_qso_rec_diff_rst: true,
discard_qso_sent_diff_rst: false,
discard_qso_rec_diff_serial: true,
discard_qso_sent_diff_serial: false,
discard_qso_rec_diff_wwl: true,
discard_qso_sent_diff_wwl: false,
discard_qso_rec_diff_code: true,
discard_qso_sent_diff_code: false,
dup_resolution_strategy: "paired_first, ok_first, earlier_time, lower_id",
operating_window_mode: "NONE",
operating_window_hours: "",
sixhr_ranking_mode: "IARU",
};
export const numberValue = (value?: number | null) =>
value === null || value === undefined ? "" : String(value);
export const toNumberOrNull = (value: string) => {
if (!value.trim()) return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
};
export const toIntOrNull = (value: string) => {
if (!value.trim()) return null;
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) ? parsed : null;
};
export const isBlank = (value: string) => !value.trim();
export const isNumberLike = (value: string) =>
value.trim() !== "" && Number.isFinite(Number(value));
export const isIntegerLike = (value: string) =>
value.trim() !== "" && Number.isInteger(Number(value));