fix(awards): the mode filter only hid rows, it did not filter the award

Selecting CW showed FT8 and SSB contacts. The filter ran client-side over
the reference list and nothing else, so a reference kept because it had
one CW contact still displayed the band cells it earned on SSB, still
counted its SSB confirmations, and opening it listed every contact
regardless of mode.

The class now narrows the log BEFORE the engine runs, so the bands, the
totals and the confirmations all describe the selected mode, and
AwardCellQSOs takes the same class. The panel cache is keyed by
"CODE|MODECLASS" — one key per award showed the previous mode's numbers
after switching.

The snapshot is cached by log revision, so this costs a matching pass and
no database read.
This commit is contained in:
2026-08-01 16:10:22 +02:00
parent b6ea07e3a3
commit 85bf0da006
5 changed files with 62 additions and 20 deletions
+16 -9
View File
@@ -69,6 +69,9 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
const [awardList, setAwardList] = useState<AwardListItem[]>([]);
// Computed results are cached per award code — each award is scanned only the
// first time it's selected (or when explicitly rescanned).
// Keyed by "CODE|MODECLASS": the same award computed for CW and for all modes
// are different results, and caching them under one key showed the previous
// modes numbers after switching.
const [byCode, setByCode] = useState<Record<string, AwardResult>>({});
const [loading, setLoading] = useState(false);
const [err, setErr] = useState('');
@@ -119,13 +122,14 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
// Compute one award (cached). force=true bypasses the cache (Rescan).
async function compute(code: string, force = false) {
if (!code) return;
if (!force && byCode[code]) { setSelected(code); return; }
const key = `${code}|${modeFilter}`;
if (!force && byCode[key]) { setSelected(code); return; }
setSelected(code);
setLoading(true);
setErr('');
try {
const r = (await GetAward(code)) as any as AwardResult;
setByCode((m) => ({ ...m, [code]: r }));
const r = (await GetAward(code, modeFilter === 'all' ? '' : modeFilter)) as any as AwardResult;
setByCode((m) => ({ ...m, [key]: r }));
} catch (e: any) {
setErr(String(e?.message ?? e));
} finally {
@@ -149,7 +153,10 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
}
useEffect(() => { loadList(); }, []);
const current = byCode[selected];
const current = byCode[`${selected}|${modeFilter}`];
// Recompute when the mode class changes: the bands, counts and confirmations
// all describe the selected mode, so they cannot be filtered client-side.
useEffect(() => { if (selected) compute(selected); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [modeFilter]);
// 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
@@ -292,7 +299,7 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
<div className="flex-1 overflow-auto">
{err && <div className="p-3 text-xs text-destructive">{err}</div>}
{awardList.map((a) => {
const r = byCode[a.code];
const r = byCode[`${a.code}|${modeFilter}`];
return (
<button
key={a.code}
@@ -539,7 +546,7 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
</div>
{cell && current && (
<CellQSOModal code={current.code} cell={cell} onClose={() => setCell(null)} />
<CellQSOModal code={current.code} cell={cell} modeClass={modeFilter === 'all' ? '' : modeFilter} onClose={() => setCell(null)} />
)}
{showMissing && current && (
<MissingQSOModal code={current.code} name={current.name} onClose={() => setShowMissing(false)} onEditQSO={onEditQSO} />
@@ -724,17 +731,17 @@ function MissingQSOModal({ code, name, onClose, onEditQSO }: { code: string; nam
}
// CellQSOModal lists the QSOs behind one award-grid cell (reference × band).
function CellQSOModal({ code, cell, onClose }: { code: string; cell: { ref: string; band: string; name?: string }; onClose: () => void }) {
function CellQSOModal({ code, cell, modeClass, onClose }: { code: string; cell: { ref: string; band: string; name?: string }; modeClass: string; onClose: () => void }) {
const { t } = useI18n();
const [qsos, setQsos] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
AwardCellQSOs(code, cell.ref, cell.band)
AwardCellQSOs(code, cell.ref, cell.band, modeClass)
.then((r) => setQsos((r ?? []) as any))
.catch(() => setQsos([]))
.finally(() => setLoading(false));
}, [code, cell.ref, cell.band]);
}, [code, cell.ref, cell.band, modeClass]);
const fmt = (s: any) => { const d = new Date(s); return isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 16).replace('T', ' '); };
+2 -2
View File
@@ -75,7 +75,7 @@ export function AudioStopTX():Promise<void>;
export function AudioTXActive():Promise<boolean>;
export function AwardCellQSOs(arg1:string,arg2:string,arg3:string):Promise<Array<qso.QSO>>;
export function AwardCellQSOs(arg1:string,arg2:string,arg3:string,arg4:string):Promise<Array<qso.QSO>>;
export function AwardFields():Promise<Array<string>>;
@@ -365,7 +365,7 @@ export function GetAudioSettings():Promise<main.AudioSettings>;
export function GetAutostartPrograms():Promise<Array<main.AutostartProgram>>;
export function GetAward(arg1:string):Promise<award.Result>;
export function GetAward(arg1:string,arg2:string):Promise<award.Result>;
export function GetAwardDefs():Promise<Array<award.Def>>;
+4 -4
View File
@@ -98,8 +98,8 @@ export function AudioTXActive() {
return window['go']['main']['App']['AudioTXActive']();
}
export function AwardCellQSOs(arg1, arg2, arg3) {
return window['go']['main']['App']['AwardCellQSOs'](arg1, arg2, arg3);
export function AwardCellQSOs(arg1, arg2, arg3, arg4) {
return window['go']['main']['App']['AwardCellQSOs'](arg1, arg2, arg3, arg4);
}
export function AwardFields() {
@@ -678,8 +678,8 @@ export function GetAutostartPrograms() {
return window['go']['main']['App']['GetAutostartPrograms']();
}
export function GetAward(arg1) {
return window['go']['main']['App']['GetAward'](arg1);
export function GetAward(arg1, arg2) {
return window['go']['main']['App']['GetAward'](arg1, arg2);
}
export function GetAwardDefs() {