feat(awards): refuse an ambiguous match on one-reference awards

Two DARC DOKs are both called "Gießen" — two clubs share the town. Exact
matching finds both and both are right as far as the matcher can tell, so
it kept both and the operator got a QSO credited to a DOK that may well
have been the other one.

Picking one is not an option: it writes a reference into the log with no
evidence behind it. So a Def can now be marked one reference per QSO, and
an ambiguous match on such an award assigns none, names the candidates in
the trace and leaves the contact under missing references — where the
operator's choice now sticks (06e3437).

Off by default, and deliberately NOT a revival of the old `Multi` flag: a
field holding several references (an n-fer POTA activation, a VUCC grid
line) is several references, correctly, and stays that way.
This commit is contained in:
2026-08-01 01:04:17 +02:00
parent 06e34372ed
commit a4623e9ea3
6 changed files with 95 additions and 13 deletions
+10 -2
View File
@@ -33,7 +33,7 @@ export type AwardDef = {
code: string; name: string; description?: string; valid?: boolean; protected?: boolean;
url?: string; download_url?: string; ref_url?: string; valid_from?: string; valid_to?: string; alias?: string;
ref_display?: string; // grid column shows: ref | name | both
type?: string; field: string; match_by?: string; exact_match?: boolean; pattern: string;
type?: string; field: string; match_by?: string; exact_match?: boolean; one_ref_per_qso?: boolean; pattern: string;
leading_str?: string; trailing_str?: string; dynamic?: boolean;
or_rules?: AwardOrRule[];
dxcc_filter: number[] | null; valid_bands?: string[]; valid_modes?: string[]; emission?: string[];
@@ -227,7 +227,7 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
};
type Explanation = {
code: string; in_scope: boolean; scope_error?: string; predefined: boolean;
ref_count: number; steps: Step[]; manual?: string[]; result: string[];
ref_count: number; steps: Step[]; manual?: string[]; ambiguous?: string[]; result: string[];
};
type TestRow = { qso: any; explanation: Explanation };
const [testCall, setTestCall] = useState('');
@@ -554,6 +554,7 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
</div>
</Field2>
<label className="flex items-center gap-2 text-xs cursor-pointer pl-[128px]"><Checkbox checked={!!cur.exact_match} onCheckedChange={(c) => patch({ exact_match: !!c })} /> {t('awed.exactMatch')}</label>
<label className="flex items-center gap-2 text-xs cursor-pointer pl-[128px]" title={t('awed.oneRefHint')}><Checkbox checked={!!cur.one_ref_per_qso} onCheckedChange={(c) => patch({ one_ref_per_qso: !!c })} /> {t('awed.oneRef')}</label>
<Field2 label={t('awed.patternRegex')}><Input className="h-8 font-mono text-xs" value={cur.pattern} onChange={(e) => patch({ pattern: e.target.value })} placeholder={t('awed.patternPlaceholder')} /></Field2>
<div className="grid grid-cols-2 gap-3">
<Field2 label={t('awed.leadingString')}><Input className="h-8 font-mono text-xs" value={cur.leading_str ?? ''} onChange={(e) => patch({ leading_str: e.target.value })} /></Field2>
@@ -708,6 +709,13 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
)}
</div>
))}
{(ex.ambiguous ?? []).length > 0 && (
<div className="px-3 py-2 text-xs">
<span className="font-semibold uppercase text-[10px] tracking-wide text-warning">{t('awed.testAmbiguous')}</span>{' '}
<span className="font-mono text-warning">{(ex.ambiguous ?? []).join(', ')}</span>{' '}
<span className="text-muted-foreground">{t('awed.testAmbiguousHint')}</span>
</div>
)}
{(ex.manual ?? []).length > 0 && (
<div className="px-3 py-2 text-xs">
<span className="font-semibold uppercase text-[10px] tracking-wide">{t('awed.testManual')}</span>{' '}