diff --git a/app.go b/app.go index 509a791..38620b3 100644 --- a/app.go +++ b/app.go @@ -3514,10 +3514,17 @@ func (a *App) GetAwards() ([]award.Result, error) { // GetAward computes progress for a single award by code (one whole-log scan, // matching only that award). This is what the awards UI calls when an award is // selected, so opening the panel doesn't scan every award up front. -func (a *App) GetAward(code string) (award.Result, error) { +// GetAward computes one award. modeClass ("CW", "PHONE", "DIGI", or "" for all) +// narrows it to contacts of that class BEFORE the engine runs, so the bands, the +// counts and the confirmations all describe that mode. +// +// Filtering only the reference list was not enough: a reference kept because it +// has one CW contact still showed the band cells it earned on SSB, which is +// precisely what an operator chasing a CW award must not be told. +func (a *App) GetAward(code, modeClass string) (award.Result, error) { for _, d := range a.awardDefs() { if strings.EqualFold(d.Code, code) { - results, err := a.computeAwards([]award.Def{d}) + results, err := a.computeAwardsForMode([]award.Def{d}, modeClass) if err != nil { return award.Result{}, err } @@ -3533,6 +3540,13 @@ func (a *App) GetAward(code string) (award.Result, error) { // computeAwards runs the engine for the given award definitions over the whole // log and enriches dynamic awards (totals + worked-reference names). func (a *App) computeAwards(defs []award.Def) ([]award.Result, error) { + return a.computeAwardsForMode(defs, "") +} + +// computeAwardsForMode is computeAwards restricted to one mode class. The +// snapshot is shared and cached by log revision, so this costs a matching pass, +// not a database read. +func (a *App) computeAwardsForMode(defs []award.Def, modeClass string) ([]award.Result, error) { if a.qso == nil { return nil, fmt.Errorf("db not initialized") } @@ -3540,6 +3554,15 @@ func (a *App) computeAwards(defs []award.Def) ([]award.Result, error) { if err != nil { return nil, err } + if want := strings.ToUpper(strings.TrimSpace(modeClass)); want != "" && want != "ALL" { + kept := make([]qso.QSO, 0, len(all)) + for _, q := range all { + if award.ModeClass(q.Mode) == want { + kept = append(kept, q) + } + } + all = kept + } nameOf := func(field, ref string) string { switch field { case "dxcc": @@ -3590,7 +3613,11 @@ func (a *App) computeAwards(defs []award.Def) ([]award.Result, error) { // AwardCellQSOs returns the QSOs that contribute to one award reference, // optionally on a single band (band="" = all bands). Powers the award-grid // cell drill-down ("show me every Canada contact on 20m"). -func (a *App) AwardCellQSOs(code, ref, band string) ([]qso.QSO, error) { +// AwardCellQSOs lists the contacts behind one cell. modeClass ("CW", "PHONE", +// "DIGI", or "" for all) applies the SAME filter as the reference list above it +// — without it, filtering the list to CW and then opening a reference showed the +// FT8 and SSB contacts that filter exists to hide. +func (a *App) AwardCellQSOs(code, ref, band, modeClass string) ([]qso.QSO, error) { if a.qso == nil { return nil, fmt.Errorf("db not initialized") } @@ -3608,12 +3635,16 @@ func (a *App) AwardCellQSOs(code, ref, band string) ([]qso.QSO, error) { metas := a.awardRefMetas([]award.Def{*def})[strings.ToUpper(def.Code)] wantRef := strings.ToUpper(strings.TrimSpace(ref)) wantBand := strings.ToLower(strings.TrimSpace(band)) + wantMode := strings.ToUpper(strings.TrimSpace(modeClass)) var out []qso.QSO err := a.qso.IterateAll(a.ctx, func(q qso.QSO) error { if wantBand != "" && strings.ToLower(strings.TrimSpace(q.Band)) != wantBand { return nil } + if wantMode != "" && wantMode != "ALL" && award.ModeClass(q.Mode) != wantMode { + return nil + } a.enrichQSOForAwards(&q) for _, c := range award.MatchQSO(*def, metas, &q) { if strings.ToUpper(c) == wantRef { diff --git a/changelog.json b/changelog.json index 7b6d86d..a14334b 100644 --- a/changelog.json +++ b/changelog.json @@ -2,8 +2,12 @@ { "version": "0.22.9", "date": "", - "en": [], - "fr": [] + "en": [ + "Awards: the CW / Phone / Digital filter now applies to the whole award — bands, counts, confirmations and the contacts behind a cell — instead of only hiding rows from the reference list." + ], + "fr": [ + "Diplômes : le filtre CW / Phonie / Numérique s’applique désormais à tout le diplôme — bandes, comptes, confirmations et QSO derrière une case — au lieu de masquer seulement des lignes de la liste des références." + ] }, { "version": "0.22.8", diff --git a/frontend/src/components/AwardsPanel.tsx b/frontend/src/components/AwardsPanel.tsx index db6599d..e04b72d 100644 --- a/frontend/src/components/AwardsPanel.tsx +++ b/frontend/src/components/AwardsPanel.tsx @@ -69,6 +69,9 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n const [awardList, setAwardList] = useState([]); // 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 + // mode’s numbers after switching. const [byCode, setByCode] = useState>({}); 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
{err &&
{err}
} {awardList.map((a) => { - const r = byCode[a.code]; + const r = byCode[`${a.code}|${modeFilter}`]; return (