Search a callsign to list its QSOs, then set QSL status below.
) : (
-
@@ -509,19 +500,12 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
) : rows.length === 0 ? (
Pick a service + sent status, then “Select required”.
) : (
-
-
- ref={gridRef}
- theme={qslTheme}
- rowData={rows}
- columnDefs={UPLOAD_COLS}
- defaultColDef={{ sortable: true, resizable: true, filter: true }}
- rowSelection={{ mode: 'multiRow', checkboxes: true, headerCheckbox: true }}
- onSelectionChanged={onUploadSelChanged}
- onRowDataUpdated={onUploadRowsLoaded}
- animateRows={false}
- suppressCellFocus
- getRowId={(p) => String((p.data as any).id)}
+
+
)}
@@ -552,7 +536,18 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
- setQslVia(e.target.value)} placeholder="BUREAU / DIRECT / manager" />
+
+
+
+
+ setQslNotes(e.target.value)} placeholder="e.g. paid 3€" />
+
+
+
+ setQslComment(e.target.value)} placeholder="comment" />
{paperMsg &&
{paperMsg}}
diff --git a/frontend/src/components/QSOEditModal.tsx b/frontend/src/components/QSOEditModal.tsx
index f92904e..3103d4d 100644
--- a/frontend/src/components/QSOEditModal.tsx
+++ b/frontend/src/components/QSOEditModal.tsx
@@ -215,6 +215,14 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
return () => window.clearTimeout(t);
}, [draft.dxcc, draft.cqz, draft.ituz, draft.cont, draft.state, draft.callsign, draft.notes, draft.band]);
+ // Contest exchange typed as free text → numeric SRX/STX when all-digits, else
+ // the SRX_STRING/STX_STRING field. Mirrors the entry strip (F5) so the field
+ // accepts letters (sections/zones), not just numbers.
+ function setExchange(which: 'srx' | 'stx', raw: string) {
+ const t = raw.trim();
+ const num = /^\d+$/.test(t) ? parseInt(t, 10) : undefined;
+ setDraft((d) => ({ ...d, [which]: num, [`${which}_string`]: num != null ? '' : t } as any));
+ }
function set
(key: K, value: QSO[K]) {
setDraft((d) => ({ ...d, [key]: value }));
}
@@ -561,7 +569,7 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
{CONFIRMATIONS.map((c) => (
-
+
| {c.label} |
|
{c.rcvd ? : —} |
@@ -578,10 +586,8 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
set('contest_id', e.target.value)} />
-
set('srx', intOrUndef(e.target.value) as any)} />
-
set('stx', intOrUndef(e.target.value) as any)} />
-
set('srx_string', e.target.value)} />
-
set('stx_string', e.target.value)} />
+
setExchange('srx', e.target.value)} />
+
setExchange('stx', e.target.value)} />
set('check', e.target.value)} />
set('precedence', e.target.value)} />
set('arrl_sect', e.target.value)} />
diff --git a/frontend/src/components/RecentQSOsGrid.tsx b/frontend/src/components/RecentQSOsGrid.tsx
index 3e4b3d5..24a4e1a 100644
--- a/frontend/src/components/RecentQSOsGrid.tsx
+++ b/frontend/src/components/RecentQSOsGrid.tsx
@@ -45,6 +45,9 @@ const hamlogTheme = themeQuartz.withParams({
type Props = {
rows: QSOForm[];
total: number;
+ // Bump this number to programmatically select every row (e.g. after a search
+ // in the QSL Manager, where the default is "all selected").
+ selectAllSignal?: number;
onRowDoubleClicked?: (q: QSOForm) => void;
onRowSelected?: (ids: number[]) => void;
onUpdateFromCty?: (ids: number[]) => void;
@@ -225,7 +228,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, onDelete, awardCols }: Props) {
+export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportFiltered, onDelete, awardCols }: Props) {
const gridRef = useRef
(null);
const [pickerOpen, setPickerOpen] = useState(false);
const [menu, setMenu] = useState(null);
@@ -310,6 +313,11 @@ export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpda
const sel = (gridRef.current?.api?.getSelectedRows() as QSOForm[] | undefined) ?? [];
onRowSelected?.(sel.map((r) => r.id as number).filter((id) => id != null));
}
+ // Select every row when the caller bumps selectAllSignal (QSL Manager search).
+ useEffect(() => {
+ if (selectAllSignal === undefined) return;
+ gridRef.current?.api?.selectAll();
+ }, [selectAllSignal]);
// ── Column picker (visibility) ──
// Drives AG Grid via setColumnsVisible(). We don't keep a parallel React
diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts
index 4834f70..342d898 100644
--- a/frontend/wailsjs/go/main/App.d.ts
+++ b/frontend/wailsjs/go/main/App.d.ts
@@ -130,7 +130,7 @@ export function ExportAwards():Promise;
export function FilterFields():Promise>;
-export function FindQSOsForUpload(arg1:string,arg2:string):Promise>;
+export function FindQSOsForUpload(arg1:string,arg2:string):Promise>;
export function FlexATUBypass():Promise;
diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts
index a021ca8..c5ffcb3 100644
--- a/frontend/wailsjs/go/models.ts
+++ b/frontend/wailsjs/go/models.ts
@@ -1564,6 +1564,8 @@ export namespace main {
sent_date: string;
rcvd_date: string;
via: string;
+ notes: string;
+ comment: string;
static createFrom(source: any = {}) {
return new QSLBulkUpdate(source);
@@ -1576,6 +1578,8 @@ export namespace main {
this.sent_date = source["sent_date"];
this.rcvd_date = source["rcvd_date"];
this.via = source["via"];
+ this.notes = source["notes"];
+ this.comment = source["comment"];
}
}
export class QSLDefaults {
@@ -2897,30 +2901,6 @@ export namespace qso {
return a;
}
}
- export class UploadRow {
- id: number;
- qso_date: string;
- callsign: string;
- band: string;
- mode: string;
- country: string;
- status: string;
-
- static createFrom(source: any = {}) {
- return new UploadRow(source);
- }
-
- constructor(source: any = {}) {
- if ('string' === typeof source) source = JSON.parse(source);
- this.id = source["id"];
- this.qso_date = source["qso_date"];
- this.callsign = source["callsign"];
- this.band = source["band"];
- this.mode = source["mode"];
- this.country = source["country"];
- this.status = source["status"];
- }
- }
export class WorkedBefore {
callsign: string;
count: number;
diff --git a/internal/qso/qso.go b/internal/qso/qso.go
index e34bcad..6b545a6 100644
--- a/internal/qso/qso.go
+++ b/internal/qso/qso.go
@@ -497,6 +497,31 @@ var uploadStatusCols = map[string]bool{
// ListForUpload returns QSOs whose per-service sent-status column equals
// value ("" matches blank/NULL). Used by the QSL Manager's "Select required".
+// ListForUploadFull is like ListForUpload but returns FULL QSO rows so the UI
+// can show the same rich, column-pickable table as Recent QSOs. column is an
+// upload-status column (validated); value is the status to match ("" = not yet
+// uploaded).
+func (r *Repo) ListForUploadFull(ctx context.Context, column, value string) ([]QSO, error) {
+ if !uploadStatusCols[column] {
+ return nil, fmt.Errorf("invalid upload column %q", column)
+ }
+ rows, err := r.db.QueryContext(ctx,
+ `SELECT `+selectCols+` FROM qso WHERE COALESCE(`+column+`,'') = ? ORDER BY qso_date DESC, id DESC`, value)
+ if err != nil {
+ return nil, fmt.Errorf("list for upload: %w", err)
+ }
+ defer rows.Close()
+ out := make([]QSO, 0, 64)
+ for rows.Next() {
+ q, err := scanQSO(rows)
+ if err != nil {
+ return nil, err
+ }
+ out = append(out, q)
+ }
+ return out, rows.Err()
+}
+
func (r *Repo) ListForUpload(ctx context.Context, column, value string) ([]UploadRow, error) {
if !uploadStatusCols[column] {
return nil, fmt.Errorf("invalid upload column %q", column)