diff --git a/app.go b/app.go index d4b386b..44f47b5 100644 --- a/app.go +++ b/app.go @@ -7765,16 +7765,31 @@ type ConfirmationItem struct { } // GetSlotStats returns the worked/confirmed slot + DXCC tallies for the QSL -// Manager. Slots are counted by mode CLASS (Phone/CW/Digital); the raw-mode -// granularity (RTTY ≠ FT8) is kept only for the cluster/matrix "new slot" flag. +// Manager. It counts over the SAME data path as the DXCC award, using the +// award's confirmation test (award.Confirmed with the DXCC sources lotw+qsl), +// so the numbers agree with the DXCC award matrix. A slot is a distinct +// DXCC × band (mode-agnostic) — i.e. the award's WORKED/CONFIRMED "Grand" +// totals, NOT split per emission. func (a *App) GetSlotStats() qso.SlotStats { if a.qso == nil { return qso.SlotStats{} } - st, err := a.qso.GetSlotStats(a.ctx) + // Delegate to the exact same matrix the DXCC award draws: the "WORKED"/ + // "CONFIRMED" (ALL-emission) rows give DXCC entities (Total) and slots = + // entity × band (GrandTotal). Guaranteed to equal the award's Grand column. + res, err := a.GetAwardStats("DXCC") if err != nil { return qso.SlotStats{} } + var st qso.SlotStats + for _, r := range res.Rows { + switch r.Label { + case "WORKED": + st.SlotsWorked, st.DXCCWorked = r.GrandTotal, r.Total + case "CONFIRMED": + st.SlotsConfirmed, st.DXCCConfirmed = r.GrandTotal, r.Total + } + } return st } diff --git a/frontend/src/components/QSLManagerModal.tsx b/frontend/src/components/QSLManagerModal.tsx index 5167bf8..6f0472a 100644 --- a/frontend/src/components/QSLManagerModal.tsx +++ b/frontend/src/components/QSLManagerModal.tsx @@ -210,8 +210,8 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi // Worked/confirmed tallies. Slots are counted by mode CLASS (PH/CW/DIGI) — the // raw-mode granularity (RTTY ≠ FT8) stays only in the cluster/matrix new-slot flag. - const [stats, setStats] = useState<{ slots_worked: number; slots_confirmed: number; dxcc_worked: number; dxcc_confirmed: number }>( - { slots_worked: 0, slots_confirmed: 0, dxcc_worked: 0, dxcc_confirmed: 0 }); + const [stats, setStats] = useState( + { slots_worked: 0, slots_confirmed: 0, dxcc_worked: 0, dxcc_confirmed: 0, ph_worked: 0, ph_confirmed: 0, cw_worked: 0, cw_confirmed: 0, dig_worked: 0, dig_confirmed: 0 }); const refreshCounts = useCallback(() => { GetSlotStats().then((c: any) => setStats(c)).catch(() => {}); }, []); useEffect(() => { refreshCounts(); }, [refreshCounts]); @@ -363,7 +363,8 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi )}
- {/* Worked / confirmed tallies. Slots by mode class (PH/CW/DIGI). */} + {/* Worked / confirmed tallies. Slots by mode class (PH/CW/DIGI), with the + per-class breakdown so the totals are checkable. */}
{t('qslm.slots')} diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 3462dc8..d68f23a 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -3664,6 +3664,12 @@ export namespace qso { slots_confirmed: number; dxcc_worked: number; dxcc_confirmed: number; + ph_worked: number; + ph_confirmed: number; + cw_worked: number; + cw_confirmed: number; + dig_worked: number; + dig_confirmed: number; static createFrom(source: any = {}) { return new SlotStats(source); @@ -3675,6 +3681,12 @@ export namespace qso { this.slots_confirmed = source["slots_confirmed"]; this.dxcc_worked = source["dxcc_worked"]; this.dxcc_confirmed = source["dxcc_confirmed"]; + this.ph_worked = source["ph_worked"]; + this.ph_confirmed = source["ph_confirmed"]; + this.cw_worked = source["cw_worked"]; + this.cw_confirmed = source["cw_confirmed"]; + this.dig_worked = source["dig_worked"]; + this.dig_confirmed = source["dig_confirmed"]; } } export class Stats { diff --git a/internal/qso/qso.go b/internal/qso/qso.go index 85190f4..1712b1d 100644 --- a/internal/qso/qso.go +++ b/internal/qso/qso.go @@ -1993,6 +1993,13 @@ type SlotStats struct { SlotsConfirmed int `json:"slots_confirmed"` // distinct DXCC × band × class, confirmed DXCCWorked int `json:"dxcc_worked"` // distinct DXCC entities worked DXCCConfirmed int `json:"dxcc_confirmed"` // distinct DXCC entities confirmed + // Per-class slot breakdown (Phone / CW / Digital) so the numbers are checkable. + PHWorked int `json:"ph_worked"` + PHConfirmed int `json:"ph_confirmed"` + CWWorked int `json:"cw_worked"` + CWConfirmed int `json:"cw_confirmed"` + DIGWorked int `json:"dig_worked"` + DIGConfirmed int `json:"dig_confirmed"` } // GetSlotStats computes the worked/confirmed slot and DXCC tallies in one pass. @@ -2008,6 +2015,9 @@ func (r *Repo) GetSlotStats(ctx context.Context) (SlotStats, error) { defer rows.Close() dxccW, slotW := map[int]bool{}, map[string]bool{} dxccC, slotC := map[int]bool{}, map[string]bool{} + // Per-class distinct slots (worked "w" / confirmed "c"). + clsW := map[string]map[string]bool{"PH": {}, "CW": {}, "DIG": {}} + clsC := map[string]map[string]bool{"PH": {}, "CW": {}, "DIG": {}} for rows.Next() { var dxcc, conf int var band, mode string @@ -2017,19 +2027,29 @@ func (r *Repo) GetSlotStats(ctx context.Context) (SlotStats, error) { if dxcc == 0 { continue } - key := SlotClassKey(dxcc, band, mode) dxccW[dxcc] = true - if band != "" { - slotW[key] = true - } if conf == 1 { dxccC[dxcc] = true - if band != "" { - slotC[key] = true - } + } + if band == "" { + continue // no band → counts for DXCC but not for a slot + } + key := SlotClassKey(dxcc, band, mode) + cl := modeClass(mode) + slotW[key] = true + clsW[cl][key] = true + if conf == 1 { + slotC[key] = true + clsC[cl][key] = true } } - return SlotStats{SlotsWorked: len(slotW), SlotsConfirmed: len(slotC), DXCCWorked: len(dxccW), DXCCConfirmed: len(dxccC)}, rows.Err() + return SlotStats{ + SlotsWorked: len(slotW), SlotsConfirmed: len(slotC), + DXCCWorked: len(dxccW), DXCCConfirmed: len(dxccC), + PHWorked: len(clsW["PH"]), PHConfirmed: len(clsC["PH"]), + CWWorked: len(clsW["CW"]), CWConfirmed: len(clsC["CW"]), + DIGWorked: len(clsW["DIG"]), DIGConfirmed: len(clsC["DIG"]), + }, rows.Err() } // confirmedCols whitelists the received-status columns ConfirmedSlots may