rigs completed
This commit is contained in:
+63
-17
@@ -257,6 +257,7 @@ func decodeExtras(s string) map[string]string {
|
||||
|
||||
// Add inserts a QSO and returns its ID.
|
||||
func (r *Repo) Add(ctx context.Context, q QSO) (int64, error) {
|
||||
q.Callsign = strings.ToUpper(strings.TrimSpace(q.Callsign))
|
||||
if q.Callsign == "" {
|
||||
return 0, fmt.Errorf("empty callsign")
|
||||
}
|
||||
@@ -293,6 +294,7 @@ func (r *Repo) AddBatch(ctx context.Context, qsos []QSO) (int64, error) {
|
||||
|
||||
var inserted int64
|
||||
for _, q := range qsos {
|
||||
q.Callsign = strings.ToUpper(strings.TrimSpace(q.Callsign))
|
||||
if q.Callsign == "" {
|
||||
continue
|
||||
}
|
||||
@@ -322,6 +324,7 @@ func (r *Repo) Update(ctx context.Context, q QSO) error {
|
||||
if q.ID == 0 {
|
||||
return fmt.Errorf("missing id")
|
||||
}
|
||||
q.Callsign = strings.ToUpper(strings.TrimSpace(q.Callsign))
|
||||
if q.Callsign == "" {
|
||||
return fmt.Errorf("empty callsign")
|
||||
}
|
||||
@@ -412,8 +415,9 @@ func (r *Repo) List(ctx context.Context, f ListFilter) ([]QSO, error) {
|
||||
q := `SELECT ` + selectCols + ` FROM qso WHERE 1=1`
|
||||
args := []any{}
|
||||
if f.Callsign != "" {
|
||||
// Contains-match so a search for "XYZ" finds F4XYZ, F4XYZ/P, etc.
|
||||
q += " AND callsign LIKE ?"
|
||||
args = append(args, f.Callsign+"%")
|
||||
args = append(args, "%"+f.Callsign+"%")
|
||||
}
|
||||
if f.Band != "" {
|
||||
q += " AND band = ?"
|
||||
@@ -428,8 +432,12 @@ func (r *Repo) List(ctx context.Context, f ListFilter) ([]QSO, error) {
|
||||
args = append(args, f.StationCallsign)
|
||||
}
|
||||
q += " ORDER BY qso_date DESC, id DESC"
|
||||
if f.Limit <= 0 || f.Limit > 1000 {
|
||||
f.Limit = 200
|
||||
if f.Limit <= 0 {
|
||||
f.Limit = 500
|
||||
}
|
||||
// Hard upper bound: 1M is enough to fit any realistic personal log.
|
||||
if f.Limit > 1_000_000 {
|
||||
f.Limit = 1_000_000
|
||||
}
|
||||
q += " LIMIT ? OFFSET ?"
|
||||
args = append(args, f.Limit, f.Offset)
|
||||
@@ -549,14 +557,14 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
|
||||
|
||||
// ---- Per-callsign stats ----
|
||||
if err := r.db.QueryRowContext(ctx,
|
||||
`SELECT COUNT(*) FROM qso WHERE callsign = ?`, wb.Callsign).Scan(&wb.Count); err != nil {
|
||||
`SELECT COUNT(*) FROM qso WHERE upper(trim(callsign)) = ?`, wb.Callsign).Scan(&wb.Count); err != nil {
|
||||
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
|
||||
FROM qso WHERE callsign = ?
|
||||
FROM qso WHERE upper(trim(callsign)) = ?
|
||||
ORDER BY qso_date DESC, id DESC
|
||||
LIMIT ?`, wb.Callsign, maxWorkedEntries)
|
||||
if err != nil {
|
||||
@@ -569,14 +577,17 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
|
||||
var (
|
||||
e WorkedEntry
|
||||
dateStr string
|
||||
band, mode sql.NullString
|
||||
rstS, rstR, qslS, qslR, lotwS, lotwR sql.NullString
|
||||
)
|
||||
if err := rows.Scan(&e.ID, &dateStr, &e.Band, &e.Mode,
|
||||
if err := rows.Scan(&e.ID, &dateStr, &band, &mode,
|
||||
&rstS, &rstR, &qslS, &qslR, &lotwS, &lotwR); 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
|
||||
@@ -584,9 +595,15 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
|
||||
e.LOTWSent = lotwS.String
|
||||
e.LOTWRcvd = lotwR.String
|
||||
wb.Entries = append(wb.Entries, e)
|
||||
bandsSet[e.Band] = struct{}{}
|
||||
modesSet[e.Mode] = struct{}{}
|
||||
bmSet[e.Band+"|"+e.Mode] = BandMode{Band: e.Band, Mode: e.Mode}
|
||||
if e.Band != "" {
|
||||
bandsSet[e.Band] = struct{}{}
|
||||
}
|
||||
if e.Mode != "" {
|
||||
modesSet[e.Mode] = struct{}{}
|
||||
}
|
||||
if e.Band != "" && e.Mode != "" {
|
||||
bmSet[e.Band+"|"+e.Mode] = BandMode{Band: e.Band, Mode: e.Mode}
|
||||
}
|
||||
if wb.Last.IsZero() {
|
||||
wb.Last = e.QSODate
|
||||
}
|
||||
@@ -597,7 +614,7 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
|
||||
if wb.Count > maxWorkedEntries {
|
||||
var firstStr sql.NullString
|
||||
_ = r.db.QueryRowContext(ctx,
|
||||
`SELECT MIN(qso_date) FROM qso WHERE callsign = ?`, wb.Callsign).Scan(&firstStr)
|
||||
`SELECT MIN(qso_date) FROM qso WHERE upper(trim(callsign)) = ?`, wb.Callsign).Scan(&firstStr)
|
||||
if firstStr.Valid {
|
||||
wb.First = parseTimeLoose(firstStr.String)
|
||||
}
|
||||
@@ -622,7 +639,7 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
|
||||
var d sql.NullInt64
|
||||
_ = r.db.QueryRowContext(ctx, `
|
||||
SELECT dxcc FROM qso
|
||||
WHERE callsign = ? AND dxcc IS NOT NULL
|
||||
WHERE upper(trim(callsign)) = ? AND dxcc IS NOT NULL
|
||||
ORDER BY qso_date DESC LIMIT 1`, wb.Callsign).Scan(&d)
|
||||
if d.Valid {
|
||||
dxcc = int(d.Int64)
|
||||
@@ -656,15 +673,17 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
|
||||
}
|
||||
|
||||
if err := r.collectDistinct(ctx, &wb.DXCCBands,
|
||||
`SELECT DISTINCT band FROM qso WHERE dxcc = ?`, dxcc); err != nil {
|
||||
`SELECT DISTINCT band FROM qso WHERE dxcc = ? AND band IS NOT NULL AND band != ''`, dxcc); err != nil {
|
||||
return wb, err
|
||||
}
|
||||
if err := r.collectDistinct(ctx, &wb.DXCCModes,
|
||||
`SELECT DISTINCT mode FROM qso WHERE dxcc = ?`, dxcc); err != nil {
|
||||
`SELECT DISTINCT mode FROM qso WHERE dxcc = ? AND mode IS NOT NULL AND mode != ''`, dxcc); err != nil {
|
||||
return wb, err
|
||||
}
|
||||
bmRows, err := r.db.QueryContext(ctx,
|
||||
`SELECT DISTINCT band, mode FROM qso WHERE dxcc = ?`, dxcc)
|
||||
`SELECT DISTINCT band, mode FROM qso WHERE dxcc = ?
|
||||
AND band IS NOT NULL AND band != ''
|
||||
AND mode IS NOT NULL AND mode != ''`, dxcc)
|
||||
if err != nil {
|
||||
return wb, fmt.Errorf("dxcc band_modes: %w", err)
|
||||
}
|
||||
@@ -684,15 +703,21 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
|
||||
// One pass over every distinct (band, mode) in the DXCC, aggregating
|
||||
// "did this call work it?" and "was anything confirmed?" via MAX.
|
||||
// Status precedence: call_c > call_w > dxcc_c > dxcc_w.
|
||||
// Filter NULL/empty band+mode rows — they'd create a NULL group key
|
||||
// that Scan into *string can't handle and would error out the whole
|
||||
// WorkedBefore call, blanking the matrix in the UI.
|
||||
statusRows, err := r.db.QueryContext(ctx, `
|
||||
SELECT band, mode,
|
||||
MAX(CASE WHEN callsign = ? THEN 1 ELSE 0 END),
|
||||
MAX(CASE WHEN callsign = ?
|
||||
MAX(CASE WHEN upper(trim(callsign)) = ? THEN 1 ELSE 0 END),
|
||||
MAX(CASE WHEN upper(trim(callsign)) = ?
|
||||
AND (lotw_rcvd = 'Y' OR qsl_rcvd = 'Y' OR eqsl_rcvd = 'Y')
|
||||
THEN 1 ELSE 0 END),
|
||||
MAX(CASE WHEN lotw_rcvd = 'Y' OR qsl_rcvd = 'Y' OR eqsl_rcvd = 'Y'
|
||||
THEN 1 ELSE 0 END)
|
||||
FROM qso WHERE dxcc = ?
|
||||
FROM qso
|
||||
WHERE dxcc = ?
|
||||
AND band IS NOT NULL AND band != ''
|
||||
AND mode IS NOT NULL AND mode != ''
|
||||
GROUP BY band, mode`, wb.Callsign, wb.Callsign, dxcc)
|
||||
if err != nil {
|
||||
return wb, fmt.Errorf("band status: %w", err)
|
||||
@@ -886,6 +911,27 @@ func (r *Repo) EntitySlotMap(ctx context.Context, resolveEntity func(callsign st
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// WorkedCallsigns returns the set of every callsign ever logged (uppercased).
|
||||
// One pass, used by the cluster spot colouring to flag "already worked this
|
||||
// exact call" regardless of band/mode — Log4OM/RUMlogNG-style call highlight.
|
||||
func (r *Repo) WorkedCallsigns(ctx context.Context) (map[string]struct{}, error) {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT DISTINCT upper(callsign) FROM qso WHERE callsign != ''`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make(map[string]struct{}, 1024)
|
||||
for rows.Next() {
|
||||
var c string
|
||||
if err := rows.Scan(&c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[c] = struct{}{}
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Count returns the total number of QSOs in the database.
|
||||
func (r *Repo) Count(ctx context.Context) (int64, error) {
|
||||
var n int64
|
||||
|
||||
Reference in New Issue
Block a user