Merge branch 'Organization_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Image_Gallery_filter

This commit is contained in:
Kartik Sharma 2025-09-22 16:34:14 +05:30
commit 98acafb9ff
81 changed files with 3413 additions and 1044 deletions

119
package-lock.json generated
View File

@ -809,9 +809,9 @@
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"license": "MIT",
"peer": true,
"dependencies": {
@ -1552,13 +1552,13 @@
"peer": true
},
"node_modules/@types/node": {
"version": "22.13.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz",
"integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==",
"version": "24.5.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz",
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.20.0"
"undici-types": "~7.12.0"
}
},
"node_modules/@types/prop-types": {
@ -1835,9 +1835,10 @@
}
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
@ -1845,6 +1846,19 @@
"node": ">=0.4.0"
}
},
"node_modules/acorn-import-phases": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.13.0"
},
"peerDependencies": {
"acorn": "^8.14.0"
}
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
@ -2625,9 +2639,9 @@
"integrity": "sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw=="
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"license": "MIT",
"peer": true,
"dependencies": {
@ -2741,9 +2755,9 @@
}
},
"node_modules/es-module-lexer": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
"integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"license": "MIT",
"peer": true
},
@ -3138,9 +3152,9 @@
"dev": true
},
"node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"funding": [
{
"type": "github",
@ -5163,9 +5177,9 @@
}
},
"node_modules/schema-utils": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz",
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"license": "MIT",
"peer": true,
"dependencies": {
@ -5567,24 +5581,28 @@
}
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/terser": {
"version": "5.39.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
"acorn": "^8.15.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
@ -5777,9 +5795,9 @@
}
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
"integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==",
"license": "MIT",
"peer": true
},
@ -5907,9 +5925,9 @@
}
},
"node_modules/watchpack": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
"integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
"license": "MIT",
"peer": true,
"dependencies": {
@ -5927,21 +5945,23 @@
"license": "BSD-2-Clause"
},
"node_modules/webpack": {
"version": "5.98.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz",
"integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==",
"version": "5.101.3",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz",
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6",
"@types/estree": "^1.0.8",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1",
"acorn": "^8.14.0",
"acorn": "^8.15.0",
"acorn-import-phases": "^1.0.3",
"browserslist": "^4.24.0",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.17.1",
"enhanced-resolve": "^5.17.3",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
@ -5951,11 +5971,11 @@
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
"schema-utils": "^4.3.0",
"schema-utils": "^4.3.2",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1",
"webpack-sources": "^3.2.3"
"webpack-sources": "^3.3.3"
},
"bin": {
"webpack": "bin/webpack.js"
@ -5974,15 +5994,22 @@
}
},
"node_modules/webpack-sources": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/webpack/node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT",
"peer": true
},
"node_modules/webpack/node_modules/eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",

View File

