feat(entry/stats/awards): Alt+W clear, slot drill-down, DXCC prefix column
Five operator-requested items: - Alt+W clears the QSO entry. Handled before the `typing` guard and above the keyer's key routing, so there is always one key that clears whatever else is running — Esc is not that key when the CW keyer reserves it. - The Grid box no longer pops outside the entry panel on a narrow window. Row 2 needed 300+130+76+gaps = 538 px inside a panel whose min-width is 520, so Grid was pushed out and clipped at every narrow width, not just extreme ones. QTH's min-width drops to 80 and the row wraps rather than overflowing if it ever still can't fit. - Selecting a QSO in the log now drives the Stats (F1) matrix. Uses its own WorkedBefore call into separate state, NOT runWorkedBefore: that one owns the entry form's wbRef and can trigger a field backfill, which browsing the log must never do. The entry form wins whenever it holds a call. - Clicking a coloured band/mode square lists the contacts behind it. Returns the exact callsign AND the rest of the entity, because that pair is what the cell's colour encodes; the call's own QSOs are bolded. The DXCC arm matches the stored dxcc column only — reconstructing it from the callsign here would disagree with the matrix above, which is built from that column. - The Awards DXCC list shows each entity's primary prefix in its own sortable column. Derived live from cty.dat into Ref.Group rather than stored on the reference row, so an existing installation needs no re-seed.
This commit is contained in:
@@ -173,6 +173,37 @@ func (m *Manager) EntityNames() []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// PrefixByDXCC maps ADIF DXCC entity number → canonical primary prefix (F, DL,
|
||||
// XE…) from the loaded cty.dat. cty.dat is keyed by entity NAME, so the join
|
||||
// goes through EntityDXCC; entries whose name doesn't resolve are skipped
|
||||
// rather than guessed. Empty until cty.dat has loaded.
|
||||
func (m *Manager) PrefixByDXCC() map[int]string {
|
||||
m.mu.RLock()
|
||||
db := m.db
|
||||
m.mu.RUnlock()
|
||||
if db == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[int]string, 400)
|
||||
for _, e := range db.Entities() {
|
||||
p := strings.TrimSpace(e.Primary)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
// cty.dat marks non-DXCC sub-entities with a leading '*' — they report
|
||||
// under a parent entity and must not overwrite the parent's prefix.
|
||||
if strings.HasPrefix(p, "*") {
|
||||
continue
|
||||
}
|
||||
if n := EntityDXCC(e.Name); n > 0 {
|
||||
if _, dup := out[n]; !dup {
|
||||
out[n] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Info returns metadata about the currently-loaded cty.dat (or zero value
|
||||
// if nothing loaded).
|
||||
func (m *Manager) Info() ctySource {
|
||||
|
||||
@@ -1461,6 +1461,51 @@ func (r *Repo) IterateByIDs(ctx context.Context, ids []int64, fn func(QSO) error
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
// BandSlotQSOs returns every contact on one band that belongs to a slot of the
|
||||
// entry matrix: the exact callsign, or any callsign in the same DXCC entity.
|
||||
// Mode is NOT filtered here — the class (phone / CW / digital) is a derived
|
||||
// notion the caller owns, so it filters the (small) band-scoped result itself.
|
||||
//
|
||||
// The DXCC arm matches the stored dxcc column only. A QSO logged before the
|
||||
// entity was resolved has a NULL there and is genuinely unattributed; guessing
|
||||
// it back from the callsign here would disagree with the matrix above, which is
|
||||
// built from the same column.
|
||||
func (r *Repo) BandSlotQSOs(ctx context.Context, callsign string, dxccNum int, band string) ([]QSO, error) {
|
||||
call := upperTrim(callsign)
|
||||
b := strings.TrimSpace(band)
|
||||
if b == "" || (call == "" && dxccNum <= 0) {
|
||||
return []QSO{}, nil
|
||||
}
|
||||
where := "band = ?"
|
||||
args := []any{b}
|
||||
switch {
|
||||
case call != "" && dxccNum > 0:
|
||||
where += " AND (callsign = ? OR dxcc = ?)"
|
||||
args = append(args, call, dxccNum)
|
||||
case call != "":
|
||||
where += " AND callsign = ?"
|
||||
args = append(args, call)
|
||||
default:
|
||||
where += " AND dxcc = ?"
|
||||
args = append(args, dxccNum)
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT `+selectCols+` FROM qso WHERE `+where+` ORDER BY qso_date DESC, id DESC LIMIT 500`, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query band slot: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []QSO{}
|
||||
for rows.Next() {
|
||||
q, err := scanQSO(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, q)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// WorkedBefore summarises prior contacts at two granularities:
|
||||
// - by exact callsign → shown as "this call worked N×"
|
||||
// - by DXCC entity → drives NEW ONE / NEW BAND / NEW MODE / NEW SLOT
|
||||
|
||||
Reference in New Issue
Block a user