fix: bugs

This commit is contained in:
2026-07-17 11:55:40 +02:00
parent cd13921322
commit 1a155e3627
4 changed files with 62 additions and 14 deletions
+18 -3
View File
@@ -7765,16 +7765,31 @@ type ConfirmationItem struct {
} }
// GetSlotStats returns the worked/confirmed slot + DXCC tallies for the QSL // GetSlotStats returns the worked/confirmed slot + DXCC tallies for the QSL
// Manager. Slots are counted by mode CLASS (Phone/CW/Digital); the raw-mode // Manager. It counts over the SAME data path as the DXCC award, using the
// granularity (RTTY ≠ FT8) is kept only for the cluster/matrix "new slot" flag. // 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 { func (a *App) GetSlotStats() qso.SlotStats {
if a.qso == nil { if a.qso == nil {
return qso.SlotStats{} 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 { if err != nil {
return qso.SlotStats{} 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 return st
} }
+4 -3
View File
@@ -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 // 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. // 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 }>( const [stats, setStats] = useState<any>(
{ slots_worked: 0, slots_confirmed: 0, dxcc_worked: 0, dxcc_confirmed: 0 }); { 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(() => {}); }, []); const refreshCounts = useCallback(() => { GetSlotStats().then((c: any) => setStats(c)).catch(() => {}); }, []);
useEffect(() => { refreshCounts(); }, [refreshCounts]); useEffect(() => { refreshCounts(); }, [refreshCounts]);
@@ -363,7 +363,8 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
</> </>
)} )}
<div className="flex-1" /> <div className="flex-1" />
{/* 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. */}
<div className="self-center flex items-center gap-3 text-[11px] font-mono" title={t('qslm.countsTip')}> <div className="self-center flex items-center gap-3 text-[11px] font-mono" title={t('qslm.countsTip')}>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<span className="text-muted-foreground uppercase text-[9px] tracking-wider">{t('qslm.slots')}</span> <span className="text-muted-foreground uppercase text-[9px] tracking-wider">{t('qslm.slots')}</span>
+12
View File
@@ -3664,6 +3664,12 @@ export namespace qso {
slots_confirmed: number; slots_confirmed: number;
dxcc_worked: number; dxcc_worked: number;
dxcc_confirmed: 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 = {}) { static createFrom(source: any = {}) {
return new SlotStats(source); return new SlotStats(source);
@@ -3675,6 +3681,12 @@ export namespace qso {
this.slots_confirmed = source["slots_confirmed"]; this.slots_confirmed = source["slots_confirmed"];
this.dxcc_worked = source["dxcc_worked"]; this.dxcc_worked = source["dxcc_worked"];
this.dxcc_confirmed = source["dxcc_confirmed"]; 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 { export class Stats {
+27 -7
View File
@@ -1993,6 +1993,13 @@ type SlotStats struct {
SlotsConfirmed int `json:"slots_confirmed"` // distinct DXCC × band × class, confirmed SlotsConfirmed int `json:"slots_confirmed"` // distinct DXCC × band × class, confirmed
DXCCWorked int `json:"dxcc_worked"` // distinct DXCC entities worked DXCCWorked int `json:"dxcc_worked"` // distinct DXCC entities worked
DXCCConfirmed int `json:"dxcc_confirmed"` // distinct DXCC entities confirmed 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. // 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() defer rows.Close()
dxccW, slotW := map[int]bool{}, map[string]bool{} dxccW, slotW := map[int]bool{}, map[string]bool{}
dxccC, slotC := 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() { for rows.Next() {
var dxcc, conf int var dxcc, conf int
var band, mode string var band, mode string
@@ -2017,19 +2027,29 @@ func (r *Repo) GetSlotStats(ctx context.Context) (SlotStats, error) {
if dxcc == 0 { if dxcc == 0 {
continue continue
} }
key := SlotClassKey(dxcc, band, mode)
dxccW[dxcc] = true dxccW[dxcc] = true
if band != "" {
slotW[key] = true
}
if conf == 1 { if conf == 1 {
dxccC[dxcc] = true dxccC[dxcc] = true
if band != "" { }
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 slotC[key] = true
clsC[cl][key] = true
} }
} }
} return SlotStats{
return SlotStats{SlotsWorked: len(slotW), SlotsConfirmed: len(slotC), DXCCWorked: len(dxccW), DXCCConfirmed: len(dxccC)}, rows.Err() 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 // confirmedCols whitelists the received-status columns ConfirmedSlots may