This commit is contained in:
2026-06-03 21:53:31 +02:00
parent 2b4326b553
commit 1a425a1b0d
15 changed files with 377 additions and 97 deletions
+57 -12
View File
@@ -8,7 +8,7 @@ import {
AddQSO, ListQSO, CountQSO,
OpenADIFFile, ImportADIF, SaveADIFFile, ExportADIF,
GetQSO, UpdateQSO, DeleteQSO, DeleteAllQSO,
UpdateQSOsFromCty, UpdateQSOsFromQRZ,
UpdateQSOsFromCty, UpdateQSOsFromQRZ, UploadQSOsManual,
LookupCallsign, GetStationSettings, GetListsSettings,
GetStartupStatus,
WorkedBefore,
@@ -555,6 +555,18 @@ export default function App() {
const [pendingImportPath, setPendingImportPath] = useState<string | null>(null);
const [importDupMode, setImportDupMode] = useState<'skip' | 'update' | 'all'>('skip');
const [importApplyCty, setImportApplyCty] = useState(true);
// QRZ profile photo lightbox (full-size, in-app — not the browser).
const [photoModal, setPhotoModal] = useState<string | null>(null);
// Esc closes the lightbox. Capture-phase + stopImmediatePropagation so the
// global ESC handler (which resets the entry) doesn't also fire.
useEffect(() => {
if (!photoModal) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') { e.stopImmediatePropagation(); e.preventDefault(); setPhotoModal(null); }
};
window.addEventListener('keydown', onKey, true);
return () => window.removeEventListener('keydown', onKey, true);
}, [photoModal]);
// === Lookup + WB ===
const [lookupResult, setLookupResult] = useState<LookupResult | null>(null);
@@ -1083,6 +1095,16 @@ export default function App() {
try { await afterBulkUpdate(await UpdateQSOsFromQRZ(ids as any), 'from QRZ.com'); }
catch (e: any) { setError(String(e?.message ?? e)); }
}
// Right-click "Send to QRZ.com / Club Log / LoTW": uploads the selected QSOs
// on demand (regardless of their current upload status). Runs in the
// background; qslmgr:done refreshes the grid when finished.
async function bulkSendTo(service: string, ids: number[]) {
if (ids.length === 0) return;
const label = service === 'qrz' ? 'QRZ.com' : service === 'clublog' ? 'Club Log' : service === 'lotw' ? 'LoTW' : service;
showToast(`Uploading ${ids.length} QSO${ids.length > 1 ? 's' : ''} to ${label}`);
try { await UploadQSOsManual(service, ids as any); }
catch (e: any) { setError(String(e?.message ?? e)); }
}
function askDelete(id: number) {
const q = qsos.find((x) => x.id === id);
if (q) setDeletingQSO(q);
@@ -1273,9 +1295,6 @@ export default function App() {
{ name: 'tools', label: 'Tools', items: [
{ type: 'item', label: 'QSL Manager…', action: 'tools.qslmanager' },
{ type: 'separator' },
{ type: 'item', label: 'Callsign lookup settings…', action: 'tools.lookup' },
{ type: 'item', label: 'CAT interface…', action: 'tools.cat' },
{ type: 'item', label: 'Rotator…', action: 'tools.rotator' },
{ type: 'item', label: wkEnabled ? '✓ WinKeyer CW keyer' : 'WinKeyer CW keyer', action: 'tools.winkeyer' },
{ type: 'separator' },
// Maintenance — bumped here while we only have one entry. Will move
@@ -1298,9 +1317,6 @@ export default function App() {
case 'edit.delete': if (selectedId !== null) askDelete(selectedId); break;
case 'edit.prefs': setShowSettings(true); break;
case 'tools.qslmanager': setQslTabOpen(true); setActiveTab('qsl'); break;
case 'tools.lookup': setSettingsSection('lookup'); setShowSettings(true); break;
case 'tools.cat': setSettingsSection('cat'); setShowSettings(true); break;
case 'tools.rotator': setSettingsSection('rotator'); setShowSettings(true); break;
case 'tools.winkeyer': wkSetEnabled(!wkEnabled); break;
case 'tools.refreshCty': refreshCtyDat(); break;
}
@@ -1389,7 +1405,10 @@ export default function App() {
tabIndex={-1}
onClick={() => {
const c = callsign.trim().toUpperCase();
OpenExternalURL(`https://www.qrz.com/db/${encodeURIComponent(c)}`)
// Encode each segment but keep the '/' literal — QRZ's URL is
// /db/5Z4/MM0ZBH, not /db/5Z4%2FMM0ZBH (which 404s).
const path = c.split('/').map(encodeURIComponent).join('/');
OpenExternalURL(`https://www.qrz.com/db/${path}`)
.catch((err) => setError(String(err?.message ?? err)));
}}
title="Open this callsign on QRZ.com"
@@ -1800,6 +1819,31 @@ export default function App() {
</div>
)}
{/* QRZ profile photo lightbox — full size, in-app. Click anywhere or
press Esc to close; click the image itself doesn't close. */}
{photoModal && (
<div
className="fixed inset-0 z-[200] flex items-center justify-center bg-stone-900/70 backdrop-blur-sm p-6 animate-in fade-in"
onClick={() => setPhotoModal(null)}
>
<button
type="button"
className="absolute top-4 right-4 rounded-md bg-white/10 hover:bg-white/20 text-white p-1.5"
onClick={() => setPhotoModal(null)}
title="Close (Esc)"
>
<X className="size-5" />
</button>
<img
src={photoModal}
alt="profile full size"
className="max-h-full max-w-full object-contain rounded-lg shadow-2xl"
referrerPolicy="no-referrer"
onClick={(e) => e.stopPropagation()}
/>
</div>
)}
{/* Transient success toast (bottom-right). */}
{toast && (
<div className="fixed bottom-4 right-4 z-[100] flex items-center gap-2 rounded-lg border border-emerald-300 bg-emerald-50 text-emerald-800 px-3.5 py-2 text-sm shadow-lg animate-in fade-in slide-in-from-bottom-2">
@@ -1966,9 +2010,9 @@ export default function App() {
<div className={cn('min-w-0 flex items-center', wkEnabled ? 'shrink-0' : 'flex-1')}>
<button
type="button"
onClick={() => lookupResult.image_url && OpenExternalURL(lookupResult.image_url).catch((err) => setError(String(err?.message ?? err)))}
onClick={() => lookupResult.image_url && setPhotoModal(lookupResult.image_url)}
className="rounded-lg border border-border overflow-hidden hover:border-primary/60 transition-colors bg-muted/20"
title="Open full-size on QRZ.com"
title="Click to view full size"
>
<img
src={lookupResult.image_url}
@@ -2101,6 +2145,7 @@ export default function App() {
onRowDoubleClicked={(q) => openEdit(q.id as number)}
onUpdateFromCty={bulkUpdateFromCty}
onUpdateFromQRZ={bulkUpdateFromQRZ}
onSendTo={bulkSendTo}
onRowSelected={(id) => setSelectedId(id)}
/>
<div className="px-3 py-1.5 border-t border-border/60 text-[11px] text-muted-foreground flex items-center justify-between gap-3 bg-muted/30">
@@ -2432,7 +2477,7 @@ export default function App() {
<TabsContent value="worked" className="mt-0 flex flex-col min-h-0 flex-1">
<WorkedBeforeGrid wb={wb} busy={wbBusy} currentCall={callsign} onRowDoubleClicked={(q) => openEdit(q.id as number)}
onUpdateFromCty={bulkUpdateFromCty} onUpdateFromQRZ={bulkUpdateFromQRZ} />
onUpdateFromCty={bulkUpdateFromCty} onUpdateFromQRZ={bulkUpdateFromQRZ} onSendTo={bulkSendTo} />
</TabsContent>
{/* Opened on demand from Tools → QSL Manager; closable via the
@@ -2526,7 +2571,7 @@ export default function App() {
})()}
{editingQSO && (
<QSOEditModal qso={editingQSO} onSave={onModalSave} onDelete={onModalDelete} onClose={() => setEditingQSO(null)} />
<QSOEditModal qso={editingQSO} onSave={onModalSave} onDelete={onModalDelete} onClose={() => setEditingQSO(null)} countries={countries} />
)}
<SendSpotModal
+1 -1
View File
@@ -499,7 +499,7 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o
</div>
<div className="px-3 py-1 text-[9px] text-muted-foreground bg-muted/30 border-t border-border font-mono text-center shrink-0">
scroll · ctrl+wheel = zoom · = jump to rig
{hidden > 0 && <span className="text-amber-600"> · {hidden} data spot{hidden > 1 ? 's' : ''} hidden (top {MAX_VISIBLE_SPOTS} shown)</span>}
{hidden > 0 && <span className="text-amber-600"> · {hidden} FT8/FT4 spot{hidden > 1 ? 's' : ''} hidden top {MAX_VISIBLE_SPOTS} kept (CW/SSB all shown)</span>}
</div>
</div>
);
+26 -3
View File
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { Globe2, RefreshCw } from 'lucide-react';
import { Globe2, RefreshCw, Upload } from 'lucide-react';
export type QSOMenuState = { x: number; y: number; ids: number[] } | null;
@@ -8,12 +8,19 @@ type Props = {
onClose: () => void;
onUpdateFromCty: (ids: number[]) => void;
onUpdateFromQRZ: (ids: number[]) => void;
onSendTo?: (service: string, ids: number[]) => void;
};
const UPLOAD_TARGETS: { service: string; label: string }[] = [
{ service: 'qrz', label: 'Send to QRZ.com' },
{ service: 'clublog', label: 'Send to Club Log' },
{ service: 'lotw', label: 'Send to LoTW' },
];
// Lightweight right-click menu for the QSO grids. AG Grid's native context
// menu is an Enterprise feature, so this is a plain floating menu driven by
// onCellContextMenu. Closes on any outside click, scroll or Escape.
export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ }: Props) {
export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ, onSendTo }: Props) {
useEffect(() => {
if (!menu) return;
const close = () => onClose();
@@ -34,7 +41,7 @@ export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ
const n = menu.ids.length;
// Keep the menu on-screen near the cursor.
const x = Math.min(menu.x, window.innerWidth - 248);
const y = Math.min(menu.y, window.innerHeight - 110);
const y = Math.min(menu.y, window.innerHeight - (onSendTo ? 230 : 110));
return (
<div
@@ -59,6 +66,22 @@ export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ
<RefreshCw className="size-4 text-sky-600" />
<span>Update from QRZ.com</span>
</button>
{onSendTo && (
<>
<div className="my-1 border-t border-border" />
{UPLOAD_TARGETS.map((t) => (
<button
key={t.service}
className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50"
onClick={() => { onSendTo(t.service, menu.ids); onClose(); }}
>
<Upload className="size-4 text-emerald-600" />
<span>{t.label}</span>
</button>
))}
</>
)}
</div>
);
}
+23 -18
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { Trash2, Search, Loader2 } from 'lucide-react';
import { LookupCallsign } from '../../wailsjs/go/main/App';
import { LookupCallsign, DXCCForCountry } from '../../wailsjs/go/main/App';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';
@@ -14,6 +14,7 @@ import {
Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
} from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { Combobox } from '@/components/ui/combobox';
import { cn } from '@/lib/utils';
import { flagURL } from '@/lib/flags';
import type { QSOForm } from '@/types';
@@ -63,6 +64,11 @@ const CONFIRMATIONS: ConfDef[] = [
// Colour-coded status cell for the confirmation grid.
function StatusCell({ value }: { value?: string }) {
const v = (value || '').toUpperCase();
// Empty = no value set yet → show a neutral dash, NOT "No" (which is the
// explicit "N" status). Mirrors the dropdown, which shows "—" for empty.
if (v === '') {
return <span className="block text-center text-[11px] text-muted-foreground"></span>;
}
const label = v === 'Y' ? 'Yes' : v === 'R' ? 'Requested' : v === 'I' ? 'Ignore' : v === 'M' ? 'Modified' : 'No';
const cls = v === 'Y' ? 'bg-emerald-600 text-white'
: v === 'R' ? 'bg-orange-400 text-white'
@@ -76,6 +82,7 @@ interface Props {
onSave: (q: QSO) => void;
onDelete: (id: number) => void;
onClose: () => void;
countries?: string[];
}
function toLocalISO(d: any): string {
@@ -138,7 +145,7 @@ function QslSelect({ value, onChange }: { value?: string; onChange: (v: string)
);
}
export function QSOEditModal({ qso, onSave, onDelete, onClose }: Props) {
export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [] }: Props) {
const [draft, setDraft] = useState<QSO>(() => JSON.parse(JSON.stringify(qso)));
// Frequencies are edited as kHz + Hz (Log4OM style) and recombined on save.
const splitHz = (hz?: number) => hz
@@ -163,6 +170,16 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose }: Props) {
setDraft((d) => ({ ...d, [key]: value }));
}
// Country drives the DXCC entity number (ADIF). The DXCC field is read-only;
// picking a Country resolves and stamps its DXCC# so they can't diverge.
async function onCountryChange(v: string) {
set('country', v);
try {
const n = await DXCCForCountry(v);
set('dxcc', (n && n > 0 ? n : undefined) as any);
} catch { /* leave DXCC as-is if resolution fails */ }
}
// Re-run a callsign lookup (QRZ/HamQTH + cty.dat) and merge the result into
// the draft — handy after correcting the callsign. Only overwrites the
// lookup-derived fields; leaves call/band/mode/RST/dates alone.
@@ -270,7 +287,6 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose }: Props) {
<TabsTrigger value="contest">Contest</TabsTrigger>
<TabsTrigger value="sat">Sat / Prop</TabsTrigger>
<TabsTrigger value="mystation">My Station</TabsTrigger>
<TabsTrigger value="notes">Notes</TabsTrigger>
<TabsTrigger value="extras">
Extras
{extrasCount > 0 && (
@@ -331,14 +347,15 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose }: Props) {
</div>
<div className="flex items-center gap-2">
<Label className="w-20 shrink-0">Country</Label>
<Input value={draft.country ?? ''} onChange={(e) => set('country', e.target.value)} className="flex-1" />
<Combobox value={draft.country ?? ''} options={countries} placeholder="Country"
onChange={onCountryChange} className="flex-1" />
</div>
<div className="flex items-center gap-2">
<Label className="w-20 shrink-0">ITU</Label>
<Input type="number" value={draft.ituz ?? ''} onChange={(e) => set('ituz', intOrUndef(e.target.value) as any)} className="font-mono w-16 text-center" />
<Label>CQ</Label>
<Input type="number" value={draft.cqz ?? ''} onChange={(e) => set('cqz', intOrUndef(e.target.value) as any)} className="font-mono w-16 text-center" />
<Input type="number" value={draft.dxcc ?? ''} onChange={(e) => set('dxcc', intOrUndef(e.target.value) as any)} className="font-mono w-16 text-center bg-muted/40" title="DXCC entity #" />
<Input type="number" value={draft.dxcc ?? ''} readOnly tabIndex={-1} className="font-mono w-16 text-center bg-muted/60 text-muted-foreground cursor-not-allowed" title="DXCC entity # — set automatically from Country" />
{flagURL(draft.dxcc) && <img src={flagURL(draft.dxcc)} alt="" className="h-4 rounded-[2px] border border-border/50" referrerPolicy="no-referrer" />}
</div>
<div className="flex items-center gap-2">
@@ -368,11 +385,6 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose }: Props) {
</div>
<div><Label>Comment</Label><Input value={draft.comment ?? ''} onChange={(e) => set('comment', e.target.value)} /></div>
<div><Label>Note</Label><Textarea rows={3} value={draft.notes ?? ''} onChange={(e) => set('notes', e.target.value)} /></div>
<div><Label>Contest</Label><Input value={draft.contest_id ?? ''} onChange={(e) => set('contest_id', e.target.value)} /></div>
<div className="flex items-end gap-2">
<div className="flex flex-col flex-1"><Label>Sent</Label><Input value={draft.stx_string ?? (draft.stx != null ? String(draft.stx) : '')} onChange={(e) => set('stx_string', e.target.value)} /></div>
<div className="flex flex-col flex-1"><Label>Received</Label><Input value={draft.srx_string ?? (draft.srx != null ? String(draft.srx) : '')} onChange={(e) => set('srx_string', e.target.value)} /></div>
</div>
</div>
</div>
</TabsContent>
@@ -495,7 +507,7 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose }: Props) {
<F label="Operator" span={3}><Input value={draft.operator ?? ''} onChange={(e) => set('operator', e.target.value)} /></F>
<F label="My grid"><Input value={draft.my_grid ?? ''} onChange={(e) => set('my_grid', e.target.value)} /></F>
<F label="Grid ext"><Input value={draft.my_gridsquare_ext ?? ''} onChange={(e) => set('my_gridsquare_ext', e.target.value)} /></F>
<F label="Country" span={2}><Input value={draft.my_country ?? ''} onChange={(e) => set('my_country', e.target.value)} /></F>
<F label="Country" span={2}><Combobox value={draft.my_country ?? ''} options={countries} placeholder="Country" onChange={(v) => set('my_country', v)} /></F>
<F label="State"><Input value={draft.my_state ?? ''} onChange={(e) => set('my_state', e.target.value)} /></F>
<F label="County"><Input value={draft.my_cnty ?? ''} onChange={(e) => set('my_cnty', e.target.value)} /></F>
<F label="DXCC"><Input type="number" value={draft.my_dxcc ?? ''} onChange={(e) => set('my_dxcc', intOrUndef(e.target.value) as any)} /></F>
@@ -514,13 +526,6 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose }: Props) {
</div>
</TabsContent>
<TabsContent value="notes" className="mt-0">
<div className="grid grid-cols-6 gap-3">
<F label="Comment" span={6}><Input value={draft.comment ?? ''} onChange={(e) => set('comment', e.target.value)} /></F>
<F label="Notes" span={6}><Textarea rows={6} value={draft.notes ?? ''} onChange={(e) => set('notes', e.target.value)} /></F>
</div>
</TabsContent>
<TabsContent value="extras" className="mt-0 space-y-2">
<p className="text-xs text-muted-foreground">
ADIF fields not promoted to first-class columns. One per line:{' '}
+16 -9
View File
@@ -49,6 +49,7 @@ type Props = {
onRowSelected?: (id: number | null) => void;
onUpdateFromCty?: (ids: number[]) => void;
onUpdateFromQRZ?: (ids: number[]) => void;
onSendTo?: (service: string, ids: number[]) => void;
};
const COL_STATE_KEY = 'hamlog.qsoColState.v2';
@@ -141,14 +142,19 @@ export const COL_CATALOG: ColEntry[] = [
{ group: 'eQSL', label: 'eQSL sent date', colId: 'eqsl_sent_date', headerName: 'eQSL S date', field: 'eqsl_sent_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },
{ group: 'eQSL', label: 'eQSL rcvd date', colId: 'eqsl_rcvd_date', headerName: 'eQSL R date', field: 'eqsl_rcvd_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },
// ── Uploads ──
{ group: 'Uploads', label: 'ClubLog upload date', colId: 'clublog_qso_upload_date', headerName: 'ClubLog date', field: 'clublog_qso_upload_date' as any, width: 130, valueFormatter: (p) => fmtDateOnly(p.value) },
{ group: 'Uploads', label: 'ClubLog upload status', colId: 'clublog_qso_upload_status', headerName: 'ClubLog status', field: 'clublog_qso_upload_status' as any, width: 110 },
{ group: 'Uploads', label: 'HRDLog upload date', colId: 'hrdlog_qso_upload_date', headerName: 'HRDLog date', field: 'hrdlog_qso_upload_date' as any, width: 130, valueFormatter: (p) => fmtDateOnly(p.value) },
{ group: 'Uploads', label: 'HRDLog upload status', colId: 'hrdlog_qso_upload_status', headerName: 'HRDLog status', field: 'hrdlog_qso_upload_status' as any, width: 110 },
{ group: 'Uploads', label: 'QRZ.com upload date', colId: 'qrzcom_qso_upload_date', headerName: 'QRZ.com date', field: 'qrzcom_qso_upload_date' as any, width: 130, valueFormatter: (p) => fmtDateOnly(p.value) },
{ group: 'Uploads', label: 'QRZ.com upload status', colId: 'qrzcom_qso_upload_status', headerName: 'QRZ.com status', field: 'qrzcom_qso_upload_status' as any, width: 110 },
{ group: 'Uploads', label: 'QRZ.com confirmed', colId: 'qrzcom_qso_download_status', headerName: 'QRZ.com cfm', field: 'qrzcom_qso_download_status' as any, width: 100 },
// ── Uploads (online logbooks) ──
// ADIF models these as an "upload status/date" (= YOU pushed the QSO) and,
// for QRZ only, a "download status/date" (= it came back confirmed). We
// relabel to the same sent/rcvd wording as LoTW/eQSL. Club Log & HRDLog have
// NO rcvd field in ADIF — they're upload-only, so only "sent" is shown.
{ group: 'Uploads', label: 'ClubLog sent', colId: 'clublog_qso_upload_status', headerName: 'ClubLog sent', field: 'clublog_qso_upload_status' as any, width: 100 },
{ group: 'Uploads', label: 'ClubLog sent date', colId: 'clublog_qso_upload_date', headerName: 'ClubLog S date', field: 'clublog_qso_upload_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },
{ group: 'Uploads', label: 'HRDLog sent', colId: 'hrdlog_qso_upload_status', headerName: 'HRDLog sent', field: 'hrdlog_qso_upload_status' as any, width: 100 },
{ group: 'Uploads', label: 'HRDLog sent date', colId: 'hrdlog_qso_upload_date', headerName: 'HRDLog S date', field: 'hrdlog_qso_upload_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },
{ group: 'Uploads', label: 'QRZ.com sent', colId: 'qrzcom_qso_upload_status', headerName: 'QRZ.com sent', field: 'qrzcom_qso_upload_status' as any, width: 100 },
{ group: 'Uploads', label: 'QRZ.com rcvd', colId: 'qrzcom_qso_download_status', headerName: 'QRZ.com rcvd', field: 'qrzcom_qso_download_status' as any, width: 100 },
{ group: 'Uploads', label: 'QRZ.com sent date', colId: 'qrzcom_qso_upload_date', headerName: 'QRZ.com S date', field: 'qrzcom_qso_upload_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },
{ group: 'Uploads', label: 'QRZ.com rcvd date', colId: 'qrzcom_qso_download_date', headerName: 'QRZ.com R date', field: 'qrzcom_qso_download_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },
// ── Contest ──
{ group: 'Contest', label: 'Contest ID', colId: 'contest_id', headerName: 'Contest', field: 'contest_id' as any, width: 110 },
@@ -201,7 +207,7 @@ export const GROUP_ORDER = [
'Contest', 'Propagation', 'My station', 'Misc',
];
export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ }: Props) {
export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onSendTo }: Props) {
const gridRef = useRef<any>(null);
const [pickerOpen, setPickerOpen] = useState(false);
const [menu, setMenu] = useState<QSOMenuState>(null);
@@ -344,6 +350,7 @@ export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpda
onClose={() => setMenu(null)}
onUpdateFromCty={(ids) => onUpdateFromCty?.(ids)}
onUpdateFromQRZ={(ids) => onUpdateFromQRZ?.(ids)}
onSendTo={onSendTo}
/>
<Dialog open={pickerOpen} onOpenChange={setPickerOpen}>
+6 -6
View File
@@ -173,7 +173,7 @@ const TREE: TreeNode[] = [
{ kind: 'item', label: 'Modes & default RST', id: 'lists-modes' },
]},
{ kind: 'item', label: 'DX Cluster', id: 'cluster' },
{ kind: 'item', label: 'UDP integrations (WSJT-X, JTDX, MSHV…)', id: 'udp' },
{ kind: 'item', label: 'UDP integrations', id: 'udp' },
{ kind: 'item', label: 'Database', id: 'database' },
{ kind: 'item', label: 'Awards', id: 'awards', disabled: true },
],
@@ -1862,13 +1862,13 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
<div className="border-t border-border/60 pt-3 space-y-3">
<div className="text-[11px] text-muted-foreground">
Upload status fields (Clublog / HRDLog / QRZ.com) are stamped on every QSO; "N" is typical when you want OpsLog to track which QSOs still need to be uploaded.
"Sent" = the QSO's upload status to the service; "N" is typical so OpsLog tracks which QSOs still need uploading. Club Log &amp; HRDLog are upload-only (no "rcvd" in ADIF); QRZ.com also has "Rcvd" = confirmed back.
</div>
{/* Clublog */}
<div className="grid grid-cols-[150px_1fr_1fr] gap-3 items-end">
<Label className="text-sm font-medium pb-1.5">Clublog</Label>
<div>
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Upload status</Label>
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Sent</Label>
{renderSelect('clublog_status', FULL_OPTIONS)}
</div>
<div />
@@ -1877,7 +1877,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
<div className="grid grid-cols-[150px_1fr_1fr] gap-3 items-end">
<Label className="text-sm font-medium pb-1.5">HRDLog</Label>
<div>
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Upload status</Label>
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Sent</Label>
{renderSelect('hrdlog_status', FULL_OPTIONS)}
</div>
<div />
@@ -1886,11 +1886,11 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
<div className="grid grid-cols-[150px_1fr_1fr] gap-3 items-end">
<Label className="text-sm font-medium pb-1.5">QRZ.com</Label>
<div>
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Upload status</Label>
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Sent</Label>
{renderSelect('qrzcom_status', FULL_OPTIONS)}
</div>
<div>
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Confirmed</Label>
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Rcvd</Label>
{renderSelect('qrzcom_confirmed', FULL_OPTIONS)}
</div>
</div>
+3 -1
View File
@@ -49,6 +49,7 @@ type Props = {
onRowDoubleClicked?: (q: QSOForm) => void;
onUpdateFromCty?: (ids: number[]) => void;
onUpdateFromQRZ?: (ids: number[]) => void;
onSendTo?: (service: string, ids: number[]) => void;
};
const COL_STATE_KEY = 'hamlog.workedBeforeColState.v2';
@@ -61,7 +62,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 }: Props) {
export function WorkedBeforeGrid({ wb, busy, currentCall, onRowDoubleClicked, onUpdateFromCty, onUpdateFromQRZ, onSendTo }: Props) {
const gridRef = useRef<any>(null);
const [pickerOpen, setPickerOpen] = useState(false);
const [menu, setMenu] = useState<QSOMenuState>(null);
@@ -232,6 +233,7 @@ export function WorkedBeforeGrid({ wb, busy, currentCall, onRowDoubleClicked, on
onClose={() => setMenu(null)}
onUpdateFromCty={(ids) => onUpdateFromCty?.(ids)}
onUpdateFromQRZ={(ids) => onUpdateFromQRZ?.(ids)}
onSendTo={onSendTo}
/>
{count > entries.length && (
+2
View File
@@ -30,6 +30,8 @@ export function CountQSO():Promise<number>;
export function CreateDatabase(arg1:string):Promise<void>;
export function DXCCForCountry(arg1:string):Promise<number>;
export function DeleteAllQSO():Promise<number>;
export function DeleteClusterServer(arg1:number):Promise<void>;
+4
View File
@@ -38,6 +38,10 @@ export function CreateDatabase(arg1) {
return window['go']['main']['App']['CreateDatabase'](arg1);
}
export function DXCCForCountry(arg1) {
return window['go']['main']['App']['DXCCForCountry'](arg1);
}
export function DeleteAllQSO() {
return window['go']['main']['App']['DeleteAllQSO']();
}