Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 96bcdffdca |
20
public/assets/vendor/css/core.css
vendored
20
public/assets/vendor/css/core.css
vendored
@ -456,7 +456,7 @@ table {
|
|||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
.tr-group{
|
.tr-group{
|
||||||
background-color: var(--bs-body-bg) !important; /* apply globale color for table row, where grouping datewise*/
|
background-color: var(--bs-body-bg); /* apply globale color for table row, where grouping datewise*/
|
||||||
}
|
}
|
||||||
caption {
|
caption {
|
||||||
padding-top: 0.782rem;
|
padding-top: 0.782rem;
|
||||||
@ -481,24 +481,6 @@ th {
|
|||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
}
|
}
|
||||||
.vs-th {
|
|
||||||
position: relative;
|
|
||||||
border: none;
|
|
||||||
background-color: white;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.vs-th::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 6px;
|
|
||||||
bottom: 6px;
|
|
||||||
width: 1px;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
4
src/assets/vendor/css/core.css
vendored
4
src/assets/vendor/css/core.css
vendored
@ -16497,10 +16497,6 @@ html:not([dir=rtl]) .toast.bs-toast .toast-header .btn-close {
|
|||||||
.z-5 {
|
.z-5 {
|
||||||
z-index: 5 !important;
|
z-index: 5 !important;
|
||||||
}
|
}
|
||||||
.z-6 {
|
|
||||||
z-index: 10 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer !important;
|
cursor: pointer !important;
|
||||||
|
|||||||
@ -85,9 +85,8 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
displayField = "Status";
|
displayField = "Status";
|
||||||
break;
|
break;
|
||||||
case "submittedBy":
|
case "submittedBy":
|
||||||
key = `${item?.createdBy?.firstName ?? ""} ${
|
key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? ""
|
||||||
item.createdBy?.lastName ?? ""
|
}`.trim();
|
||||||
}`.trim();
|
|
||||||
displayField = "Submitted By";
|
displayField = "Submitted By";
|
||||||
break;
|
break;
|
||||||
case "project":
|
case "project":
|
||||||
|
|||||||
@ -25,9 +25,7 @@ const Sidebar = () => {
|
|||||||
/>
|
/>
|
||||||
</span> */}
|
</span> */}
|
||||||
|
|
||||||
<small
|
<small className="app-brand-link fw-bold navbar-brand text-green fs-6">
|
||||||
className="app-brand-link fw-bold navbar-brand text-green fs-6"
|
|
||||||
>
|
|
||||||
<span className="app-brand-logo demo">
|
<span className="app-brand-logo demo">
|
||||||
<img src="/img/brand/marco.png" width="50" />
|
<img src="/img/brand/marco.png" width="50" />
|
||||||
</span>
|
</span>
|
||||||
@ -37,8 +35,7 @@ const Sidebar = () => {
|
|||||||
</small>
|
</small>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<small className="layout-menu-toggle menu-link text-large ms-auto">
|
<small className="layout-menu-toggle menu-link text-large ms-auto">
|
||||||
|
|
||||||
<i className="bx bx-chevron-left bx-sm d-flex align-items-center justify-content-center"></i>
|
<i className="bx bx-chevron-left bx-sm d-flex align-items-center justify-content-center"></i>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
@ -61,7 +58,7 @@ const Sidebar = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{data &&
|
{data &&
|
||||||
data?.data.map((section) => (
|
data?.data?.map((section) => (
|
||||||
<React.Fragment
|
<React.Fragment
|
||||||
key={section.id || section.header || section.items[0]?.id}
|
key={section.id || section.header || section.items[0]?.id}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,35 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import FloorTable from "./FloorTable";
|
|
||||||
import { useSelectedProject } from "../../../slices/apiDataManager";
|
|
||||||
import { useProjectInfra } from "../../../hooks/useProjects";
|
|
||||||
import { PmsGrid } from "../../../services/pmsGrid";
|
|
||||||
|
|
||||||
export default function BuildingTable() {
|
|
||||||
const project = useSelectedProject();
|
|
||||||
|
|
||||||
const { projectInfra } = useProjectInfra(project, null);
|
|
||||||
const columns = [
|
|
||||||
{ key: "buildingName", title: "Building", sortable: true,className: "text-start", },
|
|
||||||
{ key: "plannedWork", title: "Planned Work" },
|
|
||||||
{ key: "completedWork", title: "Completed Work" },
|
|
||||||
{ key: "percentage", title: "Completion %" },
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PmsGrid
|
|
||||||
columns={columns}
|
|
||||||
data={projectInfra}
|
|
||||||
rowKey="id"
|
|
||||||
features={{
|
|
||||||
expand: true,
|
|
||||||
pinning: true,
|
|
||||||
resizing: true,
|
|
||||||
selection: true,
|
|
||||||
}}
|
|
||||||
renderExpanded={(building) => <FloorTable floors={building.floors} />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import WorkAreaTable from "./WorkAreaTable";
|
|
||||||
import { PmsGrid } from "../../../services/pmsGrid";
|
|
||||||
|
|
||||||
export default function FloorTable({ floors }) {
|
|
||||||
const columns = [
|
|
||||||
{ key: "floorName", title: "Floor", sortable: true, },
|
|
||||||
{ key: "plannedWork", title: "Planned Work" },
|
|
||||||
{ key: "completedWork", title: "Completed Work" },
|
|
||||||
{ key: "percentage", title: "Completion %" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PmsGrid
|
|
||||||
columns={columns}
|
|
||||||
data={floors}
|
|
||||||
rowKey="id"
|
|
||||||
features={{
|
|
||||||
expand: true,
|
|
||||||
pinning: true,
|
|
||||||
resizing: true,
|
|
||||||
}}
|
|
||||||
renderExpanded={(floor) => (
|
|
||||||
<WorkAreaTable workAreas={floor.workAreas} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { PmsGrid } from "../../../services/pmsGrid";
|
|
||||||
// import { GridService } from "./gridService";
|
|
||||||
// import TaskTable from "./TaskTable";
|
|
||||||
|
|
||||||
export default function WorkAreaTable({ workAreas }) {
|
|
||||||
const [taskData, setTaskData] = useState({}); // workAreaId → tasks
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ key: "areaName", title: "Work Area", },
|
|
||||||
{ key: "plannedWork", title: "Planned Work" },
|
|
||||||
{ key: "completedWork", title: "Completed Work" },
|
|
||||||
{ key: "percentage", title: "Completion %" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// const loadTasks = async (workAreaId) => {
|
|
||||||
// if (!taskData[workAreaId]) {
|
|
||||||
// const res = await GridService.getTasksByWorkArea(workAreaId);
|
|
||||||
// setTaskData((prev) => ({ ...prev, [workAreaId]: res }));
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PmsGrid
|
|
||||||
columns={columns}
|
|
||||||
data={workAreas}
|
|
||||||
rowKey="id"
|
|
||||||
features={{
|
|
||||||
expand: true,
|
|
||||||
pinning: true,
|
|
||||||
resizing: true,
|
|
||||||
}}
|
|
||||||
// renderExpanded={(area) => {
|
|
||||||
// loadTasks(area.id);
|
|
||||||
// return <TaskTable tasks={taskData[area.id]} />;
|
|
||||||
// }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -15,8 +15,6 @@ import { useCollectionContext } from "../../pages/collections/CollectionPage";
|
|||||||
import { CollectionTableSkeleton } from "./CollectionSkeleton";
|
import { CollectionTableSkeleton } from "./CollectionSkeleton";
|
||||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import { PmsGrid } from "../../services/pmsGrid";
|
|
||||||
import PmGridCollection from "./PmGridCollection";
|
|
||||||
|
|
||||||
const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
@ -30,16 +28,16 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
|||||||
const selectedProject = useSelectedProject();
|
const selectedProject = useSelectedProject();
|
||||||
const searchDebounce = useDebounce(searchString, 500);
|
const searchDebounce = useDebounce(searchString, 500);
|
||||||
|
|
||||||
// const { data, isLoading, isError, error } = useCollections(
|
const { data, isLoading, isError, error } = useCollections(
|
||||||
// selectedProject,
|
selectedProject,
|
||||||
// searchDebounce,
|
searchDebounce,
|
||||||
// localToUtc(fromDate),
|
localToUtc(fromDate),
|
||||||
// localToUtc(toDate),
|
localToUtc(toDate),
|
||||||
// ITEMS_PER_PAGE,
|
ITEMS_PER_PAGE,
|
||||||
// currentPage,
|
currentPage,
|
||||||
// true,
|
true,
|
||||||
// isPending
|
isPending
|
||||||
// );
|
);
|
||||||
const { setProcessedPayment, setAddPayment, setViewCollection } =
|
const { setProcessedPayment, setAddPayment, setViewCollection } =
|
||||||
useCollectionContext();
|
useCollectionContext();
|
||||||
const paginate = (page) => {
|
const paginate = (page) => {
|
||||||
@ -159,152 +157,149 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// if (isLoading) return <CollectionTableSkeleton />;
|
if (isLoading) return <CollectionTableSkeleton />;
|
||||||
// if (isError) return <p>{error.message}</p>;
|
if (isError) return <p>{error.message}</p>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <div className="card px-sm-4 px-0">
|
<div className="card px-sm-4 px-0">
|
||||||
// <div
|
<div
|
||||||
// className="card-datatable table-responsive page-min-h"
|
className="card-datatable table-responsive page-min-h"
|
||||||
// id="horizontal-example"
|
id="horizontal-example"
|
||||||
// >
|
>
|
||||||
// <div className="dataTables_wrapper no-footer mx-3 pb-2">
|
<div className="dataTables_wrapper no-footer mx-3 pb-2">
|
||||||
// <table className="table dataTable text-nowrap">
|
<table className="table dataTable text-nowrap">
|
||||||
// <thead>
|
<thead>
|
||||||
// <tr className="table_header_border">
|
<tr className="table_header_border">
|
||||||
// {collectionColumns.map((col) => (
|
{collectionColumns.map((col) => (
|
||||||
// <th key={col.key} className={col.align}>
|
<th key={col.key} className={col.align}>
|
||||||
// {col.label}
|
{col.label}
|
||||||
// </th>
|
</th>
|
||||||
// ))}
|
))}
|
||||||
// {(isAdmin ||
|
{(isAdmin ||
|
||||||
// canAddPayment ||
|
canAddPayment ||
|
||||||
// canViewCollection ||
|
canViewCollection ||
|
||||||
// canEditCollection ||
|
canEditCollection ||
|
||||||
// canCreate) && <th>Action</th>}
|
canCreate) && <th>Action</th>}
|
||||||
// </tr>
|
</tr>
|
||||||
// </thead>
|
</thead>
|
||||||
// <tbody>
|
<tbody>
|
||||||
// {Array.isArray(data?.data) && data.data.length > 0 ? (
|
{Array.isArray(data?.data) && data.data.length > 0 ? (
|
||||||
// data.data.map((row, i) => (
|
data.data.map((row, i) => (
|
||||||
// <tr key={i}>
|
<tr key={i}>
|
||||||
// {collectionColumns.map((col) => (
|
{collectionColumns.map((col) => (
|
||||||
// <td key={col.key} className={col.align}>
|
<td key={col.key} className={col.align}>
|
||||||
// {col.getValue(row)}
|
{col.getValue(row)}
|
||||||
// </td>
|
</td>
|
||||||
// ))}
|
))}
|
||||||
// {(isAdmin ||
|
{(isAdmin ||
|
||||||
// canAddPayment ||
|
canAddPayment ||
|
||||||
// canViewCollection ||
|
canViewCollection ||
|
||||||
// canEditCollection ||
|
canEditCollection ||
|
||||||
// canCreate) && (
|
canCreate) && (
|
||||||
// <td
|
<td
|
||||||
// className="sticky-action-column text-center"
|
className="sticky-action-column text-center"
|
||||||
// style={{ padding: "12px 8px" }}
|
style={{ padding: "12px 8px" }}
|
||||||
// >
|
>
|
||||||
// <div className="dropdown z-2">
|
<div className="dropdown z-2">
|
||||||
// <button
|
<button
|
||||||
// type="button"
|
type="button"
|
||||||
// className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
||||||
// data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
// aria-expanded="false"
|
aria-expanded="false"
|
||||||
// >
|
>
|
||||||
// <i
|
<i
|
||||||
// className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
||||||
// data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
// data-bs-offset="0,8"
|
data-bs-offset="0,8"
|
||||||
// data-bs-placement="top"
|
data-bs-placement="top"
|
||||||
// data-bs-custom-class="tooltip-dark"
|
data-bs-custom-class="tooltip-dark"
|
||||||
// title="More Action"
|
title="More Action"
|
||||||
// ></i>
|
></i>
|
||||||
// </button>
|
</button>
|
||||||
|
|
||||||
// <ul className="dropdown-menu dropdown-menu-end">
|
<ul className="dropdown-menu dropdown-menu-end">
|
||||||
// {/* View */}
|
{/* View */}
|
||||||
|
|
||||||
// <li>
|
<li>
|
||||||
// <a
|
<a
|
||||||
// className="dropdown-item cursor-pointer"
|
className="dropdown-item cursor-pointer"
|
||||||
// onClick={() => setViewCollection(row.id)}
|
onClick={() => setViewCollection(row.id)}
|
||||||
// >
|
>
|
||||||
// <i className="bx bx-show me-2 text-primary"></i>
|
<i className="bx bx-show me-2 text-primary"></i>
|
||||||
// <span>View</span>
|
<span>View</span>
|
||||||
// </a>
|
</a>
|
||||||
// </li>
|
</li>
|
||||||
|
|
||||||
// {/* Only if not completed */}
|
{/* Only if not completed */}
|
||||||
// {!row?.markAsCompleted && (
|
{!row?.markAsCompleted && (
|
||||||
// <>
|
<>
|
||||||
// {/* Add Payment */}
|
{/* Add Payment */}
|
||||||
// {(isAdmin || canAddPayment) && (
|
{(isAdmin || canAddPayment) && (
|
||||||
// <li>
|
<li>
|
||||||
// <a
|
<a
|
||||||
// className="dropdown-item cursor-pointer"
|
className="dropdown-item cursor-pointer"
|
||||||
// onClick={() =>
|
onClick={() =>
|
||||||
// setAddPayment({
|
setAddPayment({
|
||||||
// isOpen: true,
|
isOpen: true,
|
||||||
// invoiceId: row.id,
|
invoiceId: row.id,
|
||||||
// })
|
})
|
||||||
// }
|
}
|
||||||
// >
|
>
|
||||||
// <i className="bx bx-wallet me-2 text-warning"></i>
|
<i className="bx bx-wallet me-2 text-warning"></i>
|
||||||
// <span>Add Payment</span>
|
<span>Add Payment</span>
|
||||||
// </a>
|
</a>
|
||||||
// </li>
|
</li>
|
||||||
// )}
|
)}
|
||||||
|
|
||||||
// {/* Mark Payment */}
|
{/* Mark Payment */}
|
||||||
// {isAdmin && (
|
{isAdmin && (
|
||||||
// <li>
|
<li>
|
||||||
// <a
|
<a
|
||||||
// className="dropdown-item cursor-pointer"
|
className="dropdown-item cursor-pointer"
|
||||||
// onClick={() =>
|
onClick={() =>
|
||||||
// setProcessedPayment({
|
setProcessedPayment({
|
||||||
// isOpen: true,
|
isOpen: true,
|
||||||
// invoiceId: row.id,
|
invoiceId: row.id,
|
||||||
// })
|
})
|
||||||
// }
|
}
|
||||||
// >
|
>
|
||||||
// <i className="bx bx-check-circle me-2 text-success"></i>
|
<i className="bx bx-check-circle me-2 text-success"></i>
|
||||||
// <span>Mark Payment</span>
|
<span>Mark Payment</span>
|
||||||
// </a>
|
</a>
|
||||||
// </li>
|
</li>
|
||||||
// )}
|
)}
|
||||||
// </>
|
</>
|
||||||
// )}
|
)}
|
||||||
// </ul>
|
</ul>
|
||||||
// </div>
|
</div>
|
||||||
// </td>
|
</td>
|
||||||
// )}
|
)}
|
||||||
// </tr>
|
</tr>
|
||||||
// ))
|
))
|
||||||
// ) : (
|
) : (
|
||||||
// <tr style={{ height: "200px" }}>
|
<tr style={{ height: "200px" }}>
|
||||||
// <td
|
<td
|
||||||
// colSpan={collectionColumns.length + 1}
|
colSpan={collectionColumns.length + 1}
|
||||||
// className="text-center border-0 align-middle"
|
className="text-center border-0 align-middle"
|
||||||
// >
|
>
|
||||||
// No Collections Found
|
No Collections Found
|
||||||
// </td>
|
</td>
|
||||||
// </tr>
|
</tr>
|
||||||
// )}
|
)}
|
||||||
// </tbody>
|
</tbody>
|
||||||
// </table>
|
</table>
|
||||||
// {data?.data?.length > 0 && (
|
{data?.data?.length > 0 && (
|
||||||
// <div className="d-flex justify-content-start mt-2">
|
<div className="d-flex justify-content-start mt-2">
|
||||||
// <Pagination
|
<Pagination
|
||||||
// currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
// totalPages={data?.totalPages}
|
totalPages={data?.totalPages}
|
||||||
// onPageChange={paginate}
|
onPageChange={paginate}
|
||||||
// />
|
/>
|
||||||
// </div>
|
</div>
|
||||||
// )}
|
)}
|
||||||
// </div>
|
</div>
|
||||||
// </div>
|
</div>
|
||||||
// </div>
|
</div>
|
||||||
<div className="card p-2">
|
|
||||||
<PmGridCollection selectedProject={selectedProject} fromDate={localToUtc(fromDate)} toDate={localToUtc(toDate)} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,123 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { PmsGrid } from "../../services/pmsGrid";
|
|
||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
|
||||||
import { formatFigure } from "../../utils/appUtils";
|
|
||||||
import { CollectionRepository } from "../../repositories/ColllectionRepository";
|
|
||||||
|
|
||||||
const PmGridCollection = ({ selectedProject, fromDate, toDate, isPending }) => {
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
key: "invoiceNumber",
|
|
||||||
title: "Invoice Number",
|
|
||||||
className: "text-start",
|
|
||||||
groupable: true,
|
|
||||||
},
|
|
||||||
{ key: "title", title: "Title", sortable: true, className: "text-start" },
|
|
||||||
{
|
|
||||||
key: "clientSubmitedDate",
|
|
||||||
title: "Submission Date",
|
|
||||||
className: "text-start",
|
|
||||||
groupable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "exceptedPaymentDate",
|
|
||||||
title: "Expected Payment Date",
|
|
||||||
className: "text-start",
|
|
||||||
},
|
|
||||||
{ key: "totalAmount", title: "Total Amount" },
|
|
||||||
{ key: "balanceAmount", title: "Balance" },
|
|
||||||
{ key: "isActive", title: "Status" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const fetcher = async ({ page, pageSize, search, filter }) => {
|
|
||||||
const response = await CollectionRepository.getCollections(
|
|
||||||
selectedProject,
|
|
||||||
search || "",
|
|
||||||
fromDate,
|
|
||||||
toDate,
|
|
||||||
pageSize,
|
|
||||||
page,
|
|
||||||
true, // isActive
|
|
||||||
isPending,
|
|
||||||
filter
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = response.data;
|
|
||||||
|
|
||||||
return {
|
|
||||||
rows: api.data.map((item) => ({
|
|
||||||
id: item.id,
|
|
||||||
invoiceNumber: item.invoiceNumber,
|
|
||||||
title: item.title,
|
|
||||||
|
|
||||||
clientSubmitedDate: formatUTCToLocalTime(item.clientSubmitedDate),
|
|
||||||
exceptedPaymentDate: formatUTCToLocalTime(item.exceptedPaymentDate),
|
|
||||||
|
|
||||||
totalAmount: formatFigure(item.basicAmount + item.taxAmount, {
|
|
||||||
type: "currency",
|
|
||||||
currency: "INR",
|
|
||||||
}),
|
|
||||||
|
|
||||||
balanceAmount: formatFigure(item.balanceAmount, {
|
|
||||||
type: "currency",
|
|
||||||
currency: "INR",
|
|
||||||
}),
|
|
||||||
|
|
||||||
isActive: item.isActive ? (
|
|
||||||
<span className="badge bg-label-primary">
|
|
||||||
<span className="badge badge-dot bg-primary me-1"></span>
|
|
||||||
Active
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="badge bg-label-danger">
|
|
||||||
<span className="badge badge-dot bg-danger me-1"></span>
|
|
||||||
In-Active
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
|
|
||||||
total: api.totalEntities,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PmsGrid
|
|
||||||
columns={columns}
|
|
||||||
serverMode
|
|
||||||
fetcher={fetcher}
|
|
||||||
rowKey="id"
|
|
||||||
features={{
|
|
||||||
search: true,
|
|
||||||
pagination: true,
|
|
||||||
pinning: true,
|
|
||||||
resizing: true,
|
|
||||||
selection: false,
|
|
||||||
reorder: true,
|
|
||||||
columnVisibility: true,
|
|
||||||
pageSizeSelector: true,
|
|
||||||
|
|
||||||
grouping: true,
|
|
||||||
groupByKey: "clientSubmitedDate",
|
|
||||||
|
|
||||||
aggregation: true,
|
|
||||||
IsNumbering: true,
|
|
||||||
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: "Edit",
|
|
||||||
icon: "bx-edit ",
|
|
||||||
onClick: (row) => console.log("Edit", row),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Delete",
|
|
||||||
icon: "bx-trash text-danger",
|
|
||||||
onClick: (row) => console.log("Delete", row),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PmGridCollection;
|
|
||||||
@ -50,6 +50,7 @@ const InputSuggestions = ({
|
|||||||
{filteredList.map((org) => (
|
{filteredList.map((org) => (
|
||||||
<li
|
<li
|
||||||
key={org}
|
key={org}
|
||||||
|
className="ropdown-item"
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
padding: "5px 12px",
|
padding: "5px 12px",
|
||||||
|
|||||||
@ -24,7 +24,6 @@ import { useProjectAccess } from "../../hooks/useProjectAccess";
|
|||||||
|
|
||||||
import "./ProjectDetails.css";
|
import "./ProjectDetails.css";
|
||||||
import ProjectOrganizations from "../../components/Project/ProjectOrganizations";
|
import ProjectOrganizations from "../../components/Project/ProjectOrganizations";
|
||||||
import BuildingTable from "../../components/Project/pmsInfrastructure/BuildingTable";
|
|
||||||
|
|
||||||
const ProjectDetails = () => {
|
const ProjectDetails = () => {
|
||||||
const projectId = useSelectedProject();
|
const projectId = useSelectedProject();
|
||||||
@ -96,7 +95,7 @@ const ProjectDetails = () => {
|
|||||||
case "teams":
|
case "teams":
|
||||||
return <Teams />;
|
return <Teams />;
|
||||||
case "infra":
|
case "infra":
|
||||||
return <BuildingTable/>
|
return <ProjectInfra data={projects_Details} onDataChange={refetch} />;
|
||||||
case "workplan":
|
case "workplan":
|
||||||
return <WorkPlan data={projects_Details} onDataChange={refetch} />;
|
return <WorkPlan data={projects_Details} onDataChange={refetch} />;
|
||||||
case "directory":
|
case "directory":
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export const CollectionRepository = {
|
|||||||
createNewCollection: (data) =>
|
createNewCollection: (data) =>
|
||||||
api.post(`/api/Collection/invoice/create`, data),
|
api.post(`/api/Collection/invoice/create`, data),
|
||||||
updateCollection: (id, 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) => {
|
// 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}`;
|
// let url = `/api/Collection/invoice/list?pageSize=${pageSize}&pageNumber=${pageNumber}&isPending=${isPending}&isActive=${isActive}&searchString=${searchString}`;
|
||||||
@ -21,17 +21,7 @@ export const CollectionRepository = {
|
|||||||
// return api.get(url);
|
// return api.get(url);
|
||||||
// },
|
// },
|
||||||
|
|
||||||
getCollections: (
|
getCollections: (projectId, searchString, fromDate, toDate, pageSize, pageNumber, isActive, isPending) => {
|
||||||
projectId,
|
|
||||||
searchString,
|
|
||||||
fromDate,
|
|
||||||
toDate,
|
|
||||||
pageSize,
|
|
||||||
pageNumber,
|
|
||||||
isActive,
|
|
||||||
isPending,
|
|
||||||
filter
|
|
||||||
) => {
|
|
||||||
let url = `/api/Collection/invoice/list`;
|
let url = `/api/Collection/invoice/list`;
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
@ -43,7 +33,6 @@ export const CollectionRepository = {
|
|||||||
if (pageNumber) params.push(`pageNumber=${pageNumber}`);
|
if (pageNumber) params.push(`pageNumber=${pageNumber}`);
|
||||||
if (isActive) params.push(`isActive=${isActive}`);
|
if (isActive) params.push(`isActive=${isActive}`);
|
||||||
if (isPending) params.push(`isPending=${isPending}`);
|
if (isPending) params.push(`isPending=${isPending}`);
|
||||||
if (filter) params.push(`filter=${filter}`);
|
|
||||||
|
|
||||||
if (params.length > 0) {
|
if (params.length > 0) {
|
||||||
url += "?" + params.join("&");
|
url += "?" + params.join("&");
|
||||||
@ -52,10 +41,9 @@ export const CollectionRepository = {
|
|||||||
return api.get(url);
|
return api.get(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
makeReceivePayment: (data) =>
|
makeReceivePayment: (data) => api.post(`/api/Collection/invoice/payment/received`, data),
|
||||||
api.post(`/api/Collection/invoice/payment/received`, data),
|
markPaymentReceived: (invoiceId) => api.put(`/api/Collection/invoice/marked/completed/${invoiceId}`),
|
||||||
markPaymentReceived: (invoiceId) =>
|
|
||||||
api.put(`/api/Collection/invoice/marked/completed/${invoiceId}`),
|
|
||||||
getCollection: (id) => api.get(`/api/Collection/invoice/details/${id}`),
|
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)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -62,7 +62,6 @@ import AdvancePaymentPage from "../pages/AdvancePayment/AdvancePaymentPage";
|
|||||||
import ServiceProjectDetail from "../pages/ServiceProject/ServiceProjectDetail";
|
import ServiceProjectDetail from "../pages/ServiceProject/ServiceProjectDetail";
|
||||||
import ManageJob from "../components/ServiceProject/ServiceProjectJob/ManageJob";
|
import ManageJob from "../components/ServiceProject/ServiceProjectJob/ManageJob";
|
||||||
import AdvancePaymentPage1 from "../pages/AdvancePayment/AdvancePaymentPage1";
|
import AdvancePaymentPage1 from "../pages/AdvancePayment/AdvancePaymentPage1";
|
||||||
import DemoBOQGrid from "../services/pmsGrid/BasicTable";
|
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@ -82,8 +81,6 @@ const router = createBrowserRouter(
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: "/auth/switch/org", element: <TenantSelectionPage /> },
|
{ path: "/auth/switch/org", element: <TenantSelectionPage /> },
|
||||||
{ path: "/help/docs", element: <DemoBOQGrid /> },
|
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "/auth/subscripe/:frequency/:planId",
|
path: "/auth/subscripe/:frequency/:planId",
|
||||||
element: <MakeSubscription />,
|
element: <MakeSubscription />,
|
||||||
@ -130,7 +127,8 @@ const router = createBrowserRouter(
|
|||||||
{ path: "/tenant/self", element: <SelfTenantDetails /> },
|
{ path: "/tenant/self", element: <SelfTenantDetails /> },
|
||||||
{ path: "/organizations", element: <OrganizationPage /> },
|
{ path: "/organizations", element: <OrganizationPage /> },
|
||||||
{ path: "/help/support", element: <Support /> },
|
{ path: "/help/support", element: <Support /> },
|
||||||
|
{ path: "/help/docs", element: <Documentation /> },
|
||||||
|
{ path: "/help/connect", element: <Connect /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,811 +0,0 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
import { PmsGrid } from "./index";
|
|
||||||
import { initPopover } from "./GridService";
|
|
||||||
import PmsHeaderOption from "./PmsHeaderOption";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CIVIL BOQ / INVENTORY DEMO DATA
|
|
||||||
* Each row = BOQ item, grouped by "Category"
|
|
||||||
*/
|
|
||||||
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: "Masonry Works",
|
|
||||||
itemCode: "M-002",
|
|
||||||
description: "Block Masonry AAC 200mm wall",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 240,
|
|
||||||
rate: 1100,
|
|
||||||
site: "Green City",
|
|
||||||
vendor: "Ambuja Blocks",
|
|
||||||
status: "Pending",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
category: "Plastering Works",
|
|
||||||
itemCode: "P-002",
|
|
||||||
description: "External Plaster 20mm thick",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 800,
|
|
||||||
rate: 320,
|
|
||||||
site: "Sunshine Plaza",
|
|
||||||
vendor: "Hindustan Plaster",
|
|
||||||
status: "In Progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
category: "Flooring Works",
|
|
||||||
itemCode: "F-001",
|
|
||||||
description: "Vitrified Tiles 600x600mm",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 600,
|
|
||||||
rate: 650,
|
|
||||||
site: "Green City - Clubhouse",
|
|
||||||
vendor: "Kajaria",
|
|
||||||
status: "Pending",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
category: "Flooring Works",
|
|
||||||
itemCode: "F-002",
|
|
||||||
description: "Granite Flooring 20mm thick",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 300,
|
|
||||||
rate: 1250,
|
|
||||||
site: "Skyline Heights - Lobby",
|
|
||||||
vendor: "Classic Stone",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
category: "Electrical Works",
|
|
||||||
itemCode: "E-001",
|
|
||||||
description: "PVC Conduit 25mm dia",
|
|
||||||
unit: "Mtr",
|
|
||||||
quantity: 1500,
|
|
||||||
rate: 45,
|
|
||||||
site: "Eco Towers",
|
|
||||||
vendor: "Finolex",
|
|
||||||
status: "In Progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
category: "Electrical Works",
|
|
||||||
itemCode: "E-002",
|
|
||||||
description: "Copper Wire 4 sqmm",
|
|
||||||
unit: "Mtr",
|
|
||||||
quantity: 2000,
|
|
||||||
rate: 65,
|
|
||||||
site: "Skyline Heights",
|
|
||||||
vendor: "Polycab",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 13,
|
|
||||||
category: "Plumbing Works",
|
|
||||||
itemCode: "PL-001",
|
|
||||||
description: "CPVC Pipe 25mm dia",
|
|
||||||
unit: "Mtr",
|
|
||||||
quantity: 1000,
|
|
||||||
rate: 120,
|
|
||||||
site: "Green City - Tower C",
|
|
||||||
vendor: "Astral Pipes",
|
|
||||||
status: "Pending",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 14,
|
|
||||||
category: "Plumbing Works",
|
|
||||||
itemCode: "PL-002",
|
|
||||||
description: "UPVC Pipe 40mm dia",
|
|
||||||
unit: "Mtr",
|
|
||||||
quantity: 800,
|
|
||||||
rate: 100,
|
|
||||||
site: "Sunshine Plaza",
|
|
||||||
vendor: "Supreme Industries",
|
|
||||||
status: "In Progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 15,
|
|
||||||
category: "Painting Works",
|
|
||||||
itemCode: "PA-001",
|
|
||||||
description: "Interior Emulsion Paint",
|
|
||||||
unit: "Ltr",
|
|
||||||
quantity: 500,
|
|
||||||
rate: 180,
|
|
||||||
site: "Skyline Heights",
|
|
||||||
vendor: "Asian Paints",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 16,
|
|
||||||
category: "Painting Works",
|
|
||||||
itemCode: "PA-002",
|
|
||||||
description: "Exterior Weatherproof Paint",
|
|
||||||
unit: "Ltr",
|
|
||||||
quantity: 400,
|
|
||||||
rate: 220,
|
|
||||||
site: "Green City - Tower D",
|
|
||||||
vendor: "Berger Paints",
|
|
||||||
status: "Pending",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 17,
|
|
||||||
category: "Waterproofing Works",
|
|
||||||
itemCode: "W-001",
|
|
||||||
description: "Cementitious Coating 2 layers",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 350,
|
|
||||||
rate: 480,
|
|
||||||
site: "Sunshine Plaza",
|
|
||||||
vendor: "Dr. Fixit",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 18,
|
|
||||||
category: "Waterproofing Works",
|
|
||||||
itemCode: "W-002",
|
|
||||||
description: "Polyurethane Waterproofing Membrane Polyurethane Waterproofing Membrane",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 200,
|
|
||||||
rate: 850,
|
|
||||||
site: "Green City - Podium",
|
|
||||||
vendor: "Pidilite",
|
|
||||||
status: "In Progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 19,
|
|
||||||
category: "HVAC Works",
|
|
||||||
itemCode: "H-001",
|
|
||||||
description: "Ducting for AHU system",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 250,
|
|
||||||
rate: 2100,
|
|
||||||
site: "Skyline Heights - Basement",
|
|
||||||
vendor: "Blue Star",
|
|
||||||
status: "Pending",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 20,
|
|
||||||
category: "HVAC Works",
|
|
||||||
itemCode: "H-002",
|
|
||||||
description: "VRF Indoor Unit Installation",
|
|
||||||
unit: "Nos",
|
|
||||||
quantity: 30,
|
|
||||||
rate: 42000,
|
|
||||||
site: "Eco Towers",
|
|
||||||
vendor: "Daikin",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 21,
|
|
||||||
category: "Plumbing Works",
|
|
||||||
itemCode: "PL-003",
|
|
||||||
description: "CP Fittings - Wash Basin Mixer",
|
|
||||||
unit: "Nos",
|
|
||||||
quantity: 50,
|
|
||||||
rate: 950,
|
|
||||||
site: "Skyline Heights",
|
|
||||||
vendor: "Jaquar",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 22,
|
|
||||||
category: "Electrical Works",
|
|
||||||
itemCode: "E-003",
|
|
||||||
description: "LED Downlight 12W",
|
|
||||||
unit: "Nos",
|
|
||||||
quantity: 120,
|
|
||||||
rate: 750,
|
|
||||||
site: "Green City - Tower B",
|
|
||||||
vendor: "Philips",
|
|
||||||
status: "Pending",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 23,
|
|
||||||
category: "Flooring Works",
|
|
||||||
itemCode: "F-003",
|
|
||||||
description: "Wooden Flooring 12mm laminate",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 150,
|
|
||||||
rate: 1450,
|
|
||||||
site: "Sunshine Plaza - Office",
|
|
||||||
vendor: "Pergo",
|
|
||||||
status: "In Progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 24,
|
|
||||||
category: "Concrete Works",
|
|
||||||
itemCode: "C-003",
|
|
||||||
description: "PCC 1:4:8 flooring",
|
|
||||||
unit: "Cum",
|
|
||||||
quantity: 60,
|
|
||||||
rate: 4800,
|
|
||||||
site: "Eco Towers",
|
|
||||||
vendor: "RMC Readymix",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 25,
|
|
||||||
category: "Masonry Works",
|
|
||||||
itemCode: "M-003",
|
|
||||||
description: "Stone masonry in foundation",
|
|
||||||
unit: "Cum",
|
|
||||||
quantity: 45,
|
|
||||||
rate: 3500,
|
|
||||||
site: "Green City - Tower C",
|
|
||||||
vendor: "Shree Bricks",
|
|
||||||
status: "In Progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 26,
|
|
||||||
category: "Carpentry Works",
|
|
||||||
itemCode: "CP-001",
|
|
||||||
description: "Flush Door with Laminate Finish",
|
|
||||||
unit: "Nos",
|
|
||||||
quantity: 80,
|
|
||||||
rate: 6500,
|
|
||||||
site: "Skyline Heights",
|
|
||||||
vendor: "Greenply",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 27,
|
|
||||||
category: "Carpentry Works",
|
|
||||||
itemCode: "CP-002",
|
|
||||||
description: "Modular Kitchen Cabinets",
|
|
||||||
unit: "Set",
|
|
||||||
quantity: 25,
|
|
||||||
rate: 42000,
|
|
||||||
site: "Sunshine Plaza",
|
|
||||||
vendor: "Godrej Interio",
|
|
||||||
status: "Pending",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 28,
|
|
||||||
category: "Glazing Works",
|
|
||||||
itemCode: "G-001",
|
|
||||||
description: "Aluminium Window Frame with Glass",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 180,
|
|
||||||
rate: 1850,
|
|
||||||
site: "Green City - Tower D",
|
|
||||||
vendor: "Saint Gobain",
|
|
||||||
status: "In Progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 29,
|
|
||||||
category: "Glazing Works",
|
|
||||||
itemCode: "G-002",
|
|
||||||
description: "Toughened Glass Door 12mm",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 60,
|
|
||||||
rate: 2750,
|
|
||||||
site: "Skyline Heights - Lobby",
|
|
||||||
vendor: "Modiguard",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 30,
|
|
||||||
category: "External Development",
|
|
||||||
itemCode: "ED-001",
|
|
||||||
description: "Paver Block 60mm thick",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 950,
|
|
||||||
rate: 380,
|
|
||||||
site: "Sunshine Plaza",
|
|
||||||
vendor: "Ultra Pavers",
|
|
||||||
status: "Pending",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 31,
|
|
||||||
category: "External Development",
|
|
||||||
itemCode: "ED-002",
|
|
||||||
description: "Kerb Stone 300x300mm",
|
|
||||||
unit: "Rm",
|
|
||||||
quantity: 200,
|
|
||||||
rate: 240,
|
|
||||||
site: "Green City - Parking Area",
|
|
||||||
vendor: "L&T Infra",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 32,
|
|
||||||
category: "Electrical Works",
|
|
||||||
itemCode: "E-004",
|
|
||||||
description: "Distribution Board with MCB",
|
|
||||||
unit: "Nos",
|
|
||||||
quantity: 25,
|
|
||||||
rate: 6200,
|
|
||||||
site: "Skyline Heights",
|
|
||||||
vendor: "Legrand",
|
|
||||||
status: "In Progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 33,
|
|
||||||
category: "Plastering Works",
|
|
||||||
itemCode: "P-003",
|
|
||||||
description: "Ceiling POP Finish",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 950,
|
|
||||||
rate: 210,
|
|
||||||
site: "Green City - Tower A",
|
|
||||||
vendor: "L&T Finishes",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 34,
|
|
||||||
category: "Painting Works",
|
|
||||||
itemCode: "PA-003",
|
|
||||||
description: "Metal Primer + Enamel Paint",
|
|
||||||
unit: "Ltr",
|
|
||||||
quantity: 350,
|
|
||||||
rate: 160,
|
|
||||||
site: "Sunshine Plaza - Basement",
|
|
||||||
vendor: "Nerolac Paints",
|
|
||||||
status: "In Progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 35,
|
|
||||||
category: "HVAC Works",
|
|
||||||
itemCode: "H-003",
|
|
||||||
description: "Duct Insulation 50mm thick",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 120,
|
|
||||||
rate: 540,
|
|
||||||
site: "Eco Towers",
|
|
||||||
vendor: "Blue Star",
|
|
||||||
status: "Pending",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 36,
|
|
||||||
category: "Plumbing Works",
|
|
||||||
itemCode: "PL-004",
|
|
||||||
description: "Overhead Water Tank Installation",
|
|
||||||
unit: "Nos",
|
|
||||||
quantity: 3,
|
|
||||||
rate: 45000,
|
|
||||||
site: "Green City - Tower B",
|
|
||||||
vendor: "Sintex",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 37,
|
|
||||||
category: "Electrical Works",
|
|
||||||
itemCode: "E-005",
|
|
||||||
description: "Earthing Pit with GI Plate",
|
|
||||||
unit: "Nos",
|
|
||||||
quantity: 6,
|
|
||||||
rate: 9500,
|
|
||||||
site: "Skyline Heights",
|
|
||||||
vendor: "KEI",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 38,
|
|
||||||
category: "Concrete Works",
|
|
||||||
itemCode: "C-004",
|
|
||||||
description: "M30 Concrete for beams",
|
|
||||||
unit: "Cum",
|
|
||||||
quantity: 95,
|
|
||||||
rate: 5800,
|
|
||||||
site: "Eco Towers - Basement",
|
|
||||||
vendor: "RMC Readymix",
|
|
||||||
status: "In Progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 39,
|
|
||||||
category: "Masonry Works",
|
|
||||||
itemCode: "M-004",
|
|
||||||
description: "Cement Mortar 1:4 plaster backing",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 700,
|
|
||||||
rate: 180,
|
|
||||||
site: "Skyline Heights - Wing E",
|
|
||||||
vendor: "Ambuja Blocks",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 40,
|
|
||||||
category: "Flooring Works",
|
|
||||||
itemCode: "F-004",
|
|
||||||
description: "Marble Flooring 20mm thick",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 260,
|
|
||||||
rate: 2100,
|
|
||||||
site: "Green City - Tower D",
|
|
||||||
vendor: "Classic Stone",
|
|
||||||
status: "Pending",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 41,
|
|
||||||
category: "Carpentry Works",
|
|
||||||
itemCode: "CP-003",
|
|
||||||
description: "Wardrobe with Laminate Finish",
|
|
||||||
unit: "Nos",
|
|
||||||
quantity: 40,
|
|
||||||
rate: 18500,
|
|
||||||
site: "Sunshine Plaza",
|
|
||||||
vendor: "Durian",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 42,
|
|
||||||
category: "Glazing Works",
|
|
||||||
itemCode: "G-003",
|
|
||||||
description: "Spider Glass Façade System",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 150,
|
|
||||||
rate: 3550,
|
|
||||||
site: "Eco Towers",
|
|
||||||
vendor: "Saint Gobain",
|
|
||||||
status: "In Progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 43,
|
|
||||||
category: "Waterproofing Works",
|
|
||||||
itemCode: "W-003",
|
|
||||||
description: "Bituminous Membrane Layer 3mm",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 400,
|
|
||||||
rate: 620,
|
|
||||||
site: "Skyline Heights - Terrace",
|
|
||||||
vendor: "Dr. Fixit",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 44,
|
|
||||||
category: "External Development",
|
|
||||||
itemCode: "ED-003",
|
|
||||||
description: "RCC Drain Construction",
|
|
||||||
unit: "Rm",
|
|
||||||
quantity: 180,
|
|
||||||
rate: 950,
|
|
||||||
site: "Green City - Service Road",
|
|
||||||
vendor: "L&T Infra",
|
|
||||||
status: "In Progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 45,
|
|
||||||
category: "Electrical Works",
|
|
||||||
itemCode: "E-006",
|
|
||||||
description: "Street Light Pole 9m",
|
|
||||||
unit: "Nos",
|
|
||||||
quantity: 25,
|
|
||||||
rate: 18500,
|
|
||||||
site: "Sunshine Plaza - Entry Road",
|
|
||||||
vendor: "Polycab",
|
|
||||||
status: "Pending",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 46,
|
|
||||||
category: "Painting Works",
|
|
||||||
itemCode: "PA-004",
|
|
||||||
description: "Texture Finish Coating",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 320,
|
|
||||||
rate: 480,
|
|
||||||
site: "Green City - Clubhouse",
|
|
||||||
vendor: "Asian Paints",
|
|
||||||
status: "In Progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 47,
|
|
||||||
category: "Flooring Works",
|
|
||||||
itemCode: "F-005",
|
|
||||||
description: "Epoxy Flooring 3mm thick",
|
|
||||||
unit: "Sqm",
|
|
||||||
quantity: 150,
|
|
||||||
rate: 850,
|
|
||||||
site: "Skyline Heights - Basement",
|
|
||||||
vendor: "Pidilite",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 48,
|
|
||||||
category: "Plumbing Works",
|
|
||||||
itemCode: "PL-005",
|
|
||||||
description: "Bathroom CP fittings set",
|
|
||||||
unit: "Set",
|
|
||||||
quantity: 40,
|
|
||||||
rate: 5400,
|
|
||||||
site: "Eco Towers",
|
|
||||||
vendor: "Hindware",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 49,
|
|
||||||
category: "Concrete Works",
|
|
||||||
itemCode: "C-005",
|
|
||||||
description: "Ready Mix M40 Concrete",
|
|
||||||
unit: "Cum",
|
|
||||||
quantity: 60,
|
|
||||||
rate: 6100,
|
|
||||||
site: "Green City - Tower E",
|
|
||||||
vendor: "ACC",
|
|
||||||
status: "Pending",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
category: "External Development",
|
|
||||||
itemCode: "ED-004",
|
|
||||||
description: "Compound Wall RCC Precast",
|
|
||||||
unit: "Rm",
|
|
||||||
quantity: 300,
|
|
||||||
rate: 1750,
|
|
||||||
site: "Sunshine Plaza - Boundary",
|
|
||||||
vendor: "UltraTech",
|
|
||||||
status: "Completed",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* COLUMN DEFINITIONS
|
|
||||||
*/
|
|
||||||
const boqColumns = [
|
|
||||||
{
|
|
||||||
key: "itemCode",
|
|
||||||
title: "Item Code",
|
|
||||||
sortable: true,
|
|
||||||
pinned: "left",
|
|
||||||
className: "text-start",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "description",
|
|
||||||
title: "Description",
|
|
||||||
sortable: true,
|
|
||||||
width: 300,
|
|
||||||
className: "text-start",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "category",
|
|
||||||
title: "Category",
|
|
||||||
sortable: true,
|
|
||||||
className: "text-start",
|
|
||||||
},
|
|
||||||
{ key: "unit", title: "Unit", width: 80, className: "text-ceter" },
|
|
||||||
{
|
|
||||||
key: "quantity",
|
|
||||||
title: "Qty",
|
|
||||||
sortable: true,
|
|
||||||
width: 100,
|
|
||||||
className: "text-cnter px-2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "rate",
|
|
||||||
title: "Rate (₹)",
|
|
||||||
sortable: true,
|
|
||||||
width: 120,
|
|
||||||
className: "text-end",
|
|
||||||
render: (r) => <span>₹{r.rate.toLocaleString()}</span>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "amount",
|
|
||||||
title: "Amount (₹)",
|
|
||||||
sortable: true,
|
|
||||||
width: 130,
|
|
||||||
className: "text-end",
|
|
||||||
render: (r) => (
|
|
||||||
<span className="fw-semibold text-end px-2 py-12">
|
|
||||||
₹{(r.quantity * r.rate).toLocaleString()}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
aggregate: (vals) =>
|
|
||||||
"₹" + vals.reduce((sum, val) => sum + val, 0).toLocaleString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "vendor",
|
|
||||||
title: "Vendor",
|
|
||||||
sortable: true,
|
|
||||||
width: 180,
|
|
||||||
className: "text-start",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "site",
|
|
||||||
title: "Site Location",
|
|
||||||
sortable: true,
|
|
||||||
width: 200,
|
|
||||||
className: "text-start",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "status",
|
|
||||||
title: "Status",
|
|
||||||
sortable: true,
|
|
||||||
width: 120,
|
|
||||||
className: "text-center",
|
|
||||||
render: (r) => (
|
|
||||||
<span
|
|
||||||
className={`badge bg-label-${
|
|
||||||
r.status === "Completed"
|
|
||||||
? "success"
|
|
||||||
: r.status === "Pending"
|
|
||||||
? "warning"
|
|
||||||
: "info"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{r.status}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DEMO COMPONENT
|
|
||||||
*/
|
|
||||||
export default function DemoBOQGrid() {
|
|
||||||
useEffect(() => {
|
|
||||||
initPopover();
|
|
||||||
}, []);
|
|
||||||
const wrapperRef = useRef();
|
|
||||||
useEffect(() => {
|
|
||||||
if (!wrapperRef.current) return;
|
|
||||||
const ps = new PerfectScrollbar(wrapperRef.current, {
|
|
||||||
wheelPropagation: false,
|
|
||||||
suppressScrollX: false,
|
|
||||||
});
|
|
||||||
return () => ps.destroy();
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<div className="container-fluid py-3">
|
|
||||||
<div className="card p-3">
|
|
||||||
|
|
||||||
|
|
||||||
<PmsGrid
|
|
||||||
data={boqData.map((r) => ({
|
|
||||||
...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,
|
|
||||||
pageSizeSelector: true,
|
|
||||||
groupByKey: "status",
|
|
||||||
aggregation: true,
|
|
||||||
expand: true,
|
|
||||||
maxHeight: "70vh",
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: "Edit",
|
|
||||||
icon: "bx-edit ",
|
|
||||||
onClick: (row) => console.log("Edit", row),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Delete",
|
|
||||||
icon: "bx-trash text-danger",
|
|
||||||
onClick: (row) => console.log("Delete", row),
|
|
||||||
},
|
|
||||||
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
renderExpanded={(row) => (
|
|
||||||
<div className="p-3 bg-light border rounded">
|
|
||||||
<h6 className="fw-semibold mb-2">Item Details</h6>
|
|
||||||
<div className="row small">
|
|
||||||
<div className="col-md-4 mb-2">
|
|
||||||
<strong>Item Code:</strong> {row.itemCode}
|
|
||||||
</div>
|
|
||||||
<div className="col-md-4 mb-2">
|
|
||||||
<strong>Category:</strong> {row.category}
|
|
||||||
</div>
|
|
||||||
<div className="col-md-4 mb-2">
|
|
||||||
<strong>Unit:</strong> {row.unit}
|
|
||||||
</div>
|
|
||||||
<div className="col-md-4 mb-2">
|
|
||||||
<strong>Quantity:</strong> {row.quantity}
|
|
||||||
</div>
|
|
||||||
<div className="col-md-4 mb-2">
|
|
||||||
<strong>Rate:</strong> ₹{row.rate.toLocaleString()}
|
|
||||||
</div>
|
|
||||||
<div className="col-md-4 mb-2">
|
|
||||||
<strong>Total Amount:</strong>{" "}
|
|
||||||
<span className="text-success fw-semibold">
|
|
||||||
₹{(row.quantity * row.rate).toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-6 mb-2">
|
|
||||||
<strong>Vendor:</strong> {row.vendor}
|
|
||||||
</div>
|
|
||||||
<div className="col-md-6 mb-2">
|
|
||||||
<strong>Site Location:</strong> {row.site}
|
|
||||||
</div>
|
|
||||||
<div className="col-md-12">
|
|
||||||
<strong>Status:</strong>{" "}
|
|
||||||
<span
|
|
||||||
className={`badge bg-${
|
|
||||||
row.status === "Completed"
|
|
||||||
? "success"
|
|
||||||
: row.status === "Pending"
|
|
||||||
? "warning"
|
|
||||||
: "info"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{row.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
// GridPinnedCalculator.js
|
|
||||||
export function computeLeftOffsets(colState) {
|
|
||||||
const ordered = colState
|
|
||||||
.filter((c) => c.visible)
|
|
||||||
.sort((a, b) => a.order - b.order);
|
|
||||||
|
|
||||||
let offset = 0;
|
|
||||||
const map = {};
|
|
||||||
|
|
||||||
for (const col of ordered) {
|
|
||||||
if (col.pinned === "left") {
|
|
||||||
map[col.key] = offset;
|
|
||||||
offset += col.width || 120;
|
|
||||||
} else {
|
|
||||||
map[col.key] = null; // not pinned-left
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function computeRightOffsets(colState) {
|
|
||||||
const ordered = colState
|
|
||||||
.filter((c) => c.visible)
|
|
||||||
.sort((a, b) => b.order - a.order);
|
|
||||||
|
|
||||||
let offset = 0;
|
|
||||||
const map = {};
|
|
||||||
|
|
||||||
for (const col of ordered) {
|
|
||||||
if (col.pinned === "right") {
|
|
||||||
map[col.key] = offset;
|
|
||||||
offset += col.width || 120;
|
|
||||||
} else {
|
|
||||||
map[col.key] = null; // not pinned-right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
|
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,651 +0,0 @@
|
|||||||
import React, { useRef } from "react";
|
|
||||||
import { useGridCore } from "./useGridCore";
|
|
||||||
import { exportToCSV } from "./utils";
|
|
||||||
import "./pms-grid.css";
|
|
||||||
import PmsHeaderOption from "./PmsHeaderOption";
|
|
||||||
|
|
||||||
/*
|
|
||||||
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",
|
|
||||||
isDropdown = false,
|
|
||||||
features = {},
|
|
||||||
renderExpanded,
|
|
||||||
}) {
|
|
||||||
const grid = useGridCore({
|
|
||||||
data,
|
|
||||||
serverMode,
|
|
||||||
fetcher,
|
|
||||||
rowKey,
|
|
||||||
initialPageSize: features.pageSize || 25,
|
|
||||||
columns,
|
|
||||||
});
|
|
||||||
const wrapperRef = useRef();
|
|
||||||
|
|
||||||
const {
|
|
||||||
rows,
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
pageSize,
|
|
||||||
setPage,
|
|
||||||
setPageSize,
|
|
||||||
setGroupBy,
|
|
||||||
groupBy,
|
|
||||||
search,
|
|
||||||
setSearch,
|
|
||||||
selected,
|
|
||||||
toggleSelect,
|
|
||||||
selectAllOnPage,
|
|
||||||
deselectAllOnPage,
|
|
||||||
changeSort,
|
|
||||||
sortBy,
|
|
||||||
visibleColumns,
|
|
||||||
colState,
|
|
||||||
updateColumn,
|
|
||||||
loading,
|
|
||||||
totalRows,
|
|
||||||
expanded,
|
|
||||||
toggleExpand,
|
|
||||||
setColState,
|
|
||||||
onAdvanceFilters,
|
|
||||||
setAdanceFilter,
|
|
||||||
} = grid;
|
|
||||||
|
|
||||||
// --- Pin / Unpin helpers ---
|
|
||||||
const pinColumn = (key, side) => {
|
|
||||||
const col = colState.find((c) => c.key === key);
|
|
||||||
if (!col) return;
|
|
||||||
// if already pinned to that side → unpin
|
|
||||||
const newPinned = col.pinned === side || side === "none" ? null : side;
|
|
||||||
updateColumn(key, { pinned: newPinned });
|
|
||||||
};
|
|
||||||
|
|
||||||
const unpinColumn = (key) => {
|
|
||||||
updateColumn(key, { pinned: null });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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 (
|
|
||||||
<div className="pms-grid ">
|
|
||||||
<div className="row mb-2 px-4">
|
|
||||||
<div className="col-8">
|
|
||||||
<div className="d-flex flex-row gap-2 gap-2 ">
|
|
||||||
<div>
|
|
||||||
{features.search && (
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
placeholder="Search..."
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{features.export && (
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-outline-secondary"
|
|
||||||
onClick={() =>
|
|
||||||
exportToCSV(
|
|
||||||
currentRows,
|
|
||||||
colState.filter((c) => c.visible)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Export CSV
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{features.grouping && (
|
|
||||||
<div className="dropdown">
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-outline-secondary"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
>
|
|
||||||
Group By
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="dropdown-menu px-1">
|
|
||||||
{visibleColumns
|
|
||||||
.filter((c) => c.groupable)
|
|
||||||
.map((c) => (
|
|
||||||
<li
|
|
||||||
key={c.key}
|
|
||||||
className="dropdown-item rounded py-1 cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
setGroupBy(c.key);
|
|
||||||
// groupByKey = c.key;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{c.title}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{grid.groupBy && (
|
|
||||||
<li
|
|
||||||
className="dropdown-item text-danger py-1 cursor-pointer rounded"
|
|
||||||
onClick={() => setGroupBy(null)}
|
|
||||||
>
|
|
||||||
Clear Grouping
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-4 ">
|
|
||||||
<div className="d-flex justify-content-end gap-2">
|
|
||||||
{features.columnVisibility && (
|
|
||||||
<ColumnVisibilityPanel
|
|
||||||
columns={colState}
|
|
||||||
onToggle={(k) =>
|
|
||||||
updateColumn(k, {
|
|
||||||
visible: !colState.find((c) => c.key === k).visible,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{features.pageSizeSelector && (
|
|
||||||
<select
|
|
||||||
className="form-select form-select-sm"
|
|
||||||
style={{ width: "80px" }}
|
|
||||||
value={pageSize}
|
|
||||||
onChange={(e) => setPageSize(Number(e.target.value))}
|
|
||||||
>
|
|
||||||
{[10, 25, 50, 100].map((n) => (
|
|
||||||
<option key={n} value={n}>
|
|
||||||
{n}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
ref={wrapperRef}
|
|
||||||
className="grid-wrapper text-nowrap border rounded"
|
|
||||||
style={{ maxHeight: features.maxHeight || "80vh" }}
|
|
||||||
>
|
|
||||||
<table
|
|
||||||
className="table table-sm rounded mb-0 "
|
|
||||||
style={{ width: "max-content", minWidth: "100%" }}
|
|
||||||
>
|
|
||||||
<thead
|
|
||||||
className="bg-light-secondary border p-2 bg-light rounded"
|
|
||||||
style={{ position: "sticky", top: 0, zIndex: 10 }}
|
|
||||||
>
|
|
||||||
<tr className="p-2">
|
|
||||||
{features.IsNumbering && <th>#</th>}
|
|
||||||
{features.expand && (
|
|
||||||
<th className="text-center ticky-action-column">
|
|
||||||
<i className="bx bx-collapse-vertical"></i>
|
|
||||||
</th>
|
|
||||||
)}
|
|
||||||
{features.selection && (
|
|
||||||
<th
|
|
||||||
style={{ width: 32, position: "sticky", top: 0, zIndex: 10 }}
|
|
||||||
className="text-center ticky-action-column"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input mx-3"
|
|
||||||
checked={
|
|
||||||
currentRows.length > 0 &&
|
|
||||||
currentRows.every((r) => selected.has(r[rowKey]))
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
|
||||||
e.target.checked
|
|
||||||
? selectAllOnPage(currentRows)
|
|
||||||
: deselectAllOnPage(currentRows)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{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 (
|
|
||||||
<th
|
|
||||||
key={col.key}
|
|
||||||
draggable={features.reorder}
|
|
||||||
onDragStart={(e) => onDragStart(e, col.key)}
|
|
||||||
onDragOver={(e) => e.preventDefault()}
|
|
||||||
onDrop={(e) => onDrop(e, col.key)}
|
|
||||||
className={`pms-col-header vs-th ${
|
|
||||||
col.pinned ? `pinned pinned-${col.pinned}` : ""
|
|
||||||
}`}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`d-flex align-items-center justify-content-between px-1 ${
|
|
||||||
col.pinned ? "z-6" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
onClick={() => col.sortable && changeSort(col.key)}
|
|
||||||
style={{ cursor: col.sortable ? "pointer" : "default" }}
|
|
||||||
>
|
|
||||||
<strong>{col.title}</strong>
|
|
||||||
{sortBy.key === col.key && (
|
|
||||||
<i
|
|
||||||
className={`bx bx-sm ${
|
|
||||||
sortBy.dir === "asc"
|
|
||||||
? "bxs-chevron-up"
|
|
||||||
: "bxs-chevron-down"
|
|
||||||
}`}
|
|
||||||
></i>
|
|
||||||
)}
|
|
||||||
{col.pinned === col.key && <i className="bx bx-x"></i>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="d-flex align-items-center gap-1">
|
|
||||||
{features.pinning && (
|
|
||||||
<PmsHeaderOption
|
|
||||||
column={col.key}
|
|
||||||
pinned={col.pinned}
|
|
||||||
onPinLeft={() => pinColumn(col.key, "left")}
|
|
||||||
onPinRight={() => pinColumn(col.key, "right")}
|
|
||||||
onUnpin={() => unpinColumn(col.key)}
|
|
||||||
onAdvancedFilter={setAdanceFilter}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{features.resizing && (
|
|
||||||
<i
|
|
||||||
className="resize-handle bx bx-move-horizontal"
|
|
||||||
onMouseDown={(e) => onResizeMouseDown(e, col.key)}
|
|
||||||
></i>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{features.actions && (
|
|
||||||
<th
|
|
||||||
className="text-center sticky-action-column vs-th bg-white fw-semibold th-lastChild"
|
|
||||||
style={{ position: "sticky", right: 0, zIndex: 10 }}
|
|
||||||
>
|
|
||||||
Actions
|
|
||||||
</th>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{loading && (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={
|
|
||||||
visibleColumns.length +
|
|
||||||
(features.selection ? 1 : 0) +
|
|
||||||
(features.actions ? 1 : 0)
|
|
||||||
}
|
|
||||||
className="text-center py-4"
|
|
||||||
>
|
|
||||||
Loading...
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
{!loading && groupBy && groupedRows && groupedRows.length > 0
|
|
||||||
? groupedRows.map((g, indG) => (
|
|
||||||
<React.Fragment key={g.key}>
|
|
||||||
<tr className="bg-light-primary">
|
|
||||||
<td
|
|
||||||
colSpan={
|
|
||||||
visibleColumns.length +
|
|
||||||
(features.selection ? 1 : 0) +
|
|
||||||
(features.actions ? 1 : 0)
|
|
||||||
}
|
|
||||||
className="text-start pinned pinned-left tr-group"
|
|
||||||
>
|
|
||||||
<strong>{g.key}</strong>
|
|
||||||
{features.aggregation &&
|
|
||||||
Object.keys(g.aggregates).length > 0 && (
|
|
||||||
<small className="ms-3 text-muted ">
|
|
||||||
{Object.entries(g.aggregates)
|
|
||||||
.map(([k, v]) => `${k}: ${v}`)
|
|
||||||
.join(" | ")}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{g.items.map((row, indG) => renderRow(row, indG))}
|
|
||||||
</React.Fragment>
|
|
||||||
))
|
|
||||||
: currentRows.map((row, ind) => renderRow(row, ind))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{features.pagination && (
|
|
||||||
<div className="d-flex justify-content-between align-items-center mt-2">
|
|
||||||
<div>
|
|
||||||
<small>{totalRows} rows</small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-outline-secondary me-1"
|
|
||||||
disabled={page <= 1}
|
|
||||||
onClick={() => setPage(page - 1)}
|
|
||||||
>
|
|
||||||
Prev
|
|
||||||
</button>
|
|
||||||
<span className="mx-2">
|
|
||||||
{page}/{totalPages}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-outline-secondary"
|
|
||||||
disabled={page >= totalPages}
|
|
||||||
onClick={() => setPage(page + 1)}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// render a single row (function hoisted so it can reference visibleColumns)
|
|
||||||
function renderRow(row, ind) {
|
|
||||||
const isSelected = selected.has(row[rowKey]);
|
|
||||||
const isExpanded = expanded.has(row[rowKey]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment key={row[rowKey]}>
|
|
||||||
<tr>
|
|
||||||
{features.IsNumbering && (
|
|
||||||
<td className="text-center align-middle p-2">
|
|
||||||
<small className="text-secondry">{ind + 1}</small>
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{/* Expand toggle next to selection */}
|
|
||||||
{features.expand && (
|
|
||||||
<td className="text-center align-middle ">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-link p-0 border-none text-secondary"
|
|
||||||
onClick={() => toggleExpand(row[rowKey])}
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
className={`bx ${
|
|
||||||
isExpanded ? "bxs-chevron-up" : "bxs-chevron-down"
|
|
||||||
} bx-sm`}
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{/* Selection checkbox (always left) */}
|
|
||||||
{features.selection && (
|
|
||||||
<td className="text-center align-middle p-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
checked={isSelected}
|
|
||||||
onChange={() => toggleSelect(row[rowKey])}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Data columns */}
|
|
||||||
{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 (
|
|
||||||
<td
|
|
||||||
key={col.key}
|
|
||||||
style={style}
|
|
||||||
className={`${col.className ?? ""} ${
|
|
||||||
col.pinned
|
|
||||||
? "pinned-left px-3 bg-white pms-grid td pinned"
|
|
||||||
: "px-3"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{col.render ? col.render(row) : row[col.key] ?? ""}
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Actions column (always right) */}
|
|
||||||
{features.actions && (
|
|
||||||
<td
|
|
||||||
className="text-center sticky-action-column bg-white td-lastChild"
|
|
||||||
style={{
|
|
||||||
position: "sticky",
|
|
||||||
right: 0,
|
|
||||||
zIndex: 2,
|
|
||||||
width: "1%",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isDropdown ? (
|
|
||||||
<div className="dropdown z-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
|
||||||
data-bs-toggle="tooltip"
|
|
||||||
data-bs-offset="0,8"
|
|
||||||
data-bs-placement="top"
|
|
||||||
data-bs-custom-class="tooltip-dark"
|
|
||||||
title="More Action"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
<ul className="dropdown-menu dropdown-menu-end">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
aria-label="click to View details"
|
|
||||||
className="dropdown-item"
|
|
||||||
>
|
|
||||||
<i className="bx bx-detail me-2"></i>
|
|
||||||
<span className="align-left">View details</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="d-inline-flex justify-content-center align-items-center gap-2"
|
|
||||||
style={{ minWidth: "fit-content", padding: "0 4px" }}
|
|
||||||
>
|
|
||||||
{Array.isArray(features.actions)
|
|
||||||
? features.actions.map((act, i) => (
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
type="button"
|
|
||||||
className="btn btn-link p-0 border-0"
|
|
||||||
title={act.label}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
act.onClick && act.onClick(row);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className={`bx ${act.icon}`} />
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
: typeof features.actions === "function"
|
|
||||||
? features.actions(row, toggleExpand)
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
{/* 5. Expanded row content (full width) */}
|
|
||||||
{isExpanded && renderExpanded && (
|
|
||||||
<tr className="table-active">
|
|
||||||
<td
|
|
||||||
colSpan={
|
|
||||||
visibleColumns.length +
|
|
||||||
(features.selection ? 1 : 0) +
|
|
||||||
(features.expand ? 1 : 0) +
|
|
||||||
(features.actions ? 1 : 0)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{renderExpanded(row)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (
|
|
||||||
<div className="dropdown">
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-outline-secondary"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
>
|
|
||||||
<i className="bx bx-sm me-2 bx-columns "></i> Columns
|
|
||||||
</button>
|
|
||||||
<div className="dropdown-menu dropdown-menu-end p-2">
|
|
||||||
{columns.map((c) => (
|
|
||||||
<div key={c.key} className="form-check">
|
|
||||||
<input
|
|
||||||
className="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
id={`colvis-${c.key}`}
|
|
||||||
checked={c.visible !== false}
|
|
||||||
onChange={() => onToggle(c.key)}
|
|
||||||
/>
|
|
||||||
<label className="form-check-label" htmlFor={`colvis-${c.key}`}>
|
|
||||||
{c.title}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// For Full screen - class card card-action card-fullscreen
|
|
||||||
@ -1,227 +0,0 @@
|
|||||||
import { useState } 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" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------- FILTER UI COMPONENT ----------
|
|
||||||
function AdvanceFilter({ type = "number", onApply, onClear }) {
|
|
||||||
const [operator, setOperator] = useState("");
|
|
||||||
const [value1, setValue1] = useState("");
|
|
||||||
const [value2, setValue2] = useState("");
|
|
||||||
|
|
||||||
const ops = OPERATORS[type];
|
|
||||||
|
|
||||||
const apply = () => {
|
|
||||||
if (!operator) return;
|
|
||||||
|
|
||||||
if (operator === "between") {
|
|
||||||
onApply({
|
|
||||||
operator,
|
|
||||||
from: value1,
|
|
||||||
to: value2,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
onApply({
|
|
||||||
operator,
|
|
||||||
value: value1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="dropdown p-3"
|
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Values */}
|
|
||||||
{operator && (
|
|
||||||
<div>
|
|
||||||
<label className="form-label">Value</label>
|
|
||||||
|
|
||||||
{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)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<input
|
|
||||||
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 (
|
|
||||||
<div className="dropdown" style={{ zIndex: 9999 }}>
|
|
||||||
<button
|
|
||||||
className="btn btn-icon btn-text-secondary rounded-pill p-0"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
data-bs-auto-close="outside"
|
|
||||||
>
|
|
||||||
<i className="bx bx-dots-vertical-rounded bx-sm text-muted"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<ul className="dropdown-menu dropdown-menu-end shadow border rounded-3 py-2">
|
|
||||||
{/* ADVANCED FILTER */}
|
|
||||||
<li className="dropdown-submenu dropstart">
|
|
||||||
<button
|
|
||||||
className="dropdown-item d-flex justify-content-between"
|
|
||||||
onClick={(e) => toggleMenu("filter", e)}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<i className="bx bx-filter-alt me-2"></i> Filter
|
|
||||||
</span>
|
|
||||||
<i className="bx bx-chevron-left"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<ul
|
|
||||||
className={`dropdown-menu shadow rounded-3 py-2 p-0 ${
|
|
||||||
openMenu === "filter" ? "show" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<li className="p-0">
|
|
||||||
<AdvanceFilter
|
|
||||||
type="number"
|
|
||||||
onApply={(f) => onAdvancedFilter({...f,column})}
|
|
||||||
onClear={() => onAdvancedFilter(null)}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{/* PIN COLUMN */}
|
|
||||||
<li className="dropdown-submenu dropstart">
|
|
||||||
<button
|
|
||||||
className="dropdown-item d-flex justify-content-between"
|
|
||||||
onClick={(e) => toggleMenu("pin", e)}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<i className="bx bx-pin me-2"></i> Pin Column
|
|
||||||
</span>
|
|
||||||
<i className="bx bx-chevron-left"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<ul
|
|
||||||
className={`dropdown-menu shadow rounded-3 py-2 ${
|
|
||||||
openMenu === "pin" ? "show" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<button className="dropdown-item" onClick={onUnpin}>
|
|
||||||
{!pinned && <i className="bx bx-check me-2"></i>}
|
|
||||||
No Pin
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
className={`dropdown-item ${pinned === "left" ? "active" : ""}`}
|
|
||||||
onClick={onPinLeft}
|
|
||||||
>
|
|
||||||
{pinned === "left" && <i className="bx bx-check me-2"></i>}
|
|
||||||
Pin Left
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
className={`dropdown-item ${
|
|
||||||
pinned === "right" ? "active" : ""
|
|
||||||
}`}
|
|
||||||
onClick={onPinRight}
|
|
||||||
>
|
|
||||||
{pinned === "right" && <i className="bx bx-check me-2"></i>}
|
|
||||||
Pin Right
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PmsHeaderOption;
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
|
|
||||||
export { default as PmsGrid } from "./PmsGrid";
|
|
||||||
export * from "./useGrid";
|
|
||||||
export * from "./useGridCore";
|
|
||||||
export * from "./utils";
|
|
||||||
@ -1,257 +0,0 @@
|
|||||||
/* ──────────────────────────────
|
|
||||||
PMS GRID – MAIN CONTAINER
|
|
||||||
────────────────────────────── */
|
|
||||||
.pms-grid {
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ──────────────────────────────
|
|
||||||
SCROLLABLE WRAPPER
|
|
||||||
────────────────────────────── */
|
|
||||||
.grid-wrapper {
|
|
||||||
max-height: 50vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: auto;
|
|
||||||
position: relative;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 0.2rem;
|
|
||||||
background: #fff;
|
|
||||||
/* Force horizontal scroll even if few columns */
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Always visible scrollbar (cross browser) */
|
|
||||||
.grid-wrapper::-webkit-scrollbar {
|
|
||||||
height: 3px;
|
|
||||||
width: 3px;
|
|
||||||
}
|
|
||||||
.grid-wrapper::-webkit-scrollbar-thumb {
|
|
||||||
background-color: #adb5bd;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.grid-wrapper::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: #868e96;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ──────────────────────────────
|
|
||||||
TABLE BASE STYLE
|
|
||||||
────────────────────────────── */
|
|
||||||
.pms-grid table {
|
|
||||||
width: max-content; /* allows scrolling horizontally */
|
|
||||||
min-width: 100%;
|
|
||||||
border-collapse: separate;
|
|
||||||
border-spacing: 0;
|
|
||||||
table-layout: fixed;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.pms-col-header {
|
|
||||||
position: relative;
|
|
||||||
overflow: visible !important;
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.pms-grid th {
|
|
||||||
padding: 2px 4px !important;
|
|
||||||
vertical-align: middle;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.pms-grid td {
|
|
||||||
padding: 8px 12px !important;
|
|
||||||
vertical-align: middle;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ──────────────────────────────
|
|
||||||
HEADER (STICKY)
|
|
||||||
────────────────────────────── */
|
|
||||||
.pms-grid thead th {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 6;
|
|
||||||
background: #fff;
|
|
||||||
border-bottom: 1px solid #dee2e6;
|
|
||||||
font-weight: 600;
|
|
||||||
box-shadow: inset 0 -1px 0 #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ──────────────────────────────
|
|
||||||
PINNED COLUMNS
|
|
||||||
────────────────────────────── */
|
|
||||||
.pms-grid td.pinned,
|
|
||||||
.pms-grid th.pinned {
|
|
||||||
position: sticky;
|
|
||||||
background: #fff;
|
|
||||||
z-index: 5;
|
|
||||||
border-right: 1px solid #dee2e6 !important;
|
|
||||||
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Always sticky first column (checkbox) */
|
|
||||||
/* .pms-grid th:first-child,
|
|
||||||
.pms-grid td:first-child {
|
|
||||||
position: sticky;
|
|
||||||
left: 0;
|
|
||||||
z-index: 8;
|
|
||||||
background: #fff;
|
|
||||||
width: 40px;
|
|
||||||
min-width: 40px;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 1px 0 1px rgba(0, 0, 0, 0.08);
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* Always sticky last column (Actions) */
|
|
||||||
.pms-grid .th-lastChild,
|
|
||||||
.pms-grid .td-lastChild {
|
|
||||||
position: sticky;
|
|
||||||
right: 0;
|
|
||||||
z-index: 8;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: -1px 0 1px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ──────────────────────────────
|
|
||||||
RESIZE HANDLE
|
|
||||||
────────────────────────────── */
|
|
||||||
.resize-handle {
|
|
||||||
width: 8px;
|
|
||||||
height: 100%;
|
|
||||||
cursor: col-resize;
|
|
||||||
display: inline-block;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
color: var(--bs-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pms-col-header:hover .resize-handle {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-handle::after {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
width: 1px;
|
|
||||||
height: 16px;
|
|
||||||
background: #adb5bd;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ──────────────────────────────
|
|
||||||
ROW HIGHLIGHT
|
|
||||||
────────────────────────────── */
|
|
||||||
.pms-grid tbody tr:hover {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pms-grid tbody tr.table-active {
|
|
||||||
background-color: #e9f5ff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pms-grid tbody td {
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ──────────────────────────────
|
|
||||||
MISC FIXES
|
|
||||||
────────────────────────────── */
|
|
||||||
.vs-th {
|
|
||||||
background: #f8f9fa;
|
|
||||||
}
|
|
||||||
.text-center {
|
|
||||||
text-align: center !important;
|
|
||||||
}
|
|
||||||
.z-6 {
|
|
||||||
z-index: 6 !important;
|
|
||||||
}
|
|
||||||
.z-100{
|
|
||||||
z-index: 100 !important;
|
|
||||||
}
|
|
||||||
/* ──────────────────────────────
|
|
||||||
PINNED HEADER COLUMNS (Fix for misalignment)
|
|
||||||
────────────────────────────── */
|
|
||||||
|
|
||||||
/* Left pinned header cells */
|
|
||||||
.pms-grid th.pinned-left {
|
|
||||||
position: sticky;
|
|
||||||
left: 0;
|
|
||||||
z-index: 12;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 1px 0 1px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Right pinned header cells */
|
|
||||||
.pms-grid th.pinned-right {
|
|
||||||
position: sticky;
|
|
||||||
right: 0;
|
|
||||||
z-index: 12;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: -1px 0 1px rgba(0, 0, 0, 0.08);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Match body pinned cell behavior */
|
|
||||||
.pms-grid td.pinned-left {
|
|
||||||
position: sticky;
|
|
||||||
left: 0;
|
|
||||||
z-index: 8;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 1px 0 1px rgba(0, 0, 0, 0.08);
|
|
||||||
border-right: 1px solid var(--bs-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pms-grid td.pinned-right {
|
|
||||||
position: sticky;
|
|
||||||
right: 0;
|
|
||||||
z-index: 8;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: -2px 0 3px rgba(0, 0, 0, 0.08);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
.dropend:hover > .dropdown-menu {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Hover color consistency with Sneat variables */
|
|
||||||
.dropdown-item:focus,
|
|
||||||
.dropdown-item:hover {
|
|
||||||
background-color: rgba(var(--bs-primary-rgb), 0.08);
|
|
||||||
color: var(--bs-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive fallback: stack submenu below parent on small screens */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.dropend .dropdown-menu {
|
|
||||||
position: relative;
|
|
||||||
left: 0;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,241 +0,0 @@
|
|||||||
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 = 5,
|
|
||||||
columns = [],
|
|
||||||
}) {
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [pageSize, setPageSize] = useState(initialPageSize);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
|
||||||
const [groupBy, setGroupBy] = useState(null);
|
|
||||||
const [serverGroupRows, setServerGroupRows] = useState(null);
|
|
||||||
const [onAdvanceFilter, setAdanceFilter] = useState(null);
|
|
||||||
|
|
||||||
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([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = setTimeout(() => {
|
|
||||||
setDebouncedSearch(search);
|
|
||||||
setPage(1); // Important — when search changes, go back to page 1
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return () => clearTimeout(handler);
|
|
||||||
}, [search]);
|
|
||||||
|
|
||||||
// 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 () => {
|
|
||||||
// sorting column wise
|
|
||||||
const sortFilters = sortBy.key
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
column: sortBy.key,
|
|
||||||
sortDescending: sortBy.dir === "desc",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// -----------------------------
|
|
||||||
// 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({
|
|
||||||
sortFilters,
|
|
||||||
groupByColumn: groupBy || null,
|
|
||||||
advanceFilters,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!serverMode || typeof fetcher !== "function") return;
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const resp = await fetcher({
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
sortBy,
|
|
||||||
search: debouncedSearch,
|
|
||||||
filter: filterPayload,
|
|
||||||
});
|
|
||||||
// expected: { rows: [], total }
|
|
||||||
setServerRows(resp.rows || []);
|
|
||||||
setTotalRows(resp.total || (resp.rows ? resp.rows.length : 0));
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
serverMode,
|
|
||||||
fetcher,
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
sortBy,
|
|
||||||
debouncedSearch,
|
|
||||||
groupBy,
|
|
||||||
onAdvanceFilter,
|
|
||||||
]);
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
// 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);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
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,
|
|
||||||
groupBy,
|
|
||||||
setGroupBy,
|
|
||||||
onAdvanceFilter,
|
|
||||||
setAdanceFilter,
|
|
||||||
// data
|
|
||||||
rows,
|
|
||||||
|
|
||||||
// mode helpers
|
|
||||||
serverMode,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user