partially added advance filter

This commit is contained in:
pramod.mahajan 2025-12-08 00:47:46 +05:30
parent 102a88aee6
commit e5fa6bb6ea
4 changed files with 227 additions and 174 deletions

View File

@ -81,40 +81,42 @@ const PmGridCollection = ({ selectedProject, fromDate, toDate, isPending }) => {
}; };
return ( return (
<PmsGrid <PmsGrid
columns={columns} columns={columns}
serverMode serverMode
fetcher={fetcher} fetcher={fetcher}
rowKey="id" rowKey="id"
features={{ features={{
search: true, search: true,
pagination: true, pagination: true,
pinning: true, pinning: true,
resizing: true, resizing: true,
selection: false, selection: false,
reorder: true, reorder: true,
columnVisibility: true, columnVisibility: true,
pageSizeSelector: true, pageSizeSelector: true,
// groupByKey: "clientSubmitedDate",
aggregation: true, grouping: true,
pinning: true, groupByKey: "clientSubmitedDate",
IsNumbering: true,
grouping: true, aggregation: true,
actions: [ IsNumbering: true,
{
label: "Edit", actions: [
icon: "bx-edit ", {
onClick: (row) => console.log("Edit", row), label: "Edit",
}, icon: "bx-edit ",
{ onClick: (row) => console.log("Edit", row),
label: "Delete", },
icon: "bx-trash text-danger", {
onClick: (row) => console.log("Delete", row), label: "Delete",
}, icon: "bx-trash text-danger",
onClick: (row) => console.log("Delete", row),
] },
}} ],
/> }}
/>
); );
}; };

View File

