feat: Winkeyer
This commit is contained in:
+45
-44
@@ -6,10 +6,39 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MergeNonZero copies every non-zero field of src onto dst. Zero-value src
|
||||
// fields (empty string, nil pointer, zero time, zero number) are skipped so
|
||||
// existing data is preserved. Maps (Extras) are merged key-by-key rather than
|
||||
// replaced. Because an imported QSO has ID==0 and CreatedAt zero, dst's
|
||||
// identity is naturally preserved. Used by the importer's "update duplicates"
|
||||
// mode so re-importing an ADIF refreshes QSL/confirmation statuses without
|
||||
// clobbering fields the source file doesn't carry.
|
||||
func MergeNonZero(dst *QSO, src QSO) {
|
||||
dv := reflect.ValueOf(dst).Elem()
|
||||
sv := reflect.ValueOf(src)
|
||||
for i := 0; i < dv.NumField(); i++ {
|
||||
df, sf := dv.Field(i), sv.Field(i)
|
||||
if !df.CanSet() || sf.IsZero() {
|
||||
continue
|
||||
}
|
||||
if sf.Kind() == reflect.Map {
|
||||
if df.IsNil() {
|
||||
df.Set(reflect.MakeMap(sf.Type()))
|
||||
}
|
||||
for _, k := range sf.MapKeys() {
|
||||
df.SetMapIndex(k, sf.MapIndex(k))
|
||||
}
|
||||
continue
|
||||
}
|
||||
df.Set(sf)
|
||||
}
|
||||
}
|
||||
|
||||
// QSO represents a contact. Fields are aligned on ADIF naming for
|
||||
// import/export. Pointers are used to distinguish "absent" from "zero".
|
||||
// Anything in ADIF that is not a promoted column lands in Extras.
|
||||
@@ -569,7 +598,7 @@ type WorkedBefore struct {
|
||||
Bands []string `json:"bands"` // distinct bands for this call
|
||||
Modes []string `json:"modes"` // distinct modes for this call
|
||||
BandModes []BandMode `json:"band_modes"` // distinct (band, mode) pairs
|
||||
Entries []WorkedEntry `json:"entries"` // up to maxWorkedEntries most recent
|
||||
Entries []QSO `json:"entries"` // up to maxWorkedEntries most recent (full records)
|
||||
|
||||
// --- Per-DXCC entity (populated when DXCC is known) ---
|
||||
DXCC int `json:"dxcc,omitempty"`
|
||||
@@ -615,19 +644,6 @@ type BandMode struct {
|
||||
|
||||
// WorkedEntry is one prior contact row, lean enough to ship to the UI for
|
||||
// rendering a recent-contacts mini-list.
|
||||
type WorkedEntry struct {
|
||||
ID int64 `json:"id"`
|
||||
QSODate time.Time `json:"qso_date"`
|
||||
Band string `json:"band"`
|
||||
Mode string `json:"mode"`
|
||||
RSTSent string `json:"rst_sent,omitempty"`
|
||||
RSTRcvd string `json:"rst_rcvd,omitempty"`
|
||||
QSLSent string `json:"qsl_sent,omitempty"`
|
||||
QSLRcvd string `json:"qsl_rcvd,omitempty"`
|
||||
LOTWSent string `json:"lotw_sent,omitempty"`
|
||||
LOTWRcvd string `json:"lotw_rcvd,omitempty"`
|
||||
}
|
||||
|
||||
const maxWorkedEntries = 50
|
||||
|
||||
// WorkedBefore returns aggregated history at both callsign and DXCC level.
|
||||
@@ -640,7 +656,7 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
|
||||
Bands: []string{},
|
||||
Modes: []string{},
|
||||
BandModes: []BandMode{},
|
||||
Entries: []WorkedEntry{},
|
||||
Entries: []QSO{},
|
||||
DXCCBands: []string{},
|
||||
DXCCModes: []string{},
|
||||
DXCCBandModes: []BandMode{},
|
||||
@@ -655,9 +671,9 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
|
||||
return wb, fmt.Errorf("count worked: %w", err)
|
||||
}
|
||||
if wb.Count > 0 {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT id, qso_date, band, mode, rst_sent, rst_rcvd,
|
||||
qsl_sent, qsl_rcvd, lotw_sent, lotw_rcvd
|
||||
// Pull the full QSO records (same columns as the Recent QSOs list) so
|
||||
// the Worked-before grid can offer the same rich column picker.
|
||||
rows, err := r.db.QueryContext(ctx, `SELECT `+selectCols+`
|
||||
FROM qso WHERE upper(trim(callsign)) = ?
|
||||
ORDER BY qso_date DESC, id DESC
|
||||
LIMIT ?`, wb.Callsign, maxWorkedEntries)
|
||||
@@ -668,40 +684,25 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
|
||||
modesSet := map[string]struct{}{}
|
||||
bmSet := map[string]BandMode{}
|
||||
for rows.Next() {
|
||||
var (
|
||||
e WorkedEntry
|
||||
dateStr string
|
||||
band, mode sql.NullString
|
||||
rstS, rstR, qslS, qslR, lotwS, lotwR sql.NullString
|
||||
)
|
||||
if err := rows.Scan(&e.ID, &dateStr, &band, &mode,
|
||||
&rstS, &rstR, &qslS, &qslR, &lotwS, &lotwR); err != nil {
|
||||
q, err := scanQSO(rows)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return wb, fmt.Errorf("scan worked: %w", err)
|
||||
}
|
||||
e.QSODate = parseTimeLoose(dateStr)
|
||||
e.Band = band.String
|
||||
e.Mode = mode.String
|
||||
e.RSTSent = rstS.String
|
||||
e.RSTRcvd = rstR.String
|
||||
e.QSLSent = qslS.String
|
||||
e.QSLRcvd = qslR.String
|
||||
e.LOTWSent = lotwS.String
|
||||
e.LOTWRcvd = lotwR.String
|
||||
wb.Entries = append(wb.Entries, e)
|
||||
if e.Band != "" {
|
||||
bandsSet[e.Band] = struct{}{}
|
||||
wb.Entries = append(wb.Entries, q)
|
||||
if q.Band != "" {
|
||||
bandsSet[q.Band] = struct{}{}
|
||||
}
|
||||
if e.Mode != "" {
|
||||
modesSet[e.Mode] = struct{}{}
|
||||
if q.Mode != "" {
|
||||
modesSet[q.Mode] = struct{}{}
|
||||
}
|
||||
if e.Band != "" && e.Mode != "" {
|
||||
bmSet[e.Band+"|"+e.Mode] = BandMode{Band: e.Band, Mode: e.Mode}
|
||||
if q.Band != "" && q.Mode != "" {
|
||||
bmSet[q.Band+"|"+q.Mode] = BandMode{Band: q.Band, Mode: q.Mode}
|
||||
}
|
||||
if wb.Last.IsZero() {
|
||||
wb.Last = e.QSODate
|
||||
wb.Last = q.QSODate
|
||||
}
|
||||
wb.First = e.QSODate
|
||||
wb.First = q.QSODate
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user