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:
@@ -3514,10 +3514,17 @@ func (a *App) GetAwards() ([]award.Result, error) {
|
|||||||
// GetAward computes progress for a single award by code (one whole-log scan,
|
// 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
|
// 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.
|
// 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() {
|
for _, d := range a.awardDefs() {
|
||||||
if strings.EqualFold(d.Code, code) {
|
if strings.EqualFold(d.Code, code) {
|
||||||
results, err := a.computeAwards([]award.Def{d})
|
results, err := a.computeAwardsForMode([]award.Def{d}, modeClass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return award.Result{}, err
|
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
|
// computeAwards runs the engine for the given award definitions over the whole
|
||||||
// log and enriches dynamic awards (totals + worked-reference names).
|
// log and enriches dynamic awards (totals + worked-reference names).
|
||||||
func (a *App) computeAwards(defs []award.Def) ([]award.Result, error) {
|
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 {
|
if a.qso == nil {
|
||||||
return nil, fmt.Errorf("db not initialized")
|
return nil, fmt.Errorf("db not initialized")
|
||||||
}
|
}
|
||||||
@@ -3540,6 +3554,15 @@ func (a *App) computeAwards(defs []award.Def) ([]award.Result, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
nameOf := func(field, ref string) string {
|
||||||
switch field {
|
switch field {
|
||||||
case "dxcc":
|
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,
|
// AwardCellQSOs returns the QSOs that contribute to one award reference,
|
||||||
// optionally on a single band (band="" = all bands). Powers the award-grid
|
// optionally on a single band (band="" = all bands). Powers the award-grid
|
||||||
// cell drill-down ("show me every Canada contact on 20m").
|
// 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 {
|
if a.qso == nil {
|
||||||
return nil, fmt.Errorf("db not initialized")
|
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)]
|
metas := a.awardRefMetas([]award.Def{*def})[strings.ToUpper(def.Code)]
|
||||||
wantRef := strings.ToUpper(strings.TrimSpace(ref))
|
wantRef := strings.ToUpper(strings.TrimSpace(ref))
|
||||||
wantBand := strings.ToLower(strings.TrimSpace(band))
|
wantBand := strings.ToLower(strings.TrimSpace(band))
|
||||||
|
wantMode := strings.ToUpper(strings.TrimSpace(modeClass))
|
||||||
|
|
||||||
var out []qso.QSO
|
var out []qso.QSO
|
||||||
err := a.qso.IterateAll(a.ctx, func(q qso.QSO) error {
|
err := a.qso.IterateAll(a.ctx, func(q qso.QSO) error {
|
||||||
if wantBand != "" && strings.ToLower(strings.TrimSpace(q.Band)) != wantBand {
|
if wantBand != "" && strings.ToLower(strings.TrimSpace(q.Band)) != wantBand {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if wantMode != "" && wantMode != "ALL" && award.ModeClass(q.Mode) != wantMode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
a.enrichQSOForAwards(&q)
|
a.enrichQSOForAwards(&q)
|
||||||
for _, c := range award.MatchQSO(*def, metas, &q) {
|
for _, c := range award.MatchQSO(*def, metas, &q) {
|
||||||
if strings.ToUpper(c) == wantRef {
|
if strings.ToUpper(c) == wantRef {
|
||||||
|
|||||||
+6
-2
@@ -2,8 +2,12 @@
|
|||||||
{
|
{
|
||||||
"version": "0.22.9",
|
"version": "0.22.9",
|
||||||
"date": "",
|
"date": "",
|
||||||
"en": [],
|
"en": [
|
||||||
"fr": []
|
"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",
|
"version": "0.22.8",
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
|
|||||||
const [awardList, setAwardList] = useState<AwardListItem[]>([]);
|
const [awardList, setAwardList] = useState<AwardListItem[]>([]);
|
||||||
// Computed results are cached per award code — each award is scanned only the
|
// Computed results are cached per award code — each award is scanned only the
|
||||||
// first time it's selected (or when explicitly rescanned).
|
// 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<Record<string, AwardResult>>({});
|
const [byCode, setByCode] = useState<Record<string, AwardResult>>({});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [err, setErr] = useState('');
|
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).
|
// Compute one award (cached). force=true bypasses the cache (Rescan).
|
||||||
async function compute(code: string, force = false) {
|
async function compute(code: string, force = false) {
|
||||||
if (!code) return;
|
if (!code) return;
|
||||||
if (!force && byCode[code]) { setSelected(code); return; }
|
const key = `${code}|${modeFilter}`;
|
||||||
|
if (!force && byCode[key]) { setSelected(code); return; }
|
||||||
setSelected(code);
|
setSelected(code);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setErr('');
|
setErr('');
|
||||||
try {
|
try {
|
||||||
const r = (await GetAward(code)) as any as AwardResult;
|
const r = (await GetAward(code, modeFilter === 'all' ? '' : modeFilter)) as any as AwardResult;
|
||||||
setByCode((m) => ({ ...m, [code]: r }));
|
setByCode((m) => ({ ...m, [key]: r }));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setErr(String(e?.message ?? e));
|
setErr(String(e?.message ?? e));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -149,7 +153,10 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
|
|||||||
}
|
}
|
||||||
useEffect(() => { loadList(); }, []);
|
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
|
// 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
|
// 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">
|
<div className="flex-1 overflow-auto">
|
||||||
{err && <div className="p-3 text-xs text-destructive">{err}</div>}
|
{err && <div className="p-3 text-xs text-destructive">{err}</div>}
|
||||||
{awardList.map((a) => {
|
{awardList.map((a) => {
|
||||||
const r = byCode[a.code];
|
const r = byCode[`${a.code}|${modeFilter}`];
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={a.code}
|
key={a.code}
|
||||||
@@ -539,7 +546,7 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{cell && current && (
|
{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 && (
|
{showMissing && current && (
|
||||||
<MissingQSOModal code={current.code} name={current.name} onClose={() => setShowMissing(false)} onEditQSO={onEditQSO} />
|
<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).
|
// 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 { t } = useI18n();
|
||||||
const [qsos, setQsos] = useState<any[]>([]);
|
const [qsos, setQsos] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
AwardCellQSOs(code, cell.ref, cell.band)
|
AwardCellQSOs(code, cell.ref, cell.band, modeClass)
|
||||||
.then((r) => setQsos((r ?? []) as any))
|
.then((r) => setQsos((r ?? []) as any))
|
||||||
.catch(() => setQsos([]))
|
.catch(() => setQsos([]))
|
||||||
.finally(() => setLoading(false));
|
.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', ' '); };
|
const fmt = (s: any) => { const d = new Date(s); return isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 16).replace('T', ' '); };
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
-2
@@ -75,7 +75,7 @@ export function AudioStopTX():Promise<void>;
|
|||||||
|
|
||||||
export function AudioTXActive():Promise<boolean>;
|
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>>;
|
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 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>>;
|
export function GetAwardDefs():Promise<Array<award.Def>>;
|
||||||
|
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ export function AudioTXActive() {
|
|||||||
return window['go']['main']['App']['AudioTXActive']();
|
return window['go']['main']['App']['AudioTXActive']();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AwardCellQSOs(arg1, arg2, arg3) {
|
export function AwardCellQSOs(arg1, arg2, arg3, arg4) {
|
||||||
return window['go']['main']['App']['AwardCellQSOs'](arg1, arg2, arg3);
|
return window['go']['main']['App']['AwardCellQSOs'](arg1, arg2, arg3, arg4);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AwardFields() {
|
export function AwardFields() {
|
||||||
@@ -678,8 +678,8 @@ export function GetAutostartPrograms() {
|
|||||||
return window['go']['main']['App']['GetAutostartPrograms']();
|
return window['go']['main']['App']['GetAutostartPrograms']();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetAward(arg1) {
|
export function GetAward(arg1, arg2) {
|
||||||
return window['go']['main']['App']['GetAward'](arg1);
|
return window['go']['main']['App']['GetAward'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetAwardDefs() {
|
export function GetAwardDefs() {
|
||||||
|
|||||||
Reference in New Issue
Block a user