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:
+5
-3
@@ -3,10 +3,12 @@
|
|||||||
"version": "0.26.20",
|
"version": "0.26.20",
|
||||||
"date": "",
|
"date": "",
|
||||||
"en": [
|
"en": [
|
||||||
"Each radio carries its own MY_RIG (Settings → CAT), written on every QSO made with it — ahead of the per-band station in Operating conditions, which says what you planned to use rather than which radio is keying. Left empty, nothing changes."
|
"Each radio carries its own MY_RIG (Settings → CAT), written on every QSO made with it — ahead of the per-band station in Operating conditions, which says what you planned to use rather than which radio is keying. Left empty, nothing changes.",
|
||||||
|
"Worked-before matrix: a dot in the corner of a cell now means the callsign you are working has already been worked on that slot, whatever colour the entity status gave the cell."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Chaque radio porte son propre MY_RIG (Réglages → CAT), inscrit sur chaque QSO fait avec elle — avant la station par bande des Conditions de trafic, qui dit ce qui était prévu et non quelle radio émet. Laissé vide, rien ne change."
|
"Chaque radio porte son propre MY_RIG (Réglages → CAT), inscrit sur chaque QSO fait avec elle — avant la station par bande des Conditions de trafic, qui dit ce qui était prévu et non quelle radio émet. Laissé vide, rien ne change.",
|
||||||
|
"Matrice des contacts : un point dans le coin d'une case indique que l'indicatif en cours a déjà été contacté sur ce créneau, quelle que soit la couleur donnée par le statut de l'entité."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1929,4 +1931,4 @@
|
|||||||
"Ce résumé « Nouveautés » s'affiche désormais au premier lancement après chaque mise à jour."
|
"Ce résumé « Nouveautés » s'affiche désormais au premier lancement après chaque mise à jour."
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -81,23 +81,38 @@ const STATUS_CLASSES: Record<string, string> = {
|
|||||||
// i18n keys the Appearance panel's colour pickers use, so the two can never
|
// 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
|
// disagree about which green is which. swatch = the background class (or a
|
||||||
// special ring marker for the current-entry cell).
|
// 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-conf', label: 'mx.callConf' },
|
||||||
{ swatch: 'bg-mx-call-work', label: 'mx.callWork' },
|
{ swatch: 'bg-mx-call-work', label: 'mx.callWork' },
|
||||||
{ swatch: 'bg-mx-dx-conf', label: 'mx.dxConf' },
|
{ swatch: 'bg-mx-dx-conf', label: 'mx.dxConf' },
|
||||||
{ swatch: 'bg-mx-dx-work', label: 'mx.dxWork' },
|
{ swatch: 'bg-mx-dx-work', label: 'mx.dxWork' },
|
||||||
{ swatch: 'bg-mx-none', label: 'mx.none' },
|
{ swatch: 'bg-mx-none', label: 'mx.none' },
|
||||||
{ swatch: 'bg-mx-none', ring: true, label: 'mx.current' },
|
{ 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 =
|
const desc =
|
||||||
status === 'call_c' ? t('mx.tipCallConf') :
|
status === 'call_c' ? t('mx.tipCallConf') :
|
||||||
status === 'call_w' ? t('mx.tipCallWork') :
|
status === 'call_w' ? t('mx.tipCallWork') :
|
||||||
status === 'dxcc_c' ? t('mx.tipDxConf') :
|
status === 'dxcc_c' ? t('mx.tipDxConf') :
|
||||||
status === 'dxcc_w' ? t('mx.tipDxWork') :
|
status === 'dxcc_w' ? t('mx.tipDxWork') :
|
||||||
t('mx.tipNone');
|
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) {
|
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;
|
return m;
|
||||||
}, [wb]);
|
}, [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.
|
// "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
|
// Derived straight from the entity's real band_status (all bands it was
|
||||||
// worked on — not just the operator's configured column list).
|
// worked on — not just the operator's configured column list).
|
||||||
@@ -308,21 +333,29 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
|
|||||||
</th>
|
</th>
|
||||||
{cols.map((b) => {
|
{cols.map((b) => {
|
||||||
const st = statusMap.get(`${b.tag}|${cls}`) ?? '';
|
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;
|
const isCurrent = hasCall && b.tag === currentBand && classCurrent;
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
key={b.tag}
|
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}
|
onClick={st ? () => setSlot({ band: b.tag, cls }) : undefined}
|
||||||
className={cn(
|
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',
|
st ? STATUS_CLASSES[st] : 'bg-mx-none',
|
||||||
// Only a filled cell has anything to show — an empty one
|
// Only a filled cell has anything to show — an empty one
|
||||||
// stays inert rather than opening a "no QSOs" dialog.
|
// stays inert rather than opening a "no QSOs" dialog.
|
||||||
st && 'cursor-pointer hover:brightness-110',
|
st && 'cursor-pointer hover:brightness-110',
|
||||||
isCurrent && 'ring-2 ring-mx-cur ring-inset',
|
isCurrent && 'ring-2 ring-mx-cur ring-inset',
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
|
{mine ? <CallMark /> : null}
|
||||||
|
</td>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tr>
|
</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 key={l.label} className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-block size-3 rounded shrink-0',
|
'relative inline-block size-3 rounded shrink-0',
|
||||||
l.swatch,
|
l.swatch,
|
||||||
l.ring && 'ring-2 ring-mx-cur ring-inset',
|
l.ring && 'ring-2 ring-mx-cur ring-inset',
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
|
{l.mark ? <CallMark /> : null}
|
||||||
|
</span>
|
||||||
{t(l.label)}
|
{t(l.label)}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ const en: Dict = {
|
|||||||
'mx.tipCallConf': 'This callsign confirmed', 'mx.tipCallWork': 'This callsign worked (not confirmed)',
|
'mx.tipCallConf': 'This callsign confirmed', 'mx.tipCallWork': 'This callsign worked (not confirmed)',
|
||||||
'mx.tipDxConf': 'Entity confirmed (other callsign)', 'mx.tipDxWork': 'Entity worked (other callsign)',
|
'mx.tipDxConf': 'Entity confirmed (other callsign)', 'mx.tipDxWork': 'Entity worked (other callsign)',
|
||||||
'mx.tipNone': 'Never worked', 'mx.tipClick': 'click to list the QSOs',
|
'mx.tipNone': 'Never worked', 'mx.tipClick': 'click to list the QSOs',
|
||||||
|
'mx.thisCall': 'this callsign',
|
||||||
|
'mx.tipThisCall': 'already worked with this callsign',
|
||||||
|
'mx.tipThisCallConf': 'already confirmed with this callsign',
|
||||||
// FTx decodes panel (Tools -> FT decodes)
|
// FTx decodes panel (Tools -> FT decodes)
|
||||||
'dec.tab': 'FT decodes', 'dec.title': 'Decodes', 'dec.cqOnly': 'CQ only',
|
'dec.tab': 'FT decodes', 'dec.title': 'Decodes', 'dec.cqOnly': 'CQ only',
|
||||||
'dec.allBands': 'All bands', 'dec.allModes': 'All modes', 'toast.qsoLogged': 'QSO logged', 'dec.contsHint': 'Continents: click to keep one or several', 'dec.allConts': 'All continents',
|
'dec.allBands': 'All bands', 'dec.allModes': 'All modes', 'toast.qsoLogged': 'QSO logged', 'dec.contsHint': 'Continents: click to keep one or several', 'dec.allConts': 'All continents',
|
||||||
@@ -613,6 +616,9 @@ const fr: Dict = {
|
|||||||
'mx.tipCallConf': 'Cet indicatif est confirmé', 'mx.tipCallWork': 'Cet indicatif est contacté (non confirmé)',
|
'mx.tipCallConf': 'Cet indicatif est confirmé', 'mx.tipCallWork': 'Cet indicatif est contacté (non confirmé)',
|
||||||
'mx.tipDxConf': 'Entité confirmée (autre indicatif)', 'mx.tipDxWork': 'Entité contactée (autre indicatif)',
|
'mx.tipDxConf': 'Entité confirmée (autre indicatif)', 'mx.tipDxWork': 'Entité contactée (autre indicatif)',
|
||||||
'mx.tipNone': 'Jamais contacté', 'mx.tipClick': 'cliquer pour lister les QSO',
|
'mx.tipNone': 'Jamais contacté', 'mx.tipClick': 'cliquer pour lister les QSO',
|
||||||
|
'mx.thisCall': 'cet indicatif',
|
||||||
|
'mx.tipThisCall': 'déjà contacté avec cet indicatif',
|
||||||
|
'mx.tipThisCallConf': 'déjà confirmé avec cet indicatif',
|
||||||
// Panneau des decodes FTx (Outils -> Decodes FT)
|
// Panneau des decodes FTx (Outils -> Decodes FT)
|
||||||
'dec.tab': 'Decodes FT', 'dec.title': 'Decodes', 'dec.cqOnly': 'CQ seulement',
|
'dec.tab': 'Decodes FT', 'dec.title': 'Decodes', 'dec.cqOnly': 'CQ seulement',
|
||||||
'dec.allBands': 'Toutes bandes', 'dec.allModes': 'Tous modes', 'toast.qsoLogged': 'QSO enregistré', 'dec.contsHint': 'Continents : clique pour en garder un ou plusieurs', 'dec.allConts': 'Tous continents',
|
'dec.allBands': 'Toutes bandes', 'dec.allModes': 'Tous modes', 'toast.qsoLogged': 'QSO enregistré', 'dec.contsHint': 'Continents : clique pour en garder un ou plusieurs', 'dec.allConts': 'Tous continents',
|
||||||
|
|||||||
@@ -5027,6 +5027,7 @@ export namespace qso {
|
|||||||
band: string;
|
band: string;
|
||||||
class: string;
|
class: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
call?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new BandStatus(source);
|
return new BandStatus(source);
|
||||||
@@ -5037,6 +5038,7 @@ export namespace qso {
|
|||||||
this.band = source["band"];
|
this.band = source["band"];
|
||||||
this.class = source["class"];
|
this.class = source["class"];
|
||||||
this.status = source["status"];
|
this.status = source["status"];
|
||||||
|
this.call = source["call"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class Bucket {
|
export class Bucket {
|
||||||
|
|||||||
@@ -1901,10 +1901,24 @@ type WorkedBefore struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BandStatus is one cell in the worked-before grid.
|
// BandStatus is one cell in the worked-before grid.
|
||||||
|
//
|
||||||
|
// Status is the single highest thing true of the cell, which is what colours
|
||||||
|
// it. Call is the SAME cell's answer to a different question — "have I worked
|
||||||
|
// THIS callsign here" — kept separately because the two are asked at the same
|
||||||
|
// moment and one was hiding the other.
|
||||||
|
//
|
||||||
|
// Chasing an expedition, an operator needs both: whether the slot is still
|
||||||
|
// missing for the entity (does this fill a DXCC hole) and whether this
|
||||||
|
// expedition has already been worked on it (would this be a dupe). A confirmed
|
||||||
|
// entity outranks a worked callsign in Status — correctly, for awards — so a
|
||||||
|
// slot worked with the DX yesterday can read "entity confirmed" and say nothing
|
||||||
|
// at all about yesterday.
|
||||||
type BandStatus struct {
|
type BandStatus struct {
|
||||||
Band string `json:"band"` // ADIF lowercase band, e.g. "20m"
|
Band string `json:"band"` // ADIF lowercase band, e.g. "20m"
|
||||||
Class string `json:"class"` // "PH" | "CW" | "DIG"
|
Class string `json:"class"` // "PH" | "CW" | "DIG"
|
||||||
Status string `json:"status"` // "call_c" | "call_w" | "dxcc_c" | "dxcc_w"
|
Status string `json:"status"` // "call_c" | "call_w" | "dxcc_c" | "dxcc_w"
|
||||||
|
// Call is "", "w" (worked with this callsign) or "c" (confirmed with it).
|
||||||
|
Call string `json:"call,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Band-status codes, lowest first. The ORDER is the rule: a cell shows the
|
// Band-status codes, lowest first. The ORDER is the rule: a cell shows the
|
||||||
@@ -2243,6 +2257,8 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int,
|
|||||||
}
|
}
|
||||||
type cellKey struct{ band, class string }
|
type cellKey struct{ band, class string }
|
||||||
best := map[cellKey]int{}
|
best := map[cellKey]int{}
|
||||||
|
// The call's own answer per cell, independent of the ladder above.
|
||||||
|
callByCell := map[cellKey]string{}
|
||||||
for statusRows.Next() {
|
for statusRows.Next() {
|
||||||
var band, mode string
|
var band, mode string
|
||||||
var callW, callC, dxccConfirmed int
|
var callW, callC, dxccConfirmed int
|
||||||
@@ -2255,12 +2271,21 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int,
|
|||||||
if cur, ok := best[k]; !ok || code > cur {
|
if cur, ok := best[k]; !ok || code > cur {
|
||||||
best[k] = code
|
best[k] = code
|
||||||
}
|
}
|
||||||
|
// Confirmed beats worked here too, and neither is ever erased by the
|
||||||
|
// entity: this is only ever about the callsign.
|
||||||
|
switch {
|
||||||
|
case callC == 1:
|
||||||
|
callByCell[k] = "c"
|
||||||
|
case callW == 1 && callByCell[k] == "":
|
||||||
|
callByCell[k] = "w"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
statusRows.Close()
|
statusRows.Close()
|
||||||
codeStr := bandStatusNames
|
codeStr := bandStatusNames
|
||||||
for k, code := range best {
|
for k, code := range best {
|
||||||
wb.BandStatus = append(wb.BandStatus, BandStatus{
|
wb.BandStatus = append(wb.BandStatus, BandStatus{
|
||||||
Band: k.band, Class: k.class, Status: codeStr[code],
|
Band: k.band, Class: k.class, Status: codeStr[code],
|
||||||
|
Call: callByCell[k],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return wb, nil
|
return wb, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user