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

This commit is contained in:
pramod mahajan 2025-09-10 18:16:17 +05:30
commit 04df99634c
85 changed files with 5033 additions and 1401 deletions

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { useEmployeeAttendacesLog } from "../../hooks/useAttendance";
import { convertShortTime } from "../../utils/dateUtils";
import { convertShortTime, formatUTCToLocalTime } from "../../utils/dateUtils";
import { useNavigate } from "react-router-dom";
import { THRESH_HOLD } from "../../utils/constants";
@ -128,7 +128,7 @@ const AttendLogs = ({ Id }) => {
<p>
Attendance logs for{" "}
{logs[0]?.employee?.firstName + " " + logs[0]?.employee?.lastName}{" "}
on {logs[0]?.activityTime.slice(0, 10)}{" "}
on {formatUTCToLocalTime(logs[0]?.activityTime)}
</p>
)}
</div>
@ -156,7 +156,7 @@ const AttendLogs = ({ Id }) => {
.sort((a, b) => b.id - a.id)
.map((log, index) => (
<tr key={index}>
<td>{log.activityTime.slice(0, 10)}</td>
<td>{formatUTCToLocalTime(log.activityTime)}</td>
<td>{convertShortTime(log.activityTime)}</td>
<td>
{whichActivityPerform(log.activity, log.activityTime)}

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect, useCallback, useMemo } from "react";
import moment from "moment";
import Avatar from "../common/Avatar";
import { convertShortTime } from "../../utils/dateUtils";
import { convertShortTime, formatUTCToLocalTime } from "../../utils/dateUtils";
import RenderAttendanceStatus from "./RenderAttendanceStatus";
import usePagination from "../../hooks/usePagination";
import { useNavigate } from "react-router-dom";
@ -10,7 +10,7 @@ import { useAttendance } from "../../hooks/useAttendance";
import { useSelector } from "react-redux";
import { useQueryClient } from "@tanstack/react-query";
import eventBus from "../../services/eventBus";
import { useSelectedproject } from "../../slices/apiDataManager";
import { useSelectedProject } from "../../slices/apiDataManager";
const Attendance = ({ getRole, handleModalData, searchTerm }) => {
const queryClient = useQueryClient();
@ -21,7 +21,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
// const selectedProject = useSelector(
// (store) => store.localVariables.projectId
// );
const selectedProject = useSelectedproject();
const selectedProject = useSelectedProject();
const {
attendance,
loading: attLoading,
@ -116,7 +116,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
<>
<div className="table-responsive text-nowrap h-100" >
<div className="d-flex text-start align-items-center py-2">
<strong>Date : {todayDate.toLocaleDateString("en-GB")}</strong>
<strong>Date : {formatUTCToLocalTime(todayDate)}</strong>
<div className="form-check form-switch text-start m-0 ms-5">
<input
type="checkbox"

View File

@ -6,11 +6,12 @@ import RenderAttendanceStatus from "./RenderAttendanceStatus";
import { useSelector, useDispatch } from "react-redux";
import { fetchAttendanceData } from "../../slices/apiSlice/attedanceLogsSlice";
import DateRangePicker from "../common/DateRangePicker";
import { clearCacheKey, getCachedData, useSelectedproject } from "../../slices/apiDataManager";
import { clearCacheKey, getCachedData, useSelectedProject } from "../../slices/apiDataManager";
import eventBus from "../../services/eventBus";
import AttendanceRepository from "../../repositories/AttendanceRepository";
import { useAttendancesLogs } from "../../hooks/useAttendance";
import { queryClient } from "../../layouts/AuthLayout";
import { ITEMS_PER_PAGE } from "../../utils/constants";
const usePagination = (data, itemsPerPage) => {
const [currentPage, setCurrentPage] = useState(1);
@ -37,7 +38,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
// const selectedProject = useSelector(
// (store) => store.localVariables.projectId
// );
const selectedProject = useSelectedproject();
const selectedProject = useSelectedProject();
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
@ -353,7 +354,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
<span className="text-secondary">No Pending Record Available !</span>
</div>
)}
{filteredSearchData.length > 10 && (
{filteredSearchData.length > ITEMS_PER_PAGE && (
<nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1">
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>

View File

@ -9,7 +9,7 @@ import { markAttendance } from "../../slices/apiSlice/attedanceLogsSlice";
import showToast from "../../services/toastService";
import { checkIfCurrentDate } from "../../utils/dateUtils";
import { useMarkAttendance } from "../../hooks/useAttendance";
import { useSelectedproject } from "../../slices/apiDataManager";
import { useSelectedProject } from "../../slices/apiDataManager";
const createSchema = (modeldata) => {
return z
@ -43,9 +43,8 @@ const createSchema = (modeldata) => {
});
};
const CheckCheckOutmodel = ({ modeldata, closeModal, handleSubmitForm }) => {
// const projectId = useSelector((store) => store.localVariables.projectId);
const projectId = useSelectedproject();
const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
const projectId = useSelectedProject();
const { mutate: MarkAttendance } = useMarkAttendance();
const [isLoading, setIsLoading] = useState(false);
const coords = usePositionTracker();
@ -173,7 +172,7 @@ const CheckCheckOutmodel = ({ modeldata, closeModal, handleSubmitForm }) => {
);
};
export default CheckCheckOutmodel;
export default CheckInCheckOut;
const schemaReg = z.object({
description: z.string().min(1, { message: "please give reason!" }),

View File

@ -15,7 +15,8 @@ import {useDispatch, useSelector} from "react-redux";
import {useProfile} from "../../hooks/useProfile";
import {refreshData, setProjectId} from "../../slices/localVariablesSlice";
import InfraTable from "../Project/Infrastructure/InfraTable";
import { useSelectedproject } from "../../slices/apiDataManager";
import { useSelectedProject } from "../../slices/apiDataManager";
import Loader from "../common/Loader";
const InfraPlanning = () =>
@ -23,7 +24,7 @@ const InfraPlanning = () =>
const {profile: LoggedUser, refetch : fetchData} = useProfile()
const dispatch = useDispatch()
// const selectedProject = useSelector((store)=>store.localVariables.projectId)
const selectedProject = useSelectedproject();
const selectedProject = useSelectedProject();
const {projectInfra, isLoading, error} = useProjectInfra( selectedProject )
@ -34,15 +35,15 @@ const InfraPlanning = () =>
const reloadedData = useSelector( ( store ) => store.localVariables.reload )
useEffect( () =>
{
if (reloadedData)
{
refetch()
dispatch( refreshData( false ) )
}
// useEffect( () =>
// {
// if (reloadedData)
// {
// refetch()
// dispatch( refreshData( false ) )
// }
},[reloadedData])
// },[reloadedData])
return (
<div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4">
@ -51,7 +52,7 @@ const InfraPlanning = () =>
{(ApprovedTaskRights || ReportTaskRights) ? (
<div className="align-items-center">
<div className="row ">
{isLoading && ( <p>Loading...</p> )}
{isLoading && (<Loader/> )}
{( !isLoading && projectInfra?.length === 0 ) && ( <p>No Result Found</p> )}
{(!isLoading && projectInfra?.length > 0) && (<InfraTable buildings={projectInfra} projectId={selectedProject}/>)}
</div>

View File

@ -7,13 +7,13 @@ import { useRegularizationRequests } from "../../hooks/useAttendance";
import moment from "moment";
import usePagination from "../../hooks/usePagination";
import eventBus from "../../services/eventBus";
import { cacheData, clearCacheKey, useSelectedproject } from "../../slices/apiDataManager";
import { cacheData, clearCacheKey, useSelectedProject } from "../../slices/apiDataManager";
import { useQueryClient } from "@tanstack/react-query";
const Regularization = ({ handleRequest, searchTerm }) => {
const queryClient = useQueryClient();
// var selectedProject = useSelector((store) => store.localVariables.projectId);
const selectedProject = useSelectedproject();
const selectedProject = useSelectedProject();
const [regularizesList, setregularizedList] = useState([]);
const { regularizes, loading, error, refetch } =
useRegularizationRequests(selectedProject);

View File

@ -4,7 +4,7 @@ import useAttendanceStatus, { ACTIONS } from '../../hooks/useAttendanceStatus';
import { useDispatch, useSelector } from 'react-redux';
import { usePositionTracker } from '../../hooks/usePositionTracker';
import {markCurrentAttendance} from '../../slices/apiSlice/attendanceAllSlice';
import {cacheData, getCachedData, useSelectedproject} from '../../slices/apiDataManager';
import {cacheData, getCachedData, useSelectedProject} from '../../slices/apiDataManager';
import showToast from '../../services/toastService';
import { useMarkAttendance } from '../../hooks/useAttendance';
import { useQueryClient } from '@tanstack/react-query';
@ -18,7 +18,7 @@ const {mutate:MarkAttendance,isPending} = useMarkAttendance()
const queryClient = useQueryClient()
// const projectId = useSelector((store)=>store.localVariables.projectId)
const projectId = useSelectedproject();
const projectId = useSelectedProject();
const {latitude,longitude} = usePositionTracker();
const dispatch = useDispatch()

View File

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

View File

@ -1,8 +1,16 @@
import React, { useEffect, useState, useMemo } from "react";
import { DirectoryRepository } from "../../repositories/DirectoryRepository";
import NoteCardDirectoryEditable from "./NoteCardDirectoryEditable";
import { useSelectedProject } from "../../slices/apiDataManager";
const NotesCardViewDirectory = ({
notes,
setNotesForFilter,
searchText,
filterAppliedNotes,
}) => {
const projectId = useSelectedProject();
const NotesCardViewDirectory = ({ notes, setNotesForFilter, searchText, filterAppliedNotes }) => {
const [allNotes, setAllNotes] = useState([]);
const [filteredNotes, setFilteredNotes] = useState([]);
const [loading, setLoading] = useState(true);
@ -13,13 +21,15 @@ const NotesCardViewDirectory = ({ notes, setNotesForFilter, searchText, filterAp
const pageSize = 20;
useEffect(() => {
fetchNotes();
}, []);
if (projectId) {
fetchNotes(projectId);
}
}, [projectId]);
const fetchNotes = async () => {
const fetchNotes = async (projId) => {
setLoading(true);
try {
const response = await DirectoryRepository.GetNotes(1000, 1);
const response = await DirectoryRepository.GetNotes(1000, 1, projId); // pass projectId
const fetchedNotes = response.data?.data || [];
setAllNotes(fetchedNotes);
setNotesForFilter(fetchedNotes)
@ -122,7 +132,7 @@ const NotesCardViewDirectory = ({ notes, setNotesForFilter, searchText, filterAp
prevNotes.map((n) => (n.id === updatedNote.id ? updatedNote : n))
);
}}
onNoteDelete={() => fetchNotes()}
onNoteDelete={() => fetchNotes(projectId)} // reload with projectId
/>
))}
</div>

View File

@ -22,6 +22,7 @@ const EmpAttendance = ({ employee }) => {
data = [],
isLoading: loading,
isFetching,
isError,
error,
refetch,
} = useAttendanceByEmployee(employee, dateRange.startDate, dateRange.endDate);
@ -125,7 +126,7 @@ const EmpAttendance = ({ employee }) => {
className="dataTables_length text-start py-2 d-flex justify-content-between "
id="DataTables_Table_0_length"
>
<div className="col-md-3 my-0 ">
<div className="col-md-4 my-0 ">
<DateRangePicker
DateDifference="30"
onRangeChange={setDateRange}
@ -145,7 +146,7 @@ const EmpAttendance = ({ employee }) => {
</div>
<div className="table-responsive text-nowrap">
{!loading && data.length === 0 && <span>No employee logs</span>}
{error && <div className="text-center">{error}</div>}
{isError && <div className="text-center">{error.message}</div>}
{loading && !data && <div className="text-center">Loading...</div>}
{data && data.length > 0 && (
<table className="table mb-0">

View File

@ -59,20 +59,20 @@ const EmpBanner = ({ profile, loggedInUser }) => {
</h4>
<ul className="list-inline mb-0 d-flex align-items-center flex-wrap justify-content-sm-start justify-content-center gap-4 mt-4">
<li className="list-inline-item">
<i className="icon-base bx bx-crown me-2 align-top"></i>
<i className="icon-base bx bx-crown me-1 align-top"></i>
<span className="fw-medium">
{profile?.jobRole || <em>NA</em>}
</span>
</li>
<li className="list-inline-item">
<i className="icon-base bx bx-phone me-2 align-top"></i>
<i className="icon-base bx bx-phone me-0 align-top"></i>
<span className="fw-medium">
{" "}
{profile?.phoneNumber || <em>NA</em>}
</span>
</li>
<li className="list-inline-item">
<i className="icon-base bx bx-calendar me-2 align-top"></i>
<i className="icon-base bx bx-calendar me-0 align-top"></i>
<span className="fw-medium">
{" "}
Joined on{" "}
@ -85,18 +85,21 @@ const EmpBanner = ({ profile, loggedInUser }) => {
</li>
</ul>
<ul className="list-inline mb-0 d-flex align-items-center flex-wrap justify-content-sm-start justify-content-center mt-4">
<li className="list-inline-item">
<button
className="btn btn-sm btn-primary btn-block"
onClick={() => setShowModal(true)}
>
Edit Profile
</button>
</li>
<li className="list-inline-item">
{profile?.id == loggedInUser?.employeeInfo?.id && (
{profile?.isActive && ( // show only if active
<li className="list-inline-item">
<button
className="btn btn-sm btn-outline-primary btn-block"
className="btn btn-sm btn-primary btn-block"
onClick={() => setShowModal(true)}
>
Edit Profile
</button>
</li>
)}
<li className="list-inline-item">
{profile?.id === loggedInUser?.employeeInfo?.id && (
<button
className="btn btn-sm btn-outline-primary btn-block"
onClick={() => openChangePassword()}
>
Change Password

View File

@ -105,6 +105,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
startField="startDate"
endField="endDate"
resetSignal={resetKey}
defaultRange={false}
/>
</div>

View File

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

View File

@ -3,7 +3,7 @@ import {
cacheData,
clearAllCache,
getCachedData,
useSelectedproject,
useSelectedProject,
} from "../../slices/apiDataManager";
import AuthRepository from "../../repositories/AuthRepository";
import { useDispatch, useSelector } from "react-redux";
@ -101,7 +101,7 @@ const Header = () => {
const { projectNames, loading: projectLoading, fetchData } = useProjectName();
const selectedProject = useSelectedproject();
const selectedProject = useSelectedProject();
const projectsForDropdown = isDashboardPath
? projectNames

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import { MANAGE_PROJECT } from "../../utils/constants";
import GlobalModel from "../common/GlobalModel";
import ManageProjectInfo from "./ManageProjectInfo";
import { useQueryClient } from "@tanstack/react-query";
import { useSelectedproject } from "../../slices/apiDataManager";
import { useSelectedProject } from "../../slices/apiDataManager";
const AboutProject = () => {
const [IsOpenModal, setIsOpenModal] = useState(false);
@ -21,7 +21,7 @@ const AboutProject = () => {
// *** MODIFIED LINE: Get projectId from Redux store using useSelector ***
// const projectId = useSelector((store) => store.localVariables.projectId);
const projectId = useSelectedproject();
const projectId = useSelectedProject();
const manageProject = useHasUserPermission(MANAGE_PROJECT);
const { projects_Details, isLoading, error, refetch } = useProjectDetails(projectId); // Pass projectId from useSelector

View File

@ -15,7 +15,7 @@ import {
cacheData,
clearCacheKey,
getCachedData,
useSelectedproject,
useSelectedProject,
} from "../../slices/apiDataManager";
import { useProjectDetails, useProjectInfra } from "../../hooks/useProjects";
import { useDispatch, useSelector } from "react-redux";
@ -27,7 +27,7 @@ import GlobalModel from "../common/GlobalModel";
const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
{
// const projectId = useSelector((store)=>store.localVariables.projectId)
const projectId = useSelectedproject();
const projectId = useSelectedProject();
const reloadedData = useSelector((store) => store.localVariables.reload);
const [ expandedBuildings, setExpandedBuildings ] = useState( [] );
const {projectInfra,isLoading,error} = useProjectInfra(projectId)

View File

@ -18,12 +18,12 @@ import {
useEmployeesByProjectAllocated,
useManageProjectAllocation,
} from "../../hooks/useProjects";
import { useSelectedproject } from "../../slices/apiDataManager";
import { useSelectedProject } from "../../slices/apiDataManager";
const Teams = () => {
// const {projectId} = useParams()
// const projectId = useSelector((store)=>store.localVariables.projectId)
const projectId = useSelectedproject();
const projectId = useSelectedProject();
const dispatch = useDispatch();
const { data, loading } = useMaster();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,248 @@
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" >
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" >
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" >
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
id="organizationSize"
className="form-select shadow-none border py-1 px-2"
style={{ fontSize: "0.875rem" }} // Bootstrap's small text size
{...register("organizationSize", { required: "Organization size is required" })}
>
{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
id="industryId"
className="form-select shadow-none border py-1 px-2 small"
{...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
id="reference"
className="form-select shadow-none border py-1 px-2 small"
{...register("reference")}
>
{reference.map((org) => (
<option key={org.val} value={org.val}>
{org.name}
</option>
))}
</select>
{errors.reference && (
<div className="danger-text">{errors.reference.message}</div>
)}
</div>
<div className="col-sm-12">
<Label htmlFor="description">Description</Label>
<textarea
id="description"
rows={3}
className={`form-control form-control-sm `}
{...register("description")}
/>
{errors.description && (
<div className="danger-text">{errors.description.message}</div>
)}
</div>
<div className="col-sm-12">
<Label htmlFor="logImage">Logo Image</Label>
<LogoUpload
preview={logoPreview}
setPreview={setLogoPreview}
fileName={logoName}
setFileName={setLogoName}
/>
</div>
<div className="d-flex justify-content-between mt-3">
<button
type="button"
className="btn btn-sm btn-secondary"
onClick={onPrev}
disabled={isPending}
>
Back
</button>
<button
type="button"
className="btn btn-sm btn-primary"
onClick={handleNext}
disabled={isPending}
>
{isPending ? "Please Wait..." : "Submit and Next"}
</button>
</div>
</div>
);
};
export default OrganizationInfo;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,124 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useState, useCallback, useEffect } 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";
import { useLocation } from "react-router-dom";
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]
);
// Close popup when navigating to another component
const location = useLocation();
useEffect(() => {
handleClosePanel();
}, [location]);
const onClear = useCallback(() => {
reset(defaultFilterValues);
setResetKey((prev) => prev + 1); // triggers DateRangePicker reset
onApply(defaultFilterValues);
}, [onApply, reset]);
if (isLoading) {
return <div className="text-center">Loading...</div>;
}
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="text-start mb-1">
<div className="text-start my-2">
<DateRangePicker1
placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="startDate"
endField="endDate"
resetSignal={resetKey}
defaultRange={false}
/>
</div>
<div className="text-strat mb-2">
<SelectMultiple
name="industryIds"
label="Industries"
options={industries}
labelKey="name"
valueKey="id"
/>
</div>
<div className="text-start mb-2">
<SelectMultiple
name="references"
label="References"
options={reference}
labelKey="name"
valueKey="val"
/>
</div>
<div className="text-start">
<SelectMultiple
name="tenantStatusIds"
label="Tenant Status"
options={TENANT_STATUS}
labelKey="name"
valueKey="id"
/>
</div>
{/* <SelectMultiple
name="references"
label="Industries :"
options={reference}
labelKey="name"
valueKey="val"
/> */}
</div>
<div className="d-flex justify-content-end py-3 gap-2">
<button
type="button"
className="btn btn-secondary btn-xs"
onClick={onClear}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-xs" >
Apply
</button>
</div>
</form>
</FormProvider>
);
};
export default TenantFilterPanel;

View File

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

View File

@ -0,0 +1,155 @@
import { z } from "zod";
export const newTenantSchema = z.object({
firstName: z
.string().trim()
.min(1, { message: "First Name is required!" })
.regex(/^[A-Za-z]+$/, { message: "First Name should contain only letters!" }),
lastName: z
.string().trim()
.min(1, { message: "Last Name is required!" })
.regex(/^[A-Za-z]+$/, { message: "Last Name should contain only letters!" }),
email: z.string().trim().email("Invalid email address"),
description: z.string().trim().optional(),
domainName: z.string().trim().optional(),
billingAddress: z.string().trim().nonempty("Billing address is required"),
taxId: z.string().trim().optional(),
logoImage: z.string().trim().optional(),
organizationName: z.string().trim().nonempty("Organization name is required"),
officeNumber: z.string().trim().optional(),
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().optional(),
billingAddress: z.string().trim().min(1, { message: "Billing Address is required!" }),
taxId: z.string().trim().optional(),
logoImage: z.string().optional(),
officeNumber: z.string().trim().optional(),
contactNumber: z.string().trim()
.nonempty("Contact number is required")
.regex(/^\+?[1-9]\d{7,14}$/, "Enter a valid contact number"),
organizationSize: z.string().min(1, { message: "Organization Size is required!" }),
industryId: z.string().min(1, { message: "Invalid Industry ID!" }),
reference: z.string().optional(),
});

View File

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

View File

@ -46,6 +46,12 @@ const DateRangePicker = ({
};
}, [onRangeChange, DateDifference, endDateMode]);
const handleIconClick = () => {
if (inputRef.current) {
inputRef.current._flatpickr.open(); // directly opens flatpickr
}
};
return (
<div className={`col-${sm} col-sm-${md} px-1`}>
<input
@ -57,7 +63,7 @@ const DateRangePicker = ({
/>
<i
className="bx bx-calendar calendar-icon cursor-pointer position-relative top-50 translate-middle-y "
className="bx bx-calendar calendar-icon cursor-pointer position-relative top-50 translate-middle-y " onClick={handleIconClick}
style={{ right: "22px", bottom: "-8px" }}
></i>
</div>
@ -76,7 +82,8 @@ export const DateRangePicker1 = ({
placeholder = "Select date range",
className = "",
allowText = false,
resetSignal, // <- NEW prop
resetSignal,
defaultRange = true,
...rest
}) => {
const inputRef = useRef(null);
@ -124,10 +131,9 @@ export const DateRangePicker1 = ({
...rest,
});
// Apply default if empty
const currentStart = getValues(startField);
const currentEnd = getValues(endField);
if (!currentStart && !currentEnd) {
if (defaultRange && !currentStart && !currentEnd) {
applyDefaultDates();
} else if (currentStart && currentEnd) {
instance.setDate([
@ -139,12 +145,11 @@ export const DateRangePicker1 = ({
return () => instance.destroy();
}, []);
// Reapply default range on resetSignal change
useEffect(() => {
if (resetSignal !== undefined) {
if (defaultRange && resetSignal !== undefined) {
applyDefaultDates();
}
}, [resetSignal]);
}, [resetSignal, defaultRange]);
const start = getValues(startField);
const end = getValues(endField);
@ -173,3 +178,4 @@ export const DateRangePicker1 = ({
</div>
);
};

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react";
import { useSelector } from "react-redux";
const FilterIcon = ({
taskListData,
@ -7,15 +8,26 @@ const FilterIcon = ({
currentSelectedFloors,
currentSelectedActivities,
}) => {
const selectedProject = useSelector((store) => store.localVariables.projectId);
const [selectedBuilding, setSelectedBuilding] = useState(currentSelectedBuilding || "");
const [selectedFloors, setSelectedFloors] = useState(currentSelectedFloors || []);
const [selectedActivities, setSelectedActivities] = useState(currentSelectedActivities || []);
const [appliedBuilding, setAppliedBuilding] = useState(currentSelectedBuilding || "");
const [appliedFloors, setAppliedFloors] = useState(currentSelectedFloors || []);
const [appliedActivities, setAppliedActivities] = useState(currentSelectedActivities || []);
// Reset filters whenever inputs OR projectId changes
useEffect(() => {
setSelectedBuilding(currentSelectedBuilding || "");
setSelectedFloors(currentSelectedFloors || []);
setSelectedActivities(currentSelectedActivities || []);
}, [currentSelectedBuilding, currentSelectedFloors, currentSelectedActivities]);
setAppliedBuilding(currentSelectedBuilding || "");
setAppliedFloors(currentSelectedFloors || []);
setAppliedActivities(currentSelectedActivities || []);
}, [currentSelectedBuilding, currentSelectedFloors, currentSelectedActivities, selectedProject]);
const getUniqueFilterValues = (key, overrideBuilding, overrideFloors) => {
if (!taskListData) return [];
@ -61,12 +73,11 @@ const FilterIcon = ({
} else if (filterType === "floor") {
if (updatedFloors.includes(value)) {
updatedFloors = updatedFloors.filter((floor) => floor !== value);
const validActivities = getUniqueFilterValues("activity", updatedBuilding, updatedFloors);
updatedActivities = updatedActivities.filter((act) => validActivities.includes(act));
} else {
updatedFloors.push(value);
}
const validActivities = getUniqueFilterValues("activity", updatedBuilding, updatedFloors);
updatedActivities = updatedActivities.filter((act) => validActivities.includes(act));
} else if (filterType === "activity") {
if (updatedActivities.includes(value)) {
updatedActivities = updatedActivities.filter((act) => act !== value);
@ -78,12 +89,20 @@ const FilterIcon = ({
setSelectedBuilding(updatedBuilding);
setSelectedFloors(updatedFloors);
setSelectedActivities(updatedActivities);
};
const applyFilters = () => {
setAppliedBuilding(selectedBuilding);
setAppliedFloors(selectedFloors);
setAppliedActivities(selectedActivities);
onApplyFilters({
selectedBuilding: updatedBuilding,
selectedFloors: updatedFloors,
selectedActivities: updatedActivities,
selectedBuilding,
selectedFloors,
selectedActivities,
});
document.getElementById("filterDropdown").click();
};
const clearAllFilters = () => {
@ -91,6 +110,10 @@ const FilterIcon = ({
setSelectedFloors([]);
setSelectedActivities([]);
setAppliedBuilding("");
setAppliedFloors([]);
setAppliedActivities([]);
onApplyFilters({
selectedBuilding: "",
selectedFloors: [],
@ -98,21 +121,51 @@ const FilterIcon = ({
});
};
// Count applied filters
const appliedFilterCount =
(appliedBuilding ? 1 : 0) + appliedFloors.length + appliedActivities.length;
return (
<div className="dropdown" style={{marginLeft:"-14px"}}>
<div className="dropdown" style={{ marginLeft: "-14px", position: "relative" }}>
<a
className="dropdown-toggle hide-arrow cursor-pointer"
id="filterDropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{/* <i className="bx bx-slider-alt ms-1" /> */}
<i
className="bx bx-slider-alt"
style={{ color: selectedBuilding || selectedFloors.length > 0 || selectedActivities.length > 0 ? "#7161EF" : "gray" }}
></i>
<div style={{ position: "relative", display: "inline-block" }}>
<i
className="bx bx-slider-alt"
style={{
color: appliedFilterCount > 0 ? "#7161EF" : "gray",
fontSize: "20px",
}}
></i>
{appliedFilterCount > 0 && (
<span
style={{
position: "absolute",
top: "-11px",
right: "-6px",
backgroundColor: "#FFC107", // yellow
color: "white",
fontSize: "10px",
fontWeight: "bold",
borderRadius: "50%",
width: "18px",
height: "18px",
display: "flex",
alignItems: "center",
justifyContent: "center",
border: "1px solid white",
}}
>
{appliedFilterCount}
</span>
)}
</div>
</a>
<ul
className="dropdown-menu p-2 mt-2"
aria-labelledby="filterDropdown"
@ -205,45 +258,30 @@ const FilterIcon = ({
)}
{/* Action Buttons */}
<li><hr className="my-1" /></li>
{(selectedBuilding || selectedFloors.length > 0 || selectedActivities.length > 0) && (
<li className="d-flex justify-content-end gap-2 px-2">
<li>
<hr className="my-1" />
</li>
{(appliedFilterCount > 0 ||
selectedBuilding ||
selectedFloors.length > 0 ||
selectedActivities.length > 0) && (
<li className="d-flex justify-content-end gap-2 px-2 mt-2 mb-2">
<button
type="button"
className="btn btn-sm"
style={{
backgroundColor: "#7161EF",
color: "white",
fontSize: "13px",
padding: "4px 16px",
borderRadius: "8px",
boxShadow: "0 1px 4px rgba(0,0,0,0.1)"
}}
className="btn btn-secondary btn-sm py-0 px-2"
onClick={clearAllFilters}
>
Clear
</button>
<button
type="button"
className="btn btn-sm"
style={{
backgroundColor: "#7161EF",
color: "white",
fontSize: "13px",
padding: "4px 16px",
borderRadius: "8px",
boxShadow: "0 1px 4px rgba(0,0,0,0.1)"
}}
onClick={() => {
document.getElementById("filterDropdown").click();
}}
className="btn btn-primary btn-sm py-0 px-2"
onClick={applyFilters}
>
Apply
</button>
</li>
)}
</ul>
</div>
);

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect,useCallback } from "react";
import React, { useState, useEffect, useCallback } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
@ -7,7 +7,7 @@ import { MasterRespository } from "../../repositories/MastersRepository";
import { clearApiCacheKey } from "../../slices/apiCacheSlice";
import { getCachedData, cacheData } from "../../slices/apiDataManager";
import showToast from "../../services/toastService";
import {useCreateActivity} from "../../hooks/masterHook/useMaster";
import { useCreateActivity } from "../../hooks/masterHook/useMaster";
const schema = z.object({
activityName: z.string().min(1, { message: "Activity Name is required" }),
@ -24,74 +24,74 @@ const schema = z.object({
});
const CreateActivity = ({ onClose }) => {
const maxDescriptionLength = 255;
const { mutate: createActivity, isPending: isLoading } = useCreateActivity(() => onClose?.());
const maxDescriptionLength = 255;
const { mutate: createActivity, isPending: isLoading } = useCreateActivity(() => onClose?.());
const {
register,
handleSubmit,
control,
setValue,
clearErrors,
setError,
getValues,
reset,
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
activityName: "",
unitOfMeasurement: "",
checkList: [],
},
});
const {
register,
handleSubmit,
control,
setValue,
clearErrors,
setError,
getValues,
reset,
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
activityName: "",
unitOfMeasurement: "",
checkList: [],
},
});
const {
fields: checkListItems,
append,
remove,
} = useFieldArray({
control,
name: "checkList",
});
const {
fields: checkListItems,
append,
remove,
} = useFieldArray({
control,
name: "checkList",
});
const addChecklistItem = useCallback(() => {
const values = getValues("checkList");
const lastIndex = checkListItems.length - 1;
const addChecklistItem = useCallback(() => {
const values = getValues("checkList");
const lastIndex = checkListItems.length - 1;
if (
checkListItems.length > 0 &&
(!values?.[lastIndex] || values[lastIndex].description.trim() === "")
) {
setError(`checkList.${lastIndex}.description`, {
type: "manual",
message: "Please fill this checklist item before adding another.",
});
return;
}
if (
checkListItems.length > 0 &&
(!values?.[lastIndex] || values[lastIndex].description.trim() === "")
) {
setError(`checkList.${lastIndex}.description`, {
type: "manual",
message: "Please fill this checklist item before adding another.",
});
return;
}
clearErrors(`checkList.${lastIndex}.description`);
append({ id: null, description: "", isMandatory: false });
}, [checkListItems, getValues, append, setError, clearErrors]);
clearErrors(`checkList.${lastIndex}.description`);
append({ id: null, description: "", isMandatory: false });
}, [checkListItems, getValues, append, setError, clearErrors]);
const removeChecklistItem = useCallback((index) => {
remove(index);
}, [remove]);
const removeChecklistItem = useCallback((index) => {
remove(index);
}, [remove]);
const handleChecklistChange = useCallback((index, value) => {
setValue(`checkList.${index}`, value);
}, [setValue]);
const handleChecklistChange = useCallback((index, value) => {
setValue(`checkList.${index}`, value);
}, [setValue]);
const onSubmit = (formData) => {
createActivity(formData);
};
const onSubmit = (formData) => {
createActivity(formData);
};
// const onSubmit = (data) => {
// setIsLoading(true);
// MasterRespository.createActivity(data)
// .then( ( resp ) =>
// {
// const cachedData = getCachedData("Activity");
// const updatedData = [ ...cachedData, resp?.data ];
// cacheData("Activity", updatedData);
@ -104,15 +104,15 @@ const onSubmit = (formData) => {
// setIsLoading(false);
// });
// };
const handleClose = useCallback(() => {
reset();
onClose();
}, [reset, onClose]);
const handleClose = useCallback(() => {
reset();
onClose();
}, [reset, onClose]);
useEffect(() => {
const tooltipTriggerList = Array.from(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach((el) => new bootstrap.Tooltip(el));
}, []);
useEffect(() => {
const tooltipTriggerList = Array.from(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach((el) => new bootstrap.Tooltip(el));
}, []);
return (
<form onSubmit={handleSubmit(onSubmit)}>
@ -123,9 +123,8 @@ useEffect(() => {
<input
type="text"
{...register("activityName")}
className={`form-control form-control-sm ${
errors.activityName ? "is-invalid" : ""
}`}
className={`form-control form-control-sm ${errors.activityName ? "is-invalid" : ""
}`}
/>
{errors.activityName && (
<p className="danger-text">{errors.activityName.message}</p>
@ -137,9 +136,8 @@ useEffect(() => {
<input
type="text"
{...register("unitOfMeasurement")}
className={`form-control form-control-sm ${
errors.unitOfMeasurement ? "is-invalid" : ""
}`}
className={`form-control form-control-sm ${errors.unitOfMeasurement ? "is-invalid" : ""
}`}
/>
{errors.unitOfMeasurement && (
<p className="danger-text">{errors.unitOfMeasurement.message}</p>
@ -147,68 +145,68 @@ useEffect(() => {
</div>
<div className="col-md-12 text-start mt-1">
<p className="py-1 my-0">{checkListItems.length > 0 ? "Check List" : "Add Check List" }</p>
<p className="py-1 my-0">{checkListItems.length > 0 ? "Check List" : "Add Check List"}</p>
{checkListItems.length > 0 && (
<table className="table mt-1 border-0">
<thead className="py-0 my-0 table-border-top-0">
<tr className="py-1">
<th colSpan={2} className="py-1">
<small>Name</small>
</th>
<th colSpan={2} className="py-1 text-center">
<small>Is Mandatory</small>
</th>
<th className="text-center py-1">Action</th>
</tr>
</thead>
<tbody className="table-border-bottom-0 ">
{checkListItems.map((item, index) => (
<tr key={index} className="border-top-0">
<td colSpan={2} className="border-top-0 border-0">
<input
className="d-none"
{...register(`checkList.${index}.id`)}
></input>
<input
{...register(`checkList.${index}.description`)}
className="form-control form-control-sm"
placeholder={`Checklist item ${index + 1}`}
onChange={(e) =>
handleChecklistChange(index, e.target.value)
}
/>
{errors.checkList?.[index]?.description && (
<small
style={{ fontSize: "10px" }}
className="danger-text"
>
{errors.checkList[index]?.description?.message}
</small>
)}
</td>
<td colSpan={2} className="text-center border-0">
<input
className="form-check-input"
type="checkbox"
{...register(`checkList.${index}.isMandatory`)}
defaultChecked={item.isMandatory}
/>
</td>
<td className="text-center border-0">
<button
type="button"
onClick={() => removeChecklistItem(index)}
className="btn btn-xs btn-icon btn-text-secondary"
>
<i className="bx bxs-minus-circle text-danger" data-bs-toggle="tooltip"
title="Remove Check"
data-bs-original-title="Remove check"></i>
</button>
</td>
<table className="table mt-1 border-0">
<thead className="py-0 my-0 table-border-top-0">
<tr className="py-1">
<th colSpan={2} className="py-1">
<small>Name</small>
</th>
<th colSpan={2} className="py-1 text-center">
<small>Is Mandatory</small>
</th>
<th className="text-center py-1">Action</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody className="table-border-bottom-0 ">
{checkListItems.map((item, index) => (
<tr key={index} className="border-top-0">
<td colSpan={2} className="border-top-0 border-0">
<input
className="d-none"
{...register(`checkList.${index}.id`)}
></input>
<input
{...register(`checkList.${index}.description`)}
className="form-control form-control-sm"
placeholder={`Checklist item ${index + 1}`}
onChange={(e) =>
handleChecklistChange(index, e.target.value)
}
/>
{errors.checkList?.[index]?.description && (
<small
style={{ fontSize: "10px" }}
className="danger-text"
>
{errors.checkList[index]?.description?.message}
</small>
)}
</td>
<td colSpan={2} className="text-center border-0">
<input
className="form-check-input"
type="checkbox"
{...register(`checkList.${index}.isMandatory`)}
defaultChecked={item.isMandatory}
/>
</td>
<td className="text-center border-0">
<button
type="button"
onClick={() => removeChecklistItem(index)}
className="btn btn-xs btn-icon btn-text-secondary"
>
<i className="bx bxs-minus-circle text-danger" data-bs-toggle="tooltip"
title="Remove Check"
data-bs-original-title="Remove check"></i>
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
<button
type="button"
@ -216,8 +214,8 @@ useEffect(() => {
onClick={addChecklistItem}
>
<i className="bx bx-plus-circle" data-bs-toggle="tooltip"
title="Add Check"
data-bs-original-title="Add check" ></i>
title="Add Check"
data-bs-original-title="Add check" ></i>
</button>
</div>
@ -226,12 +224,13 @@ useEffect(() => {
{isLoading ? "Please Wait" : "Submit"}
</button>
<button
type="reset"
type="button" // change to button
className="btn btn-sm btn-label-secondary"
onClick={handleClose}
>
Cancel
</button>
</div>
</div>
</form>

View File

@ -1,43 +1,43 @@
import React, { useEffect,useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager';
import { getCachedData, cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService';
import {useCreateContactCategory} from '../../hooks/masterHook/useMaster';
import { useCreateContactCategory } from '../../hooks/masterHook/useMaster';
const schema = z.object({
name: z.string().min(1, { message: "Category name is required" }),
description: z.string().min(1, { message: "Description is required" })
.max(255, { message: "Description cannot exceed 255 characters" }),
.max(255, { message: "Description cannot exceed 255 characters" }),
});
const CreateContactCategory = ({onClose}) => {
const CreateContactCategory = ({ onClose }) => {
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: "",
description: "",
},
});
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: "",
description: "",
},
});
const [descriptionLength, setDescriptionLength] = useState(0);
const maxDescriptionLength = 255;
const [descriptionLength, setDescriptionLength] = useState(0);
const maxDescriptionLength = 255;
const { mutate: createContactCategory, isPending: isLoading } = useCreateContactCategory(() => onClose?.());
const { mutate: createContactCategory, isPending: isLoading } = useCreateContactCategory(() => onClose?.());
const onSubmit = (payload) => {
createContactCategory(payload);
};
const onSubmit = (payload) => {
createContactCategory(payload);
};
// const onSubmit = (data) => {
// setIsLoading(true)
// MasterRespository.createContactCategory(data).then((resp)=>{
@ -54,27 +54,27 @@ const onSubmit = (payload) => {
// setIsLoading(false)
// })
// };
const resetForm = () => {
reset({ name: "", description: "" });
setDescriptionLength(0);
};
const resetForm = () => {
reset({ name: "", description: "" });
setDescriptionLength(0);
};
useEffect(() => {
return () => resetForm();
}, []);
useEffect(() => {
return () => resetForm();
}, []);
return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12">
<label className="form-label">Category Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12">
<label className="form-label">Category Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<textarea
rows="3"
{...register("description")}
@ -87,28 +87,31 @@ useEffect(() => {
<div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading? "Please Wait...":"Submit"}
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading ? "Please Wait..." : "Submit"}
</button>
<button
type="reset"
className="btn btn-sm btn-label-secondary "
data-bs-dismiss="modal"
aria-label="Close"
type="button" // not reset
className="btn btn-sm btn-label-secondary"
onClick={() => {
resetForm(); // clear inputs
onClose?.(); // close modal from parent
}}
>
Cancel
</button>
</div>
</form>
</>
</>
)
}

View File

@ -1,42 +1,42 @@
import React, { useEffect,useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager';
import { getCachedData, cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService';
import {useCreateContactTag} from '../../hooks/masterHook/useMaster';
import { useCreateContactTag } from '../../hooks/masterHook/useMaster';
const schema = z.object({
name: z.string().min(1, { message: "Tag name is required" }),
description: z.string().min(1, { message: "Description is required" })
.max(255, { message: "Description cannot exceed 255 characters" }),
.max(255, { message: "Description cannot exceed 255 characters" }),
});
const CreateContactTag = ({onClose}) => {
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: "",
description: "",
},
});
const CreateContactTag = ({ onClose }) => {
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: "",
description: "",
},
});
const [descriptionLength, setDescriptionLength] = useState(0);
const maxDescriptionLength = 255;
const [descriptionLength, setDescriptionLength] = useState(0);
const maxDescriptionLength = 255;
const { mutate: createContactTag, isPending: isLoading } = useCreateContactTag(() => onClose?.());
const { mutate: createContactTag, isPending: isLoading } = useCreateContactTag(() => onClose?.());
const onSubmit = (payload) => {
createContactTag(payload);
};
const onSubmit = (payload) => {
createContactTag(payload);
};
// const onSubmit = (data) => {
// setIsLoading(true)
// MasterRespository.createContactTag(data).then((resp)=>{
@ -54,27 +54,27 @@ const onSubmit = (payload) => {
// setIsLoading(false)
// })
// };
const resetForm = () => {
reset({ name: "", description: "" });
setDescriptionLength(0);
};
const resetForm = () => {
reset({ name: "", description: "" });
setDescriptionLength(0);
};
useEffect(() => {
return () => resetForm();
}, []);
useEffect(() => {
return () => resetForm();
}, []);
return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12">
<label className="form-label">Tag Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12">
<label className="form-label">Tag Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<textarea
rows="3"
{...register("description")}
@ -87,28 +87,31 @@ useEffect(() => {
<div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading? "Please Wait...":"Submit"}
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading ? "Please Wait..." : "Submit"}
</button>
<button
type="reset"
className="btn btn-sm btn-label-secondary "
data-bs-dismiss="modal"
aria-label="Close"
type="button" // not reset
className="btn btn-sm btn-label-secondary"
onClick={() => {
resetForm(); // clear inputs
onClose?.(); // close modal from parent
}}
>
Cancel
</button>
</div>
</form>
</>
</>
)
}

View File

@ -1,21 +1,21 @@
import React, { useEffect,useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager';
import { getCachedData, cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService';
import {useCreateJobRole} from '../../hooks/masterHook/useMaster';
import { useCreateJobRole } from '../../hooks/masterHook/useMaster';
const schema = z.object({
role: z.string().min(1, { message: "Role is required" }),
description: z.string().min(1, { message: "Description is required" })
.max(255, { message: "Description cannot exceed 255 characters" }),
.max(255, { message: "Description cannot exceed 255 characters" }),
});
const CreateJobRole = ({onClose}) => {
const CreateJobRole = ({ onClose }) => {
const maxDescriptionLength = 255;
const [descriptionLength, setDescriptionLength] = useState(0);
@ -39,34 +39,34 @@ const CreateJobRole = ({onClose}) => {
setDescriptionLength(0);
};
const { mutate: createJobRole, isPending:isLoading } = useCreateJobRole(() => {
const { mutate: createJobRole, isPending: isLoading } = useCreateJobRole(() => {
onClose?.();
} );
});
// const onSubmit = (data) => {
// setIsLoading(true)
// const result = {
// name: data.role,
// description: data.description,
// };
// MasterRespository.createJobRole(result).then((resp)=>{
// setIsLoading(false)
// resetForm()
// const cachedData = getCachedData("Job Role");
// const updatedData = [...cachedData, resp?.data];
// cacheData("Job Role", updatedData);
// showToast("JobRole Added successfully.", "success");
// const onSubmit = (data) => {
// setIsLoading(true)
// const result = {
// name: data.role,
// description: data.description,
// };
// onClose()
// }).catch((error)=>{
// showToast(error.message, "error");
// setIsLoading(false)
// })
// };
// MasterRespository.createJobRole(result).then((resp)=>{
// setIsLoading(false)
// resetForm()
// const cachedData = getCachedData("Job Role");
// const updatedData = [...cachedData, resp?.data];
// cacheData("Job Role", updatedData);
// showToast("JobRole Added successfully.", "success");
// onClose()
// }).catch((error)=>{
// showToast(error.message, "error");
// setIsLoading(false)
// })
// };
const onSubmit = (data) => {
const payload = {
name: data.role,
@ -87,20 +87,20 @@ const CreateJobRole = ({onClose}) => {
}, []);
return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
{/* <div className="col-12 col-md-12">
<label className="fs-5 text-dark text-center d-flex align-items-center justify-content-center flex-wrap">Create Job Role</label>
</div> */}
<div className="col-12 col-md-12">
<label className="form-label">Role</label>
<input type="text"
{...register("role")}
className={`form-control ${errors.role ? 'is-invalids' : ''}`}
/>
{errors.role && <p className="text-danger">{errors.role.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<div className="col-12 col-md-12">
<label className="form-label">Role</label>
<input type="text"
{...register("role")}
className={`form-control ${errors.role ? 'is-invalids' : ''}`}
/>
{errors.role && <p className="text-danger">{errors.role.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<textarea
rows="3"
{...register("description")}
@ -113,28 +113,31 @@ const CreateJobRole = ({onClose}) => {
<div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading? "Please Wait...":"Submit"}
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading ? "Please Wait..." : "Submit"}
</button>
<button
type="reset"
className="btn btn-sm btn-label-secondary "
data-bs-dismiss="modal"
aria-label="Close"
type="button" // change from reset button
className="btn btn-sm btn-label-secondary"
onClick={() => {
resetForm(); // optional: clears form
onClose?.(); // close modal via parent
}}
>
Cancel
</button>
</div>
</form>
</>
</>
)
}

View File

@ -232,13 +232,13 @@ const CreateRole = ({ modalType, onClose }) => {
{isLoading ? "Please Wait..." : "Submit"}
</button>
<button
type="reset"
type="button"
className="btn btn-sm btn-label-secondary"
data-bs-dismiss="modal"
aria-label="Close"
onClick={onClose}
>
Cancel
</button>
</div>
)}
</form>

View File

@ -1,96 +1,96 @@
import React, { useEffect,useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager';
import { getCachedData, cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService';
import {useCreateWorkCategory} from '../../hooks/masterHook/useMaster';
import { useCreateWorkCategory } from '../../hooks/masterHook/useMaster';
const schema = z.object({
name: z.string().min(1, { message: "Category name is required" }),
description: z.string().min(1, { message: "Description is required" })
.max(255, { message: "Description cannot exceed 255 characters" }),
.max(255, { message: "Description cannot exceed 255 characters" }),
});
const CreateWorkCategory = ({onClose}) => {
const CreateWorkCategory = ({ onClose }) => {
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: "",
description: "",
},
});
const [descriptionLength, setDescriptionLength] = useState(0);
const maxDescriptionLength = 255;
const { mutate: createWorkCategory, isPending: isLoading } = useCreateWorkCategory(() => {
resetForm();
onClose?.();
});
const onSubmit = (payload) => {
createWorkCategory(payload)
};
// const onSubmit = (data) => {
// setIsLoading(true)
// MasterRespository.createWorkCategory(data).then((resp)=>{
// setIsLoading(false)
// resetForm()
// const cachedData = getCachedData("Work Category");
// const updatedData = [...cachedData, resp?.data];
// cacheData("Work Category", updatedData);
// showToast("Work Category Added successfully.", "success");
// onClose()
// }).catch((error)=>{
// showToast(error?.response?.data?.message, "error");
// setIsLoading(false)
// })
// };
const resetForm = () => {
reset({
name: "",
description: "",
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: "",
description: "",
},
});
setDescriptionLength(0);
};
useEffect(() => {
return () => resetForm();
}, []);
const [descriptionLength, setDescriptionLength] = useState(0);
const maxDescriptionLength = 255;
const { mutate: createWorkCategory, isPending: isLoading } = useCreateWorkCategory(() => {
resetForm();
onClose?.();
});
const onSubmit = (payload) => {
createWorkCategory(payload)
};
// const onSubmit = (data) => {
// setIsLoading(true)
// MasterRespository.createWorkCategory(data).then((resp)=>{
// setIsLoading(false)
// resetForm()
// const cachedData = getCachedData("Work Category");
// const updatedData = [...cachedData, resp?.data];
// cacheData("Work Category", updatedData);
// showToast("Work Category Added successfully.", "success");
// onClose()
// }).catch((error)=>{
// showToast(error?.response?.data?.message, "error");
// setIsLoading(false)
// })
// };
const resetForm = () => {
reset({
name: "",
description: "",
});
setDescriptionLength(0);
};
useEffect(() => {
return () => resetForm();
}, []);
return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
{/* <div className="col-12 col-md-12">
<label className="fs-5 text-dark text-center d-flex align-items-center justify-content-center flex-wrap">Create Work Category</label>
</div> */}
<div className="col-12 col-md-12">
<label className="fs-5 text-dark text-center d-flex align-items-center justify-content-center flex-wrap">Create Work Category</label>
<label className="form-label">Category Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label">Category Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<textarea
rows="3"
{...register("description")}
@ -103,28 +103,30 @@ useEffect(() => {
<div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading? "Please Wait...":"Submit"}
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading ? "Please Wait..." : "Submit"}
</button>
<button
type="reset"
className="btn btn-sm btn-label-secondary "
data-bs-dismiss="modal"
aria-label="Close"
type="button" // not reset
className="btn btn-sm btn-label-secondary"
onClick={() => {
resetForm(); // clear inputs
onClose?.(); // close modal from parent
}}
>
Cancel
</button>
</div>
</form>
</>
</>
)
}

View File

@ -34,13 +34,15 @@ const DeleteMaster = ({ master, onClose }) => {
)}
</button>
<button
type="reset"
type="button" // not reset
className="btn btn-label-secondary"
data-bs-dismiss="modal"
aria-label="Close"
onClick={() => {
onClose?.(); // properly close modal
}}
>
Cancel
</button>
</div>
</div>
);

View File

@ -241,7 +241,7 @@ useEffect(() => {
{isLoading ? "Please Wait" : "Submit"}
</button>
<button
type="button"
type="button" // change to button
className="btn btn-sm btn-label-secondary"
onClick={onClose}
>

View File

@ -1,49 +1,49 @@
import React, { useEffect,useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager';
import { getCachedData, cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService';
import {useUpdateContactCategory} from '../../hooks/masterHook/useMaster';
import { useUpdateContactCategory } from '../../hooks/masterHook/useMaster';
const schema = z.object({
name: z.string().min(1, { message: "Category name is required" }),
description: z.string().min(1, { message: "Description is required" })
.max(255, { message: "Description cannot exceed 255 characters" }),
.max(255, { message: "Description cannot exceed 255 characters" }),
});
const EditContactCategory= ({data,onClose}) => {
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: data?.name || "",
description: data?.description || "",
},
});
const EditContactCategory = ({ data, onClose }) => {
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: data?.name || "",
description: data?.description || "",
},
});
const [descriptionLength, setDescriptionLength] = useState(data?.description?.length || 0);
const maxDescriptionLength = 255;
const [descriptionLength, setDescriptionLength] = useState(data?.description?.length || 0);
const maxDescriptionLength = 255;
const { mutate: updateContactCategory, isPending: isLoading } = useUpdateContactCategory(() => onClose?.());
const { mutate: updateContactCategory, isPending: isLoading } = useUpdateContactCategory(() => onClose?.());
const onSubmit = (formData) => {
const payload = {
id: data?.id,
name: formData.name,
description: formData.description,
const onSubmit = (formData) => {
const payload = {
id: data?.id,
name: formData.name,
description: formData.description,
};
updateContactCategory({ id: data?.id, payload });
};
updateContactCategory({id:data?.id,payload});
};
// const onSubmit = (formdata) => {
// const onSubmit = (formdata) => {
// setIsLoading(true)
// const result = {
// id:data?.id,
@ -51,8 +51,8 @@ const onSubmit = (formData) => {
// description: formdata.description,
// };
// MasterRespository.updateContactCategory(data?.id,result).then((resp)=>{
// setIsLoading(false)
// showToast("Contact Category Updated successfully.", "success");
@ -70,30 +70,30 @@ const onSubmit = (formData) => {
// showToast(error?.response?.data?.message, "error")
// setIsLoading(false)
// })
// };
const resetForm = () => {
reset({ name: "", description: "" });
setDescriptionLength(0);
};
const resetForm = () => {
reset({ name: "", description: "" });
setDescriptionLength(0);
};
useEffect(() => {
return () => resetForm();
}, []);
useEffect(() => {
return () => resetForm();
}, []);
return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12">
<label className="form-label">Category Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12">
<label className="form-label">Category Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<textarea
rows="3"
{...register("description")}
@ -106,28 +106,30 @@ useEffect(() => {
<div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading? "Please Wait...":"Submit"}
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading ? "Please Wait..." : "Submit"}
</button>
<button
type="button"
className="btn btn-sm btn-label-secondary "
data-bs-dismiss="modal"
aria-label="Close"
type="button" // not reset
className="btn btn-sm btn-label-secondary"
onClick={() => {
resetForm(); // clear inputs
onClose?.(); // close modal from parent
}}
>
Cancel
</button>
</div>
</form>
</>
</>
)
}

View File

@ -1,49 +1,49 @@
import React,{useState,useEffect} from 'react'
import {useForm} from 'react-hook-form';
import React, { useState, useEffect } from 'react'
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager';
import { getCachedData, cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService';
import {useUpdateContactTag} from '../../hooks/masterHook/useMaster';
import { useUpdateContactTag } from '../../hooks/masterHook/useMaster';
const schema = z.object({
name: z.string().min(1, { message: "Tag name is required" }),
description: z.string().min(1, { message: "Description is required" })
.max(255, { message: "Description cannot exceed 255 characters" }),
.max(255, { message: "Description cannot exceed 255 characters" }),
});
const EditContactTag= ({data,onClose}) => {
const {
register,
handleSubmit,
formState: { errors },
reset
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: data?.name || "",
description: data?.description || "",
},
});
const EditContactTag = ({ data, onClose }) => {
const {
register,
handleSubmit,
formState: { errors },
reset
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: data?.name || "",
description: data?.description || "",
},
});
const [descriptionLength, setDescriptionLength] = useState(data?.description?.length || 0);
const maxDescriptionLength = 255;
const [descriptionLength, setDescriptionLength] = useState(data?.description?.length || 0);
const maxDescriptionLength = 255;
const { mutate: updateContactTag, isPending: isLoading } = useUpdateContactTag(() => onClose?.());
const { mutate: updateContactTag, isPending: isLoading } = useUpdateContactTag(() => onClose?.());
const onSubmit = (formData) => {
const payload = {
id: data?.id,
name: formData.name,
description: formData.description,
};
debugger
updateContactTag({ id: data?.id, payload} );
const onSubmit = (formData) => {
const payload = {
id: data?.id,
name: formData.name,
description: formData.description,
};
debugger
updateContactTag({ id: data?.id, payload });
}
// const onSubmit = (formdata) => {
// const onSubmit = (formdata) => {
// setIsLoading(true)
// const result = {
// id:data?.id,
@ -51,8 +51,8 @@ const onSubmit = (formData) => {
// description: formdata.description,
// };
// MasterRespository.updateContactTag(data?.id,result).then((resp)=>{
// setIsLoading(false)
// showToast("Contact Tag Updated successfully.", "success");
@ -70,30 +70,30 @@ const onSubmit = (formData) => {
// showToast(error?.response?.data?.message, "error")
// setIsLoading(false)
// })
// };
const resetForm = () => {
reset({ name: "", description: "" });
setDescriptionLength(0);
};
const resetForm = () => {
reset({ name: "", description: "" });
setDescriptionLength(0);
};
useEffect(() => {
return () => resetForm();
}, []);
useEffect(() => {
return () => resetForm();
}, []);
return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12">
<label className="form-label">Tag Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12">
<label className="form-label">Tag Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<textarea
rows="3"
{...register("description")}
@ -106,28 +106,30 @@ useEffect(() => {
<div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading? "Please Wait...":"Submit"}
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading ? "Please Wait..." : "Submit"}
</button>
<button
type="button"
className="btn btn-sm btn-label-secondary "
data-bs-dismiss="modal"
aria-label="Close"
type="button" // not reset
className="btn btn-sm btn-label-secondary"
onClick={() => {
resetForm(); // clear inputs
onClose?.(); // close modal from parent
}}
>
Cancel
</button>
</div>
</form>
</>
</>
)
}

View File

@ -1,24 +1,24 @@
import React, { useEffect,useState } from 'react'
import { useForm ,Controller} from 'react-hook-form';
import React, { useEffect, useState } from 'react'
import { useForm, Controller } from 'react-hook-form';
import { set, z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository';
import { cacheData,getCachedData } from '../../slices/apiDataManager';
import { cacheData, getCachedData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService';
import {useUpdateJobRole} from '../../hooks/masterHook/useMaster';
import { useUpdateJobRole } from '../../hooks/masterHook/useMaster';
const schema = z.object({
role: z.string().min(1, { message: "Role is required" }),
description: z.string().min(1, { message: "Description is required" })
.max(255, { message: "Description cannot exceed 255 characters" }),
.max(255, { message: "Description cannot exceed 255 characters" }),
});
const EditJobRole = ({data,onClose}) => {
const [descriptionLength, setDescriptionLength] = useState(data?.description?.length || 0);
const EditJobRole = ({ data, onClose }) => {
const [descriptionLength, setDescriptionLength] = useState(data?.description?.length || 0);
const maxDescriptionLength = 255;
const {
@ -35,38 +35,38 @@ const [descriptionLength, setDescriptionLength] = useState(data?.description?.le
},
});
const { mutate: updateJobRole, isPendin:isLoading } = useUpdateJobRole(() => {
const { mutate: updateJobRole, isPendin: isLoading } = useUpdateJobRole(() => {
onClose?.();
});
// const onSubmit = (formdata) => {
// setIsLoading(true)
// const result = {
// id:data?.id,
// name: formdata?.role,
// description: formdata.description,
// };
// const onSubmit = (formdata) => {
// setIsLoading(true)
// const result = {
// id:data?.id,
// name: formdata?.role,
// description: formdata.description,
// };
// MasterRespository.updateJobRole(data?.id,result).then((resp)=>{
// setIsLoading(false)
// showToast("JobRole Update successfully.", "success");
// const cachedData = getCachedData("Job Role");
// if (cachedData) {
// const updatedData = cachedData.map((role) =>
// role.id === data?.id ? { ...role, ...resp.data } : role
// );
// cacheData("Job Role", updatedData);
// }
// onClose()
// }).catch((error)=>{
// showToast(error.message, "error")
// setIsLoading(false)
// })
// };
// MasterRespository.updateJobRole(data?.id,result).then((resp)=>{
// setIsLoading(false)
// showToast("JobRole Update successfully.", "success");
// const cachedData = getCachedData("Job Role");
// if (cachedData) {
// const updatedData = cachedData.map((role) =>
// role.id === data?.id ? { ...role, ...resp.data } : role
// );
// cacheData("Job Role", updatedData);
// }
// onClose()
// }).catch((error)=>{
// showToast(error.message, "error")
// setIsLoading(false)
// })
// };
const onSubmit = (formData) => {
updateJobRole({
@ -93,22 +93,22 @@ const [descriptionLength, setDescriptionLength] = useState(data?.description?.le
});
return () => sub.unsubscribe();
}, [watch]);
return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
{/* <div className="col-12 col-md-12">
<label className="fs-5 text-dark text-center d-flex align-items-center justify-content-center flex-wrap">Edit Job Role</label>
</div> */}
<div className="col-12 col-md-12">
<label className="form-label">Role</label>
<input type="text"
{...register("role")}
className={`form-control ${errors.role ? 'is-invalids' : ''}`}
/>
{errors.role && <p className="text-danger">{errors.role.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<div className="col-12 col-md-12">
<label className="form-label">Role</label>
<input type="text"
{...register("role")}
className={`form-control ${errors.role ? 'is-invalids' : ''}`}
/>
{errors.role && <p className="text-danger">{errors.role.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<textarea
rows="3"
{...register("description")}
@ -125,25 +125,25 @@ const [descriptionLength, setDescriptionLength] = useState(data?.description?.le
<p className="text-danger">{errors.description.message}</p>
)}
</div>
</div>
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading? "Please Wait...":"Submit"}
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading ? "Please Wait..." : "Submit"}
</button>
<button
type="button"
className="btn btn-sm btn-label-secondary"
data-bs-dismiss="modal"
aria-label="Close"
onClick={onClose} // 👈 This will now close the popup
>
Cancel
</button>
</div>
</form>
</>
</>
)
}

View File

@ -190,87 +190,87 @@ const EditMaster = ({ master, onClose }) => {
)}
</div>
<div className="col-12 text-start">
{/* Scrollable Container with Border */}
<div
className="border rounded p-3"
style={{
maxHeight: "350px",
overflowY: "auto",
overflowX: "hidden", // Prevent horizontal scrollbar
paddingRight: "10px",
}}
>
{masterFeatures.map((feature, featureIndex) => (
<div key={feature.id} className="mb-3">
{/* Feature Group Title */}
<div className="fw-semibold mb-2">{feature.name}</div>
<div className="col-12 text-start">
{/* Scrollable Container with Border */}
<div
className="border rounded p-3"
style={{
maxHeight: "350px",
overflowY: "auto",
overflowX: "hidden", // Prevent horizontal scrollbar
paddingRight: "10px",
}}
>
{masterFeatures.map((feature, featureIndex) => (
<div key={feature.id} className="mb-3">
{/* Feature Group Title */}
<div className="fw-semibold mb-2">{feature.name}</div>
{/* Permissions Grid */}
<div className="row">
{feature.featurePermissions.map((perm, permIndex) => {
const refIndex = featureIndex * 10 + permIndex;
return (
<div
key={perm.id}
className="col-12 col-sm-6 col-md-4 mb-3 d-flex align-items-start"
>
<label
className="form-check-label d-flex align-items-center"
htmlFor={perm.id}
>
<input
type="checkbox"
className="form-check-input me-2"
id={perm.id}
{...register(`permissions.${perm.id}`, {
value: initialPermissions[perm.id] || false,
})}
/>
{perm.name}
</label>
{/* Info Icon */}
<div style={{ display: "flex", alignItems: "center" }}>
<div
ref={(el) => (popoverRefs.current[refIndex] = el)}
tabIndex="0"
className="d-flex align-items-center justify-content-center"
data-bs-toggle="popover"
data-bs-trigger="focus"
data-bs-placement="right"
data-bs-html="true"
data-bs-content={`<div class="border border-secondary rounded custom-popover p-2 px-3">${perm.description}</div>`}
>
&nbsp;
<svg
xmlns="http://www.w3.org/2000/svg"
width="13"
height="13"
fill="currentColor"
className="bi bi-info-circle"
viewBox="0 0 16 16"
{/* Permissions Grid */}
<div className="row">
{feature.featurePermissions.map((perm, permIndex) => {
const refIndex = featureIndex * 10 + permIndex;
return (
<div
key={perm.id}
className="col-12 col-sm-6 col-md-4 mb-3 d-flex align-items-start"
>
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.547 1.11l1.91-2.011c.241-.256.384-.592.287-.984-.172-.439-.58-.827-1.13-.967a.664.664 0 0 1-.58-.309l-.15-.241-.002-.002zM8 4c-.535 0-.943.372-.943.836 0 .464.408.836.943.836.535 0 .943-.372.943-.836 0-.464-.408-.836-.943-.836z" />
</svg>
</div>
</div>
<label
className="form-check-label d-flex align-items-center"
htmlFor={perm.id}
>
<input
type="checkbox"
className="form-check-input me-2"
id={perm.id}
{...register(`permissions.${perm.id}`, {
value: initialPermissions[perm.id] || false,
})}
/>
{perm.name}
</label>
{/* Info Icon */}
<div style={{ display: "flex", alignItems: "center" }}>
<div
ref={(el) => (popoverRefs.current[refIndex] = el)}
tabIndex="0"
className="d-flex align-items-center justify-content-center"
data-bs-toggle="popover"
data-bs-trigger="focus"
data-bs-placement="right"
data-bs-html="true"
data-bs-content={`<div class="border border-secondary rounded custom-popover p-2 px-3">${perm.description}</div>`}
>
&nbsp;
<svg
xmlns="http://www.w3.org/2000/svg"
width="13"
height="13"
fill="currentColor"
className="bi bi-info-circle"
viewBox="0 0 16 16"
>
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.547 1.11l1.91-2.011c.241-.256.384-.592.287-.984-.172-.439-.58-.827-1.13-.967a.664.664 0 0 1-.58-.309l-.15-.241-.002-.002zM8 4c-.535 0-.943.372-.943.836 0 .464.408.836.943.836.535 0 .943-.372.943-.836 0-.464-.408-.836-.943-.836z" />
</svg>
</div>
</div>
</div>
);
})}
</div>
);
})}
<hr className="my-2" />
</div>
))}
</div>
<hr className="my-2" />
{/* Error Display */}
{errors.permissions && (
<p className="text-danger">{errors.permissions.message}</p>
)}
</div>
))}
</div>
{/* Error Display */}
{errors.permissions && (
<p className="text-danger">{errors.permissions.message}</p>
)}
</div>
@ -280,11 +280,11 @@ const EditMaster = ({ master, onClose }) => {
<button
type="button"
className="btn btn-sm btn-label-secondary"
data-bs-dismiss="modal"
aria-label="Close"
onClick={onClose}
>
Cancel
</button>
</div>
</form>

View File

@ -1,103 +1,103 @@
import React, { useEffect,useState } from 'react'
import { useForm ,Controller} from 'react-hook-form';
import React, { useEffect, useState } from 'react'
import { useForm, Controller } from 'react-hook-form';
import { set, z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository';
import { cacheData,getCachedData } from '../../slices/apiDataManager';
import { cacheData, getCachedData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService';
import {useUpdateWorkCategory} from '../../hooks/masterHook/useMaster';
import { useUpdateWorkCategory } from '../../hooks/masterHook/useMaster';
const schema = z.object({
name: z.string().min(1, { message: "Work Category is required" }),
description: z.string().min(1, { message: "Description is required" })
.max(255, { message: "Description cannot exceed 255 characters" }),
.max(255, { message: "Description cannot exceed 255 characters" }),
});
const EditWorkCategory = ({data,onClose}) => {
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: data?.name || "",
description: data?.description || "",
},
});
const EditWorkCategory = ({ data, onClose }) => {
const [descriptionLength, setDescriptionLength] = useState(data?.description?.length || 0);
const maxDescriptionLength = 255;
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: data?.name || "",
description: data?.description || "",
},
});
const { mutate: updateWorkCategory, isPending: isLoading } = useUpdateWorkCategory(() => onClose?.());
const [descriptionLength, setDescriptionLength] = useState(data?.description?.length || 0);
const maxDescriptionLength = 255;
const onSubmit = (formdata) => {
const payload = {
id: data?.id,
name: formdata.name,
description: formdata.description,
const { mutate: updateWorkCategory, isPending: isLoading } = useUpdateWorkCategory(() => onClose?.());
const onSubmit = (formdata) => {
const payload = {
id: data?.id,
name: formdata.name,
description: formdata.description,
};
updateWorkCategory({ id: data?.id, payload });
};
updateWorkCategory({id:data?.id,payload});
};
// const onSubmit = (formdata) => {
// setIsLoading(true)
// const result = {
// id:data?.id,
// name: formdata?.name,
// description: formdata.description,
// };
// MasterRespository.updateWorkCategory(data?.id,result).then((resp)=>{
// setIsLoading(false)
// showToast("Work Category Update successfully.", "success");
// const cachedData = getCachedData("Work Category");
// if (cachedData) {
// const updatedData = cachedData.map((category) =>
// category.id === data?.id ? { ...category, ...resp.data } : category
// );
// cacheData("Work Category", updatedData);
// }
// onClose()
// }).catch((error)=>{
// showToast(error?.response?.data?.message, "error")
// setIsLoading(false)
// })
// };
useEffect(() => {
reset({
name: data?.name || "",
description: data?.description || "",
});
setDescriptionLength(data?.description?.length || 0);
}, [data, reset]);
// const onSubmit = (formdata) => {
// setIsLoading(true)
// const result = {
// id:data?.id,
// name: formdata?.name,
// description: formdata.description,
// };
// MasterRespository.updateWorkCategory(data?.id,result).then((resp)=>{
// setIsLoading(false)
// showToast("Work Category Update successfully.", "success");
// const cachedData = getCachedData("Work Category");
// if (cachedData) {
// const updatedData = cachedData.map((category) =>
// category.id === data?.id ? { ...category, ...resp.data } : category
// );
// cacheData("Work Category", updatedData);
// }
// onClose()
// }).catch((error)=>{
// showToast(error?.response?.data?.message, "error")
// setIsLoading(false)
// })
// };
useEffect(() => {
reset({
name: data?.name || "",
description: data?.description || "",
});
setDescriptionLength(data?.description?.length || 0);
}, [data, reset]);
return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12">
<label className="form-label">Category Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12">
<label className="form-label">Category Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<textarea
rows="3"
{...register("description")}
@ -114,25 +114,25 @@ useEffect(() => {
<p className="text-danger">{errors.description.message}</p>
)}
</div>
</div>
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading? "Please Wait...":"Submit"}
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading ? "Please Wait..." : "Submit"}
</button>
<button
type="reset"
type="button"
className="btn btn-sm btn-label-secondary"
data-bs-dismiss="modal"
aria-label="Close"
onClick={onClose}
>
Cancel
</button>
</div>
</form>
</>
</>
)
}

View File

@ -17,31 +17,31 @@ const ManagePaymentMode = ({ data = null, onClose }) => {
formState: { errors },
} = useForm({
resolver: zodResolver(ExpnseSchema),
defaultValues: { name: "", description: "" },
defaultValues: { name: "", description: "" },
});
const { mutate: CreatePaymentMode, isPending } = useCreatePaymentMode(() =>
onClose?.()
);
const {mutate:UpdatePaymentMode,isPending:Updating} = useUpdatePaymentMode(()=>onClose?.())
const { mutate: UpdatePaymentMode, isPending: Updating } = useUpdatePaymentMode(() => onClose?.())
const onSubmit = (payload) => {
if(data){
UpdatePaymentMode({id:data.id,payload:{...payload,id:data.id}})
}else(
CreatePaymentMode(payload)
if (data) {
UpdatePaymentMode({ id: data.id, payload: { ...payload, id: data.id } })
} else (
CreatePaymentMode(payload)
)
};
useEffect(()=>{
if(data){
useEffect(() => {
if (data) {
reset({
name:data.name ?? "",
description:data.description ?? ""
name: data.name ?? "",
description: data.description ?? ""
})
}
},[data])
}, [data])
return (
@ -76,17 +76,17 @@ const ManagePaymentMode = ({ data = null, onClose }) => {
className="btn btn-sm btn-primary me-3"
disabled={isPending || Updating}
>
{isPending || Updating? "Please Wait..." : Updating ? "Update" : "Submit"}
{isPending || Updating ? "Please Wait..." : Updating ? "Update" : "Submit"}
</button>
<button
type="reset"
className="btn btn-sm btn-label-secondary "
data-bs-dismiss="modal"
aria-label="Close"
type="button"
className="btn btn-sm btn-label-secondary"
onClick={onClose} // call onClose here
disabled={isPending || Updating}
>
Cancel
</button>
</div>
</form>
);

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { cacheData, getCachedData, useSelectedproject } from "../slices/apiDataManager";
import { cacheData, getCachedData, useSelectedProject } from "../slices/apiDataManager";
import AttendanceRepository from "../repositories/AttendanceRepository";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import showToast from "../services/toastService";
@ -143,8 +143,7 @@ export const useRegularizationRequests = (projectId) => {
export const useMarkAttendance = () => {
const queryClient = useQueryClient();
// const selectedProject = useSelector((store)=>store.localVariables.projectId)
const selectedProject = useSelectedproject();
const selectedProject = useSelectedProject();
const selectedDateRange = useSelector((store)=>store.localVariables.defaultDateRange)
return useMutation({

View File

@ -17,13 +17,13 @@ const cleanFilter = (filter) => {
});
// moment.utc() to get consistent UTC ISO strings
if (!cleaned.startDate) {
cleaned.startDate = moment.utc().subtract(7, "days").startOf("day").toISOString();
}
// if (!cleaned.startDate) {
// cleaned.startDate = moment.utc().subtract(7, "days").startOf("day").toISOString();
// }
if (!cleaned.endDate) {
cleaned.endDate = moment.utc().startOf("day").toISOString();
}
// if (!cleaned.endDate) {
// cleaned.endDate = moment.utc().startOf("day").toISOString();
// }
return cleaned;
};

View File

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

View File

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

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

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

View File

@ -4,7 +4,7 @@ import {
clearCacheKey,
getCachedData,
getCachedProfileData,
useSelectedproject,
useSelectedProject,
} from "../../slices/apiDataManager";
import Breadcrumb from "../../components/common/Breadcrumb";
import AttendanceLog from "../../components/Activities/AttendcesLogs";
@ -26,11 +26,11 @@ import { useQueryClient } from "@tanstack/react-query";
const AttendancePage = () => {
const [activeTab, setActiveTab] = useState("all");
const [ShowPending, setShowPending] = useState(false);
const [searchTerm, setSearchTerm] = useState(""); // 🔹 New state for search
const [searchTerm, setSearchTerm] = useState("");
const queryClient = useQueryClient();
const loginUser = getCachedProfileData();
// const selectedProject = useSelector((store) => store.localVariables.projectId);
const selectedProject = useSelectedproject();
const selectedProject = useSelectedProject();
const dispatch = useDispatch();
const [attendances, setAttendances] = useState();

View File

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

View File

@ -1,38 +1,37 @@
import React,{useEffect} from "react";
import React,{useEffect,useRef} from "react";
import Breadcrumb from "../../components/common/Breadcrumb";
import InfraPlanning from "../../components/Activities/InfraPlanning";
import { useProjectName } from "../../hooks/useProjects";
import { useDispatch, useSelector } from "react-redux";
import { setProjectId } from "../../slices/localVariablesSlice";
import { useSelectedproject } from "../../slices/apiDataManager";
import { useSelectedProject } from "../../slices/apiDataManager";
const TaskPlannng = () => {
// const selectedProject = useSelector(
// (store) => store.localVariables.projectId
// );
const selectedProject = useSelectedproject();
const selectedProject = useSelectedProject();
const dispatch = useDispatch();
const { projectNames = [], loading: projectLoading } = useProjectName();
useEffect(() => {
if (!selectedProject) {
dispatch(setProjectId(projectNames[0].id));
}
}, [projectNames, selectedProject?.id, dispatch]);
const dispatch = useDispatch()
const { projectNames, loading: projectLoading, fetchData } = useProjectName();
useEffect(() => {
if(selectedProject == null){
dispatch(setProjectId(projectNames[0]?.id));
}
},[])
return (
<>
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Daily Task Planning" }
]}
></Breadcrumb>
<InfraPlanning/>
</div>
</>
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Daily Task Planning" },
]}
/>
{selectedProject ? (
<InfraPlanning />
) : (
<div className="text-center">Please Select Project</div>
)}
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ import {
VIEW_ALL_EMPLOYEES,
VIEW_TEAM_MEMBERS,
} from "../../utils/constants";
import { clearCacheKey, useSelectedproject } from "../../slices/apiDataManager";
import { clearCacheKey, useSelectedProject } from "../../slices/apiDataManager";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import SuspendEmp from "../../components/Employee/SuspendEmp"; // Keep if you use SuspendEmp
import {
@ -41,7 +41,7 @@ const EmployeeList = () => {
// const selectedProjectId = useSelector(
// (store) => store.localVariables.projectId
// );
const selectedProjectId = useSelectedproject();
const selectedProjectId = useSelectedProject();
const { projectNames, loading: projectLoading, fetchData } = useProjectName();
const dispatch = useDispatch();

View File

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

View File

@ -13,7 +13,7 @@ import {
cacheData,
clearCacheKey,
getCachedData,
useSelectedproject,
useSelectedProject,
} from "../../slices/apiDataManager";
import "./ProjectDetails.css";
import {
@ -29,7 +29,7 @@ import { setProjectId } from "../../slices/localVariablesSlice";
const ProjectDetails = () => {
const projectId = useSelectedproject()
const projectId = useSelectedProject()
const { projectNames, fetchData } = useProjectName();
const dispatch = useDispatch()
@ -137,7 +137,6 @@ const ProjectDetails = () => {
<div className="row">
<ProjectNav onPillClick={handlePillClick} activePill={activePill} />
</div>
{renderContent()}
</div>
);

View File

@ -15,6 +15,8 @@ const AuthRepository = {
logout: (data) => api.post("/api/auth/logout", data),
profile: () => api.get("/api/user/profile"),
changepassword: (data) => api.post("/api/auth/change-password", data),
appmenu:()=>api.get('/api/appmenu/get/menu')
};
export default AuthRepository;

View File

@ -34,6 +34,8 @@ export const DirectoryRepository = {
DeleteNote: (id, isActive) =>
api.delete(`/api/directory/note/${id}?active=${isActive}`),
GetNotes: (pageSize, pageNumber) =>
api.get(`/api/directory/notes?pageSize=${pageSize}&pageNumber=${pageNumber}`),
GetNotes: (pageSize, pageNumber, projectId) =>
api.get(
`/api/directory/notes?pageSize=${pageSize}&pageNumber=${pageNumber}&projectId=${projectId}`
),
};

View File

@ -1,6 +1,6 @@
import { api } from "../utils/axiosClient";
export const MarketRepository = {
requestDemo: (data) => api.post("/api/market/inquiry", data),
requestDemo: (data) => api.post("/api/market/enquire", data),
getIndustries: () => api.get("api/market/industries"),
};

View File

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

View File

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

View File

@ -36,6 +36,8 @@ import LegalInfoCard from "../pages/TermsAndConditions/LegalInfoCard";
import ProtectedRoute from "./ProtectedRoute";
import Directory from "../pages/Directory/Directory";
import LoginWithOtp from "../pages/authentication/LoginWithOtp";
import TenantPage from "../pages/Tenant/TenantPage";
import CreateTenant from "../pages/Tenant/CreateTenant";
import ExpensePage from "../pages/Expense/ExpensePage";
import LandingPage from "../pages/Home/LandingPage";
import MainLogin from "../pages/authentication/MainLogin";
@ -83,6 +85,10 @@ const router = createBrowserRouter(
{ path: "/gallary", element: <ImageGallary /> },
{ path: "/expenses", element: <ExpensePage /> },
{ path: "/masters", element: <MasterPage /> },
{ path: "/tenants", element: <TenantPage /> },
{ path: "/tenants/new-tenant", element: <CreateTenant /> },
{ path: "/tenant/:tenantId", element: <SuperTenantDetails /> },
{ path: "/tenant/self", element: <SelfTenantDetails /> },
{ path: "/help/support", element: <Support /> },
{ path: "/help/docs", element: <Documentation /> },
{ path: "/help/connect", element: <Connect /> },

View File

@ -33,13 +33,12 @@ export const clearAllCache = () => {
export const cacheProfileData = ( data) => {
store.dispatch(setLoginUserPermmisions(data));
};
// Get cached data
export const getCachedProfileData = () => {
return store.getState().globalVariables.loginUser;
};
export const useSelectedproject = () => {
export const useSelectedProject = () => {
const selectedProject = useSelector((store)=> store.localVariables.projectId);
var project = localStorage.getItem("project");
if(project){
@ -47,7 +46,7 @@ export const useSelectedproject = () => {
} else{
return selectedProject
}
// return project ? selectedProject
};

View File

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

View File

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

View File

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