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

View File

@ -1,8 +1,12 @@
:root, :root,
[data-bs-theme="light"] { [data-bs-theme="light"] {
--bs-nav-link-font-size: 0.7375rem; --bs-nav-link-font-size: 0.7375rem;
--bg-border-color :#f8f6f6
} }
.card-header { .card-header {
padding: 0.5rem var(--bs-card-cap-padding-x); 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 { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { queryClient } from "./layouts/AuthLayout"; import { queryClient } from "./layouts/AuthLayout";
import ModalProvider from "./ModalProvider";
@ -11,6 +12,7 @@ const App = () => {
return ( return (
<div className="app"> <div className="app">
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ModalProvider/>
<DireProvider> <DireProvider>
<AppRoutes /> <AppRoutes />
</DireProvider> </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 eventBus from "../../services/eventBus";
import { useSelectedProject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
const Attendance = ({ getRole, handleModalData, searchTerm }) => { const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizationId, includeInactive, date }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const [todayDate, setTodayDate] = useState(new Date()); const [todayDate, setTodayDate] = useState(new Date());
const [ShowPending, setShowPending] = useState(false); const [ShowPending, setShowPending] = useState(false);
// const selectedProject = useSelector(
// (store) => store.localVariables.projectId
// );
const selectedProject = useSelectedProject(); const selectedProject = useSelectedProject();
const { const {
attendance, attendance,
loading: attLoading, loading: attLoading,
recall: attrecall, recall: attrecall,
isFetching isFetching
} = useAttendance(selectedProject); } = useAttendance(selectedProject, organizationId, includeInactive, date);
const filteredAttendance = ShowPending const filteredAttendance = ShowPending
? attendance?.filter( ? attendance?.filter(
(att) => att?.checkInTime !== null && att?.checkOutTime === null (att) => att?.checkInTime !== null && att?.checkOutTime === null
@ -62,12 +59,11 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
const role = item.jobRoleName?.toLowerCase() || ""; const role = item.jobRoleName?.toLowerCase() || "";
return ( return (
fullName.includes(lowercasedSearchTerm) || fullName.includes(lowercasedSearchTerm) ||
role.includes(lowercasedSearchTerm) // also search by role role.includes(lowercasedSearchTerm) // also search by role
); );
}); });
}, [group1, group2, searchTerm]); }, [group1, group2, searchTerm]);
const { currentPage, totalPages, currentItems, paginate } = usePagination( const { currentPage, totalPages, currentItems, paginate } = usePagination(
finalFilteredData, finalFilteredData,
ITEMS_PER_PAGE ITEMS_PER_PAGE
@ -116,7 +112,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
<> <>
<div <div
className="table-responsive text-nowrap h-100" 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"> <div className="d-flex text-start align-items-center py-2">
<strong>Date : {formatUTCToLocalTime(todayDate)}</strong> <strong>Date : {formatUTCToLocalTime(todayDate)}</strong>
@ -142,6 +138,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
<tr className="border-top-1"> <tr className="border-top-1">
<th colSpan={2}>Name</th> <th colSpan={2}>Name</th>
<th>Role</th> <th>Role</th>
<th>Organization</th>
<th> <th>
<i className="bx bxs-down-arrow-alt text-success"></i> <i className="bx bxs-down-arrow-alt text-success"></i>
Check-In Check-In
@ -190,6 +187,8 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
</td> </td>
<td>{item.jobRoleName}</td> <td>{item.jobRoleName}</td>
<td>{item.organizationName || "--"}</td>
<td> <td>
{item.checkInTime {item.checkInTime
? convertShortTime(item.checkInTime) ? convertShortTime(item.checkInTime)
@ -213,7 +212,11 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
))} ))}
{!attendance && ( {!attendance && (
<tr> <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! No employees assigned to the project!
</td> </td>
</tr> </tr>
@ -221,6 +224,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
</tbody> </tbody>
</table> </table>
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && ( {!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
<nav aria-label="Page "> <nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1"> <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 { convertShortTime } from "../../utils/dateUtils";
import RenderAttendanceStatus from "./RenderAttendanceStatus"; import RenderAttendanceStatus from "./RenderAttendanceStatus";
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
import { fetchAttendanceData } from "../../slices/apiSlice/attedanceLogsSlice";
import DateRangePicker from "../common/DateRangePicker"; import DateRangePicker from "../common/DateRangePicker";
import { clearCacheKey, getCachedData, useSelectedProject } from "../../slices/apiDataManager"; import { clearCacheKey, getCachedData, useSelectedProject } from "../../slices/apiDataManager";
import eventBus from "../../services/eventBus"; 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( // const selectedProject = useSelector(
// (store) => store.localVariables.projectId // (store) => store.localVariables.projectId
// ); // );
@ -82,7 +81,8 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
} = useAttendancesLogs( } = useAttendancesLogs(
selectedProject, selectedProject,
dateRange.startDate, dateRange.startDate,
dateRange.endDate dateRange.endDate,
organizationId
); );
const filtering = (data) => { const filtering = (data) => {
const filteredData = showPending const filteredData = showPending
@ -151,6 +151,33 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
}); });
}, [processedData, 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 { const {
currentPage, currentPage,
totalPages, totalPages,
@ -243,14 +270,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
<label className="form-check-label ms-0">Show Pending</label> <label className="form-check-label ms-0">Show Pending</label>
</div> </div>
</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>
<div className="table-responsive text-nowrap" style={{ minHeight: "200px" }}> <div className="table-responsive text-nowrap" style={{ minHeight: "200px" }}>
{isLoading ? ( {isLoading ? (
@ -265,9 +285,9 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
Name Name
</th> </th>
<th className="border-top-1">Date</th> <th className="border-top-1">Date</th>
<th>Organization</th>
<th> <th>
<i className="bx bxs-down-arrow-alt text-success"></i>{" "} <i className="bx bxs-down-arrow-alt text-success"></i> Check-In
Check-In
</th> </th>
<th> <th>
<i className="bx bxs-up-arrow-alt text-danger"></i> Check-Out <i className="bx bxs-up-arrow-alt text-danger"></i> Check-Out
@ -294,7 +314,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
key={`header-${currentDate}`} key={`header-${currentDate}`}
className="table-row-header" className="table-row-header"
> >
<td colSpan={6} className="text-start"> <td colSpan={8} className="text-start">
<strong> <strong>
{moment(currentDate).format("DD-MM-YYYY")} {moment(currentDate).format("DD-MM-YYYY")}
</strong> </strong>
@ -324,6 +344,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
attendance.checkInTime || attendance.checkOutTime attendance.checkInTime || attendance.checkOutTime
).format("DD-MMM-YYYY")} ).format("DD-MMM-YYYY")}
</td> </td>
<td>{attendance.organizationName || "--"}</td>
<td>{convertShortTime(attendance.checkInTime)}</td> <td>{convertShortTime(attendance.checkInTime)}</td>
<td> <td>
{attendance.checkOutTime {attendance.checkOutTime
@ -345,7 +366,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
</tbody> </tbody>
</table> </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> </div>
{paginatedAttendances?.length == 0 && filteredSearchData?.length > 0 && ( {paginatedAttendances?.length == 0 && filteredSearchData?.length > 0 && (

View File

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

View File

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

View File

@ -10,13 +10,13 @@ import eventBus from "../../services/eventBus";
import { cacheData, clearCacheKey, useSelectedProject } from "../../slices/apiDataManager"; import { cacheData, clearCacheKey, useSelectedProject } from "../../slices/apiDataManager";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
const Regularization = ({ handleRequest, searchTerm }) => { const Regularization = ({ handleRequest, searchTerm,projectId, organizationId, IncludeInActive }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
// var selectedProject = useSelector((store) => store.localVariables.projectId); // var selectedProject = useSelector((store) => store.localVariables.projectId);
const selectedProject = useSelectedProject(); const selectedProject = useSelectedProject();
const [regularizesList, setregularizedList] = useState([]); const [regularizesList, setregularizedList] = useState([]);
const { regularizes, loading, error, refetch } = const { regularizes, loading, error, refetch } =
useRegularizationRequests(selectedProject); useRegularizationRequests(selectedProject, organizationId, IncludeInActive);
useEffect(() => { useEffect(() => {
setregularizedList(regularizes); setregularizedList(regularizes);
@ -59,6 +59,36 @@ const Regularization = ({ handleRequest, searchTerm }) => {
}); });
}, [regularizesList, 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 } = const { currentPage, totalPages, currentItems, paginate } =
usePagination(filteredSearchData, 20); usePagination(filteredSearchData, 20);
@ -98,6 +128,7 @@ const Regularization = ({ handleRequest, searchTerm }) => {
<tr> <tr>
<th colSpan={2}>Name</th> <th colSpan={2}>Name</th>
<th>Date</th> <th>Date</th>
<th>Organization</th>
<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>
@ -115,7 +146,7 @@ const Regularization = ({ handleRequest, searchTerm }) => {
<Avatar <Avatar
firstName={att.firstName} firstName={att.firstName}
lastName={att.lastName} lastName={att.lastName}
></Avatar> />
<div className="d-flex flex-column"> <div className="d-flex flex-column">
<a href="#" className="text-heading text-truncate"> <a href="#" className="text-heading text-truncate">
<span className="fw-normal"> <span className="fw-normal">
@ -126,6 +157,9 @@ const Regularization = ({ handleRequest, searchTerm }) => {
</div> </div>
</td> </td>
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td> <td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
<td>{att.organizationName || "--"}</td>
<td>{convertShortTime(att.checkInTime)}</td> <td>{convertShortTime(att.checkInTime)}</td>
<td> <td>
{att.checkOutTime ? convertShortTime(att.checkOutTime) : "--"} {att.checkOutTime ? convertShortTime(att.checkOutTime) : "--"}
@ -136,12 +170,12 @@ const Regularization = ({ handleRequest, searchTerm }) => {
handleRequest={handleRequest} handleRequest={handleRequest}
refresh={refetch} refresh={refetch}
/> />
{/* </div> */}
</td> </td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
) : ( ) : (
<div <div
className="d-flex justify-content-center align-items-center" className="d-flex justify-content-center align-items-center"

View File

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

View File

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

View File

@ -132,7 +132,7 @@ const AttendanceOverview = () => {
onClick={() => setView("table")} onClick={() => setView("table")}
title="Table View" title="Table View"
> >
<i class="bx bx-list-ul fs-5"></i> <i className="bx bx-list-ul fs-5"></i>
</button> </button>
</div> </div>
</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 { useDashboardProjectsCardData } from "../../hooks/useDashboard_Data";
import eventBus from "../../services/eventBus"; import eventBus from "../../services/eventBus";
import GlobalRepository from "../../repositories/GlobalRepository";
const Projects = () => { const Projects = () => {
const { projectsCardData } = useDashboardProjectsCardData(); const {
const [projectData, setProjectsData] = useState(projectsCardData); data: projectsCardData,
isLoading,
isError,
error,
refetch,
} = useDashboardProjectsCardData();
useEffect(() => { useEffect(() => {
setProjectsData(projectsCardData); // When "project" event happens, just refetch
}, [projectsCardData]); 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); eventBus.on("project", handler);
return () => eventBus.off("project", handler); return () => eventBus.off("project", handler);
}, [handler]); }, [refetch]);
const totalProjects = projectsCardData?.totalProjects ?? 0;
const ongoingProjects = projectsCardData?.ongoingProjects ?? 0;
return ( return (
<div className="card p-3 h-100 text-center d-flex justify-content-between"> <div className="card p-3 h-100 text-center d-flex justify-content-between">
@ -37,20 +32,29 @@ const Projects = () => {
Projects Projects
</h5> </h5>
</div> </div>
<div className="d-flex justify-content-around align-items-start mt-n2">
<div> {isLoading ? (
<h4 className="mb-0 fw-bold"> <div className="d-flex justify-content-center align-items-center flex-grow-1">
{projectData.totalProjects?.toLocaleString()} <div className="spinner-border text-primary" role="status">
</h4> <span className="visually-hidden">Loading...</span>
<small className="text-muted">Total</small> </div>
</div> </div>
<div> ) : isError ? (
<h4 className="mb-0 fw-bold"> <div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">
{projectData.ongoingProjects?.toLocaleString()} {error?.message || "Error loading data"}
</h4>
<small className="text-muted">Ongoing</small>
</div> </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> </div>
); );
}; };

View File

@ -1,10 +1,16 @@
import React from "react"; import React from "react";
import { useSelector } from "react-redux"; import { useSelectedProject } from "../../slices/apiDataManager";
import { useDashboardTasksCardData } from "../../hooks/useDashboard_Data"; import { useDashboardTasksCardData } from "../../hooks/useDashboard_Data";
const TasksCard = () => { const TasksCard = () => {
const projectId = useSelector((store) => store.localVariables?.projectId); const projectId = useSelectedProject();
const { tasksCardData, loading, error } = useDashboardTasksCardData(projectId);
const {
data: tasksCardData,
isLoading,
isError,
error,
} = useDashboardTasksCardData(projectId);
return ( return (
<div className="card p-3 h-100 text-center d-flex justify-content-between"> <div className="card p-3 h-100 text-center d-flex justify-content-between">
@ -14,28 +20,30 @@ const TasksCard = () => {
</h5> </h5>
</div> </div>
{loading ? ( {isLoading ? (
// Loader will be displayed when loading is true // Loader while fetching
<div className="d-flex justify-content-center align-items-center flex-grow-1"> <div className="d-flex justify-content-center align-items-center flex-grow-1">
<div className="spinner-border text-primary" role="status"> <div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span> <span className="visually-hidden">Loading...</span>
</div> </div>
</div> </div>
) : error ? ( ) : isError ? (
// Error message if there's an error // Show error
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">{error}</div> <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 className="d-flex justify-content-around align-items-start mt-n2">
<div> <div>
<h4 className="mb-0 fw-bold"> <h4 className="mb-0 fw-bold">
{tasksCardData?.totalTasks?.toLocaleString()} {tasksCardData?.totalTasks?.toLocaleString() ?? 0}
</h4> </h4>
<small className="text-muted">Total</small> <small className="text-muted">Total</small>
</div> </div>
<div> <div>
<h4 className="mb-0 fw-bold"> <h4 className="mb-0 fw-bold">
{tasksCardData?.completedTasks?.toLocaleString()} {tasksCardData?.completedTasks?.toLocaleString() ?? 0}
</h4> </h4>
<small className="text-muted">Completed</small> <small className="text-muted">Completed</small>
</div> </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 { useSelector } from "react-redux";
import { useDashboardTeamsCardData } from "../../hooks/useDashboard_Data"; import { useDashboardTeamsCardData } from "../../hooks/useDashboard_Data";
import eventBus from "../../services/eventBus"; import eventBus from "../../services/eventBus";
import { useQueryClient } from "@tanstack/react-query";
import { useSelectedProject } from "../../slices/apiDataManager";
const Teams = () => { const Teams = () => {
const projectId = useSelector((store) => store.localVariables?.projectId); const queryClient = useQueryClient();
const { teamsCardData, loading, error } = useDashboardTeamsCardData(projectId); const projectId = useSelectedProject()
const [totalEmployees, setTotalEmployee] = useState(0); const {
const [inToday, setInToday] = useState(0); data: teamsCardData,
isLoading,
// Update state when API data arrives isError,
useEffect(() => { error,
setTotalEmployee(teamsCardData?.totalEmployees || 0); } = useDashboardTeamsCardData(projectId);
setInToday(teamsCardData?.inToday || 0);
}, [teamsCardData]);
// Handle real-time updates via eventBus // Handle real-time updates via eventBus
const handler = useCallback((msg) => { const handler = useCallback(
if (msg.activity === 1) { (msg) => {
setInToday((prev) => prev + 1); if (msg.activity === 1) {
} queryClient.setQueryData(["dashboardTeams", projectId], (old) => {
}, []); if (!old) return old;
return {
...old,
inToday: (old.inToday || 0) + 1,
};
});
}
},
[queryClient, projectId]
);
useEffect(() => { useEffect(() => {
eventBus.on("attendance", handler); eventBus.on("attendance", handler);
return () => eventBus.off("attendance", handler); return () => eventBus.off("attendance", handler);
}, [handler]); }, [handler]);
const inToday = teamsCardData?.inToday ?? 0;
const totalEmployees = teamsCardData?.totalEmployees ?? 0;
return ( return (
<div className="card p-3 h-100 text-center d-flex justify-content-between"> <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"> <div className="d-flex justify-content-start align-items-center mb-3">
@ -36,18 +48,17 @@ const Teams = () => {
</h5> </h5>
</div> </div>
{loading ? ( {isLoading ? (
// Blue spinner loader
<div className="d-flex justify-content-center align-items-center flex-grow-1"> <div className="d-flex justify-content-center align-items-center flex-grow-1">
<div className="spinner-border text-primary" role="status"> <div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span> <span className="visually-hidden">Loading...</span>
</div> </div>
</div> </div>
) : error ? ( ) : isError ? (
// Error message if data fetching fails <div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">{error}</div> {error?.message || "Error loading data"}
</div>
) : ( ) : (
// Display data once loaded
<div className="d-flex justify-content-around align-items-start mt-n2"> <div className="d-flex justify-content-around align-items-start mt-n2">
<div> <div>
<h4 className="mb-0 fw-bold">{totalEmployees.toLocaleString()}</h4> <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"> <div className="dataTables_wrapper no-footer mx-5 pb-2">
<table className="table dataTable text-nowrap"> <table className="table dataTable text-nowrap">
<thead> <thead>
<tr style={{ borderBottom: "2px solid var(--bs-table-border-color)"}}> <tr className="table_header_border">
{contactList?.map((col) => ( {contactList?.map((col) => (
<th key={col.key} className={col.align}> <th key={col.key} className={col.align}>
{col.label} {col.label}

View File

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

View File

@ -19,6 +19,7 @@ import { useProjectName } from "../../hooks/useProjects";
import eventBus from "../../services/eventBus"; import eventBus from "../../services/eventBus";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { MANAGE_PROJECT } from "../../utils/constants"; import { MANAGE_PROJECT } from "../../utils/constants";
import { useAuthModal, useLogout } from "../../hooks/useAuth";
const Header = () => { const Header = () => {
const { profile } = useProfile(); const { profile } = useProfile();
@ -26,10 +27,9 @@ const Header = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { data, loading } = useMaster(); const { data, loading } = useMaster();
const navigate = useNavigate(); const navigate = useNavigate();
const {onOpen} = useAuthModal()
const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT); const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT);
// { const { mutate : logout,isPending:logouting} = useLogout()
// console.log(location.pathname);
// }
const isDashboardPath = const isDashboardPath =
/^\/dashboard$/.test(location.pathname) || /^\/$/.test(location.pathname); /^\/dashboard$/.test(location.pathname) || /^\/$/.test(location.pathname);
@ -59,41 +59,9 @@ const Header = () => {
return role ? role.name : "User"; 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 = () => { const handleProfilePage = () => {
navigate(`/employee/${profile?.employeeInfo?.id}`); navigate(`/employee/${profile?.employeeInfo?.id}`);
@ -455,6 +423,16 @@ const Header = () => {
<span className="align-middle">Change Password</span> <span className="align-middle">Change Password</span>
</a> </a>
</li> </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> <li>
<div className="dropdown-divider"></div> <div className="dropdown-divider"></div>
</li> </li>
@ -462,11 +440,10 @@ const Header = () => {
<a <a
aria-label="click to log out" aria-label="click to log out"
className="dropdown-item cusor-pointer" className="dropdown-item cusor-pointer"
href="/logout" onClick={()=>handleLogout()}
onClick={handleLogout}
> >
<i className="bx bx-power-off me-2"></i> {logouting ? "Please Wait":<> <i className="bx bx-log-out me-2"></i>
<span className="align-middle">Log Out</span> <span className="align-middle">SignOut</span></>}
</a> </a>
</li> </li>
</ul> </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 React from "react";
import WorkArea from "./WorkArea"; import WorkArea from "./WorkArea";
const Floor = ({ floor, workAreas, forBuilding }) => { const Floor = ({ floor, workAreas, forBuilding,serviceId }) => {
return ( return (
<React.Fragment key={floor.id}> <React.Fragment key={floor.id}>
{workAreas && workAreas.length > 0 ? ( {workAreas && workAreas.length > 0 ? (
@ -10,6 +10,7 @@ const Floor = ({ floor, workAreas, forBuilding }) => {
key={workArea.id} key={workArea.id}
workArea={workArea} workArea={workArea}
floor={floor} floor={floor}
serviceId={serviceId}
/> />
)) ))
) : ( ) : (

View File

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

View File

@ -6,9 +6,10 @@ import {
useActivitiesMaster, useActivitiesMaster,
useWorkCategoriesMaster, useWorkCategoriesMaster,
} from "../../../hooks/masterHook/useMaster"; } from "../../../hooks/masterHook/useMaster";
import { useManageTask } from "../../../hooks/useProjects"; import { useManageTask, useProjectAssignedServices } from "../../../hooks/useProjects";
import showToast from "../../../services/toastService"; import showToast from "../../../services/toastService";
import Label from "../../common/Label"; import Label from "../../common/Label";
import { useSelectedProject } from "../../../slices/apiDataManager";
const taskSchema = z.object({ const taskSchema = z.object({
buildingID: z.string().min(1, "Building is required"), buildingID: z.string().min(1, "Building is required"),
@ -37,6 +38,15 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
const { activities, loading: activityLoading } = useActivitiesMaster(); const { activities, loading: activityLoading } = useActivitiesMaster();
const { categories, categoryLoading } = useWorkCategoriesMaster(); 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 { const {
register, register,
handleSubmit, handleSubmit,
@ -96,10 +106,12 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
}, [categories]); }, [categories]);
const onSubmitForm = async (data) => { const onSubmitForm = async (data) => {
const payload = [data]; const payload = [data];
CreateTask({payload:payload,buildingId: data.buildingID, CreateTask({
payload: payload, buildingId: data.buildingID,
floorId: data.floorId, floorId: data.floorId,
workAreaId: data.workAreaId, PreviousPlannedWork:0,previousCompletedWork:0}); workAreaId: data.workAreaId, PreviousPlannedWork: 0, previousCompletedWork: 0
});
}; };
return ( return (
@ -150,6 +162,7 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
</div> </div>
)} )}
{/* Work Area Selection */}
{selectedFloor && ( {selectedFloor && (
<div className="col-12 text-start"> <div className="col-12 text-start">
<Label className="form-label" required>Select Work Area</Label> <Label className="form-label" required>Select Work Area</Label>
@ -172,6 +185,32 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
</div> </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 && ( {selectedWorkArea && (
<div className="col-12 text-start"> <div className="col-12 text-start">
<Label className="form-label" required>Select Activity</Label> <Label className="form-label" required>Select Activity</Label>
@ -192,6 +231,7 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
</div> </div>
)} )}
{selectedWorkArea && ( {selectedWorkArea && (
<div className="col-12 text-start"> <div className="col-12 text-start">
<label className="form-label">Select Work Category</label> <label className="form-label">Select Work Category</label>
@ -261,7 +301,7 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
</div> </div>
)} )}
<div className="col-12 text-end mt-5"> <div className="col-12 text-end mt-5">
<button <button
type="button" type="button"
className="btn btn-sm btn-label-secondary me-3" 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 ProgressBar from "../../common/ProgressBar";
import {formatNumber} from "../../../utils/dateUtils"; 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 selectedProject = useSelector((store) => store.localVariables.projectId);
const { projects_Details, loading } = useProjectDetails(selectedProject); const { projects_Details, loading } = useProjectDetails(selectedProject);
const [IsExpandedArea, setIsExpandedArea] = useState(false); const [IsExpandedArea, setIsExpandedArea] = useState(false);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [Project, setProject] = useState(); const [Project, setProject] = useState();
// const { projectId } = useParams();
const ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA); const ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
const ManageAndAssignTak = useHasUserPermission(ASSIGN_REPORT_TASK); 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({ const [workAreaStatus, setWorkAreaStatus] = useState({
completed: 0, completed: 0,

View File

@ -17,22 +17,20 @@ import {
getCachedData, getCachedData,
useSelectedProject, useSelectedProject,
} from "../../slices/apiDataManager"; } from "../../slices/apiDataManager";
import { useProjectDetails, useProjectInfra } from "../../hooks/useProjects"; import { useProjectAssignedServices, useProjectDetails, useProjectInfra } from "../../hooks/useProjects";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { refreshData } from "../../slices/localVariablesSlice"; import { refreshData } from "../../slices/localVariablesSlice";
import eventBus from "../../services/eventBus"; import eventBus from "../../services/eventBus";
import {useParams} from "react-router-dom"; import { useParams } from "react-router-dom";
import GlobalModel from "../common/GlobalModel"; import GlobalModel from "../common/GlobalModel";
const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) => const ProjectInfra = ({ data, onDataChange, eachSiteEngineer }) => {
{
// const projectId = useSelector((store)=>store.localVariables.projectId)
const projectId = useSelectedProject(); const projectId = useSelectedProject();
const reloadedData = useSelector((store) => store.localVariables.reload); const reloadedData = useSelector((store) => store.localVariables.reload);
const [ expandedBuildings, setExpandedBuildings ] = useState( [] ); const [expandedBuildings, setExpandedBuildings] = useState([]);
const {projectInfra,isLoading,error} = useProjectInfra(projectId) const { projectInfra, isLoading, error } = useProjectInfra(projectId)
const { projects_Details, refetch, loading } = useProjectDetails(data?.id); 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 ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
const ManageTask = useHasUserPermission(MANAGE_TASK) const ManageTask = useHasUserPermission(MANAGE_TASK)
const [showModalFloor, setshowModalFloor] = useState(false); const [showModalFloor, setshowModalFloor] = useState(false);
@ -40,47 +38,46 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
const [showModalTask, setshowModalTask] = useState(false); const [showModalTask, setshowModalTask] = useState(false);
const [showModalBuilding, setshowModalBuilding] = useState(false); const [showModalBuilding, setshowModalBuilding] = useState(false);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { data: assignedServices, isLoading: servicesLoading } = useProjectAssignedServices(projectId);
const [selectedService, setSelectedService] = useState("");
const handleServiceChange = (e) => {
setSelectedService(e.target.value);
};
useEffect(() => { useEffect(() => {
setProject(projectInfra); setProject(projectInfra);
}, [data, projects_Details]); }, [data, projects_Details]);
// useEffect(() => {
// if (reloadedData) {
// refetch();
// dispatch(refreshData(false));
// }
// }, [reloadedData]);
const signalRHandler = (response) => { const signalRHandler = (response) => {
setProject(response); setProject(response);
} }
return ( return (
<> <>
{showModalBuilding && <GlobalModel isOpen={showModalBuilding} size="md" closeModal={() => setshowModalBuilding( false )}> {showModalBuilding && <GlobalModel isOpen={showModalBuilding} size="md" closeModal={() => setshowModalBuilding(false)}>
<BuildingModel <BuildingModel
project={projectInfra} project={projectInfra}
onClose={() => setshowModalBuilding( false )} onClose={() => setshowModalBuilding(false)}
/> />
</GlobalModel>} </GlobalModel>}
{showModalFloor && <GlobalModel isOpen={showModalFloor} size="md" closeModal={()=>setshowModalFloor(false)}> {showModalFloor && <GlobalModel isOpen={showModalFloor} size="md" closeModal={() => setshowModalFloor(false)}>
<FloorModel <FloorModel
project={projectInfra} project={projectInfra}
onClose={()=>setshowModalFloor(false)} onClose={() => setshowModalFloor(false)}
/> />
</GlobalModel>} </GlobalModel>}
{showModalWorkArea && <GlobalModel isOpen={showModalWorkArea} size="lg" closeModal={()=>setshowModalWorkArea(false)} > {showModalWorkArea && <GlobalModel isOpen={showModalWorkArea} size="lg" closeModal={() => setshowModalWorkArea(false)} >
<WorkAreaModel <WorkAreaModel
project={projectInfra} project={projectInfra}
onClose={()=>setshowModalWorkArea(false)} onClose={() => setshowModalWorkArea(false)}
/> />
</GlobalModel>} </GlobalModel>}
{showModalTask && ( <GlobalModel isOpen={showModalTask} size="lg" closeModal={()=>setshowModalTask(false)}> {showModalTask && (<GlobalModel isOpen={showModalTask} size="lg" closeModal={() => setshowModalTask(false)}>
<TaskModel <TaskModel
project={projectInfra} project={projectInfra}
onClose={()=>setshowModalTask(false)} onClose={() => setshowModalTask(false)}
/> />
</GlobalModel>)} </GlobalModel>)}
<div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4"> <div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4">
<div className="card"> <div className="card">
@ -88,38 +85,67 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
<div className="align-items-center"> <div className="align-items-center">
<div className="row "> <div className="row ">
<div <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 && (<> {!servicesLoading && assignedServices?.length > 0 && (
<button assignedServices.length > 1 ? (
type="button" <label>
className="link-button btn btn-xs rounded-md link-button-sm m-1 btn-primary" <select
onClick={()=>setshowModalBuilding(true)} name="DataTables_Table_0_length"
> aria-controls="DataTables_Table_0"
<i className="bx bx-plus-circle me-2"></i> className="form-select form-select-sm"
Manage Building aria-label="Select Service"
</button> value={selectedService}
<button onChange={handleServiceChange}
type="button" >
className="link-button btn btn-xs rounded-md m-1 btn-primary" <option value="">All Services</option>
onClick={()=>setshowModalFloor(true)} {assignedServices.map((service) => (
> <option key={service.id} value={service.id}>
<i className="bx bx-plus-circle me-2"></i> {service.name}
Manage Floors </option>
</button> ))}
<button </select>
type="button" </label>
className="link-button btn btn-xs rounded-md m-1 btn-primary" ) : (
onClick={() => setshowModalWorkArea(true)} <h5>{assignedServices[0].name}</h5>
> )
<i className="bx bx-plus-circle me-2"></i> )}
Manage Work Areas </div>
</button></>)}
{/* 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 {(ManageTask || ManageInfra) && (<button
type="button" type="button"
className="link-button btn btn-xs rounded-md m-1 btn-primary" 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> <i className="bx bx-plus-circle me-2"></i>
Create Tasks Create Tasks
@ -132,8 +158,7 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
<InfraTable <InfraTable
buildings={projectInfra} buildings={projectInfra}
projectId={projectId} projectId={projectId}
// handleFloor={submitData} serviceId={selectedService}
// signalRHandler ={signalRHandler}
/> />
)} )}
{!isLoading && projectInfra?.length == 0 && <div className="mt-5"><p>No Infra Avaiable</p></div>} {!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 React from "react";
import { hasUserPermission } from "../../utils/authUtils";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { import {
DIRECTORY_ADMIN, DIRECTORY_ADMIN,
DIRECTORY_MANAGER, DIRECTORY_MANAGER,
@ -13,6 +11,7 @@ import {
VIEW_DOCUMENT, VIEW_DOCUMENT,
VIEW_PROJECT_INFRA, VIEW_PROJECT_INFRA,
} from "../../utils/constants"; } from "../../utils/constants";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
const ProjectNav = ({ onPillClick, activePill }) => { const ProjectNav = ({ onPillClick, activePill }) => {
const HasViewInfraStructure = useHasUserPermission(VIEW_PROJECT_INFRA); const HasViewInfraStructure = useHasUserPermission(VIEW_PROJECT_INFRA);
@ -22,7 +21,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
const DireManager = useHasUserPermission(DIRECTORY_MANAGER); const DireManager = useHasUserPermission(DIRECTORY_MANAGER);
const DirUser = useHasUserPermission(DIRECTORY_USER); const DirUser = useHasUserPermission(DIRECTORY_USER);
const isManageTeam = useHasUserPermission(MANAGE_TEAM) const isManageTeam = useHasUserPermission(MANAGE_TEAM)
const isViewDocuments = hasUserPermission(VIEW_DOCUMENT); const isViewDocuments = useHasUserPermission(VIEW_DOCUMENT);
const isUploadDocument = useHasUserPermission(UPLOAD_DOCUMENT) const isUploadDocument = useHasUserPermission(UPLOAD_DOCUMENT)
const isModifyDocument = useHasUserPermission(MODIFY_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: "documents", icon: "bx bx-folder-open", label: "Documents",hidden:!(isViewDocuments || isModifyDocument || isUploadDocument) },
{ key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam }, { key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam },
{ key: "organization", icon: "bx bx-buildings", label: "Organization"},
]; ];
return ( return (
<div className="nav-align-top"> <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 { import {
useEmployeesByProjectAllocated, useEmployeesByProjectAllocated,
useManageProjectAllocation, useManageProjectAllocation,
useProjectAssignedServices,
} from "../../hooks/useProjects"; } from "../../hooks/useProjects";
import { useSelectedProject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
const Teams = () => { const Teams = () => {
// const {projectId} = useParams()
// const projectId = useSelector((store)=>store.localVariables.projectId)
const projectId = useSelectedProject(); const projectId = useSelectedProject();
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -37,6 +36,15 @@ const Teams = () => {
const [activeEmployee, setActiveEmployee] = useState(true); const [activeEmployee, setActiveEmployee] = useState(true);
const [deleteEmployee, setDeleteEmplyee] = useState(null); const [deleteEmployee, setDeleteEmplyee] = useState(null);
const [searchTerm, setSearchTerm] = useState(""); // State for search term 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(); const navigate = useNavigate();
@ -46,7 +54,7 @@ const Teams = () => {
projectEmployees, projectEmployees,
loading: employeeLodaing, loading: employeeLodaing,
refetch, refetch,
} = useEmployeesByProjectAllocated(projectId); } = useEmployeesByProjectAllocated(projectId, selectedService);
const { const {
mutate: submitAllocations, mutate: submitAllocations,
isPending, isPending,
@ -136,7 +144,6 @@ const Teams = () => {
useEffect(() => { useEffect(() => {
if (projectEmployees) { if (projectEmployees) {
setEmployees(projectEmployees); setEmployees(projectEmployees);
//setFilteredEmployees(projectEmployees?.filter((emp) => emp.isActive));
const filtered = projectEmployees.filter((emp) => emp.isActive); const filtered = projectEmployees.filter((emp) => emp.isActive);
setFilteredEmployees(filtered); setFilteredEmployees(filtered);
} }
@ -147,7 +154,6 @@ const Teams = () => {
setEmpJobRoles(data); setEmpJobRoles(data);
} }
}, [data]); }, [data]);
const filterAndSearchEmployees = useCallback(() => { const filterAndSearchEmployees = useCallback(() => {
const statusFiltered = employees.filter((emp) => const statusFiltered = employees.filter((emp) =>
activeEmployee ? emp.isActive : !emp.isActive activeEmployee ? emp.isActive : !emp.isActive
@ -159,33 +165,31 @@ const Teams = () => {
} }
const lowercasedSearchTerm = searchTerm.toLowerCase(); const lowercasedSearchTerm = searchTerm.toLowerCase();
const searchedAndFiltered = statusFiltered.filter((item) => { const searchedAndFiltered = statusFiltered.filter((item) => {
const fullName = const fullName = `${item.firstName} ${item.middleName} ${item.lastName}`.toLowerCase();
`${item.firstName} ${item.middleName} ${item.lastName}`.toLowerCase();
const roleName = getRole(item.jobRoleId).toLowerCase(); const roleName = getRole(item.jobRoleId).toLowerCase();
const orgName = (item.organizationName || "").toLowerCase();
const serviceName = (item.serviceName || "").toLowerCase();
return ( return (
fullName.includes(lowercasedSearchTerm) || fullName.includes(lowercasedSearchTerm) ||
roleName.includes(lowercasedSearchTerm) roleName.includes(lowercasedSearchTerm) ||
orgName.includes(lowercasedSearchTerm) ||
serviceName.includes(lowercasedSearchTerm)
); );
}); });
setFilteredEmployees(searchedAndFiltered); setFilteredEmployees(searchedAndFiltered);
}, [employees, activeEmployee, searchTerm, getRole]); }, [employees, activeEmployee, searchTerm, getRole]);
useEffect(() => { useEffect(() => {
filterAndSearchEmployees(); filterAndSearchEmployees();
}, [employees, activeEmployee, searchTerm, filterAndSearchEmployees]); }, [employees, activeEmployee, searchTerm, filterAndSearchEmployees]);
const handleFilterEmployee = (e) => { const handleFilterEmployee = (e) => {
const filterValue = e.target.value; 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"); setActiveEmployee(filterValue === "true");
setSearchTerm(""); setSearchTerm("");
}; };
@ -264,6 +268,46 @@ const Teams = () => {
<div className="card-body"> <div className="card-body">
<div className="row d-flex justify-content-between mb-4"> <div className="row d-flex justify-content-between mb-4">
<div className="col-md-6 col-12 d-flex align-items-center"> <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"> <div className="dataTables_filter d-inline-flex align-items-center ms-2">
<input <input
type="search" type="search"
@ -274,32 +318,11 @@ const Teams = () => {
onChange={handleSearch} onChange={handleSearch}
/> />
</div> </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 <button
type="button" type="button"
className={`link-button btn-primary btn-sm ${ className={`link-button btn-primary btn-sm ${HasAssignUserPermission ? "" : "d-none"
HasAssignUserPermission ? "" : "d-none" }`}
}`}
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#user-model" data-bs-target="#user-model"
> >
@ -319,6 +342,8 @@ const Teams = () => {
<th> <th>
<div className="text-start ms-5">Name</div> <div className="text-start ms-5">Name</div>
</th> </th>
<th>Services</th>
<th>Organization</th>
<th>Assigned Date</th> <th>Assigned Date</th>
{!activeEmployee && <th>Release Date</th>} {!activeEmployee && <th>Release Date</th>}
<th>Project Role</th> <th>Project Role</th>
@ -334,7 +359,7 @@ const Teams = () => {
<Avatar <Avatar
firstName={item.firstName} firstName={item.firstName}
lastName={item.lastName} lastName={item.lastName}
></Avatar> />
<div className="d-flex flex-column"> <div className="d-flex flex-column">
<a <a
onClick={() => onClick={() =>
@ -352,18 +377,17 @@ const Teams = () => {
</div> </div>
</div> </div>
</td> </td>
<td>{item.serviceName || "N/A"}</td>
<td>{item.organizationName || "N/A"}</td>
<td> <td>
{" "} {moment(item.allocationDate).format("DD-MMM-YYYY")}
{moment(item.allocationDate).format(
"DD-MMM-YYYY"
)}{" "}
</td> </td>
{!activeEmployee && ( {!activeEmployee && (
<td> <td>
{item.reAllocationDate {item.reAllocationDate
? moment(item.reAllocationDate).format( ? moment(item.reAllocationDate).format("DD-MMM-YYYY")
"DD-MMM-YYYY"
)
: "Present"} : "Present"}
</td> </td>
)} )}
@ -373,7 +397,7 @@ const Teams = () => {
</span> </span>
</td> </td>
<td> <td>
{item.isActive && ( {item.isActive ? (
<button <button
aria-label="Delete" aria-label="Delete"
type="button" type="button"
@ -381,27 +405,26 @@ const Teams = () => {
className="btn p-0 dropdown-toggle hide-arrow" className="btn p-0 dropdown-toggle hide-arrow"
onClick={() => deleteModalOpen(item)} onClick={() => deleteModalOpen(item)}
> >
{" "}
{removingEmployeeId === item.id ? ( {removingEmployeeId === item.id ? (
<div <div
className="spinner-border spinner-border-sm text-primary" className="spinner-border spinner-border-sm text-primary"
role="status" role="status"
> >
<span className="visually-hidden"> <span className="visually-hidden">Loading...</span>
Loading...
</span>
</div> </div>
) : ( ) : (
<i className="bx bx-trash me-1 text-danger"></i> <i className="bx bx-trash me-1 text-danger"></i>
)} )}
</button> </button>
) : (
<span>Not in project</span>
)} )}
{!item.isActive && <span>Not in project</span>}
</td> </td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
)} )}
{!employeeLodaing && filteredEmployees.length === 0 && ( {!employeeLodaing && filteredEmployees.length === 0 && (
<div className="text-center text-muted py-3"> <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 { useTenantContext } from "../../pages/Tenant/TenantPage";
import { useTenantDetailsContext } from "../../pages/Tenant/TenantDetails"; import { useTenantDetailsContext } from "../../pages/Tenant/TenantDetails";
import IconButton from "../common/IconButton"; import IconButton from "../common/IconButton";
import { hasUserPermission } from "../../utils/authUtils";
import { MANAGE_TENANTS } from "../../utils/constants"; import { MANAGE_TENANTS } from "../../utils/constants";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
const Profile = ({ data }) => { const Profile = ({ data }) => {
const {setEditTenant} = useTenantDetailsContext() const {setEditTenant} = useTenantDetailsContext()
const canUpdateTenant = hasUserPermission(MANAGE_TENANTS) const canUpdateTenant = useHasUserPermission(MANAGE_TENANTS)
return ( return (
<> <>
<div className="container-fuid"> <div className="container-fuid">

View File

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

View File

@ -16,6 +16,7 @@ import ManagePaymentMode from "./ManagePaymentMode";
import ManageExpenseStatus from "./ManageExpenseStatus"; import ManageExpenseStatus from "./ManageExpenseStatus";
import ManageDocumentCategory from "./ManageDocumentCategory"; import ManageDocumentCategory from "./ManageDocumentCategory";
import ManageDocumentType from "./ManageDocumentType"; import ManageDocumentType from "./ManageDocumentType";
import ManageServices from "./Services/ManageServices";
const MasterModal = ({ modaldata, closeModal }) => { const MasterModal = ({ modaldata, closeModal }) => {
if (!modaldata?.modalType || modaldata.modalType === "delete") { if (!modaldata?.modalType || modaldata.modalType === "delete") {
@ -60,6 +61,12 @@ const MasterModal = ({ modaldata, closeModal }) => {
"Edit-Document Type": ( "Edit-Document Type": (
<ManageDocumentType data={item} onClose={closeModal} /> <ManageDocumentType data={item} onClose={closeModal} />
), ),
"Services": (
<ManageServices onClose={closeModal} />
),
"Edit-Services": (
<ManageServices data={item} onClose={closeModal} />
),
}; };
return modalComponents[modalType] || null; 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 = ()=>{ export const useMasterMenu = ()=>{
return useQuery({ return useQuery({
@ -221,6 +232,12 @@ const {
return { DocumentCategories, isError, isLoading, error }; return { DocumentCategories, isError, isLoading, error };
} }
export const useOrganizationType =()=>{
return useQuery({
queryKey:["orgType"],
queryFn:async()=>await MasterRespository.getOrganizationType()
})
}
// ===Application Masters Query================================================= // ===Application Masters Query=================================================
const fetchMasterData = async (masterType) => { const fetchMasterData = async (masterType) => {
@ -231,6 +248,8 @@ const fetchMasterData = async (masterType) => {
return (await MasterRespository.getJobRole()).data; return (await MasterRespository.getJobRole()).data;
case "Activity": case "Activity":
return (await MasterRespository.getActivites()).data; return (await MasterRespository.getActivites()).data;
case "Services":
return (await MasterRespository.getService()).data;
case "Work Category": case "Work Category":
return (await MasterRespository.getWorkCategory()).data; return (await MasterRespository.getWorkCategory()).data;
case "Contact Category": case "Contact Category":
@ -667,6 +686,7 @@ export const useCreatePaymentMode = (onSuccessCallback)=>{
} }
}) })
} }
export const useUpdatePaymentMode = (onSuccessCallback)=>{ export const useUpdatePaymentMode = (onSuccessCallback)=>{
const queryClient = useQueryClient(); 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---------------------------------- // -------------------Expense Status----------------------------------
export const useCreateExpenseStatus =(onSuccessCallback)=>{ export const useCreateExpenseStatus =(onSuccessCallback)=>{
const queryClient = useQueryClient(); const queryClient = useQueryClient();

View File

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

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 { useState, useEffect } from "react";
import GlobalRepository from "../repositories/GlobalRepository"; import GlobalRepository from "../repositories/GlobalRepository";
import { useQuery } from "@tanstack/react-query";
// 🔹 Dashboard Progression Data Hook
export const useDashboard_Data = ({ days, FromDate, projectId }) => { export const useDashboard_Data = ({ days, FromDate, projectId }) => {
const [dashboard_data, setDashboard_Data] = useState([]); const [dashboard_data, setDashboard_Data] = useState([]);
const [isLineChartLoading, setLoading] = useState(false); const [isLineChartLoading, setLoading] = useState(false);
@ -38,120 +39,120 @@ export const useDashboard_Data = ({ days, FromDate, projectId }) => {
}; };
export const useDashboard_AttendanceData = (date, projectId) => { // export const useDashboard_AttendanceData = (date, projectId) => {
const [dashboard_Attendancedata, setDashboard_AttendanceData] = useState([]); // const [dashboard_Attendancedata, setDashboard_AttendanceData] = useState([]);
const [isLineChartLoading, setLoading] = useState(false); // const [isLineChartLoading, setLoading] = useState(false);
const [error, setError] = useState(""); // const [error, setError] = useState("");
useEffect(() => { // useEffect(() => {
const fetchData = async () => { // const fetchData = async () => {
setLoading(true); // setLoading(true);
setError(""); // setError("");
try { // try {
const response = await GlobalRepository.getDashboardAttendanceData(date, projectId); // date in 2nd param // const response = await GlobalRepository.getDashboardAttendanceData(date, projectId); // date in 2nd param
setDashboard_AttendanceData(response.data); // setDashboard_AttendanceData(response.data);
} catch (err) { // } catch (err) {
setError("Failed to fetch dashboard data."); // setError("Failed to fetch dashboard data.");
console.error(err); // console.error(err);
} finally { // } finally {
setLoading(false); // setLoading(false);
} // }
}; // };
if (date && projectId !== null) { // if (date && projectId !== null) {
fetchData(); // fetchData();
} // }
}, [date, projectId]); // }, [date, projectId]);
return { dashboard_Attendancedata, isLineChartLoading: isLineChartLoading, error }; // return { dashboard_Attendancedata, isLineChartLoading: isLineChartLoading, error };
}; // };
// 🔹 Dashboard Projects Card Data Hook // 🔹 Dashboard Projects Card Data Hook
export const useDashboardProjectsCardData = () => { // export const useDashboardProjectsCardData = () => {
const [projectsCardData, setProjectsData] = useState([]); // const [projectsCardData, setProjectsData] = useState([]);
const [loading, setLoading] = useState(false); // const [loading, setLoading] = useState(false);
const [error, setError] = useState(""); // const [error, setError] = useState("");
useEffect(() => { // useEffect(() => {
const fetchProjectsData = async () => { // const fetchProjectsData = async () => {
setLoading(true); // setLoading(true);
setError(""); // setError("");
try { // try {
const response = await GlobalRepository.getDashboardProjectsCardData(); // const response = await GlobalRepository.getDashboardProjectsCardData();
setProjectsData(response.data); // setProjectsData(response.data);
} catch (err) { // } catch (err) {
setError("Failed to fetch projects card data."); // setError("Failed to fetch projects card data.");
console.error(err); // console.error(err);
} finally { // } finally {
setLoading(false); // setLoading(false);
} // }
}; // };
fetchProjectsData(); // fetchProjectsData();
}, []); // }, []);
return { projectsCardData, loading, error }; // return { projectsCardData, loading, error };
}; // };
// 🔹 Dashboard Teams Card Data Hook // 🔹 Dashboard Teams Card Data Hook
export const useDashboardTeamsCardData = (projectId) => { // export const useDashboardTeamsCardData = (projectId) => {
const [teamsCardData, setTeamsData] = useState({}); // const [teamsCardData, setTeamsData] = useState({});
const [loading, setLoading] = useState(false); // const [loading, setLoading] = useState(false);
const [error, setError] = useState(""); // const [error, setError] = useState("");
useEffect(() => { // useEffect(() => {
const fetchTeamsData = async () => { // const fetchTeamsData = async () => {
setLoading(true); // setLoading(true);
setError(""); // setError("");
try { // try {
const response = await GlobalRepository.getDashboardTeamsCardData(projectId); // const response = await GlobalRepository.getDashboardTeamsCardData(projectId);
setTeamsData(response.data || {}); // setTeamsData(response.data || {});
} catch (err) { // } catch (err) {
setError("Failed to fetch teams card data."); // setError("Failed to fetch teams card data.");
console.error("Error fetching teams card data:", err); // console.error("Error fetching teams card data:", err);
setTeamsData({}); // setTeamsData({});
} finally { // } finally {
setLoading(false); // setLoading(false);
} // }
}; // };
fetchTeamsData(); // fetchTeamsData();
}, [projectId]); // }, [projectId]);
return { teamsCardData, loading, error }; // return { teamsCardData, loading, error };
}; // };
export const useDashboardTasksCardData = (projectId) => { // export const useDashboardTasksCardData = (projectId) => {
const [tasksCardData, setTasksData] = useState({}); // const [tasksCardData, setTasksData] = useState({});
const [loading, setLoading] = useState(false); // const [loading, setLoading] = useState(false);
const [error, setError] = useState(""); // const [error, setError] = useState("");
useEffect(() => { // useEffect(() => {
const fetchTasksData = async () => { // const fetchTasksData = async () => {
setLoading(true); // setLoading(true);
setError(""); // setError("");
try { // try {
const response = await GlobalRepository.getDashboardTasksCardData(projectId); // const response = await GlobalRepository.getDashboardTasksCardData(projectId);
setTasksData(response.data); // setTasksData(response.data);
} catch (err) { // } catch (err) {
setError("Failed to fetch tasks card data."); // setError("Failed to fetch tasks card data.");
console.error(err); // console.error(err);
setTasksData({}); // setTasksData({});
} finally { // } finally {
setLoading(false); // setLoading(false);
} // }
}; // };
fetchTasksData(); // fetchTasksData();
}, [projectId]); // }, [projectId]);
return { tasksCardData, loading, error }; // return { tasksCardData, loading, error };
}; // };
export const useAttendanceOverviewData = (projectId, days) => { export const useAttendanceOverviewData = (projectId, days) => {
@ -180,3 +181,75 @@ export const useAttendanceOverviewData = (projectId, days) => {
return { attendanceOverviewData, loading, error }; 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 { const {
data = [], data = [],
isLoading, isLoading,
refetch, refetch,
error, error,
} = useQuery({ } = useQuery({
queryKey: ["empListByProjectAllocated", selectedProject], queryKey: ["empListByProjectAllocated", projectId, serviceId,organizationId],
queryFn: async () => { queryFn: async () => {
const res = await ProjectRepository.getProjectAllocation(selectedProject); const res = await ProjectRepository.getProjectAllocation(
return res.data || res; projectId,
organizationId,
serviceId
);
return res?.data || res;
}, },
enabled: !!selectedProject, enabled: !!projectId,
onError: (error) => { onError: (error) => {
showToast( showToast(
error.message || "Error while Fetching project Allocated Employees", error.message || "Error while fetching project allocated employees",
"error" "error"
); );
}, },
@ -177,7 +185,7 @@ export const useProjectInfra = (projectId) => {
data: projectInfra, data: projectInfra,
isLoading, isLoading,
error, error,
isFetched isFetched,
} = useQuery({ } = useQuery({
queryKey: ["ProjectInfra", projectId], queryKey: ["ProjectInfra", projectId],
queryFn: async () => { 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) => { export const useProjectTasks = (workAreaId, serviceId = null, isExpandedArea = false) => {
const { const { data, isLoading, error } = useQuery({
data: ProjectTaskList, queryKey: ["WorkItems", workAreaId, serviceId],
isLoading,
error,
} = useQuery({
queryKey: ["WorkItems", workAreaId],
queryFn: async () => { queryFn: async () => {
const res = await ProjectRepository.getProjectTasksByWorkArea(workAreaId); const res = await ProjectRepository.getProjectTasksByWorkArea(workAreaId, serviceId);
return res.data; return res.data; // return actual task list
}, },
enabled: !!workAreaId && !!IsExpandedArea, enabled: !!workAreaId && isExpandedArea, // only fetch if workAreaId exists and area is expanded
onError: (error) => { onError: (err) => {
showToast(error.message || "Error while Fetching project Tasks", "error"); showToast(err.message || "Error while fetching project tasks", "error");
}, },
}); });
return { ProjectTaskList, isLoading, error };
return { ProjectTaskList: data, isLoading, error };
}; };
export const useProjectTasksByEmployee = (employeeId, fromDate, toDate) => { 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------------------------------- // -- -------------Mutation-------------------------------
export const useCreateProject = ({ onSuccessCallback }) => { export const useCreateProject = ({ onSuccessCallback }) => {

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { useTaskList } from "../../hooks/useTasks"; import { useTaskList } from "../../hooks/useTasks";
import { useProjectName } from "../../hooks/useProjects"; import { useProjectAssignedServices, useProjectName } from "../../hooks/useProjects";
import { setProjectId } from "../../slices/localVariablesSlice"; import { setProjectId } from "../../slices/localVariablesSlice";
import Breadcrumb from "../../components/common/Breadcrumb"; import Breadcrumb from "../../components/common/Breadcrumb";
import DateRangePicker from "../../components/common/DateRangePicker"; import DateRangePicker from "../../components/common/DateRangePicker";
@ -24,6 +24,14 @@ const DailyTask = () => {
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK); const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_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({ const [filters, setFilters] = useState({
selectedBuilding: "", selectedBuilding: "",
selectedFloors: [], selectedFloors: [],
@ -38,7 +46,6 @@ const DailyTask = () => {
dateRange?.endDate || null dateRange?.endDate || null
); );
// Ensure project is set
useEffect(() => { useEffect(() => {
if (!selectedProject && projectNames.length > 0) { if (!selectedProject && projectNames.length > 0) {
debugger debugger
@ -46,7 +53,6 @@ const DailyTask = () => {
} }
}, [selectedProject, projectNames, dispatch]); }, [selectedProject, projectNames, dispatch]);
// 🔹 Reset filters when project changes
useEffect(() => { useEffect(() => {
setFilters({ setFilters({
selectedBuilding: "", selectedBuilding: "",
@ -55,7 +61,6 @@ const DailyTask = () => {
}); });
}, [selectedProject]); }, [selectedProject]);
// Memoized filtering
const filteredTasks = useMemo(() => { const filteredTasks = useMemo(() => {
if (!TaskList) return []; if (!TaskList) return [];
return TaskList.filter((task) => { return TaskList.filter((task) => {
@ -69,7 +74,6 @@ const DailyTask = () => {
}); });
}, [TaskList, filters]); }, [TaskList, filters]);
// Memoized dates
const groupedTasks = useMemo(() => { const groupedTasks = useMemo(() => {
const groups = {}; const groups = {};
filteredTasks.forEach((task) => { filteredTasks.forEach((task) => {
@ -82,13 +86,11 @@ const DailyTask = () => {
.map((date) => ({ date, tasks: groups[date] })); .map((date) => ({ date, tasks: groups[date] }));
}, [filteredTasks]); }, [filteredTasks]);
// --- Modal State
const [modal, setModal] = useState({ type: null, data: null }); const [modal, setModal] = useState({ type: null, data: null });
const openModal = (type, data = null) => setModal({ type, data }); const openModal = (type, data = null) => setModal({ type, data });
const closeModal = () => setModal({ type: null, data: null }); const closeModal = () => setModal({ type: null, data: null });
// --- Render helpers
const renderTeamMembers = (task, refIndex) => ( const renderTeamMembers = (task, refIndex) => (
<div <div
key={refIndex} key={refIndex}
@ -133,7 +135,6 @@ const DailyTask = () => {
return ( return (
<> <>
{/* --- Modals --- */}
{modal.type === "report" && ( {modal.type === "report" && (
<GlobalModel isOpen size="md" closeModal={closeModal}> <GlobalModel isOpen size="md" closeModal={closeModal}>
<ReportTask report={modal.data} closeModal={closeModal} /> <ReportTask report={modal.data} closeModal={closeModal} />
@ -161,22 +162,60 @@ const DailyTask = () => {
<div className="container-fluid"> <div className="container-fluid">
<Breadcrumb data={[{ label: "Home", link: "/dashboard" }, { label: "Daily Progress Report" }]} /> <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"> <div className="card-body p-1 p-sm-2">
{!selectedProject && (<div className="text-center text-muted">Please Select Project</div>)} {!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 --- */} {/* --- Table --- */}
<div className="table-responsive text-nowrap mt-3" style={{ minHeight: "200px" }}> <div className="table-responsive text-nowrap mt-3" style={{ minHeight: "200px" }}>
<table className="table"> <table className="table">
@ -241,11 +280,10 @@ const DailyTask = () => {
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</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 Breadcrumb from "../../components/common/Breadcrumb";
import InfraPlanning from "../../components/Activities/InfraPlanning"; import InfraPlanning from "../../components/Activities/InfraPlanning";
import { useProjectName } from "../../hooks/useProjects"; import { useProjectName } from "../../hooks/useProjects";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch } from "react-redux";
import { setProjectId } from "../../slices/localVariablesSlice"; import { setProjectId } from "../../slices/localVariablesSlice";
import { useSelectedProject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
import { useProjectAssignedServices } from "../../hooks/useProjects";
const TaskPlannng = () => { const TaskPlannng = () => {
const selectedProject = useSelectedProject(); const selectedProject = useSelectedProject();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { projectNames = [], loading: projectLoading } = useProjectName(); const { projectNames = [], loading: projectLoading } = useProjectName();
useEffect(() => { // Service dropdown state
if (!selectedProject) { const { data: assignedServices, isLoading: servicesLoading } =
dispatch(setProjectId(projectNames[0]?.id)); useProjectAssignedServices(selectedProject);
} const [selectedService, setSelectedService] = useState("");
}, [projectNames, selectedProject?.id, dispatch]);
useEffect(() => {
if (!selectedProject && projectNames?.length > 0) {
dispatch(setProjectId(projectNames[0]?.id));
}
}, [projectNames, selectedProject, dispatch]);
const handleServiceChange = (e) => {
setSelectedService(e.target.value);
};
return ( return (
<div className="container-fluid"> <div className="container-fluid">
@ -26,11 +35,50 @@ useEffect(() => {
{ label: "Daily Task Planning" }, { label: "Daily Task Planning" },
]} ]}
/> />
{selectedProject ? (
<InfraPlanning /> <div className="card">
) : ( <div className="card-body">
<div className="text-center">Please Select Project</div> {/* 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> </div>
); );
}; };

View File

@ -69,7 +69,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
}; };
if (isError) return <div>{error.message}</div>; if (isError) return <div>{error.message}</div>;
if (isLoading) return gridView ? <CardViewContactSkeleton /> : <ListViewContactSkeleton />; // if (isLoading) return gridView ? <CardViewContactSkeleton /> : <ListViewContactSkeleton />;
return ( return (
<div className="row mt-5"> <div className="row mt-5">
@ -94,11 +94,11 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
) : ( ) : (
<div className="col-12"> <div className="col-12">
<ListViewContact <ListViewContact
data={data.data} data={data?.data}
Pagination={ Pagination={
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}
totalPages={data.totalPages} totalPages={data?.totalPages}
onPageChange={paginate} 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 React, { useEffect, useMemo } from "react";
import { useProfile } from "../../hooks/useProfile"; import { useProfile } from "../../hooks/useProfile";
import TenantDetails from "./TenantDetails"; import TenantDetails from "./TenantDetails";
import { hasUserPermission } from "../../utils/authUtils";
import { VIEW_TENANTS } from "../../utils/constants"; import { VIEW_TENANTS } from "../../utils/constants";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import Loader from "../../components/common/Loader"; import Loader from "../../components/common/Loader";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
const SelfTenantDetails = () => { const SelfTenantDetails = () => {
const { profile, loading } = useProfile(); const { profile, loading } = useProfile();
const tenantId = profile?.employeeInfo?.tenantId; const tenantId = profile?.employeeInfo?.tenantId;
const navigate = useNavigate(); const navigate = useNavigate();
const isSelfTenantView = hasUserPermission(VIEW_TENANTS); const isSelfTenantView = useHasUserPermission(VIEW_TENANTS);
useEffect(() => { useEffect(() => {
if (!isSelfTenantView) { if (!isSelfTenantView) {

View File

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

View File

@ -44,7 +44,7 @@ const LoginPage = () => {
localStorage.setItem("jwtToken", response.data.token); localStorage.setItem("jwtToken", response.data.token);
localStorage.setItem("refreshToken", response.data.refreshToken); localStorage.setItem("refreshToken", response.data.refreshToken);
setLoading(false); setLoading(false);
navigate("/dashboard"); navigate("/auth/switch/org");
} else { } else {
await AuthRepository.sendOTP({ email: data.username }); await AuthRepository.sendOTP({ email: data.username });
showToast("OTP has been sent to your email.", "success"); 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"; } from "../../hooks/useEmployees";
import { useProjectName, useProjects } from "../../hooks/useProjects"; import { useProjectName, useProjects } from "../../hooks/useProjects";
import { useProfile } from "../../hooks/useProfile"; import { useProfile } from "../../hooks/useProfile";
import { hasUserPermission } from "../../utils/authUtils";
import { import {
ITEMS_PER_PAGE, ITEMS_PER_PAGE,
MANAGE_EMPLOYEES, MANAGE_EMPLOYEES,
@ -19,7 +19,6 @@ import {
VIEW_TEAM_MEMBERS, VIEW_TEAM_MEMBERS,
} from "../../utils/constants"; } from "../../utils/constants";
import { clearCacheKey } from "../../slices/apiDataManager"; import { clearCacheKey } from "../../slices/apiDataManager";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import SuspendEmp from "../../components/Employee/SuspendEmp"; // Keep if you use SuspendEmp import SuspendEmp from "../../components/Employee/SuspendEmp"; // Keep if you use SuspendEmp
import { import {
exportToCSV, exportToCSV,
@ -36,6 +35,7 @@ import { newlineChars } from "pdf-lib";
import GlobalModel from "../../components/common/GlobalModel"; import GlobalModel from "../../components/common/GlobalModel";
import usePagination from "../../hooks/usePagination"; import usePagination from "../../hooks/usePagination";
import { setProjectId } from "../../slices/localVariablesSlice"; import { setProjectId } from "../../slices/localVariablesSlice";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
const EmployeeList = () => { const EmployeeList = () => {
const selectedProjectId = useSelector( const selectedProjectId = useSelector(

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { api } from "../utils/axiosClient";
const AuthRepository = { const AuthRepository = {
// Public routes (no auth token required) // 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), refreshToken: (data) => api.postPublic("/api/auth/refresh-token", data),
forgotPassword: (data) => api.postPublic("/api/auth/forgot-password", data), forgotPassword: (data) => api.postPublic("/api/auth/forgot-password", data),
resetPassword: (data) => api.postPublic("/api/auth/reset-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), logout: (data) => api.post("/api/auth/logout", data),
profile: () => api.get("/api/user/profile"), profile: () => api.get("/api/user/profile"),
changepassword: (data) => api.post("/api/auth/change-password", data), 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"), getActivites: () => api.get("api/master/activities"),
createActivity: (data) => api.post("api/master/activity", data), 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) => updateActivity: (id, data) =>
api.post(`api/master/activity/edit/${id}`, data), api.post(`api/master/activity/edit/${id}`, data),
getIndustries: () => api.get("api/master/industries"), getIndustries: () => api.get("api/master/industries"),
@ -106,4 +113,11 @@ export const MasterRespository = {
createDocumentType: (data) => api.post(`/api/Master/document-type`, data), createDocumentType: (data) => api.post(`/api/Master/document-type`, data),
updateDocumentType: (id, data) => updateDocumentType: (id, data) =>
api.put(`/api/Master/document-type/edit/${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) => getProjectByprojectId: (projetid) =>
api.get(`/api/project/details/${projetid}`), api.get(`/api/project/details/${projetid}`),
getProjectAllocation: (projetid) => getProjectAllocation: (projectId, organizationId, serviceId) => {
api.get(`api/project/allocation/${projetid}`), 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) => getEmployeesByProject: (projectId) =>
api.get(`/api/Project/employees/get/${projectId}`), api.get(`/api/Project/employees/get/${projectId}`),
manageProject: (data) => api.post("/api/project", data), manageProject: (data) => api.post("/api/project", data),
// updateProject: (data) => api.post("/api/project/update", data),
manageProjectAllocation: (data) => api.post("/api/project/allocation", data), manageProjectAllocation: (data) => api.post("/api/project/allocation", data),
@ -32,20 +43,31 @@ const ProjectRepository = {
getProjectDetails: (id) => api.get(`/api/project/details/${id}`), getProjectDetails: (id) => api.get(`/api/project/details/${id}`),
getProjectInfraByproject: (id) => api.get(`/api/project/infra-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) => getProjectTasksByEmployee: (id, fromDate, toDate) =>
api.get( api.get(
`/api/project/tasks-employee/${id}?fromDate=${fromDate}&toDate=${toDate}` `/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`), // Services
getProjectLevelEmployeePermissions:(employeeId,projectId)=>api.get(`/api/Project/get/project-level-permission/employee/${employeeId}/project/${projectId}`), getProjectAssignedServices: (projectId) => api.get(`/api/Project/get/assigned/services/${projectId}`),
updateProjectLevelEmployeePermission:(data)=>api.post(`/api/Project/assign/project-level-permission`,data), getProjectAssignedOrganizations: (projectId) => api.get(`/api/Project/get/assigned/organization/${projectId}`)
getAllProjectLevelPermission:(projectId)=>api.get(`/api/Project/get/all/project-level-permission/${projectId}`)
}; };
export const TasksRepository = { export const TasksRepository = {

View File

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

View File

@ -8,7 +8,7 @@ const ProtectedRoute = () => {
// // const isAuthenticated = true; // // const isAuthenticated = true;
// isTokenValid(); // isTokenValid();
// return isAuthenticated ? <Outlet /> : <Navigate to="/auth/login" /> // return isAuthenticated ? <Outlet /> : <Navigate to="/auth/login" />
const [isAuthenticated, setIsAuthenticated] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(null);
useEffect(() => { useEffect(() => {
@ -66,7 +66,7 @@ const attemptTokenRefresh = async (storedRefreshToken) => {
localStorage.setItem("jwtToken", response.data.token); localStorage.setItem("jwtToken", response.data.token);
localStorage.setItem("refreshToken", response.data.refreshToken); localStorage.setItem("refreshToken", response.data.refreshToken);
return true; return true;
// api // api
// .post("/api/auth/refresh-token", { // .post("/api/auth/refresh-token", {
// token: localStorage.getItem("jwtToken"), // token: localStorage.getItem("jwtToken"),
// refreshToken: refreshToken, // 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({ const globalVariablesSlice = createSlice({
name: "globalVariables", name: "globalVariables",
initialState: { initialState: {
loginUser:null, loginUser: null,
currentTenant:null currentTenant: null,
}, },
reducers: { reducers: {
setGlobalVariable: (state, action) => { setGlobalVariable: (state, action) => {
const { key, value } = action.payload; const { key, value } = action.payload;
state[key] = value; state[key] = value;
}, },
setLoginUserPermmisions: ( state, action ) => setLoginUserPermmisions: (state, action) => {
{ state.loginUser = action.payload;
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; export default globalVariablesSlice.reducer;

View File

@ -3,37 +3,86 @@ import { createSlice } from "@reduxjs/toolkit";
const localVariablesSlice = createSlice({ const localVariablesSlice = createSlice({
name: "localVariables", name: "localVariables",
initialState: { initialState: {
selectedMaster:"Application Role", selectedMaster: "Application Role",
regularizationCount:0, regularizationCount: 0,
defaultDateRange: { defaultDateRange: {
startDate: null, startDate: null,
endDate: null, endDate: null,
}, },
projectId: null, projectId: null,
reload:false reload: false,
OrganizationModal: {
isOpen: false,
orgData: null,
prevStep: null,
startStep: 1,
flowType: "default",
},
AuthModal: {
isOpen: false,
},
}, },
reducers: { reducers: {
changeMaster: (state, action) => { changeMaster: (state, action) => {
state.selectedMaster = action.payload; state.selectedMaster = action.payload;
}, },
updateRegularizationCount: (state, action) => { updateRegularizationCount: (state, action) => {
state.regularizationCount = action.payload; state.regularizationCount = action.payload;
}, },
setProjectId: (state, action) => { setProjectId: (state, action) => {
localStorage.setItem("project",null) localStorage.setItem("project", null);
state.projectId = action.payload; state.projectId = action.payload;
localStorage.setItem("project",state.projectId || null) localStorage.setItem("project", state.projectId || null);
}, },
refreshData: ( state, action ) => refreshData: (state, action) => {
{ state.reload = action.payload;
state.reload = action.payload
}, },
setDefaultDateRange: (state, action) => { setDefaultDateRange: (state, action) => {
state.defaultDateRange = action.payload; 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 const {
export default localVariablesSlice.reducer; 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 apiCacheReducer from "../slices/apiCacheSlice";
import globalVariablesReducer from "../slices/globalVariablesSlice"; import globalVariablesReducer from "../slices/globalVariablesSlice";
import localVariableRducer from "../slices/localVariablesSlice" import localVariableRducer from "../slices/localVariablesSlice"
import attendanceReducer from "../slices/apiSlice/attedanceLogsSlice"
import employeeAttendanceReducer from "../slices/apiSlice/employeeAttendanceSlice"
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
apiCache: apiCacheReducer, apiCache: apiCacheReducer,
globalVariables: globalVariablesReducer, globalVariables: globalVariablesReducer,
localVariables:localVariableRducer, 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 showToast from "../services/toastService";
import { startSignalR, stopSignalR } from "../services/signalRService"; import { startSignalR, stopSignalR } from "../services/signalRService";
import { BASE_URL } from "./constants"; import { BASE_URL } from "./constants";
const base_Url = BASE_URL const base_Url = BASE_URL;
export const axiosClient = axios.create({ export const axiosClient = axios.create({
baseURL: base_Url, baseURL: base_Url,
@ -44,7 +44,10 @@ axiosClient.interceptors.response.use(
const originalRequest = error.config; const originalRequest = error.config;
// Skip retry for public requests or already retried ones // 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); return Promise.reject(error);
} }
@ -53,7 +56,10 @@ axiosClient.interceptors.response.use(
originalRequest._toastShown = true; originalRequest._toastShown = true;
if (error.code === "ERR_CONNECTION_REFUSED") { 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") { } else if (error.code === "ERR_NETWORK") {
showToast("Network error. Please check your connection.", "error"); showToast("Network error. Please check your connection.", "error");
redirectToLogin(); redirectToLogin();
@ -68,7 +74,10 @@ axiosClient.interceptors.response.use(
const refreshToken = localStorage.getItem("refreshToken"); 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(); redirectToLogin();
return Promise.reject(error); return Promise.reject(error);
} }
@ -88,7 +97,7 @@ axiosClient.interceptors.response.use(
localStorage.setItem("jwtToken", token); localStorage.setItem("jwtToken", token);
localStorage.setItem("refreshToken", newRefreshToken); localStorage.setItem("refreshToken", newRefreshToken);
startSignalR() startSignalR();
// Set Authorization header // Set Authorization header
originalRequest.headers["Authorization"] = `Bearer ${token}`; originalRequest.headers["Authorization"] = `Bearer ${token}`;
return axiosClient(originalRequest); return axiosClient(originalRequest);
@ -160,4 +169,4 @@ export const api = {
// Redirect helper // Redirect helper
function redirectToLogin() { function redirectToLogin() {
window.location.href = "/auth/login"; window.location.href = "/auth/login";
} }