update
This commit is contained in:
@@ -25,9 +25,13 @@ interface Props {
|
|||||||
// Semicolon-delimited "AWARD@REF" entries, e.g. "POTA@FR-11553;IOTA@EU-064"
|
// Semicolon-delimited "AWARD@REF" entries, e.g. "POTA@FR-11553;IOTA@EU-064"
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (v: string) => void;
|
onChange: (v: string) => void;
|
||||||
|
// Current QSO field values (state, cnty, …). When a predefined award reads one
|
||||||
|
// of these and it's already filled (e.g. VE9CF → state NB), the award counts
|
||||||
|
// automatically — we surface that so the operator needn't pick it by hand.
|
||||||
|
fieldValues?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AwardRefSelector({ dxcc, value, onChange }: Props) {
|
export function AwardRefSelector({ dxcc, value, onChange, fieldValues }: Props) {
|
||||||
const [defs, setDefs] = useState<AwardDef[]>([]);
|
const [defs, setDefs] = useState<AwardDef[]>([]);
|
||||||
const [metas, setMetas] = useState<Record<string, Meta>>({});
|
const [metas, setMetas] = useState<Record<string, Meta>>({});
|
||||||
const [awardCode, setAwardCode] = useState('POTA');
|
const [awardCode, setAwardCode] = useState('POTA');
|
||||||
@@ -67,7 +71,8 @@ export function AwardRefSelector({ dxcc, value, onChange }: Props) {
|
|||||||
const scope = d.dxcc_filter ?? [];
|
const scope = d.dxcc_filter ?? [];
|
||||||
if (scope.length > 0 && (!dxcc || !scope.includes(dxcc))) return false;
|
if (scope.length > 0 && (!dxcc || !scope.includes(dxcc))) return false;
|
||||||
return true;
|
return true;
|
||||||
}).map((d) => ({ code: d.code, name: d.name }));
|
}).map((d) => ({ code: d.code, name: d.name, field: String(d.field ?? '').toLowerCase() }))
|
||||||
|
.sort((a, b) => a.code.localeCompare(b.code));
|
||||||
}, [defs, metas, dxcc]);
|
}, [defs, metas, dxcc]);
|
||||||
|
|
||||||
// Keep the selected award valid as the offered list changes with the call.
|
// Keep the selected award valid as the offered list changes with the call.
|
||||||
@@ -86,6 +91,12 @@ export function AwardRefSelector({ dxcc, value, onChange }: Props) {
|
|||||||
// For dynamic lists, restrict to the contacted entity; otherwise load all.
|
// For dynamic lists, restrict to the contacted entity; otherwise load all.
|
||||||
const refDxcc = isDynamic ? (dxcc ?? 0) : 0;
|
const refDxcc = isDynamic ? (dxcc ?? 0) : 0;
|
||||||
|
|
||||||
|
// The field the selected award reads (state / cnty / note / …).
|
||||||
|
const selField = useMemo(
|
||||||
|
() => String(defs.find((d) => d.code === awardCode)?.field ?? '').toLowerCase(),
|
||||||
|
[defs, awardCode],
|
||||||
|
);
|
||||||
|
|
||||||
// Search helper with a DXCC fallback: try the entity-scoped query first, but
|
// Search helper with a DXCC fallback: try the entity-scoped query first, but
|
||||||
// if it finds nothing AND we were filtering by DXCC, retry unfiltered. This
|
// if it finds nothing AND we were filtering by DXCC, retry unfiltered. This
|
||||||
// fixes awards whose references carry no per-ref DXCC (e.g. SOTA summits,
|
// fixes awards whose references carry no per-ref DXCC (e.g. SOTA summits,
|
||||||
@@ -130,6 +141,20 @@ export function AwardRefSelector({ dxcc, value, onChange }: Props) {
|
|||||||
// When q is empty/short: show auto-results (if ≤ AUTO_SHOW_MAX); when typing: show search results.
|
// When q is empty/short: show auto-results (if ≤ AUTO_SHOW_MAX); when typing: show search results.
|
||||||
const results: AwardRef[] = q.length >= 2 ? searchResults : (tooManyAuto ? [] : autoResults);
|
const results: AwardRef[] = q.length >= 2 ? searchResults : (tooManyAuto ? [] : autoResults);
|
||||||
|
|
||||||
|
// Auto-match: when the selected award reads a QSO field that's already filled
|
||||||
|
// (e.g. RAC reads `state`, and the call resolved to state NB), find the matching
|
||||||
|
// reference so the operator can add it in one click — the award already counts
|
||||||
|
// it from the field, but this makes it explicit and confirms it.
|
||||||
|
const autoMatch = useMemo(() => {
|
||||||
|
if (!selField || !fieldValues) return null;
|
||||||
|
const v = String(fieldValues[selField] ?? '').trim().toUpperCase();
|
||||||
|
if (!v) return null;
|
||||||
|
const pool = (autoResults.length ? autoResults : searchResults);
|
||||||
|
const m = pool.find((r) => r.code.toUpperCase() === v);
|
||||||
|
return m ? { code: m.code, name: m.name } : null;
|
||||||
|
}, [selField, fieldValues, autoResults, searchResults]);
|
||||||
|
const autoAlreadyAdded = autoMatch ? entries.includes(`${awardCode}@${autoMatch.code}`) : false;
|
||||||
|
|
||||||
function addRef(ref: AwardRef) {
|
function addRef(ref: AwardRef) {
|
||||||
const entry = `${awardCode}@${ref.code}`;
|
const entry = `${awardCode}@${ref.code}`;
|
||||||
if (!entries.includes(entry)) {
|
if (!entries.includes(entry)) {
|
||||||
@@ -230,6 +255,25 @@ export function AwardRefSelector({ dxcc, value, onChange }: Props) {
|
|||||||
{/* Right panel: reference search */}
|
{/* Right panel: reference search */}
|
||||||
<div className="w-[172px] shrink-0 flex flex-col gap-1.5 border-l pl-2 min-w-0">
|
<div className="w-[172px] shrink-0 flex flex-col gap-1.5 border-l pl-2 min-w-0">
|
||||||
<span className="text-xs font-semibold">References</span>
|
<span className="text-xs font-semibold">References</span>
|
||||||
|
{/* Auto-match from the QSO field (e.g. State NB → RAC@NB). */}
|
||||||
|
{autoMatch && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={autoAlreadyAdded}
|
||||||
|
onClick={() => addRef({ code: autoMatch.code, name: autoMatch.name } as AwardRef)}
|
||||||
|
className={`text-left rounded border px-1.5 py-1 text-[11px] leading-tight ${
|
||||||
|
autoAlreadyAdded
|
||||||
|
? 'border-emerald-300 bg-emerald-50 text-emerald-800 cursor-default'
|
||||||
|
: 'border-emerald-300 bg-emerald-50/60 text-emerald-800 hover:bg-emerald-100'
|
||||||
|
}`}
|
||||||
|
title={`The ${selField.toUpperCase()} field is ${autoMatch.code} — this award counts it automatically`}
|
||||||
|
>
|
||||||
|
{autoAlreadyAdded ? '✓ ' : '+ '}
|
||||||
|
<span className="font-mono font-semibold">{autoMatch.code}</span>
|
||||||
|
<span className="text-emerald-700"> from {selField}</span>
|
||||||
|
{!autoAlreadyAdded && <span className="block text-[10px] text-emerald-700/80">auto — click to add</span>}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
className="h-6 w-full rounded border border-input bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-ring"
|
className="h-6 w-full rounded border border-input bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-ring"
|
||||||
placeholder="Search…"
|
placeholder="Search…"
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ export function DetailsPanel({ callsign: _cs, prefix, operatorGrid, remoteGrid,
|
|||||||
dxcc={details.dxcc}
|
dxcc={details.dxcc}
|
||||||
value={details.award_refs ?? ''}
|
value={details.award_refs ?? ''}
|
||||||
onChange={(v) => onChange({ award_refs: v })}
|
onChange={(v) => onChange({ award_refs: v })}
|
||||||
|
fieldValues={{ state: details.state ?? '', cnty: details.cnty ?? '' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -463,7 +463,7 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [] }:
|
|||||||
<div className="grid grid-cols-[1fr_240px] gap-5">
|
<div className="grid grid-cols-[1fr_240px] gap-5">
|
||||||
{/* Left: pick reference-list awards (POTA/SOTA/IOTA/WWFF/…) */}
|
{/* Left: pick reference-list awards (POTA/SOTA/IOTA/WWFF/…) */}
|
||||||
<div>
|
<div>
|
||||||
<AwardRefSelector dxcc={draft.dxcc} value={awardRefs} onChange={setAwardRefs} />
|
<AwardRefSelector dxcc={draft.dxcc} value={awardRefs} onChange={setAwardRefs} fieldValues={{ state: draft.state ?? '', cnty: draft.cnty ?? '' }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: computed awards (read-only) derived from this QSO */}
|
{/* Right: computed awards (read-only) derived from this QSO */}
|
||||||
|
|||||||
Reference in New Issue
Block a user