feat(awards): offer the ambiguous references at log time

The refusal to guess was only visible in the award editor's test tab and
in the awards table — both of them places an operator goes long after the
contact is over. It has to appear while the station is still in front of
you, so ComputeQSOAwardRefs now reports the candidates it declined to
assign and the F3 panel lists them as buttons: click one and it is
written to award_refs, replacing any sibling picked for that award.

They are never auto-added — adding one would be exactly the guess the
setting exists to refuse.

Also: the dark-theme fix for the native calendar glyph named only
input[type=date], so date-time fields kept an invisible icon. All the
temporal input types now share the rule.
This commit is contained in:
2026-08-01 01:10:59 +02:00
parent a4623e9ea3
commit ae21ddb9d7
6 changed files with 75 additions and 14 deletions
+33 -4
View File
@@ -134,7 +134,7 @@ export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, detai
// surfaced and auto-added the moment you enter a call or click a spot, instead
// of only appearing after logging. Pickable matches are merged into award_refs
// (idempotent; award_refs is NOT a dependency, so removing one by hand sticks).
const [detected, setDetected] = useState<Array<{ code: string; ref: string; name?: string; pickable?: boolean }>>([]);
const [detected, setDetected] = useState<Array<{ code: string; ref: string; name?: string; pickable?: boolean; ambiguous?: boolean }>>([]);
useEffect(() => {
if (open !== 'awards' || !callsign.trim()) { setDetected([]); return; }
const t = window.setTimeout(async () => {
@@ -151,7 +151,9 @@ export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, detai
const have = new Set(cur.split(';').filter(Boolean));
let next = cur;
for (const r of all) {
if (!r.pickable) continue;
// An ambiguous candidate is offered, never auto-added: adding one would
// be the guess the award's "one reference per QSO" setting exists to refuse.
if (!r.pickable || r.ambiguous) continue;
const entry = `${String(r.code).toUpperCase()}@${String(r.ref).toUpperCase()}`;
if (!have.has(entry)) { next = next ? `${next};${entry}` : entry; have.add(entry); }
}
@@ -305,10 +307,37 @@ export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, detai
fieldValues={{ state: details.state ?? '', cnty: details.cnty ?? '' }}
heightClass="flex-1 min-h-0"
/>
{detected.length > 0 && (
{/* Ambiguous candidates: the award allows one reference per QSO and
several matched, so none was kept. Click one to settle it while the
contact is still in front of you. */}
{detected.some((r) => r.ambiguous) && (
<div className="mt-2 text-[11px] shrink-0 leading-snug border-t border-warning/40 pt-1.5">
<span className="font-medium text-warning">{t('detp.ambiguous')}</span>{' '}
{detected.filter((r) => r.ambiguous).map((r) => (
<button
key={`amb-${r.code}@${r.ref}`}
type="button"
title={r.name ?? ''}
onClick={() => {
const entry = `${r.code.toUpperCase()}@${r.ref.toUpperCase()}`;
const cur = details.award_refs ?? '';
// Only one of the candidates can be right — replace any
// sibling already picked for this award rather than stacking.
const kept = cur.split(';').filter((e) => e && e.split('@')[0].toUpperCase() !== r.code.toUpperCase());
onChange({ award_refs: [...kept, entry].join(';') });
setDetected((d) => d.filter((x) => !x.ambiguous || x.code.toUpperCase() !== r.code.toUpperCase()));
}}
className="inline-block mr-1.5 px-1.5 py-0.5 rounded font-mono whitespace-nowrap border border-warning/50 text-warning hover:bg-warning/10"
>
{r.code}@{r.ref}{r.name ? `${r.name}` : ''}
</button>
))}
</div>
)}
{detected.some((r) => !r.ambiguous) && (
<div className="mt-2 text-[11px] text-muted-foreground shrink-0 max-h-14 overflow-y-auto leading-snug border-t border-border/50 pt-1.5">
<span className="font-medium text-foreground/70">{t('detp.detected')}</span>{' '}
{detected.map((r) => (
{detected.filter((r) => !r.ambiguous).map((r) => (
<span key={`${r.code}@${r.ref}`} className="inline-block mr-1.5 font-mono whitespace-nowrap" title={r.name ?? ''}>
<span className="text-foreground/80">{r.code}{r.ref ? `@${r.ref}` : ''}</span>
</span>