From cbe0b188c43080e7c8d773b7e1e888d2e53ecf51 Mon Sep 17 00:00:00 2001 From: "pramod.mahajan" Date: Fri, 31 Oct 2025 00:51:58 +0530 Subject: [PATCH] initial setup pmsGrid --- src/router/AppRoutes.jsx | 4 +- src/services/pmsGrid/BasicTable.jsx | 253 +++++++++++++++ src/services/pmsGrid/GridService.js | 65 ++++ src/services/pmsGrid/PmsGrid.jsx | 463 ++++++++++++++++++++++++++++ src/services/pmsGrid/index.js | 10 + src/services/pmsGrid/pms-grid.css | 34 ++ src/services/pmsGrid/useGrid.js | 46 +++ src/services/pmsGrid/useGridCore.js | 163 ++++++++++ src/services/pmsGrid/utils.js | 19 ++ 9 files changed, 1056 insertions(+), 1 deletion(-) create mode 100644 src/services/pmsGrid/BasicTable.jsx create mode 100644 src/services/pmsGrid/GridService.js create mode 100644 src/services/pmsGrid/PmsGrid.jsx create mode 100644 src/services/pmsGrid/index.js create mode 100644 src/services/pmsGrid/pms-grid.css create mode 100644 src/services/pmsGrid/useGrid.js create mode 100644 src/services/pmsGrid/useGridCore.js create mode 100644 src/services/pmsGrid/utils.js diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index 712f3eaf..48371d83 100644 --- a/src/router/AppRoutes.jsx +++ b/src/router/AppRoutes.jsx @@ -54,6 +54,8 @@ import ProjectPage from "../pages/project/ProjectPage"; import { ComingSoonPage } from "../pages/Misc/ComingSoonPage"; import ImageGalleryPage from "../pages/Gallary/ImageGallaryPage"; import CollectionPage from "../pages/collections/CollectionPage"; +import DemoBOQGrid from "../services/pmsGrid/BasicTable"; + const router = createBrowserRouter( [ { @@ -106,7 +108,7 @@ const router = createBrowserRouter( { path: "/organizations", element: }, { path: "/help/support", element: }, { path: "/help/docs", element: }, - { path: "/help/connect", element: }, + { path: "/help/connect", element: }, ], }, ], diff --git a/src/services/pmsGrid/BasicTable.jsx b/src/services/pmsGrid/BasicTable.jsx new file mode 100644 index 00000000..3ce430a4 --- /dev/null +++ b/src/services/pmsGrid/BasicTable.jsx @@ -0,0 +1,253 @@ +import React, { useEffect ,useRef} from "react"; +import { PmsGrid } from "./index"; +import { initPopover } from "./GridService"; + + +/** + * CIVIL BOQ / INVENTORY DEMO DATA + * Each row = BOQ item, grouped by "Category" + */ + +// ✅ BOQ data +const boqData = [ + { + id: 1, + category: "Concrete Works", + itemCode: "C-001", + description: "M20 Concrete for foundation", + unit: "Cum", + quantity: 120, + rate: 5200, + site: "Green City - Tower A", + vendor: "UltraTech", + status: "Completed", + }, + { + id: 2, + category: "Concrete Works", + itemCode: "C-002", + description: "M25 Concrete for columns", + unit: "Cum", + quantity: 80, + rate: 5600, + site: "Green City - Tower B", + vendor: "ACC", + status: "In Progress", + }, + { + id: 3, + category: "Steel Reinforcement", + itemCode: "S-101", + description: "TMT Bars Fe500 (10mm)", + unit: "MT", + quantity: 15, + rate: 64000, + site: "Skyline Heights - Wing C", + vendor: "JSW Steel", + status: "Pending", + }, + { + id: 4, + category: "Steel Reinforcement", + itemCode: "S-102", + description: "TMT Bars Fe500 (16mm)", + unit: "MT", + quantity: 25, + rate: 64500, + site: "Skyline Heights - Wing D", + vendor: "TATA Steel", + status: "In Progress", + }, + { + id: 5, + category: "Masonry Works", + itemCode: "M-001", + description: "Brick Masonry 230mm thick wall", + unit: "Sqm", + quantity: 280, + rate: 900, + site: "Sunshine Plaza", + vendor: "Shree Bricks", + status: "Completed", + }, + { + id: 6, + category: "Plastering Works", + itemCode: "P-001", + description: "Internal Plaster 12mm thick", + unit: "Sqm", + quantity: 1200, + rate: 250, + site: "Skyline Heights", + vendor: "L&T Finishes", + status: "Completed", + }, +]; + +// ✅ BOQ columns +const boqColumns = [ + { key: "itemCode", title: "Item Code", sortable: true, pinned: "left" }, + { key: "description", title: "Description", sortable: true, width: 300 }, + { key: "category", title: "Category", sortable: true }, + { key: "unit", title: "Unit", width: 80 }, + { key: "quantity", title: "Qty", sortable: true, width: 100 }, + { + key: "rate", + title: "Rate (₹)", + sortable: true, + width: 120, + render: (r) => ₹{r.rate.toLocaleString()}, + }, + { + key: "amount", + title: "Amount (₹)", + sortable: true, + width: 130, + render: (r) => ( + + ₹{(r.quantity * r.rate).toLocaleString()} + + ), + }, + { key: "vendor", title: "Vendor", sortable: true, width: 180 }, + { key: "site", title: "Site Location", sortable: true, width: 200 }, + { + key: "status", + title: "Status", + sortable: true, + width: 120, + render: (r) => ( + + {r.status} + + ), + }, +]; + +// ✅ Main component +export default function DemoBOQGrid() { + const wrapperRef = useRef(null); + + // initialize scrollbar + useEffect(() => { + if (!wrapperRef.current) return; + const ps = new PerfectScrollbar(wrapperRef.current, { + wheelPropagation: false, + suppressScrollX: false, + }); + return () => ps.destroy(); + }, []); + + // initialize bootstrap popover + useEffect(() => { + initPopover(); + }, []); + + return ( +
+
+
📦 BOQ (Bill of Quantities)
+ +
+ +
+
+ ({ + ...r, + amount: r.quantity * r.rate, + }))} + columns={boqColumns} + features={{ + search: true, + selection: true, + pagination: true, + export: true, + pinning: true, + resizing: true, + reorder: true, + columnVisibility: true, + expand: true, + aggregation: true, + maxHeight: "60vh", + actions: (row, toggleExpand) => ( + + ), + }} + renderExpanded={(row) => ( +
+
Item Details
+
+
+ Item Code: {row.itemCode} +
+
+ Category: {row.category} +
+
+ Unit: {row.unit} +
+
+ Quantity: {row.quantity} +
+
+ Rate: ₹{row.rate.toLocaleString()} +
+
+ Total Amount:{" "} + + ₹{(row.quantity * row.rate).toLocaleString()} + +
+
+ Vendor: {row.vendor} +
+
+ Site: {row.site} +
+
+ Status:{" "} + + {row.status} + +
+
+
+ )} + /> +
+
+
+ ); +} \ No newline at end of file diff --git a/src/services/pmsGrid/GridService.js b/src/services/pmsGrid/GridService.js new file mode 100644 index 00000000..b78c4922 --- /dev/null +++ b/src/services/pmsGrid/GridService.js @@ -0,0 +1,65 @@ + +export const sortArray = (data = [], columnKey, direction = 'asc') => { + if (!columnKey) return data; + const dir = direction === 'asc' ? 1 : -1; + return [...data].sort((a, b) => { + const A = a[columnKey]; + const B = b[columnKey]; + if (A === B) return 0; + if (A == null) return -1 * dir; + if (B == null) return 1 * dir; + if (typeof A === 'number' && typeof B === 'number') return (A - B) * dir; + return String(A).localeCompare(String(B)) * dir; + }); +}; + +export const filterArray = (data = [], searchTerm = '', columns = []) => { + if (!searchTerm) return data; + const q = String(searchTerm).toLowerCase(); + return data.filter(row => + columns.some(col => { + const val = row[col]; + if (val === undefined || val === null) return false; + return String(val).toLowerCase().includes(q); + }) + ); +}; + +export const paginate = (data = [], page = 1, pageSize = 10) => { + const total = data.length; + const from = (page - 1) * pageSize; + const to = from + pageSize; + return { + page, + pageSize, + total, + totalPages: Math.max(1, Math.ceil(total / pageSize)), + rows: data.slice(from, to), + }; +}; + +// selection helpers +export const toggleItem = (selected = new Set(), id) => { + const s = new Set(selected); + if (s.has(id)) s.delete(id); + else s.add(id); + return s; +}; + +export const selectAll = (rows = [], idKey = 'id') => + new Set(rows.map(r => r[idKey])); + +export const clearSelection = () => new Set(); +// src/services/pmsGrid/usePmsPopup.js +export function initPopover(selector = '[data-bs-toggle="popover"]') { + if (!window.bootstrap || !window.bootstrap.Popover) { + console.warn('⚠️ Bootstrap JS not found — make sure you included bootstrap.bundle.min.js in index.html'); + return; + } + + document.addEventListener('DOMContentLoaded', () => { + const elements = document.querySelectorAll(selector); + if (!elements.length) return; + elements.forEach(el => new window.bootstrap.Popover(el)); + }); +} diff --git a/src/services/pmsGrid/PmsGrid.jsx b/src/services/pmsGrid/PmsGrid.jsx new file mode 100644 index 00000000..af595f98 --- /dev/null +++ b/src/services/pmsGrid/PmsGrid.jsx @@ -0,0 +1,463 @@ +import React, { useRef } from "react"; +import { useGridCore } from "./useGridCore"; +import { exportToCSV } from "./utils"; +import "./pms-grid.css"; + +/* + Props: + - columns: [{ key, title, width, pinned: 'left'|'right'|null, sortable, render, aggregate }] + - data OR serverMode + fetcher + - rowKey + - features: { selection, search, export, pagination, pinning, resizing, reorder, grouping, aggregation, expand } + - renderExpanded +*/ +export default function PmsGrid({ + columns = [], + data, + serverMode = false, + fetcher, + rowKey = "id", + features = {}, + renderExpanded, +}) { + const grid = useGridCore({ + data, + serverMode, + fetcher, + rowKey, + initialPageSize: features.pageSize || 25, + columns, + }); + const wrapperRef = useRef(); + + const { + rows, + page, + totalPages, + pageSize, + setPage, + setPageSize, + search, + setSearch, + selected, + toggleSelect, + selectAllOnPage, + deselectAllOnPage, + changeSort, + sortBy, + visibleColumns, + colState, + updateColumn, + loading, + totalRows, + expanded, + toggleExpand, + setColState, + } = grid; + + // simple pin toggle + const togglePin = (key) => { + const col = colState.find((c) => c.key === key); + updateColumn(key, { pinned: col.pinned === "left" ? null : "left" }); + }; + + // resizing via mouse down on handle + const onResizeMouseDown = (e, key) => { + e.preventDefault(); + const startX = e.clientX; + const col = colState.find((c) => c.key === key); + const startWidth = col.width || 120; + + const onMove = (ev) => { + const diff = ev.clientX - startX; + const newWidth = Math.max(60, startWidth + diff); + updateColumn(key, { width: newWidth }); + }; + + const onUp = () => { + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + }; + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); + }; + + // reorder columns (drag/drop) + const dragState = useRef({ from: null }); + const onDragStart = (e, key) => { + dragState.current.from = key; + e.dataTransfer.effectAllowed = "move"; + }; + const onDrop = (e, toKey) => { + e.preventDefault(); + const fromKey = dragState.current.from; + if (!fromKey || fromKey === toKey) return; + const from = colState.find((c) => c.key === fromKey); + const to = colState.find((c) => c.key === toKey); + if (!from || !to) return; + // swap orders + setColState((prev) => { + const next = prev.map((p) => ({ ...p })); + const f = next.find((p) => p.key === fromKey); + const t = next.find((p) => p.key === toKey); + const tmp = f.order; + f.order = t.order; + t.order = tmp; + return next.sort((a, b) => a.order - b.order); + }); + }; + + // group & aggregate (simple client-side grouping by column key) + const groupBy = features.groupByKey || null; + const groupedRows = React.useMemo(() => { + if (!groupBy) return null; + const map = {}; + rows.forEach((r) => { + const g = String(r[groupBy] ?? "Unknown"); + if (!map[g]) map[g] = []; + map[g].push(r); + }); + const groups = Object.keys(map).map((k) => ({ key: k, items: map[k] })); + // apply aggregation + const aggregates = groups.map((g) => { + const ag = {}; + visibleColumns.forEach((col) => { + if (col.aggregate) { + ag[col.key] = col.aggregate(g.items.map((it) => it[col.key])); + } + }); + return { ...g, aggregates: ag }; + }); + return aggregates; + }, [rows, groupBy, visibleColumns]); + + const currentRows = rows; + + return ( +
+
+
+ {features.search && ( + setSearch(e.target.value)} + /> + )} + {features.export && ( + + )} +
+ +
+ {features.columnVisibility && ( + + updateColumn(k, { + visible: !colState.find((c) => c.key === k).visible, + }) + } + /> + )} + {features.pageSizeSelector && ( + + )} +
+
+
+ + + + {features.selection && ( + + )} + {visibleColumns.map((col) => { + const style = { + minWidth: col.width || 120, + width: col.width || undefined, + }; + if (col.pinned) style.position = "sticky"; + if (col.pinned === "left") + style.left = `${getLeftOffset(colState, col.key)}px`; + if (col.pinned === "right") + style.right = `${getRightOffset(colState, col.key)}px`; + return ( + + ); + })} + {features.actions && ( + + )} + + + + + {loading && ( + + + + )} + {!loading && groupBy && groupedRows && groupedRows.length > 0 + ? groupedRows.map((g) => ( + + + + + {g.items.map((row) => renderRow(row))} + + )) + : currentRows.map((row) => renderRow(row))} + +
+ 0 && + currentRows.every((r) => selected.has(r[rowKey])) + } + onChange={(e) => + e.target.checked + ? selectAllOnPage(currentRows) + : deselectAllOnPage(currentRows) + } + /> + onDragStart(e, col.key)} + onDragOver={(e) => e.preventDefault()} + onDrop={(e) => onDrop(e, col.key)} + className={`pms-col-header ${col.pinned ? "pinned" : ""}`} + style={style} + > +
+
col.sortable && changeSort(col.key)} + style={{ cursor: col.sortable ? "pointer" : "default" }} + > + {col.title} + {sortBy.key === col.key && ( + + [{sortBy.dir}] + + )} +
+
+ {features.pinning && ( + + )} + {features.resizing && ( +
onResizeMouseDown(e, col.key)} + /> + )} +
+
+
+ Actions +
+ Loading... +
+ {g.key} + {features.aggregation && + Object.keys(g.aggregates).length > 0 && ( + + {Object.entries(g.aggregates) + .map(([k, v]) => `${k}: ${v}`) + .join(" | ")} + + )} +
+
+ + {features.pagination && ( +
+
+ {totalRows} rows +
+
+ + + {page}/{totalPages} + + +
+
+ )} +
+ ); + + // render a single row (function hoisted so it can reference visibleColumns) + function renderRow(row) { + return ( + + + {features.selection && ( + + toggleSelect(row[rowKey])} + /> + + )} + {visibleColumns.map((col) => { + const style = { + minWidth: col.width || 120, + width: col.width || undefined, + }; + if (col.pinned) style.position = "sticky"; + if (col.pinned === "left") + style.left = `${getLeftOffset(colState, col.key)}px`; + if (col.pinned === "right") + style.right = `${getRightOffset(colState, col.key)}px`; + return ( + + {col.render ? col.render(row) : row[col.key] ?? ""} + + ); + })} + {features.actions && ( + + {features.actions(row, toggleExpand)} + + )} + + + {features.expand && expanded.has(row[rowKey]) && renderExpanded && ( + + + {renderExpanded(row)} + + + )} + + ); + } +} + +// small helpers to compute sticky offsets +function getLeftOffset(colState, key) { + let offset = 0; + for (const c of colState + .filter((c) => c.visible) + .sort((a, b) => a.order - b.order)) { + if (c.key === key) return offset; + if (c.pinned === "left") offset += c.width || 120; + } + return offset; +} +function getRightOffset(colState, key) { + let offset = 0; + const rightCols = colState + .filter((c) => c.visible && c.pinned === "right") + .sort((a, b) => b.order - a.order); + for (const c of rightCols) { + if (c.key === key) return offset; + offset += c.width || 120; + } + return offset; +} + +/* ColumnVisibilityPanel component (inline) */ +function ColumnVisibilityPanel({ columns, onToggle }) { + return ( +
+ +
+ {columns.map((c) => ( +
+ onToggle(c.key)} + /> + +
+ ))} +
+
+ ); +} diff --git a/src/services/pmsGrid/index.js b/src/services/pmsGrid/index.js new file mode 100644 index 00000000..ae25f60d --- /dev/null +++ b/src/services/pmsGrid/index.js @@ -0,0 +1,10 @@ +// src/lib/pms-grid/index.js + +// ✅ Main Grid Component +export { default as PmsGrid } from "./PmsGrid"; + +// ✅ Core logic hook +export * from "./useGridCore"; + +// ✅ Utilities (like exportToCSV) +export * from "./utils"; diff --git a/src/services/pmsGrid/pms-grid.css b/src/services/pmsGrid/pms-grid.css new file mode 100644 index 00000000..e8f28dd6 --- /dev/null +++ b/src/services/pmsGrid/pms-grid.css @@ -0,0 +1,34 @@ + + +.grid-wrapper { + max-height: 60vh; + overflow-y: auto; + overflow-x: auto; + position: relative; +} + +.grid-wrapper thead { + position: sticky; + top: 0; + z-index: 3; + background: #fff; +} + +.resize-handle { + width: 10px; + cursor: col-resize; + height: 28px; + display: inline-block; +} + +.pms-col-header.pinned { + background: #fff; + z-index: 4; +} + +.sticky-action-column { + position: sticky; + right: 0; + z-index: 5; + background: #fff; +} diff --git a/src/services/pmsGrid/useGrid.js b/src/services/pmsGrid/useGrid.js new file mode 100644 index 00000000..d8337faa --- /dev/null +++ b/src/services/pmsGrid/useGrid.js @@ -0,0 +1,46 @@ + +import { useState, useMemo, useCallback } from "react"; +import { sortArray, filterArray, paginate, toggleItem, selectAll, clearSelection } from "./GridService"; + +export const useGrid = ({ data = [], columns = [], idKey = "id", initialPageSize = 10 }) => { + const [search, setSearch] = useState(""); + const [sortBy, setSortBy] = useState({ key: null, dir: "asc" }); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(initialPageSize); + const [selected, setSelected] = useState(new Set()); + const [expanded, setExpanded] = useState(new Set()); + + const filtered = useMemo(() => filterArray(data, search, columns.map(c => c.key)), [data, search, columns]); + const sorted = useMemo(() => sortArray(filtered, sortBy.key, sortBy.dir), [filtered, sortBy]); + const pageResult = useMemo(() => paginate(sorted, page, pageSize), [sorted, page, pageSize]); + + const toggleSelect = useCallback(id => setSelected(prev => toggleItem(prev, id)), []); + const selectAllOnPage = useCallback(rows => setSelected(selectAll(rows, idKey)), [idKey]); + const deselectAllOnPage = useCallback(rows => setSelected(prev => { + const s = new Set(prev); + rows.forEach(r => s.delete(r[idKey])); + return s; + }), [idKey]); + const clearAllSelection = useCallback(() => setSelected(clearSelection()), []); + const changeSort = useCallback(key => { + setSortBy(prev => (prev.key === key ? { key, dir: prev.dir === "asc" ? "desc" : "asc" } : { key, dir: "asc" })); + }, []); + const toggleExpand = useCallback(id => { + setExpanded(prev => { + const s = new Set(prev); + s.has(id) ? s.delete(id) : s.add(id); + return s; + }); + }, []); + + return { + search, setSearch, + sortBy, changeSort, + page, setPage, + pageSize, setPageSize, + selected, toggleSelect, selectAllOnPage, deselectAllOnPage, clearAllSelection, + expanded, toggleExpand, + pageResult, + totalRows: sorted.length, + }; +}; diff --git a/src/services/pmsGrid/useGridCore.js b/src/services/pmsGrid/useGridCore.js new file mode 100644 index 00000000..291ca769 --- /dev/null +++ b/src/services/pmsGrid/useGridCore.js @@ -0,0 +1,163 @@ + +import { useState, useMemo, useCallback, useEffect } from "react"; + +/* + options: + - data (array) OR serverMode: true + fetcher({ page, pageSize, sortBy, filter, search }) + - rowKey + - initialPageSize +*/ +export function useGridCore({ data, serverMode = false, fetcher, rowKey = "id", initialPageSize = 25, columns = [] }) { + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(initialPageSize); + const [search, setSearch] = useState(""); + const [sortBy, setSortBy] = useState({ key: null, dir: "asc" }); + const [selected, setSelected] = useState(new Set()); + const [expanded, setExpanded] = useState(new Set()); + const [colState, setColState] = useState(() => + columns.map((c, i) => ({ ...c, visible: c.visible !== false, order: i })) + ); + const [totalRows, setTotalRows] = useState(data ? data.length : 0); + const [loading, setLoading] = useState(false); + const [serverRows, setServerRows] = useState([]); + + // client-side derived rows + const clientFiltered = useMemo(() => { + if (!data) return []; + const q = (search || "").toLowerCase(); + const filtered = q + ? data.filter(r => + Object.values(r).some(v => + String(v ?? "").toLowerCase().includes(q) + ) + ) + : data; + // sort if needed + if (sortBy.key) { + const dir = sortBy.dir === "asc" ? 1 : -1; + filtered.sort((a, b) => { + const A = a[sortBy.key], + B = b[sortBy.key]; + if (A == null && B == null) return 0; + if (A == null) return -1 * dir; + if (B == null) return 1 * dir; + if (typeof A === "number" && typeof B === "number") return (A - B) * dir; + return String(A).localeCompare(String(B)) * dir; + }); + } + setTotalRows(filtered.length); + const start = (page - 1) * pageSize; + return filtered.slice(start, start + pageSize); + }, [data, search, sortBy, page, pageSize]); + + // server-side fetch + const fetchServer = useCallback(async () => { + if (!serverMode || typeof fetcher !== "function") return; + setLoading(true); + try { + const resp = await fetcher({ page, pageSize, sortBy, search }); + // expected: { rows: [], total } + setServerRows(resp.rows || []); + setTotalRows(resp.total || (resp.rows ? resp.rows.length : 0)); + } finally { + setLoading(false); + } + }, [serverMode, fetcher, page, pageSize, sortBy, search]); + + useEffect(() => { + if (serverMode) fetchServer(); + }, [serverMode, fetchServer]); + + // selection + const toggleSelect = useCallback(id => { + setSelected(prev => { + const s = new Set(prev); + s.has(id) ? s.delete(id) : s.add(id); + return s; + }); + }, []); + + const selectAllOnPage = useCallback( + rows => { + setSelected(prev => { + const s = new Set(prev); + rows.forEach(r => s.add(r[rowKey])); + return s; + }); + }, + [rowKey] + ); + + const deselectAllOnPage = useCallback( + rows => { + setSelected(prev => { + const s = new Set(prev); + rows.forEach(r => s.delete(r[rowKey])); + return s; + }); + }, + [rowKey] + ); + + const toggleExpand = useCallback(id => { + setExpanded(prev => { + const s = new Set(prev); + s.has(id) ? s.delete(id) : s.add(id); + return s; + }); + }, []); + + const changeSort = useCallback(key => { + setSortBy(prev => + prev.key === key + ? { key, dir: prev.dir === "asc" ? "desc" : "asc" } + : { key, dir: "asc" } + ); + setPage(1); + }, []); + + const visibleColumns = useMemo( + () => colState.filter(c => c.visible).sort((a, b) => a.order - b.order), + [colState] + ); + + const rows = serverMode ? serverRows : clientFiltered; + const totalPages = Math.max(1, Math.ceil((totalRows || 0) / pageSize)); + + // update columns externally (reorder/pin/resize) + const updateColumn = useCallback((key, patch) => { + setColState(prev => prev.map(c => (c.key === key ? { ...c, ...patch } : c))); + }, []); + + return { + // state + page, + setPage, + pageSize, + setPageSize, + totalRows, + totalPages, + loading, + search, + setSearch, + sortBy, + changeSort, + selected, + toggleSelect, + selectAllOnPage, + deselectAllOnPage, + setSelected, + expanded, + toggleExpand, + colState, + visibleColumns, + updateColumn, + setColState, + + // data + rows, + + // mode helpers + serverMode, + }; +} diff --git a/src/services/pmsGrid/utils.js b/src/services/pmsGrid/utils.js new file mode 100644 index 00000000..83517560 --- /dev/null +++ b/src/services/pmsGrid/utils.js @@ -0,0 +1,19 @@ + +export function exportToCSV(rows = [], columns = [], filename = "export.csv") { + const cols = columns.filter(c => c.visible !== false); + const header = cols.map(c => `"${(c.title || c.key).replace(/"/g, '""')}"`).join(","); + const lines = rows.map(r => cols.map(c => { + const v = c.exportValue ? c.exportValue(r) : (r[c.key] ?? ""); + return `"${String(v).replace(/"/g,'""')}"`; + }).join(",")); + const csv = [header, ...lines].join("\r\n"); + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + const link = document.createElement("a"); + const url = URL.createObjectURL(blob); + link.setAttribute("href", url); + link.setAttribute("download", filename); + link.style.visibility = "hidden"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +}