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, Star } from 'lucide-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import type { WorkedBeforeView, QSOForm } from '@/types'; import { COL_CATALOG, GROUP_ORDER } from './RecentQSOsGrid'; import { QSOContextMenu, type QSOMenuState } from './QSOContextMenu'; import { loadLocal, loadRemote, saveState, seedLocal } from '@/lib/gridPrefs'; ModuleRegistry.registerModules([AllCommunityModule]); 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: 30, headerHeight: 32, spacing: 4, accentColor: '#b8410c', iconSize: 12, }); type WorkedEntry = QSOForm; // entries are now full QSO records type Props = { wb: WorkedBeforeView | null; busy: boolean; currentCall: string; onRowDoubleClicked?: (q: QSOForm) => void; onUpdateFromCty?: (ids: number[]) => void; onUpdateFromQRZ?: (ids: number[]) => void; onUpdateFromClublog?: (ids: number[]) => void; 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'; function fmtDate(s: any): string { if (!s) return ''; const d = new Date(s); if (isNaN(d.getTime())) return ''; const p = (n: number) => String(n).padStart(2, '0'); return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}`; } export function WorkedBeforeGrid({ wb, busy, currentCall, onRowDoubleClicked, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, awardCols }: Props) { const gridRef = useRef(null); const [pickerOpen, setPickerOpen] = useState(false); const [menu, setMenu] = useState(null); function handleRowDoubleClicked(e: RowDoubleClickedEvent) { if (e.data && onRowDoubleClicked) onRowDoubleClicked(e.data); } 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 WorkedEntry[]) .map((r) => r.id as number) .filter((n) => !!n); if (ids.length === 0) return; setMenu({ x: ev?.clientX ?? 0, y: ev?.clientY ?? 0, ids }); } const hasCall = currentCall.trim() !== ''; const count = wb?.count ?? 0; const entries = wb?.entries ?? []; const columnDefs = useMemo[]>(() => { const base = COL_CATALOG.map((c) => { const { group: _g, label: _l, defaultVisible, ...rest } = c; return { ...rest, hide: !defaultVisible }; }); const awards: ColDef[] = (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(() => ({ 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 }); 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 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(); } // Empty / loading / no-call states. if (!hasCall) { return (
Type a callsign in the entry strip to see prior contacts.
); } if (busy && count === 0) { return (
checking…
); } if (count === 0) { return (
NEW
No prior QSO with {currentCall.toUpperCase()}.
); } return ( <>
Worked before {currentCall.toUpperCase()} {count}×
First: {fmtDate(wb?.first)} ·{' '} Last: {fmtDate(wb?.last)}
{wb?.dxcc_name && (
DXCC: {wb.dxcc_name} {typeof wb.dxcc_count === 'number' && wb.dxcc_count > 0 && ( · {wb.dxcc_count} entity QSOs )}
)}
ref={gridRef} theme={hamlogTheme} rowData={entries} columnDefs={columnDefs} defaultColDef={defaultColDef} onGridReady={onGridReady} onColumnResized={saveColumnState} onColumnMoved={saveColumnState} onColumnPinned={saveColumnState} onColumnVisible={saveColumnState} onSortChanged={saveColumnState} onRowDoubleClicked={handleRowDoubleClicked} onCellContextMenu={onCellContextMenu} preventDefaultOnContextMenu rowSelection={{ mode: 'multiRow', checkboxes: false, headerCheckbox: false, enableClickSelection: true }} 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} onSendEQSL={onSendEQSL} /> {count > entries.length && (
+ {count - entries.length} older QSOs (not shown — capped for performance)
)} Worked-before columns Pick the columns you want visible in the Worked-before table.
{GROUP_ORDER.map((group) => { const cols = COL_CATALOG.filter((c) => c.group === group); if (cols.length === 0) return null; return (
{group}
{cols.map((c) => ( ))}
); })} {awardCols && awardCols.length > 0 && (
Awards
{awardCols.map((a) => ( ))}
)}
); }