feat: New badges in cluster view for new Counties and new POTA

This commit is contained in:
2026-07-17 15:28:36 +02:00
parent dd3b51a2ae
commit eb2ff8ed59
13 changed files with 3299 additions and 3189 deletions
+5 -3
View File
@@ -1063,7 +1063,7 @@ export default function App() {
// Cached per-call slot status: "new" | "new-band" | "new-slot" | "worked".
// Keyed by `${call}|${band}|${mode}` so two spots of the same call on
// different slots don't share the same colour.
const [spotStatus, setSpotStatus] = useState<Record<string, { status: string; country?: string; continent?: string; worked_call?: boolean }>>({});
const [spotStatus, setSpotStatus] = useState<Record<string, { status: string; country?: string; continent?: string; worked_call?: boolean; new_county?: boolean; new_pota?: boolean }>>({});
// === Modals ===
const [editingQSO, setEditingQSO] = useState<QSO | null>(null);
@@ -2010,14 +2010,14 @@ export default function App() {
// "new-slot" because the lookup key carried mode="".
useEffect(() => {
const t = window.setTimeout(async () => {
const unknown: { call: string; band: string; mode: string }[] = [];
const unknown: { call: string; band: string; mode: string; pota_ref: string }[] = [];
const seen = new Set<string>();
for (const s of spots) {
const mode = inferSpotMode(s.comment ?? '', s.freq_hz);
const k = spotStatusKey(s.dx_call, s.band ?? '', s.comment ?? '', s.freq_hz);
if (seen.has(k) || spotStatus[k]) continue;
seen.add(k);
unknown.push({ call: s.dx_call, band: s.band ?? '', mode });
unknown.push({ call: s.dx_call, band: s.band ?? '', mode, pota_ref: (s as any).pota_ref ?? '' });
}
if (unknown.length === 0) return;
try {
@@ -2031,6 +2031,8 @@ export default function App() {
country: r.country,
continent: (r as any).continent,
worked_call: !!(r as any).worked_call,
new_county: !!(r as any).new_county,
new_pota: !!(r as any).new_pota,
};
}
return next;
+29 -13
View File
@@ -51,6 +51,8 @@ export type SpotStatusEntry = {
country?: string;
continent?: string;
worked_call?: boolean;
new_county?: boolean;
new_pota?: boolean;
};
type Props = {
@@ -141,26 +143,40 @@ const makeColCatalog = (t: TFn): ColEntry[] => [
},
{
group: 'Spot', label: t('clg2.c.status'), colId: 'status',
headerName: t('clg2.c.status'), width: 96, sortable: true,
headerName: t('clg2.c.status'), width: 120, sortable: true,
defaultVisible: true,
// Spells out the slot status as a text badge so NEW SLOT (and the others)
// is obvious at the row level, not just a single coloured cell.
// is obvious at the row level, not just a single coloured cell. NEW COUNTY
// and NEW POTA are orthogonal, so they stack as extra badges.
valueGetter: (p: any) => {
const s = statusFor(p);
if (s?.status === 'new') return t('clg2.newDxcc');
if (s?.status === 'new-band') return t('clg2.newBand');
if (s?.status === 'new-mode') return t('clg2.newMode');
if (s?.status === 'new-slot') return t('clg2.newSlot');
return s?.worked_call ? t('clg2.wkdCall') : '';
const parts: string[] = [];
if (s?.status === 'new') parts.push(t('clg2.newDxcc'));
else if (s?.status === 'new-band') parts.push(t('clg2.newBand'));
else if (s?.status === 'new-mode') parts.push(t('clg2.newMode'));
else if (s?.status === 'new-slot') parts.push(t('clg2.newSlot'));
else if (s?.worked_call) parts.push(t('clg2.wkdCall'));
if (s?.new_county) parts.push(t('clg2.newCounty'));
if (s?.new_pota) parts.push(t('clg2.newPota'));
return parts.join(' ');
},
cellRenderer: (p: any) => {
const b = statusBadge(t, statusFor(p));
if (!b) return <span style={{ color: '#a8a29e', fontSize: 10 }}></span>;
const s = statusFor(p);
const badges: { text: string; fg: string; bg: string }[] = [];
const b = statusBadge(t, s);
if (b) badges.push(b);
if (s?.new_county) badges.push({ text: t('clg2.newCounty'), fg: '#6b21a8', bg: '#f3e8ff' });
if (s?.new_pota) badges.push({ text: t('clg2.newPota'), fg: '#166534', bg: '#dcfce7' });
if (badges.length === 0) return <span style={{ color: '#a8a29e', fontSize: 10 }}></span>;
return (
<span style={{
backgroundColor: b.bg, color: b.fg, fontWeight: 700, fontSize: 10,
padding: '1px 6px', borderRadius: 4, letterSpacing: 0.3, whiteSpace: 'nowrap',
}}>{b.text}</span>
<span style={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center' }}>
{badges.map((bd, i) => (
<span key={i} style={{
backgroundColor: bd.bg, color: bd.fg, fontWeight: 700, fontSize: 10,
padding: '1px 5px', borderRadius: 4, letterSpacing: 0.3, whiteSpace: 'nowrap',
}}>{bd.text}</span>
))}
</span>
);
},
tooltipValueGetter: (p: any) => {
+11 -4
View File
@@ -233,6 +233,13 @@ const GRP_KEYS: Record<string, string> = {
};
export const groupLabel = (t: TFn, g: string): string => t(GRP_KEYS[g] ?? g);
// Award columns are governed SOLELY by the awardShown code-set, never by AG
// Grid's saved column state. Stripping them here (on both save and restore)
// stops a stale saved state from re-hiding a shown award column on every
// awardCols rebuild — the desync that made award columns vanish mid-session.
const stripAwardCols = (st: any[] | null | undefined): any[] =>
(st ?? []).filter((s) => !String(s?.colId ?? '').startsWith('award_'));
export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportFiltered, onExportCabrilloSelected, onExportCabrilloFiltered, onDelete, awardCols }: Props) {
const { t } = useI18n();
const gridRef = useRef<any>(null);
@@ -335,12 +342,12 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo
function onGridReady(e: GridReadyEvent) {
const local = loadLocal(COL_STATE_KEY);
if (local) e.api.applyColumnState({ state: local as ColumnState[], applyOrder: true });
if (local) e.api.applyColumnState({ state: stripAwardCols(local) as ColumnState[], applyOrder: true });
// Fall back to the portable DB copy when the local cache is empty
// (fresh machine / after a reinstall), then re-seed the cache.
loadRemote(COL_STATE_KEY).then((remote) => {
if (remote && !local) {
e.api.applyColumnState({ state: remote as ColumnState[], applyOrder: true });
e.api.applyColumnState({ state: stripAwardCols(remote) as ColumnState[], applyOrder: true });
seedLocal(COL_STATE_KEY, remote);
}
});
@@ -348,7 +355,7 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo
const saveColumnState = useCallback(() => {
if (restoringRef.current) return; // ignore the events fired by a column rebuild
const state = gridRef.current?.api?.getColumnState();
if (state) saveState(COL_STATE_KEY, state);
if (state) saveState(COL_STATE_KEY, stripAwardCols(state));
}, []);
// The award columns load asynchronously; when they arrive (or change) the
@@ -359,7 +366,7 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo
useEffect(() => {
const api = gridRef.current?.api;
const local = loadLocal(COL_STATE_KEY);
if (api && local) api.applyColumnState({ state: local as ColumnState[], applyOrder: true });
if (api && local) api.applyColumnState({ state: stripAwardCols(local) as ColumnState[], applyOrder: true });
// Re-enable saving once AG Grid has settled the column events from the rebuild.
const t = window.setTimeout(() => { restoringRef.current = false; }, 0);
return () => window.clearTimeout(t);
File diff suppressed because one or more lines are too long
+6
View File
@@ -2387,6 +2387,7 @@ export namespace main {
call: string;
band: string;
mode: string;
pota_ref?: string;
static createFrom(source: any = {}) {
return new SpotQuery(source);
@@ -2397,6 +2398,7 @@ export namespace main {
this.call = source["call"];
this.band = source["band"];
this.mode = source["mode"];
this.pota_ref = source["pota_ref"];
}
}
export class SpotStatus {
@@ -2407,6 +2409,8 @@ export namespace main {
continent?: string;
status: string;
worked_call: boolean;
new_county: boolean;
new_pota: boolean;
static createFrom(source: any = {}) {
return new SpotStatus(source);
@@ -2421,6 +2425,8 @@ export namespace main {
this.continent = source["continent"];
this.status = source["status"];
this.worked_call = source["worked_call"];
this.new_county = source["new_county"];
this.new_pota = source["new_pota"];
}
}
export class StartupStatus {