feat(matrix): mark the slots already worked with the callsign in hand
The worked-before cell answered one question with two facts fused into it. bandStatusCode takes the highest of them, so a confirmed entity outranks a worked callsign — right for awards, wrong while chasing: a slot worked with the DXpedition this morning reads "entity confirmed" and says nothing about it. BandStatus now carries the callsign's own answer alongside the entity's, and the cell draws a dot for it. The dot is deliberately identical everywhere — same shape, same colour, over all five backgrounds — so it is read as one thing rather than as five variants of the colour underneath it.
This commit is contained in:
@@ -81,23 +81,38 @@ const STATUS_CLASSES: Record<string, string> = {
|
||||
// i18n keys the Appearance panel's colour pickers use, so the two can never
|
||||
// disagree about which green is which. swatch = the background class (or a
|
||||
// special ring marker for the current-entry cell).
|
||||
const LEGEND: { swatch: string; ring?: boolean; label: string }[] = [
|
||||
const LEGEND: { swatch: string; ring?: boolean; mark?: boolean; label: string }[] = [
|
||||
{ swatch: 'bg-mx-call-conf', label: 'mx.callConf' },
|
||||
{ swatch: 'bg-mx-call-work', label: 'mx.callWork' },
|
||||
{ swatch: 'bg-mx-dx-conf', label: 'mx.dxConf' },
|
||||
{ swatch: 'bg-mx-dx-work', label: 'mx.dxWork' },
|
||||
{ swatch: 'bg-mx-none', label: 'mx.none' },
|
||||
{ swatch: 'bg-mx-none', ring: true, label: 'mx.current' },
|
||||
{ swatch: 'bg-mx-none', mark: true, label: 'mx.thisCall' },
|
||||
];
|
||||
|
||||
function cellTitle(t: (k: string) => string, band: string, cls: string, status: string, current: boolean): string {
|
||||
// CallMark — "this callsign has already been worked on this slot".
|
||||
//
|
||||
// Drawn exactly the same way on every cell, whatever colour the entity status
|
||||
// gave it. That is the point: the operator learns one shape, and reads it
|
||||
// without first working out what the background means. The dot is the theme
|
||||
// foreground with a ring in the theme background, so it stays legible over all
|
||||
// five cell colours instead of needing a colour per case.
|
||||
function CallMark() {
|
||||
return (
|
||||
<span className="pointer-events-none absolute top-[2px] right-[2px] size-[5px] rounded-full bg-foreground ring-1 ring-background" />
|
||||
);
|
||||
}
|
||||
|
||||
function cellTitle(t: (k: string) => string, band: string, cls: string, status: string, current: boolean, call = ''): string {
|
||||
const desc =
|
||||
status === 'call_c' ? t('mx.tipCallConf') :
|
||||
status === 'call_w' ? t('mx.tipCallWork') :
|
||||
status === 'dxcc_c' ? t('mx.tipDxConf') :
|
||||
status === 'dxcc_w' ? t('mx.tipDxWork') :
|
||||
t('mx.tipNone');
|
||||
return `${band} ${cls}: ${desc}${current ? ' — ' + t('mx.current') : ''}`;
|
||||
const mine = call === 'c' ? t('mx.tipThisCallConf') : call === 'w' ? t('mx.tipThisCall') : '';
|
||||
return `${band} ${cls}: ${desc}${mine ? ' — ' + mine : ''}${current ? ' — ' + t('mx.current') : ''}`;
|
||||
}
|
||||
|
||||
export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCall = true, lat, lon, forCall, onEditQso }: Props) {
|
||||
@@ -126,6 +141,16 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
|
||||
return m;
|
||||
}, [wb]);
|
||||
|
||||
// Worked-with-this-callsign, per cell — carried separately by the backend
|
||||
// because collapsing it into the status is what hid it.
|
||||
const callMap = useMemo(() => {
|
||||
const m = new Map<string, string>();
|
||||
for (const s of wb?.band_status ?? []) {
|
||||
if ((s as any).call) m.set(`${s.band}|${s.class}`, (s as any).call);
|
||||
}
|
||||
return m;
|
||||
}, [wb]);
|
||||
|
||||
// "Newness" of the current band+mode entry, for the award/DX-chase badges.
|
||||
// Derived straight from the entity's real band_status (all bands it was
|
||||
// worked on — not just the operator's configured column list).
|
||||
@@ -308,21 +333,29 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
|
||||
</th>
|
||||
{cols.map((b) => {
|
||||
const st = statusMap.get(`${b.tag}|${cls}`) ?? '';
|
||||
// The same cell's other answer: worked with THIS callsign
|
||||
// here. The status above is the entity's, and a confirmed
|
||||
// entity outranks a worked call — so chasing a DXpedition,
|
||||
// the cell could say "confirmed" about a contact made years
|
||||
// ago and nothing about the one made this morning.
|
||||
const mine = callMap.get(`${b.tag}|${cls}`) ?? '';
|
||||
const isCurrent = hasCall && b.tag === currentBand && classCurrent;
|
||||
return (
|
||||
<td
|
||||
key={b.tag}
|
||||
title={cellTitle(t, b.tag, cls, st, isCurrent) + (st ? ' — ' + t('mx.tipClick') : '')}
|
||||
title={cellTitle(t, b.tag, cls, st, isCurrent, mine) + (st ? ' — ' + t('mx.tipClick') : '')}
|
||||
onClick={st ? () => setSlot({ band: b.tag, cls }) : undefined}
|
||||
className={cn(
|
||||
'w-[28px] h-[24px] rounded transition-colors p-0',
|
||||
'relative w-[28px] h-[24px] rounded transition-colors p-0',
|
||||
st ? STATUS_CLASSES[st] : 'bg-mx-none',
|
||||
// Only a filled cell has anything to show — an empty one
|
||||
// stays inert rather than opening a "no QSOs" dialog.
|
||||
st && 'cursor-pointer hover:brightness-110',
|
||||
isCurrent && 'ring-2 ring-mx-cur ring-inset',
|
||||
)}
|
||||
/>
|
||||
>
|
||||
{mine ? <CallMark /> : null}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
@@ -337,11 +370,13 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
|
||||
<span key={l.label} className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block size-3 rounded shrink-0',
|
||||
'relative inline-block size-3 rounded shrink-0',
|
||||
l.swatch,
|
||||
l.ring && 'ring-2 ring-mx-cur ring-inset',
|
||||
)}
|
||||
/>
|
||||
>
|
||||
{l.mark ? <CallMark /> : null}
|
||||
</span>
|
||||
{t(l.label)}
|
||||
</span>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user