Initial commit
This commit is contained in:
300
resources/js/components/RoundEvaluationLogOverridesTable.tsx
Normal file
300
resources/js/components/RoundEvaluationLogOverridesTable.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
import React, { useMemo } from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Divider,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableColumn,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@heroui/react";
|
||||
import type { LogResult, LogOverride } from "@/components/RoundEvaluationLogOverrides.types";
|
||||
|
||||
type GroupedResults = Array<{ key: string; items: LogResult[] }>;
|
||||
|
||||
type Props = {
|
||||
items: LogResult[];
|
||||
overrides: Record<number, LogOverride>;
|
||||
onEdit: (logId: number) => void;
|
||||
};
|
||||
|
||||
function PencilIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
className={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>
|
||||
);
|
||||
}
|
||||
|
||||
function groupBy(items: LogResult[], getKey: (r: LogResult) => string): GroupedResults {
|
||||
const map = new Map<string, LogResult[]>();
|
||||
items.forEach((item) => {
|
||||
const key = getKey(item);
|
||||
if (!map.has(key)) map.set(key, []);
|
||||
map.get(key)!.push(item);
|
||||
});
|
||||
return Array.from(map.entries()).map(([key, list]) => ({ key, items: list }));
|
||||
}
|
||||
|
||||
function sortGroups(groups: GroupedResults): GroupedResults {
|
||||
return [...groups].sort((a, b) => {
|
||||
const aItem = a.items[0];
|
||||
const bItem = b.items[0];
|
||||
const bandOrder =
|
||||
(aItem?.band?.order ?? Number.MAX_SAFE_INTEGER) -
|
||||
(bItem?.band?.order ?? Number.MAX_SAFE_INTEGER);
|
||||
if (bandOrder !== 0) return bandOrder;
|
||||
const categoryOrder =
|
||||
(aItem?.category?.order ?? Number.MAX_SAFE_INTEGER) -
|
||||
(bItem?.category?.order ?? Number.MAX_SAFE_INTEGER);
|
||||
if (categoryOrder !== 0) return categoryOrder;
|
||||
const powerOrder =
|
||||
(aItem?.power_category?.order ?? Number.MAX_SAFE_INTEGER) -
|
||||
(bItem?.power_category?.order ?? Number.MAX_SAFE_INTEGER);
|
||||
if (powerOrder !== 0) return powerOrder;
|
||||
const bandName = aItem?.band?.name ?? "";
|
||||
const bandNameB = bItem?.band?.name ?? "";
|
||||
if (bandName !== bandNameB) return bandName.localeCompare(bandNameB);
|
||||
const categoryName = aItem?.category?.name ?? "";
|
||||
const categoryNameB = bItem?.category?.name ?? "";
|
||||
if (categoryName !== categoryNameB) return categoryName.localeCompare(categoryNameB);
|
||||
const powerName = aItem?.power_category?.name ?? "";
|
||||
const powerNameB = bItem?.power_category?.name ?? "";
|
||||
return powerName.localeCompare(powerNameB);
|
||||
});
|
||||
}
|
||||
|
||||
function resolveNames(item: LogResult): [string | null, string | null, string | null] {
|
||||
const band = item.band?.name ?? null;
|
||||
const category = item.category?.name ?? null;
|
||||
const power = item.power_category?.name ?? null;
|
||||
return [band, category, power];
|
||||
}
|
||||
|
||||
type RankField = "rank_overall" | "rank_in_category";
|
||||
|
||||
function isCheckCategory(item: LogResult) {
|
||||
const name = item.category?.name?.toLowerCase() ?? "";
|
||||
return name.includes("check");
|
||||
}
|
||||
|
||||
function isPowerA(item: LogResult) {
|
||||
const name = item.power_category?.name?.toLowerCase() ?? "";
|
||||
return name === "a";
|
||||
}
|
||||
|
||||
function sortResults(items: LogResult[], rankField: RankField) {
|
||||
return [...items].sort((a, b) => {
|
||||
const ra = a[rankField] ?? Number.MAX_SAFE_INTEGER;
|
||||
const rb = b[rankField] ?? Number.MAX_SAFE_INTEGER;
|
||||
if (ra !== rb) return ra - rb;
|
||||
const sa = a.official_score ?? 0;
|
||||
const sb = b.official_score ?? 0;
|
||||
if (sa !== sb) return sb - sa;
|
||||
const qa = a.valid_qso_count ?? 0;
|
||||
const qb = b.valid_qso_count ?? 0;
|
||||
return qb - qa;
|
||||
});
|
||||
}
|
||||
|
||||
function formatNumber(value: number | null | undefined) {
|
||||
if (value === null || value === undefined) return "—";
|
||||
if (Number.isInteger(value)) return value.toString();
|
||||
return value.toFixed(1);
|
||||
}
|
||||
|
||||
function formatRatio(score?: number | null, qso?: number | null) {
|
||||
if (!score || !qso || qso === 0) return "—";
|
||||
return (score / qso).toFixed(2);
|
||||
}
|
||||
|
||||
function renderGroup(
|
||||
group: GroupedResults,
|
||||
title: string,
|
||||
rankField: RankField,
|
||||
includePowerInHeading: boolean,
|
||||
overrides: Record<number, LogOverride>,
|
||||
onEdit: (logId: number) => void
|
||||
) {
|
||||
if (group.length === 0) return [];
|
||||
return group.map(({ key, items }) => {
|
||||
const sample = items[0];
|
||||
const [bandName, categoryName, powerName] = resolveNames(sample);
|
||||
const headingParts = [bandName, categoryName, title];
|
||||
if (includePowerInHeading && !isCheckCategory(sample)) {
|
||||
headingParts.push(powerName);
|
||||
}
|
||||
const heading = headingParts.filter(Boolean).join(" ");
|
||||
const sorted = sortResults(items, rankField);
|
||||
|
||||
return (
|
||||
<Card key={key} className="mb-3">
|
||||
<CardHeader className="py-2">
|
||||
<div className="flex items-center justify-between w-full gap-2">
|
||||
<span className="text-md font-semibold">{heading}</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<Divider />
|
||||
<CardBody className="py-2">
|
||||
<Table
|
||||
radius="sm"
|
||||
isCompact
|
||||
aria-label={heading}
|
||||
removeWrapper
|
||||
fullWidth
|
||||
classNames={{
|
||||
th: "py-1 px-1 text-[11px] leading-tight",
|
||||
td: "py-0.5 px-1 text-[11px] leading-tight bg-transparent",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn className="whitespace-nowrap">Pořadí</TableColumn>
|
||||
<TableColumn className="whitespace-nowrap">Značka</TableColumn>
|
||||
<TableColumn className="whitespace-nowrap">Lokátor</TableColumn>
|
||||
<TableColumn className="whitespace-nowrap">Kategorie</TableColumn>
|
||||
<TableColumn className="whitespace-nowrap">Pásmo</TableColumn>
|
||||
<TableColumn className="whitespace-nowrap">Výkon [W]</TableColumn>
|
||||
<TableColumn className="whitespace-nowrap">Výkonová kategorie</TableColumn>
|
||||
<TableColumn className="whitespace-nowrap">Body celkem</TableColumn>
|
||||
<TableColumn className="whitespace-nowrap">Počet QSO</TableColumn>
|
||||
<TableColumn className="whitespace-nowrap">Body/QSO</TableColumn>
|
||||
<TableColumn className="whitespace-nowrap">ODX</TableColumn>
|
||||
<TableColumn className="whitespace-nowrap">Anténa</TableColumn>
|
||||
<TableColumn className="whitespace-nowrap">Ant. height</TableColumn>
|
||||
<TableColumn className="whitespace-nowrap">Status</TableColumn>
|
||||
<TableColumn className="whitespace-nowrap">Komentář rozhodčího</TableColumn>
|
||||
<TableColumn className="whitespace-nowrap">Akce</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody emptyContent="No rows to display" items={sorted} className="text-[11px] leading-tight">
|
||||
{(item) => {
|
||||
const override = overrides[item.log_id];
|
||||
const rowHighlight =
|
||||
!!override &&
|
||||
(override.forced_log_status && override.forced_log_status !== "AUTO" ||
|
||||
override.forced_band_id ||
|
||||
override.forced_category_id ||
|
||||
override.forced_power_category_id ||
|
||||
override.forced_power_w !== null && override.forced_power_w !== undefined ||
|
||||
override.forced_sixhr_category !== null && override.forced_sixhr_category !== undefined);
|
||||
return (
|
||||
<TableRow key={item.id} className={rowHighlight ? "bg-warning-100 hover:bg-warning-300" : "hover:bg-default-200"}>
|
||||
<TableCell className="whitespace-nowrap">{item[rankField] ?? "—"}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">{item.log?.pcall ?? "—"}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
{item.log?.pwwlo ?? item.log?.locator ?? "—"}
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap">{item.category?.name ?? "—"}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">{item.band?.name ?? "—"}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
{formatNumber(item.log?.power_watt)}
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
{item.powerCategory?.name ?? item.power_category?.name ?? "—"}
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap">{formatNumber(item.official_score)}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">{item.valid_qso_count ?? "—"}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
{formatRatio(item.official_score, item.valid_qso_count)}
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap">{item.log?.codxc ?? "—"}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">{item.log?.sante ?? "—"}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">{item.log?.santh ?? "—"}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">{item.status ?? "—"}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">{override?.reason ?? "—"}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
variant="light"
|
||||
className="min-w-0 w-4 h-4 p-0"
|
||||
onPress={() => onEdit(item.log_id)}
|
||||
aria-label="Upravit override"
|
||||
>
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function renderOverallWithPowers(
|
||||
overalls: GroupedResults,
|
||||
powers: GroupedResults,
|
||||
title: string,
|
||||
overrides: Record<number, LogOverride>,
|
||||
onEdit: (logId: number) => void
|
||||
) {
|
||||
if (overalls.length === 0) return null;
|
||||
|
||||
const powerIndex = powers.reduce<Record<string, GroupedResults>>((acc, group) => {
|
||||
const [bandId, categoryId] = group.key.split("|");
|
||||
const key = `${bandId}|${categoryId}`;
|
||||
acc[key] = acc[key] || [];
|
||||
acc[key].push(group);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return sortGroups(overalls).flatMap(({ key, items }) => {
|
||||
const [bandId, categoryId] = key.split("|");
|
||||
const powerGroups = sortGroups(powerIndex[`${bandId}|${categoryId}`] ?? []);
|
||||
|
||||
return [
|
||||
...renderGroup([{ key, items }], title, "rank_overall", false, overrides, onEdit),
|
||||
...renderGroup(powerGroups, "", "rank_in_category", true, overrides, onEdit),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
export default React.memo(function RoundEvaluationLogOverridesTable({
|
||||
items,
|
||||
overrides,
|
||||
onEdit,
|
||||
}: Props) {
|
||||
const grouped = useMemo(() => {
|
||||
const isSixhr = (r: LogResult) => (r.log?.sixhr_category ?? false) === true;
|
||||
const sixh = items.filter((r) => isSixhr(r));
|
||||
const standard = items.filter((r) => !isSixhr(r));
|
||||
const powerEligible = standard.filter(
|
||||
(r) => !isCheckCategory(r) && !isPowerA(r) && r.power_category && r.rank_in_category !== null
|
||||
);
|
||||
|
||||
const groupOverall = (list: LogResult[]) =>
|
||||
groupBy(list, (r) => `${r.band?.id ?? "null"}|${r.category?.id ?? "null"}`);
|
||||
const groupPower = (list: LogResult[]) =>
|
||||
groupBy(
|
||||
list,
|
||||
(r) =>
|
||||
`${r.band?.id ?? "null"}|${r.category?.id ?? "null"}|${r.power_category?.id ?? "null"}`
|
||||
);
|
||||
|
||||
return {
|
||||
sixhOverall: groupOverall(sixh),
|
||||
standardOverall: groupOverall(standard),
|
||||
standardPower: groupPower(powerEligible),
|
||||
};
|
||||
}, [items]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderOverallWithPowers(grouped.standardOverall, grouped.standardPower, "", overrides, onEdit)}
|
||||
{renderOverallWithPowers(grouped.sixhOverall, [], "6H", overrides, onEdit)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user