This commit is contained in:
2026-06-06 14:16:30 +02:00
parent f91f9ff3b8
commit 17f7a00bd7
19 changed files with 1278 additions and 91 deletions
+87 -34
View File
@@ -2,7 +2,8 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { Trash2, Search, Loader2 } from 'lucide-react';
import { LookupCallsign, DXCCForCountry, GetAwardDefs, ComputeQSOAwardRefs } from '../../wailsjs/go/main/App';
import { AwardRefSelector } from '@/components/AwardRefSelector';
import { applyAwardRefs, buildAwardRefs } from '@/lib/awardRefs';
import { AdifExtrasEditor } from '@/components/AdifExtrasEditor';
import { applyAwardRefs } from '@/lib/awardRefs';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';
@@ -100,23 +101,6 @@ function parseLocalISO(s: string): string | null {
if (!m) return null;
return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:00.000Z`;
}
function stringifyExtras(e?: Record<string, string>): string {
if (!e) return '';
return Object.entries(e).map(([k, v]) => `${k} = ${v}`).join('\n');
}
function parseExtras(t: string): Record<string, string> | undefined {
const out: Record<string, string> = {};
for (const raw of t.split('\n')) {
const line = raw.trim();
if (!line) continue;
const idx = line.indexOf('=');
if (idx < 0) continue;
const k = line.slice(0, idx).trim().toUpperCase();
const v = line.slice(idx + 1).trim();
if (k && v) out[k] = v;
}
return Object.keys(out).length ? out : undefined;
}
function numOrUndef(v: any): number | undefined {
if (v === '' || v === null || v === undefined) return undefined;
const n = typeof v === 'number' ? v : parseFloat(String(v));
@@ -163,7 +147,6 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [] }:
const [dateOff, setDateOff] = useState(toLocalISO(draft.qso_date_off));
const [endEnabled, setEndEnabled] = useState(!!draft.qso_date_off);
const [confSel, setConfSel] = useState('QSL'); // selected confirmation channel
const [extrasText, setExtrasText] = useState(stringifyExtras(draft.extras));
const [localErr, setLocalErr] = useState('');
const [saving, setSaving] = useState(false);
const [looking, setLooking] = useState(false);
@@ -183,15 +166,17 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [] }:
const fieldOf: Record<string, string> = {};
for (const d of list) fieldOf[String(d.code).toUpperCase()] = String(d.field || '').toLowerCase();
awardFieldRef.current = fieldOf;
// Which awards are reference-list (manual) ones? Ask the backend, which
// also tells us pickable vs computed for the current QSO.
// Seed the editable manual refs from the backend, which already matched
// each reference against its award's own list. Seeding from the raw QSO
// field instead would wrongly seed every state-award (WAS/RAC/WAJA) from
// the same `state` value — e.g. a US "CA" would seed RAC@CA too.
try {
const all = (await ComputeQSOAwardRefs(draft as any)) ?? [];
const pickableCodes = new Set(all.filter((r: any) => r.pickable).map((r: any) => String(r.code).toUpperCase()));
const pickable = list
.filter((d) => pickableCodes.has(String(d.code).toUpperCase()))
.map((d) => ({ code: String(d.code), field: String(d.field || '').toLowerCase() }));
setAwardRefs(buildAwardRefs(draft, pickable));
const seed = all
.filter((r: any) => r.pickable)
.map((r: any) => `${String(r.code).toUpperCase()}@${String(r.ref).toUpperCase()}`)
.join(';');
setAwardRefs(seed);
} catch { /* leave manual refs empty on failure */ }
})
.catch(() => {});
@@ -292,7 +277,12 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [] }:
my_lat: numOrUndef(draft.my_lat), my_lon: numOrUndef(draft.my_lon),
ant_az: numOrUndef(draft.ant_az), ant_el: numOrUndef(draft.ant_el),
tx_pwr: numOrUndef(draft.tx_pwr),
extras: parseExtras(extrasText),
distance: numOrUndef(draft.distance),
rx_pwr: numOrUndef(draft.rx_pwr),
a_index: numOrUndef(draft.a_index),
k_index: numOrUndef(draft.k_index),
sfi: numOrUndef(draft.sfi),
extras: draft.extras && Object.keys(draft.extras).length ? draft.extras : undefined,
};
// The Award Refs tab is authoritative for the reference-list awards. Reset
// the dedicated columns, then route the picked refs back onto the payload
@@ -334,8 +324,9 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [] }:
<TabsTrigger value="contest">Contest</TabsTrigger>
<TabsTrigger value="sat">Sat / Prop</TabsTrigger>
<TabsTrigger value="mystation">My Station</TabsTrigger>
<TabsTrigger value="moreadif">More ADIF</TabsTrigger>
<TabsTrigger value="extras">
Extras
ADIF fields
{extrasCount > 0 && (
<Badge variant="accent" className="ml-1 px-1.5 py-0 text-[9px]">{extrasCount}</Badge>
)}
@@ -602,12 +593,74 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [] }:
</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:{' '}
<code className="bg-muted px-1 py-0.5 rounded font-mono">FIELD_NAME = value</code>
</p>
<Textarea rows={14} className="font-mono text-xs" value={extrasText} onChange={(e) => setExtrasText(e.target.value)} />
<TabsContent value="moreadif" className="mt-0 space-y-4">
{/* Special activity (POTA/SOTA/WWFF/SIG) */}
<div>
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">Special activity</p>
<div className="grid grid-cols-6 gap-3">
<F label="SIG"><Input value={draft.sig ?? ''} placeholder="POTA" onChange={(e) => set('sig', e.target.value)} /></F>
<F label="SIG info" span={2}><Input value={draft.sig_info ?? ''} placeholder="US-0001" onChange={(e) => set('sig_info', e.target.value)} /></F>
<F label="WWFF ref" span={2}><Input value={draft.wwff_ref ?? ''} placeholder="ONFF-0001" onChange={(e) => set('wwff_ref', e.target.value)} className="font-mono uppercase" /></F>
<F label="Region"><Input value={draft.region ?? ''} onChange={(e) => set('region', e.target.value)} /></F>
</div>
</div>
{/* Power & propagation */}
<div>
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">Power &amp; space weather</p>
<div className="grid grid-cols-6 gap-3">
<F label="RX power (W)"><Input type="number" value={draft.rx_pwr ?? ''} onChange={(e) => set('rx_pwr', numOrUndef(e.target.value) as any)} /></F>
<F label="Distance (km)"><Input type="number" value={draft.distance ?? ''} onChange={(e) => set('distance', numOrUndef(e.target.value) as any)} /></F>
<F label="A index"><Input type="number" value={draft.a_index ?? ''} onChange={(e) => set('a_index', numOrUndef(e.target.value) as any)} /></F>
<F label="K index"><Input type="number" value={draft.k_index ?? ''} onChange={(e) => set('k_index', numOrUndef(e.target.value) as any)} /></F>
<F label="SFI"><Input type="number" value={draft.sfi ?? ''} onChange={(e) => set('sfi', numOrUndef(e.target.value) as any)} /></F>
</div>
</div>
{/* Identity & clubs */}
<div>
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">Identity &amp; clubs</p>
<div className="grid grid-cols-6 gap-3">
<F label="Contacted op" span={2}><Input value={draft.contacted_op ?? ''} placeholder="EA8XYZ" onChange={(e) => set('contacted_op', e.target.value)} className="font-mono uppercase" /></F>
<F label="Former call (EQ_CALL)" span={2}><Input value={draft.eq_call ?? ''} onChange={(e) => set('eq_call', e.target.value)} className="font-mono uppercase" /></F>
<F label="Class"><Input value={draft.class ?? ''} placeholder="1A" onChange={(e) => set('class', e.target.value)} /></F>
<F label="SKCC"><Input value={draft.skcc ?? ''} onChange={(e) => set('skcc', e.target.value)} /></F>
<F label="FISTS"><Input value={draft.fists ?? ''} onChange={(e) => set('fists', e.target.value)} /></F>
<F label="Ten-Ten"><Input value={draft.ten_ten ?? ''} onChange={(e) => set('ten_ten', e.target.value)} /></F>
<F label="DARC DOK"><Input value={draft.darc_dok ?? ''} onChange={(e) => set('darc_dok', e.target.value)} /></F>
</div>
</div>
{/* Flags & credits */}
<div>
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">Flags &amp; credits</p>
<div className="grid grid-cols-6 gap-3">
<F label="QSO complete"><Input value={draft.qso_complete ?? ''} placeholder="Y/N/NIL/?" onChange={(e) => set('qso_complete', e.target.value)} /></F>
<F label="QSO random"><Input value={draft.qso_random ?? ''} placeholder="Y/N" onChange={(e) => set('qso_random', e.target.value)} /></F>
<F label="Silent key"><Input value={draft.silent_key ?? ''} placeholder="Y/N" onChange={(e) => set('silent_key', e.target.value)} /></F>
<F label="SWL"><Input value={draft.swl ?? ''} placeholder="Y/N" onChange={(e) => set('swl', e.target.value)} /></F>
<F label="Credit granted" span={3}><Input value={draft.credit_granted ?? ''} placeholder="DXCC,WAS" onChange={(e) => set('credit_granted', e.target.value)} /></F>
<F label="Credit submitted" span={3}><Input value={draft.credit_submitted ?? ''} onChange={(e) => set('credit_submitted', e.target.value)} /></F>
</div>
</div>
{/* My station extras */}
<div>
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">My station (ADIF)</p>
<div className="grid grid-cols-6 gap-3">
<F label="My name" span={2}><Input value={draft.my_name ?? ''} onChange={(e) => set('my_name', e.target.value)} /></F>
<F label="My WWFF ref" span={2}><Input value={draft.my_wwff_ref ?? ''} onChange={(e) => set('my_wwff_ref', e.target.value)} className="font-mono uppercase" /></F>
<F label="My ARRL sect" span={2}><Input value={draft.my_arrl_sect ?? ''} onChange={(e) => set('my_arrl_sect', e.target.value)} /></F>
<F label="My SIG"><Input value={draft.my_sig ?? ''} onChange={(e) => set('my_sig', e.target.value)} /></F>
<F label="My SIG info" span={2}><Input value={draft.my_sig_info ?? ''} onChange={(e) => set('my_sig_info', e.target.value)} /></F>
<F label="My DARC DOK"><Input value={draft.my_darc_dok ?? ''} onChange={(e) => set('my_darc_dok', e.target.value)} /></F>
<F label="My VUCC grids" span={2}><Input value={draft.my_vucc_grids ?? ''} onChange={(e) => set('my_vucc_grids', e.target.value)} className="font-mono uppercase" /></F>
</div>
</div>
</TabsContent>
<TabsContent value="extras" className="mt-0">
<AdifExtrasEditor value={draft.extras} onChange={(next) => set('extras', next as any)} />
</TabsContent>
</div>
</Tabs>