feat: filter award references by mode class
All / CW / Phone / Digital, stacked on top of the worked/confirmed filter. The
question an operator actually has — which DXCC entities have I worked on CW but
not confirmed on CW — needs both at once; answering only one of them is what
sends people to a spreadsheet.
References carried bands but not modes, so the award computation now aggregates
mode CLASSES per reference, worked and confirmed separately. Classes, not ADIF
modes: nobody chasing CW cares whether the digital side was FT8 or RTTY, and a
list of twenty mode names would not answer the question. An unrecognised mode
counts as data rather than as nothing, so a new digital mode does not vanish
from the filter the day it appears.
Two rules the filter needs to be honest:
- Not-confirmed means not confirmed ON THIS MODE. An entity worked on CW and
confirmed on SSB is still a CW entity to chase, and that is exactly what the
filter is for.
- "Not worked" plus a mode is a contradiction — an unworked reference has no
mode — so the mode filter stands aside there instead of emptying the list.
This commit is contained in:
@@ -15,6 +15,8 @@ type AwardRef = {
|
||||
ref: string; name?: string; group?: string; subgrp?: string;
|
||||
worked: boolean; confirmed: boolean; validated: boolean;
|
||||
bands: string[]; confirmed_bands: string[]; validated_bands: string[];
|
||||
// Mode CLASSES — "CW" | "PHONE" | "DIGI" — not ADIF modes.
|
||||
modes?: string[]; confirmed_modes?: string[];
|
||||
};
|
||||
type AwardResult = {
|
||||
code: string; name: string; dimension: string;
|
||||
@@ -75,6 +77,10 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [view, setView] = useState<'grid' | 'list' | 'stats'>('grid');
|
||||
const [refFilter, setRefFilter] = useState<'all' | 'worked' | 'notworked' | 'worked_notconf'>('all');
|
||||
// Mode filter, stacked ON TOP of the status one. "Worked on CW but not
|
||||
// confirmed" is two questions at once, and answering only one of them is what
|
||||
// sends an operator to a spreadsheet.
|
||||
const [modeFilter, setModeFilter] = useState<'all' | 'CW' | 'PHONE' | 'DIGI'>('all');
|
||||
const [cell, setCell] = useState<{ ref: string; band: string; name?: string } | null>(null);
|
||||
const [showMissing, setShowMissing] = useState(false);
|
||||
const [stats, setStats] = useState<AwardStats | null>(null);
|
||||
@@ -222,6 +228,16 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
|
||||
if (refFilter === 'worked' && !r.worked) return false;
|
||||
if (refFilter === 'notworked' && r.worked) return false;
|
||||
if (refFilter === 'worked_notconf' && !(r.worked && !r.confirmed)) return false;
|
||||
if (modeFilter !== 'all' && refFilter !== 'notworked') {
|
||||
// A reference never worked has no mode, so "not worked" plus a mode is
|
||||
// a contradiction: the mode filter stands aside rather than emptying
|
||||
// the list.
|
||||
if (!(r.modes ?? []).includes(modeFilter)) return false;
|
||||
// Not-confirmed must mean not confirmed ON THIS MODE. An entity worked
|
||||
// on CW and confirmed on SSB is still a CW entity to chase, and the
|
||||
// whole point of the filter is to find those.
|
||||
if (refFilter === 'worked_notconf' && (r.confirmed_modes ?? []).includes(modeFilter)) return false;
|
||||
}
|
||||
if (q && !(r.ref.includes(q) || (r.name ?? '').toUpperCase().includes(q) || (r.group ?? '').toUpperCase().includes(q))) return false;
|
||||
return true;
|
||||
});
|
||||
@@ -237,7 +253,7 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
|
||||
}
|
||||
return a.ref.localeCompare(b.ref, undefined, { numeric: true }) * dir;
|
||||
});
|
||||
}, [current, refSearch, refFilter, refSort, refSortDir]);
|
||||
}, [current, refSearch, refFilter, modeFilter, refSort, refSortDir]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0">
|
||||
@@ -361,6 +377,14 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center rounded-md border border-border overflow-hidden text-sm">
|
||||
{([['all', t('awp.filterAll')], ['CW', 'CW'], ['PHONE', t('awp.modePhone')], ['DIGI', t('awp.modeDigital')]] as const).map(([k, label]) => (
|
||||
<button key={k} onClick={() => setModeFilter(k)}
|
||||
className={cn('px-2 py-1', modeFilter === k ? 'bg-accent font-medium' : 'hover:bg-accent/50 text-muted-foreground')}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{filteredRefs.length} {t('awp.refs')}</span>
|
||||
<button
|
||||
onClick={() => setShowMissing(true)}
|
||||
|
||||
Reference in New Issue
Block a user