feat: added colonns for awards in recent qso

This commit is contained in:
2026-06-18 23:20:24 +02:00
parent 183db7ac2b
commit 45d081ac0c
6 changed files with 180 additions and 13 deletions
+39 -3
View File
@@ -33,6 +33,7 @@ import {
GetAwardDefs,
GetUIPref,
ReportLiveActivity,
AwardRefsForQSOs,
} from '../wailsjs/go/main/App';
import { Combobox } from '@/components/ui/combobox';
import { applyAwardRefs } from '@/lib/awardRefs';
@@ -764,6 +765,40 @@ export default function App() {
const wbTimerRef = useRef<number | null>(null);
const [wb, setWb] = useState<WB | null>(null);
const [wbBusy, setWbBusy] = useState(false);
// Per-award columns for the Recent QSOs / Worked-before grids: load the award
// list once, then compute each shown QSO's reference per award and attach it
// to the rows (the grids render one hideable column per award).
const [awardCols, setAwardCols] = useState<{ code: string; name: string }[]>([]);
useEffect(() => {
GetAwardDefs().then((defs: any[]) =>
setAwardCols(((defs ?? []) as any[]).map((d) => ({ code: d.code, name: d.name })).sort((a, b) => a.code.localeCompare(b.code))),
).catch(() => {});
}, []);
const [qsoAwardRefs, setQsoAwardRefs] = useState<Record<string, Record<string, string>>>({});
useEffect(() => {
const ids = (qsos as any[]).map((q) => q.id).filter(Boolean);
if (ids.length === 0 || awardCols.length === 0) { setQsoAwardRefs({}); return; }
let alive = true;
AwardRefsForQSOs(ids as any).then((m: any) => { if (alive) setQsoAwardRefs(m ?? {}); }).catch(() => {});
return () => { alive = false; };
}, [qsos, awardCols.length]);
const qsosWithAwards = useMemo(
() => (qsos as any[]).map((q) => ({ ...q, award_refs: qsoAwardRefs[String(q.id)] })),
[qsos, qsoAwardRefs],
);
const [wbAwardRefs, setWbAwardRefs] = useState<Record<string, Record<string, string>>>({});
useEffect(() => {
const ids = ((wb?.entries ?? []) as any[]).map((e) => e.id).filter(Boolean);
if (ids.length === 0 || awardCols.length === 0) { setWbAwardRefs({}); return; }
let alive = true;
AwardRefsForQSOs(ids as any).then((m: any) => { if (alive) setWbAwardRefs(m ?? {}); }).catch(() => {});
return () => { alive = false; };
}, [wb, awardCols.length]);
const wbWithAwards = useMemo(
() => (wb ? { ...wb, entries: ((wb.entries ?? []) as any[]).map((e) => ({ ...e, award_refs: wbAwardRefs[String(e.id)] })) } : null),
[wb, wbAwardRefs],
);
// Always-current copy of the entry callsign, so the UDP event handlers
// (which live in a []-deps effect with a stale `callsign` closure) can
// tell whether an incoming DX call actually changed anything.
@@ -2542,7 +2577,7 @@ export default function App() {
case 'worked':
return (
<div className="h-full w-full min-h-0 flex flex-col bg-card border border-border rounded-lg overflow-hidden">
<WorkedBeforeGrid wb={wb} busy={wbBusy} currentCall={callsign} onRowDoubleClicked={(q) => openEdit(q.id as number)}
<WorkedBeforeGrid wb={wbWithAwards as any} awardCols={awardCols} busy={wbBusy} currentCall={callsign} onRowDoubleClicked={(q) => openEdit(q.id as number)}
onUpdateFromCty={bulkUpdateFromCty} onUpdateFromQRZ={bulkUpdateFromQRZ} onUpdateFromClublog={bulkUpdateFromClublog}
onSendTo={bulkSendTo} onSendRecording={bulkSendRecording} onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)} />
</div>
@@ -3225,8 +3260,9 @@ export default function App() {
)}
<RecentQSOsGrid
rows={qsos as any}
rows={qsosWithAwards as any}
total={total}
awardCols={awardCols}
onRowDoubleClicked={(q) => openEdit(q.id as number)}
onUpdateFromCty={bulkUpdateFromCty}
onUpdateFromQRZ={bulkUpdateFromQRZ}
@@ -3410,7 +3446,7 @@ export default function App() {
</TabsContent>
<TabsContent value="worked" className="mt-0 flex flex-col min-h-0 flex-1">
<WorkedBeforeGrid wb={wb} busy={wbBusy} currentCall={callsign} onRowDoubleClicked={(q) => openEdit(q.id as number)}
<WorkedBeforeGrid wb={wbWithAwards as any} awardCols={awardCols} busy={wbBusy} currentCall={callsign} onRowDoubleClicked={(q) => openEdit(q.id as number)}
onUpdateFromCty={bulkUpdateFromCty} onUpdateFromQRZ={bulkUpdateFromQRZ} onUpdateFromClublog={bulkUpdateFromClublog} onSendTo={bulkSendTo} onSendRecording={bulkSendRecording}
onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)} />
</TabsContent>
+39 -5
View File
@@ -56,6 +56,9 @@ type Props = {
onBulkEdit?: (ids: number[]) => void;
onExportSelected?: (ids: number[]) => void;
onExportFiltered?: () => void;
// One column per defined award; the cell shows the reference this QSO counts
// for (from row.award_refs[CODE], attached by the parent). Hidden by default.
awardCols?: { code: string; name: string }[];
};
const COL_STATE_KEY = 'hamlog.qsoColState.v2';
@@ -219,7 +222,7 @@ export const GROUP_ORDER = [
'Contest', 'Propagation', 'My station', 'Misc',
];
export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportFiltered }: Props) {
export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportFiltered, awardCols }: Props) {
const gridRef = useRef<any>(null);
const [pickerOpen, setPickerOpen] = useState(false);
const [menu, setMenu] = useState<QSOMenuState>(null);
@@ -245,10 +248,21 @@ export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpda
// Compute initial column defs: all columns defined, but those not marked
// defaultVisible start hidden. The user's saved state (loaded onGridReady)
// overrides this so a previously toggled column wins.
const columnDefs = useMemo<ColDef<QSOForm>[]>(() => COL_CATALOG.map((c) => {
const { group: _g, label: _l, defaultVisible, ...rest } = c;
return { ...rest, hide: !defaultVisible };
}), []);
const columnDefs = useMemo<ColDef<QSOForm>[]>(() => {
const base = COL_CATALOG.map((c) => {
const { group: _g, label: _l, defaultVisible, ...rest } = c;
return { ...rest, hide: !defaultVisible };
});
const awards: ColDef<QSOForm>[] = (awardCols ?? []).map((a) => ({
colId: `award_${a.code}`,
headerName: a.code,
headerTooltip: `${a.name} — reference this QSO counts for`,
width: 110,
cellClass: 'text-[11px]',
valueGetter: (p) => (p.data as any)?.award_refs?.[a.code.toUpperCase()] ?? '',
}));
return [...base, ...awards];
}, [awardCols]);
const defaultColDef = useMemo<ColDef>(() => ({
sortable: true,
@@ -407,6 +421,26 @@ export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpda
</div>
);
})}
{awardCols && awardCols.length > 0 && (
<div className="rounded-md border border-border p-2.5">
<div className="flex items-center justify-between mb-2 pb-1.5 border-b border-border/60">
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Awards</span>
<div className="flex gap-0.5">
<button className="text-[10px] text-primary hover:underline px-1" onClick={() => { awardCols.forEach((a) => setColVisible(`award_${a.code}`, true)); }}>all</button>
<button className="text-[10px] text-muted-foreground hover:underline px-1" onClick={() => { awardCols.forEach((a) => setColVisible(`award_${a.code}`, false)); }}>none</button>
</div>
</div>
<div className="flex flex-col gap-1">
{awardCols.map((a) => (
<label key={a.code} className="flex items-center gap-2 text-xs cursor-pointer hover:bg-accent/30 rounded px-1 py-0.5">
<Checkbox checked={isColVisible(`award_${a.code}`)} onCheckedChange={(v) => setColVisible(`award_${a.code}`, !!v)} />
<span className="font-mono font-semibold">{a.code}</span>
<span className="text-muted-foreground truncate">{a.name}</span>
</label>
))}
</div>
</div>
)}
</div>
<DialogFooter>
<Button variant="ghost" size="sm" onClick={resetDefaults}>Reset to defaults</Button>
+38 -5
View File
@@ -53,6 +53,8 @@ type Props = {
onSendTo?: (service: string, ids: number[]) => void;
onSendRecording?: (ids: number[]) => void;
onSendEQSL?: (ids: number[]) => void;
// One column per defined award (cell = the reference this QSO counts for).
awardCols?: { code: string; name: string }[];
};
const COL_STATE_KEY = 'hamlog.workedBeforeColState.v2';
@@ -65,7 +67,7 @@ function fmtDate(s: any): string {
return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}`;
}
export function WorkedBeforeGrid({ wb, busy, currentCall, onRowDoubleClicked, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL }: Props) {
export function WorkedBeforeGrid({ wb, busy, currentCall, onRowDoubleClicked, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, awardCols }: Props) {
const gridRef = useRef<any>(null);
const [pickerOpen, setPickerOpen] = useState(false);
const [menu, setMenu] = useState<QSOMenuState>(null);
@@ -93,10 +95,21 @@ export function WorkedBeforeGrid({ wb, busy, currentCall, onRowDoubleClicked, on
const count = wb?.count ?? 0;
const entries = wb?.entries ?? [];
const columnDefs = useMemo<ColDef<WorkedEntry>[]>(() => COL_CATALOG.map((c) => {
const { group: _g, label: _l, defaultVisible, ...rest } = c;
return { ...rest, hide: !defaultVisible };
}), []);
const columnDefs = useMemo<ColDef<WorkedEntry>[]>(() => {
const base = COL_CATALOG.map((c) => {
const { group: _g, label: _l, defaultVisible, ...rest } = c;
return { ...rest, hide: !defaultVisible };
});
const awards: ColDef<WorkedEntry>[] = (awardCols ?? []).map((a) => ({
colId: `award_${a.code}`,
headerName: a.code,
headerTooltip: `${a.name} — reference this QSO counts for`,
width: 110,
cellClass: 'text-[11px]',
valueGetter: (p) => (p.data as any)?.award_refs?.[a.code.toUpperCase()] ?? '',
}));
return [...base, ...awards];
}, [awardCols]);
const defaultColDef = useMemo<ColDef>(() => ({
sortable: true, resizable: true, filter: true, suppressMovable: false,
@@ -283,6 +296,26 @@ export function WorkedBeforeGrid({ wb, busy, currentCall, onRowDoubleClicked, on
</div>
);
})}
{awardCols && awardCols.length > 0 && (
<div className="rounded-md border border-border p-2.5">
<div className="flex items-center justify-between mb-2 pb-1.5 border-b border-border/60">
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Awards</span>
<div className="flex gap-0.5">
<button className="text-[10px] text-primary hover:underline px-1" onClick={() => { awardCols.forEach((a) => setColVisible(`award_${a.code}`, true)); }}>all</button>
<button className="text-[10px] text-muted-foreground hover:underline px-1" onClick={() => { awardCols.forEach((a) => setColVisible(`award_${a.code}`, false)); }}>none</button>
</div>
</div>
<div className="flex flex-col gap-1">
{awardCols.map((a) => (
<label key={a.code} className="flex items-center gap-2 text-xs cursor-pointer hover:bg-accent/30 rounded px-1 py-0.5">
<Checkbox checked={isColVisible(`award_${a.code}`)} onCheckedChange={(v) => setColVisible(`award_${a.code}`, !!v)} />
<span className="font-mono font-semibold">{a.code}</span>
<span className="text-muted-foreground truncate">{a.name}</span>
</label>
))}
</div>
</div>
)}
</div>
<DialogFooter>
<Button variant="ghost" size="sm" onClick={resetDefaults}>Reset to defaults</Button>