@ -1,8 +1,12 @@
:root,
[data-bs-theme="light"] {
--bs-nav-link-font-size: 0.7375rem;
--bg-border-color :#f8f6f6
}
.card-header {
padding: 0.5rem var(--bs-card-cap-padding-x);
}
.table_header_border {
border-bottom:2px solid var(--bs-table-border-color) ;
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="120" height="120" fill="#EFF1F3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.2503 38.4816C33.2603 37.0472 34.4199 35.8864 35.8543 35.875H83.1463C84.5848 35.875 85.7503 37.0431 85.7503 38.4816V80.5184C85.7403 81.9528 84.5807 83.1136 83.1463 83.125H35.8543C34.4158 83.1236 33.2503 81.957 33.2503 80.5184V38.4816ZM80.5006 41.1251H38.5006V77.8751L62.8921 53.4783C63.9172 52.4536 65.5788 52.4536 66.6039 53.4783L80.5006 67.4013V41.1251ZM43.75 51.6249C43.75 54.5244 46.1005 56.8749 49 56.8749C51.8995 56.8749 54.25 54.5244 54.25 51.6249C54.25 48.7254 51.8995 46.3749 49 46.3749C46.1005 46.3749 43.75 48.7254 43.75 51.6249Z" fill="#687787"/>
</svg>

After

Width:  |  Height:  |  Size: 888 B

View File

@ -4,6 +4,7 @@ import { ToastContainer } from "react-toastify";
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { queryClient } from "./layouts/AuthLayout";
import ModalProvider from "./ModalProvider";
@ -11,6 +12,7 @@ const App = () => {
return (
<div className="app">
<QueryClientProvider client={queryClient}>
<ModalProvider/>
<DireProvider>
<AppRoutes />
</DireProvider>

View File

@ -1,112 +0,0 @@
import React, {
createContext,
useContext,
useState,
useEffect,
useRef,
} from "react";
const ModalContext = createContext();
export const useModal = () => useContext(ModalContext);
// ModalProvider to manage modal state and expose functionality to the rest of the app
export const ModalProvider = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const [modalContent, setModalContent] = useState(null);
const [onSubmit, setOnSubmit] = useState(null);
const [modalSize, setModalSize] = useState("lg");
// Ref to track the modal content element
const modalRef = useRef(null);
const openModal = (content, onSubmitCallback, size = "lg") => {
setModalContent(content); // Set modal content dynamically
setOnSubmit(() => onSubmitCallback); // Set the submit handler dynamically
setIsOpen(true); // Open the modal
setModalSize(size); // Set the modal size
};
// Function to close the modal
const closeModal = () => {
setIsOpen(false); // Close the modal
setModalContent(null); // Clear modal content
setOnSubmit(null); // Clear the submit callback
setModalSize("lg"); // Reset modal size
};
useEffect(() => {
const handleEscape = (event) => {
if (event.key === "Escape") {
closeModal();
}
};
document.addEventListener("keydown", handleEscape);
return () => {
document.removeEventListener("keydown", handleEscape);
};
}, []);
useEffect(() => {
const handleClickOutside = (event) => {
if (modalRef.current && !modalRef.current.contains(event.target)) {
closeModal();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<ModalContext.Provider
value={{
isOpen,
openModal,
closeModal,
modalContent,
modalSize,
onSubmit,
}}
>
{children}
{isOpen && (
<div style={overlayStyles}>
<div
ref={modalRef}
style={{
...modalStyles,
maxWidth: modalSize === "sm" ? "400px" : "800px",
}}
>
<div>{modalContent}</div>
</div>
</div>
)}
</ModalContext.Provider>
);
};
const overlayStyles = {
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.5)",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: 1050,
};
const modalStyles = {
backgroundColor: "white",
padding: "20px",
borderRadius: "5px",
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
width: "90%",
maxWidth: "800px",
};

19
src/ModalProvider.jsx Normal file
View File

@ -0,0 +1,19 @@
import React, { useEffect } from "react";
import { useOrganizationModal } from "./hooks/useOrganization";
import OrganizationModal from "./components/Organization/OrganizationModal";
import { useAuthModal } from "./hooks/useAuth";
import SwitchTenant from "./pages/authentication/SwitchTenant";
const ModalProvider = () => {
const { isOpen, onClose } = useOrganizationModal();
const { isOpen: isAuthOpen } = useAuthModal();
return (
<>
{isOpen && <OrganizationModal />}
{isAuthOpen && <SwitchTenant />}
</>
);
};
export default ModalProvider;

View File

@ -12,22 +12,19 @@ import { useQueryClient } from "@tanstack/react-query";
import eventBus from "../../services/eventBus";
import { useSelectedProject } from "../../slices/apiDataManager";
const Attendance = ({ getRole, handleModalData, searchTerm }) => {
const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizationId, includeInactive, date }) => {
const queryClient = useQueryClient();
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const [todayDate, setTodayDate] = useState(new Date());
const [ShowPending, setShowPending] = useState(false);
// const selectedProject = useSelector(
// (store) => store.localVariables.projectId
// );
const selectedProject = useSelectedProject();
const {
attendance,
loading: attLoading,
recall: attrecall,
isFetching
} = useAttendance(selectedProject);
} = useAttendance(selectedProject, organizationId, includeInactive, date);
const filteredAttendance = ShowPending
? attendance?.filter(
(att) => att?.checkInTime !== null && att?.checkOutTime === null
@ -62,12 +59,11 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
const role = item.jobRoleName?.toLowerCase() || "";
return (
fullName.includes(lowercasedSearchTerm) ||
role.includes(lowercasedSearchTerm) // also search by role
role.includes(lowercasedSearchTerm) // also search by role
);
});
}, [group1, group2, searchTerm]);
const { currentPage, totalPages, currentItems, paginate } = usePagination(
finalFilteredData,
ITEMS_PER_PAGE
@ -116,7 +112,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
<>
<div
className="table-responsive text-nowrap h-100"
style={{ minHeight: "200px" }} // 🔹 Ensures fixed height
style={{ minHeight: "200px" }} // Ensures fixed height
>
<div className="d-flex text-start align-items-center py-2">
<strong>Date : {formatUTCToLocalTime(todayDate)}</strong>
@ -142,6 +138,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
<tr className="border-top-1">
<th colSpan={2}>Name</th>
<th>Role</th>
<th>Organization</th>
<th>
<i className="bx bxs-down-arrow-alt text-success"></i>
Check-In
@ -190,6 +187,8 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
</td>
<td>{item.jobRoleName}</td>
<td>{item.organizationName || "--"}</td>
<td>
{item.checkInTime
? convertShortTime(item.checkInTime)
@ -213,7 +212,11 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
))}
{!attendance && (
<tr>
<td colSpan={6} className="text-center text-secondary" style={{ height: "200px" }}>
<td
colSpan={7}
className="text-center text-secondary"
style={{ height: "200px" }}
>
No employees assigned to the project!
</td>
</tr>
@ -221,6 +224,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
</tbody>
</table>
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
<nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1">

View File

@ -4,7 +4,6 @@ import Avatar from "../common/Avatar";
import { convertShortTime } from "../../utils/dateUtils";
import RenderAttendanceStatus from "./RenderAttendanceStatus";
import { useSelector, useDispatch } from "react-redux";
import { fetchAttendanceData } from "../../slices/apiSlice/attedanceLogsSlice";
import DateRangePicker from "../common/DateRangePicker";
import { clearCacheKey, getCachedData, useSelectedProject } from "../../slices/apiDataManager";
import eventBus from "../../services/eventBus";
@ -34,7 +33,7 @@ const usePagination = (data, itemsPerPage) => {
};
};
const AttendanceLog = ({ handleModalData, searchTerm }) => {
const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
// const selectedProject = useSelector(
// (store) => store.localVariables.projectId
// );
@ -82,7 +81,8 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
} = useAttendancesLogs(
selectedProject,
dateRange.startDate,
dateRange.endDate
dateRange.endDate,
organizationId
);
const filtering = (data) => {
const filteredData = showPending
@ -151,6 +151,33 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
});
}, [processedData, searchTerm]);
// const filteredSearchData = useMemo(() => {
// let tempData = processedData;
// if (searchTerm) {
// const lowercasedSearchTerm = searchTerm.toLowerCase();
// tempData = tempData.filter((item) => {
// const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
// return fullName.includes(lowercasedSearchTerm);
// });
// }
// if (filters?.selectedOrganization) {
// tempData = tempData.filter(
// (item) => item.organization?.name === filters.selectedOrganization
// );
// }
// if (filters?.selectedServices?.length > 0) {
// tempData = tempData.filter((item) =>
// filters.selectedServices.includes(item.service?.name)
// );
// }
// return tempData;
// }, [processedData, searchTerm, filters]);
const {
currentPage,
totalPages,
@ -243,14 +270,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
<label className="form-check-label ms-0">Show Pending</label>
</div>
</div>
<div className="col-md-2 m-0 text-end">
<i
className={`bx bx-refresh cursor-pointer fs-4 ${isFetching ? "spin" : ""
}`}
title="Refresh"
onClick={() => refetch()}
/>
</div>
</div>
<div className="table-responsive text-nowrap" style={{ minHeight: "200px" }}>
{isLoading ? (
@ -265,9 +285,9 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
Name
</th>
<th className="border-top-1">Date</th>
<th>Organization</th>
<th>
<i className="bx bxs-down-arrow-alt text-success"></i>{" "}
Check-In
<i className="bx bxs-down-arrow-alt text-success"></i> Check-In
</th>
<th>
<i className="bx bxs-up-arrow-alt text-danger"></i> Check-Out
@ -294,7 +314,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
key={`header-${currentDate}`}
className="table-row-header"
>
<td colSpan={6} className="text-start">
<td colSpan={8} className="text-start">
<strong>
{moment(currentDate).format("DD-MM-YYYY")}
</strong>
@ -324,6 +344,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
attendance.checkInTime || attendance.checkOutTime
).format("DD-MMM-YYYY")}
</td>
<td>{attendance.organizationName || "--"}</td>
<td>{convertShortTime(attendance.checkInTime)}</td>
<td>
{attendance.checkOutTime
@ -345,7 +366,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
</tbody>
</table>
) : (
<div className="my-4"><span className="text-secondary">No Record Available !</span></div>
<div className="my-12"><span className="text-secondary">No data available for the selected date range. Please Select another date.</span></div>
)}
</div>
{paginatedAttendances?.length == 0 && filteredSearchData?.length > 0 && (

View File

@ -5,7 +5,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
import TimePicker from "../common/TimePicker";
import { usePositionTracker } from "../../hooks/usePositionTracker";
import { useDispatch, useSelector } from "react-redux";
import { markAttendance } from "../../slices/apiSlice/attedanceLogsSlice";
import showToast from "../../services/toastService";
import { checkIfCurrentDate } from "../../utils/dateUtils";
import { useMarkAttendance } from "../../hooks/useAttendance";

View File

@ -55,7 +55,7 @@ const InfraPlanning = () => {
if (isFetched && (!projectInfra || projectInfra.length === 0)) {
return (
<div className="card text-center">
<div className="text-center">
<p className="my-3">No Result Found</p>
</div>
);
@ -63,11 +63,9 @@ const InfraPlanning = () => {
return (
<div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4">
<div className="card">
<div className="card-body" style={{ padding: "0.5rem" }}>
<div className="row">
<InfraTable buildings={projectInfra} projectId={selectedProject} />
</div>
</div>
</div>
</div>

View File

@ -10,13 +10,13 @@ import eventBus from "../../services/eventBus";
import { cacheData, clearCacheKey, useSelectedProject } from "../../slices/apiDataManager";
import { useQueryClient } from "@tanstack/react-query";
const Regularization = ({ handleRequest, searchTerm }) => {
const Regularization = ({ handleRequest, searchTerm,projectId, organizationId, IncludeInActive }) => {
const queryClient = useQueryClient();
// var selectedProject = useSelector((store) => store.localVariables.projectId);
const selectedProject = useSelectedProject();
const [regularizesList, setregularizedList] = useState([]);
const { regularizes, loading, error, refetch } =
useRegularizationRequests(selectedProject);
useRegularizationRequests(selectedProject, organizationId, IncludeInActive);
useEffect(() => {
setregularizedList(regularizes);
@ -59,6 +59,36 @@ const Regularization = ({ handleRequest, searchTerm }) => {
});
}, [regularizesList, searchTerm]);
// const filteredSearchData = useMemo(() => {
// let sortedList = [...regularizesList].sort(sortByName);
// // Search filter
// if (searchTerm) {
// const lowercasedSearchTerm = searchTerm.toLowerCase();
// sortedList = sortedList.filter((item) => {
// const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
// return fullName.includes(lowercasedSearchTerm);
// });
// }
// // Organization filter
// if (filters?.selectedOrganization) {
// sortedList = sortedList.filter(
// (item) => item.organization?.name === filters.selectedOrganization
// );
// }
// // Services filter
// if (filters?.selectedServices?.length > 0) {
// sortedList = sortedList.filter((item) =>
// filters.selectedServices.includes(item.service?.name)
// );
// }
// return sortedList;
// }, [regularizesList, searchTerm, filters]);
const { currentPage, totalPages, currentItems, paginate } =
usePagination(filteredSearchData, 20);
@ -98,6 +128,7 @@ const Regularization = ({ handleRequest, searchTerm }) => {
<tr>
<th colSpan={2}>Name</th>
<th>Date</th>
<th>Organization</th>
<th>
<i className="bx bxs-down-arrow-alt text-success"></i>Check-In
</th>
@ -115,7 +146,7 @@ const Regularization = ({ handleRequest, searchTerm }) => {
<Avatar
firstName={att.firstName}
lastName={att.lastName}
></Avatar>
/>
<div className="d-flex flex-column">
<a href="#" className="text-heading text-truncate">
<span className="fw-normal">
@ -126,6 +157,9 @@ const Regularization = ({ handleRequest, searchTerm }) => {
</div>
</td>
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
<td>{att.organizationName || "--"}</td>
<td>{convertShortTime(att.checkInTime)}</td>
<td>
{att.checkOutTime ? convertShortTime(att.checkOutTime) : "--"}
@ -136,12 +170,12 @@ const Regularization = ({ handleRequest, searchTerm }) => {
handleRequest={handleRequest}
refresh={refetch}
/>
{/* </div> */}
</td>
</tr>
))}
</tbody>
</table>
) : (
<div
className="d-flex justify-content-center align-items-center"

View File

@ -1,9 +1,7 @@
import React, { act, useEffect, useState } from 'react'
import useAttendanceStatus, { ACTIONS } from '../../hooks/useAttendanceStatus';
// import AttendanceRepository from '../../repositories/AttendanceRepository';
import { useDispatch, useSelector } from 'react-redux';
import { usePositionTracker } from '../../hooks/usePositionTracker';
import {markCurrentAttendance} from '../../slices/apiSlice/attendanceAllSlice';
import {cacheData, getCachedData, useSelectedProject} from '../../slices/apiDataManager';
import showToast from '../../services/toastService';
import { useMarkAttendance } from '../../hooks/useAttendance';

View File

@ -1,21 +1,16 @@
import React, { useState, useEffect } from "react";
import LineChart from "../Charts/LineChart";
import React, { useState, useMemo } from "react";
import ApexChart from "../Charts/Circle";
import { useProjects } from "../../hooks/useProjects";
import { useDashboard_AttendanceData } from "../../hooks/useDashboard_Data";
import ApexChart from "../Charts/Circle";
const LOCAL_STORAGE_PROJECT_KEY = "selectedAttendanceProjectId";
import { useSelectedProject } from "../../hooks/useSelectedProject"; // your custom hook
const Attendance = () => {
const { projects } = useProjects();
const today = new Date().toISOString().split("T")[0]; // Format: YYYY-MM-DD
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
const [selectedDate, setSelectedDate] = useState(today);
const storedProjectId = localStorage.getItem(LOCAL_STORAGE_PROJECT_KEY);
const initialProjectId = storedProjectId || "all";
const [selectedProjectId, setSelectedProjectId] = useState(initialProjectId);
const [displayedProjectName, setDisplayedProjectName] =
useState("Select Project");
const [activeTab, setActiveTab] = useState("Summary");
// central project selection hook
const selectedProjectId = useSelectedProject()
const {
dashboard_Attendancedata: AttendanceData,
@ -23,38 +18,24 @@ const Attendance = () => {
error: isError,
} = useDashboard_AttendanceData(selectedDate, selectedProjectId);
useEffect(() => {
if (selectedProjectId === "all") {
setDisplayedProjectName("All Projects");
} else if (projects) {
const foundProject = projects.find((p) => p.id === selectedProjectId);
setDisplayedProjectName(
foundProject ? foundProject.name : "Select Project"
);
} else {
setDisplayedProjectName("Select Project");
}
localStorage.setItem(LOCAL_STORAGE_PROJECT_KEY, selectedProjectId);
// project name derived once
const displayedProjectName = useMemo(() => {
if (selectedProjectId === "all") return "All Projects";
const found = projects?.find((p) => p.id === selectedProjectId);
return found?.name || "Select Project";
}, [selectedProjectId, projects]);
const handleProjectSelect = (projectId) => {
setSelectedProjectId(projectId);
};
const handleDateChange = (e) => {
setSelectedDate(e.target.value);
};
return (
<div className="card h-100">
<div className="card-header mb-1 pb-0 ">
<div className="d-flex flex-wrap justify-content-between align-items-center mb-0 pb-0 ">
{/* Header */}
<div className="card-header mb-1 pb-0">
<div className="d-flex flex-wrap justify-content-between align-items-center">
<div className="card-title mb-0 text-start">
<h5 className="mb-1">Attendance</h5>
<p className="card-subtitle">Daily Attendance Data</p>
</div>
{/* Project Dropdown */}
<div className="btn-group">
<button
className="btn btn-outline-primary btn-sm dropdown-toggle"
@ -68,7 +49,7 @@ const Attendance = () => {
<li>
<button
className="dropdown-item"
onClick={() => handleProjectSelect("all")}
onClick={() => setSelectedProjectId("all")}
>
All Projects
</button>
@ -77,7 +58,7 @@ const Attendance = () => {
<li key={project.id}>
<button
className="dropdown-item"
onClick={() => handleProjectSelect(project.id)}
onClick={() => setSelectedProjectId(project.id)}
>
{project.name}
</button>
@ -88,52 +69,43 @@ const Attendance = () => {
</div>
</div>
<div className="d-flex flex-wrap justify-content-between align-items-center mb-0 mt-0 me-5 ms-5">
{/* Tabs */}
<div>
<ul className="nav nav-tabs " role="tablist">
<li className="nav-item">
<button
type="button"
className={`nav-link ${
activeTab === "Summary" ? "active" : ""
}`}
onClick={() => setActiveTab("Summary")}
data-bs-toggle="tab"
>
Summary
</button>
</li>
<li className="nav-item">
<button
type="button"
className={`nav-link ${
activeTab === "Details" ? "active" : ""
}`}
onClick={() => setActiveTab("Details")}
data-bs-toggle="tab"
>
Details
</button>
</li>
</ul>
</div>
{/* ✅ Date Picker Aligned Left with Padding */}
<div className="ps-6 mb-3 mt-0">
<div style={{ width: "120px" }}>
<input
type="date"
className="form-control p-1"
// style={{ fontSize: "1rem" }}
value={selectedDate}
onChange={handleDateChange}
/>
</div>
{/* Tabs + Date Picker */}
<div className="d-flex flex-wrap justify-content-between align-items-center me-5 ms-5">
<ul className="nav nav-tabs">
<li className="nav-item">
<button
type="button"
className={`nav-link ${AttendanceData?.activeTab === "Summary" ? "active" : ""}`}
onClick={() => (AttendanceData.activeTab = "Summary")}
>
Summary
</button>
</li>
<li className="nav-item">
<button
type="button"
className={`nav-link ${AttendanceData?.activeTab === "Details" ? "active" : ""}`}
onClick={() => (AttendanceData.activeTab = "Details")}
>
Details
</button>
</li>
</ul>
<div className="ps-6 mb-3">
<input
type="date"
className="form-control p-1"
style={{ width: "120px" }}
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
/>
</div>
</div>
{/* Body */}
<div className="card-body">
{activeTab === "Summary" && (
{/* Summary */}
{AttendanceData?.activeTab === "Summary" && (
<div className="row justify-content-center">
<div className="col-12 col-md-6 d-flex flex-column align-items-center text-center mb-4">
{isLoading ? (
@ -143,7 +115,7 @@ const Attendance = () => {
) : (
AttendanceData && (
<>
<h5 className="fw-bold mb-0 text-center w-100">
<h5 className="fw-bold mb-0">
<i className="bx bx-task text-info"></i> Attendance
</h5>
<h4 className="mb-0 fw-bold">
@ -164,11 +136,9 @@ const Attendance = () => {
</div>
)}
{activeTab === "Details" && (
<div
className="table-responsive"
style={{ maxHeight: "300px", overflowY: "auto" }}
>
{/* Details */}
{AttendanceData?.activeTab === "Details" && (
<div className="table-responsive" style={{ maxHeight: "300px" }}>
<table className="table table-hover mb-0 text-start">
<thead>
<tr>
@ -178,32 +148,17 @@ const Attendance = () => {
</tr>
</thead>
<tbody>
{AttendanceData?.attendanceTable &&
AttendanceData.attendanceTable.length > 0 ? (
AttendanceData.attendanceTable.map((record, index) => (
<tr key={index}>
<td>
{record.firstName} {record.lastName}
</td>
<td>
{new Date(record.inTime).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</td>
<td>
{new Date(record.outTime).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</td>
{AttendanceData?.attendanceTable?.length ? (
AttendanceData.attendanceTable.map((r, i) => (
<tr key={i}>
<td>{r.firstName} {r.lastName}</td>
<td>{r.inTime ? new Date(r.inTime).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "-"}</td>
<td>{r.outTime ? new Date(r.outTime).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "-"}</td>
</tr>
))
) : (
<tr>
<td colSpan="3" className="text-center">
No attendance data available
</td>
<td colSpan="3" className="text-center">No attendance data available</td>
</tr>
)}
</tbody>

View File

@ -132,7 +132,7 @@ const AttendanceOverview = () => {
onClick={() => setView("table")}
title="Table View"
>
<i class="bx bx-list-ul fs-5"></i>
<i className="bx bx-list-ul fs-5"></i>
</button>
</div>
</div>

View File

@ -1,33 +1,28 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useEffect } from "react";
import { useDashboardProjectsCardData } from "../../hooks/useDashboard_Data";
import eventBus from "../../services/eventBus";
import GlobalRepository from "../../repositories/GlobalRepository";
const Projects = () => {
const { projectsCardData } = useDashboardProjectsCardData();
const [projectData, setProjectsData] = useState(projectsCardData);
const {
data: projectsCardData,
isLoading,
isError,
error,
refetch,
} = useDashboardProjectsCardData();
useEffect(() => {
setProjectsData(projectsCardData);
}, [projectsCardData]);
// When "project" event happens, just refetch
const handler = () => {
refetch();
};
const handler = useCallback(
async (msg) => {
try {
const response =
await GlobalRepository.getDashboardProjectsCardData();
setProjectsData(response.data);
} catch (err) {
console.error(err);
}
},
[GlobalRepository]
);
useEffect(() => {
eventBus.on("project", handler);
return () => eventBus.off("project", handler);
}, [handler]);
}, [refetch]);
const totalProjects = projectsCardData?.totalProjects ?? 0;
const ongoingProjects = projectsCardData?.ongoingProjects ?? 0;
return (
<div className="card p-3 h-100 text-center d-flex justify-content-between">
@ -37,20 +32,29 @@ const Projects = () => {
Projects
</h5>
</div>
<div className="d-flex justify-content-around align-items-start mt-n2">
<div>
<h4 className="mb-0 fw-bold">
{projectData.totalProjects?.toLocaleString()}
</h4>
<small className="text-muted">Total</small>
{isLoading ? (
<div className="d-flex justify-content-center align-items-center flex-grow-1">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
<div>
<h4 className="mb-0 fw-bold">
{projectData.ongoingProjects?.toLocaleString()}
</h4>
<small className="text-muted">Ongoing</small>
) : isError ? (
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">
{error?.message || "Error loading data"}
</div>
</div>
) : (
<div className="d-flex justify-content-around align-items-start mt-n2">
<div>
<h4 className="mb-0 fw-bold">{totalProjects.toLocaleString()}</h4>
<small className="text-muted">Total</small>
</div>
<div>
<h4 className="mb-0 fw-bold">{ongoingProjects.toLocaleString()}</h4>
<small className="text-muted">Ongoing</small>
</div>
</div>
)}
</div>
);
};

View File

@ -1,10 +1,16 @@
import React from "react";
import { useSelector } from "react-redux";
import { useSelectedProject } from "../../slices/apiDataManager";
import { useDashboardTasksCardData } from "../../hooks/useDashboard_Data";
const TasksCard = () => {
const projectId = useSelector((store) => store.localVariables?.projectId);
const { tasksCardData, loading, error } = useDashboardTasksCardData(projectId);
const projectId = useSelectedProject();
const {
data: tasksCardData,
isLoading,
isError,
error,
} = useDashboardTasksCardData(projectId);
return (
<div className="card p-3 h-100 text-center d-flex justify-content-between">
@ -14,28 +20,30 @@ const TasksCard = () => {
</h5>
</div>
{loading ? (
// Loader will be displayed when loading is true
{isLoading ? (
// Loader while fetching
<div className="d-flex justify-content-center align-items-center flex-grow-1">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
) : error ? (
// Error message if there's an error
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">{error}</div>
) : isError ? (
// Show error
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">
{error?.message || "Error loading data"}
</div>
) : (
// Actual data when loaded successfully
// Show data
<div className="d-flex justify-content-around align-items-start mt-n2">
<div>
<h4 className="mb-0 fw-bold">
{tasksCardData?.totalTasks?.toLocaleString()}
{tasksCardData?.totalTasks?.toLocaleString() ?? 0}
</h4>
<small className="text-muted">Total</small>
</div>
<div>
<h4 className="mb-0 fw-bold">
{tasksCardData?.completedTasks?.toLocaleString()}
{tasksCardData?.completedTasks?.toLocaleString() ?? 0}
</h4>
<small className="text-muted">Completed</small>
</div>
@ -45,4 +53,4 @@ const TasksCard = () => {
);
};
export default TasksCard;
export default TasksCard;

View File

@ -1,33 +1,45 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useEffect } from "react";
import { useSelector } from "react-redux";
import { useDashboardTeamsCardData } from "../../hooks/useDashboard_Data";
import eventBus from "../../services/eventBus";
import { useQueryClient } from "@tanstack/react-query";
import { useSelectedProject } from "../../slices/apiDataManager";
const Teams = () => {
const projectId = useSelector((store) => store.localVariables?.projectId);
const { teamsCardData, loading, error } = useDashboardTeamsCardData(projectId);
const queryClient = useQueryClient();
const projectId = useSelectedProject()
const [totalEmployees, setTotalEmployee] = useState(0);
const [inToday, setInToday] = useState(0);
// Update state when API data arrives
useEffect(() => {
setTotalEmployee(teamsCardData?.totalEmployees || 0);
setInToday(teamsCardData?.inToday || 0);
}, [teamsCardData]);
const {
data: teamsCardData,
isLoading,
isError,
error,
} = useDashboardTeamsCardData(projectId);
// Handle real-time updates via eventBus
const handler = useCallback((msg) => {
if (msg.activity === 1) {
setInToday((prev) => prev + 1);
}
}, []);
const handler = useCallback(
(msg) => {
if (msg.activity === 1) {
queryClient.setQueryData(["dashboardTeams", projectId], (old) => {
if (!old) return old;
return {
...old,
inToday: (old.inToday || 0) + 1,
};
});
}
},
[queryClient, projectId]
);
useEffect(() => {
eventBus.on("attendance", handler);
return () => eventBus.off("attendance", handler);
}, [handler]);
const inToday = teamsCardData?.inToday ?? 0;
const totalEmployees = teamsCardData?.totalEmployees ?? 0;
return (
<div className="card p-3 h-100 text-center d-flex justify-content-between">
<div className="d-flex justify-content-start align-items-center mb-3">
@ -36,18 +48,17 @@ const Teams = () => {
</h5>
</div>
{loading ? (
// Blue spinner loader
{isLoading ? (
<div className="d-flex justify-content-center align-items-center flex-grow-1">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
) : error ? (
// Error message if data fetching fails
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">{error}</div>
) : isError ? (
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">
{error?.message || "Error loading data"}
</div>
) : (
// Display data once loaded
<div className="d-flex justify-content-around align-items-start mt-n2">
<div>
<h4 className="mb-0 fw-bold">{totalEmployees.toLocaleString()}</h4>
@ -63,4 +74,4 @@ const Teams = () => {
);
};
export default Teams;
export default Teams;

View File

@ -105,7 +105,7 @@ const ListViewContact = ({ data, Pagination }) => {
<div className="dataTables_wrapper no-footer mx-5 pb-2">
<table className="table dataTable text-nowrap">
<thead>
<tr style={{ borderBottom: "2px solid var(--bs-table-border-color)"}}>
<tr className="table_header_border">
{contactList?.map((col) => (
<th key={col.key} className={col.align}>
{col.label}

View File

@ -2,7 +2,6 @@ import React, { useState, useEffect } from "react";
import moment from "moment";
import DateRangePicker, { DateRangePicker1 } from "../common/DateRangePicker";
import { useDispatch, useSelector } from "react-redux";
import { fetchEmployeeAttendanceData } from "../../slices/apiSlice/employeeAttendanceSlice";
import usePagination from "../../hooks/usePagination";
import Avatar from "../common/Avatar";
import { convertShortTime } from "../../utils/dateUtils";

View File

@ -19,6 +19,7 @@ import { useProjectName } from "../../hooks/useProjects";
import eventBus from "../../services/eventBus";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { MANAGE_PROJECT } from "../../utils/constants";
import { useAuthModal, useLogout } from "../../hooks/useAuth";
const Header = () => {
const { profile } = useProfile();
@ -26,10 +27,9 @@ const Header = () => {
const dispatch = useDispatch();
const { data, loading } = useMaster();
const navigate = useNavigate();
const {onOpen} = useAuthModal()
const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT);
// {
// console.log(location.pathname);
// }
const { mutate : logout,isPending:logouting} = useLogout()
const isDashboardPath =
/^\/dashboard$/.test(location.pathname) || /^\/$/.test(location.pathname);
@ -59,41 +59,9 @@ const Header = () => {
return role ? role.name : "User";
};
const handleLogout = (e) => {
e.preventDefault();
logout();
};
const logout = async () => {
try {
let data = {
refreshToken: localStorage.getItem("refreshToken"),
};
AuthRepository.logout(data)
.then(() => {
localStorage.removeItem("jwtToken");
localStorage.removeItem("refreshToken");
localStorage.removeItem("user");
localStorage.clear();
clearAllCache();
window.location.href = "/auth/login";
})
.catch(() => {
localStorage.removeItem("jwtToken");
localStorage.removeItem("refreshToken");
localStorage.removeItem("user");
localStorage.clear();
clearAllCache();
window.location.href = "/auth/login";
});
} catch (error) {
console.error(
"Error during logout:",
error?.response?.data || error.message
);
}
};
const handleProfilePage = () => {
navigate(`/employee/${profile?.employeeInfo?.id}`);
@ -455,6 +423,16 @@ const Header = () => {
<span className="align-middle">Change Password</span>
</a>
</li>
<li onClick={()=>onOpen()}>
{" "}
<a
className="dropdown-item cusor-pointer"
>
<i className="bx bx-transfer-alt me-2"></i>
<span className="align-middle">Switch Tenant</span>
</a>
</li>
<li>
<div className="dropdown-divider"></div>
</li>
@ -462,11 +440,10 @@ const Header = () => {
<a
aria-label="click to log out"
className="dropdown-item cusor-pointer"
href="/logout"
onClick={handleLogout}
onClick={()=>handleLogout()}
>
<i className="bx bx-power-off me-2"></i>
<span className="align-middle">Log Out</span>
{logouting ? "Please Wait":<> <i className="bx bx-log-out me-2"></i>
<span className="align-middle">SignOut</span></>}
</a>
</li>
</ul>

View File

@ -0,0 +1,260 @@
import React, { useMemo, useState } from "react";
import { useProjectAssignedServices } from "../../hooks/useProjects";
import { useSelectedProject } from "../../slices/apiDataManager";
import {
useOrganizationType,
useServices,
} from "../../hooks/masterHook/useMaster";
import Label from "../common/Label";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { assignedOrgToProject } from "./OrganizationSchema";
import {
useAssignOrgToProject,
useAssignOrgToTenant,
useOrganizationModal,
} from "../../hooks/useOrganization";
const AssignOrg = ({ setStep }) => {
const { isOpen, orgData, startStep, onOpen, flowType, prevStep, onClose } =
useOrganizationModal();
const selectedProject = useSelectedProject();
const { data: masterService, isLoading: isMasterserviceLoading } =
useServices();
const { data: projectServices, isLoading } =
useProjectAssignedServices(selectedProject);
const { data: orgType, isLoading: orgLoading } = useOrganizationType();
const { mutate: AssignToProject, isPending: isPendingProject } =
useAssignOrgToProject(() => onClose());
const { mutate: AssignToTenant, isPending: isPendingTenat } =
useAssignOrgToTenant(() => {
onClose();
});
const isPending = isPendingProject || isPendingTenat;
const mergedServices = useMemo(() => {
if (!masterService || !projectServices) return [];
const combined = [...masterService?.data, ...projectServices];
return combined.filter(
(item, index, self) => index === self.findIndex((s) => s.id === item.id)
);
}, [masterService, projectServices]);
const resolver =
flowType === "default" ? undefined : zodResolver(assignedOrgToProject);
const {
register,
handleSubmit,
setValue,
formState: { errors },
} = useForm({
resolver,
defaultValues: {
organizationTypeId: "",
serviceIds: [],
},
});
const onSubmit = (formData) => {
if (flowType === "default") {
const payload = orgData.id;
AssignToTenant(payload);
} else {
const payload = {
...formData,
projectId: selectedProject,
organizationId: orgData.id,
parentOrganizationId: null,
};
AssignToProject(payload);
}
};
const handleEdit = () => {
onOpen({ startStep: 4, orgData });
};
const handleBack = () => {
if (prevStep === 1 && flowType === "assign") {
onOpen({ startStep: prevStep });
} else if (prevStep === 1 && flowType !== "assign") {
onOpen({ startStep: 1 });
} else {
onOpen({ startStep: 2 });
}
};
if (isMasterserviceLoading || isLoading)
return <div className="text-center">Loading....</div>;
return (
<div className="row text-black text-start mb-3">
{/* Organization Info Display */}
<div className="col-12 mb-3">
<div className="d-flex justify-content-between align-items-center text-start mb-2">
<div className="fw-semibold text-wrap">{orgData.name}</div>
<div className="text-end">
<button
type="button"
onClick={handleEdit}
className="btn btn-link p-0"
>
<i className="bx bx-edit text-secondary"></i>
</button>
</div>
</div>
</div>
{/* Contact Info */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
Contact Person :
</label>
<div className="text-muted">{orgData.contactPerson}</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
Contact Number :
</label>
<div className="text-muted">{orgData.contactNumber}</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
Email Address :
</label>
<div className="text-muted">{orgData.email}</div>
</div>
</div>
<div className="col-12 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ maxWidth: "130px" }}
>
Service provider Id (SPRID) :
</label>
<div className="text-muted">{orgData.sprid}</div>
</div>
</div>
<div className="col-12 mb-3">
<div className="d-flex">
<label
className="form-label me-1 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
Address :
</label>
<div className="text-muted text-start">{orgData.address}</div>
</div>
</div>
{/* Form */}
<div className="text-black text-start">
<form onSubmit={handleSubmit(onSubmit)}>
{/* Show fields only if flowType is NOT default */}
{flowType !== "default" && (
<>
{/* Organization Type */}
<div className="mb-3 text-start">
<Label htmlFor="organizationTypeId" className="mb-3 fw-semibold" required>
Organization Type
</Label>
<div className="d-flex flex-wrap gap-3 mt-1">
{orgType?.data.map((type) => (
<div
key={type.id}
className="form-check d-flex align-items-center gap-2 p-0 m-0"
>
<input
type="radio"
id={`organizationType-${type.id}`}
value={type.id}
{...register("organizationTypeId")}
className="form-check-input m-0"
/>
<label
className="form-check-label m-0"
htmlFor={`organizationType-${type.id}`}
>
{type.name}
</label>
</div>
))}
</div>
{errors.organizationTypeId && (
<span className="text-danger">
{errors.organizationTypeId.message}
</span>
)}
</div>
{/* Services */}
<div className="mb-3">
<Label htmlFor="serviceIds" className="mb-3 fw-semibold" required>
Select Services
</Label>
{mergedServices?.map((service) => (
<div key={service.id} className="form-check mb-3">
<input
type="checkbox"
value={service.id}
{...register("serviceIds")}
className="form-check-input"
/>
<label className="form-check-label">{service.name}</label>
</div>
))}
{errors.serviceIds && (
<div className="text-danger small">
{errors.serviceIds.message}
</div>
)}
</div>
</>
)}
{/* Buttons: Always visible */}
<div className="d-flex justify-content-between mt-5">
<button
type="button"
className="btn btn-xs btn-outline-secondary"
onClick={handleBack}
disabled={isPending}
>
<i className="bx bx-left-arrow-alt"></i> Back
</button>
<button
type="submit"
className="btn btn-sm btn-primary"
disabled={isPending}
>
{isPending
? "Please wait..."
: flowType === "default"
? "Assign Organization"
: "Add"}
</button>
</div>
</form>
</div>
</div>
);
};
export default AssignOrg;

View File

@ -0,0 +1,203 @@
import React, { useEffect } from "react";
import { FormProvider, useForm } from "react-hook-form";
import {
useCreateOrganization,
useOrganizationModal,
useUpdateOrganization,
} from "../../hooks/useOrganization";
import {
defaultOrganizationValues,
organizationSchema,
} from "./OrganizationSchema";
import Label from "../common/Label";
import { useGlobalServices } from "../../hooks/masterHook/useMaster";
import { zodResolver } from "@hookform/resolvers/zod";
import SelectMultiple from "../common/SelectMultiple";
const ManagOrg = () => {
const { data: service, isLoading } = useGlobalServices();
const { flowType, orgData, startStep, onOpen, onClose, prevStep } =
useOrganizationModal();
const method = useForm({
resolver: zodResolver(organizationSchema),
defaultValues: defaultOrganizationValues,
});
const {
handleSubmit,
register,
reset,
formState: { errors },
} = method;
// Create & Update mutations
const { mutate: createOrganization, isPending: isCreating } =
useCreateOrganization(() => {
reset(defaultOrganizationValues);
onOpen({ startStep: 1 });
onClose();
});
const { mutate: updateOrganization, isPending: isUpdating } =
useUpdateOrganization(() => {
reset(defaultOrganizationValues);
onOpen({ startStep: 1 });
onClose();
});
// Prefill form if editing
useEffect(() => {
if (orgData) {
console.log(orgData);
reset({
name: orgData.name || "",
contactPerson: orgData.contactPerson || "",
contactNumber: orgData.contactNumber || "",
email: orgData.email || "",
serviceIds: orgData.services?.map((s) => s.id) || [],
address: orgData.address || "",
});
}
}, [orgData, reset]);
const onSubmit = (payload) => {
if (orgData?.id) {
updateOrganization({ id: orgData.id, ...payload });
} else {
createOrganization(payload);
}
};
const handleBack = () => {
if (flowType === "edit") {
onClose();
return;
}
if (flowType === "assign") {
if (prevStep === 1) {
onOpen({ startStep: 1 });
} else {
onOpen({ startStep: prevStep ?? 2 });
}
return;
}
onOpen({ startStep: 2 });
};
return (
<FormProvider {...method}>
<form className="form" onSubmit={handleSubmit(onSubmit)}>
<div className="mb-1 text-start">
<Label htmlFor="name" required>
Organization Name
</Label>
<input
className="form-control form-control-sm"
{...register("name")}
/>
{errors.name && (
<span className="danger-text">{errors.name.message}</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="contactPerson" required>
Contact Person
</Label>
<input
className="form-control form-control-sm"
{...register("contactPerson")}
/>
{errors.contactPerson && (
<span className="danger-text">{errors.contactPerson.message}</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="contactNumber" required>
Contact Number
</Label>
<input
className="form-control form-control-sm"
{...register("contactNumber")}
/>
{errors.contactNumber && (
<span className="danger-text">{errors.contactNumber.message}</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="email" required>
Email Address
</Label>
<input
className="form-control form-control-sm"
{...register("email")}
/>
{errors.email && (
<span className="danger-text">{errors.email.message}</span>
)}
</div>
<div className="mb-1 text-start">
<SelectMultiple
name="serviceIds"
label="Services"
required
valueKey="id"
options={service?.data || []}
required = {true}
/>
{errors.serviceIds && (
<span className="danger-text">{errors.serviceIds.message}</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="address" required>
Address
</Label>
<textarea
className="form-control form-control-sm"
{...register("address")}
rows={2}
/>
{errors.address && (
<span className="danger-text">{errors.address.message}</span>
)}
</div>
<div className="d-flex justify-content-between gap-2 my-2">
<button
type="button"
className="btn btn-sm btn-outline-secondary"
onClick={handleBack}
>
{flowType === "edit" ? (
"Close"
) : (
<>
<i className="bx bx-chevron-left"></i>Back
</>
)}
</button>
<div>
<button
type="submit"
className="btn btn-sm btn-primary"
disabled={isCreating || isUpdating || isLoading}
>
{isCreating || isUpdating
? "Please Wait..."
: orgData
? "Update"
: "Submit"}
</button>
</div>
</div>
</form>
</FormProvider>
);
};
export default ManagOrg;

View File

@ -0,0 +1,145 @@
import { useState } from "react";
import {
useAssignOrgToTenant,
useOrganizationBySPRID,
useOrganizationModal,
} from "../../hooks/useOrganization";
import Label from "../common/Label";
import { useDebounce } from "../../utils/appUtils";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { spridSchema } from "./OrganizationSchema";
import { OrgCardSkeleton } from "./OrganizationSkeleton";
// Zod schema: only allow exactly 4 digits
const OrgPickerFromSPId = ({ title, placeholder }) => {
const { onClose, startStep, flowType, onOpen, prevStep } =
useOrganizationModal();
const {
register,
handleSubmit,
formState: { errors },
watch,
} = useForm({
resolver: zodResolver(spridSchema),
defaultValues: { spridSearchText: "" },
});
const [SPRID, setSPRID] = useState("");
const { data, isLoading, isError, error, refetch } =
useOrganizationBySPRID(SPRID);
const onSubmit = (formdata) => {
setSPRID(formdata.spridSearchText);
};
const handleOrg = (orgId) => {};
const SP = watch("spridSearchText");
return (
<div className="d-block">
<form
className="d-flex flex-row gap-6 text-start align-items-center"
onSubmit={handleSubmit(onSubmit)}
>
<div className="d-flex flex-row align-items-center gap-2">
<Label className="text-secondary">Search by SPRID</Label>
<input
type="search"
{...register("spridSearchText")}
className="form-control form-control-sm w-auto"
placeholder="Enter SPRID"
maxLength={4}
/>
</div>
<button type="submit" className="btn btn-sm btn-primary">
<i className="bx bx-sm bx-search-alt-2"></i> Search
</button>
</form>
<div className="text-start danger-text">
{" "}
{errors.spridSearchText && (
<p className="text-danger small mt-1">
{errors.spridSearchText.message}
</p>
)}
</div>
{/* ---- Organization list ---- */}
{isLoading ? (
<OrgCardSkeleton />
) : data && data?.data.length > 0 ? (
<div className="py-2 text-tiny text-center">
<div className="d-flex flex-column gap-2 border-0 bg-none">
{data.data.map((org) => (
<div className="d-flex flex-row gap-2 text-start text-black ">
<div className="mt-1">
<img
src="/public/assets/img/SP-Placeholdeer.svg"
alt="logo"
width={50}
height={50}
/>
</div>
<div
className="d-flex flex-column p-0 m-0 cursor-pointer"
onClick={() => onOpen({ startStep: 3, orgData: org })}
>
<span className="fs-6 fw-semibold">{org.name}</span>
<div className="d-flex gap-2">
<small
className=" fw-semibold text-uppercase"
style={{ letterSpacing: "1px" }}
>
SPRID :{" "}
</small>
<small className="fs-6">{org.sprid}</small>
</div>
<div className="d-flex flex-row gap-2">
<small className="text-small fw-semibold">Address:</small>
<div className="d-flex text-wrap">{org.address}</div>
</div>
</div>
</div>
))}
</div>
</div>
) : SPRID ? (
<div className="py-3 text-center text-secondary">
No organization found for "{SPRID}"
</div>
) : null}
<div className="py-12 text-center text-tiny text-black">
<small className="d-block text-secondary">
Do not have SPRID or could not find organization ?
</small>
<button
type="button"
className="btn btn-sm btn-primary mt-3"
onClick={() => onOpen({ startStep: 4 })}
>
<i className="bx bx-plus-circle me-2"></i>
Create New Organization
</button>
</div>
{/* ---- Footer buttons ---- */}
<div className={`d-flex text-secondary mt-3`}>
{flowType !== "default" && (
<button
type="button"
className="btn btn-xs btn-outline-secondary"
onClick={() => onOpen({ startStep: prevStep })}
>
<i className="bx bx-chevron-left"></i> Back
</button>
)}
</div>
</div>
);
};
export default OrgPickerFromSPId;

View File

@ -0,0 +1,167 @@
import { useState } from "react";
import {
useOrganizationModal,
useOrganizationsList,
} from "../../hooks/useOrganization";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import Label from "../common/Label";
import Pagination from "../common/Pagination";
const OrgPickerfromTenant = ({ title }) => {
const [searchText, setSearchText] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const { data, isLoading } = useOrganizationsList(
ITEMS_PER_PAGE - 10,
1,
true,
null,
searchText
);
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
const { isOpen, orgData, startStep, onOpen, flowType, prevStep } =
useOrganizationModal();
const handleBack = () => {
if (prevStep == 1 && flowType == "assign") {
onOpen({ startStep: prevStep });
} else if (prevStep == 1 && flowType == "assign") {
onOpen({ startStep: 1 });
} else {
onOpen({ startStep: 2 });
}
};
const contactList = [
{
key: "name",
label: "Name",
getValue: (org) => (
<div className="d-flex gap-2 py-1 ">
<i class="bx bx-buildings"></i>
<span
className="text-truncate d-inline-block "
style={{ maxWidth: "150px" }}
>
{org?.name || "N/A"}
</span>
</div>
),
align: "text-start",
},
{
key: "sprid",
label: "SPRID",
getValue: (org) => (
<span
className="text-truncate d-inline-block"
style={{ maxWidth: "200px" }}
>
{org?.sprid || "N/A"}
</span>
),
align: "text-center",
},
];
return (
<div className="d-block">
<div className="text-start mb-1">
<Label className="text-secondary">{title}</Label>
<input
type="text"
value={searchText}
onChange={(e) => setSearchText?.(e.target.value)}
className="form-control form-control-sm w-auto"
placeholder="Enter Organization Name"
/>
</div>
{/* ---- Organization list ---- */}
{isLoading ? (
<div>Loading....</div>
) : data && data?.data?.length > 0 ? (
<div className="dataTables_wrapper no-footer pb-2">
<table className="table dataTable text-nowrap">
<thead>
<tr className="table_header_border">
{contactList.map((col) => (
<th key={col.key} className={col.align}>
{col.label}
</th>
))}
<th className="sticky-action-column bg-white text-center">
Action
</th>
</tr>
</thead>
<tbody>
{Array.isArray(data.data) && data.data.length > 0
? data.data.map((row, i) => (
<tr key={i}>
{contactList.map((col) => (
<td key={col.key} className={col.align}>
{col.getValue(row)}
</td>
))}
<td className="sticky-action-column bg-white">
<button
className="btn btn-sm btn-primary"
onClick={() => onOpen({ startStep: 3, orgData: row })}
>
Select
</button>
</td>
</tr>
))
: null}
</tbody>
</table>
</div>
) : null}
<div className="d-flex flex-column align-items-center text-center text-wrap text-black gap-2">
<small className="mb-1">
Could not find organization in your database? Please search within the
global database.
</small>
<button
type="button"
className="btn btn-sm btn-primary w-auto"
onClick={() => onOpen({ startStep: 2 })}
>
Search Using SPRID
</button>
</div>
{/* ---- Footer buttons ---- */}
<div
className={`d-flex justify-content-end
text-secondary mt-3`}
>
{flowType == "default" && (
<button
type="button"
className="btn btn-sm btn-outline-secondary"
onClick={handleBack}
>
<i className="bx bx-left-arrow-alt"></i> Back
</button>
)}
{/* <button
type="button"
className="btn btn-sm btn-secondary"
onClick={() => onOpen({ startStep: 4 })}
>
<i className="bx bx-plus-circle me-2"></i>
Add New Organization
</button> */}
</div>
</div>
);
};
export default OrgPickerfromTenant;

View File

@ -0,0 +1,110 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useMemo, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import {
defaultOrganizationValues,
organizationSchema,
} from "./OrganizationSchema";
import Modal from "../common/Modal";
import {
useCreateOrganization,
useOrganizationBySPRID,
useOrganizationModal,
useOrganizationsList,
} from "../../hooks/useOrganization";
import Label from "../common/Label";
import SelectMultiple from "../common/SelectMultiple";
import { useServices } from "../../hooks/masterHook/useMaster";
import AssignOrg from "./AssignOrg";
import ManagOrg from "./ManagOrg";
import OrgPickerFromSPId from "./OrgPickerFromSPId";
import OrgPickerfromTenant from "./OrgPickerfromTenant";
const OrganizationModal = () => {
const { isOpen, orgData, startStep, onOpen, onClose, onToggle } =
useOrganizationModal();
const { data: masterService, isLoading } = useServices();
const [searchText, setSearchText] = useState();
const [SPRID, setSPRID] = useState("");
const [Organization, setOrganization] = useState({});
const method = useForm({
resolver: zodResolver(organizationSchema),
defaultValues: defaultOrganizationValues,
});
const {
handleSubmit,
register,
reset,
formState: { errors },
} = method;
const { mutate: CreateOrganization, isPending } = useCreateOrganization(
() => {
reset(defaultOrganizationValues);
onClose();
}
);
const onSubmit = (OrgPayload) => {
CreateOrganization(OrgPayload);
};
const RenderTitle = useMemo(() => {
if (orgData) {
return "Assign Organization";
}
if (startStep === 1) {
return orgData && orgData !== null
? "Add Organization"
: "Choose Organization1";
}
if (startStep === 2) {
return "Add Organization";
}
if (startStep === 3) {
return "Assign Organization";
}
return "Manage Organization";
}, [startStep, orgData]);
const contentBody = (
<div>
{/* ---------- STEP 1: Service Provider- Form Own Tenant list ---------- */}
{startStep === 1 && <OrgPickerfromTenant title="Find Organization" />}
{startStep === 2 && (
<OrgPickerFromSPId
title="Find Organization"
placeholder="Enter Service Provider Id"
projectOrganizations={orgData}
/>
)}
{/* ---------- STEP 2: Existing Organization Details ---------- */}
{startStep === 3 && Organization && (
<AssignOrg Organization={Organization} />
)}
{/* ---------- STEP 3: Add New Organization ---------- */}
{startStep === 4 && <ManagOrg />}
</div>
);
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={RenderTitle}
body={contentBody}
/>
);
};
export default OrganizationModal;

View File

@ -0,0 +1,57 @@
import { array, z } from "zod";
const phoneRegex = /^\+?[1-9]\d{6,14}$/;
export const organizationSchema = z.object({
name: z.string().min(1, { message: "Name is required" }),
contactNumber: z
.string()
.trim()
.min(7, { message: "Contact number must be at least 7 digits" })
.max(20, { message: "Contact number cannot exceed 12 digits" })
.regex(phoneRegex, { message: "Invalid phone number" }),
contactPerson: z.string().min(1, { message: "Person name required" }),
address: z.string().min(1, { message: "Address is required!" }),
email: z
.string()
.min(1, { message: "Email is required" })
.email("Invalid email address"),
serviceIds: z
.array(z.string())
.min(1, { message: "Please insert service id" }),
});
export const defaultOrganizationValues = {
name: "",
contactNumber: "",
contactPerson: "",
address: "",
email: "",
serviceIds: [],
};
export const assignedOrgToProject = z.object({
// projectId: z.string().uuid({ message: "Invalid projectId format" }),
// organizationId: z.string().string({ message: "Invalid organizationId format" }),
// parentOrganizationId: z
// .string()
// .nullable()
// .optional(),
serviceIds: z.preprocess(
(val) => (Array.isArray(val) ? val : []),
z
.array(z.string().uuid({ message: "Invalid serviceId format" }))
.nonempty({ message: "At least one service must be selected" })
),
organizationTypeId: z
.string()
.min(1, { message: "Organization is required" }),
});
export const spridSchema = z.object({
spridSearchText: z
.string()
.regex(/^\d{4}$/, { message: "SPRID must be exactly 4 digits" }),
});

View File

@ -0,0 +1,44 @@
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
<div
className={`skeleton mb-2 ${className}`}
style={{
height,
width,
}}
></div>
);
export const OrgCardSkeleton = () => {
return (
<div className="row p-3">
{[...Array(1)].map((_, idx) => (
<div
key={idx}
className="list-group-item d-flex flex-row gap-2 align-items-start border-0 shadow-sm rounded mb-3 p-3"
>
{/* Left: Logo/avatar placeholder */}
<div className="mt-1">
<SkeletonLine height={50} width={50} className="rounded-circle" />
</div>
{/* Right: Info section */}
<div className="d-flex flex-column flex-grow-1 text-start">
{/* Org name */}
<SkeletonLine height={18} width="160px" className="mb-2" />
{/* SPRID */}
<div className="d-flex gap-2 mb-2">
<SkeletonLine height={14} width="60px" />
<SkeletonLine height={14} width="100px" />
</div>
{/* Address */}
<div className="d-flex gap-2">
<SkeletonLine height={14} width="70px" />
<SkeletonLine height={14} width="100%" />
</div>
</div>
</div>
))}
</div>
);
};

View File

@ -0,0 +1,164 @@
import React, { useState } from "react";
import { useOrganizationModal, useOrganizationsList } from "../../hooks/useOrganization";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import Avatar from "../common/Avatar";
import { useDebounce } from "../../utils/appUtils";
import Pagination from "../common/Pagination";
const OrganizationsList = ({searchText}) => {
const [currentPage, setCurrentPage] = useState(1);
const searchString = useDebounce(searchText,500)
const {
data = [],
isLoading,
isFetching,
isError,
error,
} = useOrganizationsList(ITEMS_PER_PAGE, 1, true,null,searchString);
const {onClose, startStep, flowType, onOpen,orgData } = useOrganizationModal();
const organizationsColumns = [
{
key: "name",
label: "Organization Name",
getValue: (org) => (
<div className="d-flex gap-2 py-1 ">
<i class="bx bx-buildings"></i>
<span
className="text-truncate d-inline-block "
style={{ maxWidth: "150px" }}
>
{org?.name || "N/A"}
</span>
</div>
),
align: "text-start",
},
{
key: "contactPerson",
label: "Contact Person",
getValue: (org) => (
(
<div className="d-flex align-items-center ps-1">
<Avatar
size="xs"
classAvatar="m-0"
firstName={(org?.name || "").trim().split(" ")[0] || ""}
lastName={(org?.name || "").trim().split(" ")[1] || ""}
/>
<span
className="text-truncate d-inline-block "
style={{ maxWidth: "150px" }}
>
{org?.name || "N/A"}
</span>
</div>
)
),
align: "text-start",
},
{
key: "contactNumber",
label: "Phone Number",
getValue: (org) => org.contactNumber || "N/A",
align: "text-start",
},
{
key: "email",
label: "Email",
getValue: (org) => (
<span
className="text-truncate d-inline-block"
style={{ maxWidth: "200px" }}
>
{org?.email || "N/A"}
</span>
),
align: "text-start",
},
{
key: "sprid",
label: "SPRID Id",
getValue: (org) => org.sprid || "N/A",
align: "text-center",
},
];
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
if (isFetching && !isFetching) return <div>Loading...</div>;
if (isError) return <div>{error?.message || "Something went wrong"}</div>;
return (
<div className="card px-0 px-sm-4">
<div className="card-datatable table-responsive" id="horizontal-example">
<div className="dataTables_wrapper no-footer px-2">
<table className="table border-top dataTable text-nowrap">
<thead>
<tr className="table_header_border">
{organizationsColumns.map((col) => (
<th
key={col.key}
className="sorting d-table-cell"
aria-sort="descending"
>
<div className={`${col.align}`}>{col.label}</div>
</th>
))}
<th className="sticky-action-column bg-white text-center">
Action
</th>
</tr>
</thead>
<tbody>
{data?.data?.length > 0 ? (
data?.data?.map((org) => (
<tr key={org.id}>
{organizationsColumns.map((col) => (
<td
key={col.key}
className={`d-table-cell ${col.align ?? ""}`}
>
{col.customRender
? col.customRender(org)
: col.getValue(org)}
</td>
))}
<td className="sticky-action-column ">
<div className="d-flex justify-content-center gap-2">
<i className="bx bx-show text-primary cursor-pointer" ></i>
<i className="bx bx-edit text-secondary cursor-pointer" onClick={()=>onOpen({startStep:4,orgData:org,flowType:"edit"})}></i>
<i className="bx bx-trash text-danger cursor-pointer"></i>
</div>
</td>
</tr>
))
) : (
<tr>
<td
colSpan={organizationsColumns.length + 1}
className="text-center"
>
<p className="fw-semibold">Not Found</p>
</td>
</tr>
)}
</tbody>
</table>
{data?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
)}
</div>
</div>
</div>
);
};
export default OrganizationsList;

View File

@ -0,0 +1 @@
useAssignOrgToTenant

View File

@ -1,6 +1,6 @@
import React from "react";
import WorkArea from "./WorkArea";
const Floor = ({ floor, workAreas, forBuilding }) => {
const Floor = ({ floor, workAreas, forBuilding,serviceId }) => {
return (
<React.Fragment key={floor.id}>
{workAreas && workAreas.length > 0 ? (
@ -10,6 +10,7 @@ const Floor = ({ floor, workAreas, forBuilding }) => {
key={workArea.id}
workArea={workArea}
floor={floor}
serviceId={serviceId}
/>
))
) : (

View File

@ -11,7 +11,7 @@ import {
getCachedData,
} from "../../../slices/apiDataManager";
const InfraTable = ({ buildings, projectId}) => {
const InfraTable = ({ buildings, projectId, serviceId }) => {
const [projectBuilding, setProjectBuilding] = useState([]);
const [expandedBuildings, setExpandedBuildings] = useState([]);
const [showFloorModal, setShowFloorModal] = useState(false);
@ -90,6 +90,7 @@ const InfraTable = ({ buildings, projectId}) => {
forBuilding={building}
floor={floor}
workAreas={floor.workAreas}
serviceId={serviceId}
/>
))
) : (
@ -100,7 +101,7 @@ const InfraTable = ({ buildings, projectId}) => {
No floors have been added yet. Start by adding floors to manage
this building.
</p>
</div>
</td>
</tr>
@ -143,7 +144,7 @@ const InfraTable = ({ buildings, projectId}) => {
// }, [handler]);
return (
<div>
<div className="px-6">
{projectBuilding && projectBuilding.length > 0 && (
<table className="table table-bordered">
<tbody>

View File

@ -6,9 +6,10 @@ import {
useActivitiesMaster,
useWorkCategoriesMaster,
} from "../../../hooks/masterHook/useMaster";
import { useManageTask } from "../../../hooks/useProjects";
import { useManageTask, useProjectAssignedServices } from "../../../hooks/useProjects";
import showToast from "../../../services/toastService";
import Label from "../../common/Label";
import { useSelectedProject } from "../../../slices/apiDataManager";
const taskSchema = z.object({
buildingID: z.string().min(1, "Building is required"),
@ -37,6 +38,15 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
const { activities, loading: activityLoading } = useActivitiesMaster();
const { categories, categoryLoading } = useWorkCategoriesMaster();
const projectId = useSelectedProject();
const { data: assignedServices, isLoading: servicesLoading } = useProjectAssignedServices(projectId);
const [selectedService, setSelectedService] = useState("");
const handleServiceChange = (e) => {
setSelectedService(e.target.value);
};
const {
register,
handleSubmit,
@ -96,10 +106,12 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
}, [categories]);
const onSubmitForm = async (data) => {
const payload = [data];
CreateTask({payload:payload,buildingId: data.buildingID,
const payload = [data];
CreateTask({
payload: payload, buildingId: data.buildingID,
floorId: data.floorId,
workAreaId: data.workAreaId, PreviousPlannedWork:0,previousCompletedWork:0});
workAreaId: data.workAreaId, PreviousPlannedWork: 0, previousCompletedWork: 0
});
};
return (
@ -150,6 +162,7 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
</div>
)}
{/* Work Area Selection */}
{selectedFloor && (
<div className="col-12 text-start">
<Label className="form-label" required>Select Work Area</Label>
@ -172,6 +185,32 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
</div>
)}
{/* Services Selection */}
{selectedWorkArea && (
<div className="col-12 text-start">
<Label className="form-label">Select Services</Label>
<select
name="DataTables_Table_0_length"
aria-controls="DataTables_Table_0"
className="form-select form-select-sm"
aria-label="Select Service"
value={selectedService}
onChange={handleServiceChange}
>
{servicesLoading && <option>Loading...</option>}
{assignedServices?.map((service) => (
<option key={service.id} value={service.id}>
{service.name}
</option>
))}
</select>
{errors.buildingID && (
<p className="danger-text">{errors.buildingID.message}</p>
)}
</div>
)}
{/* Activity Selection */}
{selectedWorkArea && (
<div className="col-12 text-start">
<Label className="form-label" required>Select Activity</Label>
@ -192,6 +231,7 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
</div>
)}
{selectedWorkArea && (
<div className="col-12 text-start">
<label className="form-label">Select Work Category</label>
@ -261,7 +301,7 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
</div>
)}
<div className="col-12 text-end mt-5">
<div className="col-12 text-end mt-5">
<button
type="button"
className="btn btn-sm btn-label-secondary me-3"

View File

@ -16,18 +16,17 @@ import { useParams } from "react-router-dom";
import ProgressBar from "../../common/ProgressBar";
import {formatNumber} from "../../../utils/dateUtils";
const WorkArea = ({ workArea, floor, forBuilding }) => {
const WorkArea = ({ workArea, floor, forBuilding,serviceId = null }) => {
const selectedProject = useSelector((store) => store.localVariables.projectId);
const { projects_Details, loading } = useProjectDetails(selectedProject);
const [IsExpandedArea, setIsExpandedArea] = useState(false);
const dispatch = useDispatch();
const [Project, setProject] = useState();
// const { projectId } = useParams();
const ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
const ManageAndAssignTak = useHasUserPermission(ASSIGN_REPORT_TASK);
const { ProjectTaskList, isLoading } = useProjectTasks(workArea.id, IsExpandedArea);
const { ProjectTaskList, isLoading } = useProjectTasks(workArea.id,serviceId, IsExpandedArea);
const [workAreaStatus, setWorkAreaStatus] = useState({
completed: 0,

View File

@ -17,22 +17,20 @@ import {
getCachedData,
useSelectedProject,
} from "../../slices/apiDataManager";
import { useProjectDetails, useProjectInfra } from "../../hooks/useProjects";
import { useProjectAssignedServices, useProjectDetails, useProjectInfra } from "../../hooks/useProjects";
import { useDispatch, useSelector } from "react-redux";
import { refreshData } from "../../slices/localVariablesSlice";
import eventBus from "../../services/eventBus";
import {useParams} from "react-router-dom";
import { useParams } from "react-router-dom";
import GlobalModel from "../common/GlobalModel";
const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
{
// const projectId = useSelector((store)=>store.localVariables.projectId)
const ProjectInfra = ({ data, onDataChange, eachSiteEngineer }) => {
const projectId = useSelectedProject();
const reloadedData = useSelector((store) => store.localVariables.reload);
const [ expandedBuildings, setExpandedBuildings ] = useState( [] );
const {projectInfra,isLoading,error} = useProjectInfra(projectId)
const [expandedBuildings, setExpandedBuildings] = useState([]);
const { projectInfra, isLoading, error } = useProjectInfra(projectId)
const { projects_Details, refetch, loading } = useProjectDetails(data?.id);
const [ project, setProject ] = useState( projects_Details );
const [project, setProject] = useState(projects_Details);
const ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
const ManageTask = useHasUserPermission(MANAGE_TASK)
const [showModalFloor, setshowModalFloor] = useState(false);
@ -40,47 +38,46 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
const [showModalTask, setshowModalTask] = useState(false);
const [showModalBuilding, setshowModalBuilding] = useState(false);
const dispatch = useDispatch();
const { data: assignedServices, isLoading: servicesLoading } = useProjectAssignedServices(projectId);
const [selectedService, setSelectedService] = useState("");
const handleServiceChange = (e) => {
setSelectedService(e.target.value);
};
useEffect(() => {
setProject(projectInfra);
}, [data, projects_Details]);
// useEffect(() => {
// if (reloadedData) {
// refetch();
// dispatch(refreshData(false));
// }
// }, [reloadedData]);
const signalRHandler = (response) => {
setProject(response);
}
return (
<>
{showModalBuilding && <GlobalModel isOpen={showModalBuilding} size="md" closeModal={() => setshowModalBuilding( false )}>
{showModalBuilding && <GlobalModel isOpen={showModalBuilding} size="md" closeModal={() => setshowModalBuilding(false)}>
<BuildingModel
project={projectInfra}
onClose={() => setshowModalBuilding( false )}
onClose={() => setshowModalBuilding(false)}
/>
</GlobalModel>}
{showModalFloor && <GlobalModel isOpen={showModalFloor} size="md" closeModal={()=>setshowModalFloor(false)}>
</GlobalModel>}
{showModalFloor && <GlobalModel isOpen={showModalFloor} size="md" closeModal={() => setshowModalFloor(false)}>
<FloorModel
project={projectInfra}
onClose={()=>setshowModalFloor(false)}
/>
project={projectInfra}
onClose={() => setshowModalFloor(false)}
/>
</GlobalModel>}
{showModalWorkArea && <GlobalModel isOpen={showModalWorkArea} size="lg" closeModal={()=>setshowModalWorkArea(false)} >
<WorkAreaModel
project={projectInfra}
onClose={()=>setshowModalWorkArea(false)}
/>
{showModalWorkArea && <GlobalModel isOpen={showModalWorkArea} size="lg" closeModal={() => setshowModalWorkArea(false)} >
<WorkAreaModel
project={projectInfra}
onClose={() => setshowModalWorkArea(false)}
/>
</GlobalModel>}
{showModalTask && ( <GlobalModel isOpen={showModalTask} size="lg" closeModal={()=>setshowModalTask(false)}>
{showModalTask && (<GlobalModel isOpen={showModalTask} size="lg" closeModal={() => setshowModalTask(false)}>
<TaskModel
project={projectInfra}
onClose={()=>setshowModalTask(false)}
/>
project={projectInfra}
onClose={() => setshowModalTask(false)}
/>
</GlobalModel>)}
<div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4">
<div className="card">
@ -88,38 +85,67 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
<div className="align-items-center">
<div className="row ">
<div
className={`col-12 text-end mb-1 `}
className="dataTables_length text-start py-2 px-6 col-md-4 col-12"
id="DataTables_Table_0_length"
>
{ManageInfra && (<>
<button
type="button"
className="link-button btn btn-xs rounded-md link-button-sm m-1 btn-primary"
onClick={()=>setshowModalBuilding(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Building
</button>
<button
type="button"
className="link-button btn btn-xs rounded-md m-1 btn-primary"
onClick={()=>setshowModalFloor(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Floors
</button>
<button
type="button"
className="link-button btn btn-xs rounded-md m-1 btn-primary"
onClick={() => setshowModalWorkArea(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Work Areas
</button></>)}
{!servicesLoading && assignedServices?.length > 0 && (
assignedServices.length > 1 ? (
<label>
<select
name="DataTables_Table_0_length"
aria-controls="DataTables_Table_0"
className="form-select form-select-sm"
aria-label="Select Service"
value={selectedService}
onChange={handleServiceChange}
>
<option value="">All Services</option>
{assignedServices.map((service) => (
<option key={service.id} value={service.id}>
{service.name}
</option>
))}
</select>
</label>
) : (
<h5>{assignedServices[0].name}</h5>
)
)}
</div>
{/* Buttons Section (aligned to right) */}
<div className="col-md-8 col-12 text-end mb-1">
{ManageInfra && (
<>
<button
type="button"
className="link-button btn btn-xs rounded-md link-button-sm m-1 btn-primary"
onClick={() => setshowModalBuilding(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Building
</button>
<button
type="button"
className="link-button btn btn-xs rounded-md m-1 btn-primary"
onClick={() => setshowModalFloor(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Floors
</button>
<button
type="button"
className="link-button btn btn-xs rounded-md m-1 btn-primary"
onClick={() => setshowModalWorkArea(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Work Areas
</button></>)}
{(ManageTask || ManageInfra) && (<button
type="button"
className="link-button btn btn-xs rounded-md m-1 btn-primary"
onClick={()=>setshowModalTask(true)}
onClick={() => setshowModalTask(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Create Tasks
@ -132,8 +158,7 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
<InfraTable
buildings={projectInfra}
projectId={projectId}
// handleFloor={submitData}
// signalRHandler ={signalRHandler}
serviceId={selectedService}
/>
)}
{!isLoading && projectInfra?.length == 0 && <div className="mt-5"><p>No Infra Avaiable</p></div>}

View File

@ -1,6 +1,4 @@
import React from "react";
import { hasUserPermission } from "../../utils/authUtils";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import {
DIRECTORY_ADMIN,
DIRECTORY_MANAGER,
@ -13,6 +11,7 @@ import {
VIEW_DOCUMENT,
VIEW_PROJECT_INFRA,
} from "../../utils/constants";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
const ProjectNav = ({ onPillClick, activePill }) => {
const HasViewInfraStructure = useHasUserPermission(VIEW_PROJECT_INFRA);
@ -22,7 +21,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
const DireManager = useHasUserPermission(DIRECTORY_MANAGER);
const DirUser = useHasUserPermission(DIRECTORY_USER);
const isManageTeam = useHasUserPermission(MANAGE_TEAM)
const isViewDocuments = hasUserPermission(VIEW_DOCUMENT);
const isViewDocuments = useHasUserPermission(VIEW_DOCUMENT);
const isUploadDocument = useHasUserPermission(UPLOAD_DOCUMENT)
const isModifyDocument = useHasUserPermission(MODIFY_DOCUMENT)
@ -43,6 +42,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
},
{ key: "documents", icon: "bx bx-folder-open", label: "Documents",hidden:!(isViewDocuments || isModifyDocument || isUploadDocument) },
{ key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam },
{ key: "organization", icon: "bx bx-buildings", label: "Organization"},
];
return (
<div className="nav-align-top">

View File

@ -0,0 +1,99 @@
import React from "react";
import { useProjectAssignedOrganizations } from "../../../hooks/useProjects";
import { useSelectedProject } from "../../../slices/apiDataManager";
const ProjectAssignedOrgs = () => {
const selectedProject = useSelectedProject();
const { data, isLoading, isError, error } =
useProjectAssignedOrganizations(selectedProject);
const contactList = [
{
key: "name",
label: "Organization Name",
getValue: (org) => (
<div className="d-flex gap-2 py-1 ">
<i class="bx bx-buildings"></i>
<span
className="text-truncate d-inline-block "
style={{ maxWidth: "150px" }}
>
{org?.name || "N/A"}
</span>
</div>
),
align: "text-start",
},
{
key: "sprid",
label: "Service Provider Id",
getValue: (org) => (
<span
className="text-truncate d-inline-block"
style={{ maxWidth: "200px" }}
>
{org?.sprid || "N/A"}
</span>
),
align: "text-center",
},
{
key: "isActive",
label: "Status",
getValue: (org) => (
<span
className={`text-truncate d-inline-block badge bg-label-${org.isActive ? "primary" :"secondary"}`}
style={{ maxWidth: "80px" }}
>
{org?.isActive ? "Active" : "Inactive"}
</span>
),
align: "text-start",
},
];
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>{error.message}</div>;
return (
<div>
<div className="dataTables_wrapper no-footer mx-5 pb-2">
<table className="table dataTable text-nowrap">
<thead>
<tr className="table_header_border">
{contactList.map((col) => (
<th key={col.key} className={col.align}>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{Array.isArray(data) && data.length > 0 ? (
data.map((row, i) => (
<tr key={i}>
{contactList.map((col) => (
<td key={col.key} className={col.align}>
{col.getValue(row)}
</td>
))}
</tr>
))
) : (
<tr style={{ height: "200px" }}>
<td
colSpan={contactList.length}
className="text-center align-middle"
>
Not Assigned yet
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
);
};
export default ProjectAssignedOrgs;

View File

@ -0,0 +1,31 @@
import React from "react";
import { useOrganizationModal } from "../../hooks/useOrganization";
import { useSelectedProject } from "../../slices/apiDataManager";
import ProjectAssignedOrgs from "./ProjectOrganization/ProjectAssignedOrgs";
const ProjectOrganizations = () => {
const { onOpen, startStep, flowType } = useOrganizationModal();
const selectedProject = useSelectedProject();
return (
<div className="card">
<div className="card-header">
<div className="d-flex justify-content-end px-2">
<button
type="button"
className="link-button btn btn-xs rounded-md link-button-sm m-1 btn-primary"
onClick={() => onOpen({ startStep: 1, flowType: "assign" })}
>
<i className="bx bx-plus-circle me-2"></i>
Add Organization
</button>
</div>
</div>
<div className="row">
<ProjectAssignedOrgs />
</div>
</div>
);
};
export default ProjectOrganizations;

View File

@ -17,12 +17,11 @@ import eventBus from "../../services/eventBus";
import {
useEmployeesByProjectAllocated,
useManageProjectAllocation,
useProjectAssignedServices,
} from "../../hooks/useProjects";
import { useSelectedProject } from "../../slices/apiDataManager";
const Teams = () => {
// const {projectId} = useParams()
// const projectId = useSelector((store)=>store.localVariables.projectId)
const projectId = useSelectedProject();
const dispatch = useDispatch();
@ -37,6 +36,15 @@ const Teams = () => {
const [activeEmployee, setActiveEmployee] = useState(true);
const [deleteEmployee, setDeleteEmplyee] = useState(null);
const [searchTerm, setSearchTerm] = useState(""); // State for search term
const [selectedService, setSelectedService] = useState(null);
const { data: assignedServices, isLoading: servicesLoading } = useProjectAssignedServices(projectId);
const handleToggleActive = e => setActiveEmployee(e.target.checked);
const handleServiceChange = (e) => {
setSelectedService(e.target.value);
};
const navigate = useNavigate();
@ -46,7 +54,7 @@ const Teams = () => {
projectEmployees,
loading: employeeLodaing,
refetch,
} = useEmployeesByProjectAllocated(projectId);
} = useEmployeesByProjectAllocated(projectId, selectedService);
const {
mutate: submitAllocations,
isPending,
@ -136,7 +144,6 @@ const Teams = () => {
useEffect(() => {
if (projectEmployees) {
setEmployees(projectEmployees);
//setFilteredEmployees(projectEmployees?.filter((emp) => emp.isActive));
const filtered = projectEmployees.filter((emp) => emp.isActive);
setFilteredEmployees(filtered);
}
@ -147,7 +154,6 @@ const Teams = () => {
setEmpJobRoles(data);
}
}, [data]);
const filterAndSearchEmployees = useCallback(() => {
const statusFiltered = employees.filter((emp) =>
activeEmployee ? emp.isActive : !emp.isActive
@ -159,33 +165,31 @@ const Teams = () => {
}
const lowercasedSearchTerm = searchTerm.toLowerCase();
const searchedAndFiltered = statusFiltered.filter((item) => {
const fullName =
`${item.firstName} ${item.middleName} ${item.lastName}`.toLowerCase();
const fullName = `${item.firstName} ${item.middleName} ${item.lastName}`.toLowerCase();
const roleName = getRole(item.jobRoleId).toLowerCase();
const orgName = (item.organizationName || "").toLowerCase();
const serviceName = (item.serviceName || "").toLowerCase();
return (
fullName.includes(lowercasedSearchTerm) ||
roleName.includes(lowercasedSearchTerm)
roleName.includes(lowercasedSearchTerm) ||
orgName.includes(lowercasedSearchTerm) ||
serviceName.includes(lowercasedSearchTerm)
);
});
setFilteredEmployees(searchedAndFiltered);
}, [employees, activeEmployee, searchTerm, getRole]);
useEffect(() => {
filterAndSearchEmployees();
}, [employees, activeEmployee, searchTerm, filterAndSearchEmployees]);
const handleFilterEmployee = (e) => {
const filterValue = e.target.value;
// if (filterValue === "true") {
// setActiveEmployee(true);
// setFilteredEmployees(employees.filter((emp) => emp.isActive));
// } else {
// setFilteredEmployees(employees.filter((emp) => !emp.isActive));
// setActiveEmployee(false);
// }
setActiveEmployee(filterValue === "true");
setSearchTerm("");
};
@ -264,6 +268,46 @@ const Teams = () => {
<div className="card-body">
<div className="row d-flex justify-content-between mb-4">
<div className="col-md-6 col-12 d-flex align-items-center">
<div className="dataTables_length text-start py-1 px-0 col-md-4 col-12">
{!servicesLoading && assignedServices?.length > 0 && (
assignedServices.length > 1 ? (
<label>
<select
name="DataTables_Table_0_length"
aria-controls="DataTables_Table_0"
className="form-select form-select-sm"
aria-label="Select Service"
value={selectedService}
onChange={handleServiceChange}
style={{ fontSize: "0.875rem", height: "35px", width: "190px" }}
>
<option value="">All Services</option>
{assignedServices.map((service) => (
<option key={service.id} value={service.id}>
{service.name}
</option>
))}
</select>
</label>
) : (
<h5>{assignedServices[0].name}</h5>
)
)}
</div>
</div>
<div className="col-md-6 col-12 d-flex justify-content-end align-items-center">
<div className="form-check form-switch me-2 mt-2">
<input
type="checkbox"
className="form-check-input"
checked={activeEmployee}
onChange={handleToggleActive}
id="activeEmployeeSwitch"
/>
<label className="form-check-label ms-0 " htmlFor="activeEmployeeSwitch">
{activeEmployee ? "Active Employees" : "Inactive Employees"}
</label>
</div>
<div className="dataTables_filter d-inline-flex align-items-center ms-2">
<input
type="search"
@ -274,32 +318,11 @@ const Teams = () => {
onChange={handleSearch}
/>
</div>
</div>
<div className="col-md-6 col-12 d-flex justify-content-end align-items-center">
<div
className="dataTables_length text-start py-2 px-2"
id="DataTables_Table_0_length"
>
<label>
<select
name="DataTables_Table_0_length"
aria-controls="DataTables_Table_0"
className="form-select form-select-sm"
onChange={handleFilterEmployee}
// value={false}
aria-label=""
defaultValue="true"
>
<option value="true">Active Employee</option>
<option value="false">In-Active Employee</option>
</select>
</label>
</div>
<button
type="button"
className={`link-button btn-primary btn-sm ${
HasAssignUserPermission ? "" : "d-none"
}`}
className={`link-button btn-primary btn-sm ${HasAssignUserPermission ? "" : "d-none"
}`}
data-bs-toggle="modal"
data-bs-target="#user-model"
>
@ -319,6 +342,8 @@ const Teams = () => {
<th>
<div className="text-start ms-5">Name</div>
</th>
<th>Services</th>
<th>Organization</th>
<th>Assigned Date</th>
{!activeEmployee && <th>Release Date</th>}
<th>Project Role</th>
@ -334,7 +359,7 @@ const Teams = () => {
<Avatar
firstName={item.firstName}
lastName={item.lastName}
></Avatar>
/>
<div className="d-flex flex-column">
<a
onClick={() =>
@ -352,18 +377,17 @@ const Teams = () => {
</div>
</div>
</td>
<td>{item.serviceName || "N/A"}</td>
<td>{item.organizationName || "N/A"}</td>
<td>
{" "}
{moment(item.allocationDate).format(
"DD-MMM-YYYY"
)}{" "}
{moment(item.allocationDate).format("DD-MMM-YYYY")}
</td>
{!activeEmployee && (
<td>
{item.reAllocationDate
? moment(item.reAllocationDate).format(
"DD-MMM-YYYY"
)
? moment(item.reAllocationDate).format("DD-MMM-YYYY")
: "Present"}
</td>
)}
@ -373,7 +397,7 @@ const Teams = () => {
</span>
</td>
<td>
{item.isActive && (
{item.isActive ? (
<button
aria-label="Delete"
type="button"
@ -381,27 +405,26 @@ const Teams = () => {
className="btn p-0 dropdown-toggle hide-arrow"
onClick={() => deleteModalOpen(item)}
>
{" "}
{removingEmployeeId === item.id ? (
<div
className="spinner-border spinner-border-sm text-primary"
role="status"
>
<span className="visually-hidden">
Loading...
</span>
<span className="visually-hidden">Loading...</span>
</div>
) : (
<i className="bx bx-trash me-1 text-danger"></i>
)}
</button>
) : (
<span>Not in project</span>
)}
{!item.isActive && <span>Not in project</span>}
</td>
</tr>
))}
</tbody>
</table>
)}
{!employeeLodaing && filteredEmployees.length === 0 && (
<div className="text-center text-muted py-3">

View File

@ -5,12 +5,12 @@ import GlobalModel from "../common/GlobalModel";
import { useTenantContext } from "../../pages/Tenant/TenantPage";
import { useTenantDetailsContext } from "../../pages/Tenant/TenantDetails";
import IconButton from "../common/IconButton";
import { hasUserPermission } from "../../utils/authUtils";
import { MANAGE_TENANTS } from "../../utils/constants";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
const Profile = ({ data }) => {
const {setEditTenant} = useTenantDetailsContext()
const canUpdateTenant = hasUserPermission(MANAGE_TENANTS)
const canUpdateTenant = useHasUserPermission(MANAGE_TENANTS)
return (
<>
<div className="container-fuid">

View File

@ -203,7 +203,7 @@ const FilterIcon = ({
<>
<li><hr className="my-1" /></li>
<li>
<div className="fw-bold text-dark mb-1">Floors</div>
<div className="fw-bold text-dark mb-2 mt-2">Floors</div>
<div className="row">
{uniqueFloors.length > 0 ? (
uniqueFloors.map((floor, idx) => (
@ -235,7 +235,7 @@ const FilterIcon = ({
<>
<li><hr className="my-1" /></li>
<li>
<div className="fw-bold text-dark mb-1">Activities</div>
<div className="fw-bold text-dark mb-2 mt-2">Activities</div>
<div className="row">
{uniqueActivities.length > 0 ? (
uniqueActivities.map((activity, idx) => (

View File

@ -0,0 +1,50 @@
import { useCallback } from "react";
const Modal = ({
isOpen,
onClose,
title,
body,
disabled,
size = "md",
position = "top",
}) => {
const handleClose = useCallback(() => {
if (disabled) return;
onClose();
}, [disabled, onClose]);
if (!isOpen) return null;
return (
<div
className="modal fade show"
style={{ display: "block", backgroundColor: "rgba(0,0,0,0.6)" }}
tabIndex="-1"
role="dialog"
>
<div
className={`modal-dialog modal-${size} modal-dialog-${position}`}
role="document"
>
<div className="modal-content text-white shadow-lg">
{/* Header */}
<div className="modal-header justify-content-center pb-2 border-0">
<h5 className="modal-title">{title}</h5>
<button
type="button"
className="btn-close btn-close-white"
onClick={handleClose}
aria-label="Close"
></button>
</div>
{/* Body */}
<div className="modal-body pt-0">{body}</div>
</div>
</div>
</div>
);
};
export default Modal;

View File

@ -2,15 +2,17 @@ import React, { useState, useEffect, useRef } from "react";
import { useFormContext } from "react-hook-form";
import { createPortal } from "react-dom";
import "./MultiSelectDropdown.css";
import Label from "./Label";
const SelectMultiple = ({
name,
options = [],
label = "Select options",
labelKey = "name", // Can now be a function or a string
labelKey = "name",
valueKey = "id",
placeholder = "Please select...",
IsLoading = false,
required = false
}) => {
const { setValue, watch } = useFormContext();
const selectedValues = watch(name) || [];
@ -20,7 +22,11 @@ const SelectMultiple = ({
const containerRef = useRef(null);
const dropdownRef = useRef(null);
const [dropdownStyles, setDropdownStyles] = useState({ top: 0, left: 0, width: 0 });
const [dropdownStyles, setDropdownStyles] = useState({
top: 0,
left: 0,
width: 0,
});
useEffect(() => {
const handleClickOutside = (e) => {
@ -100,8 +106,14 @@ const SelectMultiple = ({
return (
<div
key={valueVal}
className={`multi-select-dropdown-option ${isChecked ? "selected" : ""}`}
style={{ display: "flex", alignItems: "center", padding: "4px 8px" }}
className={`multi-select-dropdown-option ${
isChecked ? "selected" : ""
}`}
style={{
display: "flex",
alignItems: "center",
padding: "4px 8px",
}}
>
<input
type="checkbox"
@ -130,8 +142,13 @@ const SelectMultiple = ({
return (
<>
<div ref={containerRef} className="multi-select-dropdown-container" style={{ position: "relative" }}>
<label className="form-label mb-1">{label}</label>
<div
ref={containerRef}
className="multi-select-dropdown-container"
style={{ position: "relative" }}
>
<Label required={required}>{label}</Label>
<div
className="multi-select-dropdown-header"
@ -140,7 +157,9 @@ const SelectMultiple = ({
>
<span
className={
selectedValues.length > 0 ? "placeholder-style-selected" : "placeholder-style"
selectedValues.length > 0
? "placeholder-style-selected"
: "placeholder-style"
}
>
<div className="selected-badges-container">
@ -149,7 +168,10 @@ const SelectMultiple = ({
const found = options.find((opt) => opt[valueKey] === val);
const label = found ? getLabel(found) : "";
return (
<span key={val} className="badge badge-selected-item mx-1 mb-1">
<span
key={val}
className="badge badge-selected-item mx-1 mb-1"
>
{label}
</span>
);

View File

@ -16,6 +16,7 @@ import ManagePaymentMode from "./ManagePaymentMode";
import ManageExpenseStatus from "./ManageExpenseStatus";
import ManageDocumentCategory from "./ManageDocumentCategory";
import ManageDocumentType from "./ManageDocumentType";
import ManageServices from "./Services/ManageServices";
const MasterModal = ({ modaldata, closeModal }) => {
if (!modaldata?.modalType || modaldata.modalType === "delete") {
@ -60,6 +61,12 @@ const MasterModal = ({ modaldata, closeModal }) => {
"Edit-Document Type": (
<ManageDocumentType data={item} onClose={closeModal} />
),
"Services": (
<ManageServices onClose={closeModal} />
),
"Edit-Services": (
<ManageServices data={item} onClose={closeModal} />
),
};
return modalComponents[modalType] || null;

View File

@ -0,0 +1,107 @@
import React, { useEffect } from "react";
import Label from "../../common/Label";
import { useForm } from "react-hook-form";
import { useCreateService, useUpdateService } from "../../../hooks/masterHook/useMaster";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
const schema = z.object({
name: z.string().min(1, { message: "Service Name is required" }),
description: z
.string()
.min(1, { message: "Description is required" })
.max(255, { message: "Description cannot exceed 255 characters" }),
});
const ManageServices = ({ data , onClose }) => {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
defaultValues: { name: "", description: "" },
});
const { mutate: CreateServices, isPending: Creating } = useCreateService(() =>
onClose?.()
);
const { mutate: UpdateServices, isPending: Updating } = useUpdateService(() =>
onClose?.()
);
const onSubmit = (payload) => {
if (data && data.id) {
UpdateServices({ id: data.id, payload: { ...payload, id: data.id } });
} else {
CreateServices(payload);
}
};
useEffect(() => {
if (data) {
reset({
name: data.name ?? "",
description: data.description ?? "",
});
}
}, [data, reset]);
return (
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12 text-start">
<Label className="form-label" required>
Service Name
</Label>
<input
type="text"
{...register("name")}
className={`form-control ${errors.name ? "is-invalid" : ""}`}
/>
{errors.name && <p className="danger-text">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12 text-start">
<Label className="form-label" htmlFor="description" required>
Description
</Label>
<textarea
rows="3"
{...register("description")}
className={`form-control ${errors.description ? "is-invalid" : ""}`}
></textarea>
{errors.description && (
<p className="danger-text">{errors.description.message}</p>
)}
</div>
<div className="col-12 text-end">
<button
type="reset"
className="btn btn-sm btn-label-secondary me-3"
data-bs-dismiss="modal"
aria-label="Close"
disabled={Creating || Updating}
>
Cancel
</button>
<button
type="submit"
className="btn btn-sm btn-primary"
disabled={Creating || Updating}
>
{Creating
? "Please Wait..."
: Updating
? "Please Wait..."
: data
? "Update"
: "Submit"}
</button>
</div>
</form>
);
};
export default ManageServices;

View File

@ -0,0 +1,9 @@
import { z } from "zod";
const schema = z.object({
name: z.string().min(1, { message: "Service Name is required" }),
description: z
.string()
.min(1, { message: "Description is required" })
.max(255, { message: "Description cannot exceed 255 characters" }),
});

View File

@ -9,7 +9,18 @@ import showToast from "../../services/toastService";
export const useServices = ()=>{
return useQuery({
queryKey:["services"],
queryFn:async()=> await MasterRespository.getMasterServices()
})
}
export const useGlobalServices = ()=>{
return useQuery({
queryKey:["globalServices"],
queryFn:async()=> await MasterRespository.getGlobalServices()
})
}
export const useMasterMenu = ()=>{
return useQuery({
@ -221,6 +232,12 @@ const {
return { DocumentCategories, isError, isLoading, error };
}
export const useOrganizationType =()=>{
return useQuery({
queryKey:["orgType"],
queryFn:async()=>await MasterRespository.getOrganizationType()
})
}
// ===Application Masters Query=================================================
const fetchMasterData = async (masterType) => {
@ -231,6 +248,8 @@ const fetchMasterData = async (masterType) => {
return (await MasterRespository.getJobRole()).data;
case "Activity":
return (await MasterRespository.getActivites()).data;
case "Services":
return (await MasterRespository.getService()).data;
case "Work Category":
return (await MasterRespository.getWorkCategory()).data;
case "Contact Category":
@ -667,6 +686,7 @@ export const useCreatePaymentMode = (onSuccessCallback)=>{
}
})
}
export const useUpdatePaymentMode = (onSuccessCallback)=>{
const queryClient = useQueryClient();
@ -689,6 +709,81 @@ export const useUpdatePaymentMode = (onSuccessCallback)=>{
})
}
// Services-------------------------------
// export const useCreateService = (onSuccessCallback) => {
// const queryClient = useQueryClient();
// return useMutation({
// mutationFn: async (payload) => {
// const resp = await MasterRespository.createService(payload);
// return resp.data; // full API response
// },
// onSuccess: (data) => {
// // Invalidate & refetch service list
// queryClient.invalidateQueries({ queryKey: ["masterData", "Services"] });
// showToast(data?.message || "Service added successfully", "success");
// if (onSuccessCallback) onSuccessCallback(data?.data); // pass back new service object
// },
// onError: (error) => {
// showToast(error.message || "Something went wrong", "error");
// },
// });
// };
export const useCreateService = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload) => {
debugger;
const resp = await MasterRespository.createService(payload);
debugger;
return resp.data;
},
onSuccess: (data) => {
debugger;
queryClient.invalidateQueries({ queryKey: ["masterData", "Services"] });
showToast(data?.message || "Service added successfully", "success");
if (onSuccessCallback) onSuccessCallback(data?.data);
},
onError: (error) => {
debugger;
showToast(error.message || "Something went wrong", "error");
},
});
};
export const useUpdateService = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, payload }) => {
const response = await MasterRespository.updateService(id, payload);
return response; // full response since it already has { success, message, data }
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({
queryKey: ["masterData", "Services"],
});
showToast(data.message || "Service updated successfully.", "success");
if (onSuccessCallback) onSuccessCallback(data);
},
onError: (error) => {
showToast(error?.message || "Something went wrong", "error");
},
});
};
// -------------------Expense Status----------------------------------
export const useCreateExpenseStatus =(onSuccessCallback)=>{
const queryClient = useQueryClient();

View File

@ -12,46 +12,76 @@ import { setDefaultDateRange } from "../slices/localVariablesSlice";
// ----------------------------Query-----------------------------
export const useAttendance = (projectId) => {
const dispatch = useDispatch()
// export const useAttendance = (projectId) => {
// const dispatch = useDispatch()
// const {
// data: attendance = [],
// isLoading: loading,
// error,
// refetch: recall,
// isFetching
// } = useQuery({
// queryKey: ["attendance", projectId],
// queryFn: async () => {
// const response = await AttendanceRepository.getAttendance(projectId);
// return response.data;
// },
// enabled: !!projectId,
// onError: (error) => {
// showToast(error.message || "Error while fetching Attendance", "error");
// },
// });
// return {
// attendance,
// loading,
// error,
// recall,
// isFetching
// };
// };
export const useAttendance = (projectId, organizationId, includeInactive = false, date = null) => {
const dispatch = useDispatch();
const {
data: attendance = [],
isLoading: loading,
error,
refetch: recall,
isFetching
isFetching,
} = useQuery({
queryKey: ["attendance", projectId],
queryKey: ["attendance", projectId, organizationId, includeInactive, date], // include filters in cache key
queryFn: async () => {
const response = await AttendanceRepository.getAttendance(projectId);
const response = await AttendanceRepository.getAttendance(
projectId,
organizationId,
includeInactive,
date
);
return response.data;
},
enabled: !!projectId,
enabled: !!projectId, // only run if projectId exists
onError: (error) => {
showToast(error.message || "Error while fetching Attendance", "error");
},
});
return {
attendance,
loading,
error,
recall,
isFetching
};
return { attendance, loading, error, recall, isFetching };
};
export const useAttendancesLogs = (projectId, fromDate, toDate) => {
export const useAttendancesLogs = (projectId, fromDate, toDate,organizationId) => {
const dispatch = useDispatch();
const enabled = !!projectId && !!fromDate && !!toDate;
const query = useQuery({
queryKey: ['attendanceLogs', projectId, fromDate, toDate],
queryKey: ['attendanceLogs', projectId, fromDate, toDate,organizationId],
queryFn: async () => {
const res = await AttendanceRepository.getAttendanceFilteredByDate(
projectId,
fromDate,
toDate
toDate,
organizationId
);
return res.data;
},
@ -112,30 +142,58 @@ export const useAttendanceByEmployee = (employeeId, fromDate, toDate) => {
});
};
export const useRegularizationRequests = (projectId) => {
// export const useRegularizationRequests = (projectId) => {
// const {
// data: regularizes = [],
// isLoading: loading,
// error,
// refetch,
// } = useQuery({
// queryKey: ["regularizedList", projectId],
// queryFn: async () => {
// const response = await AttendanceRepository.getRegularizeList(projectId);
// return response.data;
// },
// enabled: !!projectId,
// onError: (error) => {
// showToast(error.message || "Error while fetching Regularization Requests", "error");
// },
// });
// return {
// regularizes,
// loading,
// error,
// refetch,
// };
// };
export const useRegularizationRequests = (projectId, organizationId, IncludeInActive = false) => {
const dispatch = useDispatch();
const {
data: regularizes = [],
isLoading: loading,
error,
refetch,
refetch: recall,
isFetching,
} = useQuery({
queryKey: ["regularizedList", projectId],
queryKey: ["regularizedList", projectId, organizationId, IncludeInActive], // include filters in cache key
queryFn: async () => {
const response = await AttendanceRepository.getRegularizeList(projectId);
const response = await AttendanceRepository.getRegularizeList(
projectId,
organizationId,
IncludeInActive,
);
return response.data;
},
enabled: !!projectId,
enabled: !!projectId, // only run if projectId exists
onError: (error) => {
showToast(error.message || "Error while fetching Regularization Requests", "error");
showToast(error.message || "Error while fetching regularizes", "error");
},
});
return {
regularizes,
loading,
error,
refetch,
};
return { regularizes, loading, error, recall, isFetching };
};

83
src/hooks/useAuth.jsx Normal file
View File

@ -0,0 +1,83 @@
import { useState, useEffect, useCallback } from "react";
import {
Mutation,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { Link, useNavigate } from "react-router-dom";
import AuthRepository from "../repositories/AuthRepository.jsx";
import { useDispatch, useSelector } from "react-redux";
import {
closeAuthModal,
openAuthModal,
} from "../slices/localVariablesSlice.jsx";
export const useTenants = () => {
return useQuery({
queryKey: ["tenantlist"],
queryFn: async () => await AuthRepository.getTenantList(),
});
};
export const useSelectTenant = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (tenantId) => {
const res = await AuthRepository.selectTenant(tenantId);
return res.data;
},
onSuccess: (data) => {
localStorage.setItem("jwtToken", data.token);
localStorage.setItem("refreshToken", data.refreshToken);
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(error.message || "Error while creating project", "error");
localStorage.removeItem("jwtToken");
localStorage.removeItem("refreshToken")
localStorage.removeItem("ctnt")
},
});
};
export const useAuthModal = () => {
const dispatch = useDispatch();
const { isOpen } = useSelector((state) => state.localVariables.AuthModal);
return {
isOpen,
onOpen: () => dispatch(openAuthModal()),
onClose: () => dispatch(closeAuthModal()),
};
};
export const useLogout = ()=>{
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
let payload = {refreshToken: localStorage.getItem("refreshToken")}
return await AuthRepository.logout(payload);
},
onSuccess: (data) => {
localStorage.removeItem("jwtToken");
localStorage.removeItem("refreshToken");
localStorage.removeItem("ctnt");
localStorage.clear();
window.location.href = "/auth/login";
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(error.message || "Error while creating project", "error");
localStorage.removeItem("jwtToken");
localStorage.removeItem("refreshToken")
localStorage.removeItem("ctnt")
},
});
}

View File

@ -1,7 +1,8 @@
import { useState, useEffect } from "react";
import GlobalRepository from "../repositories/GlobalRepository";
import { useQuery } from "@tanstack/react-query";
// 🔹 Dashboard Progression Data Hook
export const useDashboard_Data = ({ days, FromDate, projectId }) => {
const [dashboard_data, setDashboard_Data] = useState([]);
const [isLineChartLoading, setLoading] = useState(false);
@ -38,120 +39,120 @@ export const useDashboard_Data = ({ days, FromDate, projectId }) => {
};
export const useDashboard_AttendanceData = (date, projectId) => {
const [dashboard_Attendancedata, setDashboard_AttendanceData] = useState([]);
const [isLineChartLoading, setLoading] = useState(false);
const [error, setError] = useState("");
// export const useDashboard_AttendanceData = (date, projectId) => {
// const [dashboard_Attendancedata, setDashboard_AttendanceData] = useState([]);
// const [isLineChartLoading, setLoading] = useState(false);
// const [error, setError] = useState("");
useEffect(() => {
const fetchData = async () => {
setLoading(true);
setError("");
// useEffect(() => {
// const fetchData = async () => {
// setLoading(true);
// setError("");
try {
const response = await GlobalRepository.getDashboardAttendanceData(date, projectId); // date in 2nd param
setDashboard_AttendanceData(response.data);
} catch (err) {
setError("Failed to fetch dashboard data.");
console.error(err);
} finally {
setLoading(false);
}
};
// try {
// const response = await GlobalRepository.getDashboardAttendanceData(date, projectId); // date in 2nd param
// setDashboard_AttendanceData(response.data);
// } catch (err) {
// setError("Failed to fetch dashboard data.");
// console.error(err);
// } finally {
// setLoading(false);
// }
// };
if (date && projectId !== null) {
fetchData();
}
}, [date, projectId]);
// if (date && projectId !== null) {
// fetchData();
// }
// }, [date, projectId]);
return { dashboard_Attendancedata, isLineChartLoading: isLineChartLoading, error };
};
// return { dashboard_Attendancedata, isLineChartLoading: isLineChartLoading, error };
// };
// 🔹 Dashboard Projects Card Data Hook
export const useDashboardProjectsCardData = () => {
const [projectsCardData, setProjectsData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
// export const useDashboardProjectsCardData = () => {
// const [projectsCardData, setProjectsData] = useState([]);
// const [loading, setLoading] = useState(false);
// const [error, setError] = useState("");
useEffect(() => {
const fetchProjectsData = async () => {
setLoading(true);
setError("");
// useEffect(() => {
// const fetchProjectsData = async () => {
// setLoading(true);
// setError("");
try {
const response = await GlobalRepository.getDashboardProjectsCardData();
setProjectsData(response.data);
} catch (err) {
setError("Failed to fetch projects card data.");
console.error(err);
} finally {
setLoading(false);
}
};
// try {
// const response = await GlobalRepository.getDashboardProjectsCardData();
// setProjectsData(response.data);
// } catch (err) {
// setError("Failed to fetch projects card data.");
// console.error(err);
// } finally {
// setLoading(false);
// }
// };
fetchProjectsData();
}, []);
// fetchProjectsData();
// }, []);
return { projectsCardData, loading, error };
};
// return { projectsCardData, loading, error };
// };
// 🔹 Dashboard Teams Card Data Hook
export const useDashboardTeamsCardData = (projectId) => {
const [teamsCardData, setTeamsData] = useState({});
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
// export const useDashboardTeamsCardData = (projectId) => {
// const [teamsCardData, setTeamsData] = useState({});
// const [loading, setLoading] = useState(false);
// const [error, setError] = useState("");
useEffect(() => {
const fetchTeamsData = async () => {
setLoading(true);
setError("");
// useEffect(() => {
// const fetchTeamsData = async () => {
// setLoading(true);
// setError("");
try {
const response = await GlobalRepository.getDashboardTeamsCardData(projectId);
setTeamsData(response.data || {});
} catch (err) {
setError("Failed to fetch teams card data.");
console.error("Error fetching teams card data:", err);
setTeamsData({});
} finally {
setLoading(false);
}
};
// try {
// const response = await GlobalRepository.getDashboardTeamsCardData(projectId);
// setTeamsData(response.data || {});
// } catch (err) {
// setError("Failed to fetch teams card data.");
// console.error("Error fetching teams card data:", err);
// setTeamsData({});
// } finally {
// setLoading(false);
// }
// };
fetchTeamsData();
}, [projectId]);
// fetchTeamsData();
// }, [projectId]);
return { teamsCardData, loading, error };
};
// return { teamsCardData, loading, error };
// };
export const useDashboardTasksCardData = (projectId) => {
const [tasksCardData, setTasksData] = useState({});
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
// export const useDashboardTasksCardData = (projectId) => {
// const [tasksCardData, setTasksData] = useState({});
// const [loading, setLoading] = useState(false);
// const [error, setError] = useState("");
useEffect(() => {
const fetchTasksData = async () => {
setLoading(true);
setError("");
// useEffect(() => {
// const fetchTasksData = async () => {
// setLoading(true);
// setError("");
try {
const response = await GlobalRepository.getDashboardTasksCardData(projectId);
setTasksData(response.data);
} catch (err) {
setError("Failed to fetch tasks card data.");
console.error(err);
setTasksData({});
} finally {
setLoading(false);
}
};
// try {
// const response = await GlobalRepository.getDashboardTasksCardData(projectId);
// setTasksData(response.data);
// } catch (err) {
// setError("Failed to fetch tasks card data.");
// console.error(err);
// setTasksData({});
// } finally {
// setLoading(false);
// }
// };
fetchTasksData();
}, [projectId]);
// fetchTasksData();
// }, [projectId]);
return { tasksCardData, loading, error };
};
// return { tasksCardData, loading, error };
// };
export const useAttendanceOverviewData = (projectId, days) => {
@ -180,3 +181,75 @@ export const useAttendanceOverviewData = (projectId, days) => {
return { attendanceOverviewData, loading, error };
};
// -------------------Query----------------------------
// export const useDashboard_Data = (days, FromDate, projectId)=>{
// return useQuery({
// queryKey:["dashboardProjectProgress"],
// queryFn:async()=> {
// const payload = {
// days,
// FromDate: FromDate || '',
// projectId: projectId || null,
// };
// const resp = await GlobalRepository.getDashboardProgressionData(payload);
// return resp.data;
// }
// })
// }
export const useDashboard_AttendanceData = (date,projectId)=>{
return useQuery({
queryKey:["dashboardAttendances",date,projectId],
queryFn:async()=> {
const resp = await await GlobalRepository.getDashboardAttendanceData(date, projectId)
return resp.data;
}
})
}
export const useDashboardTeamsCardData =(projectId)=>{
return useQuery({
queryKey:["dashboardTeams",projectId],
queryFn:async()=> {
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId)
return resp.data;
}
})
}
export const useDashboardTasksCardData = (projectId) => {
return useQuery({
queryKey:["dashboardTasks",projectId],
queryFn:async()=> {
const resp = await GlobalRepository.getDashboardTasksCardData(projectId)
return resp.data;
}
})
}
// export const useAttendanceOverviewData = (projectId, days) => {
// return useQuery({
// queryKey:["dashboardAttendanceOverView",projectId],
// queryFn:async()=> {
// const resp = await GlobalRepository.getAttendanceOverview(projectId, days);
// return resp.data;
// }
// })
// }
export const useDashboardProjectsCardData = () => {
return useQuery({
queryKey:["dashboardProjects"],
queryFn:async()=> {
const resp = await GlobalRepository.getDashboardProjectsCardData();
return resp.data;
}
})
}

View File

@ -0,0 +1,161 @@
import { useSelector, useDispatch } from "react-redux";
import {
toggleOrgModal,
openOrgModal,
closeOrgModal,
} from "../slices/localVariablesSlice";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import OrganizationRepository from "../repositories/OrganizationRespository";
import showToast from "../services/toastService";
export const useOrganizationModal = () => {
const dispatch = useDispatch();
const { isOpen, orgData, startStep, prevStep, flowType } = useSelector(
(state) => state.localVariables.OrganizationModal
);
return {
isOpen,
orgData,
startStep,
prevStep,
flowType,
onOpen: (options = {}) =>
dispatch(
openOrgModal({
isOpen: true,
orgData: options.orgData ?? orgData ?? null,
startStep: options.startStep ?? startStep ?? 1,
prevStep: options.prevStep ?? prevStep ?? 1,
flowType: options.flowType ?? flowType ?? "default",
})
),
onClose: () => dispatch(closeOrgModal()),
onToggle: () => dispatch(toggleOrgModal()),
};
};
export const useOrganizationBySPRID = (sprid) => {
return useQuery({
queryKey: ["organization by", sprid],
queryFn: async () => {
const resp = await OrganizationRepository.getOrganizationBySPRID(sprid);
return resp.data;
},
enabled: !!sprid,
});
};
export const useOrganizationsList = (
pageSize,
pageNumber,
active,
sprid,
searchString = ""
) => {
return useQuery({
queryKey: [
"organizationList",
pageSize,
pageNumber,
active,
sprid,
searchString,
],
queryFn: async () => {
const resp = await OrganizationRepository.getOrganizationList(
pageSize,
pageNumber,
active,
sprid,
searchString
);
return resp.data;
},
keepPreviousData: true,
});
};
export const useCreateOrganization = (onSuccessCallback) => {
const useClient = useQueryClient();
return useMutation({
mutationFn: async (OrgPayload) =>
await OrganizationRepository.createOrganization(OrgPayload),
onSuccess: (_, variables) => {
useClient.invalidateQueries({ queryKey: ["organizationList"] });
showToast("Organization created successfully", "success");
if (onSuccessCallback) onSuccessCallback();
},
onError: (error) => {
showToast(
error.response.data.message ||
error.message ||
"Something went wrong please try again !",
"error"
);
},
});
};
export const useAssignOrgToProject = (onSuccessCallback) => {
const useClient = useQueryClient();
return useMutation({
mutationFn: async (payload) =>
await OrganizationRepository.assignOrganizationToProject(payload),
onSuccess: (_, variables) => {
useClient.invalidateQueries({
queryKey: ["projectAssignedOrganiztions"],
});
showToast("Organization successfully", "success");
if (onSuccessCallback) onSuccessCallback();
},
onError: (error) => {
showToast(
error.response.data.message ||
error.message ||
"Something went wrong please try again !",
"error"
);
},
});
};
export const useAssignOrgToTenant = (onSuccessCallback) => {
const useClient = useQueryClient();
return useMutation({
mutationFn: async (payload) =>
await OrganizationRepository.assignOrganizationToTenanat(payload),
onSuccess: (_, variables) => {
useClient.invalidateQueries({ queryKey: ["organizationList"] });
showToast("Organization added successfully", "success");
if (onSuccessCallback) onSuccessCallback();
},
onError: (error) => {
showToast(
error.response.data.message ||
error.message ||
"Something went wrong please try again !",
"error"
);
},
});
};
export const useUpdateOrganization = () => {
const useClient = useQueryClient();
return useMutation({
mutationFn: async (payload) =>
await OrganizationRepository.assignOrganizationToProject(payload),
onSuccess: (_, variables) => {
// useClient.invalidateQueries({ queryKey: ["organizationList"] });
showToast("Organization successfully", "success");
if (onSuccessCallback) onSuccessCallback();
},
onError: (error) => {
showToast(
error.response.data.message ||
error.message ||
"Something went wrong please try again !",
"error"
);
},
});
};

View File

@ -41,22 +41,30 @@ export const useProjects = () => {
};
};
export const useEmployeesByProjectAllocated = (selectedProject) => {
export const useEmployeesByProjectAllocated = (
projectId,
serviceId,
organizationId,
) => {
const {
data = [],
isLoading,
refetch,
error,
} = useQuery({
queryKey: ["empListByProjectAllocated", selectedProject],
queryKey: ["empListByProjectAllocated", projectId, serviceId,organizationId],
queryFn: async () => {
const res = await ProjectRepository.getProjectAllocation(selectedProject);
return res.data || res;
const res = await ProjectRepository.getProjectAllocation(
projectId,
organizationId,
serviceId
);
return res?.data || res;
},
enabled: !!selectedProject,
enabled: !!projectId,
onError: (error) => {
showToast(
error.message || "Error while Fetching project Allocated Employees",
error.message || "Error while fetching project allocated employees",
"error"
);
},
@ -177,7 +185,7 @@ export const useProjectInfra = (projectId) => {
data: projectInfra,
isLoading,
error,
isFetched
isFetched,
} = useQuery({
queryKey: ["ProjectInfra", projectId],
queryFn: async () => {
@ -191,26 +199,23 @@ export const useProjectInfra = (projectId) => {
},
});
return { projectInfra, isLoading, error,isFetched };
return { projectInfra, isLoading, error, isFetched };
};
export const useProjectTasks = (workAreaId, IsExpandedArea = false) => {
const {
data: ProjectTaskList,
isLoading,
error,
} = useQuery({
queryKey: ["WorkItems", workAreaId],
export const useProjectTasks = (workAreaId, serviceId = null, isExpandedArea = false) => {
const { data, isLoading, error } = useQuery({
queryKey: ["WorkItems", workAreaId, serviceId],
queryFn: async () => {
const res = await ProjectRepository.getProjectTasksByWorkArea(workAreaId);
return res.data;
const res = await ProjectRepository.getProjectTasksByWorkArea(workAreaId, serviceId);
return res.data; // return actual task list
},
enabled: !!workAreaId && !!IsExpandedArea,
onError: (error) => {
showToast(error.message || "Error while Fetching project Tasks", "error");
enabled: !!workAreaId && isExpandedArea, // only fetch if workAreaId exists and area is expanded
onError: (err) => {
showToast(err.message || "Error while fetching project tasks", "error");
},
});
return { ProjectTaskList, isLoading, error };
return { ProjectTaskList: data, isLoading, error };
};
export const useProjectTasksByEmployee = (employeeId, fromDate, toDate) => {
@ -268,6 +273,31 @@ export const useProjectLevelEmployeePermission = (employeeId, projectId) => {
});
};
export const useProjectAssignedOrganizations = (projectId) => {
return useQuery({
queryKey: ["projectAssignedOrganiztions", projectId],
queryFn: async () => {
const resp = await ProjectRepository.getProjectAssignedOrganizations(
projectId
);
return resp.data;
},
enabled: !!projectId,
});
};
export const useProjectAssignedServices = (projectId) => {
return useQuery({
queryKey: ["projectAssignedOrganization", projectId],
queryFn: async () => {
const resp = await ProjectRepository.getProjectAssignedServices(
projectId
);
return resp.data;
},
enabled: !!projectId,
});
};
// -- -------------Mutation-------------------------------
export const useCreateProject = ({ onSuccessCallback }) => {

View File

@ -2,11 +2,9 @@
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
// import { MasterDataProvider } from "./provider/MasterDataContext";
import { Provider } from 'react-redux';
import { store } from './store/store';
import { ModalProvider } from './ModalContext.jsx';
import { ChangePasswordProvider } from './components/Context/ChangePasswordContext.jsx';
import { ModalProvider1 } from './components/ImageGallery/ModalContext.jsx';
@ -15,15 +13,12 @@ createRoot(document.getElementById('root')!).render(
// <StrictMode>
// <MasterDataProvider>
<Provider store={ store }>
<ModalProvider>
<ChangePasswordProvider >
<ModalProvider1>
<App />
</ModalProvider1>
</ChangePasswordProvider>
</ModalProvider>
</Provider>
// </MasterDataProvider>
// </StrictMode>,
)

View File

@ -13,11 +13,10 @@ import Regularization from "../../components/Activities/Regularization";
import { useAttendance } from "../../hooks/useAttendance";
import { useDispatch, useSelector } from "react-redux";
import { setProjectId } from "../../slices/localVariablesSlice";
import { hasUserPermission } from "../../utils/authUtils";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { REGULARIZE_ATTENDANCE } from "../../utils/constants";
import eventBus from "../../services/eventBus";
import { useProjectName } from "../../hooks/useProjects";
import { useProjectAssignedOrganizations, useProjectName } from "../../hooks/useProjects";
import GlobalModel from "../../components/common/GlobalModel";
import CheckCheckOutmodel from "../../components/Activities/CheckCheckOutForm";
import AttendLogs from "../../components/Activities/AttendLogs";
@ -39,6 +38,13 @@ const AttendancePage = () => {
const [modelConfig, setModelConfig] = useState();
const DoRegularized = useHasUserPermission(REGULARIZE_ATTENDANCE);
const { projectNames, loading: projectLoading, fetchData } = useProjectName();
const [appliedFilters, setAppliedFilters] = useState({
selectedOrganization: "",
selectedServices: [],
});
const { data: organizations = [], isLoading: orgLoading } =
useProjectAssignedOrganizations(selectedProject);
const [formData, setFormData] = useState({
markTime: "",
@ -95,11 +101,11 @@ const AttendancePage = () => {
{(modelConfig?.action === 0 ||
modelConfig?.action === 1 ||
modelConfig?.action === 2) && (
<CheckCheckOutmodel
modeldata={modelConfig}
closeModal={closeModal}
/>
)}
<CheckCheckOutmodel
modeldata={modelConfig}
closeModal={closeModal}
/>
)}
{/* For view logs */}
{modelConfig?.action === 6 && (
<AttendLogs Id={modelConfig?.id} closeModal={closeModal} />
@ -128,9 +134,8 @@ const AttendancePage = () => {
<li className="nav-item">
<button
type="button"
className={`nav-link ${
activeTab === "all" ? "active" : ""
} fs-6`}
className={`nav-link ${activeTab === "all" ? "active" : ""
} fs-6`}
onClick={() => handleTabChange("all")}
data-bs-toggle="tab"
data-bs-target="#navs-top-home"
@ -141,9 +146,8 @@ const AttendancePage = () => {
<li className="nav-item">
<button
type="button"
className={`nav-link ${
activeTab === "logs" ? "active" : ""
} fs-6`}
className={`nav-link ${activeTab === "logs" ? "active" : ""
} fs-6`}
onClick={() => handleTabChange("logs")}
data-bs-toggle="tab"
data-bs-target="#navs-top-profile"
@ -155,9 +159,8 @@ const AttendancePage = () => {
<li className={`nav-item ${!DoRegularized ? "d-none" : ""}`}>
<button
type="button"
className={`nav-link ${
activeTab === "regularization" ? "active" : ""
} fs-6`}
className={`nav-link ${activeTab === "regularization" ? "active" : ""
} fs-6`}
onClick={() => handleTabChange("regularization")}
data-bs-toggle="tab"
data-bs-target="#navs-top-messages"
@ -168,8 +171,30 @@ const AttendancePage = () => {
</ul>
</div>
{/* Single search input that moves */}
<div className="col-12 col-md-auto mt-2 mt-md-0 ms-md-auto px-2">
{/* Search + Organization filter */}
<div className="col-12 col-md-auto mt-2 mt-md-0 ms-md-auto d-flex gap-2 align-items-center">
{/* Organization Dropdown */}
<select
className="form-select form-select-sm"
style={{ minWidth: "180px" }}
value={appliedFilters.selectedOrganization}
onChange={(e) =>
setAppliedFilters((prev) => ({
...prev,
selectedOrganization: e.target.value,
}))
}
disabled={orgLoading}
>
<option value="">All Organizations</option>
{organizations?.map((org) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</select>
{/* Search Input */}
<input
type="text"
className="form-control form-control-sm"
@ -179,6 +204,8 @@ const AttendancePage = () => {
style={{ minWidth: "200px" }}
/>
</div>
</div>
</div>
@ -191,6 +218,7 @@ const AttendancePage = () => {
handleModalData={handleModalData}
getRole={getRole}
searchTerm={searchTerm}
organizationId={appliedFilters.selectedOrganization}
/>
</div>
)}
@ -199,12 +227,16 @@ const AttendancePage = () => {
<AttendanceLog
handleModalData={handleModalData}
searchTerm={searchTerm}
organizationId={appliedFilters.selectedOrganization}
/>
</div>
)}
{activeTab === "regularization" && DoRegularized && (
<div className="tab-pane fade show active py-0">
<Regularization searchTerm={searchTerm} />
<Regularization
searchTerm={searchTerm}
organizationId={appliedFilters.selectedOrganization}
/>
</div>
)}
</>

View File

@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from "react";
import { useDispatch } from "react-redux";
import { useTaskList } from "../../hooks/useTasks";
import { useProjectName } from "../../hooks/useProjects";
import { useProjectAssignedServices, useProjectName } from "../../hooks/useProjects";
import { setProjectId } from "../../slices/localVariablesSlice";
import Breadcrumb from "../../components/common/Breadcrumb";
import DateRangePicker from "../../components/common/DateRangePicker";
@ -24,6 +24,14 @@ const DailyTask = () => {
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK);
const { data: assignedServices, isLoading: servicesLoading } = useProjectAssignedServices(selectedProject);
const [selectedService, setSelectedService] = useState("");
const handleServiceChange = (e) => {
setSelectedService(e.target.value);
};
const [filters, setFilters] = useState({
selectedBuilding: "",
selectedFloors: [],
@ -38,7 +46,6 @@ const DailyTask = () => {
dateRange?.endDate || null
);
// Ensure project is set
useEffect(() => {
if (!selectedProject && projectNames.length > 0) {
debugger
@ -46,7 +53,6 @@ const DailyTask = () => {
}
}, [selectedProject, projectNames, dispatch]);
// 🔹 Reset filters when project changes
useEffect(() => {
setFilters({
selectedBuilding: "",
@ -55,7 +61,6 @@ const DailyTask = () => {
});
}, [selectedProject]);
// Memoized filtering
const filteredTasks = useMemo(() => {
if (!TaskList) return [];
return TaskList.filter((task) => {
@ -69,7 +74,6 @@ const DailyTask = () => {
});
}, [TaskList, filters]);
// Memoized dates
const groupedTasks = useMemo(() => {
const groups = {};
filteredTasks.forEach((task) => {
@ -82,13 +86,11 @@ const DailyTask = () => {
.map((date) => ({ date, tasks: groups[date] }));
}, [filteredTasks]);
// --- Modal State
const [modal, setModal] = useState({ type: null, data: null });
const openModal = (type, data = null) => setModal({ type, data });
const closeModal = () => setModal({ type: null, data: null });
// --- Render helpers
const renderTeamMembers = (task, refIndex) => (
<div
key={refIndex}
@ -133,7 +135,6 @@ const DailyTask = () => {
return (
<>
{/* --- Modals --- */}
{modal.type === "report" && (
<GlobalModel isOpen size="md" closeModal={closeModal}>
<ReportTask report={modal.data} closeModal={closeModal} />
@ -161,22 +162,60 @@ const DailyTask = () => {
<div className="container-fluid">
<Breadcrumb data={[{ label: "Home", link: "/dashboard" }, { label: "Daily Progress Report" }]} />
<div className="card card-action mb-6">
<div className="card card-action mb-6 p-5">
<div className="card-body p-1 p-sm-2">
{!selectedProject && (<div className="text-center text-muted">Please Select Project</div>)}
{/* --- Filters --- */}
<div className="d-flex align-items-center mb-2">
<DateRangePicker onRangeChange={setDateRange} endDateMode="today" DateDifference="6" dateFormat="DD-MM-YYYY" />
<FilterIcon
taskListData={TaskList}
onApplyFilters={setFilters}
currentSelectedBuilding={filters.selectedBuilding}
currentSelectedFloors={filters.selectedFloors}
currentSelectedActivities={filters.selectedActivities}
selectedProject={selectedProject}
/>
</div>
<div className="d-flex align-items-center justify-content-between mb-2">
{/* --- Left: Service Dropdown + Filter Icon --- */}
<div className="d-flex align-items-center gap-6">
<div className="me-3">
{!servicesLoading && assignedServices?.length > 0 && (
assignedServices.length > 1 ? (
<label>
<select
name="DataTables_Table_0_length"
aria-controls="DataTables_Table_0"
className="form-select form-select-sm"
aria-label="Select Service"
value={selectedService}
onChange={handleServiceChange}
style={{ fontSize: "0.875rem", height: "35px", width: "190px" }}
>
<option value="">All Services</option>
{assignedServices.map((service) => (
<option key={service.id} value={service.id}>
{service.name}
</option>
))}
</select>
</label>
) : (
<h5>{assignedServices[0].name}</h5>
)
)}
</div>
</div>
{/* --- Right: DateRangePicker --- */}
<div className="d-flex justify-content-end align-items-center gap-3 me-3">
<FilterIcon
taskListData={TaskList}
onApplyFilters={setFilters}
currentSelectedBuilding={filters.selectedBuilding}
currentSelectedFloors={filters.selectedFloors}
currentSelectedActivities={filters.selectedActivities}
selectedProject={selectedProject}
/>
<DateRangePicker
onRangeChange={setDateRange}
endDateMode="today"
DateDifference="6"
dateFormat="DD-MM-YYYY"
/>
</div>
</div>
{/* --- Table --- */}
<div className="table-responsive text-nowrap mt-3" style={{ minHeight: "200px" }}>
<table className="table">
@ -241,11 +280,10 @@ const DailyTask = () => {
</tbody>
</table>
</div>
</div>
</div>
</div>
</>
);
};
export default DailyTask;
export default DailyTask;

View File

@ -1,22 +1,31 @@
import React,{useEffect,useRef} from "react";
import React, { useEffect, useState } from "react";
import Breadcrumb from "../../components/common/Breadcrumb";
import InfraPlanning from "../../components/Activities/InfraPlanning";
import { useProjectName } from "../../hooks/useProjects";
import { useDispatch, useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { setProjectId } from "../../slices/localVariablesSlice";
import { useSelectedProject } from "../../slices/apiDataManager";
import { useProjectAssignedServices } from "../../hooks/useProjects";
const TaskPlannng = () => {
const selectedProject = useSelectedProject();
const dispatch = useDispatch();
const { projectNames = [], loading: projectLoading } = useProjectName();
const selectedProject = useSelectedProject();
const dispatch = useDispatch();
const { projectNames = [], loading: projectLoading } = useProjectName();
useEffect(() => {
if (!selectedProject) {
dispatch(setProjectId(projectNames[0]?.id));
}
}, [projectNames, selectedProject?.id, dispatch]);
// Service dropdown state
const { data: assignedServices, isLoading: servicesLoading } =
useProjectAssignedServices(selectedProject);
const [selectedService, setSelectedService] = useState("");
useEffect(() => {
if (!selectedProject && projectNames?.length > 0) {
dispatch(setProjectId(projectNames[0]?.id));
}
}, [projectNames, selectedProject, dispatch]);
const handleServiceChange = (e) => {
setSelectedService(e.target.value);
};
return (
<div className="container-fluid">
@ -26,11 +35,50 @@ useEffect(() => {
{ label: "Daily Task Planning" },
]}
/>
{selectedProject ? (
<InfraPlanning />
) : (
<div className="text-center">Please Select Project</div>
)}
<div className="card">
<div className="card-body">
{/* Service Dropdown */}
<div
className="dataTables_length text-start py-2 px-5 col-md-4 col-12"
id="DataTables_Table_0_length"
>
{!servicesLoading && assignedServices?.length > 0 && (
assignedServices.length > 1 ? (
<label>
<select
name="DataTables_Table_0_length"
aria-controls="DataTables_Table_0"
className="form-select form-select-sm"
aria-label="Select Service"
value={selectedService}
onChange={handleServiceChange}
style={{ fontSize: "0.875rem", height: "35px", width: "190px" }}
>
<option value="">All Services</option>
{assignedServices.map((service) => (
<option key={service.id} value={service.id}>
{service.name}
</option>
))}
</select>
</label>
) : (
<h5>{assignedServices[0].name}</h5>
)
)}
</div>
{/* Infra Planning Component */}
{selectedProject ? (
<InfraPlanning selectedService={selectedService} />
) : (
<div className="text-center">Please Select Project</div>
)}
</div>
</div>
</div>
);
};

View File

@ -69,7 +69,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
};
if (isError) return <div>{error.message}</div>;
if (isLoading) return gridView ? <CardViewContactSkeleton /> : <ListViewContactSkeleton />;
// if (isLoading) return gridView ? <CardViewContactSkeleton /> : <ListViewContactSkeleton />;
return (
<div className="row mt-5">
@ -94,11 +94,11 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
) : (
<div className="col-12">
<ListViewContact
data={data.data}
data={data?.data}
Pagination={
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
totalPages={data?.totalPages}
onPageChange={paginate}
/>
}

View File

@ -0,0 +1,51 @@
import React, { useState } from "react";
import Breadcrumb from "../../components/common/Breadcrumb";
import { useOrganizationModal } from "../../hooks/useOrganization";
import OrganizationsList from "../../components/Organization/OrganizationsList";
const OrganizationPage = () => {
const { isOpen, orgData, startStep, onOpen, flowType } =
useOrganizationModal();
const [searchText,setSearchText] = useState("")
return (
<div className="container-fluid">
<Breadcrumb
data={[{ label: "Home", link: "/" }, { label: "Organizations" }]}
/>
<div className="card my-3 px-sm-2 px-0">
<div className="card-body py-2 px-3">
<div className="row align-items-center">
<div className="col-6 d-flex ">
<div className="d-flex align-items-center">
<input
type="search"
value={searchText}
onChange={(e)=>setSearchText(e.target.value)}
className="form-control form-control-sm w-auto"
placeholder="Search Organization"
aria-describedby="search-label"
/>
</div>
</div>
<div className="col-6 text-end mt-2 mt-sm-0">
<button
type="button"
className="p-1 me-1 m-sm-0 bg-primary rounded-circle"
title="Add New Organization"
onClick={()=>onOpen({ startStep: 2,flowType:"default" })}
>
<i className="bx bx-plus fs-4 text-white"></i>
</button>
</div>
</div>
</div>
</div>
<OrganizationsList searchText={searchText}/>
</div>
);
};
export default OrganizationPage;

View File

@ -1,16 +1,16 @@
import React, { useEffect, useMemo } from "react";
import { useProfile } from "../../hooks/useProfile";
import TenantDetails from "./TenantDetails";
import { hasUserPermission } from "../../utils/authUtils";
import { VIEW_TENANTS } from "../../utils/constants";
import { useNavigate } from "react-router-dom";
import Loader from "../../components/common/Loader";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
const SelfTenantDetails = () => {
const { profile, loading } = useProfile();
const tenantId = profile?.employeeInfo?.tenantId;
const navigate = useNavigate();
const isSelfTenantView = hasUserPermission(VIEW_TENANTS);
const isSelfTenantView = useHasUserPermission(VIEW_TENANTS);
useEffect(() => {
if (!isSelfTenantView) {

View File

@ -20,7 +20,6 @@ import TenantFilterPanel from "../../components/Tenant/TenantFilterPanel";
import { useDebounce } from "../../utils/appUtils";
import { useFab } from "../../Context/FabContext";
import { setCurrentTenant } from "../../slices/globalVariablesSlice";
import { hasUserPermission } from "../../utils/authUtils";
// ------ Schema -------
import {
@ -35,6 +34,7 @@ import {
VIEW_TENANTS,
} from "../../utils/constants";
import { useProfile } from "../../hooks/useProfile";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
// ---------- Context ----------
export const TenantContext = createContext();
@ -63,9 +63,9 @@ const TenantPage = () => {
const debouncedSearch = useDebounce(searchText, 500);
const { setOffcanvasContent, setShowTrigger } = useFab();
const isSuperTenant = hasUserPermission(SUPPER_TENANT);
const canManageTenants = hasUserPermission(MANAGE_TENANTS);
const isSelfTenant = hasUserPermission(VIEW_TENANTS);
const isSuperTenant = useHasUserPermission(SUPPER_TENANT);
const canManageTenants = useHasUserPermission(MANAGE_TENANTS);
const isSelfTenant = useHasUserPermission(VIEW_TENANTS);
const methods = useForm({
resolver: zodResolver(filterSchema),

View File

@ -44,7 +44,7 @@ const LoginPage = () => {
localStorage.setItem("jwtToken", response.data.token);
localStorage.setItem("refreshToken", response.data.refreshToken);
setLoading(false);
navigate("/dashboard");
navigate("/auth/switch/org");
} else {
await AuthRepository.sendOTP({ email: data.username });
showToast("OTP has been sent to your email.", "success");

View File

@ -0,0 +1,105 @@
import React, { useState } from "react";
import Modal from "../../components/common/Modal";
import { useAuthModal, useSelectTenant, useTenants } from "../../hooks/useAuth";
import { useProfile } from "../../hooks/useProfile";
import { useQueryClient } from "@tanstack/react-query";
import AuthRepository from "../../repositories/AuthRepository";
import Loader from "../../components/common/Loader";
const SwitchTenant = () => {
const queryClient = useQueryClient();
const { profile } = useProfile();
const [pendingTenant, setPendingTenant] = useState(null);
const { isOpen, onClose, onOpen } = useAuthModal();
const { data, isLoading, isError, error } = useTenants();
const { mutate: chooseTenant, isPending } = useSelectTenant(() => {
onClose();
queryClient.clear();
// 2. Force fetch profile fresh for the new tenant
queryClient.fetchQuery({
queryKey: ["profile"],
queryFn: () => AuthRepository.profile(),
});
window.location.reload();
});
const currentTenant = localStorage.getItem("ctnt");
const handleTenantselection = (tenantId) => {
setPendingTenant(tenantId);
localStorage.setItem("ctnt", tenantId);
chooseTenant(tenantId);
};
const contentBody = (
<div className="container text-black">
<p className=" fs-5">Switch Workplace</p>
<div className="row justify-content-center g-4">
{data?.data.map((tenant) => (
<div key={tenant.id} className="col-12 ">
<div
className={`d-flex flex-column flex-md-row gap-3 align-items-center align-items-md-start p-1 border ${
currentTenant === tenant.id ? "border-primary" : ""
} `}
>
<div
className="flex-shrink-0 text-center"
style={{
width: "80px",
aspectRatio: "1 / 1",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<img
src={tenant?.logoImage || "/assets/img/SP-Placeholdeer.svg"}
alt={tenant.name}
className="img-fluid"
style={{
width: "100%",
height: "100%",
objectFit: "contain",
}}
/>
</div>
<div className="d-flex flex-column text-start gap-2">
<p className="fs-5 text-muted fw-semibold mb-1">
{tenant?.name}
</p>
<div className="d-flex flex-wrap gap-2 align-items-center">
<p className="fw-semibold m-0">Industry:</p>
<p className="m-0">
{tenant?.industry?.name || "Not Available"}
</p>
</div>
{tenant?.description && (
<p className="text-start text-wrap m-0">
{tenant?.description}
</p>
)}
<button
className={` ${currentTenant === tenant.id ? "badge bg-label-primary w-50" :"btn btn-primary btn-sm mt-2 align-self-start" }`}
onClick={() => handleTenantselection(tenant?.id)}
disabled={isPending && pendingTenant === tenant.id || currentTenant === tenant.id }
>
{currentTenant === tenant.id ? "Active Tenant" :isPending && pendingTenant === tenant.id
? "Please Wait.."
: "Go To Dashboard"}
</button>
</div>
</div>
</div>
))}
</div>
</div>
);
return <Modal isOpen={isOpen} onClose={onClose} body={isLoading ? <Loader/>:contentBody} />;
};
export default SwitchTenant;

View File

@ -0,0 +1,138 @@
import { useEffect, useState } from "react";
import { useTenants, useSelectTenant, useLogout } from "../../hooks/useAuth.jsx";
import { Link, useNavigate } from "react-router-dom";
import Dashboard from "../../components/Dashboard/Dashboard.jsx";
import Loader from "../../components/common/Loader.jsx";
const TenantSelectionPage = () => {
const [pendingTenant, setPendingTenant] = useState(null);
const navigate = useNavigate();
const { data, isLoading, isError, error } = useTenants();
const { mutate: chooseTenant, isPending } = useSelectTenant(() => {
navigate("/dashboard");
});
const handleTenantselection = (tenantId) => {
setPendingTenant(tenantId);
localStorage.setItem("ctnt", tenantId);
chooseTenant(tenantId);
};
const {mutate:handleLogout,isPending:isLogouting} = useLogout(()=>{})
// useEffect(() => {
// if (localStorage.getItem("ctnt")) {
// navigate("/dashboard");
// }
// }, [navigate]);
useEffect(() => {
if (!isLoading && data?.data?.length === 1) {
const tenant = data.data[0];
handleTenantselection(tenant.id);
}
}, [isLoading, data]);
if (isLoading) return <Loader />;
if (isLoading) {
return <Loader />;
}
if (!data?.data?.length) {
return (
<div className="text-center py-5">
<p>No tenant assigned to your account.</p>
</div>
);
}
return (
<div className="container-fluid">
{/* Logo */}
<div className="text-center">
<img
src="/img/brand/marco.png"
alt="marco-logo"
className="app-brand-logo-login img-fluid"
style={{ maxHeight: "80px" }}
/>
</div>
{/* Heading */}
<div className="text-center mb-4">
<p className="fs-4 fw-bold mb-1">Welcome</p>
<p className="fs-6 fs-md-5">
Please select which dashboard you want to explore!!!
</p>
<div onClick={()=>handleLogout()}>
{isLogouting ? "Please Wait...":<span className="fs-6 fw-semibold cursor-pointer text-decoration-underline"><i className='bx bx-log-out'></i>SignOut</span>}
</div>
</div>
{/* Card Section */}
<div className="row justify-content-center g-4 ">
{data?.data.map((tenant) => (
<div key={tenant.id} className="col-12 col-md-10 col-lg-8">
<div className="d-flex flex-column flex-md-row gap-4 align-items-center align-items-md-start p-3 border rounded shadow-sm bg-white h-100">
{/* Image */}
<div className="flex-shrink-0 text-center">
<img
src={tenant?.logoImage || "/assets/img/SP-Placeholdeer.svg"}
alt={tenant.name}
className="img-fluid rounded"
style={{
maxWidth: "140px",
aspectRatio: "3 / 2",
objectFit: "contain",
}}
/>
</div>
{/* Content */}
<div className="d-flex flex-column text-start gap-2 w-100">
{/* Title */}
<p className="fs-5 fs-md-4 text-dark fw-semibold mb-1">
{tenant?.name}
</p>
{/* Industry */}
<div className="d-flex flex-wrap gap-2 align-items-center">
<p className="fw-semibold m-0">Industry:</p>
<p className="m-0 text-muted">
{tenant?.industry?.name || "Not Available"}
</p>
</div>
{/* Description */}
{tenant?.description && (
<p className="text-start text-wrap text-muted small m-0">
{tenant?.description}
</p>
)}
{/* Button */}
<button
className="btn btn-primary btn-sm mt-2 align-self-start"
onClick={() => handleTenantselection(tenant?.id)}
disabled={pendingTenant === tenant.id && isPending}
>
{isPending && pendingTenant === tenant.id
? "Please Wait.."
: "Go To Dashboard"}
</button>
</div>
</div>
</div>
))}
</div>
</div>
);
};
export default TenantSelectionPage;

View File

@ -0,0 +1 @@
login

View File

@ -11,7 +11,7 @@ import {
} from "../../hooks/useEmployees";
import { useProjectName, useProjects } from "../../hooks/useProjects";
import { useProfile } from "../../hooks/useProfile";
import { hasUserPermission } from "../../utils/authUtils";
import {
ITEMS_PER_PAGE,
MANAGE_EMPLOYEES,
@ -19,7 +19,6 @@ import {
VIEW_TEAM_MEMBERS,
} from "../../utils/constants";
import { clearCacheKey } from "../../slices/apiDataManager";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import SuspendEmp from "../../components/Employee/SuspendEmp"; // Keep if you use SuspendEmp
import {
exportToCSV,
@ -36,6 +35,7 @@ import { newlineChars } from "pdf-lib";
import GlobalModel from "../../components/common/GlobalModel";
import usePagination from "../../hooks/usePagination";
import { setProjectId } from "../../slices/localVariablesSlice";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
const EmployeeList = () => {
const selectedProjectId = useSelector(

View File

@ -91,7 +91,7 @@ const MasterPage = () => {
{modalConfig && (
<GlobalModel
size={
["Application Role", "Edit-Application Role"].includes(
["Application Role", "Edit-Application Role", "Services"].includes(
modalConfig.masterType
)
? "lg"

View File

@ -20,9 +20,10 @@ import { setProjectId } from "../../slices/localVariablesSlice";
import ProjectDocuments from "../../components/Project/ProjectDocuments";
import ProjectSetting from "../../components/Project/ProjectSetting";
import DirectoryPage from "../Directory/DirectoryPage";
import { useProjectAccess } from "../../hooks/useProjectAccess"; // new
import { useProjectAccess } from "../../hooks/useProjectAccess";
import "./ProjectDetails.css";
import ProjectOrganizations from "../../components/Project/ProjectOrganizations";
const ProjectDetails = () => {
const projectId = useSelectedProject();
@ -96,6 +97,8 @@ const ProjectDetails = () => {
return <ProjectDocuments />;
case "setting":
return <ProjectSetting />;
case "organization":
return <ProjectOrganizations />;
default:
return <ComingSoonPage />;
}

View File

@ -1,13 +1,24 @@
import { api } from "../utils/axiosClient";
const AttendanceRepository = {
markAttendance: (data) => api.post("/api/attendance/record", data),
getAttendance: (projectId, organizationId, includeInactive,date) => {
let url = `/api/attendance/project/team?projectId=${projectId}`;
const AttendanceRepository = {
markAttendance:(data)=>api.post("/api/attendance/record",data),
getAttendance:(id)=>api.get(`api/attendance/project/team?projectId=${id}`),
getAttendanceFilteredByDate: ( projectId, fromDate, toDate ) =>
{
let url = `api/Attendance/project/log?projectId=${ projectId }`
const params = [];
if (organizationId) params.push(`organizationId=${organizationId}`);
if (includeInactive) params.push(`includeInactive=${includeInactive}`);
if (date) params.push(`date=${date}`);
if (params.length > 0) {
url += `&${params.join("&")}`; }
return api.get(url);
},
getAttendanceFilteredByDate: (projectId, fromDate, toDate,organizationId) => {
let url = `api/Attendance/project/log?projectId=${projectId}`;
if (fromDate) {
url += `&dateFrom=${fromDate}`;
}
@ -15,27 +26,38 @@ const AttendanceRepository = {
if (toDate) {
url += `&dateTo=${toDate}`;
}
return api.get(url)
if (organizationId) {
url += `&organizationId=${organizationId}`;
}
return api.get(url);
},
getAttendanceLogs: ( id ) => api.get( `api/attendance/log/attendance/${ id }` ),
getRegularizeList: ( id ) => api.get( `api/attendance/regularize?projectId=${ id }` ),
getAttendanceByEmployee: ( employeeId, fromDate, toDate ) =>
{
let url = `api/Attendance/log/employee/${ employeeId }?`
if (fromDate) {
url += `&dateFrom=${fromDate}`;
}
if (toDate) {
url += `&dateTo=${toDate}`;
}
return api.get(url)
},
}
getAttendanceLogs: (id) => api.get(`api/attendance/log/attendance/${id}`),
getRegularizeList: (projectId, organizationId, IncludeInActive) => {
let url = `/api/attendance/regularize?projectId=${projectId}`;
const params = [];
if (organizationId) params.push(`organizationId=${organizationId}`);
if (IncludeInActive) params.push(`IncludeInActive=${IncludeInActive}`);
if (params.length > 0) {
url += `&${params.join("&")}`; }
return api.get(url);
},
getAttendanceByEmployee: (employeeId, fromDate, toDate) => {
let url = `api/Attendance/log/employee/${employeeId}?`;
if (fromDate) {
url += `&dateFrom=${fromDate}`;
}
if (toDate) {
url += `&dateTo=${toDate}`;
}
return api.get(url);
},
};
export default AttendanceRepository;

View File

@ -2,7 +2,7 @@ import { api } from "../utils/axiosClient";
const AuthRepository = {
// Public routes (no auth token required)
login: (data) => api.postPublic("/api/auth/login", data),
login: (data) => api.postPublic("/api/auth/login/v1", data),
refreshToken: (data) => api.postPublic("/api/auth/refresh-token", data),
forgotPassword: (data) => api.postPublic("/api/auth/forgot-password", data),
resetPassword: (data) => api.postPublic("/api/auth/reset-password", data),
@ -15,7 +15,9 @@ const AuthRepository = {
logout: (data) => api.post("/api/auth/logout", data),
profile: () => api.get("/api/user/profile"),
changepassword: (data) => api.post("/api/auth/change-password", data),
appmenu:()=>api.get('/api/appmenu/get/menu')
appmenu: () => api.get('/api/appmenu/get/menu'),
selectTenant: (tenantId) => api.post(`/api/Auth/select-tenant/${tenantId}`),
getTenantList: () => api.get("/api/Auth/get/user/tenants"),
};

View File

@ -31,6 +31,13 @@ export const MasterRespository = {
getActivites: () => api.get("api/master/activities"),
createActivity: (data) => api.post("api/master/activity", data),
//Services
getService: () => api.get("api/master/service/list"),
createService: (data) => api.post("api/master/service/create", data),
updateService: (id, data) => api.put(`api/master/service/edit/${id}`, data),
"Services": (id) => api.delete(`/api/master/service/delete/${id}`),
updateActivity: (id, data) =>
api.post(`api/master/activity/edit/${id}`, data),
getIndustries: () => api.get("api/master/industries"),
@ -106,4 +113,11 @@ export const MasterRespository = {
createDocumentType: (data) => api.post(`/api/Master/document-type`, data),
updateDocumentType: (id, data) =>
api.put(`/api/Master/document-type/edit/${id}`, data),
getGlobalServices:()=>api.get("/api/Master/global-service/list"),
getMasterServices:()=>api.get("/api/Master/service/list"),
getOrganizationType:()=>api.get('/api/Master/organization-type/list')
};

View File

@ -0,0 +1,20 @@
import { api } from "../utils/axiosClient";
const OrganizationRepository = {
createOrganization: (data) => api.post("/api/Organization/create", data),
getOrganizationList: (pageSize, pageNumber, active, sprid, searchString) => {
return api.get(
`/api/Organization/list?pageSize=${pageSize}&pageNumber=${pageNumber}&active=${active}&${
sprid ? `sprid=${sprid}&` : ""
}searchString=${searchString}`
);
},
getOrganizationBySPRID :(sprid)=>api.get(`/api/Organization/list?sprid=${sprid}`),
assignOrganizationToProject:(data)=>api.post(`/api/Organization/assign/project`,data),
assignOrganizationToTenanat:(organizationId)=>api.post(`/api/Organization/assign/tenant/${organizationId}`)
};
export default OrganizationRepository;

View File

@ -5,14 +5,25 @@ const ProjectRepository = {
getProjectByprojectId: (projetid) =>
api.get(`/api/project/details/${projetid}`),
getProjectAllocation: (projetid) =>
api.get(`api/project/allocation/${projetid}`),
getProjectAllocation: (projectId, organizationId, serviceId) => {
let url = `/api/project/allocation/${projectId}`;
const params = [];
if (organizationId) params.push(`organizationId=${organizationId}`);
if (serviceId) params.push(`serviceId=${serviceId}`);
if (params.length > 0) {
url += `?${params.join("&")}`;
}
return api.get(url);
},
getEmployeesByProject: (projectId) =>
api.get(`/api/Project/employees/get/${projectId}`),
manageProject: (data) => api.post("/api/project", data),
// updateProject: (data) => api.post("/api/project/update", data),
manageProjectAllocation: (data) => api.post("/api/project/allocation", data),
@ -32,20 +43,31 @@ const ProjectRepository = {
getProjectDetails: (id) => api.get(`/api/project/details/${id}`),
getProjectInfraByproject: (id) => api.get(`/api/project/infra-details/${id}`),
getProjectTasksByWorkArea: (id) => api.get(`/api/project/tasks/${id}`),
getProjectTasksByWorkArea: (workAreaId, serviceId) => {
let url = `/api/project/tasks/${workAreaId}`;
if (serviceId) {
url += `?serviceId=${serviceId}`;
}
return api.get(url);
},
getProjectTasksByEmployee: (id, fromDate, toDate) =>
api.get(
`/api/project/tasks-employee/${id}?fromDate=${fromDate}&toDate=${toDate}`
),
// Permission Managment for Employee at Project Level
// Permission Managment for Employee at Project Level
getProjectLevelEmployeeList: (projectId) => api.get(`/api/Project/get/proejct-level/employees/${projectId}`),
getProjectLevelModules: () => api.get(`/api/Project/get/proejct-level/modules`),
getProjectLevelEmployeePermissions: (employeeId, projectId) => api.get(`/api/Project/get/project-level-permission/employee/${employeeId}/project/${projectId}`),
updateProjectLevelEmployeePermission: (data) => api.post(`/api/Project/assign/project-level-permission`, data),
getAllProjectLevelPermission: (projectId) => api.get(`/api/Project/get/all/project-level-permission/${projectId}`),
getProjectLevelEmployeeList:(projectId)=>api.get(`/api/Project/get/proejct-level/employees/${projectId}`),
getProjectLevelModules:()=>api.get(`/api/Project/get/proejct-level/modules`),
getProjectLevelEmployeePermissions:(employeeId,projectId)=>api.get(`/api/Project/get/project-level-permission/employee/${employeeId}/project/${projectId}`),
updateProjectLevelEmployeePermission:(data)=>api.post(`/api/Project/assign/project-level-permission`,data),
getAllProjectLevelPermission:(projectId)=>api.get(`/api/Project/get/all/project-level-permission/${projectId}`)
// Services
getProjectAssignedServices: (projectId) => api.get(`/api/Project/get/assigned/services/${projectId}`),
getProjectAssignedOrganizations: (projectId) => api.get(`/api/Project/get/assigned/organization/${projectId}`)
};
export const TasksRepository = {

View File

@ -50,7 +50,9 @@ import MainResetPasswordPage from "../pages/authentication/MainResetPasswordPage
import TenantPage from "../pages/Tenant/TenantPage";
import { Navigate } from "react-router-dom";
import CreateTenant from "../pages/Tenant/CreateTenant";
import OrganizationPage from "../pages/Organization/OrganizationPage";
import LandingPage from "../pages/Home/LandingPage";
import TenantSelectionPage from "../pages/authentication/TenantSelectionPage";
const router = createBrowserRouter(
[
{
@ -69,6 +71,7 @@ const router = createBrowserRouter(
{ path: "/auth/changepassword", element: <ChangePasswordPage /> },
],
},
{ path: "/auth/switch/org", element: <TenantSelectionPage /> },
{
element: <ProtectedRoute />,
errorElement: <ErrorPage />,
@ -97,6 +100,7 @@ const router = createBrowserRouter(
{ path: "/tenants/new-tenant", element: <CreateTenant /> },
{ path: "/tenant/:tenantId", element: <SuperTenantDetails /> },
{ path: "/tenant/self", element: <SelfTenantDetails /> },
{ path: "/organizations", element: <OrganizationPage /> },
{ path: "/help/support", element: <Support /> },
{ path: "/help/docs", element: <Documentation /> },
{ path: "/help/connect", element: <Connect /> },

View File

@ -8,7 +8,7 @@ const ProtectedRoute = () => {
// // const isAuthenticated = true;
// isTokenValid();
// return isAuthenticated ? <Outlet /> : <Navigate to="/auth/login" />
const [isAuthenticated, setIsAuthenticated] = useState(null);
useEffect(() => {
@ -66,7 +66,7 @@ const attemptTokenRefresh = async (storedRefreshToken) => {
localStorage.setItem("jwtToken", response.data.token);
localStorage.setItem("refreshToken", response.data.refreshToken);
return true;
// api
// api
// .post("/api/auth/refresh-token", {
// token: localStorage.getItem("jwtToken"),
// refreshToken: refreshToken,

View File

@ -1,93 +0,0 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import AttendanceRepository from '../../repositories/AttendanceRepository';
import {clearCacheKey} from '../apiDataManager';
// Fetch attendance data
export const fetchAttendanceData = createAsyncThunk(
'attendanceLogs/fetchAttendanceData',
async ( {projectId, fromDate, toDate}, thunkAPI ) =>
{
try {
const response = await AttendanceRepository.getAttendanceFilteredByDate(projectId, fromDate, toDate);
return response?.data?.filter((log) => log.checkInTime !== null && log.activity !== 0);
} catch (error) {
return thunkAPI.rejectWithValue(error.message);
}
}
);
export const markAttendance = createAsyncThunk(
'attendanceLogs/markAttendance', // Updated action type prefix
async ( formData, thunkAPI ) =>
{
try {
let newRecordAttendance = {
Id: formData.id || null,
comment: formData.description,
employeeID: formData.employeeId,
projectID: formData.projectId,
date: new Date().toISOString(),
markTime: formData.markTime,
latitude: formData.latitude.toString(),
longitude: formData.longitude.toString(),
action: formData.action,
image: null,
};
const response = await AttendanceRepository.markAttendance( newRecordAttendance );
return response.data;
} catch ( error )
{
const message = error?.response?.data?.message || error.message || "Error Occured During Api Call";
return thunkAPI.rejectWithValue(message);
}
}
);
// Attendance Logs Slice
const attendanceLogsSlice = createSlice({
name: 'attendanceLogs', // Updated slice name
initialState: {
data: [],
loading: false,
error: null,
},
reducers: {
setAttendanceData: (state, action) => {
state.data = action.payload;
},
},
extraReducers: (builder) => {
builder
// Fetch attendance data
.addCase(fetchAttendanceData.pending, (state) => {
state.loading = true;
})
.addCase(fetchAttendanceData.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchAttendanceData.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
})
// Mark attendance - log attenace data
.addCase(markAttendance.fulfilled, (state, action) => {
const updatedRecord = action.payload;
const index = state.data.findIndex(item => item.id === updatedRecord.id);
if (index !== -1) {
state.data[index] = { ...state.data[index], ...updatedRecord };
} else {
state.data.push(updatedRecord);
}
});
},
});
export const { setAttendanceData } = attendanceLogsSlice.actions;
export default attendanceLogsSlice.reducer;

View File

@ -1,38 +0,0 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import AttendanceRepository from '../../repositories/AttendanceRepository';
import {clearCacheKey} from '../apiDataManager';
export const markCurrentAttendance = createAsyncThunk(
'attendanceCurrentDate/markAttendance',
async ( formData, {getState, dispatch, rejectWithValue} ) =>
{
const { projectId } = getState().localVariables
try
{
// Create the new attendance record
const newRecordAttendance = {
Id: formData.id || null,
comment: formData.description,
employeeID: formData.employeeId,
projectId: projectId,
date: new Date().toISOString(),
markTime: formData.markTime,
latitude: formData.latitude.toString(),
longitude: formData.longitude.toString(),
action: formData.action,
image: null,
};
const response = await AttendanceRepository.markAttendance(newRecordAttendance);
const markedAttendance = response.data
clearCacheKey("AttendanceLogs")
return markedAttendance;
} catch (error) {
console.error('Error marking attendance:', error);
return rejectWithValue(error.message); // Reject with error message
}
}
);

View File

@ -1,56 +0,0 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import AttendanceRepository from '../../repositories/AttendanceRepository';
import { markAttendance } from './attedanceLogsSlice';
export const fetchEmployeeAttendanceData = createAsyncThunk(
'employeeAttendance/fetchEmployeeAttendanceData',
async ( {employeeId, fromDate, toDate}, thunkAPI ) =>
{
try {
const response = await AttendanceRepository.getAttendanceByEmployee( employeeId, fromDate, toDate );
// return response?.data?.filter((log) => log.checkInTime !== null && log.activity !== 0);
return response.data
} catch (error) {
return thunkAPI.rejectWithValue(error.message);
}
}
);
const employeeAttendancesSlice = createSlice({
name: 'employeeAttendance', // Updated slice name
initialState: {
data: [],
loading: false,
error: null,
},
reducers: {
setEmployeeAttendanceData: (state, action) => {
state.data = action.payload;
},
},
extraReducers: (builder) => {
builder
// Fetch attendance data
.addCase(fetchEmployeeAttendanceData.pending, (state) => {
state.loading = true;
})
.addCase(fetchEmployeeAttendanceData.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchEmployeeAttendanceData.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
})
},
});
export const { setEmployeeAttendanceData } = employeeAttendancesSlice.actions;
export default employeeAttendancesSlice.reducer;

View File

@ -3,23 +3,23 @@ import { createSlice } from "@reduxjs/toolkit";
const globalVariablesSlice = createSlice({
name: "globalVariables",
initialState: {
loginUser:null,
currentTenant:null
loginUser: null,
currentTenant: null,
},
reducers: {
setGlobalVariable: (state, action) => {
const { key, value } = action.payload;
state[key] = value;
},
setLoginUserPermmisions: ( state, action ) =>
{
state.loginUser = action.payload
setLoginUserPermmisions: (state, action) => {
state.loginUser = action.payload;
},
setCurrentTenant: (state, action) => {
state.currentTenant = action.payload;
},
setCurrentTenant:(state,action)=>{
state.currentTenant = action.payload
}
},
});
export const { setGlobalVariable,setLoginUserPermmisions,setCurrentTenant } = globalVariablesSlice.actions;
export const { setGlobalVariable, setLoginUserPermmisions, setCurrentTenant } =
globalVariablesSlice.actions;
export default globalVariablesSlice.reducer;

View File

@ -3,37 +3,86 @@ import { createSlice } from "@reduxjs/toolkit";
const localVariablesSlice = createSlice({
name: "localVariables",
initialState: {
selectedMaster:"Application Role",
regularizationCount:0,
defaultDateRange: {
selectedMaster: "Application Role",
regularizationCount: 0,
defaultDateRange: {
startDate: null,
endDate: null,
},
projectId: null,
reload:false
reload: false,
OrganizationModal: {
isOpen: false,
orgData: null,
prevStep: null,
startStep: 1,
flowType: "default",
},
AuthModal: {
isOpen: false,
},
},
reducers: {
changeMaster: (state, action) => {
state.selectedMaster = action.payload;
state.selectedMaster = action.payload;
},
updateRegularizationCount: (state, action) => {
state.regularizationCount = action.payload;
},
setProjectId: (state, action) => {
localStorage.setItem("project",null)
setProjectId: (state, action) => {
localStorage.setItem("project", null);
state.projectId = action.payload;
localStorage.setItem("project",state.projectId || null)
localStorage.setItem("project", state.projectId || null);
},
refreshData: ( state, action ) =>
{
state.reload = action.payload
refreshData: (state, action) => {
state.reload = action.payload;
},
setDefaultDateRange: (state, action) => {
state.defaultDateRange = action.payload;
},
openOrgModal: (state, action) => {
state.OrganizationModal.isOpen = true;
state.OrganizationModal.orgData = action.payload?.orgData || null;
if (state.OrganizationModal.startStep) {
state.OrganizationModal.prevStep = state.OrganizationModal.startStep;
}
state.OrganizationModal.startStep = action.payload?.startStep || 1;
state.OrganizationModal.flowType = action.payload?.flowType || "default";
},
closeOrgModal: (state) => {
state.OrganizationModal.isOpen = false;
state.OrganizationModal.orgData = null;
state.OrganizationModal.startStep = 1;
state.OrganizationModal.prevStep = null;
},
toggleOrgModal: (state) => {
state.OrganizationModal.isOpen = !state.OrganizationModal.isOpen;
},
openAuthModal: (state, action) => {
state.AuthModal.isOpen = true;
},
closeAuthModal: (state, action) => {
state.AuthModal.isOpen = false;
},
},
});
export const { changeMaster ,updateRegularizationCount,setProjectId,refreshData,setDefaultDateRange} = localVariablesSlice.actions;
export default localVariablesSlice.reducer;
export const {
changeMaster,
updateRegularizationCount,
setProjectId,
refreshData,
setDefaultDateRange,
openOrgModal,
closeOrgModal,
toggleOrgModal,
openAuthModal,
closeAuthModal,
} = localVariablesSlice.actions;
export default localVariablesSlice.reducer;

View File

@ -2,15 +2,12 @@ import { configureStore } from "@reduxjs/toolkit";
import apiCacheReducer from "../slices/apiCacheSlice";
import globalVariablesReducer from "../slices/globalVariablesSlice";
import localVariableRducer from "../slices/localVariablesSlice"
import attendanceReducer from "../slices/apiSlice/attedanceLogsSlice"
import employeeAttendanceReducer from "../slices/apiSlice/employeeAttendanceSlice"
export const store = configureStore({
reducer: {
apiCache: apiCacheReducer,
globalVariables: globalVariablesReducer,
localVariables:localVariableRducer,
attendanceLogs: attendanceReducer,
employeeAttendance: employeeAttendanceReducer,
},
});

View File

@ -1,12 +0,0 @@
import { useProfile } from "../hooks/useProfile";
export const hasUserPermission = (permission) => {
const { profile } = useProfile();
if (profile) {
if (!permission || typeof permission !== "string") {
return false;
}
return profile?.featurePermissions.includes(permission);
}
return false;
};

View File

@ -4,7 +4,7 @@ import axiosRetry from "axios-retry";
import showToast from "../services/toastService";
import { startSignalR, stopSignalR } from "../services/signalRService";
import { BASE_URL } from "./constants";
const base_Url = BASE_URL
const base_Url = BASE_URL;
export const axiosClient = axios.create({
baseURL: base_Url,
@ -44,7 +44,10 @@ axiosClient.interceptors.response.use(
const originalRequest = error.config;
// Skip retry for public requests or already retried ones
if (!originalRequest && originalRequest._retry || originalRequest.authRequired === false) {
if (
(!originalRequest && originalRequest._retry) ||
originalRequest.authRequired === false
) {
return Promise.reject(error);
}
@ -53,7 +56,10 @@ axiosClient.interceptors.response.use(
originalRequest._toastShown = true;
if (error.code === "ERR_CONNECTION_REFUSED") {
showToast("Unable to connect to the server. Please try again later.", "error");
showToast(
"Unable to connect to the server. Please try again later.",
"error"
);
} else if (error.code === "ERR_NETWORK") {
showToast("Network error. Please check your connection.", "error");
redirectToLogin();
@ -68,7 +74,10 @@ axiosClient.interceptors.response.use(
const refreshToken = localStorage.getItem("refreshToken");
if (!refreshToken || error.response.data?.errors === "Invalid or expired refresh token.") {
if (
!refreshToken ||
error.response.data?.errors === "Invalid or expired refresh token."
) {
redirectToLogin();
return Promise.reject(error);
}
@ -88,7 +97,7 @@ axiosClient.interceptors.response.use(
localStorage.setItem("jwtToken", token);
localStorage.setItem("refreshToken", newRefreshToken);
startSignalR()
startSignalR();
// Set Authorization header
originalRequest.headers["Authorization"] = `Bearer ${token}`;
return axiosClient(originalRequest);
@ -160,4 +169,4 @@ export const api = {
// Redirect helper
function redirectToLogin() {
window.location.href = "/auth/login";
}
}