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

250 lines
9.5 KiB
TypeScript

import { memo, useEffect, useState } from "react";
import { Button, Select, SelectItem, Textarea, type Selection } from "@heroui/react";
import { useTranslation } from "react-i18next";
import type { LogItem, LogOverride, OverrideForm } from "@/components/RoundEvaluationOverrides.types";
type RoundEvaluationOverrideRowProps = {
log: LogItem;
form?: OverrideForm;
override?: LogOverride;
bands: { id: number; name: string }[];
categories: { id: number; name: string }[];
powerCategories: { id: number; name: string }[];
onFieldChange: (logId: number, field: keyof OverrideForm, value: string) => void;
onReasonCommit: (logId: number, reason: string) => void;
onSave: (logId: number, reason?: string) => void;
onOpenDetail: (logId: number) => void;
};
const getFirstSelection = (keys: Selection) => {
if (keys === "all") return "";
const [first] = Array.from(keys);
return typeof first === "string" || typeof first === "number" ? String(first) : "";
};
const highlightSelectClassNames = (isHighlighted: boolean) =>
isHighlighted
? {
trigger: "border-warning-400 bg-warning-50",
}
: undefined;
const RoundEvaluationOverrideRow = memo(function RoundEvaluationOverrideRow({
log,
form,
override,
bands,
categories,
powerCategories,
onFieldChange,
onReasonCommit,
onSave,
onOpenDetail,
}: RoundEvaluationOverrideRowProps) {
const { t } = useTranslation("common");
const highlightStatus = !!override?.forced_log_status && override.forced_log_status !== "AUTO";
const highlightBand = override?.forced_band_id !== null && override?.forced_band_id !== undefined;
const highlightCategory =
override?.forced_category_id !== null && override?.forced_category_id !== undefined;
const highlightPower =
override?.forced_power_category_id !== null && override?.forced_power_category_id !== undefined;
const highlightSixhr =
override?.forced_sixhr_category !== null && override?.forced_sixhr_category !== undefined;
const [localReason, setLocalReason] = useState(form?.reason ?? override?.reason ?? "");
useEffect(() => {
setLocalReason(form?.reason ?? override?.reason ?? "");
}, [form?.reason, override?.reason]);
return (
<div className="rounded border border-divider px-2 py-1 text-xs">
<div className="flex flex-wrap items-center gap-2 mb-2 text-sm">
<span className="font-semibold">{log.pcall ?? "—"}</span>
<span>PBAND: {log.pband ?? "—"}</span>
<span>PSECT: {log.psect ?? "—"}</span>
<span>SPowe: {log.power_watt ?? "—"}</span>
<span>PWWLo: {log.pwwlo ?? "—"}</span>
<Button
type="button"
size="sm"
variant="light"
className="h-7 min-h-[28px] text-xs px-2"
onPress={() => onOpenDetail(log.id)}
>
{t("override_detail") ?? "Detail"}
</Button>
</div>
{override?.reason && (
<div className="mb-2 text-foreground-500 text-sm">
{t("override_reason_prefix") ?? "Důvod"}: {override.reason}
</div>
)}
<div className="grid gap-1 md:grid-cols-7">
<Select
label={t("override_status_label") ?? "Status"}
aria-label="Status"
size="sm"
variant="bordered"
classNames={{
...highlightSelectClassNames(highlightStatus),
trigger: "h-8 min-h-[32px]",
}}
selectedKeys={new Set([form?.status ?? "AUTO"])}
onSelectionChange={(keys) =>
onFieldChange(log.id, "status", getFirstSelection(keys) || "AUTO")
}
selectionMode="single"
disallowEmptySelection={true}
>
{[
{ value: "AUTO", label: t("override_status_auto") ?? "AUTO" },
{ value: "IGNORED", label: t("override_status_ignored") ?? "IGNORED" },
{ value: "CHECK", label: t("override_status_check") ?? "CHECK" },
{ value: "OK", label: t("override_status_ok") ?? "OK" },
{ value: "DQ", label: t("override_status_dq") ?? "DQ" },
].map((opt) => (
<SelectItem key={opt.value} textValue={opt.label}>
{opt.label}
</SelectItem>
))}
</Select>
<Select
label={t("override_band_label") ?? "Band"}
aria-label="Band"
size="sm"
variant="bordered"
classNames={{
...highlightSelectClassNames(highlightBand),
trigger: "h-8 min-h-[32px]",
}}
selectedKeys={new Set([form?.bandId ? form.bandId : "auto"])}
onSelectionChange={(keys) => {
const value = getFirstSelection(keys);
onFieldChange(log.id, "bandId", value === "auto" ? "" : value);
}}
selectionMode="single"
disallowEmptySelection={true}
>
<SelectItem key="auto" textValue={t("override_auto") ?? "AUTO"}>
{t("override_auto") ?? "AUTO"}
</SelectItem>
{bands.map((band) => (
<SelectItem key={String(band.id)} textValue={band.name}>
{band.name}
</SelectItem>
))}
</Select>
<Select
label={t("override_category_label") ?? "Kategorie"}
aria-label="Kategorie"
size="sm"
variant="bordered"
classNames={{
...highlightSelectClassNames(highlightCategory),
trigger: "h-8 min-h-[32px]",
}}
selectedKeys={new Set([form?.categoryId ? form.categoryId : "auto"])}
onSelectionChange={(keys) => {
const value = getFirstSelection(keys);
onFieldChange(log.id, "categoryId", value === "auto" ? "" : value);
}}
selectionMode="single"
disallowEmptySelection={true}
>
<SelectItem key="auto" textValue={t("override_auto") ?? "AUTO"}>
{t("override_auto") ?? "AUTO"}
</SelectItem>
{categories.map((cat) => (
<SelectItem key={String(cat.id)} textValue={cat.name}>
{cat.name}
</SelectItem>
))}
</Select>
<Select
aria-label="Výkon"
size="sm"
label={t("override_power_label") ?? "Výkon"}
variant="bordered"
classNames={{
...highlightSelectClassNames(highlightPower),
trigger: "h-8 min-h-[32px]",
}}
selectedKeys={new Set([form?.powerCategoryId ? form.powerCategoryId : "auto"])}
onSelectionChange={(keys) => {
const value = getFirstSelection(keys);
onFieldChange(log.id, "powerCategoryId", value === "auto" ? "" : value);
}}
selectionMode="single"
disallowEmptySelection={true}
>
<SelectItem key="auto" textValue={t("override_auto") ?? "AUTO"}>
{t("override_auto") ?? "AUTO"}
</SelectItem>
{powerCategories.map((cat) => (
<SelectItem key={String(cat.id)} textValue={cat.name}>
{cat.name}
</SelectItem>
))}
</Select>
<Select
aria-label="6H"
size="sm"
label={t("override_sixhr_label") ?? "6H"}
variant="bordered"
classNames={{
...highlightSelectClassNames(highlightSixhr),
trigger: "h-8 min-h-[32px]",
}}
selectedKeys={new Set([form?.sixhrCategory ? form.sixhrCategory : "auto"])}
onSelectionChange={(keys) => {
const value = getFirstSelection(keys);
onFieldChange(log.id, "sixhrCategory", value === "auto" ? "" : value);
}}
selectionMode="single"
disallowEmptySelection={true}
>
<SelectItem key="auto" textValue={t("override_auto") ?? "AUTO"}>
{t("override_auto") ?? "AUTO"}
</SelectItem>
<SelectItem key="1" textValue={t("yes") ?? "Ano"}>
{t("yes") ?? "Ano"}
</SelectItem>
<SelectItem key="0" textValue={t("no") ?? "Ne"}>
{t("no") ?? "Ne"}
</SelectItem>
</Select>
<Textarea
label={t("override_reason_label") ?? "Důvod změny"}
size="sm"
variant="bordered"
minRows={1}
value={localReason}
onChange={(e) => setLocalReason(e.target.value)}
onBlur={() => onReasonCommit(log.id, localReason)}
classNames={{
inputWrapper: "h-8 min-h-[32px] py-0",
input: "h-5 text-xs",
}}
/>
<div className="flex items-center gap-3">
<Button
type="button"
size="sm"
color="primary"
isDisabled={form?.saving}
onPress={() => onSave(log.id, localReason)}
>
{form?.saving
? t("override_saving") ?? "Ukládám…"
: t("override_save") ?? "Uložit"}
</Button>
{form?.error && <span className="text-red-600">{form.error}</span>}
{form?.success && <span className="text-green-600">{form.success}</span>}
</div>
</div>
</div>
);
});
export default RoundEvaluationOverrideRow;