awards
This commit is contained in:
@@ -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 } = {}) {
|
export function AwardsPanel({ onEditQSO }: { onEditQSO?: (id: number) => void } = {}) {
|
||||||
const [awardList, setAwardList] = useState<AwardListItem[]>([]);
|
const [awardList, setAwardList] = useState<AwardListItem[]>([]);
|
||||||
@@ -109,7 +109,7 @@ export function AwardsPanel({ onEditQSO }: { onEditQSO?: (id: number) => void }
|
|||||||
try {
|
try {
|
||||||
const defs = ((await GetAwardDefs()) ?? []) as any[];
|
const defs = ((await GetAwardDefs()) ?? []) as any[];
|
||||||
const list: AwardListItem[] = defs
|
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));
|
.sort((a, b) => a.code.localeCompare(b.code));
|
||||||
setAwardList(list);
|
setAwardList(list);
|
||||||
const first = list.find((a) => a.code === selected) ?? list[0];
|
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];
|
const current = byCode[selected];
|
||||||
|
|
||||||
// Band columns for the reference matrix: restrict to the award's own valid
|
// Bands relevant to the selected award, used by BOTH the grid and the stats
|
||||||
// bands (e.g. WAPC = 40/20/15/10m) so the grid isn't padded with bands the
|
// matrix so neither is padded with bands the award doesn't use. Rule: the
|
||||||
// award doesn't count. An award with no band restriction shows all bands.
|
// award's explicit valid_bands if it has any (e.g. WAPC = 40/20/15/10m); else
|
||||||
const gridBands = useMemo(() => {
|
// 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());
|
const vb = (awardList.find((a) => a.code === selected)?.bands ?? []).map((b) => b.toLowerCase());
|
||||||
if (vb.length === 0) return GRID_BANDS;
|
if (vb.length > 0) return new Set(vb);
|
||||||
const set = new Set(vb);
|
const worked = (current?.bands ?? [])
|
||||||
const filtered = GRID_BANDS.filter((b) => set.has(b));
|
.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;
|
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(() => {
|
const filteredRefs = useMemo(() => {
|
||||||
if (!current) return [];
|
if (!current) return [];
|
||||||
@@ -299,21 +335,21 @@ export function AwardsPanel({ onEditQSO }: { onEditQSO?: (id: number) => void }
|
|||||||
<thead className="sticky top-0 z-10">
|
<thead className="sticky top-0 z-10">
|
||||||
<tr className="bg-card">
|
<tr className="bg-card">
|
||||||
<th className="sticky left-0 z-20 bg-card text-left py-1.5 pr-3 font-medium border-b border-border">Statistic</th>
|
<th className="sticky left-0 z-20 bg-card text-left py-1.5 pr-3 font-medium border-b border-border">Statistic</th>
|
||||||
{stats.bands.map((b) => <th key={b} className="py-1.5 px-1 font-mono font-medium border-b border-border text-center w-11">{b}</th>)}
|
{statsBandIdx.map((i) => <th key={stats.bands[i]} className="py-1.5 px-1 font-mono font-medium border-b border-border text-center w-11">{stats.bands[i]}</th>)}
|
||||||
<th className="py-1.5 px-2 font-medium border-b border-border text-center">Total</th>
|
<th className="py-1.5 px-2 font-medium border-b border-border text-center">Total</th>
|
||||||
<th className="py-1.5 px-2 font-medium border-b border-border text-center">Grand</th>
|
<th className="py-1.5 px-2 font-medium border-b border-border text-center">Grand</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{stats.rows.map((row, i) => {
|
{statsRows.map((row, i) => {
|
||||||
const isGroupStart = row.label === 'WORKED' || /^WORKED /.test(row.label);
|
const isGroupStart = row.label === 'WORKED' || /^WORKED /.test(row.label);
|
||||||
return (
|
return (
|
||||||
<tr key={row.label} className={cn(i % 3 === 0 && i > 0 && 'border-t-2 border-border')}>
|
<tr key={row.label} className={cn(i % 3 === 0 && i > 0 && 'border-t-2 border-border')}>
|
||||||
<td className={cn('sticky left-0 bg-card py-1 pr-3 border-b border-border/30 whitespace-nowrap',
|
<td className={cn('sticky left-0 bg-card py-1 pr-3 border-b border-border/30 whitespace-nowrap',
|
||||||
isGroupStart ? 'font-semibold text-foreground' : 'text-muted-foreground')}>{row.label}</td>
|
isGroupStart ? 'font-semibold text-foreground' : 'text-muted-foreground')}>{row.label}</td>
|
||||||
{row.cells.map((c, j) => (
|
{statsBandIdx.map((i) => { const c = row.cells[i] ?? 0; return (
|
||||||
<td key={j} className={cn('text-center py-1 border-b border-l border-border/20 font-mono', c > 0 ? 'bg-emerald-500/15 text-emerald-700' : 'text-muted-foreground/30')}>{c || ''}</td>
|
<td key={i} className={cn('text-center py-1 border-b border-l border-border/20 font-mono', c > 0 ? 'bg-emerald-500/15 text-emerald-700' : 'text-muted-foreground/30')}>{c || ''}</td>
|
||||||
))}
|
); })}
|
||||||
<td className="text-center py-1 px-2 border-b border-l border-border font-mono font-semibold">{row.total || ''}</td>
|
<td className="text-center py-1 px-2 border-b border-l border-border font-mono font-semibold">{row.total || ''}</td>
|
||||||
<td className="text-center py-1 px-2 border-b border-l border-border/20 font-mono text-muted-foreground">{row.grand_total || ''}</td>
|
<td className="text-center py-1 px-2 border-b border-l border-border/20 font-mono text-muted-foreground">{row.grand_total || ''}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user