From 33af1229649fa7d91cbe18b4547e086adca52b66 Mon Sep 17 00:00:00 2001 From: Gregory Salaun Date: Tue, 16 Jun 2026 09:50:48 +0200 Subject: [PATCH] awards --- frontend/src/components/AwardsPanel.tsx | 66 +++++++++++++++++++------ 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/AwardsPanel.tsx b/frontend/src/components/AwardsPanel.tsx index add0018..893d97c 100644 --- a/frontend/src/components/AwardsPanel.tsx +++ b/frontend/src/components/AwardsPanel.tsx @@ -58,7 +58,7 @@ function ProgressBar({ worked, confirmed, total }: { worked: number; confirmed: ); } -type AwardListItem = { code: string; name: string; valid?: boolean; bands?: string[] }; +type AwardListItem = { code: string; name: string; valid?: boolean; bands?: string[]; emission?: string[] }; export function AwardsPanel({ onEditQSO }: { onEditQSO?: (id: number) => void } = {}) { const [awardList, setAwardList] = useState([]); @@ -109,7 +109,7 @@ export function AwardsPanel({ onEditQSO }: { onEditQSO?: (id: number) => void } try { const defs = ((await GetAwardDefs()) ?? []) as any[]; const list: AwardListItem[] = defs - .map((d) => ({ code: d.code, name: d.name, valid: d.valid, bands: d.valid_bands ?? [] })) + .map((d) => ({ code: d.code, name: d.name, valid: d.valid, bands: d.valid_bands ?? [], emission: d.emission ?? [] })) .sort((a, b) => a.code.localeCompare(b.code)); setAwardList(list); const first = list.find((a) => a.code === selected) ?? list[0]; @@ -122,16 +122,52 @@ export function AwardsPanel({ onEditQSO }: { onEditQSO?: (id: number) => void } const current = byCode[selected]; - // Band columns for the reference matrix: restrict to the award's own valid - // bands (e.g. WAPC = 40/20/15/10m) so the grid isn't padded with bands the - // award doesn't count. An award with no band restriction shows all bands. - const gridBands = useMemo(() => { + // Bands relevant to the selected award, used by BOTH the grid and the stats + // matrix so neither is padded with bands the award doesn't use. Rule: the + // award's explicit valid_bands if it has any (e.g. WAPC = 40/20/15/10m); else + // the bands the operator actually has contacts on (so DXCC, which has no band + // restriction, shows 160m–6m instead of empty VHF/UHF columns). Empty set = + // no basis yet → callers fall back to all bands. + const awardBands = useMemo(() => { const vb = (awardList.find((a) => a.code === selected)?.bands ?? []).map((b) => b.toLowerCase()); - if (vb.length === 0) return GRID_BANDS; - const set = new Set(vb); - const filtered = GRID_BANDS.filter((b) => set.has(b)); + if (vb.length > 0) return new Set(vb); + const worked = (current?.bands ?? []) + .filter((b) => (b.worked ?? 0) > 0) + .map((b) => String(b.band).toLowerCase()); + return new Set(worked); + }, [awardList, selected, current]); + + const gridBands = useMemo(() => { + if (awardBands.size === 0) return GRID_BANDS; + const filtered = GRID_BANDS.filter((b) => awardBands.has(b)); return filtered.length ? filtered : GRID_BANDS; - }, [awardList, selected]); + }, [awardBands]); + + // Stats rows to show: drop the mode-category rows (CW / DIGITAL / PHONE) the + // award doesn't cover. The "ALL" rows (no suffix) always show; a category row + // shows only when the award has no emission restriction or lists that emission. + const statsRows = useMemo(() => { + if (!stats) return []; + const em = new Set((awardList.find((a) => a.code === selected)?.emission ?? []).map((e) => e.toUpperCase())); + if (em.size === 0) return stats.rows; + return stats.rows.filter((r) => { + const m = String(r.label).toUpperCase().match(/\b(CW|DIGITAL|PHONE)$/); + return !m || em.has(m[1]); + }); + }, [stats, awardList, selected]); + + // Indices into stats.bands to display (and to slice each row's cells by), same + // band basis as the grid. + const statsBandIdx = useMemo(() => { + if (!stats) return [] as number[]; + const all = stats.bands.map((_, i) => i); + if (awardBands.size === 0) return all; + const idx = stats.bands + .map((b, i) => ({ b: b.toLowerCase(), i })) + .filter((x) => awardBands.has(x.b)) + .map((x) => x.i); + return idx.length ? idx : all; + }, [stats, awardBands]); const filteredRefs = useMemo(() => { if (!current) return []; @@ -299,21 +335,21 @@ export function AwardsPanel({ onEditQSO }: { onEditQSO?: (id: number) => void } Statistic - {stats.bands.map((b) => {b})} + {statsBandIdx.map((i) => {stats.bands[i]})} Total Grand - {stats.rows.map((row, i) => { + {statsRows.map((row, i) => { const isGroupStart = row.label === 'WORKED' || /^WORKED /.test(row.label); return ( 0 && 'border-t-2 border-border')}> {row.label} - {row.cells.map((c, j) => ( - 0 ? 'bg-emerald-500/15 text-emerald-700' : 'text-muted-foreground/30')}>{c || ''} - ))} + {statsBandIdx.map((i) => { const c = row.cells[i] ?? 0; return ( + 0 ? 'bg-emerald-500/15 text-emerald-700' : 'text-muted-foreground/30')}>{c || ''} + ); })} {row.total || ''} {row.grand_total || ''}