feat(awards): settle each RDA disagreement on its own row
The comparison could show two hundred contacts where the log and the database name different districts, and leave the operator with nothing to do about any of them but open each QSO and edit it by hand. Now each row carries the two values as buttons: click the district you keep, then apply. There are shortcuts for keeping one source everywhere, and applying is a separate act — clicking through two hundred rows with each one written as it is clicked would make a slip permanent before the reading was finished. The choice is written as the contact's AWARD REFERENCE, not over the imported CNTY, and that distinction is the point. CNTY is what the downloaded log said; overwriting it would destroy the evidence the comparison runs on, and the next comparison would agree with itself and prove nothing. The override is the operator's decision, it is what the award engine counts, and it beats both sources. So the log keeps saying what it said, the database keeps saying what it says, and the contact counts for the district that was chosen. The buttons show the districts themselves rather than a tick box: someone arbitrating between RO-19 and KO-05 should be picking a value they can read, not remembering which source a checked box stood for.
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
ListProfiles, GetActiveProfile, SaveProfile, DeleteProfile, ActivateProfile, DuplicateProfile,
|
||||
GetRotators, SaveRotators, TestRotatorDevice, RotatorPark, RotatorStop,
|
||||
GetRotorPresets, SaveRotorPresets, ResetRotorPresets,
|
||||
GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam, CompactDatabase, CheckHamlogKey, CompareRDASources,
|
||||
GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam, CompactDatabase, CheckHamlogKey, CompareRDASources, ApplyRDAChoices,
|
||||
GetAntGeniusSettings, SaveAntGeniusSettings,
|
||||
GetTunerGeniusSettings, SaveTunerGeniusSettings,
|
||||
GetPSUSettings, SavePSUSettings,
|
||||
@@ -1946,6 +1946,11 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
// nobody had pressed, and the one that had been pressed showed nothing at
|
||||
// all, which reads as "the button does nothing".
|
||||
const [rdaCmpMsg, setRdaCmpMsg] = useState<string>('');
|
||||
// Which source the operator kept, per QSO id. By id and not by row: a second
|
||||
// comparison rebuilds the list, and a choice that followed a row number would
|
||||
// then be applied to somebody else's contact.
|
||||
const [rdaPick, setRdaPick] = useState<Record<number, 'log' | 'db'>>({});
|
||||
const [rdaApplyBusy, setRdaApplyBusy] = useState(false);
|
||||
const runRDACompare = async () => {
|
||||
setRdaCmpBusy(true); setRdaCmp(null); setRdaCmpMsg('');
|
||||
try {
|
||||
@@ -7517,6 +7522,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
<th className="py-1 pr-2 font-medium">{t('rda.cmpFromLog')}</th>
|
||||
<th className="py-1 pr-2 font-medium">{t('rda.cmpFromDb')}</th>
|
||||
<th className="py-1 pr-2 font-medium">{t('rda.cmpKind')}</th>
|
||||
<th className="py-1 pr-2 font-medium text-center">{t('rda.cmpKeep')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -7541,11 +7547,82 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
<td className="py-1 pr-2 text-muted-foreground">
|
||||
{c.dated ? t('rda.cmpDated') : t('rda.cmpCurrent')}
|
||||
</td>
|
||||
{/* Which one wins, on the row that shows the two values.
|
||||
Two buttons rather than a tick box: a tick box would
|
||||
have to mean one of the sources silently, and an
|
||||
operator arbitrating between "RO-19" and "KO-05"
|
||||
should be picking the value they can read, not
|
||||
remembering what a checked box stands for. */}
|
||||
<td className="py-1 pr-2 whitespace-nowrap text-center">
|
||||
<div className="inline-flex rounded border border-border overflow-hidden">
|
||||
{(['log', 'db'] as const).map((side) => (
|
||||
<button
|
||||
key={side}
|
||||
type="button"
|
||||
onClick={() => setRdaPick((p) => {
|
||||
const next = { ...p };
|
||||
if (next[c.qso_id] === side) delete next[c.qso_id];
|
||||
else next[c.qso_id] = side;
|
||||
return next;
|
||||
})}
|
||||
className={`px-2 py-0.5 font-mono text-[10px] ${rdaPick[c.qso_id] === side
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-muted'}`}
|
||||
title={side === 'log' ? t('rda.cmpKeepLog') : t('rda.cmpKeepDb')}
|
||||
>
|
||||
{side === 'log' ? c.from_log : c.from_db}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* Applying is a separate, deliberate act. Clicking through two
|
||||
hundred rows and having each one written as it is clicked would
|
||||
make a slip permanent before the operator had finished
|
||||
reading. */}
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<Button size="sm" variant="secondary" disabled={rdaApplyBusy || Object.keys(rdaPick).length === 0}
|
||||
onClick={async () => {
|
||||
setRdaApplyBusy(true);
|
||||
try {
|
||||
const choices = rdaCmp.conflicts
|
||||
.filter((c: any) => rdaPick[c.qso_id])
|
||||
.map((c: any) => ({
|
||||
qso_id: c.qso_id,
|
||||
district: rdaPick[c.qso_id] === 'log' ? c.from_log : c.from_db,
|
||||
}));
|
||||
const r: any = await ApplyRDAChoices(choices);
|
||||
setRdaCmpMsg(r?.message || '');
|
||||
setRdaPick({});
|
||||
} catch (e: any) {
|
||||
setRdaCmpMsg(String(e?.message ?? e));
|
||||
} finally {
|
||||
setRdaApplyBusy(false);
|
||||
}
|
||||
}}>
|
||||
{rdaApplyBusy ? <Loader2 className="size-3.5 animate-spin mr-1.5" /> : null}
|
||||
{t('rda.cmpApply', { n: Object.keys(rdaPick).length })}
|
||||
</Button>
|
||||
<button type="button" className="text-[11px] underline decoration-dotted hover:text-primary"
|
||||
onClick={() => setRdaPick(Object.fromEntries(rdaCmp.conflicts.map((c: any) => [c.qso_id, 'db'])))}>
|
||||
{t('rda.cmpAllDb')}
|
||||
</button>
|
||||
<button type="button" className="text-[11px] underline decoration-dotted hover:text-primary"
|
||||
onClick={() => setRdaPick(Object.fromEntries(rdaCmp.conflicts.map((c: any) => [c.qso_id, 'log'])))}>
|
||||
{t('rda.cmpAllLog')}
|
||||
</button>
|
||||
{Object.keys(rdaPick).length > 0 && (
|
||||
<button type="button" className="text-[11px] underline decoration-dotted hover:text-primary"
|
||||
onClick={() => setRdaPick({})}>
|
||||
{t('rda.cmpClear')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">{t('rda.cmpApplyHint')}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user