feat: per-operator filter in the statistics dashboard
New Operator picker next to the contest picker, populated with the operators actually present in the log. Selecting one recomputes every statistic for that operator — QSO/DXCC totals, top countries, continent split, by band/mode, and the rate/best-60. "— Station owner —" buckets QSOs logged with no OPERATOR set. Shown only when the log has more than one operator. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
+53
-7
@@ -210,9 +210,49 @@ func yes(s string) bool {
|
||||
// it is set and no explicit window is given, the window becomes the contest's own
|
||||
// span — so rate, best-hour and off-air figures are computed over the contest
|
||||
// itself without the operator having to look its dates up.
|
||||
func (r *Repo) Stats(ctx context.Context, from, to time.Time, contestID string, year int) (Stats, error) {
|
||||
// Operators returns the distinct operators in the log (upper-cased), with "—"
|
||||
// for QSOs the station owner logged himself (empty OPERATOR). Sorted, with "—"
|
||||
// last so the picker reads real callsigns first.
|
||||
func (r *Repo) Operators(ctx context.Context) ([]string, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `SELECT DISTINCT COALESCE(operator,'') FROM qso`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
seen := map[string]struct{}{}
|
||||
for rows.Next() {
|
||||
var op string
|
||||
if err := rows.Scan(&op); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
op = strings.ToUpper(strings.TrimSpace(op))
|
||||
if op == "" {
|
||||
op = "—"
|
||||
}
|
||||
seen[op] = struct{}{}
|
||||
}
|
||||
out := make([]string, 0, len(seen))
|
||||
hasOwner := false
|
||||
for op := range seen {
|
||||
if op == "—" {
|
||||
hasOwner = true
|
||||
continue
|
||||
}
|
||||
out = append(out, op)
|
||||
}
|
||||
sort.Strings(out)
|
||||
if hasOwner {
|
||||
out = append(out, "—")
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (r *Repo) Stats(ctx context.Context, from, to time.Time, contestID string, year int, operator string) (Stats, error) {
|
||||
var s Stats
|
||||
contestID = strings.ToUpper(strings.TrimSpace(contestID))
|
||||
// Operator filter: "" = all operators; "—" = QSOs the station owner logged
|
||||
// himself (empty OPERATOR); any other value = that operator's QSOs only.
|
||||
opFilter := strings.ToUpper(strings.TrimSpace(operator))
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT callsign, qso_date, band, mode, cont, country, dxcc,
|
||||
@@ -257,6 +297,17 @@ func (r *Repo) Stats(ctx context.Context, from, to time.Time, contestID string,
|
||||
continue
|
||||
}
|
||||
|
||||
// An empty OPERATOR means "the station owner logged it himself" — bucket
|
||||
// it as "—". Computed here so the operator filter can also drop QSOs that
|
||||
// aren't this operator's before they reach ANY bucket.
|
||||
op := strings.ToUpper(strings.TrimSpace(oper.String))
|
||||
if op == "" {
|
||||
op = "—"
|
||||
}
|
||||
if opFilter != "" && op != opFilter {
|
||||
continue
|
||||
}
|
||||
|
||||
// Window first: a QSO outside the period must not reach ANY bucket. Doing
|
||||
// this after the counting (the obvious mistake) would leave the mode/band/
|
||||
// operator charts showing the whole log while only the trend was filtered.
|
||||
@@ -288,12 +339,7 @@ func (r *Repo) Stats(ctx context.Context, from, to time.Time, contestID string,
|
||||
if b := strings.ToLower(strings.TrimSpace(band.String)); b != "" {
|
||||
bandC[b]++
|
||||
}
|
||||
// An empty OPERATOR means "the station owner logged it himself" — bucket
|
||||
// it explicitly rather than dropping the QSO from the operator chart.
|
||||
op := strings.ToUpper(strings.TrimSpace(oper.String))
|
||||
if op == "" {
|
||||
op = "—"
|
||||
}
|
||||
// op was resolved above (with the operator filter applied).
|
||||
opC[op]++
|
||||
if st := strings.ToUpper(strings.TrimSpace(station.String)); st != "" {
|
||||
stationC[st]++
|
||||
|
||||
Reference in New Issue
Block a user