fix: recording not being sent if mail enable was not checked

This commit is contained in:
2026-06-28 19:08:32 +02:00
parent 464a1c702c
commit 165f33caa5
9 changed files with 166 additions and 114 deletions
+7 -4
View File
@@ -35,8 +35,11 @@ export interface DetailsState {
sat_name: string;
sat_mode: string;
contest_id: string;
srx?: number;
stx?: number;
// Contest exchanges as free text — most are serials (001) but some are
// alphanumeric (e.g. a section/zone like "ON4"). Stored to ADIF SRX/STX when
// purely numeric, else to SRX_STRING/STX_STRING (handled in App on save).
srx_string?: string;
stx_string?: string;
email: string;
// Award references for the contacted station (set via the Awards tab picker).
// Semicolon-delimited "AWARD@REF" entries, e.g. "POTA@FR-11553;IOTA@EU-064".
@@ -371,10 +374,10 @@ export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, detai
<Input value={details.contest_id} onChange={(e) => onChange({ contest_id: e.target.value })} />
</Field>
<Field label="SRX">
<Input type="number" value={details.srx ?? ''} onChange={(e) => onChange({ srx: numOrUndef(e.target.value) })} />
<Input value={details.srx_string ?? ''} placeholder="rcvd exchange" onChange={(e) => onChange({ srx_string: e.target.value })} />
</Field>
<Field label="STX">
<Input type="number" value={details.stx ?? ''} onChange={(e) => onChange({ stx: numOrUndef(e.target.value) })} />
<Input value={details.stx_string ?? ''} placeholder="sent exchange" onChange={(e) => onChange({ stx_string: e.target.value })} />
</Field>
<Field label="Contacted email" span={3}>
<Input value={details.email} placeholder="[email protected]" onChange={(e) => onChange({ email: e.target.value })} />
+58 -63
View File
@@ -10,6 +10,7 @@ import {
import { cn } from '@/lib/utils';
import { FindQSOsForUpload, UploadQSOsManual, DownloadConfirmations, SyncPOTAHunterLog, ListQSO, BulkUpdateQSL, UploadCallsign } from '../../wailsjs/go/main/App';
import { Input } from '@/components/ui/input';
import { RecentQSOsGrid } from '@/components/RecentQSOsGrid';
import { EventsOn } from '../../wailsjs/runtime/runtime';
ModuleRegistry.registerModules([AllCommunityModule]);
@@ -60,6 +61,14 @@ const QSL_STATUSES = [
{ v: 'I', label: 'Ignore' },
];
// QSL routing methods for the paper-QSL "Via" dropdown (was free text).
const QSL_VIA_OPTIONS = [
{ v: '_', label: '— leave —' },
{ v: 'Bureau', label: 'Bureau' },
{ v: 'Direct', label: 'Direct' },
{ v: 'Electronic', label: 'Electronic' },
];
type LogQSO = {
id: number; qso_date: string; callsign: string; band: string; mode: string; country?: string;
qsl_sent?: string; qsl_rcvd?: string; qsl_via?: string; qsl_sent_date?: string; qsl_rcvd_date?: string;
@@ -127,13 +136,16 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
const [paperCall, setPaperCall] = useState('');
const [paperRows, setPaperRows] = useState<LogQSO[]>([]);
const [paperSel, setPaperSel] = useState<Set<number>>(new Set());
const [paperSelAllSig, setPaperSelAllSig] = useState(0); // bump → grid selects all
const [paperBusy, setPaperBusy] = useState(false);
const [paperMsg, setPaperMsg] = useState('');
const [qslRcvd, setQslRcvd] = useState('Y');
const [qslRcvd, setQslRcvd] = useState('N');
const [qslSent, setQslSent] = useState('_');
const [qslRcvdDate, setQslRcvdDate] = useState('');
const [qslSentDate, setQslSentDate] = useState('');
const [qslVia, setQslVia] = useState('');
const [qslVia, setQslVia] = useState('_');
const [qslNotes, setQslNotes] = useState('');
const [qslComment, setQslComment] = useState('');
const searchPaper = useCallback(async () => {
const c = paperCall.trim().toUpperCase();
@@ -144,14 +156,12 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
const list = (r ?? []) as LogQSO[];
setPaperRows(list);
setPaperSel(new Set(list.map((x) => x.id)));
setPaperSelAllSig((n) => n + 1); // tell the grid to select every row
} catch (e: any) { setPaperMsg(String(e?.message ?? e)); setPaperRows([]); }
finally { setPaperBusy(false); }
}, [paperCall]);
function togglePaper(id: number) {
setPaperSel((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
}
const paperAllSel = paperRows.length > 0 && paperSel.size === paperRows.length;
async function applyPaper() {
const ids = paperRows.filter((r) => paperSel.has(r.id)).map((r) => r.id);
@@ -164,7 +174,9 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
rcvd_status: qslRcvd === '_' ? '' : qslRcvd,
sent_date: ymd(qslSentDate),
rcvd_date: ymd(qslRcvdDate),
via: qslVia,
via: qslVia === '_' ? '' : qslVia,
notes: qslNotes,
comment: qslComment,
} as any);
setPaperMsg(`${n} QSO updated.`);
await searchPaper();
@@ -172,11 +184,11 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
finally { setPaperBusy(false); }
}
const [sent, setSent] = useState('R');
const [rows, setRows] = useState<UploadRow[]>([]);
// Selection lives in the (virtualized) ag-grid — it handles 25k rows smoothly.
const gridRef = useRef<AgGridReact<UploadRow>>(null);
// Full QSO rows (so the upload view uses the same rich grid as Recent QSOs).
const [rows, setRows] = useState<any[]>([]);
const [selectedCount, setSelectedCount] = useState(0);
const selectAllNext = useRef(false); // selectAll once after the next data load
const [uploadSelIds, setUploadSelIds] = useState<number[]>([]); // selected QSO ids → upload
const [uploadSelAllSig, setUploadSelAllSig] = useState(0); // bump → grid selects all
const [searching, setSearching] = useState(false);
const [error, setError] = useState('');
const [addNotFound, setAddNotFound] = useState(false);
@@ -210,15 +222,9 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
const serviceLabel = useMemo(() => SERVICES.find((s) => s.v === service)?.label ?? service, [service]);
// Grid selection → just track the count; ids are read from the grid at upload.
function onUploadSelChanged() {
setSelectedCount(gridRef.current?.api?.getSelectedNodes()?.length ?? 0);
}
// After "Select required" loads new rows, select them all (the old default).
function onUploadRowsLoaded() {
if (selectAllNext.current) {
selectAllNext.current = false;
gridRef.current?.api?.selectAll();
}
function onUploadRowSelected(ids: number[]) {
setUploadSelIds(ids);
setSelectedCount(ids.length);
}
const shownConfs = useMemo(() => confirmations.filter((c) => {
@@ -236,10 +242,11 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
setError('');
try {
const r: any = await FindQSOsForUpload(service, sent === '_' ? '' : sent);
const list = (r ?? []) as UploadRow[];
selectAllNext.current = true; // pre-select everything once the grid renders
const list = (r ?? []) as any[];
setRows(list);
setUploadSelIds(list.map((x) => x.id));
setSelectedCount(list.length);
setUploadSelAllSig((n) => n + 1); // pre-select everything once the grid renders
setViewMode('upload');
setShowLog(false);
} catch (e: any) {
@@ -252,7 +259,7 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
}, [service, sent]);
async function upload() {
const ids = ((gridRef.current?.api?.getSelectedRows() as UploadRow[] | undefined) ?? []).map((r) => r.id);
const ids = uploadSelIds;
if (ids.length === 0) return;
setLogLines([]); setBusy(true); setLogAction('upload'); setShowLog(true);
try { await UploadQSOsManual(service, ids); }
@@ -387,32 +394,16 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
paperRows.length === 0 ? (
<div className="text-sm text-muted-foreground py-10 text-center">Search a callsign to list its QSOs, then set QSL status below.</div>
) : (
<table className="w-full text-xs border-collapse">
<thead className="sticky top-0 bg-card">
<tr className="text-left text-muted-foreground border-b border-border">
<th className="py-1.5 px-2 w-8"><Checkbox checked={paperAllSel} onCheckedChange={() => setPaperSel(paperAllSel ? new Set() : new Set(paperRows.map((r) => r.id)))} /></th>
<th className="py-1.5 px-2">Date UTC</th><th className="py-1.5 px-2">Callsign</th>
<th className="py-1.5 px-2">Band</th><th className="py-1.5 px-2">Mode</th>
<th className="py-1.5 px-2">QSL Sent</th><th className="py-1.5 px-2">QSL Rcvd</th><th className="py-1.5 px-2">Via</th>
</tr>
</thead>
<tbody>
{paperRows.map((r) => (
<tr key={r.id}
className={cn('border-b border-border/40 cursor-pointer hover:bg-accent/30', paperSel.has(r.id) && 'bg-primary/5')}
onClick={() => togglePaper(r.id)}>
<td className="py-1 px-2" onClick={(e) => e.stopPropagation()}><Checkbox checked={paperSel.has(r.id)} onCheckedChange={() => togglePaper(r.id)} /></td>
<td className="py-1 px-2 font-mono">{fmtDate(r.qso_date)}</td>
<td className="py-1 px-2 font-mono font-bold">{r.callsign}</td>
<td className="py-1 px-2">{r.band}</td>
<td className="py-1 px-2">{r.mode}</td>
<td className="py-1 px-2 font-mono">{r.qsl_sent || '—'}{r.qsl_sent_date ? ` ${fmtQslDate(r.qsl_sent_date)}` : ''}</td>
<td className="py-1 px-2 font-mono">{r.qsl_rcvd || '—'}{r.qsl_rcvd_date ? ` ${fmtQslDate(r.qsl_rcvd_date)}` : ''}</td>
<td className="py-1 px-2 text-muted-foreground truncate max-w-[160px]">{r.qsl_via}</td>
</tr>
))}
</tbody>
</table>
// Same grid as Recent QSOs (full columns + column picker). Selection
// drives which QSOs the apply-form below updates; a search selects all.
<div className="flex flex-col h-full min-h-0 -mx-3 -my-2">
<RecentQSOsGrid
rows={paperRows as any}
total={paperRows.length}
selectAllSignal={paperSelAllSig}
onRowSelected={(ids) => setPaperSel(new Set(ids))}
/>
</div>
)
) : service === 'pota' ? (
<div className="space-y-3">
@@ -509,19 +500,12 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
) : rows.length === 0 ? (
<div className="text-sm text-muted-foreground py-10 text-center">Pick a service + sent status, then Select required.</div>
) : (
<div className="h-full w-full">
<AgGridReact<UploadRow>
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)}
<div className="flex flex-col h-full min-h-0 -mx-3 -my-2">
<RecentQSOsGrid
rows={rows as any}
total={rows.length}
selectAllSignal={uploadSelAllSig}
onRowSelected={onUploadRowSelected}
/>
</div>
)}
@@ -552,7 +536,18 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] uppercase tracking-wider text-muted-foreground">Via</label>
<Input className="h-8 w-40" value={qslVia} onChange={(e) => setQslVia(e.target.value)} placeholder="BUREAU / DIRECT / manager" />
<Select value={qslVia} onValueChange={setQslVia}>
<SelectTrigger className="h-8 w-32"><SelectValue /></SelectTrigger>
<SelectContent>{QSL_VIA_OPTIONS.map((o) => <SelectItem key={o.v} value={o.v}>{o.label}</SelectItem>)}</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] uppercase tracking-wider text-muted-foreground">Notes</label>
<Input className="h-8 w-40" value={qslNotes} onChange={(e) => setQslNotes(e.target.value)} placeholder="e.g. paid 3€" />
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] uppercase tracking-wider text-muted-foreground">Comment</label>
<Input className="h-8 w-40" value={qslComment} onChange={(e) => setQslComment(e.target.value)} placeholder="comment" />
</div>
<div className="flex-1" />
{paperMsg && <span className="text-[11px] text-muted-foreground self-center">{paperMsg}</span>}
+11 -5
View File
@@ -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<K extends keyof QSO>(key: K, value: QSO[K]) {
setDraft((d) => ({ ...d, [key]: value }));
}
@@ -561,7 +569,7 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
</thead>
<tbody>
{CONFIRMATIONS.map((c) => (
<tr key={c.key} className={cn('text-xs', c.key === confSel && 'bg-accent/40')}>
<tr key={c.key} className="text-xs">
<td className="font-medium pr-2 py-0.5">{c.label}</td>
<td className="w-24"><StatusCell value={val(c.sent)} /></td>
<td className="w-24">{c.rcvd ? <StatusCell value={val(c.rcvd)} /> : <span className="block text-center text-[11px] text-muted-foreground"></span>}</td>
@@ -578,10 +586,8 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
<TabsContent value="contest" className="mt-0">
<div className="grid grid-cols-6 gap-3">
<F label="Contest ID" span={2}><Input value={draft.contest_id ?? ''} onChange={(e) => set('contest_id', e.target.value)} /></F>
<F label="SRX"><Input type="number" value={draft.srx ?? ''} onChange={(e) => set('srx', intOrUndef(e.target.value) as any)} /></F>
<F label="STX"><Input type="number" value={draft.stx ?? ''} onChange={(e) => set('stx', intOrUndef(e.target.value) as any)} /></F>
<F label="SRX string" span={3}><Input value={draft.srx_string ?? ''} onChange={(e) => set('srx_string', e.target.value)} /></F>
<F label="STX string" span={3}><Input value={draft.stx_string ?? ''} onChange={(e) => set('stx_string', e.target.value)} /></F>
<F label="SRX" span={2}><Input value={draft.srx_string || (draft.srx ?? '')} placeholder="rcvd exchange" onChange={(e) => setExchange('srx', e.target.value)} /></F>
<F label="STX" span={2}><Input value={draft.stx_string || (draft.stx ?? '')} placeholder="sent exchange" onChange={(e) => setExchange('stx', e.target.value)} /></F>
<F label="Check"><Input value={draft.check ?? ''} onChange={(e) => set('check', e.target.value)} /></F>
<F label="Precedence"><Input value={draft.precedence ?? ''} onChange={(e) => set('precedence', e.target.value)} /></F>
<F label="ARRL section"><Input value={draft.arrl_sect ?? ''} onChange={(e) => set('arrl_sect', e.target.value)} /></F>
+9 -1
View File
@@ -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<any>(null);
const [pickerOpen, setPickerOpen] = useState(false);
const [menu, setMenu] = useState<QSOMenuState>(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