fix: normalization of city name address

This commit is contained in:
2026-05-28 23:23:22 +02:00
parent 5c004f5e2f
commit edda183c16
8 changed files with 216 additions and 50 deletions
+4 -2
View File
@@ -1288,14 +1288,14 @@ export default function App() {
<div className="flex flex-col w-24">
<Label className="mb-1 h-3.5 flex items-center gap-1">Band <LockBtn k="band" title="band" /></Label>
<Select value={band} onValueChange={onBandUserChange}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectTrigger tabIndex={-1}><SelectValue /></SelectTrigger>
<SelectContent>{bands.map((b) => <SelectItem key={b} value={b}>{b}</SelectItem>)}</SelectContent>
</Select>
</div>
<div className="flex flex-col w-28">
<Label className="mb-1 h-3.5 flex items-center gap-1">Mode <LockBtn k="mode" title="mode" /></Label>
<Select value={mode} onValueChange={onModeUserChange}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectTrigger tabIndex={-1}><SelectValue /></SelectTrigger>
<SelectContent>{modes.map((m) => <SelectItem key={m} value={m}>{m}</SelectItem>)}</SelectContent>
</Select>
</div>
@@ -1304,6 +1304,7 @@ export default function App() {
{catState.split ? 'TX Freq (MHz)' : 'Freq (MHz)'} <LockBtn k="freq" title="frequency" />
</Label>
<Input
tabIndex={-1}
className="font-mono"
value={freqFocused ? freqMhz : (freqMhz ? fmtFreqDots(freqMhz) : '')}
placeholder="14.250"
@@ -1316,6 +1317,7 @@ export default function App() {
<div className="flex flex-col w-28">
<Label className="mb-1 h-3.5 text-rose-600">RX Freq (MHz)</Label>
<Input
tabIndex={-1}
value={freqFocused ? rxFreqMhz : (rxFreqMhz ? fmtFreqDots(rxFreqMhz) : '')}
placeholder="14.255"
onFocus={() => setFreqFocused(true)}
+60 -36
View File
@@ -125,41 +125,6 @@ interface Props {
// the callsign + grid in the Station Information form. Debounces the
// backend resolver so we don't fire on every keystroke; refreshes when
// inputs change. Empty card when no callsign yet.
function StationInfoComputedBadge({ callsign, grid }: { callsign: string; grid: string }) {
const [info, setInfo] = useState<{
country: string; dxcc: number; cqz: number; ituz: number; lat: number; lon: number;
} | null>(null);
useEffect(() => {
const c = callsign.trim();
if (!c) { setInfo(null); return; }
const t = window.setTimeout(async () => {
try {
const i = await ComputeStationInfo(c, grid.trim());
setInfo(i as any);
} catch { setInfo(null); }
}, 200);
return () => window.clearTimeout(t);
}, [callsign, grid]);
if (!info || (!info.country && !info.cqz && !info.ituz)) {
return null;
}
return (
<div className="rounded-md border border-primary/30 bg-primary/5 p-2.5">
<div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground mb-1.5">
Auto-filled on each QSO (MY_*)
</div>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-[11px] font-mono">
{info.country && <span><span className="text-muted-foreground">Country:</span> <strong>{info.country}</strong></span>}
{info.dxcc > 0 && <span><span className="text-muted-foreground">DXCC#:</span> <strong>{info.dxcc}</strong></span>}
{info.cqz > 0 && <span><span className="text-muted-foreground">CQ:</span> <strong>{info.cqz}</strong></span>}
{info.ituz > 0 && <span><span className="text-muted-foreground">ITU:</span> <strong>{info.ituz}</strong></span>}
{info.lat !== 0 && <span><span className="text-muted-foreground">Lat:</span> <strong>{info.lat.toFixed(4)}</strong></span>}
{info.lon !== 0 && <span><span className="text-muted-foreground">Lon:</span> <strong>{info.lon.toFixed(4)}</strong></span>}
</div>
</div>
);
}
/* ====== Tree definition ======
Section IDs are stable strings — adding new ones means adding a panel below.
`disabled: true` greys them out and shows the "coming soon" placeholder. */
@@ -499,6 +464,36 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
})();
}, []);
// Auto-fill the active profile's MY_* DXCC metadata from the station
// callsign (country, DXCC#, CQ/ITU zones) and the grid (lat/lon). These
// are derived values, so they always recompute when the callsign or grid
// changes — the user can still edit a field, it just re-populates when the
// source changes. Debounced so we don't hammer cty.dat while typing.
useEffect(() => {
const call = (activeProfile?.callsign ?? '').trim();
if (!call) return;
const grid = (activeProfile?.my_grid ?? '').trim();
const t = window.setTimeout(async () => {
try {
const i: any = await ComputeStationInfo(call, grid);
setActiveProfile((p) => {
if (!p) return p;
const patch: any = {};
if (i.country) patch.my_country = i.country;
if (i.dxcc) patch.my_dxcc = i.dxcc;
if (i.cqz) patch.my_cqz = i.cqz;
if (i.ituz) patch.my_ituz = i.ituz;
if (i.lat) patch.my_lat = i.lat;
if (i.lon) patch.my_lon = i.lon;
// Only re-render when a value actually changed (prevents loops).
const changed = Object.keys(patch).some((k) => (p as any)[k] !== patch[k]);
return changed ? { ...p, ...patch } : p;
});
} catch { /* offline / unknown prefix — leave fields as-is */ }
}, 250);
return () => window.clearTimeout(t);
}, [activeProfile?.callsign, activeProfile?.my_grid]);
// ── Band selection helpers (dual-list shuttle) ──────────────────────────
function addBand(tag: string) {
const b = tag.trim().toLowerCase();
@@ -681,7 +676,9 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
<Input className="font-mono uppercase max-w-xs" value={p.owner_callsign ?? ''} onChange={(e) => updateActive({ owner_callsign: e.target.value })} placeholder="(leave blank if same as station)" />
<div className="text-[10px] text-muted-foreground">Legal station owner only differs at club stations or remote setups (ADIF STATION_OWNER).</div>
</div>
<div className="col-span-2"><StationInfoComputedBadge callsign={p.callsign ?? ''} grid={p.my_grid ?? ''} /></div>
<div className="col-span-2 text-[10px] text-muted-foreground uppercase tracking-wider mt-1">
Auto-filled from the callsign editable (stamped as MY_* on each QSO)
</div>
<div className="space-y-1">
<Label>My grid</Label>
<Input className="font-mono uppercase" value={p.my_grid ?? ''} onChange={(e) => updateActive({ my_grid: e.target.value })} placeholder="JN18BU" />
@@ -690,6 +687,33 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
<Label>My country</Label>
<Input value={p.my_country ?? ''} onChange={(e) => updateActive({ my_country: e.target.value })} placeholder="France" />
</div>
<div className="grid grid-cols-3 gap-2 col-span-2">
<div className="space-y-1">
<Label>DXCC #</Label>
<Input type="number" className="font-mono" value={(p as any).my_dxcc ?? ''}
onChange={(e) => updateActive({ my_dxcc: e.target.value === '' ? undefined : (parseInt(e.target.value, 10) || undefined) } as any)} placeholder="—" />
</div>
<div className="space-y-1">
<Label>CQ zone</Label>
<Input type="number" className="font-mono" value={(p as any).my_cqz ?? ''}
onChange={(e) => updateActive({ my_cqz: e.target.value === '' ? undefined : (parseInt(e.target.value, 10) || undefined) } as any)} placeholder="—" />
</div>
<div className="space-y-1">
<Label>ITU zone</Label>
<Input type="number" className="font-mono" value={(p as any).my_ituz ?? ''}
onChange={(e) => updateActive({ my_ituz: e.target.value === '' ? undefined : (parseInt(e.target.value, 10) || undefined) } as any)} placeholder="—" />
</div>
<div className="space-y-1">
<Label>Latitude</Label>
<Input type="number" step="0.0001" className="font-mono" value={(p as any).my_lat ?? ''}
onChange={(e) => updateActive({ my_lat: e.target.value === '' ? undefined : (parseFloat(e.target.value) || undefined) } as any)} placeholder="—" />
</div>
<div className="space-y-1">
<Label>Longitude</Label>
<Input type="number" step="0.0001" className="font-mono" value={(p as any).my_lon ?? ''}
onChange={(e) => updateActive({ my_lon: e.target.value === '' ? undefined : (parseFloat(e.target.value) || undefined) } as any)} placeholder="—" />
</div>
</div>
<div className="space-y-1">
<Label>State / pref</Label>
<Input value={p.my_state ?? ''} onChange={(e) => updateActive({ my_state: e.target.value })} />
+10
View File
@@ -741,6 +741,11 @@ export namespace profile {
my_pota_ref: string;
my_rig: string;
my_antenna: string;
my_dxcc?: number;
my_cqz?: number;
my_ituz?: number;
my_lat?: number;
my_lon?: number;
tx_pwr?: number;
is_active: boolean;
sort_order: number;
@@ -771,6 +776,11 @@ export namespace profile {
this.my_pota_ref = source["my_pota_ref"];
this.my_rig = source["my_rig"];
this.my_antenna = source["my_antenna"];
this.my_dxcc = source["my_dxcc"];
this.my_cqz = source["my_cqz"];
this.my_ituz = source["my_ituz"];
this.my_lat = source["my_lat"];
this.my_lon = source["my_lon"];
this.tx_pwr = source["tx_pwr"];
this.is_active = source["is_active"];
this.sort_order = source["sort_order"];