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/animate-css/animate.css" />
|
||||||
<link rel="stylesheet" href="/assets/vendor/libs/sweetalert2/sweetalert2.css" />
|
<link rel="stylesheet" href="/assets/vendor/libs/sweetalert2/sweetalert2.css" />
|
||||||
|
<link rel="stylesheet" href="/assets/vendor/libs/spinkit/spinkit.css" />
|
||||||
|
|
||||||
<!-- Helpers -->
|
<!-- Helpers -->
|
||||||
<script src="/assets/vendor/js/helpers.js"></script>
|
<script src="/assets/vendor/js/helpers.js"></script>
|
||||||
|
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-vh-100 {
|
||||||
min-height: 100vh !important;
|
min-height: 100vh !important;
|
||||||
}
|
}
|
||||||
|
.page-min-h{
|
||||||
|
min-height: 70vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-fill {
|
.flex-fill {
|
||||||
flex: 1 1 auto !important;
|
flex: 1 1 auto !important;
|
||||||
|
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 {refreshData, setProjectId} from "../../slices/localVariablesSlice";
|
||||||
import InfraTable from "../Project/Infrastructure/InfraTable";
|
import InfraTable from "../Project/Infrastructure/InfraTable";
|
||||||
import { useSelectedproject } from "../../slices/apiDataManager";
|
import { useSelectedproject } from "../../slices/apiDataManager";
|
||||||
|
import Loader from "../common/Loader";
|
||||||
|
|
||||||
|
|
||||||
const InfraPlanning = () =>
|
const InfraPlanning = () =>
|
||||||
@ -51,7 +52,7 @@ const InfraPlanning = () =>
|
|||||||
{(ApprovedTaskRights || ReportTaskRights) ? (
|
{(ApprovedTaskRights || ReportTaskRights) ? (
|
||||||
<div className="align-items-center">
|
<div className="align-items-center">
|
||||||
<div className="row ">
|
<div className="row ">
|
||||||
{isLoading && ( <p>Loading...</p> )}
|
{isLoading && (<Loader/> )}
|
||||||
{( !isLoading && projectInfra?.length === 0 ) && ( <p>No Result Found</p> )}
|
{( !isLoading && projectInfra?.length === 0 ) && ( <p>No Result Found</p> )}
|
||||||
{(!isLoading && projectInfra?.length > 0) && (<InfraTable buildings={projectInfra} projectId={selectedProject}/>)}
|
{(!isLoading && projectInfra?.length > 0) && (<InfraTable buildings={projectInfra} projectId={selectedProject}/>)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -202,3 +202,4 @@ export const ReportTask = ({ report, closeModal }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
export default ReportTask;
|
@ -22,6 +22,7 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
data = [],
|
data = [],
|
||||||
isLoading: loading,
|
isLoading: loading,
|
||||||
isFetching,
|
isFetching,
|
||||||
|
isError,
|
||||||
error,
|
error,
|
||||||
refetch,
|
refetch,
|
||||||
} = useAttendanceByEmployee(employee, dateRange.startDate, dateRange.endDate);
|
} = useAttendanceByEmployee(employee, dateRange.startDate, dateRange.endDate);
|
||||||
@ -145,7 +146,7 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="table-responsive text-nowrap">
|
<div className="table-responsive text-nowrap">
|
||||||
{!loading && data.length === 0 && <span>No employee logs</span>}
|
{!loading && data.length === 0 && <span>No employee logs</span>}
|
||||||
{error && <div className="text-center">{error}</div>}
|
{isError && <div className="text-center">{error.message}</div>}
|
||||||
{loading && !data && <div className="text-center">Loading...</div>}
|
{loading && !data && <div className="text-center">Loading...</div>}
|
||||||
{data && data.length > 0 && (
|
{data && data.length > 0 && (
|
||||||
<table className="table mb-0">
|
<table className="table mb-0">
|
||||||
|
@ -34,7 +34,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
error: ExpenseErrorLoad,
|
error: ExpenseErrorLoad,
|
||||||
} = useExpense(expenseToEdit);
|
} = useExpense(expenseToEdit);
|
||||||
console.log(data)
|
|
||||||
const [ExpenseType, setExpenseType] = useState();
|
const [ExpenseType, setExpenseType] = useState();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const {
|
const {
|
||||||
|
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 { Link, NavLink, useLocation, useNavigate } from "react-router-dom";
|
||||||
import menuData from "../../data/menuData.json";
|
import menuData from "../../data/menuData.json";
|
||||||
import { getCachedProfileData } from "../../slices/apiDataManager";
|
import { getCachedProfileData } from "../../slices/apiDataManager";
|
||||||
|
import { useSidBarMenu } from "../../hooks/useProfile";
|
||||||
|
import { MenuItemSkeleton } from "./MenuItemSkeleton";
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { data, isError, isLoading, isFetched, error } = useSidBarMenu();
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
id="layout-menu"
|
id="layout-menu"
|
||||||
@ -33,33 +35,49 @@ const Sidebar = () => {
|
|||||||
<div className="menu-inner-shadow"></div>
|
<div className="menu-inner-shadow"></div>
|
||||||
|
|
||||||
<ul className="menu-inner py-1">
|
<ul className="menu-inner py-1">
|
||||||
{menuData.map((section) => (
|
{isError && (
|
||||||
<React.Fragment key={(Math.random() + 1).toString(36)}>
|
<div className="text-center text-small">{error.message}</div>
|
||||||
{section.header && (
|
)}
|
||||||
|
{isLoading && (
|
||||||
|
<>
|
||||||
|
{[...Array(7)].map((_, idx) => (
|
||||||
|
<MenuItemSkeleton
|
||||||
|
key={idx}
|
||||||
|
hasSubmenu={idx % 2 === 1}
|
||||||
|
submenuCount={Math.floor(Math.random() * 3) + 2}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{data &&
|
||||||
|
data?.data.map((section) => (
|
||||||
|
<React.Fragment
|
||||||
|
key={section.id || section.header || section.items[0]?.id}
|
||||||
|
>
|
||||||
|
{/* {section.header && (
|
||||||
<li className="menu-header small text-uppercase">
|
<li className="menu-header small text-uppercase">
|
||||||
<span className="menu-header-text">{section.header}</span>
|
<span className="menu-header-text">{section.header}</span>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)} */}
|
||||||
{section.items.map(MenuItem)}
|
{section.items.map((item) => (
|
||||||
</React.Fragment>
|
<MenuItem key={item.id || item.link} {...item} />
|
||||||
))}
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MenuItem = (item) => {
|
const MenuItem = (item) => {
|
||||||
item.id = Math.random();
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isActive = location.pathname === item.link;
|
const isActive = location.pathname === item.link;
|
||||||
const hasSubmenu = item.submenu && item.submenu.length > 0;
|
const hasSubmenu = Array.isArray(item.submenu) && item.submenu.length > 0;
|
||||||
const isSubmenuActive =
|
const isSubmenuActive =
|
||||||
hasSubmenu &&
|
hasSubmenu && item.submenu.some((sub) => location.pathname === sub.link);
|
||||||
item.submenu.some((subitem) => location.pathname === subitem.link);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={(Math.random() + 1).toString(36)}
|
|
||||||
className={`menu-item ${isActive || isSubmenuActive ? "active" : ""} ${
|
className={`menu-item ${isActive || isSubmenuActive ? "active" : ""} ${
|
||||||
hasSubmenu && isSubmenuActive ? "open" : ""
|
hasSubmenu && isSubmenuActive ? "open" : ""
|
||||||
}`}
|
}`}
|
||||||
@ -67,21 +85,24 @@ const MenuItem = (item) => {
|
|||||||
<NavLink
|
<NavLink
|
||||||
aria-label={`Navigate to ${item.text} ${!item.available ? "Pro" : ""}`}
|
aria-label={`Navigate to ${item.text} ${!item.available ? "Pro" : ""}`}
|
||||||
to={item.link}
|
to={item.link}
|
||||||
className={`menu-link ${item.submenu ? "menu-toggle" : ""}`}
|
className={`menu-link ${hasSubmenu ? "menu-toggle" : ""}`}
|
||||||
key={(Math.random() + 1).toString(36)}
|
target={item.link?.includes("http") ? "_blank" : undefined}
|
||||||
target={item.link.includes("http") ? "_blank" : undefined}
|
|
||||||
>
|
>
|
||||||
<i className={`menu-icon tf-icons ${item.icon}`}></i>
|
<i className={`menu-icon tf-icons ${item.icon}`}></i>
|
||||||
<div>{item.text}</div>{" "}
|
<div>{item.name}</div>
|
||||||
{item.available === false && (
|
{item.available === false && (
|
||||||
<div className="badge bg-label-primary fs-tiny rounded-pill ms-auto">
|
<div className="badge bg-label-primary fs-tiny rounded-pill ms-auto">
|
||||||
Pro
|
Pro
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{item.submenu && (
|
|
||||||
<ul className="menu-sub" key={(Math.random() + 1).toString(36)}>
|
{/* Only render submenu if exists */}
|
||||||
{item.submenu.map(MenuItem)}
|
{hasSubmenu && (
|
||||||
|
<ul className="menu-sub">
|
||||||
|
{item.submenu.map((sub) => (
|
||||||
|
<MenuItem key={sub.id || sub.link} {...sub} />
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
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",
|
placeholder = "Select date range",
|
||||||
className = "",
|
className = "",
|
||||||
allowText = false,
|
allowText = false,
|
||||||
resetSignal, // <- NEW prop
|
resetSignal,
|
||||||
|
defaultRange = true,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
@ -124,10 +125,9 @@ export const DateRangePicker1 = ({
|
|||||||
...rest,
|
...rest,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply default if empty
|
|
||||||
const currentStart = getValues(startField);
|
const currentStart = getValues(startField);
|
||||||
const currentEnd = getValues(endField);
|
const currentEnd = getValues(endField);
|
||||||
if (!currentStart && !currentEnd) {
|
if (defaultRange && !currentStart && !currentEnd) {
|
||||||
applyDefaultDates();
|
applyDefaultDates();
|
||||||
} else if (currentStart && currentEnd) {
|
} else if (currentStart && currentEnd) {
|
||||||
instance.setDate([
|
instance.setDate([
|
||||||
@ -139,12 +139,11 @@ export const DateRangePicker1 = ({
|
|||||||
return () => instance.destroy();
|
return () => instance.destroy();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Reapply default range on resetSignal change
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (resetSignal !== undefined) {
|
if (defaultRange && resetSignal !== undefined) {
|
||||||
applyDefaultDates();
|
applyDefaultDates();
|
||||||
}
|
}
|
||||||
}, [resetSignal]);
|
}, [resetSignal, defaultRange]);
|
||||||
|
|
||||||
const start = getValues(startField);
|
const start = getValues(startField);
|
||||||
const end = getValues(endField);
|
const end = getValues(endField);
|
||||||
@ -173,3 +172,4 @@ export const DateRangePicker1 = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ const IconButton = ({
|
|||||||
iconClass, // icon class string like 'bx bx-user'
|
iconClass, // icon class string like 'bx bx-user'
|
||||||
color = "primary",
|
color = "primary",
|
||||||
onClick,
|
onClick,
|
||||||
size = 20,
|
size = 5,
|
||||||
radius=null,
|
radius=null,
|
||||||
style = {},
|
style = {},
|
||||||
...rest
|
...rest
|
||||||
@ -31,7 +31,7 @@ const IconButton = ({
|
|||||||
style={{
|
style={{
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
color: iconColor,
|
color: iconColor,
|
||||||
padding: "0.4rem",
|
padding: "0.3rem",
|
||||||
margin:'0rem 0.2rem',
|
margin:'0rem 0.2rem',
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
|
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 = () => {
|
const Loader = () => {
|
||||||
return (
|
return (
|
||||||
<div className="demo-inline-spacing">
|
<div
|
||||||
<div className="spinner-grow text-primary" role="status">
|
className="d-flex justify-content-center align-items-center"
|
||||||
<span className="visually-hidden">Loading...</span>
|
style={{ height: "50vh" }}
|
||||||
|
>
|
||||||
|
<div className="sk-wave">
|
||||||
|
<div className="sk-wave-rect"></div>
|
||||||
|
<div className="sk-wave-rect"></div>
|
||||||
|
<div className="sk-wave-rect"></div>
|
||||||
|
<div className="sk-wave-rect"></div>
|
||||||
|
<div className="sk-wave-rect"></div>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="spinner-grow" role="status">
|
|
||||||
<span className="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="spinner-grow text-secondary" role="status">
|
|
||||||
<span className="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
<div className="spinner-grow text-success" role="status">
|
|
||||||
<span className="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
<div className="spinner-grow text-danger" role="status">
|
|
||||||
<span className="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
<div className="spinner-grow text-warning" role="status">
|
|
||||||
<span className="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
<div className="spinner-grow text-info" role="status">
|
|
||||||
<span className="visually-hidden">Loading...</span>
|
|
||||||
</div> */}
|
|
||||||
<div className="spinner-grow text-light" role="status">
|
|
||||||
<span className="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
{/* <div className="spinner-grow text-dark" role="status">
|
|
||||||
<span className="visually-hidden">Loading...</span>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Loader;
|
export default Loader;
|
||||||
|
|
||||||
|
@ -66,7 +66,6 @@
|
|||||||
"available": true,
|
"available": true,
|
||||||
"link": "/expenses"
|
"link": "/expenses"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"text": "Image Gallary",
|
"text": "Image Gallary",
|
||||||
"icon": "bx bx-images",
|
"icon": "bx bx-images",
|
||||||
@ -80,9 +79,9 @@
|
|||||||
"link": "",
|
"link": "",
|
||||||
"submenu": [
|
"submenu": [
|
||||||
{
|
{
|
||||||
"text": "Users",
|
"text": "Tenant",
|
||||||
"available": true,
|
"available": true,
|
||||||
"link": "/employees/"
|
"link": "/tenants"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "Masters",
|
"text": "Masters",
|
||||||
|
@ -11,7 +11,15 @@ import showToast from "../../services/toastService";
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const useMasterMenu = ()=>{
|
||||||
|
return useQuery({
|
||||||
|
queryKey:["MasterMenu"],
|
||||||
|
queryFn:async()=> {
|
||||||
|
const resp = await MasterRespository.getMasterMenus();
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const useActivitiesMaster = () =>
|
export const useActivitiesMaster = () =>
|
||||||
{
|
{
|
||||||
|
@ -99,3 +99,13 @@ export const useProfile = () => {
|
|||||||
refetch,
|
refetch,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useSidBarMenu = ()=>{
|
||||||
|
const userLogged = useSelector((store)=>store.globalVariables.loginUser);
|
||||||
|
return useQuery({
|
||||||
|
queryKey:["AppMenu"],
|
||||||
|
queryFn:async()=> await AuthRepository.appmenu(),
|
||||||
|
enabled: !!userLogged
|
||||||
|
})
|
||||||
|
}
|
@ -175,6 +175,7 @@ export const useProjectInfra = (projectId) => {
|
|||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["ProjectInfra", projectId],
|
queryKey: ["ProjectInfra", projectId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
if(!projectId) return null;
|
||||||
const res = await ProjectRepository.getProjectInfraByproject(projectId);
|
const res = await ProjectRepository.getProjectInfraByproject(projectId);
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
|
177
src/hooks/useTenant.js
Normal file
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 React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
|
||||||
import { useTaskList } from "../../hooks/useTasks";
|
import { useTaskList } from "../../hooks/useTasks";
|
||||||
import { useProjectName, useProjects } from "../../hooks/useProjects";
|
import { useProjectName } from "../../hooks/useProjects";
|
||||||
import { setProjectId } from "../../slices/localVariablesSlice";
|
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||||
import { ReportTask } from "../../components/Activities/ReportTask";
|
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||||
import ReportTaskComments from "../../components/Activities/ReportTaskComments";
|
|
||||||
import DateRangePicker from "../../components/common/DateRangePicker";
|
import DateRangePicker from "../../components/common/DateRangePicker";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import FilterIcon from "../../components/common/FilterIcon";
|
||||||
import moment from "moment";
|
|
||||||
import FilterIcon from "../../components/common/FilterIcon";
|
|
||||||
import GlobalModel from "../../components/common/GlobalModel";
|
import GlobalModel from "../../components/common/GlobalModel";
|
||||||
import AssignTask from "../../components/Project/AssignTask";
|
import ReportTask from "../../components/Activities/ReportTask";
|
||||||
|
import ReportTaskComments from "../../components/Activities/ReportTaskComments";
|
||||||
import SubTask from "../../components/Activities/SubTask";
|
import SubTask from "../../components/Activities/SubTask";
|
||||||
import {formatNumber} from "../../utils/dateUtils";
|
import { formatNumber, formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import { APPROVE_TASK, ASSIGN_REPORT_TASK } from "../../utils/constants";
|
import { APPROVE_TASK, ASSIGN_REPORT_TASK } from "../../utils/constants";
|
||||||
import { useSelectedproject } from "../../slices/apiDataManager";
|
import { useSelectedproject } from "../../slices/apiDataManager";
|
||||||
|
import moment from "moment";
|
||||||
|
import Loader from "../../components/common/Loader";
|
||||||
|
|
||||||
const DailyTask = () => {
|
const DailyTask = () => {
|
||||||
// const selectedProject = useSelector(
|
const dispatch = useDispatch();
|
||||||
// (store) => store.localVariables.projectId
|
|
||||||
// );
|
|
||||||
const selectedProject = useSelectedproject();
|
const selectedProject = useSelectedproject();
|
||||||
const dispatch = useDispatch()
|
const { projectNames } = useProjectName();
|
||||||
const { projectNames, loading: projectLoading, fetchData } = useProjectName();
|
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
|
||||||
|
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK);
|
||||||
|
|
||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
selectedBuilding: "",
|
selectedBuilding: "",
|
||||||
@ -33,427 +30,201 @@ const DailyTask = () => {
|
|||||||
selectedActivities: [],
|
selectedActivities: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
|
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
|
||||||
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK)
|
|
||||||
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK)
|
|
||||||
|
|
||||||
const {
|
const { TaskList, loading: taskLoading } = useTaskList(
|
||||||
TaskList,
|
selectedProject || null,
|
||||||
loading: task_loading,
|
dateRange?.startDate || null,
|
||||||
error: task_error,
|
|
||||||
refetch,
|
|
||||||
} = useTaskList(
|
|
||||||
selectedProject || null,
|
|
||||||
dateRange?.startDate || null,
|
|
||||||
dateRange?.endDate || null
|
dateRange?.endDate || null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Ensure project is set
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(selectedProject == null){
|
if (!selectedProject && projectNames.length > 0) {
|
||||||
dispatch(setProjectId(projectNames[0]?.id));
|
dispatch(setProjectId(projectNames[0].id));
|
||||||
}
|
|
||||||
},[])
|
|
||||||
const [TaskLists, setTaskLists] = useState([]);
|
|
||||||
const [dates, setDates] = useState([]);
|
|
||||||
const popoverRefs = useRef([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (TaskList) {
|
|
||||||
let filteredTasks = TaskList;
|
|
||||||
|
|
||||||
if (filters.selectedBuilding) {
|
|
||||||
filteredTasks = filteredTasks.filter(
|
|
||||||
(task) =>
|
|
||||||
task?.workItem?.workArea?.floor?.building?.name ===
|
|
||||||
filters.selectedBuilding
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.selectedFloors.length > 0) {
|
|
||||||
filteredTasks = filteredTasks?.filter((task) =>
|
|
||||||
filters.selectedFloors?.includes(
|
|
||||||
task?.workItem?.workArea?.floor?.floorName
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.selectedActivities.length > 0) {
|
|
||||||
filteredTasks = filteredTasks.filter((task) =>
|
|
||||||
filters.selectedActivities.includes(
|
|
||||||
task?.workItem?.activityMaster?.activityName
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
setTaskLists(filteredTasks);
|
|
||||||
} else {
|
|
||||||
setTaskLists([]);
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [selectedProject, projectNames, dispatch]);
|
||||||
TaskList,
|
|
||||||
filters?.selectedBuilding,
|
|
||||||
filters?.selectedFloors,
|
|
||||||
filters?.selectedActivities,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Memoized filtering
|
||||||
const AssignmentDates = [
|
const filteredTasks = useMemo(() => {
|
||||||
...new Set(TaskLists.map((task) => task.assignmentDate.split("T")[0])),
|
if (!TaskList) return [];
|
||||||
].sort((a, b) => new Date(b) - new Date(a));
|
return TaskList.filter((task) => {
|
||||||
setDates(AssignmentDates);
|
const { selectedBuilding, selectedFloors, selectedActivities } = filters;
|
||||||
}, [TaskLists]);
|
|
||||||
|
|
||||||
const [selectedTask, selectTask] = useState(null);
|
if (selectedBuilding && task?.workItem?.workArea?.floor?.building?.name !== selectedBuilding) return false;
|
||||||
const [comments, setComment] = useState({ task: null, isActionAllow: false });
|
if (selectedFloors.length > 0 && !selectedFloors.includes(task?.workItem?.workArea?.floor?.floorName)) return false;
|
||||||
|
if (selectedActivities.length > 0 && !selectedActivities.includes(task?.workItem?.activityMaster?.activityName)) return false;
|
||||||
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
return true;
|
||||||
const [isModalOpenComment, setIsModalOpenComment] = useState(false);
|
|
||||||
|
|
||||||
const openModal = () => setIsModalOpen(true);
|
|
||||||
const closeModal = () => setIsModalOpen(false);
|
|
||||||
|
|
||||||
const openComment = () => setIsModalOpenComment(true);
|
|
||||||
const closeCommentModal = () => setIsModalOpenComment(false);
|
|
||||||
const [IsSubTaskNeeded, setIsSubTaskNeeded] = useState(false);
|
|
||||||
const [SubTaskData, setSubTaskData] = useState();
|
|
||||||
const handletask = (task) => {
|
|
||||||
selectTask(task);
|
|
||||||
openModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
popoverRefs.current.forEach((el) => {
|
|
||||||
if (el) {
|
|
||||||
new bootstrap.Popover(el, {
|
|
||||||
trigger: "focus",
|
|
||||||
placement: "left",
|
|
||||||
html: true,
|
|
||||||
content: el.getAttribute("data-bs-content"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}, [dates, TaskLists]);
|
}, [TaskList, filters]);
|
||||||
|
|
||||||
|
// Memoized dates
|
||||||
|
const groupedTasks = useMemo(() => {
|
||||||
|
const groups = {};
|
||||||
|
filteredTasks.forEach((task) => {
|
||||||
|
const date = task.assignmentDate.split("T")[0];
|
||||||
|
if (!groups[date]) groups[date] = [];
|
||||||
|
groups[date].push(task);
|
||||||
|
});
|
||||||
|
return Object.keys(groups)
|
||||||
|
.sort((a, b) => new Date(b) - new Date(a))
|
||||||
|
.map((date) => ({ date, tasks: groups[date] }));
|
||||||
|
}, [filteredTasks]);
|
||||||
|
|
||||||
const handlecloseModal = () =>
|
// --- Modal State
|
||||||
{
|
const [modal, setModal] = useState({ type: null, data: null });
|
||||||
setIsModalOpen( false )
|
|
||||||
// refetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCloseAction = (IsSubTask) => {
|
const openModal = (type, data = null) => setModal({ type, data });
|
||||||
if (IsSubTask) {
|
const closeModal = () => setModal({ type: null, data: null });
|
||||||
setIsSubTaskNeeded(true);
|
|
||||||
setIsModalOpenComment(false);
|
|
||||||
} else {
|
|
||||||
// refetch();
|
|
||||||
setIsModalOpenComment(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const hanleCloseSubTask = () => {
|
|
||||||
setIsSubTaskNeeded(false);
|
|
||||||
setComment( null );
|
|
||||||
// refetch();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// --- Render helpers
|
||||||
|
const renderTeamMembers = (task, refIndex) => (
|
||||||
|
<div
|
||||||
|
key={refIndex}
|
||||||
|
tabIndex="0"
|
||||||
|
className="d-flex align-items-center avatar-group justify-content-center"
|
||||||
|
data-bs-toggle="popover"
|
||||||
|
data-bs-trigger="focus"
|
||||||
|
data-bs-placement="left"
|
||||||
|
data-bs-html="true"
|
||||||
|
data-bs-content={`
|
||||||
|
<div class="border border-secondary rounded custom-popover p-2 px-3">
|
||||||
|
${task.teamMembers
|
||||||
|
.map(
|
||||||
|
(m) => `
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-2">
|
||||||
|
<div class="avatar avatar-xs">
|
||||||
|
<span class="avatar-initial rounded-circle bg-label-primary">
|
||||||
|
${m?.firstName?.charAt(0) || ""}${m?.lastName?.charAt(0) || ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span>${m.firstName} ${m.lastName}</span>
|
||||||
|
</div>`
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{task.teamMembers.slice(0, 3).map((m) => (
|
||||||
|
<div key={m.id} className="avatar avatar-xs" title={`${m.firstName} ${m.lastName}`}>
|
||||||
|
<span className="avatar-initial rounded-circle bg-label-primary">
|
||||||
|
{m?.firstName.slice(0, 1)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{task.teamMembers.length > 3 && (
|
||||||
|
<div className="avatar avatar-xs" title={`${task.teamMembers.length - 3} more`}>
|
||||||
|
<span className="avatar-initial rounded-circle bg-label-secondary">+{task.teamMembers.length - 3}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isModalOpen && <GlobalModel isOpen={isModalOpen} size="md" closeModal={handlecloseModal} >
|
{/* --- Modals --- */}
|
||||||
<ReportTask
|
{modal.type === "report" && (
|
||||||
report={selectedTask}
|
<GlobalModel isOpen size="md" closeModal={closeModal}>
|
||||||
closeModal={handlecloseModal}
|
<ReportTask report={modal.data} closeModal={closeModal} />
|
||||||
// refetch={refetch}
|
</GlobalModel>
|
||||||
/>
|
)}
|
||||||
</GlobalModel>}
|
{modal.type === "comments" && (
|
||||||
|
<GlobalModel isOpen size="lg" closeModal={closeModal}>
|
||||||
{isModalOpenComment && (
|
|
||||||
<GlobalModel
|
|
||||||
isOpen={isModalOpenComment}
|
|
||||||
size="lg"
|
|
||||||
closeModal={() => setIsModalOpenComment(false)}
|
|
||||||
>
|
|
||||||
<ReportTaskComments
|
<ReportTaskComments
|
||||||
commentsData={comments.task}
|
commentsData={modal.data.task}
|
||||||
actionAllow={comments.isActionAllow}
|
actionAllow={modal.data.isActionAllow}
|
||||||
handleCloseAction={handleCloseAction}
|
handleCloseAction={(isSubTask) => {
|
||||||
closeModal={closeCommentModal}
|
if (isSubTask) openModal("subtask", modal.data.task);
|
||||||
|
else closeModal();
|
||||||
|
}}
|
||||||
|
closeModal={closeModal}
|
||||||
/>
|
/>
|
||||||
</GlobalModel>
|
</GlobalModel>
|
||||||
)}
|
)}
|
||||||
|
{modal.type === "subtask" && (
|
||||||
{IsSubTaskNeeded && (
|
<GlobalModel isOpen size="lg" closeModal={closeModal}>
|
||||||
<GlobalModel
|
<SubTask activity={modal.data} onClose={closeModal} />
|
||||||
isOpen={IsSubTaskNeeded}
|
|
||||||
size="lg"
|
|
||||||
closeModal={hanleCloseSubTask}
|
|
||||||
>
|
|
||||||
<SubTask activity={comments.task} onClose={hanleCloseSubTask} />
|
|
||||||
</GlobalModel>
|
</GlobalModel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
<Breadcrumb
|
<Breadcrumb data={[{ label: "Home", link: "/dashboard" }, { label: "Daily Progress Report" }]} />
|
||||||
data={[
|
|
||||||
{ label: "Home", link: "/dashboard" },
|
<div className="card card-action mb-6">
|
||||||
{ label: "Daily Progress Report", link: null },
|
|
||||||
]}
|
|
||||||
></Breadcrumb>
|
|
||||||
<div className="card card-action mb-6 ">
|
|
||||||
<div className="card-body p-1 p-sm-2">
|
<div className="card-body p-1 p-sm-2">
|
||||||
<div className="row d-flex justify-content-between align-items-center">
|
{!selectedProject && (<div className="text-center text-muted">Please Select Project</div>)}
|
||||||
<div className="col-md-12 d-flex align-items-center col-12 text-start mb-2 mb-md-0">
|
{/* --- Filters --- */}
|
||||||
<DateRangePicker
|
<div className="d-flex align-items-center mb-2">
|
||||||
onRangeChange={setDateRange}
|
<DateRangePicker onRangeChange={setDateRange} endDateMode="today" DateDifference="6" dateFormat="DD-MM-YYYY" />
|
||||||
endDateMode="today"
|
<FilterIcon
|
||||||
DateDifference="6"
|
taskListData={TaskList}
|
||||||
dateFormat="DD-MM-YYYY"
|
onApplyFilters={setFilters}
|
||||||
/>
|
currentSelectedBuilding={filters.selectedBuilding}
|
||||||
<FilterIcon
|
currentSelectedFloors={filters.selectedFloors}
|
||||||
taskListData={TaskList}
|
currentSelectedActivities={filters.selectedActivities}
|
||||||
onApplyFilters={setFilters}
|
/>
|
||||||
currentSelectedBuilding={filters.selectedBuilding}
|
|
||||||
currentSelectedFloors={filters.selectedFloors}
|
|
||||||
currentSelectedActivities={filters.selectedActivities}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* --- Table --- */}
|
||||||
<div className="table-responsive text-nowrap mt-3">
|
<div className="table-responsive text-nowrap mt-3">
|
||||||
<table className="table">
|
<table className="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Activity</th>
|
<th>Activity</th>
|
||||||
<th>Assigned </th>
|
<th>Assigned</th>
|
||||||
<th>Completed</th>
|
<th>Completed</th>
|
||||||
<th>Assign On</th>
|
<th>Assign On</th>
|
||||||
<th>Team</th>
|
<th>Team</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="table-border-bottom-0">
|
<tbody>
|
||||||
{/* --- Spinner when tasks are loading --- */}
|
{taskLoading && (
|
||||||
{task_loading && (
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="text-center">
|
<td colSpan={6} className="text-center">
|
||||||
{" "}
|
<Loader/>
|
||||||
<div className="mt-10 mb-10 pt-5 pb-10">
|
|
||||||
<div
|
|
||||||
className="spinner-border text-primary"
|
|
||||||
role="status"
|
|
||||||
>
|
|
||||||
<span className="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2">Loading Tasks...</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{!task_loading &&
|
{!taskLoading && groupedTasks.length === 0 && (
|
||||||
TaskLists.length === 0 && (
|
<tr>
|
||||||
<tr>
|
<td colSpan={6} className="text-center">No Reports Found</td>
|
||||||
<td colSpan={6} className="text-center">
|
</tr>
|
||||||
<div className="mt-10 mb-10 pt-10 pb-10">
|
)}
|
||||||
{" "}
|
{!taskLoading &&
|
||||||
<p>No Reports Found</p>
|
groupedTasks.map(({ date, tasks }) => (
|
||||||
</div>
|
<React.Fragment key={date}>
|
||||||
</td>
|
<tr className="table-row-header text-start">
|
||||||
</tr>
|
<td colSpan={6}><strong>{formatUTCToLocalTime(date)}</strong></td>
|
||||||
)}
|
</tr>
|
||||||
{!task_loading &&
|
{tasks.map((task, idx) => (
|
||||||
TaskLists.length > 0 &&
|
<tr key={task.id || idx}>
|
||||||
dates.map((date, i) => {
|
<td className="flex-wrap text-start">
|
||||||
const tasksForDate = TaskLists.filter((task) =>
|
<div>{task.workItem.activityMaster?.activityName || "No Activity Name"}</div>
|
||||||
task.assignmentDate.includes(date)
|
<div className="text-sm">
|
||||||
);
|
{task.workItem.workArea?.floor?.building?.name} › {task.workItem.workArea?.floor?.floorName} › {task.workItem.workArea?.areaName}
|
||||||
if (tasksForDate.length === 0) return null;
|
</div>
|
||||||
|
</td>
|
||||||
return (
|
<td>{formatNumber(task.plannedTask)} / {formatNumber(task.workItem.plannedWork - task.workItem.completedWork)}</td>
|
||||||
<React.Fragment key={i}>
|
<td>{task.completedTask}</td>
|
||||||
<tr className="table-row-header">
|
<td>{formatUTCToLocalTime(task.assignmentDate)}</td>
|
||||||
<td colSpan={6} className="text-start">
|
<td className="text-center">{renderTeamMembers(task, idx)}</td>
|
||||||
{" "}
|
<td className="text-center">
|
||||||
<strong>
|
<div className="d-flex justify-content-end gap-2">
|
||||||
{moment(date).format("DD-MM-YYYY")}
|
{ReportTaskRights && !task.reportedDate && (
|
||||||
</strong>
|
<button className="btn btn-xs btn-primary" onClick={() => openModal("report", task)}>Report</button>
|
||||||
|
)}
|
||||||
|
{ApprovedTaskRights && task.reportedDate && !task.approvedBy && (
|
||||||
|
<button className="btn btn-xs btn-warning" onClick={() => openModal("comments", { task, isActionAllow: true })}>QC</button>
|
||||||
|
)}
|
||||||
|
<button className="btn btn-xs btn-primary" onClick={() => openModal("comments", { task, isActionAllow: false })}>Comment</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{tasksForDate.map((task, index) => {
|
))}
|
||||||
const refIndex = index * 10 + i;
|
</React.Fragment>
|
||||||
return (
|
))}
|
||||||
<React.Fragment key={refIndex}>
|
|
||||||
<tr>
|
|
||||||
<td className="flex-wrap text-start">
|
|
||||||
<div>
|
|
||||||
{task.workItem.activityMaster
|
|
||||||
.activityName || "No Activity Name"}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="col-form-label text-sm">
|
|
||||||
{" "}
|
|
||||||
{
|
|
||||||
task?.workItem?.workArea?.floor
|
|
||||||
?.building?.name
|
|
||||||
}{" "}
|
|
||||||
<i
|
|
||||||
className="bx bx-chevron-right text-sm"
|
|
||||||
style={{ fontSize: ".75rem" }}
|
|
||||||
></i>{" "}
|
|
||||||
{
|
|
||||||
task?.workItem?.workArea?.floor
|
|
||||||
?.floorName
|
|
||||||
}{" "}
|
|
||||||
<i
|
|
||||||
className="bx bx-chevron-right text-sm"
|
|
||||||
style={{ fontSize: ".75rem" }}
|
|
||||||
>
|
|
||||||
{" "}
|
|
||||||
</i>
|
|
||||||
{task?.workItem?.workArea?.areaName}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{formatNumber(task.plannedTask)} / {formatNumber(task.workItem.plannedWork - task.workItem.completedWork)}
|
|
||||||
</td>
|
|
||||||
<td>{task.completedTask}</td>
|
|
||||||
<td>
|
|
||||||
{moment(task.assignmentDate).format(
|
|
||||||
"DD-MM-YYYY"
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="text-center">
|
|
||||||
<div
|
|
||||||
key={refIndex}
|
|
||||||
ref={(el) =>
|
|
||||||
(popoverRefs.current[refIndex] = el)
|
|
||||||
}
|
|
||||||
tabIndex="0"
|
|
||||||
className="d-flex align-items-center avatar-group justify-content-center"
|
|
||||||
data-bs-toggle="popover"
|
|
||||||
data-bs-trigger="focus"
|
|
||||||
data-bs-placement="left"
|
|
||||||
data-bs-html="true"
|
|
||||||
data-bs-content={`
|
|
||||||
<div class="border border-secondary rounded custom-popover p-2 px-3">
|
|
||||||
${task.teamMembers
|
|
||||||
.map(
|
|
||||||
(member) => `
|
|
||||||
<div class="d-flex align-items-center gap-2 mb-2">
|
|
||||||
<div class="avatar avatar-xs">
|
|
||||||
<span class="avatar-initial rounded-circle bg-label-primary">
|
|
||||||
${
|
|
||||||
member?.firstName?.charAt(
|
|
||||||
0
|
|
||||||
) || ""
|
|
||||||
}${
|
|
||||||
member?.lastName?.charAt(0) || ""
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span>${member.firstName} ${
|
|
||||||
member.lastName
|
|
||||||
}</span>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{task.teamMembers
|
|
||||||
.slice(0, 3)
|
|
||||||
.map((member) => (
|
|
||||||
<div
|
|
||||||
key={member.id}
|
|
||||||
data-bs-toggle="tooltip"
|
|
||||||
data-bs-html="true"
|
|
||||||
data-popup="tooltip-custom"
|
|
||||||
data-bs-placement="top"
|
|
||||||
title={`${member.firstName} ${member.lastName}`}
|
|
||||||
className="avatar avatar-xs"
|
|
||||||
>
|
|
||||||
<span className="avatar-initial rounded-circle bg-label-primary">
|
|
||||||
{member?.firstName.slice(0, 1)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{task.teamMembers.length > 3 && (
|
|
||||||
<div
|
|
||||||
className="avatar avatar-xs"
|
|
||||||
data-bs-toggle="tooltip"
|
|
||||||
data-bs-placement="bottom"
|
|
||||||
title={`${
|
|
||||||
task.teamMembers.length - 3
|
|
||||||
} more`}
|
|
||||||
>
|
|
||||||
<span className="avatar-initial rounded-circle bg-label-secondary pull-up">
|
|
||||||
+ {task.teamMembers.length - 3}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="text-center">
|
|
||||||
<div className="d-flex justify-content-end">
|
|
||||||
{ ReportTaskRights &&
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`btn btn-xs btn-primary ${
|
|
||||||
task.reportedDate != null
|
|
||||||
? "d-none"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
selectTask(task);
|
|
||||||
openModal();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Report
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
{(ApprovedTaskRights && task.reportedDate ) && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`btn btn-xs btn-warning ${
|
|
||||||
task.reportedDate && task.approvedBy
|
|
||||||
? "d-none"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
setComment({
|
|
||||||
task: task,
|
|
||||||
isActionAllow: true,
|
|
||||||
});
|
|
||||||
openComment();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
QC
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-xs btn-primary ms-2"
|
|
||||||
onClick={() => {
|
|
||||||
setComment({
|
|
||||||
task: task,
|
|
||||||
isActionAllow: false,
|
|
||||||
});
|
|
||||||
openComment();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Comment
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React,{useEffect} from "react";
|
import React,{useEffect,useRef} from "react";
|
||||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||||
import InfraPlanning from "../../components/Activities/InfraPlanning";
|
import InfraPlanning from "../../components/Activities/InfraPlanning";
|
||||||
import { useProjectName } from "../../hooks/useProjects";
|
import { useProjectName } from "../../hooks/useProjects";
|
||||||
@ -6,33 +6,34 @@ import { useDispatch, useSelector } from "react-redux";
|
|||||||
import { setProjectId } from "../../slices/localVariablesSlice";
|
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||||
import { useSelectedproject } from "../../slices/apiDataManager";
|
import { useSelectedproject } from "../../slices/apiDataManager";
|
||||||
|
|
||||||
|
|
||||||
const TaskPlannng = () => {
|
const TaskPlannng = () => {
|
||||||
// const selectedProject = useSelector(
|
|
||||||
// (store) => store.localVariables.projectId
|
|
||||||
// );
|
|
||||||
const selectedProject = useSelectedproject();
|
const selectedProject = useSelectedproject();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { projectNames, loading: projectLoading } = useProjectName();
|
||||||
|
|
||||||
|
const initialized = useRef(false);
|
||||||
|
|
||||||
const dispatch = useDispatch()
|
|
||||||
const { projectNames, loading: projectLoading, fetchData } = useProjectName();
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(selectedProject == null){
|
if (!initialized.current && projectNames.length > 0 && !selectedProject?.id) {
|
||||||
dispatch(setProjectId(projectNames[0]?.id));
|
dispatch(setProjectId(projectNames[0].id));
|
||||||
}
|
initialized.current = true;
|
||||||
},[])
|
}
|
||||||
|
}, [projectNames, selectedProject, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="container-fluid">
|
||||||
<div className="container-fluid">
|
<Breadcrumb
|
||||||
<Breadcrumb
|
data={[
|
||||||
data={[
|
{ label: "Home", link: "/dashboard" },
|
||||||
{ label: "Home", link: "/dashboard" },
|
{ label: "Daily Task Planning" },
|
||||||
{ label: "Daily Task Planning" }
|
]}
|
||||||
]}
|
/>
|
||||||
></Breadcrumb>
|
{selectedProject ? (
|
||||||
<InfraPlanning/>
|
<InfraPlanning />
|
||||||
</div>
|
) : (
|
||||||
</>
|
<div className="text-center">Please Select Project</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -80,21 +80,23 @@ const ExpensePage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowTrigger(true);
|
if (IsViewAll || IsViewSelf || IsCreatedAble) {
|
||||||
setOffcanvasContent(
|
setShowTrigger(true);
|
||||||
"Expense Filters",
|
setOffcanvasContent(
|
||||||
<ExpenseFilterPanel
|
"Expense Filters",
|
||||||
onApply={setFilter}
|
<ExpenseFilterPanel
|
||||||
handleGroupBy={setGroupBy}
|
onApply={setFilter}
|
||||||
clearFilter={clearFilter}
|
handleGroupBy={setGroupBy}
|
||||||
/>
|
clearFilter={clearFilter}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
setShowTrigger(false);
|
setShowTrigger(false);
|
||||||
setOffcanvasContent("", null);
|
setOffcanvasContent("", null);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [IsViewAll, IsViewSelf, IsCreatedAble]);
|
||||||
|
|
||||||
const contextValue = {
|
const contextValue = {
|
||||||
setViewExpense,
|
setViewExpense,
|
||||||
@ -105,16 +107,17 @@ const ExpensePage = () => {
|
|||||||
return (
|
return (
|
||||||
<ExpenseContext.Provider value={contextValue}>
|
<ExpenseContext.Provider value={contextValue}>
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
<Breadcrumb data={[{ label: "Home", link: "/" }, { label: "Expense" }]} />
|
<Breadcrumb
|
||||||
|
data={[{ label: "Home", link: "/" }, { label: "Expense" }]}
|
||||||
|
/>
|
||||||
|
|
||||||
{(IsViewAll || IsViewSelf || IsCreatedAble) ? (
|
{IsViewAll || IsViewSelf || IsCreatedAble ? (
|
||||||
<>
|
<>
|
||||||
<div className="card my-3 px-sm-4 px-0">
|
<div className="card my-3 px-sm-4 px-0">
|
||||||
<div className="card-body py-2 px-3">
|
<div className="card-body py-2 px-3">
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
<div className="col-6 ">
|
<div className="col-6 ">
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
className="form-control form-control-sm w-auto"
|
className="form-control form-control-sm w-auto"
|
||||||
@ -132,7 +135,12 @@ const ExpensePage = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
className="p-1 me-1 m-sm-0 bg-primary rounded-circle"
|
className="p-1 me-1 m-sm-0 bg-primary rounded-circle"
|
||||||
title="Add New Expense"
|
title="Add New Expense"
|
||||||
onClick={() => setManageExpenseModal({ IsOpen: true, expenseId: null })}
|
onClick={() =>
|
||||||
|
setManageExpenseModal({
|
||||||
|
IsOpen: true,
|
||||||
|
expenseId: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-plus fs-4 text-white"></i>
|
<i className="bx bx-plus fs-4 text-white"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -142,12 +150,18 @@ const ExpensePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ExpenseList filters={filters} groupBy={groupBy} searchText={searchText} />
|
<ExpenseList
|
||||||
|
filters={filters}
|
||||||
|
groupBy={groupBy}
|
||||||
|
searchText={searchText}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="card text-center py-1">
|
<div className="card text-center py-1">
|
||||||
<i className="fa-solid fa-triangle-exclamation fs-5" />
|
<i className="fa-solid fa-triangle-exclamation fs-5" />
|
||||||
<p>Access Denied: You don't have permission to perform this action!</p>
|
<p>
|
||||||
|
Access Denied: You don't have permission to perform this action !
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -156,12 +170,16 @@ const ExpensePage = () => {
|
|||||||
<GlobalModel
|
<GlobalModel
|
||||||
isOpen
|
isOpen
|
||||||
size="lg"
|
size="lg"
|
||||||
closeModal={() => setManageExpenseModal({ IsOpen: null, expenseId: null })}
|
closeModal={() =>
|
||||||
|
setManageExpenseModal({ IsOpen: null, expenseId: null })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ManageExpense
|
<ManageExpense
|
||||||
key={ManageExpenseModal.expenseId ?? "new"}
|
key={ManageExpenseModal.expenseId ?? "new"}
|
||||||
expenseToEdit={ManageExpenseModal.expenseId}
|
expenseToEdit={ManageExpenseModal.expenseId}
|
||||||
closeModal={() => setManageExpenseModal({ IsOpen: null, expenseId: null })}
|
closeModal={() =>
|
||||||
|
setManageExpenseModal({ IsOpen: null, expenseId: null })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</GlobalModel>
|
</GlobalModel>
|
||||||
)}
|
)}
|
||||||
|
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 { mastersList } from "../../data/masters";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { changeMaster } from "../../slices/localVariablesSlice";
|
import { changeMaster } from "../../slices/localVariablesSlice";
|
||||||
import useMaster from "../../hooks/masterHook/useMaster"
|
import useMaster, { useMasterMenu } from "../../hooks/masterHook/useMaster"
|
||||||
import MasterTable from "./MasterTable";
|
import MasterTable from "./MasterTable";
|
||||||
import { getCachedData } from "../../slices/apiDataManager";
|
import { getCachedData } from "../../slices/apiDataManager";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
@ -13,6 +13,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
|
|
||||||
const MasterPage = () => {
|
const MasterPage = () => {
|
||||||
|
const {data,isLoading,isError,error:menuError} = useMasterMenu()
|
||||||
const [modalConfig, setModalConfig] = useState({ modalType: "", item: null, masterType: null });
|
const [modalConfig, setModalConfig] = useState({ modalType: "", item: null, masterType: null });
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [filteredResults, setFilteredResults] = useState([]);
|
const [filteredResults, setFilteredResults] = useState([]);
|
||||||
@ -23,7 +24,7 @@ const MasterPage = () => {
|
|||||||
const selectedMaster = useSelector((store) => store.localVariables.selectedMaster);
|
const selectedMaster = useSelector((store) => store.localVariables.selectedMaster);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { data: masterData = [], loading, error, RecallApi } = useMaster();
|
const { data: masterData = [], loading, error, RecallApi,isError:isMasterError } = useMaster();
|
||||||
|
|
||||||
const openModal = () => setIsCreateModalOpen(true);
|
const openModal = () => setIsCreateModalOpen(true);
|
||||||
|
|
||||||
@ -83,7 +84,10 @@ const MasterPage = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if(isError || isMasterError) return <div className="d-flex flex-column align-items-center justify-content-center py-5">
|
||||||
|
<h4 className=" mb-3"><i className="fa-solid fa-triangle-exclamation fs-5" /> Oops, an error occurred</h4>
|
||||||
|
<p className="text-muted">{error?.message || menuError?.message}</p>
|
||||||
|
</div>
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isCreateModalOpen && (
|
{isCreateModalOpen && (
|
||||||
@ -121,8 +125,8 @@ const MasterPage = () => {
|
|||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
value={selectedMaster}
|
value={selectedMaster}
|
||||||
>
|
>
|
||||||
|
{isLoading && (<option value={null}>Loading...</option>)}
|
||||||
{mastersList.map((item) => (
|
{(!isLoading && data) && data?.map((item) => (
|
||||||
|
|
||||||
<option key={item.id} value={item.name}>{item.name}</option>
|
<option key={item.id} value={item.name}>{item.name}</option>
|
||||||
))}
|
))}
|
||||||
@ -154,7 +158,6 @@ const MasterPage = () => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm add-new btn-primary `}
|
className={`btn btn-sm add-new btn-primary `}
|
||||||
// ${hasUserPermission('660131a4-788c-4739-a082-cbbf7879cbf2') ? "":"d-none"}
|
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
aria-controls="DataTables_Table_0"
|
aria-controls="DataTables_Table_0"
|
||||||
type="button"
|
type="button"
|
||||||
@ -162,7 +165,6 @@ const MasterPage = () => {
|
|||||||
data-bs-target="#master-modal"
|
data-bs-target="#master-modal"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleModalData(selectedMaster, "null", selectedMaster)
|
handleModalData(selectedMaster, "null", selectedMaster)
|
||||||
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
|
@ -137,7 +137,6 @@ const ProjectDetails = () => {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<ProjectNav onPillClick={handlePillClick} activePill={activePill} />
|
<ProjectNav onPillClick={handlePillClick} activePill={activePill} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -15,6 +15,8 @@ const AuthRepository = {
|
|||||||
logout: (data) => api.post("/api/auth/logout", data),
|
logout: (data) => api.post("/api/auth/logout", data),
|
||||||
profile: () => api.get("/api/user/profile"),
|
profile: () => api.get("/api/user/profile"),
|
||||||
changepassword: (data) => api.post("/api/auth/change-password", data),
|
changepassword: (data) => api.post("/api/auth/change-password", data),
|
||||||
|
appmenu:()=>api.get('/api/appmenu/get/menu')
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AuthRepository;
|
export default AuthRepository;
|
||||||
|
@ -18,6 +18,8 @@ export const RolesRepository = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MasterRespository = {
|
export const MasterRespository = {
|
||||||
|
getMasterMenus:()=>api.get("/api/AppMenu/get/master-list"),
|
||||||
|
|
||||||
getRoles: () => api.get("/api/roles"),
|
getRoles: () => api.get("/api/roles"),
|
||||||
createRole: (data) => api.post("/api/roles", data),
|
createRole: (data) => api.post("/api/roles", data),
|
||||||
updateRoles: (id, data) => api.put(`/api/roles/${id}`, data),
|
updateRoles: (id, data) => api.put(`/api/roles/${id}`, data),
|
||||||
|
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,19 +38,24 @@ import LegalInfoCard from "../pages/TermsAndConditions/LegalInfoCard";
|
|||||||
import ProtectedRoute from "./ProtectedRoute";
|
import ProtectedRoute from "./ProtectedRoute";
|
||||||
import Directory from "../pages/Directory/Directory";
|
import Directory from "../pages/Directory/Directory";
|
||||||
import LoginWithOtp from "../pages/authentication/LoginWithOtp";
|
import LoginWithOtp from "../pages/authentication/LoginWithOtp";
|
||||||
|
import TenantPage from "../pages/Tenant/TenantPage";
|
||||||
|
import CreateTenant from "../pages/Tenant/CreateTenant";
|
||||||
import ExpensePage from "../pages/Expense/ExpensePage";
|
import ExpensePage from "../pages/Expense/ExpensePage";
|
||||||
|
import TenantDetails from "../pages/Tenant/TenantDetails";
|
||||||
|
import SelfTenantDetails from "../pages/Tenant/SelfTenantDetails";
|
||||||
|
import SuperTenantDetails from "../pages/Tenant/SuperTenantDetails";
|
||||||
|
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
element: <AuthLayout />,
|
element: <AuthLayout />,
|
||||||
children: [
|
children: [
|
||||||
{path: "/auth/login", element: <LoginPage />},
|
{ path: "/auth/login", element: <LoginPage /> },
|
||||||
{path: "/auth/login-otp", element: <LoginWithOtp />},
|
{ path: "/auth/login-otp", element: <LoginWithOtp /> },
|
||||||
{ path: "/auth/reqest/demo", element: <RegisterPage /> },
|
{ path: "/auth/reqest/demo", element: <RegisterPage /> },
|
||||||
{ path: "/auth/forgot-password", element: <ForgotPasswordPage /> },
|
{ path: "/auth/forgot-password", element: <ForgotPasswordPage /> },
|
||||||
{ path: "/reset-password", element: <ResetPasswordPage /> },
|
{ path: "/reset-password", element: <ResetPasswordPage /> },
|
||||||
{ path: "/legal-info", element: <LegalInfoCard /> },
|
{ path: "/legal-info", element: <LegalInfoCard /> },
|
||||||
{ path: "/auth/changepassword", element: <ChangePasswordPage /> },
|
{ path: "/auth/changepassword", element: <ChangePasswordPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -79,6 +84,10 @@ const router = createBrowserRouter(
|
|||||||
{ path: "/gallary", element: <ImageGallary /> },
|
{ path: "/gallary", element: <ImageGallary /> },
|
||||||
{ path: "/expenses", element: <ExpensePage /> },
|
{ path: "/expenses", element: <ExpensePage /> },
|
||||||
{ path: "/masters", element: <MasterPage /> },
|
{ path: "/masters", element: <MasterPage /> },
|
||||||
|
{ path: "/tenants", element: <TenantPage /> },
|
||||||
|
{ path: "/tenants/new-tenant", element: <CreateTenant /> },
|
||||||
|
{ path: "/tenant/:tenantId", element: <SuperTenantDetails /> },
|
||||||
|
{ path: "/tenant/self", element: <SelfTenantDetails /> },
|
||||||
{ path: "/help/support", element: <Support /> },
|
{ path: "/help/support", element: <Support /> },
|
||||||
{ path: "/help/docs", element: <Documentation /> },
|
{ path: "/help/docs", element: <Documentation /> },
|
||||||
{ path: "/help/connect", element: <Connect /> },
|
{ path: "/help/connect", element: <Connect /> },
|
||||||
|
@ -3,7 +3,8 @@ import { createSlice } from "@reduxjs/toolkit";
|
|||||||
const globalVariablesSlice = createSlice({
|
const globalVariablesSlice = createSlice({
|
||||||
name: "globalVariables",
|
name: "globalVariables",
|
||||||
initialState: {
|
initialState: {
|
||||||
loginUser:null
|
loginUser:null,
|
||||||
|
currentTenant:null
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
setGlobalVariable: (state, action) => {
|
setGlobalVariable: (state, action) => {
|
||||||
@ -13,9 +14,12 @@ const globalVariablesSlice = createSlice({
|
|||||||
setLoginUserPermmisions: ( state, action ) =>
|
setLoginUserPermmisions: ( state, action ) =>
|
||||||
{
|
{
|
||||||
state.loginUser = action.payload
|
state.loginUser = action.payload
|
||||||
|
},
|
||||||
|
setCurrentTenant:(state,action)=>{
|
||||||
|
state.currentTenant = action.payload
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setGlobalVariable,setLoginUserPermmisions } = globalVariablesSlice.actions;
|
export const { setGlobalVariable,setLoginUserPermmisions,setCurrentTenant } = globalVariablesSlice.actions;
|
||||||
export default globalVariablesSlice.reducer;
|
export default globalVariablesSlice.reducer;
|
||||||
|
@ -5,6 +5,8 @@ export const OTP_EXPIRY_SECONDS = 600 // OTP time
|
|||||||
|
|
||||||
export const MANAGE_MASTER = "588a8824-f924-4955-82d8-fc51956cf323";
|
export const MANAGE_MASTER = "588a8824-f924-4955-82d8-fc51956cf323";
|
||||||
|
|
||||||
|
export const VIEW_MASTER = "5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d"
|
||||||
|
|
||||||
export const MANAGE_PROJECT = "172fc9b6-755b-4f62-ab26-55c34a330614"
|
export const MANAGE_PROJECT = "172fc9b6-755b-4f62-ab26-55c34a330614"
|
||||||
|
|
||||||
export const VIEW_PROJECTS = "6ea44136-987e-44ba-9e5d-1cf8f5837ebc"
|
export const VIEW_PROJECTS = "6ea44136-987e-44ba-9e5d-1cf8f5837ebc"
|
||||||
@ -21,6 +23,8 @@ export const MANAGE_PROJECT_INFRA = "cf2825ad-453b-46aa-91d9-27c124d63373"
|
|||||||
export const VIEW_PROJECT_INFRA = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"
|
export const VIEW_PROJECT_INFRA = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"
|
||||||
|
|
||||||
export const REGULARIZE_ATTENDANCE ="57802c4a-00aa-4a1f-a048-fd2f70dd44b6"
|
export const REGULARIZE_ATTENDANCE ="57802c4a-00aa-4a1f-a048-fd2f70dd44b6"
|
||||||
|
export const TEAM_ATTENDANCE = "915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"
|
||||||
|
export const SELF_ATTENDANCE = "ccb0589f-712b-43de-92ed-5b6088e7dc4e"
|
||||||
|
|
||||||
|
|
||||||
export const ASSIGN_TO_PROJECT = "b94802ce-0689-4643-9e1d-11c86950c35b";
|
export const ASSIGN_TO_PROJECT = "b94802ce-0689-4643-9e1d-11c86950c35b";
|
||||||
@ -59,10 +63,45 @@ export const EXPENSE_MANAGE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"
|
|||||||
export const EXPENSE_REJECTEDBY = ["d1ee5eec-24b6-4364-8673-a8f859c60729","965eda62-7907-4963-b4a1-657fb0b2724b"]
|
export const EXPENSE_REJECTEDBY = ["d1ee5eec-24b6-4364-8673-a8f859c60729","965eda62-7907-4963-b4a1-657fb0b2724b"]
|
||||||
|
|
||||||
export const EXPENSE_DRAFT = "297e0d8f-f668-41b5-bfea-e03b354251c8"
|
export const EXPENSE_DRAFT = "297e0d8f-f668-41b5-bfea-e03b354251c8"
|
||||||
|
|
||||||
|
export const SUPPER_TENANT = "d032cb1a-3f30-462c-bef0-7ace73a71c0b"
|
||||||
|
export const MANAGE_TENANTS = "00e20637-ce8d-4417-bec4-9b31b5e65092"
|
||||||
|
export const VIEW_TENANTS = "647145c6-2108-4c98-aab4-178602236e55"
|
||||||
|
export const ActiveTenant = "297e0d8f-f668-41b5-bfea-e03b354251c8"
|
||||||
// -------------------Application Role------------------------------
|
// -------------------Application Role------------------------------
|
||||||
|
|
||||||
// 1 - Expense Manage
|
// 1 - Expense Manage
|
||||||
export const EXPENSE_MANAGEMENT = "a4e25142-449b-4334-a6e5-22f70e4732d7"
|
export const EXPENSE_MANAGEMENT = "a4e25142-449b-4334-a6e5-22f70e4732d7"
|
||||||
|
|
||||||
|
export const TENANT_STATUS = [
|
||||||
|
{id:"62b05792-5115-4f99-8ff5-e8374859b191",name:"Active"},
|
||||||
|
{id:"c0b5def8-087e-4235-b3a4-8e2f0ed91b94",name:"In Active"},
|
||||||
|
{id:"35d7840a-164a-448b-95e6-efb2ec84a751",name:"Supspended"}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const CONSTANT_TEXT = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SUBSCRIPTION_PLAN_FREQUENCIES = {
|
||||||
|
0: "Monthly",
|
||||||
|
1:"Quarterly",
|
||||||
|
2:"Half-Yearly",
|
||||||
|
3:"Yearly"
|
||||||
|
}
|
||||||
|
export const reference = [
|
||||||
|
{ val: "google", name: "Google" },
|
||||||
|
{ val: "frineds", name: "Friends" },
|
||||||
|
{ val: "advertisement", name: "Advertisement" },
|
||||||
|
{ val: "root tenant", name: "Root Tenant" },
|
||||||
|
];
|
||||||
|
export const orgSize = [
|
||||||
|
{ val: "1-50", name: "1-50" },
|
||||||
|
{ val: "51-100", name: "51-100" },
|
||||||
|
{ val: "101-500", name: "101-500" },
|
||||||
|
{ val: "500+", name: "500+" },
|
||||||
|
];
|
||||||
|
|
||||||
export const BASE_URL = process.env.VITE_BASE_URL;
|
export const BASE_URL = process.env.VITE_BASE_URL;
|
||||||
|
|
||||||
// export const BASE_URL = "https://api.marcoaiot.com";
|
// export const BASE_URL = "https://api.marcoaiot.com";
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { ActiveTenant } from "./constants";
|
||||||
|
|
||||||
export const getDateDifferenceInDays = (startDate, endDate) => {
|
export const getDateDifferenceInDays = (startDate, endDate) => {
|
||||||
if (!startDate || !endDate) {
|
if (!startDate || !endDate) {
|
||||||
@ -82,4 +83,8 @@ export const getCompletionPercentage = (completedWork, plannedWork)=> {
|
|||||||
const clamped = Math.min(Math.max(percentage, 0), 100);
|
const clamped = Math.min(Math.max(percentage, 0), 100);
|
||||||
|
|
||||||
return clamped.toFixed(2);
|
return clamped.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTenantStatus =(statusId)=>{
|
||||||
|
return ActiveTenant === statusId ? " bg-label-success":"bg-label-secondary"
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user