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

View File

@@ -0,0 +1,325 @@
import React from "react";
import { Button, Input, Select, SelectItem } from "@heroui/react";
import { type TFunction } from "i18next";
import { type EdiHeaderForm, type PSectResult } from "@/types/edi";
import { type BandOption } from "@/hooks/useRoundMeta";
type HeaderFormFieldsProps = {
headerForm: EdiHeaderForm;
setHeaderForm: React.Dispatch<React.SetStateAction<EdiHeaderForm>>;
markEdited: () => void;
onPbandChange: (value: string) => void;
onSpoweChange: (value: string) => void;
onApplyPsectCanonical: () => void;
t: TFunction<"common">;
tNameInvalid: boolean;
tDateError: string | null;
pCallEmpty: boolean;
pCallInvalid: boolean;
pwwloEmpty: boolean;
pwwloFormatInvalid: boolean;
pwwloInvalid: boolean;
psectMissing: boolean;
psectHasErrors: boolean;
psectValidation: PSectResult;
psectNeedsFormat: boolean;
psectCanonical: string | null;
shouldShowIaruAdjustButton: boolean;
bands: BandOption[];
bandsLoading: boolean;
bandsError: string | null;
bandUnknown: boolean;
bandMissing: boolean;
bandValue: string;
pbandInfo: string | null;
rhbbsWarning: string | null;
spoweInvalid: boolean;
spoweEmpty: boolean;
spoweTooLong: boolean;
spoweOverLimit: boolean;
spoweLimitError: string | null;
spoweInfo: string | null;
santeInvalid: boolean;
santeValue: string;
santeTooLong: boolean;
sectionNeedsSingle: boolean;
sectionNeedsMulti: boolean;
rcallInvalid: boolean;
mopeInvalid: boolean;
};
export default function HeaderFormFields({
headerForm,
setHeaderForm,
markEdited,
onPbandChange,
onSpoweChange,
onApplyPsectCanonical,
t,
tNameInvalid,
tDateError,
pCallEmpty,
pCallInvalid,
pwwloEmpty,
pwwloFormatInvalid,
pwwloInvalid,
psectMissing,
psectHasErrors,
psectValidation,
psectNeedsFormat,
psectCanonical,
shouldShowIaruAdjustButton,
bands,
bandsLoading,
bandsError,
bandUnknown,
bandMissing,
bandValue,
pbandInfo,
rhbbsWarning,
spoweInvalid,
spoweEmpty,
spoweTooLong,
spoweOverLimit,
spoweLimitError,
spoweInfo,
santeInvalid,
santeValue,
santeTooLong,
sectionNeedsSingle,
sectionNeedsMulti,
rcallInvalid,
mopeInvalid,
}: HeaderFormFieldsProps) {
return (
<>
<Input
label="TName"
value={headerForm.TName}
onChange={(e) => {
setHeaderForm((prev) => ({ ...prev, TName: e.target.value }));
markEdited();
}}
isInvalid={tNameInvalid}
errorMessage={tNameInvalid ? "TName je povinné." : undefined}
/>
<Input
label="TDate (YYYYMMDD;YYYYMMDD)"
value={headerForm.TDate}
onChange={(e) => {
setHeaderForm((prev) => ({ ...prev, TDate: e.target.value }));
markEdited();
}}
isInvalid={!!tDateError}
errorMessage={tDateError || undefined}
/>
<Input
label="PCall"
value={headerForm.PCall}
onChange={(e) => {
setHeaderForm((prev) => ({ ...prev, PCall: e.target.value }));
markEdited();
}}
isInvalid={pCallInvalid}
errorMessage={
pCallEmpty ? "PCall je povinné." : pCallInvalid ? "PCall musí být validní volací znak." : undefined
}
/>
<Input
label="PWWLo"
value={headerForm.PWWLo}
onChange={(e) => {
const normalized = e.target.value.toUpperCase();
setHeaderForm((prev) => ({ ...prev, PWWLo: normalized }));
markEdited();
}}
isInvalid={pwwloInvalid}
errorMessage={
pwwloEmpty
? "PWWLo je povinné."
: pwwloFormatInvalid
? "PWWLo musí mít 6 znaků ve formátu lokátoru (AA00AA)."
: undefined
}
/>
<div className="flex flex-col gap-1">
<div className="flex items-end gap-2">
<Input
className="flex-1"
label="PSect"
value={headerForm.PSect}
onChange={(e) => {
setHeaderForm((prev) => ({ ...prev, PSect: e.target.value }));
markEdited();
}}
isInvalid={psectMissing || psectHasErrors}
errorMessage={
psectMissing
? (t("upload_error_psect_required") as string) || "PSect je povinné."
: psectHasErrors
? psectValidation.errors.join(" ")
: undefined
}
/>
{shouldShowIaruAdjustButton && (
<Button
size="lg"
variant="flat"
className="shrink-0"
onPress={onApplyPsectCanonical}
isDisabled={psectMissing}
>
{psectCanonical ? (
<span className="flex flex-col text-left leading-tight">
<span>{(t("upload_psect_format_button") as string) || "Upravit kategorie podle IARU"}</span>
<span className="text-xs text-foreground-500">{psectCanonical}</span>
</span>
) : (
(t("upload_psect_format_button") as string) || "Upravit kategorie podle IARU"
)}
</Button>
)}
</div>
{psectNeedsFormat && (
<div className="text-xs text-foreground-500">
{(t("upload_error_psect_not_iaru") as string) || "PSect není ve formátu IARU."}
</div>
)}
</div>
<div className="flex flex-col gap-1">
{bands.length > 0 ? (
<Select
label="PBand"
selectedKeys={!bandUnknown && headerForm.PBand ? [headerForm.PBand] : []}
onChange={(e) => onPbandChange(e.target.value)}
isLoading={bandsLoading}
isInvalid={bandUnknown || bandMissing}
errorMessage={
bandUnknown
? `Neznámé pásmo "${bandValue}", vyber správnou hodnotu.`
: bandMissing
? "PBand není vyplněné, vyber pásmo ze seznamu."
: bandsError ?? undefined
}
>
{bands.map((band) => (
<SelectItem key={band.name} value={band.name}>
{band.name}
</SelectItem>
))}
</Select>
) : (
<Input
label="PBand"
value={headerForm.PBand}
onChange={(e) => onPbandChange(e.target.value)}
isDisabled={bandsLoading}
errorMessage={bandsError ?? undefined}
/>
)}
{pbandInfo && <div className="text-xs text-foreground-500">{pbandInfo}</div>}
</div>
<Input
label="RHBBS (e-mail)"
value={headerForm.RHBBS}
onChange={(e) => {
setHeaderForm((prev) => ({ ...prev, RHBBS: e.target.value }));
markEdited();
}}
isInvalid={false}
/>
{rhbbsWarning && <div className="text-xs text-amber-600">{rhbbsWarning}</div>}
<div className="flex flex-col gap-1">
<Input
label="SPowe"
value={headerForm.SPowe}
onChange={(e) => onSpoweChange(e.target.value)}
isInvalid={spoweInvalid}
errorMessage={
spoweEmpty
? (t("upload_error_spowe_required") as string) || "SPowe je povinné."
: spoweTooLong
? (t("upload_error_spowe_length") as string) || "SPowe může mít maximálně 12 znaků."
: spoweInvalid || spoweOverLimit
? (t("upload_error_spowe_format") as string) || "SPowe musí být celé číslo (bez jednotek)."
: undefined
}
/>
{spoweLimitError && <div className="text-xs text-red-600">{spoweLimitError}</div>}
{spoweInfo && <div className="text-xs text-foreground-500">{spoweInfo}</div>}
</div>
<div>
<Input
label="SAnte"
value={headerForm.SAnte}
onChange={(e) => {
setHeaderForm((prev) => ({ ...prev, SAnte: e.target.value }));
markEdited();
}}
isInvalid={santeInvalid}
errorMessage={
santeValue === "" ? (t("upload_error_sante_required") as string) || "SAnte je povinné." : undefined
}
/>
{santeTooLong && (
<div className="text-xs text-warning-600">
{t("upload_warn_sante_length") ??
"Ve výsledcích bude zobrazeno pouze 12 znaků, váš popis antény je delší a bude oříznut."}
</div>
)}
</div>
{(sectionNeedsSingle || sectionNeedsMulti) && (
<Input
label="RCall"
value={headerForm.RCall}
onChange={(e) => {
setHeaderForm((prev) => ({ ...prev, RCall: e.target.value }));
markEdited();
}}
isInvalid={rcallInvalid}
errorMessage={
headerForm.RCall.trim() === ""
? "RCall je povinné pro zvolenou kategorii."
: rcallInvalid
? "RCall musí být validní volací znak."
: undefined
}
/>
)}
{sectionNeedsMulti && (
<>
<Input
label="MOpe1"
value={headerForm.MOpe1}
onChange={(e) => {
setHeaderForm((prev) => ({ ...prev, MOpe1: e.target.value }));
markEdited();
}}
isInvalid={mopeInvalid}
errorMessage={
mopeInvalid
? (t("upload_error_mope_missing") as string) ||
"Vyplň alespoň MOpe1 nebo MOpe2 (můžeš zadat více značek oddělených mezerou/středníkem/čárkou)."
: undefined
}
/>
<Input
label="MOpe2"
value={headerForm.MOpe2}
onChange={(e) => {
setHeaderForm((prev) => ({ ...prev, MOpe2: e.target.value }));
markEdited();
}}
isInvalid={mopeInvalid}
errorMessage={
mopeInvalid
? (t("upload_error_mope_missing") as string) ||
"Vyplň alespoň MOpe1 nebo MOpe2 (můžeš zadat více značek oddělených mezerou/středníkem/čárkou)."
: undefined
}
/>
</>
)}
</>
);
}