feat: New badges in cluster view for new Counties and new POTA
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user