Initial commit
This commit is contained in:
249
resources/js/components/RoundEvaluationOverrideRow.tsx
Normal file
249
resources/js/components/RoundEvaluationOverrideRow.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user