import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { AllCommunityModule, ModuleRegistry, type ColDef, type ColumnState, type GridReadyEvent, type RowDoubleClickedEvent, } from 'ag-grid-community'; import { hamlogGridTheme } from '@/lib/gridTheme'; 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 { makeColCatalog, GROUP_ORDER, groupLabel } from './RecentQSOsGrid'; import { QSOContextMenu, type QSOMenuState } from './QSOContextMenu'; import { loadLocal, loadRemote, saveState, seedLocal } from '@/lib/gridPrefs'; import { useI18n } from '@/lib/i18n'; ModuleRegistry.registerModules([AllCommunityModule]); const hamlogTheme = hamlogGridTheme; 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; onDelete?: (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, onDelete, awardCols }: Props) { const { t } = useI18n(); const gridRef = useRef(null); const [pickerOpen, setPickerOpen] = useState(false); const [menu, setMenu] = useState(null); // Localized column catalog (shared with the Recent QSOs grid). const COL_CATALOG = useMemo(() => makeColCatalog(t), [t]); 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 ?? []; // Suppress auto-save while AG Grid rebuilds columns (award columns load async), // so the default visibility doesn't clobber the user's saved state. See the // matching note in RecentQSOsGrid. const restoringRef = useRef(true); const columnDefs = useMemo[]>(() => { restoringRef.current = true; 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: t('wbg.awardTip', { name: a.name }), width: 110, cellClass: 'text-[11px]', valueGetter: (p) => (p.data as any)?.award_refs?.[a.code.toUpperCase()] ?? '', })); return [...base, ...awards]; }, [awardCols, COL_CATALOG, t]); 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(() => { if (restoringRef.current) return; // ignore events fired by a column rebuild const state = gridRef.current?.api?.getColumnState(); if (state) saveState(COL_STATE_KEY, state); }, []); // Re-apply the saved column state after the award columns load (they rebuild // the column set), so the user's visibility choices win over the defaults. useEffect(() => { const api = gridRef.current?.api; const local = loadLocal(COL_STATE_KEY); if (api && local) api.applyColumnState({ state: local as ColumnState[], applyOrder: true }); const t = window.setTimeout(() => { restoringRef.current = false; }, 0); return () => window.clearTimeout(t); }, [awardCols]); 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 (
{t('wbg.typeCall')}
); } if (busy && count === 0) { return (
{t('wbg.checking')}
); } if (count === 0) { return (
{t('wbg.new')}
{t('wbg.noPriorPre')}{currentCall.toUpperCase()}{t('wbg.noPriorPost')}
); } return ( <>
{t('wbg.workedBefore')} {currentCall.toUpperCase()} {count}×
{t('wbg.first')} {fmtDate(wb?.first)} ·{' '} {t('wbg.last')} {fmtDate(wb?.last)}
{wb?.dxcc_name && (
{t('wbg.dxcc')} {wb.dxcc_name} {typeof wb.dxcc_count === 'number' && wb.dxcc_count > 0 && ( · {t('wbg.entityQsos', { n: wb.dxcc_count })} )}
)}
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} onDelete={onDelete} /> {count > entries.length && (
{t('wbg.olderQsos', { n: count - entries.length })}
)} {t('wbg.pickerTitle')} {t('wbg.pickerDesc')}
{GROUP_ORDER.map((group) => { const cols = COL_CATALOG.filter((c) => c.group === group); if (cols.length === 0) return null; return (
{groupLabel(t, group)}
{cols.map((c) => ( ))}
); })} {awardCols && awardCols.length > 0 && (
{t('wbg.grpAwards')}
{awardCols.map((a) => ( ))}
)}
); }