From 45d081ac0ceb98e65ef79862f22278621fbc87b0 Mon Sep 17 00:00:00 2001 From: Gregory Salaun Date: Thu, 18 Jun 2026 23:20:24 +0200 Subject: [PATCH] feat: added colonns for awards in recent qso --- app.go | 58 ++++++++++++++++++++ frontend/src/App.tsx | 42 +++++++++++++- frontend/src/components/RecentQSOsGrid.tsx | 44 +++++++++++++-- frontend/src/components/WorkedBeforeGrid.tsx | 43 +++++++++++++-- frontend/wailsjs/go/main/App.d.ts | 2 + frontend/wailsjs/go/main/App.js | 4 ++ 6 files changed, 180 insertions(+), 13 deletions(-) diff --git a/app.go b/app.go index c7ca3b8..7795956 100644 --- a/app.go +++ b/app.go @@ -2718,6 +2718,64 @@ func (a *App) ComputeQSOAwardRefs(q qso.QSO) ([]QSOAwardRef, error) { return out, nil } +// AwardRefsForQSOs returns, per QSO id, a map of award code → the reference(s) +// that QSO contributes to (joined when several). Powers the per-award columns in +// the Recent QSOs / Worked-before grids. The reference metadata is computed ONCE +// for the whole batch so a page of QSOs stays cheap. +func (a *App) AwardRefsForQSOs(ids []int64) (map[int64]map[string]string, error) { + out := map[int64]map[string]string{} + if a.qso == nil || len(ids) == 0 { + return out, nil + } + defs := a.awardDefs() + metas := a.awardRefMetas(defs) + fieldByCode := map[string]string{} + for _, d := range defs { + fieldByCode[strings.ToUpper(d.Code)] = strings.ToLower(strings.TrimSpace(d.Field)) + } + nameOf := func(field, ref string) string { + switch field { + case "dxcc": + if n, err := strconv.Atoi(ref); err == nil { + return dxcc.NameForDXCC(n) + } + case "cont": + return continentName(ref) + } + return "" + } + err := a.qso.IterateByIDs(a.ctx, ids, func(q qso.QSO) error { + a.enrichQSOForAwards(&q) + results := award.Compute(defs, []qso.QSO{q}, metas, nameOf) + m := map[string]string{} + for i := range results { + r := &results[i] + code := strings.ToUpper(r.Code) + dxccField := fieldByCode[code] == "dxcc" + var refs []string + for _, rf := range r.Refs { + if !rf.Worked { + continue + } + // DXCC's ref is a number → show the country name instead. + label := rf.Ref + if dxccField && rf.Name != "" { + label = rf.Name + } + refs = append(refs, label) + } + if len(refs) > 0 { + m[code] = strings.Join(refs, ", ") + } + } + if len(m) > 0 { + out[q.ID] = m + } + return nil + }) + return out, err +} + // AwardRefMeta describes a reference list's state for the UI. type AwardRefMeta struct { Code string `json:"code"` diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 16adc42..dfd43f6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -33,6 +33,7 @@ import { GetAwardDefs, GetUIPref, ReportLiveActivity, + AwardRefsForQSOs, } from '../wailsjs/go/main/App'; import { Combobox } from '@/components/ui/combobox'; import { applyAwardRefs } from '@/lib/awardRefs'; @@ -764,6 +765,40 @@ export default function App() { const wbTimerRef = useRef(null); const [wb, setWb] = useState(null); const [wbBusy, setWbBusy] = useState(false); + + // Per-award columns for the Recent QSOs / Worked-before grids: load the award + // list once, then compute each shown QSO's reference per award and attach it + // to the rows (the grids render one hideable column per award). + const [awardCols, setAwardCols] = useState<{ code: string; name: string }[]>([]); + useEffect(() => { + GetAwardDefs().then((defs: any[]) => + setAwardCols(((defs ?? []) as any[]).map((d) => ({ code: d.code, name: d.name })).sort((a, b) => a.code.localeCompare(b.code))), + ).catch(() => {}); + }, []); + const [qsoAwardRefs, setQsoAwardRefs] = useState>>({}); + useEffect(() => { + const ids = (qsos as any[]).map((q) => q.id).filter(Boolean); + if (ids.length === 0 || awardCols.length === 0) { setQsoAwardRefs({}); return; } + let alive = true; + AwardRefsForQSOs(ids as any).then((m: any) => { if (alive) setQsoAwardRefs(m ?? {}); }).catch(() => {}); + return () => { alive = false; }; + }, [qsos, awardCols.length]); + const qsosWithAwards = useMemo( + () => (qsos as any[]).map((q) => ({ ...q, award_refs: qsoAwardRefs[String(q.id)] })), + [qsos, qsoAwardRefs], + ); + const [wbAwardRefs, setWbAwardRefs] = useState>>({}); + useEffect(() => { + const ids = ((wb?.entries ?? []) as any[]).map((e) => e.id).filter(Boolean); + if (ids.length === 0 || awardCols.length === 0) { setWbAwardRefs({}); return; } + let alive = true; + AwardRefsForQSOs(ids as any).then((m: any) => { if (alive) setWbAwardRefs(m ?? {}); }).catch(() => {}); + return () => { alive = false; }; + }, [wb, awardCols.length]); + const wbWithAwards = useMemo( + () => (wb ? { ...wb, entries: ((wb.entries ?? []) as any[]).map((e) => ({ ...e, award_refs: wbAwardRefs[String(e.id)] })) } : null), + [wb, wbAwardRefs], + ); // Always-current copy of the entry callsign, so the UDP event handlers // (which live in a []-deps effect with a stale `callsign` closure) can // tell whether an incoming DX call actually changed anything. @@ -2542,7 +2577,7 @@ export default function App() { case 'worked': return (
- openEdit(q.id as number)} + openEdit(q.id as number)} onUpdateFromCty={bulkUpdateFromCty} onUpdateFromQRZ={bulkUpdateFromQRZ} onUpdateFromClublog={bulkUpdateFromClublog} onSendTo={bulkSendTo} onSendRecording={bulkSendRecording} onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)} />
@@ -3225,8 +3260,9 @@ export default function App() { )} openEdit(q.id as number)} onUpdateFromCty={bulkUpdateFromCty} onUpdateFromQRZ={bulkUpdateFromQRZ} @@ -3410,7 +3446,7 @@ export default function App() { - openEdit(q.id as number)} + openEdit(q.id as number)} onUpdateFromCty={bulkUpdateFromCty} onUpdateFromQRZ={bulkUpdateFromQRZ} onUpdateFromClublog={bulkUpdateFromClublog} onSendTo={bulkSendTo} onSendRecording={bulkSendRecording} onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)} /> diff --git a/frontend/src/components/RecentQSOsGrid.tsx b/frontend/src/components/RecentQSOsGrid.tsx index 8a99f12..f8e91bf 100644 --- a/frontend/src/components/RecentQSOsGrid.tsx +++ b/frontend/src/components/RecentQSOsGrid.tsx @@ -56,6 +56,9 @@ type Props = { onBulkEdit?: (ids: number[]) => void; onExportSelected?: (ids: number[]) => void; onExportFiltered?: () => void; + // One column per defined award; the cell shows the reference this QSO counts + // for (from row.award_refs[CODE], attached by the parent). Hidden by default. + awardCols?: { code: string; name: string }[]; }; const COL_STATE_KEY = 'hamlog.qsoColState.v2'; @@ -219,7 +222,7 @@ export const GROUP_ORDER = [ 'Contest', 'Propagation', 'My station', 'Misc', ]; -export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportFiltered }: Props) { +export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportFiltered, awardCols }: Props) { const gridRef = useRef(null); const [pickerOpen, setPickerOpen] = useState(false); const [menu, setMenu] = useState(null); @@ -245,10 +248,21 @@ export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpda // Compute initial column defs: all columns defined, but those not marked // defaultVisible start hidden. The user's saved state (loaded onGridReady) // overrides this so a previously toggled column wins. - const columnDefs = useMemo[]>(() => COL_CATALOG.map((c) => { - const { group: _g, label: _l, defaultVisible, ...rest } = c; - return { ...rest, hide: !defaultVisible }; - }), []); + const columnDefs = useMemo[]>(() => { + const base = COL_CATALOG.map((c) => { + const { group: _g, label: _l, defaultVisible, ...rest } = c; + return { ...rest, hide: !defaultVisible }; + }); + const awards: ColDef[] = (awardCols ?? []).map((a) => ({ + colId: `award_${a.code}`, + headerName: a.code, + headerTooltip: `${a.name} — reference this QSO counts for`, + width: 110, + cellClass: 'text-[11px]', + valueGetter: (p) => (p.data as any)?.award_refs?.[a.code.toUpperCase()] ?? '', + })); + return [...base, ...awards]; + }, [awardCols]); const defaultColDef = useMemo(() => ({ sortable: true, @@ -407,6 +421,26 @@ export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpda ); })} + {awardCols && awardCols.length > 0 && ( +
+
+ Awards +
+ + +
+
+
+ {awardCols.map((a) => ( + + ))} +
+
+ )} diff --git a/frontend/src/components/WorkedBeforeGrid.tsx b/frontend/src/components/WorkedBeforeGrid.tsx index 9705b00..8ca8bc7 100644 --- a/frontend/src/components/WorkedBeforeGrid.tsx +++ b/frontend/src/components/WorkedBeforeGrid.tsx @@ -53,6 +53,8 @@ type Props = { onSendTo?: (service: string, ids: number[]) => void; onSendRecording?: (ids: number[]) => void; onSendEQSL?: (ids: number[]) => void; + // One column per defined award (cell = the reference this QSO counts for). + awardCols?: { code: string; name: string }[]; }; const COL_STATE_KEY = 'hamlog.workedBeforeColState.v2'; @@ -65,7 +67,7 @@ function fmtDate(s: any): string { return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}`; } -export function WorkedBeforeGrid({ wb, busy, currentCall, onRowDoubleClicked, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL }: Props) { +export function WorkedBeforeGrid({ wb, busy, currentCall, onRowDoubleClicked, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, awardCols }: Props) { const gridRef = useRef(null); const [pickerOpen, setPickerOpen] = useState(false); const [menu, setMenu] = useState(null); @@ -93,10 +95,21 @@ export function WorkedBeforeGrid({ wb, busy, currentCall, onRowDoubleClicked, on const count = wb?.count ?? 0; const entries = wb?.entries ?? []; - const columnDefs = useMemo[]>(() => COL_CATALOG.map((c) => { - const { group: _g, label: _l, defaultVisible, ...rest } = c; - return { ...rest, hide: !defaultVisible }; - }), []); + const columnDefs = useMemo[]>(() => { + const base = COL_CATALOG.map((c) => { + const { group: _g, label: _l, defaultVisible, ...rest } = c; + return { ...rest, hide: !defaultVisible }; + }); + const awards: ColDef[] = (awardCols ?? []).map((a) => ({ + colId: `award_${a.code}`, + headerName: a.code, + headerTooltip: `${a.name} — reference this QSO counts for`, + width: 110, + cellClass: 'text-[11px]', + valueGetter: (p) => (p.data as any)?.award_refs?.[a.code.toUpperCase()] ?? '', + })); + return [...base, ...awards]; + }, [awardCols]); const defaultColDef = useMemo(() => ({ sortable: true, resizable: true, filter: true, suppressMovable: false, @@ -283,6 +296,26 @@ export function WorkedBeforeGrid({ wb, busy, currentCall, onRowDoubleClicked, on ); })} + {awardCols && awardCols.length > 0 && ( +
+
+ Awards +
+ + +
+
+
+ {awardCols.map((a) => ( + + ))} +
+
+ )} diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index b7547ca..836df3e 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -33,6 +33,8 @@ export function AwardFields():Promise>; export function AwardMissingQSOs(arg1:string):Promise>; +export function AwardRefsForQSOs(arg1:Array):Promise>>; + export function BrowseExecutable():Promise; export function BulkUpdateField(arg1:Array,arg2:string,arg3:string):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index 629f521..63d6ec3 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -38,6 +38,10 @@ export function AwardMissingQSOs(arg1) { return window['go']['main']['App']['AwardMissingQSOs'](arg1); } +export function AwardRefsForQSOs(arg1) { + return window['go']['main']['App']['AwardRefsForQSOs'](arg1); +} + export function BrowseExecutable() { return window['go']['main']['App']['BrowseExecutable'](); }