import { useCallback, useMemo, useRef, useState } from 'react'; import { AllCommunityModule, ModuleRegistry, themeQuartz, type ColDef, type ColumnState, type GridReadyEvent, type RowDoubleClickedEvent, } from 'ag-grid-community'; import { AgGridReact } from 'ag-grid-react'; import { Columns3, FilterX } from 'lucide-react'; import type { QSOForm } from '@/types'; import { QSOContextMenu, type QSOMenuState } from './QSOContextMenu'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { loadLocal, loadRemote, saveState, seedLocal } from '@/lib/gridPrefs'; // Register every Community feature once. v32+ requires explicit registration; // AllCommunityModule keeps it simple and pulls in sort/filter/resize/reorder/ // virtual-scroll — everything we want out of the box for a logbook table. ModuleRegistry.registerModules([AllCommunityModule]); // Custom Quartz theme tuned to match OpsLog's warm palette. const hamlogTheme = themeQuartz.withParams({ fontFamily: 'inherit', fontSize: 12.5, backgroundColor: '#faf6ea', foregroundColor: '#2a2419', headerBackgroundColor: '#e8dfc9', headerTextColor: '#5a4f3a', headerFontWeight: 600, oddRowBackgroundColor: '#f5efe0', rowHoverColor: '#ecdcb4', selectedRowBackgroundColor: '#f0d9a8', borderColor: '#c8b994', rowBorder: { color: '#d8c9a8', width: 1 }, columnBorder: { color: '#d8c9a8', width: 1 }, cellHorizontalPadding: 10, rowHeight: 32, headerHeight: 34, spacing: 4, accentColor: '#b8410c', iconSize: 12, }); type Props = { rows: QSOForm[]; total: number; onRowDoubleClicked?: (q: QSOForm) => void; onRowSelected?: (id: number | null) => void; onUpdateFromCty?: (ids: number[]) => void; onUpdateFromQRZ?: (ids: number[]) => void; onUpdateFromClublog?: (ids: number[]) => void; onSendTo?: (service: string, ids: number[]) => void; onSendRecording?: (ids: number[]) => void; onExportSelected?: (ids: number[]) => void; onExportFiltered?: () => void; }; const COL_STATE_KEY = 'hamlog.qsoColState.v2'; function fmtMhzDots(hz?: number): string { if (!hz) return ''; const mhz = (hz / 1_000_000).toFixed(6); const [i, f] = mhz.split('.'); return `${i}.${f.slice(0, 3)}.${f.slice(3, 6)}`; } function fmtDateUTC(s: any): string { if (!s) return ''; const d = new Date(s); if (isNaN(d.getTime())) return s; const p = (n: number) => String(n).padStart(2, '0'); return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`; } function fmtDateOnly(s: any): string { if (!s) return ''; const t = String(s).trim(); // QSL/LoTW/eQSL/ClubLog dates are ADIF YYYYMMDD; upload dates may be ISO. const m = t.match(/^(\d{4})(\d{2})(\d{2})/); if (m) return `${m[1]}-${m[2]}-${m[3]}`; const d = new Date(t); if (isNaN(d.getTime())) return t; const p = (n: number) => String(n).padStart(2, '0'); return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}`; } // Full catalog of selectable columns, grouped for the picker. `defaultVisible` // = shown out of the box; anything else stays hidden until the user toggles // it in the Columns dialog. export type ColEntry = ColDef & { group: string; label: string; defaultVisible?: boolean }; // Shared so the Worked-before grid (which now also shows full QSO records) // can offer the exact same column choices without duplicating the catalog. export const COL_CATALOG: ColEntry[] = [ // ── QSO basics ── { group: 'QSO', label: 'Date UTC', colId: 'qso_date', headerName: 'Date UTC', field: 'qso_date' as any, width: 150, cellClass: 'font-mono', valueFormatter: (p) => fmtDateUTC(p.value), sort: 'desc', defaultVisible: true }, { group: 'QSO', label: 'Date Off', colId: 'qso_date_off', headerName: 'Date off', field: 'qso_date_off' as any, width: 150, cellClass: 'font-mono', valueFormatter: (p) => fmtDateUTC(p.value) }, { group: 'QSO', label: 'Callsign', colId: 'callsign', headerName: 'Callsign', field: 'callsign' as any, width: 110, cellClass: 'font-mono font-semibold', cellStyle: { color: '#b8410c' }, defaultVisible: true }, { group: 'QSO', label: 'Band', colId: 'band', headerName: 'Band', field: 'band' as any, width: 75, cellClass: 'font-mono', defaultVisible: true }, { group: 'QSO', label: 'Band RX', colId: 'band_rx', headerName: 'Band RX', field: 'band_rx' as any, width: 75, cellClass: 'font-mono' }, { group: 'QSO', label: 'Mode', colId: 'mode', headerName: 'Mode', field: 'mode' as any, width: 80, cellClass: 'font-mono', defaultVisible: true }, { group: 'QSO', label: 'Submode', colId: 'submode', headerName: 'Submode', field: 'submode' as any, width: 80, cellClass: 'font-mono' }, { group: 'QSO', label: 'MHz (TX)', colId: 'freq_hz', headerName: 'MHz', field: 'freq_hz' as any, width: 110, type: 'rightAligned', cellClass: 'font-mono', valueFormatter: (p) => fmtMhzDots(p.value as number | undefined), comparator: (a, b) => (a ?? 0) - (b ?? 0), defaultVisible: true }, { group: 'QSO', label: 'MHz (RX)', colId: 'freq_rx_hz', headerName: 'MHz RX', field: 'freq_rx_hz' as any, width: 110, type: 'rightAligned', cellClass: 'font-mono', valueFormatter: (p) => fmtMhzDots(p.value as number | undefined), comparator: (a, b) => (a ?? 0) - (b ?? 0) }, { group: 'QSO', label: 'RST sent', colId: 'rst_sent', headerName: 'RST sent', field: 'rst_sent' as any, width: 85, cellClass: 'font-mono', defaultVisible: true }, { group: 'QSO', label: 'RST rcvd', colId: 'rst_rcvd', headerName: 'RST rcvd', field: 'rst_rcvd' as any, width: 85, cellClass: 'font-mono', defaultVisible: true }, { group: 'QSO', label: 'TX Power', colId: 'tx_pwr', headerName: 'TX Power', field: 'tx_pwr' as any, width: 90, type: 'rightAligned', cellClass: 'font-mono' }, // ── Contacted station ── { group: 'Contacted', label: 'Name', colId: 'name', headerName: 'Name', field: 'name' as any, width: 170, defaultVisible: true }, { group: 'Contacted', label: 'QTH', colId: 'qth', headerName: 'QTH', field: 'qth' as any, width: 200, defaultVisible: true }, { group: 'Contacted', label: 'Address', colId: 'address', headerName: 'Address', field: 'address' as any, width: 200 }, { group: 'Contacted', label: 'Country', colId: 'country', headerName: 'Country', field: 'country' as any, width: 150, defaultVisible: true }, { group: 'Contacted', label: 'State', colId: 'state', headerName: 'State', field: 'state' as any, width: 80 }, { group: 'Contacted', label: 'County', colId: 'cnty', headerName: 'County', field: 'cnty' as any, width: 130 }, { group: 'Contacted', label: 'Continent',colId: 'cont', headerName: 'Cont', field: 'cont' as any, width: 60 }, { group: 'Contacted', label: 'Grid', colId: 'grid', headerName: 'Grid', field: 'grid' as any, width: 85, cellClass: 'font-mono', defaultVisible: true }, { group: 'Contacted', label: 'Grid Ext', colId: 'gridsquare_ext', headerName: 'GridExt', field: 'gridsquare_ext' as any, width: 85, cellClass: 'font-mono' }, { group: 'Contacted', label: 'VUCC grids',colId: 'vucc_grids', headerName: 'VUCC', field: 'vucc_grids' as any, width: 130, cellClass: 'font-mono' }, { group: 'Contacted', label: 'DXCC #', colId: 'dxcc', headerName: 'DXCC #', field: 'dxcc' as any, width: 70, type: 'rightAligned', cellClass: 'font-mono' }, { group: 'Contacted', label: 'CQZ', colId: 'cqz', headerName: 'CQZ', field: 'cqz' as any, width: 60, type: 'rightAligned', cellClass: 'font-mono' }, { group: 'Contacted', label: 'ITU', colId: 'ituz', headerName: 'ITU', field: 'ituz' as any, width: 60, type: 'rightAligned', cellClass: 'font-mono' }, { group: 'Contacted', label: 'IOTA', colId: 'iota', headerName: 'IOTA', field: 'iota' as any, width: 80, cellClass: 'font-mono' }, { group: 'Contacted', label: 'SOTA ref', colId: 'sota_ref', headerName: 'SOTA', field: 'sota_ref' as any, width: 110, cellClass: 'font-mono' }, { group: 'Contacted', label: 'POTA ref', colId: 'pota_ref', headerName: 'POTA', field: 'pota_ref' as any, width: 110, cellClass: 'font-mono' }, { group: 'Contacted', label: 'Age', colId: 'age', headerName: 'Age', field: 'age' as any, width: 60, type: 'rightAligned' }, { group: 'Contacted', label: 'Lat', colId: 'lat', headerName: 'Lat', field: 'lat' as any, width: 90, type: 'rightAligned', cellClass: 'font-mono' }, { group: 'Contacted', label: 'Lon', colId: 'lon', headerName: 'Lon', field: 'lon' as any, width: 90, type: 'rightAligned', cellClass: 'font-mono' }, { group: 'Contacted', label: 'Email', colId: 'email', headerName: 'Email', field: 'email' as any, width: 180 }, { group: 'Contacted', label: 'Web', colId: 'web', headerName: 'Web', field: 'web' as any, width: 180 }, // ── QSL ── { group: 'QSL', label: 'QSL sent', colId: 'qsl_sent', headerName: 'QSL sent', field: 'qsl_sent' as any, width: 80 }, { group: 'QSL', label: 'QSL rcvd', colId: 'qsl_rcvd', headerName: 'QSL rcvd', field: 'qsl_rcvd' as any, width: 80 }, { group: 'QSL', label: 'QSL sent date',colId: 'qsl_sent_date', headerName: 'QSL S date', field: 'qsl_sent_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) }, { group: 'QSL', label: 'QSL rcvd date',colId: 'qsl_rcvd_date', headerName: 'QSL R date', field: 'qsl_rcvd_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) }, { group: 'QSL', label: 'QSL via', colId: 'qsl_via', headerName: 'QSL via', field: 'qsl_via' as any, width: 130 }, { group: 'QSL', label: 'QSL msg', colId: 'qsl_msg', headerName: 'QSL msg', field: 'qsl_msg' as any, width: 200 }, { group: 'QSL', label: 'QSL msg rcvd', colId: 'qslmsg_rcvd', headerName: 'QSL msg rcvd', field: 'qslmsg_rcvd' as any, width: 200 }, // ── LoTW ── { group: 'LoTW', label: 'LoTW sent', colId: 'lotw_sent', headerName: 'LoTW sent', field: 'lotw_sent' as any, width: 80 }, { group: 'LoTW', label: 'LoTW rcvd', colId: 'lotw_rcvd', headerName: 'LoTW rcvd', field: 'lotw_rcvd' as any, width: 80 }, { group: 'LoTW', label: 'LoTW sent date', colId: 'lotw_sent_date', headerName: 'LoTW S date', field: 'lotw_sent_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) }, { group: 'LoTW', label: 'LoTW rcvd date', colId: 'lotw_rcvd_date', headerName: 'LoTW R date', field: 'lotw_rcvd_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) }, // ── eQSL ── { group: 'eQSL', label: 'eQSL sent', colId: 'eqsl_sent', headerName: 'eQSL sent', field: 'eqsl_sent' as any, width: 80 }, { group: 'eQSL', label: 'eQSL rcvd', colId: 'eqsl_rcvd', headerName: 'eQSL rcvd', field: 'eqsl_rcvd' as any, width: 80 }, { group: 'eQSL', label: 'eQSL sent date', colId: 'eqsl_sent_date', headerName: 'eQSL S date', field: 'eqsl_sent_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) }, { group: 'eQSL', label: 'eQSL rcvd date', colId: 'eqsl_rcvd_date', headerName: 'eQSL R date', field: 'eqsl_rcvd_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) }, // ── Uploads (online logbooks) ── // ADIF models these as an "upload status/date" (= YOU pushed the QSO) and, // for QRZ only, a "download status/date" (= it came back confirmed). We // relabel to the same sent/rcvd wording as LoTW/eQSL. Club Log & HRDLog have // NO rcvd field in ADIF — they're upload-only, so only "sent" is shown. { group: 'Uploads', label: 'ClubLog sent', colId: 'clublog_qso_upload_status', headerName: 'ClubLog sent', field: 'clublog_qso_upload_status' as any, width: 100 }, { group: 'Uploads', label: 'ClubLog sent date', colId: 'clublog_qso_upload_date', headerName: 'ClubLog S date', field: 'clublog_qso_upload_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) }, { group: 'Uploads', label: 'HRDLog sent', colId: 'hrdlog_qso_upload_status', headerName: 'HRDLog sent', field: 'hrdlog_qso_upload_status' as any, width: 100 }, { group: 'Uploads', label: 'HRDLog sent date', colId: 'hrdlog_qso_upload_date', headerName: 'HRDLog S date', field: 'hrdlog_qso_upload_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) }, { group: 'Uploads', label: 'QRZ.com sent', colId: 'qrzcom_qso_upload_status', headerName: 'QRZ.com sent', field: 'qrzcom_qso_upload_status' as any, width: 100 }, { group: 'Uploads', label: 'QRZ.com rcvd', colId: 'qrzcom_qso_download_status', headerName: 'QRZ.com rcvd', field: 'qrzcom_qso_download_status' as any, width: 100 }, { group: 'Uploads', label: 'QRZ.com sent date', colId: 'qrzcom_qso_upload_date', headerName: 'QRZ.com S date', field: 'qrzcom_qso_upload_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) }, { group: 'Uploads', label: 'QRZ.com rcvd date', colId: 'qrzcom_qso_download_date', headerName: 'QRZ.com R date', field: 'qrzcom_qso_download_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) }, // ── Contest ── { group: 'Contest', label: 'Contest ID', colId: 'contest_id', headerName: 'Contest', field: 'contest_id' as any, width: 110 }, { group: 'Contest', label: 'SRX', colId: 'srx', headerName: 'SRX', field: 'srx' as any, width: 60, type: 'rightAligned' }, { group: 'Contest', label: 'STX', colId: 'stx', headerName: 'STX', field: 'stx' as any, width: 60, type: 'rightAligned' }, { group: 'Contest', label: 'SRX string', colId: 'srx_string', headerName: 'SRX str', field: 'srx_string' as any, width: 100 }, { group: 'Contest', label: 'STX string', colId: 'stx_string', headerName: 'STX str', field: 'stx_string' as any, width: 100 }, { group: 'Contest', label: 'Check', colId: 'check', headerName: 'Check', field: 'check' as any, width: 70 }, { group: 'Contest', label: 'Precedence', colId: 'precedence', headerName: 'Precedence', field: 'precedence' as any, width: 90 }, { group: 'Contest', label: 'ARRL section',colId: 'arrl_sect', headerName: 'ARRL sect', field: 'arrl_sect' as any, width: 90 }, // ── Propagation / antenna ── { group: 'Propagation', label: 'Prop mode', colId: 'prop_mode', headerName: 'Prop', field: 'prop_mode' as any, width: 80 }, { group: 'Propagation', label: 'Sat name', colId: 'sat_name', headerName: 'Sat', field: 'sat_name' as any, width: 110 }, { group: 'Propagation', label: 'Sat mode', colId: 'sat_mode', headerName: 'Sat mode', field: 'sat_mode' as any, width: 80 }, { group: 'Propagation', label: 'Ant az', colId: 'ant_az', headerName: 'Az', field: 'ant_az' as any, width: 70, type: 'rightAligned' }, { group: 'Propagation', label: 'Ant el', colId: 'ant_el', headerName: 'El', field: 'ant_el' as any, width: 70, type: 'rightAligned' }, { group: 'Propagation', label: 'Ant path', colId: 'ant_path', headerName: 'Path', field: 'ant_path' as any, width: 70 }, // ── My station (operator side) ── { group: 'My station', label: 'Station call', colId: 'station_callsign', headerName: 'Station', field: 'station_callsign' as any, width: 100, cellClass: 'font-mono', defaultVisible: true }, { group: 'My station', label: 'Operator', colId: 'operator', headerName: 'Operator',field: 'operator' as any, width: 100, cellClass: 'font-mono' }, { group: 'My station', label: 'My grid', colId: 'my_grid', headerName: 'My grid', field: 'my_grid' as any, width: 85, cellClass: 'font-mono' }, { group: 'My station', label: 'My country', colId: 'my_country', headerName: 'My ctry', field: 'my_country' as any, width: 130 }, { group: 'My station', label: 'My state', colId: 'my_state', headerName: 'My state',field: 'my_state' as any, width: 80 }, { group: 'My station', label: 'My county', colId: 'my_cnty', headerName: 'My cnty', field: 'my_cnty' as any, width: 110 }, { group: 'My station', label: 'My IOTA', colId: 'my_iota', headerName: 'My IOTA', field: 'my_iota' as any, width: 80, cellClass: 'font-mono' }, { group: 'My station', label: 'My SOTA', colId: 'my_sota_ref', headerName: 'My SOTA', field: 'my_sota_ref' as any, width: 110, cellClass: 'font-mono' }, { group: 'My station', label: 'My POTA', colId: 'my_pota_ref', headerName: 'My POTA', field: 'my_pota_ref' as any, width: 110, cellClass: 'font-mono' }, { group: 'My station', label: 'My DXCC', colId: 'my_dxcc', headerName: 'My DXCC#',field: 'my_dxcc' as any, width: 80, type: 'rightAligned' }, { group: 'My station', label: 'My CQ zone', colId: 'my_cq_zone', headerName: 'My CQZ', field: 'my_cq_zone' as any, width: 70, type: 'rightAligned' }, { group: 'My station', label: 'My ITU zone', colId: 'my_itu_zone', headerName: 'My ITU', field: 'my_itu_zone' as any, width: 70, type: 'rightAligned' }, { group: 'My station', label: 'My lat', colId: 'my_lat', headerName: 'My lat', field: 'my_lat' as any, width: 90, type: 'rightAligned' }, { group: 'My station', label: 'My lon', colId: 'my_lon', headerName: 'My lon', field: 'my_lon' as any, width: 90, type: 'rightAligned' }, { group: 'My station', label: 'My street', colId: 'my_street', headerName: 'Street', field: 'my_street' as any, width: 160 }, { group: 'My station', label: 'My city', colId: 'my_city', headerName: 'City', field: 'my_city' as any, width: 130 }, { group: 'My station', label: 'My ZIP', colId: 'my_postal_code', headerName: 'ZIP', field: 'my_postal_code' as any, width: 80 }, { group: 'My station', label: 'My rig', colId: 'my_rig', headerName: 'My rig', field: 'my_rig' as any, width: 130 }, { group: 'My station', label: 'My antenna', colId: 'my_antenna', headerName: 'My ant', field: 'my_antenna' as any, width: 130 }, // ── Misc ── { group: 'Misc', label: 'Comment', colId: 'comment', headerName: 'Comment', field: 'comment' as any, flex: 1, minWidth: 160, defaultVisible: true }, { group: 'Misc', label: 'Notes', colId: 'notes', headerName: 'Notes', field: 'notes' as any, width: 240 }, { group: 'Misc', label: 'Created', colId: 'created_at', headerName: 'Created at', field: 'created_at' as any, width: 150, valueFormatter: (p) => fmtDateUTC(p.value) }, { group: 'Misc', label: 'Updated', colId: 'updated_at', headerName: 'Updated at', field: 'updated_at' as any, width: 150, valueFormatter: (p) => fmtDateUTC(p.value) }, ]; export const GROUP_ORDER = [ 'QSO', 'Contacted', 'QSL', 'LoTW', 'eQSL', 'Uploads', 'Contest', 'Propagation', 'My station', 'Misc', ]; export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onExportSelected, onExportFiltered }: Props) { const gridRef = useRef(null); const [pickerOpen, setPickerOpen] = useState(false); const [menu, setMenu] = useState(null); // Right-click: if the clicked row isn't already part of the selection, // select just it; then open the bulk-action menu on the whole selection. function onCellContextMenu(e: any) { const ev = e.event as MouseEvent | undefined; ev?.preventDefault(); const api = gridRef.current?.api; if (!api) return; if (e.node && !e.node.isSelected()) { api.deselectAll(); e.node.setSelected(true); } const ids = (api.getSelectedRows() as QSOForm[]) .map((r) => r.id as number) .filter((n) => !!n); if (ids.length === 0) return; setMenu({ x: ev?.clientX ?? 0, y: ev?.clientY ?? 0, ids }); } // 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[]>(() => COL_CATALOG.map((c) => { const { group: _g, label: _l, defaultVisible, ...rest } = c; return { ...rest, hide: !defaultVisible }; }), []); const defaultColDef = useMemo(() => ({ sortable: true, resizable: true, filter: true, suppressMovable: false, }), []); function onGridReady(e: GridReadyEvent) { const local = loadLocal(COL_STATE_KEY); if (local) e.api.applyColumnState({ state: 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 }); seedLocal(COL_STATE_KEY, remote); } }); } const saveColumnState = useCallback(() => { const state = gridRef.current?.api?.getColumnState(); if (state) saveState(COL_STATE_KEY, state); }, []); function handleRowDoubleClicked(e: RowDoubleClickedEvent) { if (e.data && onRowDoubleClicked) onRowDoubleClicked(e.data); } function onSelectionChanged() { const sel = gridRef.current?.api?.getSelectedRows() as QSOForm[] | undefined; onRowSelected?.(sel && sel[0] ? (sel[0].id as number) : null); } // ── Column picker (visibility) ── // Drives AG Grid via setColumnsVisible(). We don't keep a parallel React // state for "which columns are visible" — AG Grid's column state is the // source of truth, and saveColumnState persists it. function isColVisible(colId: string): boolean { const col = gridRef.current?.api?.getColumn(colId); return col ? col.isVisible() : !!COL_CATALOG.find((c) => c.colId === colId)?.defaultVisible; } function setColVisible(colId: string, visible: boolean) { const api = gridRef.current?.api; if (!api) return; api.setColumnsVisible([colId], visible); saveColumnState(); } function showAll(group?: string) { const api = gridRef.current?.api; if (!api) return; const ids = COL_CATALOG.filter((c) => !group || c.group === group).map((c) => c.colId!); api.setColumnsVisible(ids, true); saveColumnState(); } function hideAll(group?: string) { const api = gridRef.current?.api; if (!api) return; const ids = COL_CATALOG.filter((c) => !group || c.group === group).map((c) => c.colId!); api.setColumnsVisible(ids, false); saveColumnState(); } function resetDefaults() { const api = gridRef.current?.api; if (!api) return; const visible = COL_CATALOG.filter((c) => c.defaultVisible).map((c) => c.colId!); const hidden = COL_CATALOG.filter((c) => !c.defaultVisible).map((c) => c.colId!); api.setColumnsVisible(visible, true); api.setColumnsVisible(hidden, false); saveColumnState(); } return ( <>
ref={gridRef} theme={hamlogTheme} rowData={rows} columnDefs={columnDefs} defaultColDef={defaultColDef} rowSelection={{ mode: 'multiRow', checkboxes: false, headerCheckbox: false, enableClickSelection: true }} onGridReady={onGridReady} onColumnResized={saveColumnState} onColumnMoved={saveColumnState} onColumnPinned={saveColumnState} onColumnVisible={saveColumnState} onSortChanged={saveColumnState} onRowDoubleClicked={handleRowDoubleClicked} onSelectionChanged={onSelectionChanged} onCellContextMenu={onCellContextMenu} preventDefaultOnContextMenu animateRows={false} suppressCellFocus getRowId={(p) => String((p.data as any).id)} />
setMenu(null)} onUpdateFromCty={(ids) => onUpdateFromCty?.(ids)} onUpdateFromQRZ={(ids) => onUpdateFromQRZ?.(ids)} onUpdateFromClublog={onUpdateFromClublog} onSendTo={onSendTo} onSendRecording={onSendRecording} onExportSelected={onExportSelected} onExportFiltered={onExportFiltered} /> Columns Pick the columns you want visible in the Recent QSOs table. Your selection is saved.
{GROUP_ORDER.map((group) => { const cols = COL_CATALOG.filter((c) => c.group === group); if (cols.length === 0) return null; return (
{group}
{cols.map((c) => ( ))}
); })}
); }