@ -57,6 +57,8 @@ export default function PmsGrid({
expanded, expanded,
toggleExpand, toggleExpand,
setColState, setColState,
onAdvanceFilters,
setAdanceFilter,
} = grid; } = grid;
// --- Pin / Unpin helpers --- // --- Pin / Unpin helpers ---
@ -254,14 +256,10 @@ export default function PmsGrid({
style={{ position: "sticky", top: 0, zIndex: 10 }} style={{ position: "sticky", top: 0, zIndex: 10 }}
> >
<tr className="p-2"> <tr className="p-2">
{features.IsNumbering && ( {features.IsNumbering && <th>#</th>}
<th>
#
</th>
)}
{features.expand && ( {features.expand && (
<th className="text-center ticky-action-column"> <th className="text-center ticky-action-column">
<i className='bx bx-collapse-vertical'></i> <i className="bx bx-collapse-vertical"></i>
</th> </th>
)} )}
{features.selection && ( {features.selection && (
@ -284,7 +282,7 @@ export default function PmsGrid({
/> />
</th> </th>
)} )}
{visibleColumns.map((col) => { {visibleColumns.map((col) => {
const style = { const style = {
minWidth: col.width || 120, minWidth: col.width || 120,
@ -333,10 +331,12 @@ export default function PmsGrid({
<div className="d-flex align-items-center gap-1"> <div className="d-flex align-items-center gap-1">
{features.pinning && ( {features.pinning && (
<PmsHeaderOption <PmsHeaderOption
column={col.key}
pinned={col.pinned} pinned={col.pinned}
onPinLeft={() => pinColumn(col.key, "left")} onPinLeft={() => pinColumn(col.key, "left")}
onPinRight={() => pinColumn(col.key, "right")} onPinRight={() => pinColumn(col.key, "right")}
onUnpin={() => unpinColumn(col.key)} onUnpin={() => unpinColumn(col.key)}
onAdvancedFilter={setAdanceFilter}
/> />
)} )}
{features.resizing && ( {features.resizing && (
@ -378,9 +378,9 @@ export default function PmsGrid({
</tr> </tr>
)} )}
{!loading && groupBy && groupedRows && groupedRows.length > 0 {!loading && groupBy && groupedRows && groupedRows.length > 0
? groupedRows.map((g,indG) => ( ? groupedRows.map((g, indG) => (
<React.Fragment key={g.key}> <React.Fragment key={g.key}>
<tr className="bg-light-primary" > <tr className="bg-light-primary">
<td <td
colSpan={ colSpan={
visibleColumns.length + visibleColumns.length +
@ -400,7 +400,7 @@ export default function PmsGrid({
)} )}
</td> </td>
</tr> </tr>
{g.items.map((row,indG) => renderRow(row,indG))} {g.items.map((row, indG) => renderRow(row, indG))}
</React.Fragment> </React.Fragment>
)) ))
: currentRows.map((row, ind) => renderRow(row, ind))} : currentRows.map((row, ind) => renderRow(row, ind))}
@ -450,7 +450,7 @@ export default function PmsGrid({
<small className="text-secondry">{ind + 1}</small> <small className="text-secondry">{ind + 1}</small>
</td> </td>
)} )}
{/* Expand toggle next to selection */} {/* Expand toggle next to selection */}
{features.expand && ( {features.expand && (
<td className="text-center align-middle "> <td className="text-center align-middle ">
<button <button
@ -478,8 +478,6 @@ export default function PmsGrid({
</td> </td>
)} )}
{/* Data columns */} {/* Data columns */}
{visibleColumns.map((col) => { {visibleColumns.map((col) => {
const style = { const style = {

View File

@ -1,101 +1,142 @@
// const PmsHeaderOption = ({ pinned, onPinLeft, onPinRight, onUnpin }) => { import { useState } from "react";
// return (
// <div className="dropdown z-100">
// <button
// type="button"
// className="btn btn-icon btn-text-secondary rounded-pill p-0"
// data-bs-toggle="dropdown"
// data-bs-auto-close="outside"
// aria-expanded="true"
// >
// <i className="bx bx-dots-vertical-rounded bx-sm text-muted"></i>
// </button>
import { useEffect } from "react"; const OPERATORS = {
number: [
{ key: "eq", label: "Equals" },
{ key: "neq", label: "Not Equal" },
{ key: "gt", label: "Greater Than" },
{ key: "gte", label: "Greater or Equal" },
{ key: "less than", label: "Less Than" },
{ key: "lte", label: "Less or Equal" },
{ key: "between", label: "Between" },
],
text: [
{ key: "contains", label: "Contains" },
{ key: "starts", label: "Starts With" },
{ key: "ends", label: "Ends With" },
{ key: "eq", label: "Equals" },
],
date: [
{ key: "before", label: "Before" },
{ key: "after", label: "After" },
{ key: "between", label: "Between" },
],
};
// <ul className="dropdown-menu dropdown-menu-end border shadow rounded-3 py-2"> // ----------- FILTER UI COMPONENT ----------
// <li> function AdvanceFilter({ type = "number", onApply, onClear }) {
// <button const [operator, setOperator] = useState("");
// className={`dropdown-item d-flex align-items-center ${ const [value1, setValue1] = useState("");
// pinned === "left" ? "active" : "" const [value2, setValue2] = useState("");
// }`}
// onClick={onPinLeft}
// >
// <i className="bx bx-pin me-2"></i> Pin Left
// </button>
// </li>
// <li>
// <button
// className={`dropdown-item d-flex align-items-center ${
// pinned === "right" ? "active" : ""
// }`}
// onClick={onPinRight}
// >
// <i className="bx bx-pin me-2"></i> Pin Right
// </button>
// </li>
// {pinned && (
// <>
// <li>
// <hr className="dropdown-divider" />
// </li>
// <li>
// <button
// className="dropdown-item text-danger d-flex align-items-center"
// onClick={onUnpin}
// >
// <i className="bx bx-x me-2"></i> Unpin Column
// </button>
// </li>
// </>
// )}
// </ul>
// </div>
// );
// };
// export default PmsHeaderOption;
const PmsHeaderOption = ({ pinned, onPinLeft, onPinRight, onUnpin }) => {
const toggleSubmenu = (e) => {
e.stopPropagation();
const currentSubMenu = e.currentTarget.nextElementSibling; const ops = OPERATORS[type];
if (!currentSubMenu) return;
const allSubMenus = document.querySelectorAll( const apply = () => {
".dropdown-submenu > .dropdown-menu" if (!operator) return;
);
allSubMenus.forEach((menu) => { if (operator === "between") {
if (menu !== currentSubMenu) { onApply({
menu.classList.remove("show"); operator,
} from: value1,
}); to: value2,
});
currentSubMenu.classList.toggle("show"); } else {
onApply({
operator,
value: value1,
});
}
}; };
useEffect(() => { return (
const handleOutsideClick = () => { <div
const allSubMenus = document.querySelectorAll( className="dropdown p-3"
".dropdown-submenu > .dropdown-menu" onClick={(e) => e.stopPropagation()} // prevent closing menu
); style={{ width: 240 }}
>
{/* Operator Dropdown */}
<div className="mb-2">
<label className="form-label">Condition</label>
<select
className="form-select form-select-sm"
value={operator}
onChange={(e) => setOperator(e.target.value)}
>
<option value="">Select</option>
{ops.map((o) => (
<option key={o.key} value={o.key}>
{o.label}
</option>
))}
</select>
</div>
allSubMenus.forEach((menu) => { {/* Values */}
menu.classList.remove("show"); {operator && (
}); <div>
}; <label className="form-label">Value</label>
document.addEventListener("click", handleOutsideClick); {operator !== "between" ? (
<input
type={type === "date" ? "date" : "number"}
className="form-control form-control-sm"
value={value1}
onChange={(e) => setValue1(e.target.value)}
/>
) : (
<div className="d-flex gap-2">
<input
type={type === "date" ? "date" : "number"}
className="form-control form-control-sm"
placeholder="From"
value={value1}
onChange={(e) => setValue1(e.target.value)}
/>
return () => { <input
document.removeEventListener("click", handleOutsideClick); type={type === "date" ? "date" : "number"}
}; className="form-control form-control-sm"
}, []); placeholder="To"
value={value2}
onChange={(e) => setValue2(e.target.value)}
/>
</div>
)}
</div>
)}
{/* Buttons */}
<div className="d-flex justify-content-between mt-3">
<button className="btn btn-light btn-sm" onClick={onClear}>
Clear
</button>
<button className="btn btn-primary btn-sm" onClick={apply}>
Apply
</button>
</div>
</div>
);
}
// -------- HEADER OPTION ---------------
const PmsHeaderOption = ({
column,
pinned,
onPinLeft,
onPinRight,
onUnpin,
onAdvancedFilter,
}) => {
const [openMenu, setOpenMenu] = useState(null);
const toggleMenu = (key, e) => {
e.stopPropagation();
setOpenMenu((prev) => (prev === key ? null : key));
};
return ( return (
<div className="dropdown" style={{ zIndex: 9999 }}> <div className="dropdown" style={{ zIndex: 9999 }}>
<button <button
type="button"
className="btn btn-icon btn-text-secondary rounded-pill p-0" className="btn btn-icon btn-text-secondary rounded-pill p-0"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
data-bs-auto-close="outside" data-bs-auto-close="outside"
@ -103,72 +144,61 @@ const PmsHeaderOption = ({ pinned, onPinLeft, onPinRight, onUnpin }) => {
<i className="bx bx-dots-vertical-rounded bx-sm text-muted"></i> <i className="bx bx-dots-vertical-rounded bx-sm text-muted"></i>
</button> </button>
<ul className="dropdown-menu dropdown-menu-end border shadow rounded-3 py-2"> <ul className="dropdown-menu dropdown-menu-end shadow border rounded-3 py-2">
{/* ADVANCED FILTER */}
<li className="dropdown-submenu dropstart"> <li className="dropdown-submenu dropstart">
<button <button
type="button" className="dropdown-item d-flex justify-content-between"
className="dropdown-item d-flex align-items-center justify-content-between" onClick={(e) => toggleMenu("filter", e)}
onClick={toggleSubmenu}
> >
<span> <span>
<i className="bx bx-left-arrow-alt me-2"></i> <i className="bx bx-filter-alt me-2"></i> Filter
Move Column
</span> </span>
<i className="bx bx-chevron-left"></i> <i className="bx bx-chevron-left"></i>
</button> </button>
<ul className="dropdown-menu border shadow rounded-3 py-2"> <ul
<li> className={`dropdown-menu shadow rounded-3 py-2 p-0 ${
<button openMenu === "filter" ? "show" : ""
className="dropdown-item d-flex align-items-center" }`}
onClick={() => console.log("Move Left")} >
> <li className="p-0">
<i className="bx bx-arrow-to-left me-2"></i> <AdvanceFilter
Move Left type="number"
</button> onApply={(f) => onAdvancedFilter({...f,column})}
</li> onClear={() => onAdvancedFilter(null)}
/>
<li>
<button
className="dropdown-item d-flex align-items-center"
onClick={() => console.log("Move Right")}
>
<i className="bx bx-arrow-to-right me-2"></i>
Move Right
</button>
</li> </li>
</ul> </ul>
</li> </li>
{/* PIN COLUMN */}
<li className="dropdown-submenu dropstart"> <li className="dropdown-submenu dropstart">
<button <button
type="button" className="dropdown-item d-flex justify-content-between"
className="dropdown-item d-flex align-items-center justify-content-between" onClick={(e) => toggleMenu("pin", e)}
onClick={toggleSubmenu}
> >
<span> <span>
<i className="bx bx-pin me-2"></i> Pin Column <i className="bx bx-pin me-2"></i> Pin Column
</span> </span>
<i className="bx bx-chevron-right"></i> <i className="bx bx-chevron-left"></i>
</button> </button>
<ul className="dropdown-menu border shadow rounded-3 py-2"> <ul
className={`dropdown-menu shadow rounded-3 py-2 ${
openMenu === "pin" ? "show" : ""
}`}
>
<li> <li>
<button <button className="dropdown-item" onClick={onUnpin}>
className="dropdown-item d-flex align-items-center" {!pinned && <i className="bx bx-check me-2"></i>}
onClick={onUnpin}
>
{pinned !== "left" && pinned !== "right" && (
<i className="bx bx-check me-2"></i>
)}
No Pin No Pin
</button> </button>
</li> </li>
<li> <li>
<button <button
className={`dropdown-item d-flex align-items-center ${ className={`dropdown-item ${pinned === "left" ? "active" : ""}`}
pinned === "left" ? "active" : ""
}`}
onClick={onPinLeft} onClick={onPinLeft}
> >
{pinned === "left" && <i className="bx bx-check me-2"></i>} {pinned === "left" && <i className="bx bx-check me-2"></i>}
@ -176,9 +206,9 @@ const PmsHeaderOption = ({ pinned, onPinLeft, onPinRight, onUnpin }) => {
</button> </button>
</li> </li>
<li className="d-flex flex-row gap-2"> <li>
<button <button
className={`dropdown-item d-flex align-items-center ${ className={`dropdown-item ${
pinned === "right" ? "active" : "" pinned === "right" ? "active" : ""
}`} }`}
onClick={onPinRight} onClick={onPinRight}

View File

@ -20,6 +20,7 @@ export function useGridCore({
const [debouncedSearch, setDebouncedSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState("");
const [groupBy, setGroupBy] = useState(null); const [groupBy, setGroupBy] = useState(null);
const [serverGroupRows, setServerGroupRows] = useState(null); const [serverGroupRows, setServerGroupRows] = useState(null);
const [onAdvanceFilter, setAdanceFilter] = useState(null);
const [sortBy, setSortBy] = useState({ key: null, dir: "asc" }); const [sortBy, setSortBy] = useState({ key: null, dir: "asc" });
const [selected, setSelected] = useState(new Set()); const [selected, setSelected] = useState(new Set());
@ -74,6 +75,7 @@ export function useGridCore({
// server-side fetch // server-side fetch
const fetchServer = useCallback(async () => { const fetchServer = useCallback(async () => {
// sorting column wise
const sortFilters = sortBy.key const sortFilters = sortBy.key
? [ ? [
{ {
@ -83,9 +85,19 @@ export function useGridCore({
] ]
: []; : [];
// -----------------------------
// 3. ADVANCED FILTERS (NUMERIC FILTER POPOVER)
// The grid will pass "filter" already shaped like:
// filter = { totalAmount: { type: "gt", value: 200 } }
// -----------------------------
let advanceFilters = [];
if (onAdvanceFilter) {
advanceFilters.push(onAdvanceFilter);
}
const filterPayload = JSON.stringify({ const filterPayload = JSON.stringify({
sortFilters, sortFilters,
groupByColumn: groupBy || null, groupByColumn: groupBy || null,
advanceFilters,
}); });
if (!serverMode || typeof fetcher !== "function") return; if (!serverMode || typeof fetcher !== "function") return;
@ -104,7 +116,16 @@ export function useGridCore({
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [serverMode, fetcher, page, pageSize, sortBy, debouncedSearch, groupBy]); }, [
serverMode,
fetcher,
page,
pageSize,
sortBy,
debouncedSearch,
groupBy,
onAdvanceFilter,
]);
useEffect(() => { useEffect(() => {
if (serverMode) fetchServer(); if (serverMode) fetchServer();
@ -209,6 +230,8 @@ export function useGridCore({
setColState, setColState,
groupBy, groupBy,
setGroupBy, setGroupBy,
onAdvanceFilter,
setAdanceFilter,
// data // data
rows, rows,