Merge pull request 'Tenant_Manag : Feature #901 : Tenant Management' (#334) from Tenant_Manag into main

Reviewed-on: #334
Merged
This commit is contained in:
pramod.mahajan 2025-08-26 10:05:29 +00:00
commit 9dddba4e30
50 changed files with 4045 additions and 510 deletions

View File

@ -45,6 +45,7 @@
<link rel="stylesheet" href="/assets/vendor/libs/animate-css/animate.css" /> <link rel="stylesheet" href="/assets/vendor/libs/animate-css/animate.css" />
<link rel="stylesheet" href="/assets/vendor/libs/sweetalert2/sweetalert2.css" /> <link rel="stylesheet" href="/assets/vendor/libs/sweetalert2/sweetalert2.css" />
<link rel="stylesheet" href="/assets/vendor/libs/spinkit/spinkit.css" />
<!-- Helpers --> <!-- Helpers -->
<script src="/assets/vendor/js/helpers.js"></script> <script src="/assets/vendor/js/helpers.js"></script>

View File

@ -18609,6 +18609,9 @@ li:not(:first-child) .dropdown-item,
.min-vh-100 { .min-vh-100 {
min-height: 100vh !important; min-height: 100vh !important;
} }
.page-min-h{
min-height: 70vh !important;
}
.flex-fill { .flex-fill {
flex: 1 1 auto !important; flex: 1 1 auto !important;

View File

@ -0,0 +1,837 @@
/* Config */
:root {
--sk-size: 40px;
--sk-color: #ff3e1d;
}
/* Utility class for centering */
.sk-center {
margin: auto;
}
/* Plane
<div class="sk-plane"></div>
*/
.sk-plane {
width: var(--sk-size);
height: var(--sk-size);
background-color: var(--sk-color);
animation: sk-plane 1.2s infinite ease-in-out;
}
@keyframes sk-plane {
0% {
transform: perspective(120px) rotateX(0deg) rotateY(0deg);
}
50% {
transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);
}
100% {
transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
}
}
/* Chase
<div class="sk-chase">
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
</div>
*/
.sk-chase {
width: var(--sk-size);
height: var(--sk-size);
position: relative;
animation: sk-chase 2.5s infinite linear both;
}
.sk-chase-dot {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
animation: sk-chase-dot 2s infinite ease-in-out both;
}
.sk-chase-dot:before {
content: "";
display: block;
width: 25%;
height: 25%;
background-color: var(--sk-color);
border-radius: 100%;
animation: sk-chase-dot-before 2s infinite ease-in-out both;
}
.sk-chase-dot:nth-child(1) {
animation-delay: -1.1s;
}
.sk-chase-dot:nth-child(2) {
animation-delay: -1s;
}
.sk-chase-dot:nth-child(3) {
animation-delay: -0.9s;
}
.sk-chase-dot:nth-child(4) {
animation-delay: -0.8s;
}
.sk-chase-dot:nth-child(5) {
animation-delay: -0.7s;
}
.sk-chase-dot:nth-child(6) {
animation-delay: -0.6s;
}
.sk-chase-dot:nth-child(1):before {
animation-delay: -1.1s;
}
.sk-chase-dot:nth-child(2):before {
animation-delay: -1s;
}
.sk-chase-dot:nth-child(3):before {
animation-delay: -0.9s;
}
.sk-chase-dot:nth-child(4):before {
animation-delay: -0.8s;
}
.sk-chase-dot:nth-child(5):before {
animation-delay: -0.7s;
}
.sk-chase-dot:nth-child(6):before {
animation-delay: -0.6s;
}
@keyframes sk-chase {
100% {
transform: rotate(360deg);
}
}
@keyframes sk-chase-dot {
80%, 100% {
transform: rotate(360deg);
}
}
@keyframes sk-chase-dot-before {
50% {
transform: scale(0.4);
}
100%, 0% {
transform: scale(1);
}
}
/* Bounce
<div class="sk-bounce">
<div class="sk-bounce-dot"></div>
<div class="sk-bounce-dot"></div>
</div>
*/
.sk-bounce {
width: var(--sk-size);
height: var(--sk-size);
position: relative;
}
.sk-bounce-dot {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: var(--sk-color);
opacity: 0.6;
position: absolute;
top: 0;
left: 0;
animation: sk-bounce 2s infinite cubic-bezier(0.455, 0.03, 0.515, 0.955);
}
.sk-bounce-dot:nth-child(2) {
animation-delay: -1s;
}
@keyframes sk-bounce {
0%, 100% {
transform: scale(0);
}
45%, 55% {
transform: scale(1);
}
}
/* Wave
<div class="sk-wave">
<div class="sk-wave-rect"></div>
<div class="sk-wave-rect"></div>
<div class="sk-wave-rect"></div>
<div class="sk-wave-rect"></div>
<div class="sk-wave-rect"></div>
</div>
*/
.sk-wave {
width: var(--sk-size);
height: var(--sk-size);
display: flex;
justify-content: space-between;
}
.sk-wave-rect {
background-color: var(--sk-color);
height: 100%;
width: 15%;
animation: sk-wave 1.2s infinite ease-in-out;
}
.sk-wave-rect:nth-child(1) {
animation-delay: -1.2s;
}
.sk-wave-rect:nth-child(2) {
animation-delay: -1.1s;
}
.sk-wave-rect:nth-child(3) {
animation-delay: -1s;
}
.sk-wave-rect:nth-child(4) {
animation-delay: -0.9s;
}
.sk-wave-rect:nth-child(5) {
animation-delay: -0.8s;
}
@keyframes sk-wave {
0%, 40%, 100% {
transform: scaleY(0.4);
}
20% {
transform: scaleY(1);
}
}
/* Pulse
<div class="sk-pulse"></div>
*/
.sk-pulse {
width: var(--sk-size);
height: var(--sk-size);
background-color: var(--sk-color);
border-radius: 100%;
animation: sk-pulse 1.2s infinite cubic-bezier(0.455, 0.03, 0.515, 0.955);
}
@keyframes sk-pulse {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
opacity: 0;
}
}
/* Flow
<div class="sk-flow">
<div class="sk-flow-dot"></div>
<div class="sk-flow-dot"></div>
<div class="sk-flow-dot"></div>
</div>
*/
.sk-flow {
width: calc(var(--sk-size) * 1.3);
height: calc(var(--sk-size) * 1.3);
display: flex;
justify-content: space-between;
}
.sk-flow-dot {
width: 25%;
height: 25%;
background-color: var(--sk-color);
border-radius: 50%;
animation: sk-flow 1.4s cubic-bezier(0.455, 0.03, 0.515, 0.955) 0s infinite both;
}
.sk-flow-dot:nth-child(1) {
animation-delay: -0.3s;
}
.sk-flow-dot:nth-child(2) {
animation-delay: -0.15s;
}
@keyframes sk-flow {
0%, 80%, 100% {
transform: scale(0.3);
}
40% {
transform: scale(1);
}
}
/* Swing
<div class="sk-swing">
<div class="sk-swing-dot"></div>
<div class="sk-swing-dot"></div>
</div>
*/
.sk-swing {
width: var(--sk-size);
height: var(--sk-size);
position: relative;
animation: sk-swing 1.8s infinite linear;
}
.sk-swing-dot {
width: 45%;
height: 45%;
position: absolute;
top: 0;
left: 0;
right: 0;
margin: auto;
background-color: var(--sk-color);
border-radius: 100%;
animation: sk-swing-dot 2s infinite ease-in-out;
}
.sk-swing-dot:nth-child(2) {
top: auto;
bottom: 0;
animation-delay: -1s;
}
@keyframes sk-swing {
100% {
transform: rotate(360deg);
}
}
@keyframes sk-swing-dot {
0%, 100% {
transform: scale(0.2);
}
50% {
transform: scale(1);
}
}
/* Circle
<div class="sk-circle">
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
</div>
*/
.sk-circle {
width: var(--sk-size);
height: var(--sk-size);
position: relative;
}
.sk-circle-dot {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
.sk-circle-dot:before {
content: "";
display: block;
width: 15%;
height: 15%;
background-color: var(--sk-color);
border-radius: 100%;
animation: sk-circle 1.2s infinite ease-in-out both;
}
.sk-circle-dot:nth-child(1) {
transform: rotate(30deg);
}
.sk-circle-dot:nth-child(2) {
transform: rotate(60deg);
}
.sk-circle-dot:nth-child(3) {
transform: rotate(90deg);
}
.sk-circle-dot:nth-child(4) {
transform: rotate(120deg);
}
.sk-circle-dot:nth-child(5) {
transform: rotate(150deg);
}
.sk-circle-dot:nth-child(6) {
transform: rotate(180deg);
}
.sk-circle-dot:nth-child(7) {
transform: rotate(210deg);
}
.sk-circle-dot:nth-child(8) {
transform: rotate(240deg);
}
.sk-circle-dot:nth-child(9) {
transform: rotate(270deg);
}
.sk-circle-dot:nth-child(10) {
transform: rotate(300deg);
}
.sk-circle-dot:nth-child(11) {
transform: rotate(330deg);
}
.sk-circle-dot:nth-child(1):before {
animation-delay: -1.1s;
}
.sk-circle-dot:nth-child(2):before {
animation-delay: -1s;
}
.sk-circle-dot:nth-child(3):before {
animation-delay: -0.9s;
}
.sk-circle-dot:nth-child(4):before {
animation-delay: -0.8s;
}
.sk-circle-dot:nth-child(5):before {
animation-delay: -0.7s;
}
.sk-circle-dot:nth-child(6):before {
animation-delay: -0.6s;
}
.sk-circle-dot:nth-child(7):before {
animation-delay: -0.5s;
}
.sk-circle-dot:nth-child(8):before {
animation-delay: -0.4s;
}
.sk-circle-dot:nth-child(9):before {
animation-delay: -0.3s;
}
.sk-circle-dot:nth-child(10):before {
animation-delay: -0.2s;
}
.sk-circle-dot:nth-child(11):before {
animation-delay: -0.1s;
}
@keyframes sk-circle {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
/* Circle Fade
<div class="sk-circle-fade">
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
</div>
*/
.sk-circle-fade {
width: var(--sk-size);
height: var(--sk-size);
position: relative;
}
.sk-circle-fade-dot {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
.sk-circle-fade-dot:before {
content: "";
display: block;
width: 15%;
height: 15%;
background-color: var(--sk-color);
border-radius: 100%;
animation: sk-circle-fade 1.2s infinite ease-in-out both;
}
.sk-circle-fade-dot:nth-child(1) {
transform: rotate(30deg);
}
.sk-circle-fade-dot:nth-child(2) {
transform: rotate(60deg);
}
.sk-circle-fade-dot:nth-child(3) {
transform: rotate(90deg);
}
.sk-circle-fade-dot:nth-child(4) {
transform: rotate(120deg);
}
.sk-circle-fade-dot:nth-child(5) {
transform: rotate(150deg);
}
.sk-circle-fade-dot:nth-child(6) {
transform: rotate(180deg);
}
.sk-circle-fade-dot:nth-child(7) {
transform: rotate(210deg);
}
.sk-circle-fade-dot:nth-child(8) {
transform: rotate(240deg);
}
.sk-circle-fade-dot:nth-child(9) {
transform: rotate(270deg);
}
.sk-circle-fade-dot:nth-child(10) {
transform: rotate(300deg);
}
.sk-circle-fade-dot:nth-child(11) {
transform: rotate(330deg);
}
.sk-circle-fade-dot:nth-child(1):before {
animation-delay: -1.1s;
}
.sk-circle-fade-dot:nth-child(2):before {
animation-delay: -1s;
}
.sk-circle-fade-dot:nth-child(3):before {
animation-delay: -0.9s;
}
.sk-circle-fade-dot:nth-child(4):before {
animation-delay: -0.8s;
}
.sk-circle-fade-dot:nth-child(5):before {
animation-delay: -0.7s;
}
.sk-circle-fade-dot:nth-child(6):before {
animation-delay: -0.6s;
}
.sk-circle-fade-dot:nth-child(7):before {
animation-delay: -0.5s;
}
.sk-circle-fade-dot:nth-child(8):before {
animation-delay: -0.4s;
}
.sk-circle-fade-dot:nth-child(9):before {
animation-delay: -0.3s;
}
.sk-circle-fade-dot:nth-child(10):before {
animation-delay: -0.2s;
}
.sk-circle-fade-dot:nth-child(11):before {
animation-delay: -0.1s;
}
@keyframes sk-circle-fade {
0%, 39%, 100% {
opacity: 0;
transform: scale(0.6);
}
40% {
opacity: 1;
transform: scale(1);
}
}
/* Grid
<div class="sk-grid">
<div class="sk-grid-cube"></div>
<div class="sk-grid-cube"></div>
<div class="sk-grid-cube"></div>
<div class="sk-grid-cube"></div>
<div class="sk-grid-cube"></div>
<div class="sk-grid-cube"></div>
<div class="sk-grid-cube"></div>
<div class="sk-grid-cube"></div>
<div class="sk-grid-cube"></div>
</div>
*/
.sk-grid {
width: var(--sk-size);
height: var(--sk-size);
/* Cube positions
* 1 2 3
* 4 5 6
* 7 8 9
*/
}
.sk-grid-cube {
width: 33.33%;
height: 33.33%;
background-color: var(--sk-color);
float: left;
animation: sk-grid 1.3s infinite ease-in-out;
}
.sk-grid-cube:nth-child(1) {
animation-delay: 0.2s;
}
.sk-grid-cube:nth-child(2) {
animation-delay: 0.3s;
}
.sk-grid-cube:nth-child(3) {
animation-delay: 0.4s;
}
.sk-grid-cube:nth-child(4) {
animation-delay: 0.1s;
}
.sk-grid-cube:nth-child(5) {
animation-delay: 0.2s;
}
.sk-grid-cube:nth-child(6) {
animation-delay: 0.3s;
}
.sk-grid-cube:nth-child(7) {
animation-delay: 0s;
}
.sk-grid-cube:nth-child(8) {
animation-delay: 0.1s;
}
.sk-grid-cube:nth-child(9) {
animation-delay: 0.2s;
}
@keyframes sk-grid {
0%, 70%, 100% {
transform: scale3D(1, 1, 1);
}
35% {
transform: scale3D(0, 0, 1);
}
}
/* Fold
<div class="sk-fold">
<div class="sk-fold-cube"></div>
<div class="sk-fold-cube"></div>
<div class="sk-fold-cube"></div>
<div class="sk-fold-cube"></div>
</div>
*/
.sk-fold {
width: var(--sk-size);
height: var(--sk-size);
position: relative;
transform: rotateZ(45deg);
}
.sk-fold-cube {
float: left;
width: 50%;
height: 50%;
position: relative;
transform: scale(1.1);
}
.sk-fold-cube:before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--sk-color);
animation: sk-fold 2.4s infinite linear both;
transform-origin: 100% 100%;
}
.sk-fold-cube:nth-child(2) {
transform: scale(1.1) rotateZ(90deg);
}
.sk-fold-cube:nth-child(4) {
transform: scale(1.1) rotateZ(180deg);
}
.sk-fold-cube:nth-child(3) {
transform: scale(1.1) rotateZ(270deg);
}
.sk-fold-cube:nth-child(2):before {
animation-delay: 0.3s;
}
.sk-fold-cube:nth-child(4):before {
animation-delay: 0.6s;
}
.sk-fold-cube:nth-child(3):before {
animation-delay: 0.9s;
}
@keyframes sk-fold {
0%, 10% {
transform: perspective(140px) rotateX(-180deg);
opacity: 0;
}
25%, 75% {
transform: perspective(140px) rotateX(0deg);
opacity: 1;
}
90%, 100% {
transform: perspective(140px) rotateY(180deg);
opacity: 0;
}
}
/* Wander
<div class="sk-wander">
<div class="sk-wander-cube"></div>
<div class="sk-wander-cube"></div>
<div class="sk-wander-cube"></div>
<div class="sk-wander-cube"></div>
</div>
*/
.sk-wander {
width: var(--sk-size);
height: var(--sk-size);
position: relative;
}
.sk-wander-cube {
background-color: var(--sk-color);
width: 20%;
height: 20%;
position: absolute;
top: 0;
left: 0;
--sk-wander-distance: calc(var(--sk-size) * 0.75);
animation: sk-wander 2s ease-in-out -2s infinite both;
}
.sk-wander-cube:nth-child(2) {
animation-delay: -0.5s;
}
.sk-wander-cube:nth-child(3) {
animation-delay: -1s;
}
@keyframes sk-wander {
0% {
transform: rotate(0deg);
}
25% {
transform: translateX(var(--sk-wander-distance)) rotate(-90deg) scale(0.6);
}
50% { /* Make FF rotate in the right direction */
transform: translateX(var(--sk-wander-distance)) translateY(var(--sk-wander-distance)) rotate(-179deg);
}
50.1% {
transform: translateX(var(--sk-wander-distance)) translateY(var(--sk-wander-distance)) rotate(-180deg);
}
75% {
transform: translateX(0) translateY(var(--sk-wander-distance)) rotate(-270deg) scale(0.6);
}
100% {
transform: rotate(-360deg);
}
}
:root {
--sk-size: 30px;
}
.sk-wave {
width: 40px;
white-space: nowrap;
}
.sk-fading-circle .sk-circle {
margin-top: 0;
margin-bottom: 0;
}
.sk-wave {
width: 40px;
white-space: nowrap;
}
.sk-fading-circle .sk-circle {
margin-top: 0;
margin-bottom: 0;
}

View File

@ -16,6 +16,7 @@ import {useProfile} from "../../hooks/useProfile";
import {refreshData, setProjectId} from "../../slices/localVariablesSlice"; import {refreshData, setProjectId} from "../../slices/localVariablesSlice";
import InfraTable from "../Project/Infrastructure/InfraTable"; import InfraTable from "../Project/Infrastructure/InfraTable";
import { useSelectedproject } from "../../slices/apiDataManager"; import { useSelectedproject } from "../../slices/apiDataManager";
import Loader from "../common/Loader";
const InfraPlanning = () => const InfraPlanning = () =>
@ -51,7 +52,7 @@ const InfraPlanning = () =>
{(ApprovedTaskRights || ReportTaskRights) ? ( {(ApprovedTaskRights || ReportTaskRights) ? (
<div className="align-items-center"> <div className="align-items-center">
<div className="row "> <div className="row ">
{isLoading && ( <p>Loading...</p> )} {isLoading && (<Loader/> )}
{( !isLoading && projectInfra?.length === 0 ) && ( <p>No Result Found</p> )} {( !isLoading && projectInfra?.length === 0 ) && ( <p>No Result Found</p> )}
{(!isLoading && projectInfra?.length > 0) && (<InfraTable buildings={projectInfra} projectId={selectedProject}/>)} {(!isLoading && projectInfra?.length > 0) && (<InfraTable buildings={projectInfra} projectId={selectedProject}/>)}
</div> </div>

View File

@ -202,3 +202,4 @@ export const ReportTask = ({ report, closeModal }) => {
</div> </div>
); );
}; };
export default ReportTask;

View File

@ -22,6 +22,7 @@ const EmpAttendance = ({ employee }) => {
data = [], data = [],
isLoading: loading, isLoading: loading,
isFetching, isFetching,
isError,
error, error,
refetch, refetch,
} = useAttendanceByEmployee(employee, dateRange.startDate, dateRange.endDate); } = useAttendanceByEmployee(employee, dateRange.startDate, dateRange.endDate);
@ -145,7 +146,7 @@ const EmpAttendance = ({ employee }) => {
</div> </div>
<div className="table-responsive text-nowrap"> <div className="table-responsive text-nowrap">
{!loading && data.length === 0 && <span>No employee logs</span>} {!loading && data.length === 0 && <span>No employee logs</span>}
{error && <div className="text-center">{error}</div>} {isError && <div className="text-center">{error.message}</div>}
{loading && !data && <div className="text-center">Loading...</div>} {loading && !data && <div className="text-center">Loading...</div>}
{data && data.length > 0 && ( {data && data.length > 0 && (
<table className="table mb-0"> <table className="table mb-0">

View File

@ -34,7 +34,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
isLoading, isLoading,
error: ExpenseErrorLoad, error: ExpenseErrorLoad,
} = useExpense(expenseToEdit); } = useExpense(expenseToEdit);
console.log(data)
const [ExpenseType, setExpenseType] = useState(); const [ExpenseType, setExpenseType] = useState();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { const {

View File

@ -0,0 +1,36 @@
const SkeletonLine = ({ height = 50, width = "100%", className = "" }) => (
<div
className={`skeleton mb-2 ${className}`}
style={{
height,
width,
}}
></div>
);
export const MenuItemSkeleton = ({ hasSubmenu = false, submenuCount = 3 }) => {
return (
<li className="menu-item">
<div className="menu-link d-flex align-items-center gap-2 ">
{/* icon placeholder */}
<SkeletonLine height={25} width="25px" className="rounded" />
{/* text placeholder */}
<SkeletonLine height={25} width="100%" />
</div>
{/* Submenu skeletons */}
{hasSubmenu && (
<ul className="menu-sub mt-1 ms-4">
{[...Array(submenuCount)].map((_, idx) => (
<li key={idx} className="menu-item">
<div className="menu-link d-flex align-items-center gap-2">
<SkeletonLine height={20} width="20px" className="rounded" />
<SkeletonLine height={24} width="100px" />
</div>
</li>
))}
</ul>
)}
</li>
);
};

View File

@ -2,10 +2,12 @@ import React from "react";
import { Link, NavLink, useLocation, useNavigate } from "react-router-dom"; import { Link, NavLink, useLocation, useNavigate } from "react-router-dom";
import menuData from "../../data/menuData.json"; import menuData from "../../data/menuData.json";
import { getCachedProfileData } from "../../slices/apiDataManager"; import { getCachedProfileData } from "../../slices/apiDataManager";
import { useSidBarMenu } from "../../hooks/useProfile";
import { MenuItemSkeleton } from "./MenuItemSkeleton";
const Sidebar = () => { const Sidebar = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { data, isError, isLoading, isFetched, error } = useSidBarMenu();
return ( return (
<aside <aside
id="layout-menu" id="layout-menu"
@ -33,33 +35,49 @@ const Sidebar = () => {
<div className="menu-inner-shadow"></div> <div className="menu-inner-shadow"></div>
<ul className="menu-inner py-1"> <ul className="menu-inner py-1">
{menuData.map((section) => ( {isError && (
<React.Fragment key={(Math.random() + 1).toString(36)}> <div className="text-center text-small">{error.message}</div>
{section.header && ( )}
{isLoading && (
<>
{[...Array(7)].map((_, idx) => (
<MenuItemSkeleton
key={idx}
hasSubmenu={idx % 2 === 1}
submenuCount={Math.floor(Math.random() * 3) + 2}
/>
))}
</>
)}
{data &&
data?.data.map((section) => (
<React.Fragment
key={section.id || section.header || section.items[0]?.id}
>
{/* {section.header && (
<li className="menu-header small text-uppercase"> <li className="menu-header small text-uppercase">
<span className="menu-header-text">{section.header}</span> <span className="menu-header-text">{section.header}</span>
</li> </li>
)} )} */}
{section.items.map(MenuItem)} {section.items.map((item) => (
</React.Fragment> <MenuItem key={item.id || item.link} {...item} />
))} ))}
</React.Fragment>
))}
</ul> </ul>
</aside> </aside>
); );
}; };
const MenuItem = (item) => { const MenuItem = (item) => {
item.id = Math.random();
const location = useLocation(); const location = useLocation();
const isActive = location.pathname === item.link; const isActive = location.pathname === item.link;
const hasSubmenu = item.submenu && item.submenu.length > 0; const hasSubmenu = Array.isArray(item.submenu) && item.submenu.length > 0;
const isSubmenuActive = const isSubmenuActive =
hasSubmenu && hasSubmenu && item.submenu.some((sub) => location.pathname === sub.link);
item.submenu.some((subitem) => location.pathname === subitem.link);
return ( return (
<li <li
key={(Math.random() + 1).toString(36)}
className={`menu-item ${isActive || isSubmenuActive ? "active" : ""} ${ className={`menu-item ${isActive || isSubmenuActive ? "active" : ""} ${
hasSubmenu && isSubmenuActive ? "open" : "" hasSubmenu && isSubmenuActive ? "open" : ""
}`} }`}
@ -67,21 +85,24 @@ const MenuItem = (item) => {
<NavLink <NavLink
aria-label={`Navigate to ${item.text} ${!item.available ? "Pro" : ""}`} aria-label={`Navigate to ${item.text} ${!item.available ? "Pro" : ""}`}
to={item.link} to={item.link}
className={`menu-link ${item.submenu ? "menu-toggle" : ""}`} className={`menu-link ${hasSubmenu ? "menu-toggle" : ""}`}
key={(Math.random() + 1).toString(36)} target={item.link?.includes("http") ? "_blank" : undefined}
target={item.link.includes("http") ? "_blank" : undefined}
> >
<i className={`menu-icon tf-icons ${item.icon}`}></i> <i className={`menu-icon tf-icons ${item.icon}`}></i>
<div>{item.text}</div>{" "} <div>{item.name}</div>
{item.available === false && ( {item.available === false && (
<div className="badge bg-label-primary fs-tiny rounded-pill ms-auto"> <div className="badge bg-label-primary fs-tiny rounded-pill ms-auto">
Pro Pro
</div> </div>
)} )}
</NavLink> </NavLink>
{item.submenu && (
<ul className="menu-sub" key={(Math.random() + 1).toString(36)}> {/* Only render submenu if exists */}
{item.submenu.map(MenuItem)} {hasSubmenu && (
<ul className="menu-sub">
{item.submenu.map((sub) => (
<MenuItem key={sub.id || sub.link} {...sub} />
))}
</ul> </ul>
)} )}
</li> </li>

View File

@ -0,0 +1,17 @@
import React from 'react'
import { Link, useNavigate } from 'react-router-dom'
const Congratulation = () => {
const navigate = useNavigate()
return (
<div className="text-center p-4">
<h2>🎉 Congratulations!</h2>
<p>Your tenant is successfully onboarded.</p>
<div className="d-flex justify-content-center gap-3">
<p className='btn btn-sm btn-primary' onClick={()=>navigate('/tenants')}>Go To Tenant list</p> <p className='btn btn-sm btn-secondary' >Preview Tenant</p>
</div>
</div>
)
}
export default Congratulation

View File

@ -0,0 +1,109 @@
import React from "react";
import Label from "../common/Label";
import { useFormContext } from "react-hook-form";
const ContactInfro = ({ onNext }) => {
const {
register,
control,
trigger,
formState: { errors },
} = useFormContext();
const handleNext = async () => {
const valid = await trigger([
"firstName",
"lastName",
"email",
"contactNumber",
"billingAddress",
]);
if (valid) {
onNext(); // go to next tab
}
};
return (
<div className="row g-6">
<div className="col-sm-6">
<Label htmlFor="firstName" required>
First Name
</Label>
<input
id="firstName"
type="text"
className={`form-control form-control-sm`}
{...register("firstName")}
/>
{errors.firstName && (
<div className="danger-text">{errors.firstName.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="lastName" required>
Last Name
</Label>
<input
id="lastName"
type="text"
className={`form-control form-control-sm `}
{...register("lastName")}
/>
{errors.lastName && (
<div className="danger-text">{errors.lastName.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="email" required>
Email
</Label>
<input
id="email"
type="email"
className={`form-control form-control-sm `}
{...register("email")}
/>
{errors.email && (
<div className="danger-text">{errors.email.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="contactNumber" required>
Contact Number
</Label>
<input
id="contactNumber"
type="text"
className={`form-control form-control-sm `}
{...register("contactNumber")}
inputMode="tel"
placeholder="+91 9876543210"
/>
{errors.contactNumber && (
<div className="danger-text">{errors.contactNumber.message}</div>
)}
</div>
<div className="col-12">
<Label htmlFor="billingAddress" required>
Billing Address
</Label>
<textarea
id="billingAddress"
className={`form-control `}
{...register("billingAddress")}
rows={3}
/>
{errors.billingAddress && (
<div className="danger-text">{errors.billingAddress.message}</div>
)}
</div>
<div className="d-flex justify-content-end mt-3">
<button type="button" className="btn btn-sm btn-primary" onClick={handleNext}>
Next
</button>
</div>
</div>
);
};
export default ContactInfro;

View File

@ -0,0 +1,188 @@
import React, { useEffect, useState } from 'react';
import Label from '../common/Label';
import { useFormContext,useForm,FormProvider } from 'react-hook-form';
import { useIndustries, useTenantDetails, useUpdateTenantDetails } from '../../hooks/useTenant';
import { orgSize, reference } from '../../utils/constants';
import { LogoUpload } from './LogoUpload';
import showToast from '../../services/toastService';
import { zodResolver } from '@hookform/resolvers/zod';
import { EditTenant } from './TenantSchema';
const EditProfile = ({ TenantId,onClose }) => {
const { data, isLoading, isError, error } = useTenantDetails(TenantId);
const [logoPreview, setLogoPreview] = useState(null);
const [logoName, setLogoName] = useState("");
const { data: Industries, isLoading: industryLoading, isError: industryError } = useIndustries();
const {mutate:UpdateTenant,isPending,} = useUpdateTenantDetails(()=>{
showToast("Tenant Details Updated Successfully","success")
onClose()
})
const methods = useForm({
resolver:zodResolver(EditTenant),
defaultValues: {
firstName: "",
lastName: "",
email: "",
contactNumber: "",
description: "",
domainName: "",
billingAddress: "",
taxId: "",
logoImage: "",
officeNumber: "",
organizationSize: "",
industryId: "",
reference: "",
}
});
const { register, reset, handleSubmit, formState: { errors } } = methods;
const onSubmit = (formData) => {
const tenantPayload = {...formData,contactName:`${formData.firstName} ${formData.lastName}`,id:data.id,}
UpdateTenant({id:data.id,tenantPayload})
};
useEffect(() => {
if (data && Industries) {
const [first = "", last = ""] = (data.contactName ?? "").split(" ");
reset({
firstName: first,
lastName: last,
contactNumber: data.contactNumber ?? "",
description: data.description ?? "",
domainName: data.domainName ?? "",
billingAddress: data.billingAddress ?? "",
taxId: data.taxId ?? "",
logoImage: data.logoImage ?? "",
officeNumber: data.officeNumber ?? "",
organizationSize: data.organizationSize ?? "",
industryId: data.industry?.id ?? "",
reference: data.reference ?? "",
});
setLogoPreview(data.logoImage)
}
}, [data, Industries, reset]);
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>{error?.message}</div>;
return (
<FormProvider {...methods}>
<form className="row g-6" onSubmit={handleSubmit(onSubmit)}>
<h6>Edit Tenant</h6>
<div className="col-sm-6 mt-1">
<Label htmlFor="firstName" required>First Name</Label>
<input id="firstName" type="text" className="form-control form-control-sm" {...register("firstName")} inputMode='text' />
{errors.firstName && <div className="danger-text">{errors.firstName.message}</div>}
</div>
<div className="col-sm-6 mt-1">
<Label htmlFor="lastName" required>Last Name</Label>
<input id="lastName" type="text" className="form-control form-control-sm" {...register("lastName")} />
{errors.lastName && <div className="danger-text">{errors.lastName.message}</div>}
</div>
<div className="col-sm-6 mt-1">
<Label htmlFor="contactNumber" required>Contact Number</Label>
<input id="contactNumber" type="text" className="form-control form-control-sm" {...register("contactNumber")} inputMode="tel"
placeholder="+91 9876543210" />
{errors.contactNumber && <div className="danger-text">{errors.contactNumber.message}</div>}
</div>
<div className="col-sm-6 mt-1">
<Label htmlFor="domainName" required>Domain Name</Label>
<input id="domainName" type="text" className="form-control form-control-sm" {...register("domainName")} />
{errors.domainName && <div className="danger-text">{errors.domainName.message}</div>}
</div>
<div className="col-sm-6 mt-1">
<Label htmlFor="taxId" required>Tax ID</Label>
<input id="taxId" type="text" className="form-control form-control-sm" {...register("taxId")} />
{errors.taxId && <div className="danger-text">{errors.taxId.message}</div>}
</div>
<div className="col-sm-6 mt-1">
<Label htmlFor="officeNumber" required>Office Number</Label>
<input id="officeNumber" type="text" className="form-control form-control-sm" {...register("officeNumber")} />
{errors.officeNumber && <div className="danger-text">{errors.officeNumber.message}</div>}
</div>
<div className="col-sm-6 mt-1">
<Label htmlFor="industryId" required>Industry</Label>
<select className="form-select form-select-sm" {...register("industryId")}>
{industryLoading ? <option value="">Loading...</option> :
Industries?.map((indu) => (
<option key={indu.id} value={indu.id}>{indu.name}</option>
))
}
</select>
{errors.industryId && <div className="danger-text">{errors.industryId.message}</div>}
</div>
<div className="col-sm-6 mt-1">
<Label htmlFor="reference">Reference</Label>
<select className="form-select form-select-sm" {...register("reference")}>
{reference.map((org) => (
<option key={org.val} value={org.val}>{org.name}</option>
))}
</select>
{errors.reference && <div className="danger-text">{errors.reference.message}</div>}
</div>
<div className="col-sm-6">
<Label htmlFor="organizationSize" required>
Organization Size
</Label>
<select
className="form-select form-select-sm"
{...register("organizationSize")}
>
{orgSize.map((org) => (
<option key={org.val} value={org.val}>
{org.name}
</option>
))}
</select>
{errors.organizationSize && (
<div className="danger-text">{errors.organizationSize.message}</div>
)}
</div>
<div className="col-12 mt-1">
<Label htmlFor="billingAddress" required>Billing Address</Label>
<textarea id="billingAddress" className="form-control" {...register("billingAddress")} rows={2} />
{errors.billingAddress && <div className="danger-text">{errors.billingAddress.message}</div>}
</div>
<div className="col-12 mt-1">
<Label htmlFor="description">Description</Label>
<textarea id="description" className="form-control" {...register("description")} rows={2} />
{errors.description && <div className="danger-text">{errors.description.message}</div>}
</div>
<div className="col-sm-12">
<Label htmlFor="logImage">Logo Image</Label>
<LogoUpload
preview={logoPreview}
setPreview={setLogoPreview}
fileName={logoName}
setFileName={setLogoName}
/>
</div>
<div className="d-flex justify-content-center gap-2 mt-3">
<button type="submit" disabled={isPending} className="btn btn-sm btn-primary">{isPending ? "Please Wait..." : "Submit"}</button>
<button type="button" disabled={isPending} className="btn btn-sm btn-secondary" onClick={onClose}>Cancel</button>
</div>
</form>
</FormProvider>
);
};
export default EditProfile;

View File

@ -0,0 +1,85 @@
import React from "react";
import { useFormContext } from "react-hook-form";
const toBase64 = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
export const LogoUpload = ({ preview, setPreview, fileName, setFileName }) => {
const {
register,
setValue,
formState: { errors },
} = useFormContext();
const handleUpload = async (e) => {
const file = e.target.files?.[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) {
alert("File exceeds 5MB limit");
return;
}
const base64 = await toBase64(file);
setValue("logoImage", base64, { shouldValidate: true });
setFileName(file.name);
setPreview(URL.createObjectURL(file));
e.target.value = "";
};
const handleClear = () => {
setValue("logoImage", "", { shouldValidate: true });
setPreview(null);
setFileName("");
};
return (
<div className="col-sm-12 mb-3">
<div
className="border border-secondary border-dashed rounded p-2 text-center position-relative"
style={{ cursor: "pointer" }}
onClick={() => document.getElementById("logoImageInput")?.click()}
>
<i className="bx bx-cloud-upload d-block bx-lg mb-2"></i>
<span className="text-muted">Click or browse to upload</span>
<input
type="file"
id="logoImageInput"
accept="image/png, image/jpeg"
style={{ display: "none" }}
{...register("logoImage")}
onChange={handleUpload}
/>
</div>
{errors.logoImage && (
<small className="danger-text">{errors.logoImage.message}</small>
)}
{preview && (
<div className="mt-2 d-flex align-items-start gap-2">
<img
src={preview}
alt="Preview"
className="img-thumbnail rounded"
style={{ maxHeight: "35px" }}
/>
<div className="d-flex align-items-center gap-2 mt-1">
<span className="small text-muted">{fileName}</span>
<i
className="bx bx-trash bx-sm text-danger"
style={{ cursor: "pointer" }}
onClick={handleClear}
></i>
</div>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,17 @@
import React from 'react'
import { formatUTCToLocalTime } from '../../utils/dateUtils'
const Organization = ({data}) => {
return (
<div className='container-fluid'>
{/* <div className='col-12'>
<h4>{data?.name}</h4>
</div> */}
</div>
)
}
export default Organization

View File

@ -0,0 +1,242 @@
import React, { useState } from "react";
import { useFormContext, Controller } from "react-hook-form";
import Label from "../common/Label";
import DatePicker from "../common/DatePicker";
import { useCreateTenant, useIndustries } from "../../hooks/useTenant";
import { LogoUpload } from "./LogoUpload";
import { orgSize, reference } from "../../utils/constants";
import moment from "moment";
const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
const { data, isError, isLoading: industryLoading } = useIndustries();
const [logoPreview, setLogoPreview] = useState(null);
const [logoName, setLogoName] = useState("");
const {
register,
control,
setValue,
getValues,
trigger,
formState: { errors },
} = useFormContext();
const {
mutate: CreateTenant,
isError: tenantError,
error,
isPending,
} = useCreateTenant(() => {
debugger
onNext()
});
const handleNext = async () => {
const valid = await trigger([
"organizationName",
"officeNumber",
"domainName",
"description",
"onBoardingDate",
"organizationSize",
"taxId",
"industryId",
"reference",
"logoImage",
]);
if (valid) {
const data = getValues();
// onSubmitTenant(data);
// onNext();
const tenantPayload = {...data,onBoardingDate: moment.utc(data.onBoardingDate, "DD-MM-YYYY").toISOString() }
CreateTenant(tenantPayload);
}
};
return (
<div className="row g-2">
<div className="col-sm-6">
<Label htmlFor="organizationName" required>
Organization Name
</Label>
<input
id="organizationName"
className={`form-control form-control-sm `}
{...register("organizationName")}
/>
{errors.organizationName && (
<div className="danger-text">{errors.organizationName.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="officeNumber" required>
Office Number
</Label>
<input
id="officeNumber"
className={`form-control form-control-sm `}
{...register("officeNumber")}
/>
{errors.officeNumber && (
<div className="danger-text">{errors.officeNumber.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="domainName" required>
Domain Name
</Label>
<input
id="domainName"
className={`form-control form-control-sm `}
{...register("domainName")}
/>
{errors.domainName && (
<div className="danger-text">{errors.domainName.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="taxId" required>
Tax ID
</Label>
<input
id="taxId"
className={`form-control form-control-sm `}
{...register("taxId")}
/>
{errors.taxId && (
<div className="danger-text">{errors.taxId.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="onBoardingDate" required>
Onboarding Date
</Label>
<DatePicker
name="onBoardingDate"
control={control}
placeholder="DD-MM-YYYY"
maxDate={new Date()}
className={errors.onBoardingDate ? "is-invalid" : ""}
/>
{errors.onBoardingDate && (
<div className="invalid-feedback">
{errors.onBoardingDate.message}
</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="organizationSize" required>
Organization Size
</Label>
<select
className="form-select form-select-sm"
{...register("organizationSize")}
>
{orgSize.map((org) => (
<option key={org.val} value={org.val}>
{org.name}
</option>
))}
</select>
{errors.organizationSize && (
<div className="danger-text">{errors.organizationSize.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="industryId" required>
Industry
</Label>
<select
className="form-select form-select-sm"
{...register("industryId")}
>
{industryLoading ? (
<option value="">Loading...</option>
) : (
data?.map((indu) => (
<option key={indu.id} value={indu.id}>
{indu.name}
</option>
))
)}
</select>
{errors.industryId && (
<div className="danger-text">{errors.industryId.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="reference">Reference</Label>
<select
className="form-select form-select-sm"
{...register("reference")}
>
{reference.map((org) => (
<option key={org.val} value={org.val}>
{org.name}
</option>
))}
</select>
{errors.reference && (
<div className="danger-text">{errors.reference.message}</div>
)}
</div>
<div className="col-sm-12">
<Label htmlFor="description">Description</Label>
<textarea
id="description"
rows={3}
className={`form-control form-control-sm `}
{...register("description")}
/>
{errors.description && (
<div className="danger-text">{errors.description.message}</div>
)}
</div>
<div className="col-sm-12">
<Label htmlFor="logImage">Logo Image</Label>
<LogoUpload
preview={logoPreview}
setPreview={setLogoPreview}
fileName={logoName}
setFileName={setLogoName}
/>
</div>
<div className="d-flex justify-content-between mt-3">
<button
type="button"
className="btn btn-sm btn-secondary"
onClick={onPrev}
disabled={isPending}
>
Back
</button>
<button
type="button"
className="btn btn-sm btn-primary"
onClick={handleNext}
disabled={isPending}
>
{isPending ? "Please Wait..." : "Submit and Next"}
</button>
</div>
</div>
);
};
export default OrganizationInfo;

View File

@ -0,0 +1,190 @@
import React, { useState } from "react";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import EditProfile from "./EditProfile";
import GlobalModel from "../common/GlobalModel";
import { useTenantContext } from "../../pages/Tenant/TenantPage";
import { useTenantDetailsContext } from "../../pages/Tenant/TenantDetails";
import IconButton from "../common/IconButton";
import { hasUserPermission } from "../../utils/authUtils";
import { MANAGE_TENANTS } from "../../utils/constants";
const Profile = ({ data }) => {
const {setEditTenant} = useTenantDetailsContext()
const canUpdateTenant = hasUserPermission(MANAGE_TENANTS)
return (
<>
<div className="container-fuid">
<div className="row">
<div className="col-12 my-2">
<div className="d-flex flex-wrap align-items-start position-relative ">
<div className=" d-flex align-items-start gap-2">
{data.logoImage ? (<img
src={data.logoImage}
alt="Preview"
className="img-thumbnail rounded"
style={{ maxHeight: "35px" }}
/>):( <IconButton
iconClass="bx bx-sm bx-building"
color="warning"
size={8}
/>)}
</div>
<div className="ms-2 ">
<h4 className="m-0">{data.name}</h4>
<div className="block">
<i className="bx bx-globe text-primary bx-xs me-1"></i>
<span>{data?.domainName}</span>
</div>
{canUpdateTenant && ( <span
className="position-absolute top-0 end-0 cursor-auto"
onClick={() => setEditTenant(true)}
>
<i className="bx bx-edit bs-sm text-primary cursor-pointer"></i>
</span>)}
</div>
</div>
</div>
{data?.description && (
<div className="col rounded-2 justify-content-start p-2">
<p className="m-0">{data?.description}</p>
</div>
)}
</div>
<div className="divider text-start my-1">
<div className="divider-text">Personal</div>
</div>
<div className="row ">
<div className="col-12 col-md-6 d-flex align-items-center">
<i className="bx bx-sm bx-user me-1"></i>
<span className="fw-semibold">Contact Person:</span>
<span className="ms-2">{data.contactName}</span>
</div>
<div className="col-12 col-md-6 d-flex align-items-center my-4 m-0">
<i className="bx bx-sm bx-envelope me-1"></i>
<span className="fw-semibold">Email:</span>
<span className="ms-2">{data.email}</span>
</div>
<div className="col-12 col-md-6 d-flex align-items-center">
<i className="bx bx-sm bx-mobile me-1"></i>
<span className="fw-semibold">Contact Number:</span>
<span className="ms-2">{data.contactNumber}</span>
</div>
{data.billingAddress && (
<div className="col-12 d-flex text-wrap align-items-start mt-4 m-0">
<i className='bx bxs-flag-alt bx-sm me-1'></i>
<span className="fw-semibold">Address:</span>
<span className="ms-2">{data.billingAddress}</span>
</div>
)}
</div>
<div className="divider text-start ">
<div className="divider-text">Organization</div>
</div>
<div className="col-12 d-flex align-items-center mb-2">
<i className="bx bx-sm bxs-building me-1"></i>
<span className="fw-semibold">Industry:</span>
<span className="ms-2">{data?.industry?.name}</span>
</div>
<div className="row ">
{data?.taxId && (
<div className="col-12 col-md-6 d-flex align-items-center ">
<i className="bx bx-sm bx-id-card me-1"></i>
<span className="fw-semibold">Tax Id:</span>
<span className="ms-2">{data?.taxId}</span>
</div>
)}
<div className="col-12 col-md-6 d-flex align-items-center mb-2 m-0">
<i className="bx bx-sm bx-group me-1"></i>
<span className="fw-semibold">Organization Size:</span>
<span className="ms-2">{data?.organizationSize}</span>
</div>
<div className="col-12 col-md-6 d-flex align-items-center my-2 m-0">
<i className="bx bx-sm bx-group me-1"></i>
<span className="fw-semibold">Seat Available:</span>
<span className="ms-2">{data?.seatsAvailable}</span>
</div>
<div className="col-12 col-md-6 d-flex align-items-center my-2 m-0">
<i className="bx bx-sm bx-group me-1"></i>
<span className="fw-semibold">Total Seat:</span>
<span className="ms-2">{data?.currentPlan?.maxUsers}</span>
</div>
<div className="col-12 col-md-6 d-flex align-items-center">
<i className="bx bx-sm bxs-calendar me-1"></i>
<span className="fw-semibold">On-Boarding Date:</span>
<span className="ms-2">
{formatUTCToLocalTime(data?.onBoardingDate)}
</span>
</div>
<table className="table table-bordered text-center text-nowrap table-responsive my-4">
<tbody>
<tr>
<td colSpan="1">
<strong>Status</strong>
</td>
<td colSpan="1">
<strong>Active</strong>
</td>
<td colSpan="1">
<strong>In-Progress</strong>
</td>
<td colSpan="1">
<strong>On Hold</strong>
</td>
<td>
<strong>In-Active</strong>
</td>
<td>
<strong>Completed</strong>
</td>
</tr>
<tr>
<td>
<strong>Projects</strong>
</td>
<td>
<strong>{data?.activeProjects}</strong>
</td>
<td>
<strong>{data?.inProgressProjects}</strong>
</td>
<td>
<strong>{data?.onHoldProjects}</strong>
</td>
<td>
<strong>{data?.inActiveProjects}</strong>
</td>
<td>
<strong>{data?.completedProjects}</strong>
</td>
</tr>
</tbody>
</table>
</div>
<div className="row">
<div className="col-12 col-md-6 d-flex align-items-center">
<i className="bx bx-sm bx-group me-1"></i>
<span className="fw-semibold">Activite Employees:</span>
<span className="ms-2">{data?.activeEmployees}</span>
</div>
<div className="col-12 col-md-6 d-flex align-items-center my-4 m-0">
<i className="bx bx-sm bx-group me-1"></i>
<span className="fw-semibold">In-Active Employee:</span>
<span className="ms-2">{data?.inActiveEmployees}</span>
</div>
</div>
</div>
</>
);
};
export default Profile;

View File

@ -0,0 +1,45 @@
import React, { useEffect, useState } from 'react';
const SegmentedControl = ({setFrequency,defultFequency}) => {
const [selected, setSelected] = useState(defultFequency);
useEffect(()=>{
setFrequency(selected)
},[selected])
return (
<div className='text-center mt-6'>
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
<button
type="button"
className={`btn px-4 py-2 rounded-0 ${selected === 0 ? 'active btn-secondary text-white' : ''}`}
onClick={() => setSelected(0)}
>
Monthly
</button>
<button
type="button"
className={`btn px-4 py-2 rounded-0 ${selected === 1? 'active btn-secondary text-white' : ''}`}
onClick={() => setSelected(1)}
>
Quaterly
</button>
<button
type="button"
className={`btn px-4 py-2 rounded-0 ${selected === 2 ? 'active btn-secondary text-white' : ''}`}
onClick={() => setSelected(2)}
>
Half-Yearly
</button>
<button
type="button"
className={`btn px-4 py-2 rounded-0 ${selected === 3 ? 'active btn-secondary text-white' : ''}`}
onClick={() => setSelected(3)}
>
Yearly
</button>
</div>
</div>
);
};
export default SegmentedControl;

View File

@ -0,0 +1,252 @@
import React, { useState, useEffect } from "react";
import {
useAddSubscription,
useSubscriptionPlan,
useUpgradeSubscription,
} from "../../hooks/useTenant";
import SegmentedControl from "./SegmentedControl";
import { useFormContext } from "react-hook-form";
import { CONSTANT_TEXT } from "../../utils/constants";
import Label from "../common/Label";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
const SubScription = ({ onSubmitSubScription, onNext }) => {
const [frequency, setFrequency] = useState(3);
const [selectedPlanId, setSelectedPlanId] = useState(null);
const selectedTenant = useSelector(
(store) => store.globalVariables.currentTenant
);
const naviget = useNavigate();
const {
data: plans = [],
isError,
isLoading,
error: subscriptionGettingError,
} = useSubscriptionPlan(frequency);
const {
register,
setValue,
getValues,
trigger,
formState: { errors },
} = useFormContext();
const {
mutate: AddSubScription,
isPending,
error,
} = useAddSubscription(() => {
naviget("/tenants");
});
const { mutate: updgradeSubscription, isPending: upgrading } =
useUpgradeSubscription(() => {
naviget("/tenants");
});
const handleSubscriptionSubmit = async () => {
const isValid = await trigger([
"planId",
"currencyId",
"maxUsers",
"frequency",
"isTrial",
"autoRenew",
]);
if (isValid) {
const payload = getValues();
// onSubmitSubScription(payload);
let subscriptionPayload = null;
if (selectedTenant?.operationMode === 1) {
subscriptionPayload = {
planId: payload.planId,
currencyId: payload.currencyId,
maxUsers: payload.maxUsers,
tenantId: selectedTenant?.data?.id,
};
updgradeSubscription(subscriptionPayload);
} else {
subscriptionPayload = {
...payload,
tenantId: selectedTenant?.data?.id,
};
AddSubScription(subscriptionPayload);
}
}
};
const handlePlanSelection = (plan) => {
setSelectedPlanId(plan.id);
setValue("planId", plan.id);
setValue("currencyId", plan.currency?.id);
setValue("frequency", frequency);
};
const selectedPlan = plans.find((p) => p.id === selectedPlanId);
if (isLoading) return <div className="text-center">Loading....</div>;
if (isError)
return (
<div className="text-center">{subscriptionGettingError?.message}</div>
);
return (
<div className="text-start">
<SegmentedControl
setFrequency={setFrequency}
defultFequency={frequency}
/>
{!isLoading && !isError && plans.length > 0 && (
<div className="row g-4 my-6">
{plans.map((plan) => {
const isSelected = plan.id === selectedPlanId;
return (
<div key={plan.id} className="col-md-4">
<div
className={`card h-100 shadow-none border-1 cursor-pointer ${
isSelected ? "border-primary border-1 shadow-md" : ""
}`}
onClick={() => handlePlanSelection(plan)}
>
<div className="card-body d-flex flex-column p-3">
<div className="d-flex align-items-center gap-3 mb-3">
<i className="bx bxs-package text-primary fs-1"></i>
<div>
<p className="card-title fs-4 fw-bold mb-1">
{plan.planName}
</p>
<p className="text-muted mb-0">{plan.description}</p>
</div>
</div>
<h4 className="fw-semibold mt-auto mb-3">
{plan.currency?.symbol} {plan.price}
</h4>
<ul className="list-unstyled d-flex gap-4 flex-wrap mb-2">
<li className="d-flex align-items-center">
<i className="bx bx-server me-1"></i>
Storage {plan.maxStorage} MB
</li>
<li className="d-flex align-items-center">
<i className="bx bx-check-double text-success me-2"></i>
Trial Days {plan.trialDays}
</li>
</ul>
<div>
<div className="divider my-3">
<div className="divider-text card-text text-uppercase text-muted small">
Features
</div>
</div>
{plan?.features &&
Object.entries(plan?.features?.modules || {})
.filter(([key]) => key !== "id")
.map(([key, mod]) => (
<div
key={key}
className="mb-2 d-flex align-items-center"
>
<i
className={`fa-regular ${
mod.enabled
? "fa-circle-check text-success"
: "fa-circle-xmark text-danger"
}`}
></i>
<small className="ms-1">{mod.name}</small>
</div>
))}
</div>
<button
className={`btn mt-3 ${
isSelected ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => handlePlanSelection(plan)}
>
{isSelected ? "Selected" : "Select Plan"}
</button>
</div>
</div>
</div>
);
})}
{/* Form Inputs */}
<div className="row g-2 mt-3">
<div className="col-sm-4">
<Label htmlFor="maxUsers" required>
{" "}
Team Size
</Label>
<input
type="number"
step={1}
className="form-control form-control-sm"
{...register("maxUsers", {
valueAsNumber: true,
})}
onKeyDown={(e) => {
if (["e", "E", "+", "-", "."].includes(e.key)) {
e.preventDefault();
}
}}
/>
</div>
<div className="col-12">
<div className="d-flex justify-content-start align-items-center gap-2">
<label className="form-label d-block">Enable auto renew</label>
<label className="switch switch-square switch-sm">
<input
type="checkbox"
className="switch-input"
{...register("autoRenew")}
/>
<span className="switch-toggle-slider">
<span className="switch-on">
<i className="icon-base bx bx-check"></i>
</span>
<span className="switch-off">
<i className="icon-base bx bx-x"></i>
</span>
</span>
</label>
</div>
<small className="text-secondary text-tiny">
{CONSTANT_TEXT.RenewsubscriptionLabel}
</small>
</div>
</div>
{Object.keys(errors).length > 0 && (
<div class="alert alert-danger" role="alert">
{Object.entries(errors).map(([key, error]) => (
<div key={key} className="danger-text">
{error?.message}
</div>
))}
</div>
)}
<div className="d-flex text-center mt-4">
<button
onClick={handleSubscriptionSubmit}
className="btn btn-sm btn-primary"
type="button"
disabled={isPending || upgrading}
>
{isPending || upgrading ? "Please Wait..." : "Submit"}
</button>
</div>
</div>
)}
</div>
);
};
export default SubScription;

View File

@ -0,0 +1,219 @@
import React, { useEffect } from "react";
import { useTenantDetails } from "../../hooks/useTenant";
import { useDispatch } from "react-redux";
import { setCurrentTenant } from "../../slices/globalVariablesSlice";
import { useNavigate } from "react-router-dom";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import { SUBSCRIPTION_PLAN_FREQUENCIES } from "../../utils/constants";
const SubScriptionHistory = ({ tenantId }) => {
const { data, isLoading, isError, error } = useTenantDetails(tenantId);
const dispatch = useDispatch();
const navigate = useNavigate();
useEffect(() => {
if (data) {
dispatch(setCurrentTenant({ operationMode: 1, data }));
} else {
dispatch(setCurrentTenant({ operationMode: 0, data: null }));
}
}, [data, dispatch]);
const handleUpgradePlan = () => {
navigate("/tenants/new-tenant");
};
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>{error}</div>;
const plan = data?.currentPlan;
const features = data?.currentPlanFeatures;
const subscriptionHistory = data?.subscriptionHistery;
if (!plan) {
return (
<div className="text-center p-4">
<button className="btn btn-success" onClick={handleUpgradePlan}>
Add Subscription
</button>
<div className="mt-2 text-center small text-muted">
<i className="bx bx-info-circle bx-xs"></i> Add your new subscription
</div>
</div>
);
}
// Format dates
const end = plan?.endDate ? new Date(plan.endDate) : null;
const today = new Date();
const daysLeft = end
? Math.max(0, Math.ceil((end - today) / (1000 * 60 * 60 * 24)))
: 0;
// Render logic for subscription history table
const renderSubscriptionHistory = () => {
if (!subscriptionHistory || subscriptionHistory.length === 0) {
return (
<div className="text-center text-muted p-4">
No subscription history found
</div>
);
}
const sortedHistory = subscriptionHistory
.slice()
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
return (
<>
{/* Table for larger screens */}
<div className=" d-md-block table-responsive">
<table className="table border-top dataTable text-nowrap align-middle">
<thead className="align-middle">
<tr>
<th>Date</th>
<th>Type</th>
<th>Amount</th>
<th>Plan Name</th>
<th className="text-center">Action</th>
</tr>
</thead>
<tbody className="align-middle">
{sortedHistory.map((item) => (
<tr key={item.id}>
<td>{formatUTCToLocalTime(item?.createdAt)}</td>
<td>{SUBSCRIPTION_PLAN_FREQUENCIES[item.frequency] || "N/A"}</td>
<td>
{item.currency?.symbol || "₹"} {item.price}
</td>
<td>{item.planName}</td>
<td className="text-center">
<div className="dropdown">
<button
className="btn btn-icon btn-sm dropdown-toggle hide-arrow"
data-bs-toggle="dropdown"
>
<i className="bx bx-dots-vertical-rounded"></i>
</button>
<div className="dropdown-menu dropdown-menu-end">
<button
className="dropdown-item py-1"
>
<i className="bx bx-detail bx-sm"></i> View
</button>
<button
className="dropdown-item py-1"
onClick={() =>
console.log("Download clicked for", item.id)
}
>
<i className="bx bx-cloud-download bx-sm"></i> Download
</button>
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Card-based view for smaller screens */}
</>
);
};
return (
<div className="p-2 p-md-4">
<div className="row g-4">
{/* Left Card: Active Subscription */}
<div className="col-12 col-lg-6">
<div className="card shadow-sm border rounded p-3 h-100 text-start">
<div className="divider text-start mb-3">
<div className="divider-text">Active Subscription</div>
</div>
<p className="text-primary fw-bold m-0 fs-4">
{plan.planName || "N/A"}
</p>
{plan.description && (
<p className="m-0 text-muted small">{plan.description}</p>
)}
<div className="mt-2">
<h3 className="m-0">
{plan.currency?.symbol || "₹"} {plan.price}
</h3>
<small className="text-muted">
{SUBSCRIPTION_PLAN_FREQUENCIES[plan.frequency] || ""}
</small>
</div>
<div className="mt-3 small text-muted">
<div>
Activated Since:{" "}
{plan.startDate ? formatUTCToLocalTime(plan.startDate) : "N/A"} (
{daysLeft} days left)
</div>
<div className="mt-1">
Ends on:{" "}
{plan.endDate ? formatUTCToLocalTime(plan.endDate) : "N/A"}
</div>
</div>
{/* Features list */}
<div className="mt-4">
<h6 className="text-secondary">Features</h6>
<div className="row g-2">
{features?.modules &&
Object.entries(features.modules).map(([key, mod]) => {
if (!mod?.name) return null;
return (
<div
key={key}
className="col-12 col-sm-6 d-flex align-items-center"
>
<i
className={`fa-regular ${
mod.enabled
? "fa-circle-check text-success"
: "fa-circle-xmark text-danger"
}`}
></i>
<span className="ms-2">{mod.name}</span>
</div>
);
})}
</div>
</div>
<div className="mt-3 text-end">
<button
className="btn btn-sm btn-primary"
onClick={handleUpgradePlan}
>
Upgrade Plan
</button>
</div>
</div>
</div>
{/* Right Card: Subscription History */}
<div className="col-12 col-lg-6">
<div className="card shadow-sm border rounded p-3 h-100">
<div className="divider text-start mb-3">
<div className="divider-text">
<i className="bx bx-history"></i> <small>History</small>
</div>
</div>
{renderSubscriptionHistory()}
</div>
</div>
</div>
</div>
);
};
export default SubScriptionHistory;

View File

@ -0,0 +1,97 @@
import React from "react";
const SkeletonCell = ({ width = "100%", height = 20, style = {} }) => (
<div
className="skeleton"
style={{
width,
height,
borderRadius: 4,
...style,
}}
/>
);
export const TenantTableSkeleton = ({ columns, rows = 5 }) => {
return (
<div className="card p-2 mt-3">
<div className="card-datatable text-nowrap table-responsive">
<table className="table border-top dataTable text-nowrap">
<thead>
<tr>
{columns.map((col) => (
<th key={col.key} className="sorting d-table-cell">
<div className={col.align}>{col.label}</div>
</th>
))}
</tr>
</thead>
<tbody>
{[...Array(rows)].map((_, rowIdx) => (
<tr key={rowIdx}>
{columns.map((col, colIdx) => (
<td
key={col.key || colIdx}
className={`d-table-cell px-3 py-2 align-middle ${
col.align ?? ""
}`}
>
{/* Icon + text skeleton for first few columns */}
{col.key === "name" && (
<div className="d-flex align-items-center">
<div
className="me-2"
style={{
width: 24,
height: 24,
borderRadius: "5px",
background: "#ddd",
}}
/>
<SkeletonCell width="120px" />
</div>
)}
{col.key === "domainName" && (
<div className="d-flex align-items-center">
<div
className="me-2"
style={{
width: 14,
height: 14,
borderRadius: "50%",
background: "#ddd",
}}
/>
<SkeletonCell width="140px" />
</div>
)}
{col.key === "contactName" && (
<div className="d-flex align-items-center ">
<div
className="me-2"
style={{
width: 20,
height: 20,
borderRadius: "50%",
background: "#ddd",
}}
/>
<SkeletonCell width="100px" />
</div>
)}
{col.key === "contactNumber" && (
<SkeletonCell width="100px"/>
)}
{col.key === "status" && (
<SkeletonCell width="60px" height={24} />
)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};

View File

@ -0,0 +1,116 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useState,useCallback } from "react";
import { FormProvider, useForm, useFormContext } from "react-hook-form";
import { defaultFilterValues, filterSchema } from "./TenantSchema";
import Label from "../common/Label";
import SelectMultiple from "../common/SelectMultiple";
import { useIndustries } from "../../hooks/useTenant";
import { reference, TENANT_STATUS } from "../../utils/constants";
import { DateRangePicker1 } from "../common/DateRangePicker";
import moment from "moment";
const TenantFilterPanel = ({onApply}) => {
const [resetKey, setResetKey] = useState(0);
const methods = useForm({
resolver: zodResolver(filterSchema),
defaultValues: defaultFilterValues,
});
const { handleSubmit, reset } = methods;
const { data: industries = [], isLoading } = useIndustries();
const handleClosePanel = useCallback(() => {
document.querySelector(".offcanvas.show .btn-close")?.click();
}, []);
const onSubmit = useCallback(
(formData) => {
onApply({
...formData,
startDate: moment.utc(formData.startDate, "DD-MM-YYYY").toISOString(),
endDate: moment.utc(formData.endDate, "DD-MM-YYYY").toISOString(),
});
handleClosePanel();
},
[onApply, handleClosePanel]
);
const onClear = useCallback(() => {
reset(defaultFilterValues);
setResetKey((prev) => prev + 1); // triggers DateRangePicker reset
onApply(defaultFilterValues);
}, [onApply, reset]);
if (isLoading) {
return <div className="text-center">Loading...</div>;
}
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="text-start mb-1">
<div className="text-start my-2">
<DateRangePicker1
placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="startDate"
endField="endDate"
resetSignal={resetKey}
defaultRange={false}
/>
</div>
<div className="text-strat mb-2">
<SelectMultiple
name="industryIds"
label="Industries"
options={industries}
labelKey="name"
valueKey="id"
/>
</div>
<div className="text-start mb-2">
<SelectMultiple
name="references"
label="References"
options={reference}
labelKey="name"
valueKey="val"
/>
</div>
<div className="text-start">
<SelectMultiple
name="tenantStatusIds"
label="Tenant Status"
options={TENANT_STATUS}
labelKey="name"
valueKey="id"
/>
</div>
{/* <SelectMultiple
name="references"
label="Industries :"
options={reference}
labelKey="name"
valueKey="val"
/> */}
</div>
<div className="d-flex justify-content-end py-3 gap-2">
<button
type="button"
className="btn btn-secondary btn-xs"
onClick={onClear}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-xs" >
Apply
</button>
</div>
</form>
</FormProvider>
);
};
export default TenantFilterPanel;

View File

@ -0,0 +1,212 @@
import React, { useState, useEffect } from "react";
import ContactInfro from "./ContactInfro";
import SubScription from "./SubScription";
import OrganizationInfo from "./OrganizationInfo";
import Congratulation from "./Congratulation";
import { useForm, FormProvider } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
getStepFields,
getSubscriptionSchema,
newTenantSchema,
subscriptionDefaultValues,
tenantDefaultValues,
} from "./TenantSchema";
import { useSelector } from "react-redux";
const TenantForm = () => {
const HasSelectedCurrentTenant = useSelector(
(store) => store.globalVariables.currentTenant
);
const [activeTab, setActiveTab] = useState(0);
const [completedTabs, setCompletedTabs] = useState([]);
const PlanTextLabel =
HasSelectedCurrentTenant?.operationMode === 1
? "Upgrade Plan"
: "Select Plan";
// Jump to subscription if tenant already exists
useEffect(() => {
if (HasSelectedCurrentTenant) {
if (HasSelectedCurrentTenant.operationMode === 1) {
// Skip to subscription step
setActiveTab(2); // index for "SubScription"
setCompletedTabs([0, 1]); // mark previous steps as completed
} else if (HasSelectedCurrentTenant.operationMode === 0) {
setActiveTab(0); // start from beginning
}
}
}, [HasSelectedCurrentTenant]);
const tenantForm = useForm({
resolver: zodResolver(newTenantSchema),
defaultValues: tenantDefaultValues,
});
const subscriptionForm = useForm({
resolver: zodResolver(getSubscriptionSchema(HasSelectedCurrentTenant?.data?.activeEmployees)),
defaultValues: subscriptionDefaultValues,
});
const getCurrentTrigger = () =>
activeTab === 2 ? subscriptionForm.trigger : tenantForm.trigger;
const handleNext = async () => {
const currentStepFields = getStepFields(activeTab);
const trigger = getCurrentTrigger();
const valid = await trigger(currentStepFields);
if (valid) {
setCompletedTabs((prev) => [...new Set([...prev, activeTab])]);
setActiveTab((prev) => {
let nextStep = Math.min(prev + 1, newTenantConfig.length - 1);
// Check tenant operationMode to decide navigation
if (
HasSelectedCurrentTenant &&
HasSelectedCurrentTenant.operationMode === 1 &&
nextStep === 2
) {
// If tenant already has subscription, show upgrade
nextStep = 2;
} else if (
HasSelectedCurrentTenant &&
[0, 2].includes(HasSelectedCurrentTenant.operationMode) &&
nextStep === 2
) {
// If tenant just created (0) OR exists without subscription (2)
// stay on subscription tab
nextStep = 2;
}
return nextStep;
});
}
};
const handlePrev = () => {
setActiveTab((prev) => Math.max(prev - 1, 0));
};
const onSubmitTenant = (data) => {
console.log("Tenant Data:", data);
};
const onSubmitSubScription = (data) => {
console.log("Subscription Data:", data);
};
const newTenantConfig = [
{
name: "Contact Info",
icon: "bx bx-user bx-md",
subtitle: "Provide Contact Details",
component: <ContactInfro onNext={handleNext} />,
},
{
name: "Organization",
icon: "bx bx-buildings bx-md",
subtitle: "Organization Details",
component: (
<OrganizationInfo
onNext={handleNext}
onPrev={handlePrev}
onSubmitTenant={onSubmitTenant}
/>
),
},
{
name: "SubScription",
icon: "bx bx-star bx-md",
subtitle: PlanTextLabel,
component: (
<SubScription
onSubmitSubScription={onSubmitSubScription}
onNext={handleNext}
/>
),
},
{
name: "Congratulation",
icon: "bx bx-check-circle bx-md",
subtitle: "Completed",
component: <Congratulation />,
},
];
const isSubscriptionTab = activeTab === 2;
return (
<div
id="wizard-property-listing"
className="bs-stepper horizontically mt-2"
>
<div className="bs-stepper-header border-end text-start ">
{newTenantConfig
.filter((step) => step.name.toLowerCase() !== "congratulation")
.map((step, index) => {
const isActive = activeTab === index;
const isCompleted = completedTabs.includes(index);
return (
<React.Fragment key={step.name}>
<div
className={`step ${isActive ? "active" : ""} ${
isCompleted ? "crossed" : ""
}`}
data-target={`#step-${index}`}
>
<button
type="button"
className={`step-trigger ${isActive ? "active" : ""}`}
// onClick={() => setActiveTab(index)} // optional
>
<span className="bs-stepper-circle">
{isCompleted ? (
<i className="bx bx-check"></i>
) : (
<i className={step.icon}></i>
)}
</span>
<span className="bs-stepper-label">
<span className="bs-stepper-title">{step.name}</span>
<span className="bs-stepper-subtitle">
{step.subtitle}
</span>
</span>
</button>
</div>
{index < newTenantConfig.length - 1 && (
<div className="line text-primary"></div>
)}
</React.Fragment>
);
})}
</div>
<div className="bs-stepper-content py-2">
{isSubscriptionTab ? (
<FormProvider {...subscriptionForm}>
<form
onSubmit={subscriptionForm.handleSubmit(onSubmitSubScription)}
>
{newTenantConfig[activeTab].component}
</form>
</FormProvider>
) : (
<FormProvider {...tenantForm}>
<form onSubmit={tenantForm.handleSubmit(onSubmitTenant)}>
{newTenantConfig[activeTab].component}
</form>
</FormProvider>
)}
</div>
</div>
);
};
export default TenantForm;

View File

@ -0,0 +1,155 @@
import { z } from "zod";
export const newTenantSchema = z.object({
firstName: z
.string().trim()
.min(1, { message: "First Name is required!" })
.regex(/^[A-Za-z]+$/, { message: "First Name should contain only letters!" }),
lastName: z
.string().trim()
.min(1, { message: "Last Name is required!" })
.regex(/^[A-Za-z]+$/, { message: "Last Name should contain only letters!" }),
email: z.string().trim().email("Invalid email address"),
description: z.string().trim().optional(),
domainName: z.string().trim().nonempty("Domain name is required"),
billingAddress: z.string().trim().nonempty("Billing address is required"),
taxId: z.string().trim().nonempty("Tax ID is required"),
logoImage: z.string().trim().optional(),
organizationName: z.string().trim().nonempty("Organization name is required"),
officeNumber: z.string().trim().nonempty("Office number is required"),
contactNumber: z.string().trim()
.nonempty("Contact number is required")
.regex(/^\+?[1-9]\d{7,14}$/, "Enter a valid contact number"),
onBoardingDate: z.preprocess((val) => {
if (typeof val === "string" && val.includes("-")) {
const [day, month, year] = val.split("-");
return new Date(`${year}-${month}-${day}`);
}
return val;
}, z.date({
required_error: "Onboarding date is required",
invalid_type_error: "Invalid date format",
})),
organizationSize: z.string().nonempty("Organization size is required"),
industryId: z.string().uuid("Invalid industry ID"),
reference: z.string().nonempty("Reference is required"),
});
export const tenantDefaultValues = {
firstName: "",
lastName: "",
email: "",
description: "",
domainName: "",
billingAddress: "",
taxId: "",
logoImage: "",
organizationName: "",
officeNumber: "",
contactNumber: "",
onBoardingDate: new Date(), // or `null` if you want it empty
organizationSize: "",
industryId: "", // should be a valid UUID if pre-filled
reference: "",
};
export const getSubscriptionSchema = (minUsers) =>
z.object({
planId: z.string().min(1, { message: "Please select Plan" }),
currencyId: z.string().uuid("Invalid currency"),
maxUsers: z
.number({ invalid_type_error: "Must be a number" })
.min(minUsers, { message: `Team size must be greater than or equal to ${minUsers}` }),
frequency: z
.number({ invalid_type_error: "Frequency must be a number" })
.min(0, "Please select any one Frequency"),
isTrial: z.boolean(),
autoRenew: z.boolean(),
});
export const subscriptionDefaultValues = {
// tenantId: "",
planId: "",
currencyId: "",
maxUsers: 1,
frequency: 1,
isTrial: false,
autoRenew: false,
};
export const filterSchema = z.object({
industryIds: z.array(z.string()).optional(),
// createdByIds: z.array(z.string()).optional(),
tenantStatusIds: z.array(z.string()).optional(),
references: z.array(z.string()).optional(),
startDate: z.string().optional(),
endDate: z.string().optional(),
});
export const defaultFilterValues = {
industryIds: [],
// createdByIds: [],
tenantStatusIds: [],
references: [],
startDate:null,
endDate:null,
};
export const getStepFields = (stepIndex) => {
const stepFieldMap = {
0: [
"firstName",
"lastName",
"email",
"contactNumber",
"billingAddress",
],
1: [
"organizationName",
"officeNumber",
"domainName",
"description",
"onBoardingDate",
"organizationSize",
"taxId",
"industryId",
"reference",
"logoImage",
],
2: [
"tenantId",
"planId",
"currencyId",
"maxUsers",
"frequency",
"isTrial",
"autoRenew",
],
};
return stepFieldMap[stepIndex] || [];
};
export const EditTenant = z.object({
firstName: z
.string().trim()
.min(1, { message: "First Name is required!" })
.regex(/^[A-Za-z]+$/, { message: "First Name should contain only letters!" }),
lastName: z
.string().trim()
.min(1, { message: "Last Name is required!" })
.regex(/^[A-Za-z]+$/, { message: "Last Name should contain only letters!" }),
description: z.string().trim().optional(),
domainName: z.string().trim().min(1, { message: "Domain Name is required!" }),
billingAddress: z.string().trim().min(1, { message: "Billing Address is required!" }),
taxId: z.string().trim().min(1, { message: "Tax ID is required!" }),
logoImage: z.string().optional(),
officeNumber: z.string().trim().min(1, { message: "Office Number is required!" }),
contactNumber: z.string().trim()
.nonempty("Contact number is required")
.regex(/^\+?[1-9]\d{7,14}$/, "Enter a valid contact number"),
organizationSize: z.string().min(1, { message: "Organization Size is required!" }),
industryId: z.string().min(1,{ message: "Invalid Industry ID!" }),
reference: z.string().optional(),
});

View File

@ -0,0 +1,194 @@
import React, { useEffect, useState } from "react";
import { useTenants } from "../../hooks/useTenant";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import { getTenantStatus } from "../../utils/dateUtils";
import IconButton from "../common/IconButton";
import Pagination from "../common/Pagination";
import { TenantTableSkeleton } from "./TenanatSkeleton";
import { useTenantContext } from "../../pages/Tenant/TenantPage";
import { useNavigate } from "react-router-dom";
const TenantsList = ({
filters,
searchText,
setIsRefetching,
setRefetchFn,
}) => {
const [currentPage, setCurrentPage] = useState(1);
const navigate = useNavigate();
const {
data,
isLoading,
isError,
isInitialLoading,
error,
refetch,
isFetching,
} = useTenants(currentPage, filters, searchText);
const { setRefetching } = useTenantContext();
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
// Pass the refetch function to parent when component mounts
useEffect(() => {
setRefetchFn(() => refetch); // store in parent
}, [setRefetchFn, refetch]);
// Sync fetching status with parent
useEffect(() => {
setIsRefetching(isFetching);
}, [isFetching, setIsRefetching]);
const TenantColumns = [
{
key: "name",
label: "Organization",
getValue: (t) => (
<div
className="d-flex align-items-center py-1 cursor-pointer"
onClick={() => navigate(`/tenant/${t.id}`)}
>
{t.logoImage ? (
<img
src={t.logoImage}
alt={`${t.name} Logo`}
style={{
height: "25px",
width: "25px",
objectFit: "contain",
borderRadius: "4px",
}}
className="me-2"
/>
) : (
<IconButton
iconClass="bx bx-sm bx-building"
color="warning"
size={8}
/>
)}
{t.name || "N/A"}
</div>
),
align: "text-start",
},
{
key: "domainName",
label: "Domain",
getValue: (t) => (
<div style={{ width: "160px" }} className="text-truncate">
<a href={t.domainName} className="text-decoration-none">
<i className="bx bx-globe text-primary bx-xs me-2"></i>
{t.domainName || "N/A"}
</a>
</div>
),
align: "text-start",
},
{
key: "contactName",
label: "Contact Person",
getValue: (t) => (
<div className="d-flex align-items-center text-start">
<i className="bx bx-sm bx-user me-1" />
{t.contactName || "N/A"}
</div>
),
align: "text-start",
},
{
key: "contactNumber",
label: "Contact",
getValue: (t) => t.contactNumber || "N/A",
isAlwaysVisible: true,
},
{
key: "status",
label: "Status",
align: "text-center",
getValue: (t) => (
<span
className={`badge ${
getTenantStatus(t.tenantStatus?.id) || "secondary"
}`}
>
{t.tenantStatus?.name || "Unknown"}
</span>
),
},
];
if (isInitialLoading)
return <TenantTableSkeleton columns={TenantColumns} rows={13} />;
if (isError)
return (
<div className="">
<div className="card text-center my-4 p-2">
<i className="fa-solid fa-triangle-exclamation fs-5"></i>
<p>{error.message}</p>
</div>
</div>
);
return (
<>
<div className="card p-2 mt-3">
<div className="card-datatable text-nowrap table-responsive">
<table className="table border-top dataTable text-nowrap">
<thead>
<tr className="shadow-sm">
{TenantColumns.map((col) => (
<th key={col.key} className="sorting d-table-cell">
<div className={col.align}>{col.label}</div>
</th>
))}
</tr>
</thead>
<tbody>
{data?.data.length > 0 ? (
data.data.map((tenant) => (
<tr key={tenant.id}>
{TenantColumns.map((col) => (
<td
key={col.key}
className={`d-table-cell px-3 py-2 align-middle ${
col.align ?? ""
}`}
>
{col.customRender
? col.customRender(tenant)
: col.getValue(tenant)}
</td>
))}
</tr>
))
) : (
<tr>
<td
colSpan={TenantColumns.length + 1}
className="text-center py-4 border-0"
>
No Tenants Found
</td>
</tr>
)}
</tbody>
</table>
{data?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
)}
</div>
</div>
</>
);
};
export default TenantsList;

View File

@ -76,7 +76,8 @@ export const DateRangePicker1 = ({
placeholder = "Select date range", placeholder = "Select date range",
className = "", className = "",
allowText = false, allowText = false,
resetSignal, // <- NEW prop resetSignal,
defaultRange = true,
...rest ...rest
}) => { }) => {
const inputRef = useRef(null); const inputRef = useRef(null);
@ -124,10 +125,9 @@ export const DateRangePicker1 = ({
...rest, ...rest,
}); });
// Apply default if empty
const currentStart = getValues(startField); const currentStart = getValues(startField);
const currentEnd = getValues(endField); const currentEnd = getValues(endField);
if (!currentStart && !currentEnd) { if (defaultRange && !currentStart && !currentEnd) {
applyDefaultDates(); applyDefaultDates();
} else if (currentStart && currentEnd) { } else if (currentStart && currentEnd) {
instance.setDate([ instance.setDate([
@ -139,12 +139,11 @@ export const DateRangePicker1 = ({
return () => instance.destroy(); return () => instance.destroy();
}, []); }, []);
// Reapply default range on resetSignal change
useEffect(() => { useEffect(() => {
if (resetSignal !== undefined) { if (defaultRange && resetSignal !== undefined) {
applyDefaultDates(); applyDefaultDates();
} }
}, [resetSignal]); }, [resetSignal, defaultRange]);
const start = getValues(startField); const start = getValues(startField);
const end = getValues(endField); const end = getValues(endField);
@ -173,3 +172,4 @@ export const DateRangePicker1 = ({
</div> </div>
); );
}; };

View File

@ -15,7 +15,7 @@ const IconButton = ({
iconClass, // icon class string like 'bx bx-user' iconClass, // icon class string like 'bx bx-user'
color = "primary", color = "primary",
onClick, onClick,
size = 20, size = 5,
radius=null, radius=null,
style = {}, style = {},
...rest ...rest
@ -31,7 +31,7 @@ const IconButton = ({
style={{ style={{
backgroundColor, backgroundColor,
color: iconColor, color: iconColor,
padding: "0.4rem", padding: "0.3rem",
margin:'0rem 0.2rem', margin:'0rem 0.2rem',
...style, ...style,
}} }}

View File

@ -0,0 +1,12 @@
import React from "react";
const Label = ({ htmlFor, children, required = false, className = "" }) => {
return (
<label htmlFor={htmlFor} className={`form-label d-block ${className}`}>
{children}
{required && <span className="text-danger ms-1">*</span>}
</label>
);
};
export default Label;

View File

@ -2,37 +2,20 @@ import React from "react";
const Loader = () => { const Loader = () => {
return ( return (
<div className="demo-inline-spacing"> <div
<div className="spinner-grow text-primary" role="status"> className="d-flex justify-content-center align-items-center"
<span className="visually-hidden">Loading...</span> style={{ height: "50vh" }}
>
<div className="sk-wave">
<div className="sk-wave-rect"></div>
<div className="sk-wave-rect"></div>
<div className="sk-wave-rect"></div>
<div className="sk-wave-rect"></div>
<div className="sk-wave-rect"></div>
</div> </div>
{/* <div className="spinner-grow" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<div className="spinner-grow text-secondary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<div className="spinner-grow text-success" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<div className="spinner-grow text-danger" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<div className="spinner-grow text-warning" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<div className="spinner-grow text-info" role="status">
<span className="visually-hidden">Loading...</span>
</div> */}
<div className="spinner-grow text-light" role="status">
<span className="visually-hidden">Loading...</span>
</div>
{/* <div className="spinner-grow text-dark" role="status">
<span className="visually-hidden">Loading...</span>
</div> */}
</div> </div>
); );
}; };
export default Loader; export default Loader;

View File

@ -66,7 +66,6 @@
"available": true, "available": true,
"link": "/expenses" "link": "/expenses"
}, },
{ {
"text": "Image Gallary", "text": "Image Gallary",
"icon": "bx bx-images", "icon": "bx bx-images",
@ -80,9 +79,9 @@
"link": "", "link": "",
"submenu": [ "submenu": [
{ {
"text": "Users", "text": "Tenant",
"available": true, "available": true,
"link": "/employees/" "link": "/tenants"
}, },
{ {
"text": "Masters", "text": "Masters",

View File

@ -11,7 +11,15 @@ import showToast from "../../services/toastService";
export const useMasterMenu = ()=>{
return useQuery({
queryKey:["MasterMenu"],
queryFn:async()=> {
const resp = await MasterRespository.getMasterMenus();
return resp.data
}
})
}
export const useActivitiesMaster = () => export const useActivitiesMaster = () =>
{ {

View File

@ -99,3 +99,13 @@ export const useProfile = () => {
refetch, refetch,
}; };
}; };
export const useSidBarMenu = ()=>{
const userLogged = useSelector((store)=>store.globalVariables.loginUser);
return useQuery({
queryKey:["AppMenu"],
queryFn:async()=> await AuthRepository.appmenu(),
enabled: !!userLogged
})
}

View File

@ -175,6 +175,7 @@ export const useProjectInfra = (projectId) => {
} = useQuery({ } = useQuery({
queryKey: ["ProjectInfra", projectId], queryKey: ["ProjectInfra", projectId],
queryFn: async () => { queryFn: async () => {
if(!projectId) return null;
const res = await ProjectRepository.getProjectInfraByproject(projectId); const res = await ProjectRepository.getProjectInfraByproject(projectId);
return res.data; return res.data;
}, },

177
src/hooks/useTenant.js Normal file
View File

@ -0,0 +1,177 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { TenantRepository } from "../repositories/TenantRepository";
import { MarketRepository } from "../repositories/MarketRepository";
import showToast from "../services/toastService";
import { useDispatch } from "react-redux";
import { setCurrentTenant } from "../slices/globalVariablesSlice";
import { ITEMS_PER_PAGE } from "../utils/constants";
import moment from "moment";
const cleanFilter = (filter) => {
const cleaned = { ...filter };
["industryIds", "references"].forEach((key) => {
if (Array.isArray(cleaned[key]) && cleaned[key].length === 0) {
delete cleaned[key];
}
});
return cleaned;
};
export const useTenants = (pageNumber, filter, searchString = "") => {
return useQuery({
queryKey: ["Tenants", pageNumber, filter, searchString],
queryFn: async () => {
const cleanedFilter = cleanFilter(filter);
const response = await TenantRepository.getTenantList(
ITEMS_PER_PAGE,
pageNumber,
cleanedFilter,
searchString
);
return response.data;
},
keepPreviousData: true,
});
};
export const useTenantDetails = (id) => {
return useQuery({
queryKey: ["Tenant", id],
queryFn: async () => {
const response = await TenantRepository.getTenantDetails(id);
return response.data;
},
enabled:!!id
});
};
export const useIndustries = () => {
return useQuery({
queryKey: ["Industries"],
queryFn: async () => {
const res = await MarketRepository.getIndustries();
return res.data;
},
});
};
export const useSubscriptionPlan = (freq) => {
return useQuery({
queryKey: ["SubscriptionPlan", freq],
queryFn: async () => {
const res = await TenantRepository.getSubscriptionPlan(freq);
return res.data;
},
});
};
// ------------Mutation---------------------
export const useCreateTenant = (onSuccessCallback) => {
const dispatch = useDispatch();
return useMutation({
mutationFn: async (tenantPayload) => {
const res = await TenantRepository.createTenant(tenantPayload);
return res.data;
},
onSuccess: (data, variables) => {
showToast("Tenant Created SuccessFully", "success");
// dispatch(setCurrentTenant({operationMode:0,data:data}))
let operationMode = 0; // default = new tenant, needs subscription
if (data?.subscriptionHistery?.length > 0) {
operationMode = 1; // tenant already has a subscription
} else if (data && !data.subscriptionHistery) {
operationMode = 2; // tenant exists but subscription not added yet
}
dispatch(setCurrentTenant({ operationMode, data }));
if (onSuccessCallback) onSuccessCallback();
},
onError: (error) => {
showToast(
error.response.message ||
error?.response?.data?.errors ||
`Something went wrong`,
"error"
);
},
});
};
export const useUpdateTenantDetails = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, tenantPayload }) =>
TenantRepository.updateTenantDetails(id, tenantPayload),
onSuccess: (_, variables) => {
const { id } = variables.tenantPayload;
queryClient.invalidateQueries({ queryKey: ["Tenant", id] });
queryClient.invalidateQueries({ queryKey: ["Tenants"] });
if (onSuccessCallback) onSuccessCallback();
},
onError: (error) => {
showToast(
error.response.message || error.message || `Something went wrong`,
"error"
);
},
});
};
export const useAddSubscription = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (subscriptionPayload) => {
const res = await TenantRepository.addSubscription(subscriptionPayload);
return res.data;
},
onSuccess: (data, variables) => {
const { tenantId } = variables;
showToast("Tenant Plan Added SuccessFully", "success");
queryClient.invalidateQueries({ queryKey: ["Tenant", tenantId] });
if (onSuccessCallback) onSuccessCallback();
},
onError: (error) => {
showToast(
error.response.message || error.message || `Something went wrong`,
"error"
);
},
});
};
export const useUpgradeSubscription = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (subscriptionPayload) => {
const res = await TenantRepository.upgradeSubscription(
subscriptionPayload
);
return res.data;
},
onSuccess: (data, variables) => {
const { tenantId } = variables;
showToast("Tenant Plan Upgraded Successfully", "success");
// Refetch tenant details
queryClient.invalidateQueries({ queryKey: ["Tenant", tenantId] });
queryClient.invalidateQueries({ queryKey: ["Tenants"] });
if (onSuccessCallback) onSuccessCallback();
},
onError: (error) => {
showToast(
error?.response?.message ||
error?.response?.data?.errors ||
error.message ||
"Something went wrong",
"error"
);
},
});
};

View File

@ -1,31 +1,28 @@
import React, { useEffect, useState, useRef } from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch } from "react-redux";
import Breadcrumb from "../../components/common/Breadcrumb";
import { useTaskList } from "../../hooks/useTasks"; import { useTaskList } from "../../hooks/useTasks";
import { useProjectName, useProjects } from "../../hooks/useProjects"; import { useProjectName } from "../../hooks/useProjects";
import { setProjectId } from "../../slices/localVariablesSlice"; import { setProjectId } from "../../slices/localVariablesSlice";
import { ReportTask } from "../../components/Activities/ReportTask"; import Breadcrumb from "../../components/common/Breadcrumb";
import ReportTaskComments from "../../components/Activities/ReportTaskComments";
import DateRangePicker from "../../components/common/DateRangePicker"; import DateRangePicker from "../../components/common/DateRangePicker";
import { useSearchParams } from "react-router-dom"; import FilterIcon from "../../components/common/FilterIcon";
import moment from "moment";
import FilterIcon from "../../components/common/FilterIcon";
import GlobalModel from "../../components/common/GlobalModel"; import GlobalModel from "../../components/common/GlobalModel";
import AssignTask from "../../components/Project/AssignTask"; import ReportTask from "../../components/Activities/ReportTask";
import ReportTaskComments from "../../components/Activities/ReportTaskComments";
import SubTask from "../../components/Activities/SubTask"; import SubTask from "../../components/Activities/SubTask";
import {formatNumber} from "../../utils/dateUtils"; import { formatNumber, formatUTCToLocalTime } from "../../utils/dateUtils";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { APPROVE_TASK, ASSIGN_REPORT_TASK } from "../../utils/constants"; import { APPROVE_TASK, ASSIGN_REPORT_TASK } from "../../utils/constants";
import { useSelectedproject } from "../../slices/apiDataManager"; import { useSelectedproject } from "../../slices/apiDataManager";
import moment from "moment";
import Loader from "../../components/common/Loader";
const DailyTask = () => { const DailyTask = () => {
// const selectedProject = useSelector( const dispatch = useDispatch();
// (store) => store.localVariables.projectId
// );
const selectedProject = useSelectedproject(); const selectedProject = useSelectedproject();
const dispatch = useDispatch() const { projectNames } = useProjectName();
const { projectNames, loading: projectLoading, fetchData } = useProjectName(); const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK);
const [filters, setFilters] = useState({ const [filters, setFilters] = useState({
selectedBuilding: "", selectedBuilding: "",
@ -33,427 +30,201 @@ const DailyTask = () => {
selectedActivities: [], selectedActivities: [],
}); });
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" }); const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK)
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK)
const { const { TaskList, loading: taskLoading } = useTaskList(
TaskList, selectedProject || null,
loading: task_loading, dateRange?.startDate || null,
error: task_error,
refetch,
} = useTaskList(
selectedProject || null,
dateRange?.startDate || null,
dateRange?.endDate || null dateRange?.endDate || null
); );
// Ensure project is set
useEffect(() => { useEffect(() => {
if(selectedProject == null){ if (!selectedProject && projectNames.length > 0) {
dispatch(setProjectId(projectNames[0]?.id)); dispatch(setProjectId(projectNames[0].id));
}
},[])
const [TaskLists, setTaskLists] = useState([]);
const [dates, setDates] = useState([]);
const popoverRefs = useRef([]);
useEffect(() => {
if (TaskList) {
let filteredTasks = TaskList;
if (filters.selectedBuilding) {
filteredTasks = filteredTasks.filter(
(task) =>
task?.workItem?.workArea?.floor?.building?.name ===
filters.selectedBuilding
);
}
if (filters.selectedFloors.length > 0) {
filteredTasks = filteredTasks?.filter((task) =>
filters.selectedFloors?.includes(
task?.workItem?.workArea?.floor?.floorName
)
);
}
if (filters.selectedActivities.length > 0) {
filteredTasks = filteredTasks.filter((task) =>
filters.selectedActivities.includes(
task?.workItem?.activityMaster?.activityName
)
);
}
setTaskLists(filteredTasks);
} else {
setTaskLists([]);
} }
}, [ }, [selectedProject, projectNames, dispatch]);
TaskList,
filters?.selectedBuilding,
filters?.selectedFloors,
filters?.selectedActivities,
]);
useEffect(() => { // Memoized filtering
const AssignmentDates = [ const filteredTasks = useMemo(() => {
...new Set(TaskLists.map((task) => task.assignmentDate.split("T")[0])), if (!TaskList) return [];
].sort((a, b) => new Date(b) - new Date(a)); return TaskList.filter((task) => {
setDates(AssignmentDates); const { selectedBuilding, selectedFloors, selectedActivities } = filters;
}, [TaskLists]);
const [selectedTask, selectTask] = useState(null); if (selectedBuilding && task?.workItem?.workArea?.floor?.building?.name !== selectedBuilding) return false;
const [comments, setComment] = useState({ task: null, isActionAllow: false }); if (selectedFloors.length > 0 && !selectedFloors.includes(task?.workItem?.workArea?.floor?.floorName)) return false;
if (selectedActivities.length > 0 && !selectedActivities.includes(task?.workItem?.activityMaster?.activityName)) return false;
const [isModalOpen, setIsModalOpen] = useState(false); return true;
const [isModalOpenComment, setIsModalOpenComment] = useState(false);
const openModal = () => setIsModalOpen(true);
const closeModal = () => setIsModalOpen(false);
const openComment = () => setIsModalOpenComment(true);
const closeCommentModal = () => setIsModalOpenComment(false);
const [IsSubTaskNeeded, setIsSubTaskNeeded] = useState(false);
const [SubTaskData, setSubTaskData] = useState();
const handletask = (task) => {
selectTask(task);
openModal();
};
useEffect(() => {
popoverRefs.current.forEach((el) => {
if (el) {
new bootstrap.Popover(el, {
trigger: "focus",
placement: "left",
html: true,
content: el.getAttribute("data-bs-content"),
});
}
}); });
}, [dates, TaskLists]); }, [TaskList, filters]);
// Memoized dates
const groupedTasks = useMemo(() => {
const groups = {};
filteredTasks.forEach((task) => {
const date = task.assignmentDate.split("T")[0];
if (!groups[date]) groups[date] = [];
groups[date].push(task);
});
return Object.keys(groups)
.sort((a, b) => new Date(b) - new Date(a))
.map((date) => ({ date, tasks: groups[date] }));
}, [filteredTasks]);
const handlecloseModal = () => // --- Modal State
{ const [modal, setModal] = useState({ type: null, data: null });
setIsModalOpen( false )
// refetch();
}
const handleCloseAction = (IsSubTask) => { const openModal = (type, data = null) => setModal({ type, data });
if (IsSubTask) { const closeModal = () => setModal({ type: null, data: null });
setIsSubTaskNeeded(true);
setIsModalOpenComment(false);
} else {
// refetch();
setIsModalOpenComment(false);
}
};
const hanleCloseSubTask = () => {
setIsSubTaskNeeded(false);
setComment( null );
// refetch();
};
// --- Render helpers
const renderTeamMembers = (task, refIndex) => (
<div
key={refIndex}
tabIndex="0"
className="d-flex align-items-center avatar-group justify-content-center"
data-bs-toggle="popover"
data-bs-trigger="focus"
data-bs-placement="left"
data-bs-html="true"
data-bs-content={`
<div class="border border-secondary rounded custom-popover p-2 px-3">
${task.teamMembers
.map(
(m) => `
<div class="d-flex align-items-center gap-2 mb-2">
<div class="avatar avatar-xs">
<span class="avatar-initial rounded-circle bg-label-primary">
${m?.firstName?.charAt(0) || ""}${m?.lastName?.charAt(0) || ""}
</span>
</div>
<span>${m.firstName} ${m.lastName}</span>
</div>`
)
.join("")}
</div>
`}
>
{task.teamMembers.slice(0, 3).map((m) => (
<div key={m.id} className="avatar avatar-xs" title={`${m.firstName} ${m.lastName}`}>
<span className="avatar-initial rounded-circle bg-label-primary">
{m?.firstName.slice(0, 1)}
</span>
</div>
))}
{task.teamMembers.length > 3 && (
<div className="avatar avatar-xs" title={`${task.teamMembers.length - 3} more`}>
<span className="avatar-initial rounded-circle bg-label-secondary">+{task.teamMembers.length - 3}</span>
</div>
)}
</div>
);
return ( return (
<> <>
{isModalOpen && <GlobalModel isOpen={isModalOpen} size="md" closeModal={handlecloseModal} > {/* --- Modals --- */}
<ReportTask {modal.type === "report" && (
report={selectedTask} <GlobalModel isOpen size="md" closeModal={closeModal}>
closeModal={handlecloseModal} <ReportTask report={modal.data} closeModal={closeModal} />
// refetch={refetch} </GlobalModel>
/> )}
</GlobalModel>} {modal.type === "comments" && (
<GlobalModel isOpen size="lg" closeModal={closeModal}>
{isModalOpenComment && (
<GlobalModel
isOpen={isModalOpenComment}
size="lg"
closeModal={() => setIsModalOpenComment(false)}
>
<ReportTaskComments <ReportTaskComments
commentsData={comments.task} commentsData={modal.data.task}
actionAllow={comments.isActionAllow} actionAllow={modal.data.isActionAllow}
handleCloseAction={handleCloseAction} handleCloseAction={(isSubTask) => {
closeModal={closeCommentModal} if (isSubTask) openModal("subtask", modal.data.task);
else closeModal();
}}
closeModal={closeModal}
/> />
</GlobalModel> </GlobalModel>
)} )}
{modal.type === "subtask" && (
{IsSubTaskNeeded && ( <GlobalModel isOpen size="lg" closeModal={closeModal}>
<GlobalModel <SubTask activity={modal.data} onClose={closeModal} />
isOpen={IsSubTaskNeeded}
size="lg"
closeModal={hanleCloseSubTask}
>
<SubTask activity={comments.task} onClose={hanleCloseSubTask} />
</GlobalModel> </GlobalModel>
)} )}
<div className="container-fluid"> <div className="container-fluid">
<Breadcrumb <Breadcrumb data={[{ label: "Home", link: "/dashboard" }, { label: "Daily Progress Report" }]} />
data={[
{ label: "Home", link: "/dashboard" }, <div className="card card-action mb-6">
{ label: "Daily Progress Report", link: null },
]}
></Breadcrumb>
<div className="card card-action mb-6 ">
<div className="card-body p-1 p-sm-2"> <div className="card-body p-1 p-sm-2">
<div className="row d-flex justify-content-between align-items-center"> {!selectedProject && (<div className="text-center text-muted">Please Select Project</div>)}
<div className="col-md-12 d-flex align-items-center col-12 text-start mb-2 mb-md-0"> {/* --- Filters --- */}
<DateRangePicker <div className="d-flex align-items-center mb-2">
onRangeChange={setDateRange} <DateRangePicker onRangeChange={setDateRange} endDateMode="today" DateDifference="6" dateFormat="DD-MM-YYYY" />
endDateMode="today" <FilterIcon
DateDifference="6" taskListData={TaskList}
dateFormat="DD-MM-YYYY" onApplyFilters={setFilters}
/> currentSelectedBuilding={filters.selectedBuilding}
<FilterIcon currentSelectedFloors={filters.selectedFloors}
taskListData={TaskList} currentSelectedActivities={filters.selectedActivities}
onApplyFilters={setFilters} />
currentSelectedBuilding={filters.selectedBuilding}
currentSelectedFloors={filters.selectedFloors}
currentSelectedActivities={filters.selectedActivities}
/>
</div>
</div> </div>
{/* --- Table --- */}
<div className="table-responsive text-nowrap mt-3"> <div className="table-responsive text-nowrap mt-3">
<table className="table"> <table className="table">
<thead> <thead>
<tr> <tr>
<th>Activity</th> <th>Activity</th>
<th>Assigned </th> <th>Assigned</th>
<th>Completed</th> <th>Completed</th>
<th>Assign On</th> <th>Assign On</th>
<th>Team</th> <th>Team</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody className="table-border-bottom-0"> <tbody>
{/* --- Spinner when tasks are loading --- */} {taskLoading && (
{task_loading && (
<tr> <tr>
<td colSpan={6} className="text-center"> <td colSpan={6} className="text-center">
{" "} <Loader/>
<div className="mt-10 mb-10 pt-5 pb-10">
<div
className="spinner-border text-primary"
role="status"
>
<span className="visually-hidden">Loading...</span>
</div>
<p className="mt-2">Loading Tasks...</p>
</div>
</td> </td>
</tr> </tr>
)} )}
{!task_loading && {!taskLoading && groupedTasks.length === 0 && (
TaskLists.length === 0 && ( <tr>
<tr> <td colSpan={6} className="text-center">No Reports Found</td>
<td colSpan={6} className="text-center"> </tr>
<div className="mt-10 mb-10 pt-10 pb-10"> )}
{" "} {!taskLoading &&
<p>No Reports Found</p> groupedTasks.map(({ date, tasks }) => (
</div> <React.Fragment key={date}>
</td> <tr className="table-row-header text-start">
</tr> <td colSpan={6}><strong>{formatUTCToLocalTime(date)}</strong></td>
)} </tr>
{!task_loading && {tasks.map((task, idx) => (
TaskLists.length > 0 && <tr key={task.id || idx}>
dates.map((date, i) => { <td className="flex-wrap text-start">
const tasksForDate = TaskLists.filter((task) => <div>{task.workItem.activityMaster?.activityName || "No Activity Name"}</div>
task.assignmentDate.includes(date) <div className="text-sm">
); {task.workItem.workArea?.floor?.building?.name} {task.workItem.workArea?.floor?.floorName} {task.workItem.workArea?.areaName}
if (tasksForDate.length === 0) return null; </div>
</td>
return ( <td>{formatNumber(task.plannedTask)} / {formatNumber(task.workItem.plannedWork - task.workItem.completedWork)}</td>
<React.Fragment key={i}> <td>{task.completedTask}</td>
<tr className="table-row-header"> <td>{formatUTCToLocalTime(task.assignmentDate)}</td>
<td colSpan={6} className="text-start"> <td className="text-center">{renderTeamMembers(task, idx)}</td>
{" "} <td className="text-center">
<strong> <div className="d-flex justify-content-end gap-2">
{moment(date).format("DD-MM-YYYY")} {ReportTaskRights && !task.reportedDate && (
</strong> <button className="btn btn-xs btn-primary" onClick={() => openModal("report", task)}>Report</button>
)}
{ApprovedTaskRights && task.reportedDate && !task.approvedBy && (
<button className="btn btn-xs btn-warning" onClick={() => openModal("comments", { task, isActionAllow: true })}>QC</button>
)}
<button className="btn btn-xs btn-primary" onClick={() => openModal("comments", { task, isActionAllow: false })}>Comment</button>
</div>
</td> </td>
</tr> </tr>
{tasksForDate.map((task, index) => { ))}
const refIndex = index * 10 + i; </React.Fragment>
return ( ))}
<React.Fragment key={refIndex}>
<tr>
<td className="flex-wrap text-start">
<div>
{task.workItem.activityMaster
.activityName || "No Activity Name"}
</div>
<div>
<label className="col-form-label text-sm">
{" "}
{
task?.workItem?.workArea?.floor
?.building?.name
}{" "}
<i
className="bx bx-chevron-right text-sm"
style={{ fontSize: ".75rem" }}
></i>{" "}
{
task?.workItem?.workArea?.floor
?.floorName
}{" "}
<i
className="bx bx-chevron-right text-sm"
style={{ fontSize: ".75rem" }}
>
{" "}
</i>
{task?.workItem?.workArea?.areaName}
</label>
</div>
</td>
<td>
{formatNumber(task.plannedTask)} / {formatNumber(task.workItem.plannedWork - task.workItem.completedWork)}
</td>
<td>{task.completedTask}</td>
<td>
{moment(task.assignmentDate).format(
"DD-MM-YYYY"
)}
</td>
<td className="text-center">
<div
key={refIndex}
ref={(el) =>
(popoverRefs.current[refIndex] = el)
}
tabIndex="0"
className="d-flex align-items-center avatar-group justify-content-center"
data-bs-toggle="popover"
data-bs-trigger="focus"
data-bs-placement="left"
data-bs-html="true"
data-bs-content={`
<div class="border border-secondary rounded custom-popover p-2 px-3">
${task.teamMembers
.map(
(member) => `
<div class="d-flex align-items-center gap-2 mb-2">
<div class="avatar avatar-xs">
<span class="avatar-initial rounded-circle bg-label-primary">
${
member?.firstName?.charAt(
0
) || ""
}${
member?.lastName?.charAt(0) || ""
}
</span>
</div>
<span>${member.firstName} ${
member.lastName
}</span>
</div>
`
)
.join("")}
</div>
`}
>
{task.teamMembers
.slice(0, 3)
.map((member) => (
<div
key={member.id}
data-bs-toggle="tooltip"
data-bs-html="true"
data-popup="tooltip-custom"
data-bs-placement="top"
title={`${member.firstName} ${member.lastName}`}
className="avatar avatar-xs"
>
<span className="avatar-initial rounded-circle bg-label-primary">
{member?.firstName.slice(0, 1)}
</span>
</div>
))}
{task.teamMembers.length > 3 && (
<div
className="avatar avatar-xs"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
title={`${
task.teamMembers.length - 3
} more`}
>
<span className="avatar-initial rounded-circle bg-label-secondary pull-up">
+ {task.teamMembers.length - 3}
</span>
</div>
)}
</div>
</td>
<td className="text-center">
<div className="d-flex justify-content-end">
{ ReportTaskRights &&
<button
type="button"
className={`btn btn-xs btn-primary ${
task.reportedDate != null
? "d-none"
: ""
}`}
onClick={() => {
selectTask(task);
openModal();
}}
>
Report
</button>
}
{(ApprovedTaskRights && task.reportedDate ) && (
<button
type="button"
className={`btn btn-xs btn-warning ${
task.reportedDate && task.approvedBy
? "d-none"
: ""
}`}
onClick={() => {
setComment({
task: task,
isActionAllow: true,
});
openComment();
}}
>
QC
</button>
)}
<button
type="button"
className="btn btn-xs btn-primary ms-2"
onClick={() => {
setComment({
task: task,
isActionAllow: false,
});
openComment();
}}
>
Comment
</button>
</div>
</td>
</tr>
</React.Fragment>
);
})}
</React.Fragment>
);
})}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -1,4 +1,4 @@
import React,{useEffect} from "react"; import React,{useEffect,useRef} 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";
@ -6,33 +6,34 @@ import { useDispatch, useSelector } from "react-redux";
import { setProjectId } from "../../slices/localVariablesSlice"; import { setProjectId } from "../../slices/localVariablesSlice";
import { useSelectedproject } from "../../slices/apiDataManager"; import { useSelectedproject } from "../../slices/apiDataManager";
const TaskPlannng = () => { const TaskPlannng = () => {
// const selectedProject = useSelector(
// (store) => store.localVariables.projectId
// );
const selectedProject = useSelectedproject(); const selectedProject = useSelectedproject();
const dispatch = useDispatch();
const { projectNames, loading: projectLoading } = useProjectName();
const initialized = useRef(false);
const dispatch = useDispatch()
const { projectNames, loading: projectLoading, fetchData } = useProjectName();
useEffect(() => { useEffect(() => {
if(selectedProject == null){ if (!initialized.current && projectNames.length > 0 && !selectedProject?.id) {
dispatch(setProjectId(projectNames[0]?.id)); dispatch(setProjectId(projectNames[0].id));
} initialized.current = true;
},[]) }
}, [projectNames, selectedProject, dispatch]);
return ( return (
<> <div className="container-fluid">
<div className="container-fluid"> <Breadcrumb
<Breadcrumb data={[
data={[ { label: "Home", link: "/dashboard" },
{ label: "Home", link: "/dashboard" }, { label: "Daily Task Planning" },
{ label: "Daily Task Planning" } ]}
]} />
></Breadcrumb> {selectedProject ? (
<InfraPlanning/> <InfraPlanning />
</div> ) : (
</> <div className="text-center">Please Select Project</div>
)}
</div>
); );
}; };

View File

@ -80,21 +80,23 @@ const ExpensePage = () => {
}; };
useEffect(() => { useEffect(() => {
setShowTrigger(true); if (IsViewAll || IsViewSelf || IsCreatedAble) {
setOffcanvasContent( setShowTrigger(true);
"Expense Filters", setOffcanvasContent(
<ExpenseFilterPanel "Expense Filters",
onApply={setFilter} <ExpenseFilterPanel
handleGroupBy={setGroupBy} onApply={setFilter}
clearFilter={clearFilter} handleGroupBy={setGroupBy}
/> clearFilter={clearFilter}
); />
);
}
return () => { return () => {
setShowTrigger(false); setShowTrigger(false);
setOffcanvasContent("", null); setOffcanvasContent("", null);
}; };
}, []); }, [IsViewAll, IsViewSelf, IsCreatedAble]);
const contextValue = { const contextValue = {
setViewExpense, setViewExpense,
@ -105,16 +107,17 @@ const ExpensePage = () => {
return ( return (
<ExpenseContext.Provider value={contextValue}> <ExpenseContext.Provider value={contextValue}>
<div className="container-fluid"> <div className="container-fluid">
<Breadcrumb data={[{ label: "Home", link: "/" }, { label: "Expense" }]} /> <Breadcrumb
data={[{ label: "Home", link: "/" }, { label: "Expense" }]}
/>
{(IsViewAll || IsViewSelf || IsCreatedAble) ? ( {IsViewAll || IsViewSelf || IsCreatedAble ? (
<> <>
<div className="card my-3 px-sm-4 px-0"> <div className="card my-3 px-sm-4 px-0">
<div className="card-body py-2 px-3"> <div className="card-body py-2 px-3">
<div className="row align-items-center"> <div className="row align-items-center">
<div className="col-6 "> <div className="col-6 ">
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<input <input
type="search" type="search"
className="form-control form-control-sm w-auto" className="form-control form-control-sm w-auto"
@ -132,7 +135,12 @@ const ExpensePage = () => {
type="button" type="button"
className="p-1 me-1 m-sm-0 bg-primary rounded-circle" className="p-1 me-1 m-sm-0 bg-primary rounded-circle"
title="Add New Expense" title="Add New Expense"
onClick={() => setManageExpenseModal({ IsOpen: true, expenseId: null })} onClick={() =>
setManageExpenseModal({
IsOpen: true,
expenseId: null,
})
}
> >
<i className="bx bx-plus fs-4 text-white"></i> <i className="bx bx-plus fs-4 text-white"></i>
</button> </button>
@ -142,12 +150,18 @@ const ExpensePage = () => {
</div> </div>
</div> </div>
<ExpenseList filters={filters} groupBy={groupBy} searchText={searchText} /> <ExpenseList
filters={filters}
groupBy={groupBy}
searchText={searchText}
/>
</> </>
) : ( ) : (
<div className="card text-center py-1"> <div className="card text-center py-1">
<i className="fa-solid fa-triangle-exclamation fs-5" /> <i className="fa-solid fa-triangle-exclamation fs-5" />
<p>Access Denied: You don't have permission to perform this action!</p> <p>
Access Denied: You don't have permission to perform this action !
</p>
</div> </div>
)} )}
@ -156,12 +170,16 @@ const ExpensePage = () => {
<GlobalModel <GlobalModel
isOpen isOpen
size="lg" size="lg"
closeModal={() => setManageExpenseModal({ IsOpen: null, expenseId: null })} closeModal={() =>
setManageExpenseModal({ IsOpen: null, expenseId: null })
}
> >
<ManageExpense <ManageExpense
key={ManageExpenseModal.expenseId ?? "new"} key={ManageExpenseModal.expenseId ?? "new"}
expenseToEdit={ManageExpenseModal.expenseId} expenseToEdit={ManageExpenseModal.expenseId}
closeModal={() => setManageExpenseModal({ IsOpen: null, expenseId: null })} closeModal={() =>
setManageExpenseModal({ IsOpen: null, expenseId: null })
}
/> />
</GlobalModel> </GlobalModel>
)} )}

View File

@ -0,0 +1,20 @@
import React from 'react'
import Breadcrumb from '../../components/common/Breadcrumb'
import TenantForm from '../../components/Tenant/TenantForm'
const CreateTenant = () => {
return (
<div className='container-fluid'>
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Tenant", link: '/tenants' },
{ label: "New Tenant", link: null },
]}
/>
<TenantForm/>
</div>
)
}
export default CreateTenant

View File

@ -0,0 +1,36 @@
import React, { useEffect, useMemo } from "react";
import { useProfile } from "../../hooks/useProfile";
import TenantDetails from "./TenantDetails";
import { hasUserPermission } from "../../utils/authUtils";
import { VIEW_TENANTS } from "../../utils/constants";
import { useNavigate } from "react-router-dom";
import Loader from "../../components/common/Loader";
const SelfTenantDetails = () => {
const { profile, loading } = useProfile();
const tenantId = profile?.employeeInfo?.tenantId;
const navigate = useNavigate();
const isSelfTenantView = hasUserPermission(VIEW_TENANTS);
useEffect(() => {
if (!isSelfTenantView) {
navigate("/tenants");
}
}, [isSelfTenantView, navigate]);
if (loading || !tenantId) {
return <Loader/>;
}
return (
<TenantDetails
tenantId={tenantId}
wrapInContainer={true}
showBreadcrumb={true}
iTSelf={true}
/>
);
};
export default SelfTenantDetails;

View File

@ -0,0 +1,8 @@
import React from "react";
import TenantDetails from "./TenantDetails";
const SuperTenantDetails = () => {
return <TenantDetails wrapInContainer showBreadcrumb iTSelf={false} />;
};
export default SuperTenantDetails;

View File

@ -0,0 +1,182 @@
import React, { createContext, useContext, useState, useMemo } from "react";
import { useParams } from "react-router-dom";
import Breadcrumb from "../../components/common/Breadcrumb";
import Profile from "../../components/Tenant/Profile";
import { useTenantDetails } from "../../hooks/useTenant";
import { ComingSoonPage } from "../Misc/ComingSoonPage";
import GlobalModel from "../../components/common/GlobalModel";
import EditProfile from "../../components/Tenant/EditProfile";
import SubScriptionHistory from "../../components/Tenant/SubScriptionHistory";
import Loader from "../../components/common/Loader";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { MANAGE_TENANTS, SUPPER_TENANT } from "../../utils/constants";
const TenantDetailsContext = createContext();
export const useTenantDetailsContext = () => useContext(TenantDetailsContext);
const TenantDetails = ({
tenantId: tenantIdProp,
wrapInContainer = true,
showBreadcrumb = true,
iTSelf = true,
}) => {
const { tenantId: tenantIdFromUrl } = useParams();
const activeTenantId = tenantIdFromUrl || tenantIdProp;
const { data, isLoading, isError, error } = useTenantDetails(activeTenantId);
const ManageTenant = useHasUserPermission(SUPPER_TENANT);
const ModifyTenant = useHasUserPermission(MANAGE_TENANTS);
const [editTenant, setEditTenant] = useState(false);
const contextValues = useMemo(
() => ({ editTenant, setEditTenant }),
[editTenant]
);
const tabs = useMemo(() => {
const allTabs = [
{
id: "navs-left-home",
label: "Profile",
icon: "bx bx-user-circle",
iconSize: "bx-sm",
content: <Profile data={data} />,
},
{
id: "navs-left-bill",
label: "Bills and Plan",
icon: "bx bx-receipt",
iconSize: "bx-sm",
content: (
<div className="text-center">
<SubScriptionHistory tenantId={activeTenantId} />
</div>
),
},
{
id: "navs-left-messages",
label: "Messages",
icon: "bx bx-message-rounded",
iconSize: "bx-sm",
content: (
<div className="text-center">
<ComingSoonPage />
</div>
),
},
];
return ManageTenant
? allTabs
: [allTabs[0], allTabs[allTabs.length - 1]];
}, [data, activeTenantId, ManageTenant, ModifyTenant]);
if (!activeTenantId) return <div className="my-4">No tenant selected.</div>;
if (isLoading)
return (
<div className="my-4">
<Loader />
</div>
);
if (isError)
return (
<div className="container-fluid">
{error.status === 403 ? (
<div className="card text-center my-4 p-2">
<i className="fa-solid fa-triangle-exclamation fs-5"></i>
<p>
Access Denied: You don't have permission to perform this action!
</p>
</div>
) : (
<div className="card text-center my-4 p-2">
<i className="fa-solid fa-triangle-exclamation fs-5"></i>
<p>{error.message}</p>
</div>
)}
</div>
);
const Shell = ({ children }) =>
wrapInContainer ? (
<div className="container-fluid py-0">{children}</div>
) : (
<>{children}</>
);
return (
<>
<TenantDetailsContext.Provider value={contextValues}>
<Shell>
{showBreadcrumb && (
<Breadcrumb
data={
iTSelf
? [
{ label: "Home", link: "/dashboard" },
{ label: "Tenant Details", link: null },
]
: [
{ label: "Home", link: "/dashboard" },
{ label: "Tenant", link: "/tenants" },
{ label: "Tenant Details", link: null },
]
}
/>
)}
<div className="nav-align-left nav-tabs-shadow mb-6">
<ul className="nav nav-tabs py-2 page-min-h" role="tablist">
{tabs.map((tab, index) => (
<li key={tab.id} className="nav-item">
<button
type="button"
className={`nav-link d-flex align-items-center text-tiny gap-2 ${
index === 0 ? "active" : ""
}`}
role="tab"
data-bs-toggle="tab"
data-bs-target={`#${tab.id}`}
aria-controls={tab.id}
aria-selected={index === 0}
>
{tab.icon && (
<i className={`${tab.icon} ${tab.iconSize}`} />
)}
{tab.label}
</button>
</li>
))}
</ul>
<div className="tab-content">
{tabs.map((tab, index) => (
<div
key={tab.id}
className={`tab-pane fade ${
index === 0 ? "show active" : ""
} text-start`}
id={tab.id}
>
{tab.content}
</div>
))}
</div>
</div>
</Shell>
</TenantDetailsContext.Provider>
{editTenant && (
<GlobalModel
size="lg"
isOpen={editTenant}
closeModal={() => setEditTenant(false)}
>
<EditProfile
TenantId={activeTenantId}
onClose={() => setEditTenant(false)}
/>
</GlobalModel>
)}
</>
);
};
export default TenantDetails;

View File

@ -0,0 +1,189 @@
import React, {
useState,
createContext,
useEffect,
useContext,
useCallback,
useMemo,
} from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
// ------ Components -------
import Breadcrumb from "../../components/common/Breadcrumb";
import TenantsList from "../../components/Tenant/TenantsList";
import TenantFilterPanel from "../../components/Tenant/TenantFilterPanel";
// ------ Context & Utils -------
import { useDebounce } from "../../utils/appUtils";
import { useFab } from "../../Context/FabContext";
import { setCurrentTenant } from "../../slices/globalVariablesSlice";
import { hasUserPermission } from "../../utils/authUtils";
// ------ Schema -------
import {
defaultFilterValues,
filterSchema,
} from "../../components/Tenant/TenantSchema";
// ------ Constants -------
import {
MANAGE_TENANTS,
SUPPER_TENANT,
VIEW_TENANTS,
} from "../../utils/constants";
import { useProfile } from "../../hooks/useProfile";
// ---------- Context ----------
export const TenantContext = createContext();
export const useTenantContext = () => {
const context = useContext(TenantContext);
if (!context) {
throw new Error(
"useTenantContext must be used within a TenantContext.Provider"
);
}
return context;
};
const TenantPage = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { profile } = useProfile();
// ---------- State ----------
const [searchText, setSearchText] = useState("");
const [isRefetching, setIsRefetching] = useState(false);
const [refetchFn, setRefetchFn] = useState(null);
const [filters, setFilters] = useState();
// ---------- Hooks ----------
const debouncedSearch = useDebounce(searchText, 500);
const { setOffcanvasContent, setShowTrigger } = useFab();
const isSuperTenant = hasUserPermission(SUPPER_TENANT);
const canManageTenants = hasUserPermission(MANAGE_TENANTS);
const isSelfTenant = hasUserPermission(VIEW_TENANTS);
const methods = useForm({
resolver: zodResolver(filterSchema),
defaultValues: defaultFilterValues,
});
const { reset } = methods;
const handleApplyFilters = useCallback((values) => {
setFilters(values);
}, []);
const filterPanelElement = useMemo(
() => <TenantFilterPanel onApply={handleApplyFilters} />,
[handleApplyFilters]
);
// ---------- Fab Filter Panel ----------
useEffect(() => {
if (!isSuperTenant) return;
setShowTrigger(true);
setOffcanvasContent("Tenant Filters", filterPanelElement);
return () => {
setShowTrigger(false);
setOffcanvasContent("", null);
};
}, [isSuperTenant, filterPanelElement, profile]);
// ---------- Redirect for Self Tenant ----------
useEffect(() => {
if (!isSuperTenant && isSelfTenant) {
// Delay navigation to next tick to avoid "update during render" warning
setTimeout(() => {
navigate("/tenant/self");
}, 0);
}
}, [isSuperTenant, isSelfTenant, navigate]);
// ---------- Handlers ----------
const handleNewTenant = () => {
dispatch(setCurrentTenant(null));
navigate("/tenants/new-tenant");
};
// ---------- Context Value ----------
const contextValue = {};
return (
<TenantContext.Provider value={contextValue}>
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Tenant", link: null },
]}
/>
{/* Super Tenant Actions */}
{isSuperTenant && (
<div className="card d-flex p-2">
<div className="row align-items-center">
{/* Search */}
<div className="col-6 col-md-6 col-lg-3 mb-md-0">
<input
type="search"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="form-control form-control-sm"
placeholder="Search Tenant"
/>
</div>
{/* Actions */}
<div className="col-6 col-md-6 col-lg-9 text-end">
<span
className="text-tiny text-muted p-1 border-0 bg-none lead mx-3 cursor-pointer"
disabled={isRefetching}
onClick={() => refetchFn && refetchFn()}
>
Refresh{" "}
<i
className={`bx bx-refresh ms-1 ${
isRefetching ? "bx-spin" : ""
}`}
></i>
</span>
<button
type="button"
title="Add New Tenant"
className="p-1 bg-primary rounded-circle cursor-pointer"
onClick={handleNewTenant}
>
<i className="bx bx-plus fs-4 text-white"></i>
</button>
</div>
</div>
</div>
)}
{/* Tenant List or Access Denied */}
{isSuperTenant ? (
<TenantsList
filters={filters}
searchText={debouncedSearch}
setIsRefetching={setIsRefetching}
setRefetchFn={setRefetchFn}
/>
) : !isSelfTenant ? (
<div className="card text-center my-4 p-2">
<i className="fa-solid fa-triangle-exclamation fs-5"></i>
<p>
Access Denied: You don't have permission to perform this action!
</p>
</div>
) : null}
</div>
</TenantContext.Provider>
);
};
export default TenantPage;

View File

@ -4,7 +4,7 @@ import MasterModal from "../../components/master/MasterModal";
import { mastersList } from "../../data/masters"; import { mastersList } from "../../data/masters";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice"; import { changeMaster } from "../../slices/localVariablesSlice";
import useMaster from "../../hooks/masterHook/useMaster" import useMaster, { useMasterMenu } from "../../hooks/masterHook/useMaster"
import MasterTable from "./MasterTable"; import MasterTable from "./MasterTable";
import { getCachedData } from "../../slices/apiDataManager"; import { getCachedData } from "../../slices/apiDataManager";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
@ -13,6 +13,7 @@ import { useQueryClient } from "@tanstack/react-query";
const MasterPage = () => { const MasterPage = () => {
const {data,isLoading,isError,error:menuError} = useMasterMenu()
const [modalConfig, setModalConfig] = useState({ modalType: "", item: null, masterType: null }); const [modalConfig, setModalConfig] = useState({ modalType: "", item: null, masterType: null });
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [filteredResults, setFilteredResults] = useState([]); const [filteredResults, setFilteredResults] = useState([]);
@ -23,7 +24,7 @@ const MasterPage = () => {
const selectedMaster = useSelector((store) => store.localVariables.selectedMaster); const selectedMaster = useSelector((store) => store.localVariables.selectedMaster);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: masterData = [], loading, error, RecallApi } = useMaster(); const { data: masterData = [], loading, error, RecallApi,isError:isMasterError } = useMaster();
const openModal = () => setIsCreateModalOpen(true); const openModal = () => setIsCreateModalOpen(true);
@ -83,7 +84,10 @@ const MasterPage = () => {
}; };
}, []); }, []);
if(isError || isMasterError) return <div className="d-flex flex-column align-items-center justify-content-center py-5">
<h4 className=" mb-3"><i className="fa-solid fa-triangle-exclamation fs-5" /> Oops, an error occurred</h4>
<p className="text-muted">{error?.message || menuError?.message}</p>
</div>
return ( return (
<> <>
{isCreateModalOpen && ( {isCreateModalOpen && (
@ -121,8 +125,8 @@ const MasterPage = () => {
className="form-select form-select-sm" className="form-select form-select-sm"
value={selectedMaster} value={selectedMaster}
> >
{isLoading && (<option value={null}>Loading...</option>)}
{mastersList.map((item) => ( {(!isLoading && data) && data?.map((item) => (
<option key={item.id} value={item.name}>{item.name}</option> <option key={item.id} value={item.name}>{item.name}</option>
))} ))}
@ -154,7 +158,6 @@ const MasterPage = () => {
<button <button
className={`btn btn-sm add-new btn-primary `} className={`btn btn-sm add-new btn-primary `}
// ${hasUserPermission('660131a4-788c-4739-a082-cbbf7879cbf2') ? "":"d-none"}
tabIndex="0" tabIndex="0"
aria-controls="DataTables_Table_0" aria-controls="DataTables_Table_0"
type="button" type="button"
@ -162,7 +165,6 @@ const MasterPage = () => {
data-bs-target="#master-modal" data-bs-target="#master-modal"
onClick={() => { onClick={() => {
handleModalData(selectedMaster, "null", selectedMaster) handleModalData(selectedMaster, "null", selectedMaster)
}} }}
> >
<span> <span>

View File

@ -137,7 +137,6 @@ const ProjectDetails = () => {
<div className="row"> <div className="row">
<ProjectNav onPillClick={handlePillClick} activePill={activePill} /> <ProjectNav onPillClick={handlePillClick} activePill={activePill} />
</div> </div>
{renderContent()} {renderContent()}
</div> </div>
); );

View File

@ -15,6 +15,8 @@ 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')
}; };
export default AuthRepository; export default AuthRepository;

View File

@ -18,6 +18,8 @@ export const RolesRepository = {
}; };
export const MasterRespository = { export const MasterRespository = {
getMasterMenus:()=>api.get("/api/AppMenu/get/master-list"),
getRoles: () => api.get("/api/roles"), getRoles: () => api.get("/api/roles"),
createRole: (data) => api.post("/api/roles", data), createRole: (data) => api.post("/api/roles", data),
updateRoles: (id, data) => api.put(`/api/roles/${id}`, data), updateRoles: (id, data) => api.put(`/api/roles/${id}`, data),

View File

@ -0,0 +1,20 @@
import { api } from "../utils/axiosClient";
export const TenantRepository = {
getTenantList: ( pageSize, pageNumber, filter,searchString) => {
const payloadJsonString = JSON.stringify(filter);
return api.get(`/api/Tenant/list?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}`);
},
getTenantDetails:(id)=>api.get(`/api/Tenant/details/${id}`),
getSubscriptionPlan: (freq) =>
api.get(`/api/Tenant/list/subscription-plan?frequency=${freq}`),
createTenant: (data) => api.post("/api/Tenant/create", data),
updateTenantDetails :(id,data)=> api.put(`/api/Tenant/edit/${id}`,data),
addSubscription: (data) => api.post("/api/Tenant/add-subscription", data),
upgradeSubscription :(data)=> api.put("/api/Tenant/update-subscription",data)
};

View File

@ -38,19 +38,24 @@ import LegalInfoCard from "../pages/TermsAndConditions/LegalInfoCard";
import ProtectedRoute from "./ProtectedRoute"; import ProtectedRoute from "./ProtectedRoute";
import Directory from "../pages/Directory/Directory"; import Directory from "../pages/Directory/Directory";
import LoginWithOtp from "../pages/authentication/LoginWithOtp"; import LoginWithOtp from "../pages/authentication/LoginWithOtp";
import TenantPage from "../pages/Tenant/TenantPage";
import CreateTenant from "../pages/Tenant/CreateTenant";
import ExpensePage from "../pages/Expense/ExpensePage"; import ExpensePage from "../pages/Expense/ExpensePage";
import TenantDetails from "../pages/Tenant/TenantDetails";
import SelfTenantDetails from "../pages/Tenant/SelfTenantDetails";
import SuperTenantDetails from "../pages/Tenant/SuperTenantDetails";
const router = createBrowserRouter( const router = createBrowserRouter(
[ [
{ {
element: <AuthLayout />, element: <AuthLayout />,
children: [ children: [
{path: "/auth/login", element: <LoginPage />}, { path: "/auth/login", element: <LoginPage /> },
{path: "/auth/login-otp", element: <LoginWithOtp />}, { path: "/auth/login-otp", element: <LoginWithOtp /> },
{ path: "/auth/reqest/demo", element: <RegisterPage /> }, { path: "/auth/reqest/demo", element: <RegisterPage /> },
{ path: "/auth/forgot-password", element: <ForgotPasswordPage /> }, { path: "/auth/forgot-password", element: <ForgotPasswordPage /> },
{ path: "/reset-password", element: <ResetPasswordPage /> }, { path: "/reset-password", element: <ResetPasswordPage /> },
{ path: "/legal-info", element: <LegalInfoCard /> }, { path: "/legal-info", element: <LegalInfoCard /> },
{ path: "/auth/changepassword", element: <ChangePasswordPage /> }, { path: "/auth/changepassword", element: <ChangePasswordPage /> },
], ],
}, },
@ -79,6 +84,10 @@ const router = createBrowserRouter(
{ path: "/gallary", element: <ImageGallary /> }, { path: "/gallary", element: <ImageGallary /> },
{ path: "/expenses", element: <ExpensePage /> }, { path: "/expenses", element: <ExpensePage /> },
{ path: "/masters", element: <MasterPage /> }, { path: "/masters", element: <MasterPage /> },
{ path: "/tenants", element: <TenantPage /> },
{ path: "/tenants/new-tenant", element: <CreateTenant /> },
{ path: "/tenant/:tenantId", element: <SuperTenantDetails /> },
{ path: "/tenant/self", element: <SelfTenantDetails /> },
{ 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

@ -3,7 +3,8 @@ import { createSlice } from "@reduxjs/toolkit";
const globalVariablesSlice = createSlice({ const globalVariablesSlice = createSlice({
name: "globalVariables", name: "globalVariables",
initialState: { initialState: {
loginUser:null loginUser:null,
currentTenant:null
}, },
reducers: { reducers: {
setGlobalVariable: (state, action) => { setGlobalVariable: (state, action) => {
@ -13,9 +14,12 @@ const globalVariablesSlice = createSlice({
setLoginUserPermmisions: ( state, action ) => setLoginUserPermmisions: ( state, action ) =>
{ {
state.loginUser = action.payload state.loginUser = action.payload
},
setCurrentTenant:(state,action)=>{
state.currentTenant = action.payload
} }
}, },
}); });
export const { setGlobalVariable,setLoginUserPermmisions } = globalVariablesSlice.actions; export const { setGlobalVariable,setLoginUserPermmisions,setCurrentTenant } = globalVariablesSlice.actions;
export default globalVariablesSlice.reducer; export default globalVariablesSlice.reducer;

View File

@ -5,6 +5,8 @@ export const OTP_EXPIRY_SECONDS = 600 // OTP time
export const MANAGE_MASTER = "588a8824-f924-4955-82d8-fc51956cf323"; export const MANAGE_MASTER = "588a8824-f924-4955-82d8-fc51956cf323";
export const VIEW_MASTER = "5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d"
export const MANAGE_PROJECT = "172fc9b6-755b-4f62-ab26-55c34a330614" export const MANAGE_PROJECT = "172fc9b6-755b-4f62-ab26-55c34a330614"
export const VIEW_PROJECTS = "6ea44136-987e-44ba-9e5d-1cf8f5837ebc" export const VIEW_PROJECTS = "6ea44136-987e-44ba-9e5d-1cf8f5837ebc"
@ -21,6 +23,8 @@ export const MANAGE_PROJECT_INFRA = "cf2825ad-453b-46aa-91d9-27c124d63373"
export const VIEW_PROJECT_INFRA = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4" export const VIEW_PROJECT_INFRA = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"
export const REGULARIZE_ATTENDANCE ="57802c4a-00aa-4a1f-a048-fd2f70dd44b6" export const REGULARIZE_ATTENDANCE ="57802c4a-00aa-4a1f-a048-fd2f70dd44b6"
export const TEAM_ATTENDANCE = "915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"
export const SELF_ATTENDANCE = "ccb0589f-712b-43de-92ed-5b6088e7dc4e"
export const ASSIGN_TO_PROJECT = "b94802ce-0689-4643-9e1d-11c86950c35b"; export const ASSIGN_TO_PROJECT = "b94802ce-0689-4643-9e1d-11c86950c35b";
@ -59,10 +63,45 @@ export const EXPENSE_MANAGE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"
export const EXPENSE_REJECTEDBY = ["d1ee5eec-24b6-4364-8673-a8f859c60729","965eda62-7907-4963-b4a1-657fb0b2724b"] export const EXPENSE_REJECTEDBY = ["d1ee5eec-24b6-4364-8673-a8f859c60729","965eda62-7907-4963-b4a1-657fb0b2724b"]
export const EXPENSE_DRAFT = "297e0d8f-f668-41b5-bfea-e03b354251c8" export const EXPENSE_DRAFT = "297e0d8f-f668-41b5-bfea-e03b354251c8"
export const SUPPER_TENANT = "d032cb1a-3f30-462c-bef0-7ace73a71c0b"
export const MANAGE_TENANTS = "00e20637-ce8d-4417-bec4-9b31b5e65092"
export const VIEW_TENANTS = "647145c6-2108-4c98-aab4-178602236e55"
export const ActiveTenant = "297e0d8f-f668-41b5-bfea-e03b354251c8"
// -------------------Application Role------------------------------ // -------------------Application Role------------------------------
// 1 - Expense Manage // 1 - Expense Manage
export const EXPENSE_MANAGEMENT = "a4e25142-449b-4334-a6e5-22f70e4732d7" export const EXPENSE_MANAGEMENT = "a4e25142-449b-4334-a6e5-22f70e4732d7"
export const TENANT_STATUS = [
{id:"62b05792-5115-4f99-8ff5-e8374859b191",name:"Active"},
{id:"c0b5def8-087e-4235-b3a4-8e2f0ed91b94",name:"In Active"},
{id:"35d7840a-164a-448b-95e6-efb2ec84a751",name:"Supspended"}
]
export const CONSTANT_TEXT = {
}
export const SUBSCRIPTION_PLAN_FREQUENCIES = {
0: "Monthly",
1:"Quarterly",
2:"Half-Yearly",
3:"Yearly"
}
export const reference = [
{ val: "google", name: "Google" },
{ val: "frineds", name: "Friends" },
{ val: "advertisement", name: "Advertisement" },
{ val: "root tenant", name: "Root Tenant" },
];
export const orgSize = [
{ val: "1-50", name: "1-50" },
{ val: "51-100", name: "51-100" },
{ val: "101-500", name: "101-500" },
{ val: "500+", name: "500+" },
];
export const BASE_URL = process.env.VITE_BASE_URL; export const BASE_URL = process.env.VITE_BASE_URL;
// export const BASE_URL = "https://api.marcoaiot.com"; // export const BASE_URL = "https://api.marcoaiot.com";

View File

@ -1,4 +1,5 @@
import moment from "moment"; import moment from "moment";
import { ActiveTenant } from "./constants";
export const getDateDifferenceInDays = (startDate, endDate) => { export const getDateDifferenceInDays = (startDate, endDate) => {
if (!startDate || !endDate) { if (!startDate || !endDate) {
@ -82,4 +83,8 @@ export const getCompletionPercentage = (completedWork, plannedWork)=> {
const clamped = Math.min(Math.max(percentage, 0), 100); const clamped = Math.min(Math.max(percentage, 0), 100);
return clamped.toFixed(2); return clamped.toFixed(2);
}
export const getTenantStatus =(statusId)=>{
return ActiveTenant === statusId ? " bg-label-success":"bg-label-secondary"
} }