up
This commit is contained in:
@@ -2861,7 +2861,7 @@ export default function App() {
|
||||
<DialogHeader className="px-2">
|
||||
<DialogTitle>Export ADIF</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose which fields to include in the export.
|
||||
Choose which fields to include. OpsLog writes ADIF 3.1.7.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="px-2 py-1 space-y-2.5">
|
||||
@@ -2870,10 +2870,10 @@ export default function App() {
|
||||
onClick={() => runExport(false)}
|
||||
className="w-full text-left rounded-lg border border-border p-3 hover:border-primary/60 hover:bg-accent/30 transition-colors"
|
||||
>
|
||||
<div className="font-semibold text-sm">Standard ADIF</div>
|
||||
<div className="font-semibold text-sm">Standard ADIF only</div>
|
||||
<div className="text-xs text-muted-foreground mt-0.5">
|
||||
Only standard ADIF-defined fields — portable to other loggers (Log4OM, N1MM, LoTW…).
|
||||
Application-specific <span className="font-mono">APP_*</span> tags are stripped.
|
||||
Only fields defined in the ADIF 3.1.7 spec — portable to other loggers (Log4OM, N1MM, LoTW…).
|
||||
Application-specific <span className="font-mono">APP_*</span> and any non-standard / vendor tags are stripped.
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
@@ -2881,10 +2881,10 @@ export default function App() {
|
||||
onClick={() => runExport(true)}
|
||||
className="w-full text-left rounded-lg border border-border p-3 hover:border-primary/60 hover:bg-accent/30 transition-colors"
|
||||
>
|
||||
<div className="font-semibold text-sm">Full (OpsLog round-trip)</div>
|
||||
<div className="font-semibold text-sm">All fields (OpsLog round-trip)</div>
|
||||
<div className="text-xs text-muted-foreground mt-0.5">
|
||||
Every field including OpsLog/application-specific <span className="font-mono">APP_*</span> tags —
|
||||
for a lossless backup you'll re-import into OpsLog.
|
||||
Every field including application-specific <span className="font-mono">APP_*</span> and vendor tags —
|
||||
a lossless backup you'll re-import into OpsLog.
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { X, Plus } from 'lucide-react';
|
||||
import { ADIFFields } from '../../wailsjs/go/main/App';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
type FieldDef = {
|
||||
name: string; kind: string; category: string;
|
||||
promoted: boolean; deprecated: boolean; intl: boolean;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
// The QSO's extras map (uppercase ADIF tag → raw value).
|
||||
value: Record<string, string> | undefined;
|
||||
onChange: (next: Record<string, string> | undefined) => void;
|
||||
}
|
||||
|
||||
// AdifExtrasEditor — a dictionary-driven editor for every ADIF field that is
|
||||
// NOT promoted to a first-class column. Backed by the QSO's extras map, so any
|
||||
// ADIF 3.1.7 field (plus custom/vendor tags) can be viewed and edited. This is
|
||||
// what makes "100% of ADIF fields available" true without 160 DB columns.
|
||||
export function AdifExtrasEditor({ value, onChange }: Props) {
|
||||
const [dict, setDict] = useState<FieldDef[]>([]);
|
||||
const [showDeprecated, setShowDeprecated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
ADIFFields().then((f) => setDict((f ?? []) as any)).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Entries currently set, sorted for stable display.
|
||||
const entries = useMemo(
|
||||
() => Object.entries(value ?? {}).sort((a, b) => a[0].localeCompare(b[0])),
|
||||
[value],
|
||||
);
|
||||
|
||||
// Addable fields: standard non-promoted ADIF fields not already present.
|
||||
// Deprecated (import-only) fields are hidden unless the toggle is on.
|
||||
const addable = useMemo(() => {
|
||||
const have = new Set(Object.keys(value ?? {}));
|
||||
return dict
|
||||
.filter((f) => !f.promoted && !have.has(f.name) && (showDeprecated || !f.deprecated))
|
||||
.map((f) => f.name);
|
||||
}, [dict, value, showDeprecated]);
|
||||
|
||||
const meta = useMemo(() => {
|
||||
const m: Record<string, FieldDef> = {};
|
||||
for (const f of dict) m[f.name] = f;
|
||||
return m;
|
||||
}, [dict]);
|
||||
|
||||
function setKV(key: string, val: string) {
|
||||
const next = { ...(value ?? {}) };
|
||||
next[key] = val;
|
||||
onChange(next);
|
||||
}
|
||||
function remove(key: string) {
|
||||
const next = { ...(value ?? {}) };
|
||||
delete next[key];
|
||||
onChange(Object.keys(next).length ? next : undefined);
|
||||
}
|
||||
function addField(name: string) {
|
||||
const key = name.trim().toUpperCase();
|
||||
if (!key || (value && key in value)) return;
|
||||
setKV(key, '');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Every ADIF 3.1.7 field not shown in the other tabs. Pick a field to add it,
|
||||
or type a custom/vendor tag (e.g. <code className="bg-muted px-1 rounded font-mono">APP_*</code>).
|
||||
Stored losslessly and exported in the <strong>full</strong> ADIF mode.
|
||||
</p>
|
||||
|
||||
{/* Add a field */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 max-w-xs">
|
||||
<Combobox
|
||||
value=""
|
||||
options={addable}
|
||||
placeholder="Add ADIF field…"
|
||||
allowFreeText
|
||||
onChange={addField}
|
||||
/>
|
||||
</div>
|
||||
<label className="flex items-center gap-1.5 text-[11px] text-muted-foreground cursor-pointer">
|
||||
<input type="checkbox" checked={showDeprecated} onChange={(e) => setShowDeprecated(e.target.checked)} />
|
||||
Show deprecated
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Current entries */}
|
||||
{entries.length === 0 ? (
|
||||
<div className="text-[11px] text-muted-foreground italic border border-dashed border-border rounded-md px-3 py-4 text-center">
|
||||
No extra ADIF fields. Use the picker above to add one.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{entries.map(([k, v]) => {
|
||||
const def = meta[k];
|
||||
return (
|
||||
<div key={k} className="flex items-center gap-2">
|
||||
<div className="w-48 shrink-0">
|
||||
<span className="font-mono text-xs font-semibold">{k}</span>
|
||||
{def && (
|
||||
<span className="block text-[10px] text-muted-foreground leading-tight">
|
||||
{def.category}{def.deprecated ? ' · deprecated' : ''}{def.intl ? ' · intl' : ''}
|
||||
{!def && ''}
|
||||
</span>
|
||||
)}
|
||||
{!def && <span className="block text-[10px] text-amber-600 leading-tight">non-standard</span>}
|
||||
</div>
|
||||
<Input
|
||||
className="flex-1 h-8 text-xs font-mono"
|
||||
value={v}
|
||||
onChange={(e) => setKV(k, e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
type="button" variant="ghost" size="icon" className="h-7 w-7 shrink-0 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => remove(k)} title="Remove field"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Plus, Trash2, RotateCcw, Save, Download, Loader2, Search } from 'lucide-react';
|
||||
import { Plus, Trash2, RotateCcw, Save, Download, Upload, Loader2, Search } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
ImportAwardReferencesText, GetAwardPresets, ApplyAwardPreset,
|
||||
ListCountries, DXCCForCountry, DXCCName,
|
||||
PopulateBuiltinReferences, HasBuiltinReferences,
|
||||
ExportAwards, ImportAwards,
|
||||
} from '../../wailsjs/go/main/App';
|
||||
|
||||
// Above this many references the editor stops loading the whole list and
|
||||
@@ -182,6 +183,28 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
|
||||
async function reset() {
|
||||
try { setDefs((await ResetAwardDefs()) as any); setSel(0); } catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}
|
||||
// Export every award definition + reference list to a JSON bundle (backup
|
||||
// that survives a reinstall / PC change, independent of the database).
|
||||
async function exportAwards() {
|
||||
setErr('');
|
||||
try {
|
||||
const p = await ExportAwards();
|
||||
if (p) setErr(`Awards exported to:\n${p}`);
|
||||
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}
|
||||
// Import an award bundle: definitions are upserted by code, reference lists
|
||||
// replaced. Reloads the editor afterwards.
|
||||
async function importAwards() {
|
||||
setErr('');
|
||||
try {
|
||||
const r: any = await ImportAwards();
|
||||
if (!r || (!r.awards && !r.references)) return; // cancelled
|
||||
const [d] = await Promise.all([GetAwardDefs(), loadMeta()]);
|
||||
setDefs((d ?? []) as any); setSel(0);
|
||||
onSaved();
|
||||
setErr(`Imported ${r.awards} award(s) and ${r.references} reference(s).`);
|
||||
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}
|
||||
async function updateList(code: string) {
|
||||
setUpdating(code); setErr('');
|
||||
try { await UpdateAwardReferenceList(code); await loadMeta(); }
|
||||
@@ -228,7 +251,7 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
|
||||
|
||||
{/* Right: tabbed editor for selected award */}
|
||||
<div className="flex flex-col min-h-0 overflow-hidden">
|
||||
{err && <div className="mx-4 mt-3 text-xs text-destructive bg-destructive/10 border border-destructive/30 rounded px-3 py-1.5">{err}</div>}
|
||||
{err && <div className="mx-4 mt-3 text-xs text-destructive bg-destructive/10 border border-destructive/30 rounded px-3 py-1.5 whitespace-pre-line break-all">{err}</div>}
|
||||
{!cur ? (
|
||||
<div className="flex-1 grid place-items-center text-sm text-muted-foreground">Select or create an award.</div>
|
||||
) : (
|
||||
@@ -338,6 +361,12 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
|
||||
|
||||
<DialogFooter className="px-5 py-3 border-t !flex-row">
|
||||
<Button variant="ghost" onClick={reset}><RotateCcw className="size-3.5 mr-1" /> Reset to defaults</Button>
|
||||
<Button variant="outline" onClick={exportAwards} title="Export all award definitions + reference lists to a JSON backup">
|
||||
<Download className="size-3.5 mr-1" /> Export…
|
||||
</Button>
|
||||
<Button variant="outline" onClick={importAwards} title="Import an award bundle (definitions + reference lists)">
|
||||
<Upload className="size-3.5 mr-1" /> Import…
|
||||
</Button>
|
||||
<div className="flex-1" />
|
||||
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
||||
<Button onClick={save}><Save className="size-3.5 mr-1" /> Save</Button>
|
||||
|
||||
@@ -11,7 +11,11 @@ type Meta = { code: string; count: number; can_update: boolean };
|
||||
|
||||
// Fields auto-derived from structured QSO data — their awards (DXCC/WAZ/WAS/…)
|
||||
// are computed, never manually picked, so they don't belong in this picker.
|
||||
const COMPUTED_FIELDS = new Set(['dxcc', 'cqz', 'ituz', 'prefix', 'callsign', 'state', 'cont', 'country', 'grid']);
|
||||
// Fields purely derived from the callsign / cty.dat — their awards are computed,
|
||||
// never picked. NB: 'state' and 'cnty' are NOT here — they're operator-settable
|
||||
// QSO fields driving predefined-list awards (WAS/RAC/WAJA/JCC), so they ARE
|
||||
// pickable (a lookup rarely fills the JA prefecture or VE province).
|
||||
const COMPUTED_FIELDS = new Set(['dxcc', 'cqz', 'ituz', 'prefix', 'callsign', 'cont', 'country', 'grid']);
|
||||
|
||||
// If DXCC-filtered auto-results exceed this, require the user to type instead.
|
||||
const AUTO_SHOW_MAX = 100;
|
||||
@@ -72,15 +76,26 @@ export function AwardRefSelector({ dxcc, value, onChange }: Props) {
|
||||
if (!awards.find((a) => a.code === awardCode)) setAwardCode(awards[0].code);
|
||||
}, [awards, awardCode]);
|
||||
|
||||
// Auto-load DXCC-filtered refs on award/dxcc change with empty query.
|
||||
// Fetches AUTO_SHOW_MAX+1 so we can distinguish "all results shown" from "too many".
|
||||
// Is the selected award a giant dynamic list (POTA/SOTA/IOTA)? Those carry a
|
||||
// per-reference DXCC so we filter by entity; predefined lists (WAS/RAC/WAJA)
|
||||
// are small and their refs may lack a per-ref DXCC, so we load them whole.
|
||||
const isDynamic = useMemo(
|
||||
() => !!defs.find((d) => d.code === awardCode)?.dynamic,
|
||||
[defs, awardCode],
|
||||
);
|
||||
// For dynamic lists, restrict to the contacted entity; otherwise load all.
|
||||
const refDxcc = isDynamic ? (dxcc ?? 0) : 0;
|
||||
|
||||
// Auto-load refs on award/dxcc change with empty query. Fetches AUTO_SHOW_MAX+1
|
||||
// so we can distinguish "all results shown" from "too many to list".
|
||||
useEffect(() => {
|
||||
setAutoResults([]);
|
||||
if (!dxcc) return;
|
||||
SearchAwardReferences(awardCode, '', dxcc, AUTO_SHOW_MAX + 1)
|
||||
// Dynamic lists need an entity to scope to; predefined lists load regardless.
|
||||
if (isDynamic && !dxcc) return;
|
||||
SearchAwardReferences(awardCode, '', refDxcc, AUTO_SHOW_MAX + 1)
|
||||
.then((r) => setAutoResults((r ?? []) as any))
|
||||
.catch(() => {});
|
||||
}, [awardCode, dxcc]);
|
||||
}, [awardCode, dxcc, isDynamic, refDxcc]);
|
||||
|
||||
// Typed search (2+ chars).
|
||||
useEffect(() => {
|
||||
@@ -88,13 +103,13 @@ export function AwardRefSelector({ dxcc, value, onChange }: Props) {
|
||||
const t = window.setTimeout(async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
const r = await SearchAwardReferences(awardCode, q, dxcc ?? 0, 50);
|
||||
const r = await SearchAwardReferences(awardCode, q, refDxcc, 50);
|
||||
setSearchResults((r ?? []) as any);
|
||||
} catch { setSearchResults([]); }
|
||||
finally { setBusy(false); }
|
||||
}, 200);
|
||||
return () => window.clearTimeout(t);
|
||||
}, [awardCode, q, dxcc]);
|
||||
}, [awardCode, q, refDxcc]);
|
||||
|
||||
const tooManyAuto = autoResults.length > AUTO_SHOW_MAX;
|
||||
// When q is empty/short: show auto-results (if ≤ AUTO_SHOW_MAX); when typing: show search results.
|
||||
@@ -212,22 +227,19 @@ export function AwardRefSelector({ dxcc, value, onChange }: Props) {
|
||||
<Loader2 className="size-3 animate-spin" />Searching…
|
||||
</div>
|
||||
)}
|
||||
{/* No callsign yet */}
|
||||
{!busy && !dxcc && q.length < 2 && (
|
||||
<div className="px-2 py-1.5 text-[11px] text-muted-foreground leading-snug">
|
||||
Enter a callsign, or type to search.
|
||||
</div>
|
||||
)}
|
||||
{/* DXCC known but too many auto-results → require typed search */}
|
||||
{!busy && !!dxcc && q.length < 2 && tooManyAuto && (
|
||||
{/* Too many auto-results → require typed search */}
|
||||
{!busy && q.length < 2 && tooManyAuto && (
|
||||
<div className="px-2 py-1.5 text-[11px] text-muted-foreground leading-snug">
|
||||
Type 2+ chars to search
|
||||
</div>
|
||||
)}
|
||||
{/* DXCC known, auto-results loaded, none found */}
|
||||
{!busy && !!dxcc && q.length < 2 && !tooManyAuto && autoResults.length === 0 && (
|
||||
{/* Empty short-query state: prompt for a callsign (dynamic lists) or
|
||||
note the list is empty (predefined awards with no references). */}
|
||||
{!busy && q.length < 2 && !tooManyAuto && autoResults.length === 0 && (
|
||||
<div className="px-2 py-1.5 text-[11px] text-muted-foreground leading-snug">
|
||||
No references for this entity.
|
||||
{isDynamic && !dxcc
|
||||
? 'Enter a callsign, or type to search.'
|
||||
: 'No references for this entity.'}
|
||||
</div>
|
||||
)}
|
||||
{/* Typed search, no results */}
|
||||
|
||||
@@ -120,7 +120,7 @@ export function AwardsPanel() {
|
||||
const filteredRefs = useMemo(() => {
|
||||
if (!current) return [];
|
||||
const q = refSearch.trim().toUpperCase();
|
||||
return current.refs.filter((r) => {
|
||||
return (current.refs ?? []).filter((r) => {
|
||||
if (refFilter === 'worked' && !r.worked) return false;
|
||||
if (refFilter === 'notworked' && r.worked) return false;
|
||||
if (refFilter === 'worked_notconf' && !(r.worked && !r.confirmed)) return false;
|
||||
@@ -206,11 +206,11 @@ export function AwardsPanel() {
|
||||
</div>
|
||||
|
||||
{/* Band breakdown */}
|
||||
{current.bands.length > 0 && (
|
||||
{(current.bands ?? []).length > 0 && (
|
||||
<div className="px-4 py-2 border-b border-border/60">
|
||||
<div className="text-[11px] uppercase tracking-wider text-muted-foreground mb-1.5">By band (confirmed / worked)</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{current.bands.map((b) => (
|
||||
{(current.bands ?? []).map((b) => (
|
||||
<div key={b.band} className="rounded-md border border-border bg-card px-2 py-1 text-xs">
|
||||
<span className="font-mono font-semibold">{b.band}</span>{' '}
|
||||
<span className="text-emerald-600 font-mono">{b.confirmed}</span>
|
||||
|
||||
@@ -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 & 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 & 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 & 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>
|
||||
|
||||
@@ -44,6 +44,10 @@ export function applyAwardRefs(payload: any, awardRefs: string, fieldOf: Record<
|
||||
case 'iota': payload.iota = ref; break;
|
||||
case 'sota_ref': payload.sota_ref = ref; break;
|
||||
case 'pota_ref': payload.pota_ref = ref; break;
|
||||
// Predefined-list awards on a QSO field (WAS/RAC/WAJA on state, JCC on
|
||||
// county): picking a reference writes it straight into that column.
|
||||
case 'state': payload.state = ref; break;
|
||||
case 'cnty': payload.cnty = ref; break;
|
||||
case 'wwff':
|
||||
extras['WWFF_REF'] = ref;
|
||||
extras['SIG'] = 'WWFF';
|
||||
@@ -73,6 +77,8 @@ export function awardRefValue(qso: any, code: string, field: string): string {
|
||||
case 'iota': return (qso.iota ?? '').toUpperCase();
|
||||
case 'sota_ref': return (qso.sota_ref ?? '').toUpperCase();
|
||||
case 'pota_ref': return (qso.pota_ref ?? '').toUpperCase();
|
||||
case 'state': return (qso.state ?? '').toUpperCase();
|
||||
case 'cnty': return (qso.cnty ?? '').toUpperCase();
|
||||
case 'wwff': {
|
||||
const ex = qso.extras ?? {};
|
||||
if (ex['WWFF_REF']) return String(ex['WWFF_REF']).toUpperCase();
|
||||
|
||||
Vendored
+8
-6
@@ -1,9 +1,9 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {adif} from '../models';
|
||||
import {qso} from '../models';
|
||||
import {main} from '../models';
|
||||
import {profile} from '../models';
|
||||
import {adif} from '../models';
|
||||
import {award} from '../models';
|
||||
import {awardref} from '../models';
|
||||
import {cat} from '../models';
|
||||
@@ -15,6 +15,10 @@ import {operating} from '../models';
|
||||
import {udp} from '../models';
|
||||
import {lookup} from '../models';
|
||||
|
||||
export function ADIFFields():Promise<Array<adif.FieldDef>>;
|
||||
|
||||
export function ADIFVersion():Promise<string>;
|
||||
|
||||
export function ActivateProfile(arg1:number):Promise<void>;
|
||||
|
||||
export function AddQSO(arg1:qso.QSO):Promise<number>;
|
||||
@@ -77,8 +81,6 @@ export function DeleteQSO(arg1:number):Promise<void>;
|
||||
|
||||
export function DeleteUDPIntegration(arg1:number):Promise<void>;
|
||||
|
||||
export function DisablePortableMode():Promise<void>;
|
||||
|
||||
export function DisconnectAllClusters():Promise<void>;
|
||||
|
||||
export function DisconnectClusterServer(arg1:number):Promise<void>;
|
||||
@@ -89,14 +91,14 @@ export function DownloadConfirmations(arg1:string,arg2:boolean):Promise<void>;
|
||||
|
||||
export function DuplicateProfile(arg1:number,arg2:string):Promise<profile.Profile>;
|
||||
|
||||
export function EnablePortableMode():Promise<void>;
|
||||
|
||||
export function ExportADIF(arg1:string,arg2:boolean):Promise<adif.ExportResult>;
|
||||
|
||||
export function ExportADIFFiltered(arg1:string,arg2:boolean,arg3:qso.QueryFilter):Promise<adif.ExportResult>;
|
||||
|
||||
export function ExportADIFSelected(arg1:string,arg2:boolean,arg3:Array<number>):Promise<adif.ExportResult>;
|
||||
|
||||
export function ExportAwards():Promise<string>;
|
||||
|
||||
export function FilterFields():Promise<Array<string>>;
|
||||
|
||||
export function FindQSOsForUpload(arg1:string,arg2:string):Promise<Array<qso.UploadRow>>;
|
||||
@@ -175,7 +177,7 @@ export function ImportADIF(arg1:string,arg2:string,arg3:boolean):Promise<adif.Im
|
||||
|
||||
export function ImportAwardReferencesText(arg1:string,arg2:string):Promise<number>;
|
||||
|
||||
export function IsPortableMode():Promise<boolean>;
|
||||
export function ImportAwards():Promise<main.AwardImportResult>;
|
||||
|
||||
export function ListAudioInputDevices():Promise<Array<audio.Device>>;
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function ADIFFields() {
|
||||
return window['go']['main']['App']['ADIFFields']();
|
||||
}
|
||||
|
||||
export function ADIFVersion() {
|
||||
return window['go']['main']['App']['ADIFVersion']();
|
||||
}
|
||||
|
||||
export function ActivateProfile(arg1) {
|
||||
return window['go']['main']['App']['ActivateProfile'](arg1);
|
||||
}
|
||||
@@ -126,10 +134,6 @@ export function DeleteUDPIntegration(arg1) {
|
||||
return window['go']['main']['App']['DeleteUDPIntegration'](arg1);
|
||||
}
|
||||
|
||||
export function DisablePortableMode() {
|
||||
return window['go']['main']['App']['DisablePortableMode']();
|
||||
}
|
||||
|
||||
export function DisconnectAllClusters() {
|
||||
return window['go']['main']['App']['DisconnectAllClusters']();
|
||||
}
|
||||
@@ -150,10 +154,6 @@ export function DuplicateProfile(arg1, arg2) {
|
||||
return window['go']['main']['App']['DuplicateProfile'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function EnablePortableMode() {
|
||||
return window['go']['main']['App']['EnablePortableMode']();
|
||||
}
|
||||
|
||||
export function ExportADIF(arg1, arg2) {
|
||||
return window['go']['main']['App']['ExportADIF'](arg1, arg2);
|
||||
}
|
||||
@@ -166,6 +166,10 @@ export function ExportADIFSelected(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['ExportADIFSelected'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function ExportAwards() {
|
||||
return window['go']['main']['App']['ExportAwards']();
|
||||
}
|
||||
|
||||
export function FilterFields() {
|
||||
return window['go']['main']['App']['FilterFields']();
|
||||
}
|
||||
@@ -322,8 +326,8 @@ export function ImportAwardReferencesText(arg1, arg2) {
|
||||
return window['go']['main']['App']['ImportAwardReferencesText'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function IsPortableMode() {
|
||||
return window['go']['main']['App']['IsPortableMode']();
|
||||
export function ImportAwards() {
|
||||
return window['go']['main']['App']['ImportAwards']();
|
||||
}
|
||||
|
||||
export function ListAudioInputDevices() {
|
||||
|
||||
@@ -16,6 +16,28 @@ export namespace adif {
|
||||
this.size_kb = source["size_kb"];
|
||||
}
|
||||
}
|
||||
export class FieldDef {
|
||||
name: string;
|
||||
kind: string;
|
||||
category: string;
|
||||
promoted: boolean;
|
||||
deprecated: boolean;
|
||||
intl: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FieldDef(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.name = source["name"];
|
||||
this.kind = source["kind"];
|
||||
this.category = source["category"];
|
||||
this.promoted = source["promoted"];
|
||||
this.deprecated = source["deprecated"];
|
||||
this.intl = source["intl"];
|
||||
}
|
||||
}
|
||||
export class ImportResult {
|
||||
total: number;
|
||||
imported: number;
|
||||
@@ -660,6 +682,20 @@ export namespace main {
|
||||
this.mic_gain = source["mic_gain"];
|
||||
}
|
||||
}
|
||||
export class AwardImportResult {
|
||||
awards: number;
|
||||
references: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new AwardImportResult(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.awards = source["awards"];
|
||||
this.references = source["references"];
|
||||
}
|
||||
}
|
||||
export class AwardRefMeta {
|
||||
code: string;
|
||||
count: number;
|
||||
@@ -1184,6 +1220,7 @@ export namespace main {
|
||||
ok: boolean;
|
||||
err: string;
|
||||
db_path: string;
|
||||
migrated_from_app_data: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new StartupStatus(source);
|
||||
@@ -1194,6 +1231,7 @@ export namespace main {
|
||||
this.ok = source["ok"];
|
||||
this.err = source["err"];
|
||||
this.db_path = source["db_path"];
|
||||
this.migrated_from_app_data = source["migrated_from_app_data"];
|
||||
}
|
||||
}
|
||||
export class StationInfoComputed {
|
||||
@@ -1696,6 +1734,36 @@ export namespace qso {
|
||||
tx_pwr?: number;
|
||||
comment?: string;
|
||||
notes?: string;
|
||||
sig?: string;
|
||||
sig_info?: string;
|
||||
my_sig?: string;
|
||||
my_sig_info?: string;
|
||||
wwff_ref?: string;
|
||||
my_wwff_ref?: string;
|
||||
distance?: number;
|
||||
rx_pwr?: number;
|
||||
a_index?: number;
|
||||
k_index?: number;
|
||||
sfi?: number;
|
||||
skcc?: string;
|
||||
fists?: string;
|
||||
ten_ten?: string;
|
||||
contacted_op?: string;
|
||||
eq_call?: string;
|
||||
pfx?: string;
|
||||
my_name?: string;
|
||||
class?: string;
|
||||
darc_dok?: string;
|
||||
my_darc_dok?: string;
|
||||
region?: string;
|
||||
silent_key?: string;
|
||||
swl?: string;
|
||||
qso_complete?: string;
|
||||
qso_random?: string;
|
||||
credit_granted?: string;
|
||||
credit_submitted?: string;
|
||||
my_arrl_sect?: string;
|
||||
my_vucc_grids?: string;
|
||||
extras?: Record<string, string>;
|
||||
// Go type: time
|
||||
created_at: any;
|
||||
@@ -1803,6 +1871,36 @@ export namespace qso {
|
||||
this.tx_pwr = source["tx_pwr"];
|
||||
this.comment = source["comment"];
|
||||
this.notes = source["notes"];
|
||||
this.sig = source["sig"];
|
||||
this.sig_info = source["sig_info"];
|
||||
this.my_sig = source["my_sig"];
|
||||
this.my_sig_info = source["my_sig_info"];
|
||||
this.wwff_ref = source["wwff_ref"];
|
||||
this.my_wwff_ref = source["my_wwff_ref"];
|
||||
this.distance = source["distance"];
|
||||
this.rx_pwr = source["rx_pwr"];
|
||||
this.a_index = source["a_index"];
|
||||
this.k_index = source["k_index"];
|
||||
this.sfi = source["sfi"];
|
||||
this.skcc = source["skcc"];
|
||||
this.fists = source["fists"];
|
||||
this.ten_ten = source["ten_ten"];
|
||||
this.contacted_op = source["contacted_op"];
|
||||
this.eq_call = source["eq_call"];
|
||||
this.pfx = source["pfx"];
|
||||
this.my_name = source["my_name"];
|
||||
this.class = source["class"];
|
||||
this.darc_dok = source["darc_dok"];
|
||||
this.my_darc_dok = source["my_darc_dok"];
|
||||
this.region = source["region"];
|
||||
this.silent_key = source["silent_key"];
|
||||
this.swl = source["swl"];
|
||||
this.qso_complete = source["qso_complete"];
|
||||
this.qso_random = source["qso_random"];
|
||||
this.credit_granted = source["credit_granted"];
|
||||
this.credit_submitted = source["credit_submitted"];
|
||||
this.my_arrl_sect = source["my_arrl_sect"];
|
||||
this.my_vucc_grids = source["my_vucc_grids"];
|
||||
this.extras = source["extras"];
|
||||
this.created_at = this.convertValues(source["created_at"], null);
|
||||
this.updated_at = this.convertValues(source["updated_at"], null);
|
||||
|
||||
Reference in New Issue
Block a user