added filter - sorting, and groupby

This commit is contained in:
pramod.mahajan 2025-11-22 19:17:47 +05:30
parent b8861fbf41
commit ee1cf01743
7 changed files with 251 additions and 85 deletions

View File

@ -29,7 +29,7 @@ const PmGridCollection = ({ selectedProject, fromDate, toDate, isPending }) => {
{ key: "isActive", title: "Status" },
];
const fetcher = async ({ page, pageSize, search }) => {
const fetcher = async ({ page, pageSize, search, filter }) => {
const response = await CollectionRepository.getCollections(
selectedProject,
search || "",
@ -38,7 +38,8 @@ const PmGridCollection = ({ selectedProject, fromDate, toDate, isPending }) => {
pageSize,
page,
true, // isActive
isPending
isPending,
filter
);
const api = response.data;

View File

@ -1,45 +1,68 @@
import React from 'react'
import React from "react";
export const EmployeeChip = ({handleRemove,employee}) => {
return(
<span
key={employee?.id}
className="tagify__tag d-inline-flex align-items-center me-1 mb-1"
role="listitem"
>
<div className="d-flex align-items-center">
{employee?.photo ? (
<span className="tagify__tag__avatar-wrap me-1">
<img
src={employee?.avataremployeerl || "/defaemployeelt-avatar.png"}
alt={`${employee?.firstName || ""} ${employee?.lastName || ""}`}
style={{ width: 12, height: 12, objectFit: "cover" }}
/>
</span>
) : (
<div className="avatar avatar-xs me-2">
<span className="avatar-initial roemployeended-circle bg-label-secondary">
{employee?.firstName?.[0] || ""}
{employee?.lastName?.[0] || ""}
</span>
</div>
)}
export const EmployeeChip = ({ handleRemove, employee }) => {
return (
<span
key={employee?.id}
className="tagify__tag d-inline-flex align-items-center me-1 mb-1"
role="listitem"
>
<div className="d-flex align-items-center">
{employee?.photo ? (
<span className="tagify__tag__avatar-wrap me-1">
<img
src={employee?.avataremployeerl || "/defaemployeelt-avatar.png"}
alt={`${employee?.firstName || ""} ${employee?.lastName || ""}`}
style={{ width: 12, height: 12, objectFit: "cover" }}
/>
</span>
) : (
<div className="avatar avatar-xs me-2">
<span className="avatar-initial roemployeended-circle bg-label-secondary">
{employee?.firstName?.[0] || ""}
{employee?.lastName?.[0] || ""}
</span>
</div>
)}
<div className="d-flex flex-colemployeemn">
<span className="tagify__tag-text">
{employee?.firstName} {employee?.lastName}
</span>
</div>
</div>
<div className="d-flex flex-colemployeemn">
<span className="tagify__tag-text">
{employee?.firstName} {employee?.lastName}
</span>
</div>
</div>
<bemployeetton
type="bemployeetton"
className="tagify__tag__removeBtn border-none"
onClick={() => handleRemove(employee?.id)}
aria-label={`Remove ${employee?.firstName}`}
title="Remove"
/>
</span>
)
}
<bemployeetton
type="bemployeetton"
className="tagify__tag__removeBtn border-none"
onClick={() => handleRemove(employee?.id)}
aria-label={`Remove ${employee?.firstName}`}
title="Remove"
/>
</span>
);
};
export const Chip = ({ handleRemove, chipObj }) => {
return (
<span
key={chipObj?.id}
className="tagify__tag d-inline-flex align-items-center me-1 mb-1"
role="listitem"
>
<div className="d-flex align-items-center">
<div className="d-flex flex-colemployeemn">
<span className="tagify__tag-text">{chipObj?.name}</span>
</div>
</div>
<bemployeetton
type="bemployeetton"
className="tagify__tag__removeBtn border-none"
onClick={() => handleRemove(chipObj?.id)}
aria-label={`Remove ${chipObj.name}`}
title="Remove"
/>
</span>
);
};

View File

@ -5,7 +5,7 @@ export const CollectionRepository = {
createNewCollection: (data) =>
api.post(`/api/Collection/invoice/create`, data),
updateCollection: (id, data) => {
api.put(`/api/Collection/invoice/edit/${id}`, data)
api.put(`/api/Collection/invoice/edit/${id}`, data);
},
// getCollections: (pageSize, pageNumber,fromDate,toDate, isPending,isActive,projectId, searchString) => {
// let url = `/api/Collection/invoice/list?pageSize=${pageSize}&pageNumber=${pageNumber}&isPending=${isPending}&isActive=${isActive}&searchString=${searchString}`;
@ -21,7 +21,17 @@ export const CollectionRepository = {
// return api.get(url);
// },
getCollections: (projectId, searchString, fromDate, toDate, pageSize, pageNumber, isActive, isPending) => {
getCollections: (
projectId,
searchString,
fromDate,
toDate,
pageSize,
pageNumber,
isActive,
isPending,
filter
) => {
let url = `/api/Collection/invoice/list`;
const params = [];
@ -33,6 +43,7 @@ export const CollectionRepository = {
if (pageNumber) params.push(`pageNumber=${pageNumber}`);
if (isActive) params.push(`isActive=${isActive}`);
if (isPending) params.push(`isPending=${isPending}`);
if (filter) params.push(`filter=${filter}`);
if (params.length > 0) {
url += "?" + params.join("&");
@ -41,9 +52,10 @@ export const CollectionRepository = {
return api.get(url);
},
makeReceivePayment: (data) => api.post(`/api/Collection/invoice/payment/received`, data),
markPaymentReceived: (invoiceId) => api.put(`/api/Collection/invoice/marked/completed/${invoiceId}`),
makeReceivePayment: (data) =>
api.post(`/api/Collection/invoice/payment/received`, data),
markPaymentReceived: (invoiceId) =>
api.put(`/api/Collection/invoice/marked/completed/${invoiceId}`),
getCollection: (id) => api.get(`/api/Collection/invoice/details/${id}`),
addComment: (data) => api.post(`/api/Collection/invoice/add/comment`, data)
addComment: (data) => api.post(`/api/Collection/invoice/add/comment`, data),
};

View File

@ -38,8 +38,7 @@ export default function PmsGrid({
pageSize,
setPage,
setPageSize,
setGroupBy
,
setGroupBy,
search,
setSearch,
selected,
@ -181,27 +180,26 @@ export default function PmsGrid({
Group By
</button>
<div className="dropdown-menu p-2">
<div className="dropdown-menu px-1">
{visibleColumns
.filter((c) => c.groupable)
.map((c) => (
<div
<li
key={c.key}
className="dropdown-item"
onClick={() => grid.setGroupBy(c.key)}
style={{ cursor: "pointer" }}
className="dropdown-item rounded py-1 cursor-pointer"
onClick={() => setGroupBy(c.key)}
>
{c.title}
</div>
</li>
))}
{grid.groupBy && (
<div
className="dropdown-item text-danger"
onClick={() => grid.setGroupBy(null)}
<li
className="dropdown-item text-danger py-1 cursor-pointer rounded"
onClick={() => setGroupBy(null)}
>
Clear Grouping
</div>
</li>
)}
</div>
</div>

View File

@ -1,46 +1,133 @@
// const PmsHeaderOption = ({ pinned, onPinLeft, onPinRight, onUnpin }) => {
// 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>
// <ul className="dropdown-menu dropdown-menu-end border shadow rounded-3 py-2">
// <li>
// <button
// className={`dropdown-item d-flex align-items-center ${
// pinned === "left" ? "active" : ""
// }`}
// 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 }) => {
return (
<div className="dropdown z-100">
<div className="dropdown" style={{ zIndex: 9999 }}>
<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="false"
>
<i className="bx bx-dots-vertical-rounded bx-sm text-muted"></i>
</button>
<ul className="dropdown-menu dropdown-menu-end shadow-sm border-0 rounded-3 py-2">
<ul className="dropdown-menu dropdown-menu-end border shadow rounded-3 py-2">
<li className="dropdown-submenu dropstart">
<button
type="button"
className="dropdown-item d-flex align-items-center justify-content-between"
onClick={(e) => {
e.stopPropagation();
e.currentTarget.nextElementSibling.classList.toggle("show");
}}
>
<span>
<i className="bx bx-pin me-2"></i> Pin
</span>
<i className="bx bx-chevron-right"></i>
</button>
<ul className="dropdown-menu border shadow rounded-3 py-2">
<li>
<button
className={`dropdown-item d-flex align-items-center ${
pinned === "left" ? "active" : ""
}`}
onClick={onPinLeft}
>
<i className="bx bx-left-arrow-alt me-2"></i>
Left
</button>
</li>
<li>
<button
className={`dropdown-item d-flex align-items-center ${
pinned === "right" ? "active" : ""
}`}
onClick={onPinRight}
>
<i className="bx bx-right-arrow-alt me-2"></i>
Right
</button>
</li>
</ul>
</li>
<li>
<button
className={`dropdown-item d-flex align-items-center ${
pinned === "left" ? "active" : ""
}`}
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
<i className="bx bx-left-arrow-alt me-2"></i>
</button>
</li>
{pinned && (
<>
<li><hr className="dropdown-divider" /></li>
<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
<i className="bx bx-x me-2"></i>
Unpin Column
</button>
</li>
</>
@ -49,4 +136,5 @@ const PmsHeaderOption = ({ pinned, onPinLeft, onPinRight, onUnpin }) => {
</div>
);
};
export default PmsHeaderOption;

View File

@ -229,4 +229,21 @@
top: 0;
margin-left: 0;
}
}
}
.dropdown-submenu {
position: relative;
}
.dropdown-submenu > .dropdown-menu {
top: 0;
left: 100%;
margin-left: 0.1rem;
display: none;
}
.dropdown-submenu > .dropdown-menu.show {
display: block;
}

View File

@ -19,6 +19,8 @@ export function useGridCore({
const [search, setSearch] = useState("");
const [debouncedSearch, setDebouncedSearch] = useState("");
const [groupBy, setGroupBy] = useState(null);
const [serverGroupRows, setServerGroupRows] = useState(null);
const [sortBy, setSortBy] = useState({ key: null, dir: "asc" });
const [selected, setSelected] = useState(new Set());
const [expanded, setExpanded] = useState(new Set());
@ -72,6 +74,20 @@ export function useGridCore({
// server-side fetch
const fetchServer = useCallback(async () => {
const sortFilters = sortBy.key
? [
{
column: sortBy.key,
sortDescending: sortBy.dir === "desc",
},
]
: [];
const filterPayload = JSON.stringify({
sortFilters,
groupByColumn: groupBy || null,
});
if (!serverMode || typeof fetcher !== "function") return;
setLoading(true);
try {
@ -80,6 +96,7 @@ export function useGridCore({
pageSize,
sortBy,
search: debouncedSearch,
filter: filterPayload,
});
// expected: { rows: [], total }
setServerRows(resp.rows || []);
@ -87,7 +104,7 @@ export function useGridCore({
} finally {
setLoading(false);
}
}, [serverMode, fetcher, page, pageSize, sortBy, debouncedSearch]);
}, [serverMode, fetcher, page, pageSize, sortBy, debouncedSearch, groupBy]);
useEffect(() => {
if (serverMode) fetchServer();
@ -133,11 +150,21 @@ export function useGridCore({
}, []);
const changeSort = useCallback((key) => {
setSortBy((prev) =>
prev.key === key
? { key, dir: prev.dir === "asc" ? "desc" : "asc" }
: { key, dir: "asc" }
);
setSortBy((prev) => {
// first click = asc
if (prev.key !== key) {
return { key, dir: "asc" };
}
// second click = desc
if (prev.dir === "asc") {
return { key, dir: "desc" };
}
// third click = remove sort
return { key: null, dir: "asc" };
});
setPage(1);
}, []);