bug
This commit is contained in:
+57
-12
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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:{' '}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Vendored
+2
@@ -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>;
|
||||
|
||||
@@ -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']();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user