Merge pull request 'Tenant_Manag : Feature #901 : Tenant Management' (#334) from Tenant_Manag into main
Reviewed-on: #334 Merged
This commit is contained in:
commit
9dddba4e30
@ -45,6 +45,7 @@
|
||||
|
||||
<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/spinkit/spinkit.css" />
|
||||
|
||||
<!-- Helpers -->
|
||||
<script src="/assets/vendor/js/helpers.js"></script>
|
||||
|
3
public/assets/vendor/css/core.css
vendored
3
public/assets/vendor/css/core.css
vendored
@ -18609,6 +18609,9 @@ li:not(:first-child) .dropdown-item,
|
||||
.min-vh-100 {
|
||||
min-height: 100vh !important;
|
||||
}
|
||||
.page-min-h{
|
||||
min-height: 70vh !important;
|
||||
}
|
||||
|
||||
.flex-fill {
|
||||
flex: 1 1 auto !important;
|
||||
|
837
public/assets/vendor/libs/spinkit/spinkit.css
vendored
Normal file
837
public/assets/vendor/libs/spinkit/spinkit.css
vendored
Normal 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;
|
||||
}
|
@ -16,6 +16,7 @@ import {useProfile} from "../../hooks/useProfile";
|
||||
import {refreshData, setProjectId} from "../../slices/localVariablesSlice";
|
||||
import InfraTable from "../Project/Infrastructure/InfraTable";
|
||||
import { useSelectedproject } from "../../slices/apiDataManager";
|
||||
import Loader from "../common/Loader";
|
||||
|
||||
|
||||
const InfraPlanning = () =>
|
||||
@ -51,7 +52,7 @@ const InfraPlanning = () =>
|
||||
{(ApprovedTaskRights || ReportTaskRights) ? (
|
||||
<div className="align-items-center">
|
||||
<div className="row ">
|
||||
{isLoading && ( <p>Loading...</p> )}
|
||||
{isLoading && (<Loader/> )}
|
||||
{( !isLoading && projectInfra?.length === 0 ) && ( <p>No Result Found</p> )}
|
||||
{(!isLoading && projectInfra?.length > 0) && (<InfraTable buildings={projectInfra} projectId={selectedProject}/>)}
|
||||
</div>
|
||||
|
@ -202,3 +202,4 @@ export const ReportTask = ({ report, closeModal }) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ReportTask;
|
@ -22,6 +22,7 @@ const EmpAttendance = ({ employee }) => {
|
||||
data = [],
|
||||
isLoading: loading,
|
||||
isFetching,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
} = useAttendanceByEmployee(employee, dateRange.startDate, dateRange.endDate);
|
||||
@ -145,7 +146,7 @@ const EmpAttendance = ({ employee }) => {
|
||||
</div>
|
||||
<div className="table-responsive text-nowrap">
|
||||
{!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>}
|
||||
{data && data.length > 0 && (
|
||||
<table className="table mb-0">
|
||||
|
@ -34,7 +34,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
isLoading,
|
||||
error: ExpenseErrorLoad,
|
||||
} = useExpense(expenseToEdit);
|
||||
console.log(data)
|
||||
const [ExpenseType, setExpenseType] = useState();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
|
36
src/components/Layout/MenuItemSkeleton.jsx
Normal file
36
src/components/Layout/MenuItemSkeleton.jsx
Normal 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>
|
||||
);
|
||||
};
|
@ -2,10 +2,12 @@ import React from "react";
|
||||
import { Link, NavLink, useLocation, useNavigate } from "react-router-dom";
|
||||
import menuData from "../../data/menuData.json";
|
||||
import { getCachedProfileData } from "../../slices/apiDataManager";
|
||||
import { useSidBarMenu } from "../../hooks/useProfile";
|
||||
import { MenuItemSkeleton } from "./MenuItemSkeleton";
|
||||
|
||||
const Sidebar = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data, isError, isLoading, isFetched, error } = useSidBarMenu();
|
||||
return (
|
||||
<aside
|
||||
id="layout-menu"
|
||||
@ -33,33 +35,49 @@ const Sidebar = () => {
|
||||
<div className="menu-inner-shadow"></div>
|
||||
|
||||
<ul className="menu-inner py-1">
|
||||
{menuData.map((section) => (
|
||||
<React.Fragment key={(Math.random() + 1).toString(36)}>
|
||||
{section.header && (
|
||||
{isError && (
|
||||
<div className="text-center text-small">{error.message}</div>
|
||||
)}
|
||||
{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">
|
||||
<span className="menu-header-text">{section.header}</span>
|
||||
</li>
|
||||
)}
|
||||
{section.items.map(MenuItem)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
)} */}
|
||||
{section.items.map((item) => (
|
||||
<MenuItem key={item.id || item.link} {...item} />
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ul>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuItem = (item) => {
|
||||
item.id = Math.random();
|
||||
const location = useLocation();
|
||||
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 =
|
||||
hasSubmenu &&
|
||||
item.submenu.some((subitem) => location.pathname === subitem.link);
|
||||
hasSubmenu && item.submenu.some((sub) => location.pathname === sub.link);
|
||||
|
||||
return (
|
||||
<li
|
||||
key={(Math.random() + 1).toString(36)}
|
||||
className={`menu-item ${isActive || isSubmenuActive ? "active" : ""} ${
|
||||
hasSubmenu && isSubmenuActive ? "open" : ""
|
||||
}`}
|
||||
@ -67,21 +85,24 @@ const MenuItem = (item) => {
|
||||
<NavLink
|
||||
aria-label={`Navigate to ${item.text} ${!item.available ? "Pro" : ""}`}
|
||||
to={item.link}
|
||||
className={`menu-link ${item.submenu ? "menu-toggle" : ""}`}
|
||||
key={(Math.random() + 1).toString(36)}
|
||||
target={item.link.includes("http") ? "_blank" : undefined}
|
||||
className={`menu-link ${hasSubmenu ? "menu-toggle" : ""}`}
|
||||
target={item.link?.includes("http") ? "_blank" : undefined}
|
||||
>
|
||||
<i className={`menu-icon tf-icons ${item.icon}`}></i>
|
||||
<div>{item.text}</div>{" "}
|
||||
<div>{item.name}</div>
|
||||
{item.available === false && (
|
||||
<div className="badge bg-label-primary fs-tiny rounded-pill ms-auto">
|
||||
Pro
|
||||
</div>
|
||||
)}
|
||||
</NavLink>
|
||||
{item.submenu && (
|
||||
<ul className="menu-sub" key={(Math.random() + 1).toString(36)}>
|
||||
{item.submenu.map(MenuItem)}
|
||||
|
||||
{/* Only render submenu if exists */}
|
||||
{hasSubmenu && (
|
||||
<ul className="menu-sub">
|
||||
{item.submenu.map((sub) => (
|
||||
<MenuItem key={sub.id || sub.link} {...sub} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
|
17
src/components/Tenant/Congratulation.jsx
Normal file
17
src/components/Tenant/Congratulation.jsx
Normal 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
|
109
src/components/Tenant/ContactInfro.jsx
Normal file
109
src/components/Tenant/ContactInfro.jsx
Normal 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;
|
188
src/components/Tenant/EditProfile.jsx
Normal file
188
src/components/Tenant/EditProfile.jsx
Normal 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;
|
85
src/components/Tenant/LogoUpload.jsx
Normal file
85
src/components/Tenant/LogoUpload.jsx
Normal 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>
|
||||
);
|
||||
};
|
17
src/components/Tenant/Organization.jsx
Normal file
17
src/components/Tenant/Organization.jsx
Normal 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
|
||||
|
||||
|
242
src/components/Tenant/OrganizationInfo.jsx
Normal file
242
src/components/Tenant/OrganizationInfo.jsx
Normal 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;
|
190
src/components/Tenant/Profile.jsx
Normal file
190
src/components/Tenant/Profile.jsx
Normal 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;
|
||||
|
45
src/components/Tenant/SegmentedControl.jsx
Normal file
45
src/components/Tenant/SegmentedControl.jsx
Normal 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;
|
252
src/components/Tenant/SubScription.jsx
Normal file
252
src/components/Tenant/SubScription.jsx
Normal 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;
|
219
src/components/Tenant/SubScriptionHistory.jsx
Normal file
219
src/components/Tenant/SubScriptionHistory.jsx
Normal 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;
|
97
src/components/Tenant/TenanatSkeleton.jsx
Normal file
97
src/components/Tenant/TenanatSkeleton.jsx
Normal 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>
|
||||
);
|
||||
};
|
116
src/components/Tenant/TenantFilterPanel.jsx
Normal file
116
src/components/Tenant/TenantFilterPanel.jsx
Normal 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;
|
212
src/components/Tenant/TenantForm.jsx
Normal file
212
src/components/Tenant/TenantForm.jsx
Normal 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;
|
155
src/components/Tenant/TenantSchema.js
Normal file
155
src/components/Tenant/TenantSchema.js
Normal 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(),
|
||||
});
|
194
src/components/Tenant/TenantsList.jsx
Normal file
194
src/components/Tenant/TenantsList.jsx
Normal 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;
|
@ -76,7 +76,8 @@ export const DateRangePicker1 = ({
|
||||
placeholder = "Select date range",
|
||||
className = "",
|
||||
allowText = false,
|
||||
resetSignal, // <- NEW prop
|
||||
resetSignal,
|
||||
defaultRange = true,
|
||||
...rest
|
||||
}) => {
|
||||
const inputRef = useRef(null);
|
||||
@ -124,10 +125,9 @@ export const DateRangePicker1 = ({
|
||||
...rest,
|
||||
});
|
||||
|
||||
// Apply default if empty
|
||||
const currentStart = getValues(startField);
|
||||
const currentEnd = getValues(endField);
|
||||
if (!currentStart && !currentEnd) {
|
||||
if (defaultRange && !currentStart && !currentEnd) {
|
||||
applyDefaultDates();
|
||||
} else if (currentStart && currentEnd) {
|
||||
instance.setDate([
|
||||
@ -139,12 +139,11 @@ export const DateRangePicker1 = ({
|
||||
return () => instance.destroy();
|
||||
}, []);
|
||||
|
||||
// Reapply default range on resetSignal change
|
||||
useEffect(() => {
|
||||
if (resetSignal !== undefined) {
|
||||
if (defaultRange && resetSignal !== undefined) {
|
||||
applyDefaultDates();
|
||||
}
|
||||
}, [resetSignal]);
|
||||
}, [resetSignal, defaultRange]);
|
||||
|
||||
const start = getValues(startField);
|
||||
const end = getValues(endField);
|
||||
@ -173,3 +172,4 @@ export const DateRangePicker1 = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -15,7 +15,7 @@ const IconButton = ({
|
||||
iconClass, // icon class string like 'bx bx-user'
|
||||
color = "primary",
|
||||
onClick,
|
||||
size = 20,
|
||||
size = 5,
|
||||
radius=null,
|
||||
style = {},
|
||||
...rest
|
||||
@ -31,7 +31,7 @@ const IconButton = ({
|
||||
style={{
|
||||
backgroundColor,
|
||||
color: iconColor,
|
||||
padding: "0.4rem",
|
||||
padding: "0.3rem",
|
||||
margin:'0rem 0.2rem',
|
||||
...style,
|
||||
}}
|
||||
|
12
src/components/common/Label.jsx
Normal file
12
src/components/common/Label.jsx
Normal 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;
|
@ -2,37 +2,20 @@ import React from "react";
|
||||
|
||||
const Loader = () => {
|
||||
return (
|
||||
<div className="demo-inline-spacing">
|
||||
<div className="spinner-grow text-primary" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
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 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>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loader;
|
||||
|
||||
|
@ -66,7 +66,6 @@
|
||||
"available": true,
|
||||
"link": "/expenses"
|
||||
},
|
||||
|
||||
{
|
||||
"text": "Image Gallary",
|
||||
"icon": "bx bx-images",
|
||||
@ -80,9 +79,9 @@
|
||||
"link": "",
|
||||
"submenu": [
|
||||
{
|
||||
"text": "Users",
|
||||
"text": "Tenant",
|
||||
"available": true,
|
||||
"link": "/employees/"
|
||||
"link": "/tenants"
|
||||
},
|
||||
{
|
||||
"text": "Masters",
|
||||
|
@ -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 = () =>
|
||||
{
|
||||
|
@ -99,3 +99,13 @@ export const useProfile = () => {
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export const useSidBarMenu = ()=>{
|
||||
const userLogged = useSelector((store)=>store.globalVariables.loginUser);
|
||||
return useQuery({
|
||||
queryKey:["AppMenu"],
|
||||
queryFn:async()=> await AuthRepository.appmenu(),
|
||||
enabled: !!userLogged
|
||||
})
|
||||
}
|
@ -175,6 +175,7 @@ export const useProjectInfra = (projectId) => {
|
||||
} = useQuery({
|
||||
queryKey: ["ProjectInfra", projectId],
|
||||
queryFn: async () => {
|
||||
if(!projectId) return null;
|
||||
const res = await ProjectRepository.getProjectInfraByproject(projectId);
|
||||
return res.data;
|
||||
},
|
||||
|
177
src/hooks/useTenant.js
Normal file
177
src/hooks/useTenant.js
Normal 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"
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
@ -1,31 +1,28 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useTaskList } from "../../hooks/useTasks";
|
||||
import { useProjectName, useProjects } from "../../hooks/useProjects";
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||
import { ReportTask } from "../../components/Activities/ReportTask";
|
||||
import ReportTaskComments from "../../components/Activities/ReportTaskComments";
|
||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||
import DateRangePicker from "../../components/common/DateRangePicker";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import moment from "moment";
|
||||
import FilterIcon from "../../components/common/FilterIcon";
|
||||
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 {formatNumber} from "../../utils/dateUtils";
|
||||
import { formatNumber, formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { APPROVE_TASK, ASSIGN_REPORT_TASK } from "../../utils/constants";
|
||||
import { useSelectedproject } from "../../slices/apiDataManager";
|
||||
import moment from "moment";
|
||||
import Loader from "../../components/common/Loader";
|
||||
|
||||
const DailyTask = () => {
|
||||
// const selectedProject = useSelector(
|
||||
// (store) => store.localVariables.projectId
|
||||
// );
|
||||
const dispatch = useDispatch();
|
||||
const selectedProject = useSelectedproject();
|
||||
const dispatch = useDispatch()
|
||||
const { projectNames, loading: projectLoading, fetchData } = useProjectName();
|
||||
|
||||
const { projectNames } = useProjectName();
|
||||
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
|
||||
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK);
|
||||
|
||||
const [filters, setFilters] = useState({
|
||||
selectedBuilding: "",
|
||||
@ -33,427 +30,201 @@ const DailyTask = () => {
|
||||
selectedActivities: [],
|
||||
});
|
||||
|
||||
|
||||
|
||||
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
|
||||
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK)
|
||||
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK)
|
||||
|
||||
const {
|
||||
TaskList,
|
||||
loading: task_loading,
|
||||
error: task_error,
|
||||
refetch,
|
||||
} = useTaskList(
|
||||
selectedProject || null,
|
||||
dateRange?.startDate || null,
|
||||
const { TaskList, loading: taskLoading } = useTaskList(
|
||||
selectedProject || null,
|
||||
dateRange?.startDate || null,
|
||||
dateRange?.endDate || null
|
||||
);
|
||||
);
|
||||
|
||||
// Ensure project is set
|
||||
useEffect(() => {
|
||||
if(selectedProject == null){
|
||||
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([]);
|
||||
if (!selectedProject && projectNames.length > 0) {
|
||||
dispatch(setProjectId(projectNames[0].id));
|
||||
}
|
||||
}, [
|
||||
TaskList,
|
||||
filters?.selectedBuilding,
|
||||
filters?.selectedFloors,
|
||||
filters?.selectedActivities,
|
||||
]);
|
||||
}, [selectedProject, projectNames, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const AssignmentDates = [
|
||||
...new Set(TaskLists.map((task) => task.assignmentDate.split("T")[0])),
|
||||
].sort((a, b) => new Date(b) - new Date(a));
|
||||
setDates(AssignmentDates);
|
||||
}, [TaskLists]);
|
||||
// Memoized filtering
|
||||
const filteredTasks = useMemo(() => {
|
||||
if (!TaskList) return [];
|
||||
return TaskList.filter((task) => {
|
||||
const { selectedBuilding, selectedFloors, selectedActivities } = filters;
|
||||
|
||||
const [selectedTask, selectTask] = useState(null);
|
||||
const [comments, setComment] = useState({ task: null, isActionAllow: false });
|
||||
if (selectedBuilding && task?.workItem?.workArea?.floor?.building?.name !== selectedBuilding) return 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);
|
||||
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"),
|
||||
});
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [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 = () =>
|
||||
{
|
||||
setIsModalOpen( false )
|
||||
// refetch();
|
||||
}
|
||||
// --- Modal State
|
||||
const [modal, setModal] = useState({ type: null, data: null });
|
||||
|
||||
const handleCloseAction = (IsSubTask) => {
|
||||
if (IsSubTask) {
|
||||
setIsSubTaskNeeded(true);
|
||||
setIsModalOpenComment(false);
|
||||
} else {
|
||||
// refetch();
|
||||
setIsModalOpenComment(false);
|
||||
}
|
||||
};
|
||||
const hanleCloseSubTask = () => {
|
||||
setIsSubTaskNeeded(false);
|
||||
setComment( null );
|
||||
// refetch();
|
||||
};
|
||||
const openModal = (type, data = null) => setModal({ type, data });
|
||||
const closeModal = () => setModal({ type: null, data: null });
|
||||
|
||||
// --- 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 (
|
||||
<>
|
||||
{isModalOpen && <GlobalModel isOpen={isModalOpen} size="md" closeModal={handlecloseModal} >
|
||||
<ReportTask
|
||||
report={selectedTask}
|
||||
closeModal={handlecloseModal}
|
||||
// refetch={refetch}
|
||||
/>
|
||||
</GlobalModel>}
|
||||
|
||||
{isModalOpenComment && (
|
||||
<GlobalModel
|
||||
isOpen={isModalOpenComment}
|
||||
size="lg"
|
||||
closeModal={() => setIsModalOpenComment(false)}
|
||||
>
|
||||
{/* --- Modals --- */}
|
||||
{modal.type === "report" && (
|
||||
<GlobalModel isOpen size="md" closeModal={closeModal}>
|
||||
<ReportTask report={modal.data} closeModal={closeModal} />
|
||||
</GlobalModel>
|
||||
)}
|
||||
{modal.type === "comments" && (
|
||||
<GlobalModel isOpen size="lg" closeModal={closeModal}>
|
||||
<ReportTaskComments
|
||||
commentsData={comments.task}
|
||||
actionAllow={comments.isActionAllow}
|
||||
handleCloseAction={handleCloseAction}
|
||||
closeModal={closeCommentModal}
|
||||
commentsData={modal.data.task}
|
||||
actionAllow={modal.data.isActionAllow}
|
||||
handleCloseAction={(isSubTask) => {
|
||||
if (isSubTask) openModal("subtask", modal.data.task);
|
||||
else closeModal();
|
||||
}}
|
||||
closeModal={closeModal}
|
||||
/>
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
{IsSubTaskNeeded && (
|
||||
<GlobalModel
|
||||
isOpen={IsSubTaskNeeded}
|
||||
size="lg"
|
||||
closeModal={hanleCloseSubTask}
|
||||
>
|
||||
<SubTask activity={comments.task} onClose={hanleCloseSubTask} />
|
||||
{modal.type === "subtask" && (
|
||||
<GlobalModel isOpen size="lg" closeModal={closeModal}>
|
||||
<SubTask activity={modal.data} onClose={closeModal} />
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
<div className="container-fluid">
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: "Home", link: "/dashboard" },
|
||||
{ label: "Daily Progress Report", link: null },
|
||||
]}
|
||||
></Breadcrumb>
|
||||
<div className="card card-action mb-6 ">
|
||||
<Breadcrumb data={[{ label: "Home", link: "/dashboard" }, { label: "Daily Progress Report" }]} />
|
||||
|
||||
<div className="card card-action mb-6">
|
||||
<div className="card-body p-1 p-sm-2">
|
||||
<div className="row d-flex justify-content-between align-items-center">
|
||||
<div className="col-md-12 d-flex align-items-center col-12 text-start mb-2 mb-md-0">
|
||||
<DateRangePicker
|
||||
onRangeChange={setDateRange}
|
||||
endDateMode="today"
|
||||
DateDifference="6"
|
||||
dateFormat="DD-MM-YYYY"
|
||||
/>
|
||||
<FilterIcon
|
||||
taskListData={TaskList}
|
||||
onApplyFilters={setFilters}
|
||||
currentSelectedBuilding={filters.selectedBuilding}
|
||||
currentSelectedFloors={filters.selectedFloors}
|
||||
currentSelectedActivities={filters.selectedActivities}
|
||||
/>
|
||||
</div>
|
||||
{!selectedProject && (<div className="text-center text-muted">Please Select Project</div>)}
|
||||
{/* --- Filters --- */}
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<DateRangePicker onRangeChange={setDateRange} endDateMode="today" DateDifference="6" dateFormat="DD-MM-YYYY" />
|
||||
<FilterIcon
|
||||
taskListData={TaskList}
|
||||
onApplyFilters={setFilters}
|
||||
currentSelectedBuilding={filters.selectedBuilding}
|
||||
currentSelectedFloors={filters.selectedFloors}
|
||||
currentSelectedActivities={filters.selectedActivities}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* --- Table --- */}
|
||||
<div className="table-responsive text-nowrap mt-3">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Activity</th>
|
||||
<th>Assigned </th>
|
||||
<th>Assigned</th>
|
||||
<th>Completed</th>
|
||||
<th>Assign On</th>
|
||||
<th>Team</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="table-border-bottom-0">
|
||||
{/* --- Spinner when tasks are loading --- */}
|
||||
{task_loading && (
|
||||
<tbody>
|
||||
{taskLoading && (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center">
|
||||
{" "}
|
||||
<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>
|
||||
<Loader/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!task_loading &&
|
||||
TaskLists.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center">
|
||||
<div className="mt-10 mb-10 pt-10 pb-10">
|
||||
{" "}
|
||||
<p>No Reports Found</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!task_loading &&
|
||||
TaskLists.length > 0 &&
|
||||
dates.map((date, i) => {
|
||||
const tasksForDate = TaskLists.filter((task) =>
|
||||
task.assignmentDate.includes(date)
|
||||
);
|
||||
if (tasksForDate.length === 0) return null;
|
||||
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
<tr className="table-row-header">
|
||||
<td colSpan={6} className="text-start">
|
||||
{" "}
|
||||
<strong>
|
||||
{moment(date).format("DD-MM-YYYY")}
|
||||
</strong>
|
||||
{!taskLoading && groupedTasks.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center">No Reports Found</td>
|
||||
</tr>
|
||||
)}
|
||||
{!taskLoading &&
|
||||
groupedTasks.map(({ date, tasks }) => (
|
||||
<React.Fragment key={date}>
|
||||
<tr className="table-row-header text-start">
|
||||
<td colSpan={6}><strong>{formatUTCToLocalTime(date)}</strong></td>
|
||||
</tr>
|
||||
{tasks.map((task, idx) => (
|
||||
<tr key={task.id || idx}>
|
||||
<td className="flex-wrap text-start">
|
||||
<div>{task.workItem.activityMaster?.activityName || "No Activity Name"}</div>
|
||||
<div className="text-sm">
|
||||
{task.workItem.workArea?.floor?.building?.name} › {task.workItem.workArea?.floor?.floorName} › {task.workItem.workArea?.areaName}
|
||||
</div>
|
||||
</td>
|
||||
<td>{formatNumber(task.plannedTask)} / {formatNumber(task.workItem.plannedWork - task.workItem.completedWork)}</td>
|
||||
<td>{task.completedTask}</td>
|
||||
<td>{formatUTCToLocalTime(task.assignmentDate)}</td>
|
||||
<td className="text-center">{renderTeamMembers(task, idx)}</td>
|
||||
<td className="text-center">
|
||||
<div className="d-flex justify-content-end gap-2">
|
||||
{ReportTaskRights && !task.reportedDate && (
|
||||
<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>
|
||||
</tr>
|
||||
{tasksForDate.map((task, index) => {
|
||||
const refIndex = index * 10 + i;
|
||||
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>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React,{useEffect} from "react";
|
||||
import React,{useEffect,useRef} from "react";
|
||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||
import InfraPlanning from "../../components/Activities/InfraPlanning";
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
@ -6,33 +6,34 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||
import { useSelectedproject } from "../../slices/apiDataManager";
|
||||
|
||||
|
||||
const TaskPlannng = () => {
|
||||
// const selectedProject = useSelector(
|
||||
// (store) => store.localVariables.projectId
|
||||
// );
|
||||
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(() => {
|
||||
if(selectedProject == null){
|
||||
dispatch(setProjectId(projectNames[0]?.id));
|
||||
}
|
||||
},[])
|
||||
if (!initialized.current && projectNames.length > 0 && !selectedProject?.id) {
|
||||
dispatch(setProjectId(projectNames[0].id));
|
||||
initialized.current = true;
|
||||
}
|
||||
}, [projectNames, selectedProject, dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container-fluid">
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: "Home", link: "/dashboard" },
|
||||
{ label: "Daily Task Planning" }
|
||||
]}
|
||||
></Breadcrumb>
|
||||
<InfraPlanning/>
|
||||
</div>
|
||||
</>
|
||||
<div className="container-fluid">
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: "Home", link: "/dashboard" },
|
||||
{ label: "Daily Task Planning" },
|
||||
]}
|
||||
/>
|
||||
{selectedProject ? (
|
||||
<InfraPlanning />
|
||||
) : (
|
||||
<div className="text-center">Please Select Project</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -80,21 +80,23 @@ const ExpensePage = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setShowTrigger(true);
|
||||
setOffcanvasContent(
|
||||
"Expense Filters",
|
||||
<ExpenseFilterPanel
|
||||
onApply={setFilter}
|
||||
handleGroupBy={setGroupBy}
|
||||
clearFilter={clearFilter}
|
||||
/>
|
||||
);
|
||||
if (IsViewAll || IsViewSelf || IsCreatedAble) {
|
||||
setShowTrigger(true);
|
||||
setOffcanvasContent(
|
||||
"Expense Filters",
|
||||
<ExpenseFilterPanel
|
||||
onApply={setFilter}
|
||||
handleGroupBy={setGroupBy}
|
||||
clearFilter={clearFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
setShowTrigger(false);
|
||||
setOffcanvasContent("", null);
|
||||
};
|
||||
}, []);
|
||||
}, [IsViewAll, IsViewSelf, IsCreatedAble]);
|
||||
|
||||
const contextValue = {
|
||||
setViewExpense,
|
||||
@ -105,16 +107,17 @@ const ExpensePage = () => {
|
||||
return (
|
||||
<ExpenseContext.Provider value={contextValue}>
|
||||
<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-body py-2 px-3">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-6 ">
|
||||
<div className="d-flex align-items-center">
|
||||
|
||||
<input
|
||||
type="search"
|
||||
className="form-control form-control-sm w-auto"
|
||||
@ -132,7 +135,12 @@ const ExpensePage = () => {
|
||||
type="button"
|
||||
className="p-1 me-1 m-sm-0 bg-primary rounded-circle"
|
||||
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>
|
||||
</button>
|
||||
@ -142,12 +150,18 @@ const ExpensePage = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ExpenseList filters={filters} groupBy={groupBy} searchText={searchText} />
|
||||
<ExpenseList
|
||||
filters={filters}
|
||||
groupBy={groupBy}
|
||||
searchText={searchText}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="card text-center py-1">
|
||||
<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>
|
||||
)}
|
||||
|
||||
@ -156,12 +170,16 @@ const ExpensePage = () => {
|
||||
<GlobalModel
|
||||
isOpen
|
||||
size="lg"
|
||||
closeModal={() => setManageExpenseModal({ IsOpen: null, expenseId: null })}
|
||||
closeModal={() =>
|
||||
setManageExpenseModal({ IsOpen: null, expenseId: null })
|
||||
}
|
||||
>
|
||||
<ManageExpense
|
||||
key={ManageExpenseModal.expenseId ?? "new"}
|
||||
expenseToEdit={ManageExpenseModal.expenseId}
|
||||
closeModal={() => setManageExpenseModal({ IsOpen: null, expenseId: null })}
|
||||
closeModal={() =>
|
||||
setManageExpenseModal({ IsOpen: null, expenseId: null })
|
||||
}
|
||||
/>
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
20
src/pages/Tenant/CreateTenant.jsx
Normal file
20
src/pages/Tenant/CreateTenant.jsx
Normal 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
|
36
src/pages/Tenant/SelfTenantDetails.jsx
Normal file
36
src/pages/Tenant/SelfTenantDetails.jsx
Normal 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;
|
||||
|
8
src/pages/Tenant/SuperTenantDetails.jsx
Normal file
8
src/pages/Tenant/SuperTenantDetails.jsx
Normal file
@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import TenantDetails from "./TenantDetails";
|
||||
|
||||
const SuperTenantDetails = () => {
|
||||
return <TenantDetails wrapInContainer showBreadcrumb iTSelf={false} />;
|
||||
};
|
||||
|
||||
export default SuperTenantDetails;
|
182
src/pages/Tenant/TenantDetails.jsx
Normal file
182
src/pages/Tenant/TenantDetails.jsx
Normal 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;
|
189
src/pages/Tenant/TenantPage.jsx
Normal file
189
src/pages/Tenant/TenantPage.jsx
Normal 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;
|
@ -4,7 +4,7 @@ import MasterModal from "../../components/master/MasterModal";
|
||||
import { mastersList } from "../../data/masters";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { changeMaster } from "../../slices/localVariablesSlice";
|
||||
import useMaster from "../../hooks/masterHook/useMaster"
|
||||
import useMaster, { useMasterMenu } from "../../hooks/masterHook/useMaster"
|
||||
import MasterTable from "./MasterTable";
|
||||
import { getCachedData } from "../../slices/apiDataManager";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
@ -13,6 +13,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
|
||||
const MasterPage = () => {
|
||||
const {data,isLoading,isError,error:menuError} = useMasterMenu()
|
||||
const [modalConfig, setModalConfig] = useState({ modalType: "", item: null, masterType: null });
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filteredResults, setFilteredResults] = useState([]);
|
||||
@ -23,7 +24,7 @@ const MasterPage = () => {
|
||||
const selectedMaster = useSelector((store) => store.localVariables.selectedMaster);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: masterData = [], loading, error, RecallApi } = useMaster();
|
||||
const { data: masterData = [], loading, error, RecallApi,isError:isMasterError } = useMaster();
|
||||
|
||||
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 (
|
||||
<>
|
||||
{isCreateModalOpen && (
|
||||
@ -121,8 +125,8 @@ const MasterPage = () => {
|
||||
className="form-select form-select-sm"
|
||||
value={selectedMaster}
|
||||
>
|
||||
|
||||
{mastersList.map((item) => (
|
||||
{isLoading && (<option value={null}>Loading...</option>)}
|
||||
{(!isLoading && data) && data?.map((item) => (
|
||||
|
||||
<option key={item.id} value={item.name}>{item.name}</option>
|
||||
))}
|
||||
@ -154,7 +158,6 @@ const MasterPage = () => {
|
||||
|
||||
<button
|
||||
className={`btn btn-sm add-new btn-primary `}
|
||||
// ${hasUserPermission('660131a4-788c-4739-a082-cbbf7879cbf2') ? "":"d-none"}
|
||||
tabIndex="0"
|
||||
aria-controls="DataTables_Table_0"
|
||||
type="button"
|
||||
@ -162,7 +165,6 @@ const MasterPage = () => {
|
||||
data-bs-target="#master-modal"
|
||||
onClick={() => {
|
||||
handleModalData(selectedMaster, "null", selectedMaster)
|
||||
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
|
@ -137,7 +137,6 @@ const ProjectDetails = () => {
|
||||
<div className="row">
|
||||
<ProjectNav onPillClick={handlePillClick} activePill={activePill} />
|
||||
</div>
|
||||
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
|
@ -15,6 +15,8 @@ const AuthRepository = {
|
||||
logout: (data) => api.post("/api/auth/logout", data),
|
||||
profile: () => api.get("/api/user/profile"),
|
||||
changepassword: (data) => api.post("/api/auth/change-password", data),
|
||||
appmenu:()=>api.get('/api/appmenu/get/menu')
|
||||
|
||||
};
|
||||
|
||||
export default AuthRepository;
|
||||
|
@ -18,6 +18,8 @@ export const RolesRepository = {
|
||||
};
|
||||
|
||||
export const MasterRespository = {
|
||||
getMasterMenus:()=>api.get("/api/AppMenu/get/master-list"),
|
||||
|
||||
getRoles: () => api.get("/api/roles"),
|
||||
createRole: (data) => api.post("/api/roles", data),
|
||||
updateRoles: (id, data) => api.put(`/api/roles/${id}`, data),
|
||||
|
20
src/repositories/TenantRepository.jsx
Normal file
20
src/repositories/TenantRepository.jsx
Normal 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)
|
||||
};
|
@ -38,15 +38,20 @@ import LegalInfoCard from "../pages/TermsAndConditions/LegalInfoCard";
|
||||
import ProtectedRoute from "./ProtectedRoute";
|
||||
import Directory from "../pages/Directory/Directory";
|
||||
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 TenantDetails from "../pages/Tenant/TenantDetails";
|
||||
import SelfTenantDetails from "../pages/Tenant/SelfTenantDetails";
|
||||
import SuperTenantDetails from "../pages/Tenant/SuperTenantDetails";
|
||||
|
||||
const router = createBrowserRouter(
|
||||
[
|
||||
{
|
||||
element: <AuthLayout />,
|
||||
children: [
|
||||
{path: "/auth/login", element: <LoginPage />},
|
||||
{path: "/auth/login-otp", element: <LoginWithOtp />},
|
||||
{ path: "/auth/login", element: <LoginPage /> },
|
||||
{ path: "/auth/login-otp", element: <LoginWithOtp /> },
|
||||
{ path: "/auth/reqest/demo", element: <RegisterPage /> },
|
||||
{ path: "/auth/forgot-password", element: <ForgotPasswordPage /> },
|
||||
{ path: "/reset-password", element: <ResetPasswordPage /> },
|
||||
@ -79,6 +84,10 @@ const router = createBrowserRouter(
|
||||
{ path: "/gallary", element: <ImageGallary /> },
|
||||
{ path: "/expenses", element: <ExpensePage /> },
|
||||
{ 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/docs", element: <Documentation /> },
|
||||
{ path: "/help/connect", element: <Connect /> },
|
||||
|
@ -3,7 +3,8 @@ import { createSlice } from "@reduxjs/toolkit";
|
||||
const globalVariablesSlice = createSlice({
|
||||
name: "globalVariables",
|
||||
initialState: {
|
||||
loginUser:null
|
||||
loginUser:null,
|
||||
currentTenant:null
|
||||
},
|
||||
reducers: {
|
||||
setGlobalVariable: (state, action) => {
|
||||
@ -13,9 +14,12 @@ const globalVariablesSlice = createSlice({
|
||||
setLoginUserPermmisions: ( state, action ) =>
|
||||
{
|
||||
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;
|
||||
|
@ -5,6 +5,8 @@ export const OTP_EXPIRY_SECONDS = 600 // OTP time
|
||||
|
||||
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 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 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";
|
||||
@ -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_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------------------------------
|
||||
|
||||
// 1 - Expense Manage
|
||||
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 = "https://api.marcoaiot.com";
|
||||
|
@ -1,4 +1,5 @@
|
||||
import moment from "moment";
|
||||
import { ActiveTenant } from "./constants";
|
||||
|
||||
export const getDateDifferenceInDays = (startDate, endDate) => {
|
||||
if (!startDate || !endDate) {
|
||||
@ -83,3 +84,7 @@ export const getCompletionPercentage = (completedWork, plannedWork)=> {
|
||||
|
||||
return clamped.toFixed(2);
|
||||
}
|
||||
|
||||
export const getTenantStatus =(statusId)=>{
|
||||
return ActiveTenant === statusId ? " bg-label-success":"bg-label-secondary"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user