fix(editor): a corrected frequency corrects its band

Band and frequency have to agree — the log, every award and every upload
are read on the BAND — and an operator fixing a wrong frequency is not
also expecting to fix the band by hand. That is exactly how a QSO ends
up filed on 20m at 7 MHz. Both sides follow, TX and RX.

Only when the number lands in a known allocation, because half a
frequency is typed on the way to all of it and a band must never be
blanked by that. bandForMHz moves out of App.tsx into lib/bandplan,
where the two callers can share one answer instead of drifting.
This commit is contained in:
2026-08-31 22:21:22 +02:00
parent e3332d1e27
commit 22e4266d38
4 changed files with 46 additions and 21 deletions
+1 -15
View File
@@ -59,6 +59,7 @@ import { Combobox } from '@/components/ui/combobox';
import { applyAwardRefs, parseAwardRefs as parseManualRefs, spotRefList , withIOTARef, withRDARef } from '@/lib/awardRefs';
import { ListRadios, SetActiveRadio } from '../wailsjs/go/main/App';
import { formatDistance } from '@/lib/units';
import { bandForMHz } from '@/lib/bandplan';
import { EventsOn, BrowserOpenURL, WindowMinimise, WindowToggleMaximise, WindowIsMaximised, Quit } from '../wailsjs/runtime/runtime';
import type { adif as adifModels, lookup as lookupModels, cat as catModels } from '../wailsjs/go/models';
import type { QSOForm, WorkedBeforeView, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
@@ -432,21 +433,6 @@ function entryQSYCommand(text: string): { band?: string; hz?: number } | null {
return null;
}
function bandForMHz(mhz: number): string {
if (!mhz || isNaN(mhz)) return '';
const plan: [number, number, string][] = [
[1.8, 2.0, '160m'], [3.5, 4.0, '80m'], [5.06, 5.45, '60m'], [7.0, 7.3, '40m'],
[10.1, 10.15, '30m'], [14.0, 14.35, '20m'], [18.068, 18.168, '17m'], [21.0, 21.45, '15m'],
[24.89, 24.99, '12m'], [28.0, 29.7, '10m'], [50, 54, '6m'], [70, 71, '4m'],
[144, 148, '2m'], [222, 225, '1.25m'], [420, 450, '70cm'], [902, 928, '33cm'], [1240, 1300, '23cm'],
// Microwave, ADIF 3.1.7 ranges — kept in step with BandFromHz on the Go side.
[2300, 2450, '13cm'], [3300, 3500, '9cm'], [5650, 5925, '6cm'], [10000, 10500, '3cm'],
[24000, 24250, '1.25cm'], [47000, 47200, '6mm'], [75500, 81000, '4mm'],
[119980, 123000, '2.5mm'], [134000, 149000, '2mm'], [241000, 250000, '1mm'],
];
for (const [lo, hi, b] of plan) if (mhz >= lo && mhz <= hi) return b;
return '';
}
// modeAccent maps a mode to a theme-aware colour for the live-stations widget:
// CW gold, phone green, digital blue, unknown muted.
+18 -4
View File
@@ -22,6 +22,7 @@ import { Combobox } from '@/components/ui/combobox';
import { cn } from '@/lib/utils';
import { flagURL } from '@/lib/flags';
import { useI18n } from '@/lib/i18n';
import { bandForMHz } from '@/lib/bandplan';
import { titleCase, sentenceCase } from '@/lib/textCase';
import type { QSOForm } from '@/types';
@@ -312,6 +313,19 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
const splitHz = (hz?: number) => hz
? { khz: String(Math.floor(hz / 1000)), hz: String(hz % 1000).padStart(3, '0') }
: { khz: '', hz: '' };
// Correcting a frequency corrects its band. The pair has to agree — the log,
// every award and every upload are read on the BAND — and an operator fixing
// a wrong frequency is not also expecting to fix the band by hand, which is
// exactly how a QSO ends up filed on 20m at 7 MHz.
//
// Only when the number lands in a known allocation: half a frequency is typed
// on the way to all of it, and a band must never be blanked by that.
const syncBand = (khz: string, hz: string, field: 'band' | 'band_rx') => {
if (!khz.trim()) return;
const b = bandForMHz((parseInt(khz, 10) * 1000 + (parseInt(hz, 10) || 0)) / 1_000_000);
if (b) set(field, b as any);
};
const f0 = splitHz(draft.freq_hz);
const fr0 = splitHz(draft.freq_rx_hz);
const [freqKHz, setFreqKHz] = useState(f0.khz);
@@ -634,13 +648,13 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
</div>
<div className="flex items-center gap-2">
<Label className="w-20 shrink-0">{t('qedit.txFreq')}</Label>
<Input value={freqKHz} onChange={(e) => setFreqKHz(e.target.value.replace(/\D/g, ''))} className="font-mono w-24" placeholder="kHz" />
<Input value={freqHz} onChange={(e) => setFreqHz(e.target.value.replace(/\D/g, ''))} maxLength={3} className="font-mono w-16" placeholder="Hz" />
<Input value={freqKHz} onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); setFreqKHz(v); syncBand(v, freqHz, 'band'); }} className="font-mono w-24" placeholder="kHz" />
<Input value={freqHz} onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); setFreqHz(v); syncBand(freqKHz, v, 'band'); }} maxLength={3} className="font-mono w-16" placeholder="Hz" />
</div>
<div className="flex items-center gap-2">
<Label className="w-20 shrink-0">{t('qedit.rxFreq')}</Label>
<Input value={freqRxKHz} onChange={(e) => setFreqRxKHz(e.target.value.replace(/\D/g, ''))} className="font-mono w-24" placeholder="kHz" />
<Input value={freqRxHz} onChange={(e) => setFreqRxHz(e.target.value.replace(/\D/g, ''))} maxLength={3} className="font-mono w-16" placeholder="Hz" />
<Input value={freqRxKHz} onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); setFreqRxKHz(v); syncBand(v, freqRxHz, 'band_rx'); }} className="font-mono w-24" placeholder="kHz" />
<Input value={freqRxHz} onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); setFreqRxHz(v); syncBand(freqRxKHz, v, 'band_rx'); }} maxLength={3} className="font-mono w-16" placeholder="Hz" />
</div>
</div>
+23
View File
@@ -111,3 +111,26 @@ export function bandRange(band: string): [number, number] | undefined {
export function bandSegments(band: string): Seg[] {
return plan().segments[band] ?? [];
}
// bandForMHz maps a dial frequency (MHz) to its ADIF band, or '' when it falls
// outside every known allocation.
//
// Lives here rather than in a panel because more than one place has to answer
// the same question the same way: the entry form retunes on a typed frequency,
// and the QSO editor has to keep band and frequency agreeing when one of them
// is corrected. Kept in step with BandFromHz on the Go side.
export function bandForMHz(mhz: number): string {
if (!mhz || isNaN(mhz)) return '';
const plan: [number, number, string][] = [
[1.8, 2.0, '160m'], [3.5, 4.0, '80m'], [5.06, 5.45, '60m'], [7.0, 7.3, '40m'],
[10.1, 10.15, '30m'], [14.0, 14.35, '20m'], [18.068, 18.168, '17m'], [21.0, 21.45, '15m'],
[24.89, 24.99, '12m'], [28.0, 29.7, '10m'], [50, 54, '6m'], [70, 71, '4m'],
[144, 148, '2m'], [222, 225, '1.25m'], [420, 450, '70cm'], [902, 928, '33cm'], [1240, 1300, '23cm'],
// Microwave, ADIF 3.1.7 ranges.
[2300, 2450, '13cm'], [3300, 3500, '9cm'], [5650, 5925, '6cm'], [10000, 10500, '3cm'],
[24000, 24250, '1.25cm'], [47000, 47200, '6mm'], [75500, 81000, '4mm'],
[119980, 123000, '2.5mm'], [134000, 149000, '2mm'], [241000, 250000, '1mm'],
];
for (const [lo, hi, b] of plan) if (mhz >= lo && mhz <= hi) return b;
return '';
}