/* ============================================================ KODILA — DataGrid (AG Grid wrapper) + orodna vrstica Privzeto vedenje za VSE gride v sistemu: - filter je del glave kolone (klik na meni glave), ni ločene tipke - "Kolone" omogoča shranjevanje/ponastavitev postavitve (na uporabnika) - klik z desno tipko na vrstico odpre meni: kopiraj vrstico/celico, rekapitulacija ============================================================ */ /* Build a print window for PDF export */ function exportPdf(title, columns, rows) { const head = columns.map(c => `${c.header}`).join(''); const body = rows.map(r => '' + columns.map(c => `${(r[c.field] ?? '')}`).join('') + '' ).join(''); const w = window.open('', '_blank'); if (!w) return; w.document.write(`${title}
K

${title}

KODILA · Poslovno-informacijski sistem · ${new Date().toLocaleString('sl-SI')}
${head}${body}
Izvoženo iz sistema KODILA — ${rows.length} zapisov
`); w.document.close(); setTimeout(() => { w.focus(); w.print(); }, 350); } /* --- shranjevanje postavitve kolon (na uporabnika, na grid) --- */ function gridPrefsKey(gridId) { const uid = (window.__KODILA_USER__ && window.__KODILA_USER__.id) || 'anon'; return `kodila_grid_layout:${gridId}:${uid}`; } /* --- formatiranje vrednosti celice za kopiranje/rekapitulacijo (upošteva valueFormatter) --- */ function formatCellForCopy(colDef, data) { const raw = data ? data[colDef.field] : undefined; if (colDef.valueFormatter) { try { return colDef.valueFormatter({ value: raw, data, colDef }); } catch (e) { /* ignore */ } } return raw === null || raw === undefined ? '' : String(raw); } async function copyToClipboard(text, toast) { try { await navigator.clipboard.writeText(text); toast('Kopirano v odložišče', 'copy'); } catch (e) { toast('Kopiranje ni uspelo', 'alert'); } } /* --- Meni z desnim klikom na vrstico --- */ function RowContextMenu({ x, y, columnDefs, data, field, onClose, onRecap, toast }) { const ref = useRef(null); useEffect(() => { const close = () => onClose(); const onKey = (e) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('mousedown', close); document.addEventListener('scroll', close, true); document.addEventListener('keydown', onKey); return () => { document.removeEventListener('mousedown', close); document.removeEventListener('scroll', close, true); document.removeEventListener('keydown', onKey); }; }, [onClose]); const dataCols = columnDefs.filter(c => c.colId !== '__sel' && c.field); const clickedCol = dataCols.find(c => c.field === field); const copyRow = () => { const line = dataCols.map(c => formatCellForCopy(c, data)).join('\t'); copyToClipboard(line, toast); onClose(); }; const copyCell = () => { if (!clickedCol) { onClose(); return; } copyToClipboard(formatCellForCopy(clickedCol, data), toast); onClose(); }; const vw = window.innerWidth, vh = window.innerHeight; const left = Math.min(x, vw - 220); const top = Math.min(y, vh - 160); return (
e.stopPropagation()}>
); } /* --- Rekapitulacija postavke (podrobnosti vrstice) --- */ function RecapDrawer({ columnDefs, data, onClose }) { const dataCols = columnDefs.filter(c => c.colId !== '__sel' && c.field); return (
{ if (e.target.classList.contains('overlay')) onClose(); }}>
e.stopPropagation()} style={{ maxWidth: 460 }}>
Rekapitulacija postavke
Podroben pregled vrstice
{dataCols.map(c => (
{c.headerName} {formatCellForCopy(c, data) || '—'}
))}
); } function DataGrid({ columnDefs, rowData, getRowId, selectable, exportName, exportTitle, exportColumns, actions, filters, searchPlaceholder, onRowOpen, onCellValueChanged, onSelectionChange }) { const elRef = useRef(null); const apiRef = useRef(null); const toast = useToast(); const openRef = useRef(onRowOpen); openRef.current = onRowOpen; const cellChangedRef = useRef(onCellValueChanged); cellChangedRef.current = onCellValueChanged; const selectionChangeRef = useRef(onSelectionChange); selectionChangeRef.current = onSelectionChange; const [search, setSearch] = useState(''); const [selCount, setSelCount] = useState(0); const [cols, setCols] = useState([]); // {colId, header, visible} const [total, setTotal] = useState(rowData.length); const [ctxMenu, setCtxMenu] = useState(null); // {x,y,data,field} const [recap, setRecap] = useState(null); // rowData | null const gridId = exportName || 'grid'; // build full colDefs (inject selection col) const fullDefs = useMemo(() => { const base = columnDefs.map(c => ({ ...c })); if (selectable) { base.unshift({ colId: '__sel', headerName: '', width: 46, minWidth: 46, maxWidth: 46, checkboxSelection: true, headerCheckboxSelection: true, pinned: 'left', sortable: false, filter: false, resizable: false, suppressMovable: true, suppressHeaderMenuButton: true, lockPosition: true, }); } return base; }, [columnDefs, selectable]); useEffect(() => { const options = { columnDefs: fullDefs, rowData, getRowId: getRowId ? (p) => getRowId(p.data) : undefined, defaultColDef: { sortable: true, resizable: true, filter: true, minWidth: 90, suppressHeaderMenuButton: false, }, rowSelection: selectable ? 'multiple' : undefined, suppressRowClickSelection: true, singleClickEdit: true, stopEditingWhenCellsLoseFocus: true, animateRows: true, rowHeight: 46, headerHeight: 44, domLayout: 'normal', enableCellTextSelection: true, preventDefaultOnContextMenu: true, pagination: true, paginationPageSize: 25, paginationPageSizeSelector: [10, 25, 50, 100], localeText: { page: 'Stran', more: 'Več', to: 'do', of: 'od', next: 'Naslednja', last: 'Zadnja', first: 'Prva', previous: 'Prejšnja', loadingOoo: 'Nalaganje…', pageSizeSelectorLabel: 'Prikaz vrstic:', }, onGridReady: (p) => { apiRef.current = p.api; const saved = localStorage.getItem(gridPrefsKey(gridId)); if (saved) { try { p.api.applyColumnState({ state: JSON.parse(saved), applyOrder: true }); } catch (e) { /* ignore corrupt prefs */ } } const list = p.api.getColumns() .filter(c => c.getColId() !== '__sel') .map(c => ({ colId: c.getColId(), header: c.getColDef().headerName || c.getColId(), visible: c.isVisible() })); setCols(list); setTotal(p.api.getDisplayedRowCount()); }, onSelectionChanged: (p) => { setSelCount(p.api.getSelectedNodes().length); if (selectionChangeRef.current) selectionChangeRef.current(p.api.getSelectedRows()); }, onFilterChanged: (p) => setTotal(p.api.getDisplayedRowCount()), onRowDoubleClicked: (p) => { if (openRef.current) openRef.current(p.data); }, onCellValueChanged: (p) => { if (cellChangedRef.current) cellChangedRef.current(p); }, onCellContextMenu: (p) => { if (!p.event) return; p.event.preventDefault(); setCtxMenu({ x: p.event.clientX, y: p.event.clientY, data: p.data, field: p.column.getColId() }); }, }; const api = agGrid.createGrid(elRef.current, options); apiRef.current = api; return () => api.destroy(); }, []); // mount once // react to rowData changes useEffect(() => { if (apiRef.current) { apiRef.current.setGridOption('rowData', rowData); setTotal(rowData.length); } }, [rowData]); // search useEffect(() => { if (apiRef.current) apiRef.current.setGridOption('quickFilterText', search); }, [search]); const toggleCol = (colId) => { setCols(cs => cs.map(c => { if (c.colId === colId) { apiRef.current.setColumnsVisible([colId], !c.visible); return { ...c, visible: !c.visible }; } return c; })); }; const saveLayout = () => { const state = apiRef.current.getColumnState(); localStorage.setItem(gridPrefsKey(gridId), JSON.stringify(state)); toast('Postavitev kolon shranjena', 'save'); }; const resetLayout = () => { apiRef.current.resetColumnState(); localStorage.removeItem(gridPrefsKey(gridId)); const list = apiRef.current.getColumns() .filter(c => c.getColId() !== '__sel') .map(c => ({ colId: c.getColId(), header: c.getColDef().headerName || c.getColId(), visible: c.isVisible() })); setCols(list); toast('Postavitev ponastavljena na privzeto', 'history'); }; const getVisibleData = () => { const api = apiRef.current; const visCols = api.getColumns().filter(c => c.isVisible() && c.getColId() !== '__sel'); const headers = visCols.map(c => ({ field: c.getColId(), header: c.getColDef().headerName || c.getColId(), align: (c.getColDef().type === 'numericColumn' || c.getColDef().cellClass === 'cell-num') ? 'right' : 'left' })); const rows = []; api.forEachNodeAfterFilterAndSort(n => { const row = {}; visCols.forEach(c => { const cd = c.getColDef(); let v = api.getCellValue ? api.getCellValue({ rowNode: n, colKey: c.getColId() }) : n.data[c.getColId()]; if (v === undefined || v === null) v = n.data[cd.field] ?? ''; row[c.getColId()] = typeof v === 'object' ? (n.data[cd.field] ?? '') : v; }); rows.push(row); }); return { headers, rows }; }; const doExport = (kind) => { const api = apiRef.current; if (kind === 'csv') { api.exportDataAsCsv({ fileName: exportName + '.csv', columnSeparator: ';', processCellCallback: p => p.value }); toast('Izvoženo v CSV', 'download'); } else if (kind === 'xls') { const { headers, rows } = getVisibleData(); const aoa = [headers.map(h => h.header)]; rows.forEach(r => aoa.push(headers.map(h => r[h.field]))); const ws = XLSX.utils.aoa_to_sheet(aoa); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Izvoz'); XLSX.writeFile(wb, exportName + '.xlsx'); toast('Izvoženo v Excel (XLSX)', 'download'); } else if (kind === 'pdf') { const { headers, rows } = getVisibleData(); exportPdf(exportTitle || exportName, headers, rows); toast('Pripravljeno za PDF / tisk', 'printer'); } }; const hiddenCount = cols.filter(c => !c.visible).length; return (
setSearch(e.target.value)} placeholder={searchPlaceholder || 'Iskanje…'} /> {search &&
{filters} {/* Column chooser + postavitev */} ( )}>
Prikaz kolon
{cols.map(c => ( ))}
{/* Export */} ( )}>
{selCount > 0 && ( Izbrano: {selCount} )} {actions}
Prikazano: {total} zapisov Povleci glave za premik · klik glave za sortiranje · meni glave za filter · desni klik za več možnosti
{ctxMenu && ( setCtxMenu(null)} onRecap={() => setRecap(ctxMenu.data)} /> )} {recap && ( setRecap(null)} /> )}
); } Object.assign(window, { DataGrid, exportPdf });