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

This commit is contained in:
pramod mahajan 2025-09-08 11:50:58 +05:30
commit ea022d37c8
84 changed files with 4516 additions and 1415 deletions

View File

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

View File

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

View File

@ -6,11 +6,12 @@ import RenderAttendanceStatus from "./RenderAttendanceStatus";
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
import { fetchAttendanceData } from "../../slices/apiSlice/attedanceLogsSlice"; import { fetchAttendanceData } from "../../slices/apiSlice/attedanceLogsSlice";
import DateRangePicker from "../common/DateRangePicker"; import DateRangePicker from "../common/DateRangePicker";
import { clearCacheKey, getCachedData, useSelectedproject } from "../../slices/apiDataManager"; import { clearCacheKey, getCachedData, useSelectedProject } from "../../slices/apiDataManager";
import eventBus from "../../services/eventBus"; import eventBus from "../../services/eventBus";
import AttendanceRepository from "../../repositories/AttendanceRepository"; import AttendanceRepository from "../../repositories/AttendanceRepository";
import { useAttendancesLogs } from "../../hooks/useAttendance"; import { useAttendancesLogs } from "../../hooks/useAttendance";
import { queryClient } from "../../layouts/AuthLayout"; import { queryClient } from "../../layouts/AuthLayout";
import { ITEMS_PER_PAGE } from "../../utils/constants";
const usePagination = (data, itemsPerPage) => { const usePagination = (data, itemsPerPage) => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@ -37,7 +38,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
// const selectedProject = useSelector( // const selectedProject = useSelector(
// (store) => store.localVariables.projectId // (store) => store.localVariables.projectId
// ); // );
const selectedProject = useSelectedproject(); const selectedProject = useSelectedProject();
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" }); const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
const dispatch = useDispatch(); const dispatch = useDispatch();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -353,7 +354,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
<span className="text-secondary">No Pending Record Available !</span> <span className="text-secondary">No Pending Record Available !</span>
</div> </div>
)} )}
{filteredSearchData.length > 10 && ( {filteredSearchData.length > ITEMS_PER_PAGE && (
<nav aria-label="Page "> <nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1"> <ul className="pagination pagination-sm justify-content-end py-1">
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}> <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 showToast from "../../services/toastService";
import { checkIfCurrentDate } from "../../utils/dateUtils"; import { checkIfCurrentDate } from "../../utils/dateUtils";
import { useMarkAttendance } from "../../hooks/useAttendance"; import { useMarkAttendance } from "../../hooks/useAttendance";
import { useSelectedproject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
const createSchema = (modeldata) => { const createSchema = (modeldata) => {
return z return z
@ -43,9 +43,8 @@ const createSchema = (modeldata) => {
}); });
}; };
const CheckCheckOutmodel = ({ modeldata, closeModal, handleSubmitForm }) => { const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
// const projectId = useSelector((store) => store.localVariables.projectId); const projectId = useSelectedProject();
const projectId = useSelectedproject();
const { mutate: MarkAttendance } = useMarkAttendance(); const { mutate: MarkAttendance } = useMarkAttendance();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const coords = usePositionTracker(); const coords = usePositionTracker();
@ -174,7 +173,7 @@ const CheckCheckOutmodel = ({ modeldata, closeModal, handleSubmitForm }) => {
); );
}; };
export default CheckCheckOutmodel; export default CheckInCheckOut;
const schemaReg = z.object({ const schemaReg = z.object({
description: z.string().min(1, { message: "please give reason!" }), description: z.string().min(1, { message: "please give reason!" }),

View File

@ -15,7 +15,7 @@ import {useDispatch, useSelector} from "react-redux";
import {useProfile} from "../../hooks/useProfile"; import {useProfile} from "../../hooks/useProfile";
import {refreshData, setProjectId} from "../../slices/localVariablesSlice"; import {refreshData, setProjectId} from "../../slices/localVariablesSlice";
import InfraTable from "../Project/Infrastructure/InfraTable"; import InfraTable from "../Project/Infrastructure/InfraTable";
import { useSelectedproject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
import Loader from "../common/Loader"; import Loader from "../common/Loader";
@ -24,7 +24,7 @@ const InfraPlanning = () =>
const {profile: LoggedUser, refetch : fetchData} = useProfile() const {profile: LoggedUser, refetch : fetchData} = useProfile()
const dispatch = useDispatch() const dispatch = useDispatch()
// const selectedProject = useSelector((store)=>store.localVariables.projectId) // const selectedProject = useSelector((store)=>store.localVariables.projectId)
const selectedProject = useSelectedproject(); const selectedProject = useSelectedProject();
const {projectInfra, isLoading, error} = useProjectInfra( selectedProject ) const {projectInfra, isLoading, error} = useProjectInfra( selectedProject )
@ -35,15 +35,15 @@ const InfraPlanning = () =>
const reloadedData = useSelector( ( store ) => store.localVariables.reload ) const reloadedData = useSelector( ( store ) => store.localVariables.reload )
useEffect( () => // useEffect( () =>
{ // {
if (reloadedData) // if (reloadedData)
{ // {
refetch() // refetch()
dispatch( refreshData( false ) ) // dispatch( refreshData( false ) )
} // }
},[reloadedData]) // },[reloadedData])
return ( return (
<div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4"> <div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4">

View File

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

View File

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

View File

@ -198,23 +198,14 @@ const ManageBucket = () => {
return ( return (
<> <>
{deleteBucket && ( {deleteBucket && (
<div <ConfirmModal
className={`modal fade ${deleteBucket ? "show" : ""}`} isOpen={!!deleteBucket}
tabIndex="-1" type="delete"
role="dialog" header="Delete Bucket"
style={{ message="Are you sure you want to delete this bucket?"
display: deleteBucket ? "block" : "none", onSubmit={handleDeleteContact}
backgroundColor: deleteBucket ? "rgba(0,0,0,0.5)" : "transparent", onClose={() => setDeleteBucket(null)}
}} />
>
<ConfirmModal
type={"delete"}
header={"Delete Bucket"}
message={"Are you sure you want to delete this bucket?"}
onSubmit={handleDeleteContact}
onClose={() => setDeleteBucket(null)}
/>
</div>
)} )}
<div className="container m-0 p-0" style={{ minHeight: "00px" }}> <div className="container m-0 p-0" style={{ minHeight: "00px" }}>
@ -237,8 +228,9 @@ const ManageBucket = () => {
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
/> />
<i <i
className={`bx bx-refresh cursor-pointer fs-4 ${loading ? "spin" : "" className={`bx bx-refresh cursor-pointer fs-4 ${
}`} loading ? "spin" : ""
}`}
title="Refresh" title="Refresh"
onClick={() => refetch()} onClick={() => refetch()}
/> />
@ -247,8 +239,9 @@ const ManageBucket = () => {
<button <button
type="button" type="button"
className={`btn btn-sm btn-primary ms-auto ${action_bucket ? "d-none" : "" className={`btn btn-sm btn-primary ms-auto ${
}`} action_bucket ? "d-none" : ""
}`}
onClick={() => { onClick={() => {
setAction_bucket(true); setAction_bucket(true);
select_bucket(null); select_bucket(null);
@ -285,16 +278,18 @@ const ManageBucket = () => {
</div> </div>
)} )}
{!loading && buckets.length > 0 && sortedBucktesList.length === 0 && ( {!loading &&
<div className="col-12"> buckets.length > 0 &&
<div sortedBucktesList.length === 0 && (
className="d-flex justify-content-center align-items-center py-5 w-100" <div className="col-12">
style={{ marginLeft: "250px" }} <div
> className="d-flex justify-content-center align-items-center py-5 w-100"
No matching buckets found. style={{ marginLeft: "250px" }}
>
No matching buckets found.
</div>
</div> </div>
</div> )}
)}
{!loading && {!loading &&
sortedBucktesList.map((bucket) => ( sortedBucktesList.map((bucket) => (
<div className="col" key={bucket.id}> <div className="col" key={bucket.id}>
@ -305,29 +300,29 @@ const ManageBucket = () => {
{(DirManager || {(DirManager ||
DirAdmin || DirAdmin ||
bucket?.createdBy?.id === bucket?.createdBy?.id ===
profile?.employeeInfo?.id) && ( profile?.employeeInfo?.id) && (
<div className="d-flex gap-2"> <div className="d-flex gap-2">
<i <i
className="bx bx-edit bx-sm text-primary cursor-pointer" className="bx bx-edit bx-sm text-primary cursor-pointer"
onClick={() => { onClick={() => {
select_bucket(bucket); select_bucket(bucket);
setAction_bucket(true); setAction_bucket(true);
const initialSelectedEmployees = employeesList const initialSelectedEmployees = employeesList
.filter((emp) => .filter((emp) =>
bucket.employeeIds?.includes( bucket.employeeIds?.includes(
emp.employeeId emp.employeeId
)
) )
.map((emp) => ({ ...emp, isActive: true })); )
setSelectEmployee(initialSelectedEmployees); .map((emp) => ({ ...emp, isActive: true }));
}} setSelectEmployee(initialSelectedEmployees);
></i> }}
<i ></i>
className="bx bx-trash bx-sm text-danger cursor-pointer ms-0" <i
onClick={() => setDeleteBucket(bucket?.id)} className="bx bx-trash bx-sm text-danger cursor-pointer ms-0"
></i> onClick={() => setDeleteBucket(bucket?.id)}
</div> ></i>
)} </div>
)}
</h6> </h6>
<h6 className="card-subtitle mb-2 text-muted text-start"> <h6 className="card-subtitle mb-2 text-muted text-start">
Contacts:{" "} Contacts:{" "}

View File

@ -33,7 +33,10 @@ const NoteCardDirectoryEditable = ({
note: editorValue, note: editorValue,
contactId, contactId,
}; };
const response = await DirectoryRepository.UpdateNote(noteItem.id, payload); const response = await DirectoryRepository.UpdateNote(
noteItem.id,
payload
);
const cachedContactProfile = getCachedData("Contact Profile"); const cachedContactProfile = getCachedData("Contact Profile");
if (cachedContactProfile?.contactId === contactId) { if (cachedContactProfile?.contactId === contactId) {
@ -75,9 +78,9 @@ const NoteCardDirectoryEditable = ({
const contactProfile = (contactId) => { const contactProfile = (contactId) => {
DirectoryRepository.GetContactProfile(contactId).then((res) => { DirectoryRepository.GetContactProfile(contactId).then((res) => {
setOpen_contact(res?.data); setOpen_contact(res?.data);
setIsOpenModalNote(true); setIsOpenModalNote(true);
}); });
}; };
const handleRestore = async () => { const handleRestore = async () => {
@ -95,7 +98,6 @@ const NoteCardDirectoryEditable = ({
return ( return (
<> <>
{isOpenModalNote && ( {isOpenModalNote && (
<GlobalModel <GlobalModel
isOpen={isOpenModalNote} isOpen={isOpenModalNote}
@ -125,7 +127,6 @@ const NoteCardDirectoryEditable = ({
{/* Header */} {/* Header */}
<div className="d-flex justify-content-between align-items-center mb-1"> <div className="d-flex justify-content-between align-items-center mb-1">
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<Avatar <Avatar
size="xxs" size="xxs"
firstName={noteItem?.createdBy?.firstName} firstName={noteItem?.createdBy?.firstName}
@ -133,30 +134,36 @@ const NoteCardDirectoryEditable = ({
className="m-0" className="m-0"
/> />
<div> <div>
<div className="d-flex ms-0 align-middle cursor-pointer" onClick={() =>contactProfile(noteItem.contactId)}> <div
className="d-flex ms-0 align-middle cursor-pointer"
onClick={() => contactProfile(noteItem.contactId)}
>
<span> <span>
<span className="fw-bold "> {noteItem?.contactName} </span> <span className="text-muted font-weight-normal"> <span className="fw-bold "> {noteItem?.contactName} </span>{" "}
<span className="text-muted font-weight-normal">
({noteItem?.organizationName}) ({noteItem?.organizationName})
</span> </span>
</span> </span>
</div>
<div className="d-flex ms-0 align-middle">
</div> </div>
<div className="d-flex ms-0 align-middle"></div>
<div className="d-flex ms-0 mt-2"> <div className="d-flex ms-0 mt-2">
<span className="text-muted"> <span className="text-muted">
by <span className="fw-bold "> {noteItem?.createdBy?.firstName} {noteItem?.createdBy?.lastName} </span> by{" "}
&nbsp; <span className="text-muted"> <span className="fw-bold ">
on {moment {" "}
{noteItem?.createdBy?.firstName}{" "}
{noteItem?.createdBy?.lastName}{" "}
</span>
&nbsp;{" "}
<span className="text-muted">
on{" "}
{moment
.utc(noteItem?.createdAt) .utc(noteItem?.createdAt)
.add(5, "hours") .add(5, "hours")
.add(30, "minutes") .add(30, "minutes")
.format("DD MMMM, YYYY [at] hh:mm A")} .format("DD MMMM, YYYY [at] hh:mm A")}
</span> </span>
</span> </span>
</div> </div>
</div> </div>
</div> </div>
@ -228,26 +235,16 @@ const NoteCardDirectoryEditable = ({
{/* Delete Confirm Modal */} {/* Delete Confirm Modal */}
{isDeleteModalOpen && ( {isDeleteModalOpen && (
<div <ConfirmModal
className={`modal fade ${isDeleteModalOpen ? "show" : ""}`} isOpen={isDeleteModalOpen}
tabIndex="-1" type="delete"
role="dialog" header="Delete Note"
style={{ message="Are you sure you want to delete this note?"
display: isDeleteModalOpen ? "block" : "none", onSubmit={suspendEmployee}
backgroundColor: "rgba(0,0,0,0.5)", onClose={() => setIsDeleteModalOpen(false)}
}} loading={isDeleting}
aria-hidden="false" paramData={noteItem}
> />
<ConfirmModal
type={"delete"}
header={"Delete Note"}
message={"Are you sure you want to delete this note?"}
onSubmit={suspendEmployee}
onClose={() => setIsDeleteModalOpen(false)}
loading={isDeleting}
paramData={noteItem}
/>
</div>
)} )}
</> </>
); );

View File

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

View File

@ -0,0 +1,88 @@
import React from "react";
import VersionListSkeleton from "./VersionListSkeleton";
const SkeletonLine = ({ height = 16, width = "100%", className = "" }) => (
<div
className={`skeleton mb-2 ${className}`}
style={{
height,
width,
borderRadius: 4,
}}
></div>
);
const DocumentDetailsSkeleton = () => {
return (
<div className="p-1">
<p className="fw-bold fs-6">Document Details</p>
{/* Row 1 */}
<div className="row mb-2">
<div className="col-12 col-md-6">
<div className="d-flex gap-2">
<SkeletonLine width="130px" />
<SkeletonLine width="60%" />
</div>
</div>
<div className="col-12 col-md-6">
<div className="d-flex gap-2">
<SkeletonLine width="130px" />
<SkeletonLine width="50%" />
</div>
</div>
</div>
{/* Row 2 */}
<div className="row mb-2">
<div className="col-12 col-md-6">
<div className="d-flex gap-2">
<SkeletonLine width="130px" />
<SkeletonLine width="40%" />
</div>
</div>
<div className="col-12 col-md-6">
<div className="d-flex gap-2">
<SkeletonLine width="130px" />
<SkeletonLine width="60%" />
</div>
</div>
</div>
{/* Row 3 */}
<div className="row mb-2">
<div className="col-12 col-md-6">
<div className="d-flex gap-2">
<SkeletonLine width="130px" />
<SkeletonLine width="40%" />
</div>
</div>
<div className="col-12 col-md-6">
<div className="d-flex gap-2">
<SkeletonLine width="130px" />
<SkeletonLine width="50%" />
</div>
</div>
</div>
{/* Row 6 - Description */}
<div className="row mb-2">
<div className="col-12">
<div className="d-flex">
<SkeletonLine width="100%" height={40} />
</div>
</div>
</div>
{/* Version list skeleton */}
<div className="row text-start py-2">
<VersionListSkeleton items={2} />
</div>
</div>
);
};
export default DocumentDetailsSkeleton;

View File

@ -0,0 +1,205 @@
import React, { useState } from "react";
import { useDocumentFilterEntities } from "../../hooks/useDocument";
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
DocumentFilterDefaultValues,
DocumentFilterSchema,
} from "./DocumentSchema";
import { DateRangePicker1 } from "../common/DateRangePicker";
import SelectMultiple from "../common/SelectMultiple";
import moment from "moment";
const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
const [resetKey, setResetKey] = useState(0);
const { data, isError, isLoading, error } =
useDocumentFilterEntities(entityTypeId);
const methods = useForm({
resolver: zodResolver(DocumentFilterSchema),
defaultValues: DocumentFilterDefaultValues,
});
const { handleSubmit, reset, setValue, watch } = methods;
// Watch values from form
const isUploadedAt = watch("isUploadedAt");
const isVerified = watch("isVerified");
// Close the offcanvas (bootstrap specific)
const closePanel = () => {
document.querySelector(".offcanvas.show .btn-close")?.click();
};
const onSubmit = (values) => {
onApply({
...values,
startDate: values.startDate
? moment.utc(values.startDate, "DD-MM-YYYY").toISOString()
: null,
endDate: values.endDate
? moment.utc(values.endDate, "DD-MM-YYYY").toISOString()
: null,
});
closePanel();
};
const onClear = () => {
reset(DocumentFilterDefaultValues);
setResetKey((prev) => prev + 1);
onApply(DocumentFilterDefaultValues);
closePanel();
};
if (isLoading) return <div>Loading...</div>;
if (isError)
return <div>Error: {error?.message || "Something went wrong!"}</div>;
const {
uploadedBy = [],
documentCategory = [],
documentType = [],
documentTag = [],
} = data?.data || {};
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
{/* Date Range Section */}
<div className="mb-2">
<div className="text-start d-flex align-items-center my-1">
<label className="form-label me-2 my-0">Choose Date:</label>
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
isUploadedAt ? "active btn-secondary text-white" : ""
}`}
onClick={() => setValue("isUploadedAt", true)}
>
Uploaded On
</button>
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
!isUploadedAt ? "active btn-secondary text-white" : ""
}`}
onClick={() => setValue("isUploadedAt", false)}
>
Updated On
</button>
</div>
</div>
<DateRangePicker1
placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="startDate"
endField="endDate"
defaultRange={false}
resetSignal={resetKey}
/>
</div>
{/* Dropdown Filters */}
<div className="row g-2 text-start">
<SelectMultiple
name="uploadedByIds"
label="Uploaded By:"
options={uploadedBy}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="documentCategoryIds"
label="Document Category:"
options={documentCategory}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="documentTypeIds"
label="Document Type:"
options={documentType}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="documentTagIds"
label="Tags:"
options={documentTag}
labelKey="name"
valueKey="id"
/>
</div>
{/* Status Filter */}
<div className="text-start my-2">
<label className="form-label d-block mb-2">Choose Status:</label>
<div className="d-flex gap-4">
<label className="switch switch-sm">
<input
type="radio"
className="switch-input"
name="isVerified"
checked={isVerified === null}
onChange={() => setValue("isVerified", null)}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className="switch-label">All</span>
</label>
<label className="switch switch-sm">
<input
type="radio"
className="switch-input"
name="isVerified"
checked={isVerified === true}
onChange={() => setValue("isVerified", true)}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className="switch-label">Verified</span>
</label>
<label className="switch switch-sm">
<input
type="radio"
className="switch-input"
name="isVerified"
checked={isVerified === false}
onChange={() => setValue("isVerified", false)}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className="switch-label">Rejected</span>
</label>
</div>
</div>
{/* Footer Buttons */}
<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 DocumentFilterPanel;

View File

@ -0,0 +1,132 @@
import { z } from "zod";
import { normalizeAllowedContentTypes } from "../../utils/appUtils";
export const AttachmentSchema = (allowedContentType, maxSizeAllowedInMB) => {
const allowedTypes = normalizeAllowedContentTypes(allowedContentType);
return z.object({
fileName: z.string().min(1, { message: "File name is required" }),
base64Data: z.string().min(1, { message: "File data is required" }),
contentType: z
.string()
.min(1, { message: "MIME type is required" })
.refine(
(val) => (allowedTypes.length ? allowedTypes.includes(val) : true),
{
message: `File type must be one of: ${allowedTypes.join(", ")}`,
}
),
fileSize: z
.number()
.int()
.nonnegative("fileSize must be ≥ 0")
.max(
(maxSizeAllowedInMB ?? 25) * 1024 * 1024,
`fileSize must be ≤ ${maxSizeAllowedInMB ?? 25}MB`
),
description: z.string().optional().default(""),
isActive: z.boolean(),
});
};
export const TagSchema = z.object({
name: z.string().min(1, "Tag name is required"),
isActive: z.boolean().default(true),
});
export const DocumentPayloadSchema = (docConfig = {}) => {
const {
isMandatory,
regexExpression,
allowedContentType,
maxSizeAllowedInMB,
isUpdateForm,
} = docConfig;
let documentIdSchema = z.string();
if (isMandatory) {
documentIdSchema = documentIdSchema.min(1, {
message: "DocumentId is required",
});
}
if (regexExpression) {
documentIdSchema = documentIdSchema.regex(
new RegExp(regexExpression),
"Invalid DocumentId format"
);
}
// Base attachment schema
let attachmentSchema = AttachmentSchema(
allowedContentType,
maxSizeAllowedInMB
).nullable();
// If not update form, require attachment
if (!isUpdateForm) {
attachmentSchema = attachmentSchema.refine((val) => val !== null, {
message: "Attachment is required",
});
}
return z.object({
name: z.string().min(1, "Name is required"),
documentId: documentIdSchema,
description: z.string().min(1, { message: "Description is required" }),
documentTypeId: z
.string()
.min(1, { message: "Please Select Document Type" }),
documentCategoryId: z
.string()
.min(1, { message: "Please Select Document Category" }),
attachment: attachmentSchema,
tags: z.array(TagSchema).optional().default([]),
});
};
export const defaultDocumentValues = {
name: "",
documentId: "",
description: "",
// entityId: "",
documentTypeId: "",
documentCategoryId: "",
// attachment: {
// fileName: "",
// base64Data: "",
// contentType: "",
// fileSize: 0,
// description: "",
// isActive: true,
// },
attachment:null,
tags: [],
};
//--------------------Filter-------------------------
export const DocumentFilterSchema = z.object({
uploadedByIds: z.array(z.string()).default([]),
documentCategoryIds: z.array(z.string()).default([]),
documentTypeIds: z.array(z.string()).default([]),
documentTagIds: z.array(z.string()).default([]),
isUploadedAt: z.boolean().default(true),
isVerified: z.boolean().nullable().optional(),
startDate: z.string().nullable().optional(),
endDate: z.string().nullable().optional(),
});
export const DocumentFilterDefaultValues = {
uploadedByIds: [],
documentCategoryIds: [],
documentTypeIds: [],
documentTagIds: [],
isUploadedAt: true,
isVerified: null,
startDate: null,
endDate: null,
};

View File

@ -0,0 +1,70 @@
import React from "react";
const SkeletonCell = ({
width = "100%",
height = 20,
className = "",
style = {},
}) => (
<div
className={`skeleton ${className}`}
style={{
width,
height,
borderRadius: 4,
...style,
}}
/>
);
export const DocumentTableSkeleton = ({ rows = 5 }) => {
return (
<table className="card-body table border-top dataTable no-footer dtr-column text-nowrap">
<thead>
<tr>
<th className="text-start">Name</th>
<th className="text-start">Document Type</th>
<th className="text-start">Uploaded By</th>
<th className="text-center">Uploaded on</th>
<th className="text-center">Status</th>
</tr>
</thead>
<tbody>
{[...Array(rows)].map((_, idx) => (
<tr key={idx} className={idx % 2 === 0 ? "odd" : "even"}>
{/* Name */}
<td className="text-start">
<SkeletonCell width="120px" height={16} />
</td>
{/* Document Type */}
<td className="text-start">
<SkeletonCell width="100px" height={16} />
</td>
{/* Uploaded By (Avatar + Name) */}
<td className="text-start">
<div className="d-flex align-items-center gap-2">
<SkeletonCell width="30px" height={30} className="rounded-circle" />
<SkeletonCell width="80px" height={16} />
</div>
</td>
{/* Uploaded on */}
<td className="text-center">
<SkeletonCell width="80px" height={16} />
</td>
{/* Status */}
<td className="text-center">
<SkeletonCell width="70px" height={20} className="rounded" />
</td>
</tr>
))}
</tbody>
</table>
);
};

View File

@ -0,0 +1,170 @@
import React from "react";
import VersionListSkeleton from "./VersionListSkeleton";
import { getDocuementsStatus } from "./Documents";
import Avatar from "../common/Avatar";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { DOWNLOAD_DOCUMENT, VERIFY_DOCUMENT } from "../../utils/constants";
import { FileIcon } from "../../utils/FileIcon";
const DocumentVersionList = ({
versionLoding,
versionList,
isPending,
setOpenDocument,
VerifyDocument,
}) => {
const canVerifyDocument = useHasUserPermission(VERIFY_DOCUMENT);
const canDownloadDocument = useHasUserPermission(DOWNLOAD_DOCUMENT);
const handleOpenDocument = () => {
if (canDownloadDocument) {
setOpenDocument(true);
}
};
const contentTypeIcons = {
"application/pdf": "fa-solid fa-file-pdf text-primary",
"application/msword": "fa-solid fa-file-word text-primary",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
"fa-solid fa-file-word text-primary",
"application/vnd.ms-excel": "fa-solid fa-file-excel text-success",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
"fa-solid fa-file-excel text-primary",
"application/vnd.ms-powerpoint": "fa-solid fa-file-powerpoint text-primary",
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
"fa-solid fa-file-powerpoint text-primary",
"image/jpg": "fa-solid fa-file-image text-primary",
"image/jpeg": "fa-solid fa-file-image text-primary",
"image/png": "fa-solid fa-file-image text-primary",
"image/gif": "fa-solid fa-file-image text-primary",
"text/plain": "fa-solid fa-file-lines text-primary",
"text/csv": "fa-solid fa-file-csv text-primary",
"application/json": "fa-solid fa-file-code text-primary",
default: "fa-solid fa-file text-primary",
};
const getIcon = (fileName = "") => {
const ext = fileName.split(".").pop().toLowerCase();
return contentTypeIcons[ext] || contentTypeIcons.default;
};
const sortedVersions = versionList?.data
? [...versionList.data].sort((a, b) => b.version - a.version)
: [];
if (versionLoding) {
return <VersionListSkeleton items={2} />;
}
if (!sortedVersions.length) {
return <p className="text-muted">No documents available.</p>;
}
const latestDoc = sortedVersions[0];
return (
<div className="accordion" id="docAccordion">
<div className="accordion-item shadow-none">
<h2 className="accordion-header" id="headingDoc">
<button
className="accordion-button"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapseDoc"
aria-expanded="true"
aria-controls="collapseDoc"
>
<i className="bx bxs-folder me-2 text-warning fs-5"></i>
{latestDoc.name} (Latest v{latestDoc.version})
</button>
</h2>
<div
id="collapseDoc"
className="accordion-collapse collapse show"
aria-labelledby="headingDoc"
data-bs-parent="#docAccordion"
>
<div className="accordion-body p-2">
<div className="list-group list-group-flush">
{sortedVersions.map((document, index) => (
<div
key={document.id}
className={`list-group-item list-group-item-action d-flex align-items-center cursor-pointer ${
index > 0 ? "ms-4" : "" // indent only older versions
}`}
>
<FileIcon
type={document.contentType}
size="fs-2"
className="me-2"
/>
<div className="w-100">
<div className="d-flex justify-content-between align-items-center">
<small className=" fw-normal">{document.name}</small>
<small className=" fw-normal">
Version-{document.version}
</small>{" "}
<small className=" text-secondary fw-normal">
fileSize: {document.fileSize} Kb
</small>
</div>
<div className="d-flex justify-content-between m-0">
<div
className="user-info text-start m-0"
onClick={handleOpenDocument}
>
<div className="d-flex align-items-center">
{formatUTCToLocalTime(document?.uploadedAt)} |
Uploaded by{" "}
<div className="d-flex align-items-center ms-1">
<Avatar
size="xs"
classAvatar="m-0"
firstName={document.uploadedBy?.firstName}
lastName={document.uploadedBy?.lastName}
/>
<span className="text-truncate ms-1">
{`${document.uploadedBy?.firstName ?? ""} ${
document.uploadedBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
</div>
{document?.updatedAt && (
<div className="d-flex align-items-center">
{formatUTCToLocalTime(document?.updatedAt)} |
Updated by{" "}
<div className="d-flex align-items-center ms-1">
<Avatar
size="xs"
classAvatar="m-0"
firstName={document.updatedBy?.firstName}
lastName={document.updatedBy?.lastName}
/>
<span className="text-truncate ms-1">
{`${document.updatedBy?.firstName ?? ""} ${
document.updatedBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
</div>
)}
</div>
<div className="d-flex align-items-end">
{getDocuementsStatus(document.isVerified)}
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
};
export default DocumentVersionList;

View File

@ -0,0 +1,27 @@
import React, { useEffect } from "react";
import { useDocumentVersion } from "../../hooks/useDocument";
import { useDocumentContext } from "./Documents";
import { error } from "pdf-lib";
const DocumentViewerModal = () => {
const { viewDoc,setOpenDocument } = useDocumentContext();
const { data, isLoading, isError,error } = useDocumentVersion(viewDoc.document);
useEffect(() => {
if (data?.data) {
const fileUrl = data.data;
window.open(fileUrl, "_blank");
setOpenDocument(false)
}
}, [data]);
if (isLoading) return <p>Loading document...</p>;
if (isError) return <div>
<p className="danger-text">{error.message}</p>
</div>;
// Nothing to render inside modal since we redirect
return null;
};
export default DocumentViewerModal;

View File

@ -0,0 +1,239 @@
import React, { createContext, useContext, useEffect, useState } from "react";
import GlobalModel from "../common/GlobalModel";
import NewDocument from "./ManageDocument";
import { DOCUMENTS_ENTITIES, UPLOAD_DOCUMENT } from "../../utils/constants";
import { useParams } from "react-router-dom";
import DocumentsList from "./DocumentsList";
import DocumentFilterPanel from "./DocumentFilterPanel";
import { useFab } from "../../Context/FabContext";
import { useForm } from "react-hook-form";
import {
DocumentFilterDefaultValues,
DocumentFilterSchema,
} from "./DocumentSchema";
import { zodResolver } from "@hookform/resolvers/zod";
import ManageDocument from "./ManageDocument";
import ViewDocument from "./ViewDocument";
import DocumentViewerModal from "./DocumentViewerModal";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
// Context
export const DocumentContext = createContext();
export const useDocumentContext = () => {
const context = useContext(DocumentContext);
if (!context) {
throw new Error(
"useDocumentContext must be used within an DocumentProvider"
);
}
return context;
};
export const getDocuementsStatus = (status) => {
switch (status) {
case true:
return (
<span className="badge rounded-pill bg-label-success">Verified</span>
);
case false:
return (
<span className="badge rounded-pill bg-label-danger">Rejected</span>
);
case null:
default:
return (
<span className="badge rounded-pill bg-label-warning"> Pending</span>
);
}
};
const Documents = ({ Document_Entity, Entity }) => {
const [searchText, setSearchText] = useState("");
const [isActive, setIsActive] = useState(true);
const [filters, setFilter] = useState();
const [isRefetching, setIsRefetching] = useState(false);
const [refetchFn, setRefetchFn] = useState(null);
const [DocumentEntity, setDocumentEntity] = useState(Document_Entity);
const { employeeId } = useParams();
const [OpenDocument, setOpenDocument] = useState(false);
const [ManageDoc, setManageDoc] = useState({
document: null,
isOpen: false,
});
const [viewDoc, setViewDoc] = useState({
document: null,
isOpen: false,
});
const canUploadDocument = useHasUserPermission(UPLOAD_DOCUMENT)
const { setOffcanvasContent, setShowTrigger } = useFab();
const methods = useForm({
resolver: zodResolver(DocumentFilterSchema),
defaultValues: DocumentFilterDefaultValues,
});
const { reset } = methods;
const clearFilter = () => {
setFilter(DocumentFilterDefaultValues);
reset();
};
useEffect(() => {
setShowTrigger(true);
setOffcanvasContent(
"Document Filters",
<DocumentFilterPanel entityTypeId={DocumentEntity} onApply={setFilter} />
);
return () => {
setShowTrigger(false);
setOffcanvasContent("", null);
};
}, []);
const contextValues = {
ManageDoc,
setManageDoc,
viewDoc,
setViewDoc,
setOpenDocument,
OpenDocument,
};
useEffect(() => {
if (Document_Entity) {
setDocumentEntity(Document_Entity);
}
}, [Document_Entity]);
return (
<DocumentContext.Provider value={contextValues}>
<div className="mt-5">
<div className="card d-flex p-2">
<div className="row align-items-center">
{/* Search */}
<div className="d-flex col-8 col-md-8 col-lg-4 mb-md-0 align-items-center">
<div className="d-flex"> <input
type="search"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="form-control form-control-sm"
placeholder="Search Document"
/></div>
<label className="switch switch-sm mx-2">
<input
type="checkbox"
className="switch-input"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className="switch-label">
{isActive ? "Active" : "In-Active"}
</span>
</label>
</div>
{/* Actions */}
<div className="col-6 col-md-6 col-lg-8 text-end">
{/* <span
className="text-tiny text-muted p-1 border-0 bg-none lead mx-3 cursor-pointer"
disabled={isRefetching}
onClick={() => {
setSearchText("");
setFilter(DocumentFilterDefaultValues);
refetchFn && refetchFn();
}}
>
Refresh
<i
className={`bx bx-refresh ms-1 ${
isRefetching ? "bx-spin" : ""
}`}
></i>
</span> */}
{canUploadDocument && (<button
type="button"
title="Add New Document"
className="p-1 bg-primary rounded-circle cursor-pointer"
onClick={() =>
setManageDoc({
document: null,
isOpen: true,
})
}
>
<i className="bx bx-plus fs-4 text-white"></i>
</button>)}
</div>
</div>
<DocumentsList
Document_Entity={DocumentEntity}
Entity={Entity}
filters={filters}
searchText={searchText}
setIsRefetching={setIsRefetching}
setRefetchFn={setRefetchFn}
isActive={isActive}
/>
</div>
{ManageDoc.isOpen && (
<GlobalModel
isOpen={ManageDoc.isOpen}
closeModal={() =>
setManageDoc({
document: null,
isOpen: false,
})
}
>
<ManageDocument
closeModal={() =>
setManageDoc({
document: null,
isOpen: false,
})
}
Document_Entity={DocumentEntity}
Entity={Entity}
/>
</GlobalModel>
)}
{viewDoc.isOpen && (
<GlobalModel
size="lg"
isOpen={viewDoc.isOpen}
closeModal={() =>
setViewDoc({
document: null,
isOpen: false,
})
}
>
<ViewDocument />
</GlobalModel>
)}
{OpenDocument && (
<GlobalModel
isOpen={OpenDocument}
closeModal={() => setOpenDocument(false)}
>
<DocumentViewerModal />
</GlobalModel>
)}
</div>
</DocumentContext.Provider>
);
};
export default Documents;

View File

@ -0,0 +1,251 @@
import React, { useEffect, useState } from "react";
import {
useActiveInActiveDocument,
useDocumentListByEntityId,
} from "../../hooks/useDocument";
import {
DELETE_DOCUMENT,
ITEMS_PER_PAGE,
MODIFY_DOCUMENT,
} from "../../utils/constants";
import Avatar from "../common/Avatar";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import { useDebounce } from "../../utils/appUtils";
import { DocumentTableSkeleton } from "./DocumentSkeleton";
import { getDocuementsStatus, useDocumentContext } from "./Documents";
import Pagination from "../common/Pagination";
import ConfirmModal from "../common/ConfirmModal";
import { isPending } from "@reduxjs/toolkit";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
const DocumentsList = ({
Document_Entity,
Entity,
filters,
searchText,
setIsRefetching,
setRefetchFn,
isActive,
}) => {
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deletingId, setDeletingId] = useState(null);
const [restoringIds, setRestoringIds] = useState([]);
const debouncedSearch = useDebounce(searchText, 500);
const [currentPage, setCurrentPage] = useState(1);
const canDeleteDocument = useHasUserPermission(DELETE_DOCUMENT);
const canModifyDocument = useHasUserPermission(MODIFY_DOCUMENT);
const { data, isError, isLoading, error, refetch, isFetching } =
useDocumentListByEntityId(
Document_Entity,
Entity,
ITEMS_PER_PAGE,
currentPage,
filters,
debouncedSearch,
isActive
);
useEffect(() => {
setRefetchFn(() => refetch);
}, [setRefetchFn, refetch]);
useEffect(() => {
setIsRefetching(isFetching);
}, [isFetching, setIsRefetching]);
const { setManageDoc, setViewDoc } = useDocumentContext();
const { mutate: ActiveInActive, isPending } = useActiveInActiveDocument();
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
const noData = !isLoading && !isError && data?.data.length === 0;
const isSearchEmpty = noData && !!debouncedSearch;
const isFilterEmpty = noData && !!filters && Object.keys(filters).length > 0;
const isInitialEmpty = noData && !debouncedSearch && !isFilterEmpty;
if (isLoading || isFetching) return <DocumentTableSkeleton />;
if (isError)
return <div>Error: {error?.message || "Something went wrong"}</div>;
if (isInitialEmpty) return <div>No documents found yet.</div>;
if (isSearchEmpty) return <div>No results found for "{debouncedSearch}"</div>;
if (isFilterEmpty) return <div>No documents match your filter.</div>;
const handleDelete = () => {
ActiveInActive(
{ documentId: deletingId, isActive: !isActive },
{
onSettled: () => {
setDeletingId(null);
setIsDeleteModalOpen(false);
},
}
);
};
const handleRestore = (docId) => {
setRestoringIds((prev) => [...prev, docId]);
ActiveInActive(
{ documentId: docId, isActive: true },
{
onSettled: () => {
setRestoringIds((prev) => prev.filter((id) => id !== docId));
refetch();
},
}
);
};
const DocumentColumns = [
{
key: "name",
label: "Name",
getValue: (e) => e.name || "N/A",
align: "text-start",
},
{
key: "documentType",
label: "Document Type",
getValue: (e) => e.documentType?.name || "N/A",
align: "text-start",
},
{
key: "uploadedBy",
label: "Uploaded By",
align: "text-start",
customRender: (e) => (
<div className="d-flex align-items-center">
<Avatar
size="xs"
classAvatar="m-0"
firstName={e.uploadedBy?.firstName}
lastName={e.uploadedBy?.lastName}
/>
<span className="text-truncate ms-1">
{`${e.uploadedBy?.firstName ?? ""} ${
e.uploadedBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
),
getValue: (e) =>
`${e.uploadedBy?.firstName ?? ""} ${
e.uploadedBy?.lastName ?? ""
}`.trim() || "N/A",
},
{
key: "uploadedAt",
label: "Uploaded on",
getValue: (e) => formatUTCToLocalTime(e.uploadedAt),
align: "text-center",
isAlwaysVisible: true,
},
{
key: "Status",
label: "Status",
getValue: (e) => getDocuementsStatus(e.isVerified),
align: "text-center",
isAlwaysVisible: true,
},
];
return (
<>
{IsDeleteModalOpen && (
<ConfirmModal
isOpen={IsDeleteModalOpen}
type="delete"
header="Delete Document"
message="Are you sure you want to delete this document?"
onSubmit={handleDelete}
onClose={() => setIsDeleteModalOpen(false)}
loading={!!isPending}
paramData={deletingId}
/>
)}
<div className="table-responsive">
<table className="table border-top dataTable text-nowrap">
<thead>
<tr className="shadow-sm">
{DocumentColumns.map((col) => (
<th key={col.key} className={`sorting ${col.align}`}>
{col.label}
</th>
))}
<th className="sticky-action-column bg-white text-center">
Action
</th>
</tr>
</thead>
<tbody className="text-start">
{data?.data?.map((doc) => {
const isRestoring = restoringIds.includes(doc.id);
return (
<tr key={doc.id}>
{DocumentColumns.map((col) => (
<td key={col.key} className={`sorting ${col.align}`}>
{col.customRender
? col.customRender(doc)
: col.getValue(doc)}
</td>
))}
<td className="text-center">
{doc.isActive ? (
<div className="d-flex justify-content-center gap-2">
<i
className="bx bx-show text-primary cursor-pointer"
onClick={() =>
setViewDoc({ document: doc.id, isOpen: true })
}
></i>
{canModifyDocument && (
<i
className="bx bx-edit text-secondary cursor-pointer"
onClick={() =>
setManageDoc({ document: doc.id, isOpen: true })
}
></i>
)}
{canDeleteDocument && (
<i
className="bx bx-trash text-danger cursor-pointer"
onClick={() => {
setIsDeleteModalOpen(true);
setDeletingId(doc.id);
}}
></i>
)}
</div>
) : isRestoring ? (
<div
className="spinner-border spinner-border-sm text-primary"
role="status"
>
<span className="visually-hidden">Loading...</span>
</div>
) : (
<i
className="bx bx-recycle me-1 text-primary cursor-pointer"
onClick={() => handleRestore(doc.id)}
></i>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</>
);
};
export default DocumentsList;

View File

@ -0,0 +1,428 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect, useState } from "react";
import { useForm, FormProvider } from "react-hook-form";
import { defaultDocumentValues, DocumentPayloadSchema } from "./DocumentSchema";
import Label from "../common/Label";
import {
useDocumentCategories,
useDocumentTypes,
} from "../../hooks/masterHook/useMaster";
import TagInput from "../common/TagInput";
import {
useDocumentDetails,
useDocumentTags,
useUpdateDocument,
useUploadDocument,
} from "../../hooks/useDocument";
import showToast from "../../services/toastService";
import { useDocumentContext } from "./Documents";
import { isPending } from "@reduxjs/toolkit";
const toBase64 = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (err) => reject(err);
});
const MergedTagsWithExistenStatus = (formTags = [], originalTags = []) => {
const tagMap = new Map();
const safeFormTags = Array.isArray(formTags) ? formTags : [];
const safeOriginalTags = Array.isArray(originalTags) ? originalTags : [];
safeOriginalTags.forEach(tag => {
if (tag?.name) {
tagMap.set(tag.name, { ...tag, isActive: tag.isActive ?? true });
}
});
safeFormTags.forEach(tag => {
if (tag?.name) {
tagMap.set(tag.name, { ...tag, isActive: true });
}
});
safeOriginalTags.forEach(tag => {
if (tag?.name && !safeFormTags.some(t => t.name === tag.name)) {
tagMap.set(tag.name, { ...tag, isActive: false });
}
});
return Array.from(tagMap.values());
};
const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
const { ManageDoc } = useDocumentContext();
const isUpdateForm = Boolean(ManageDoc?.document);
const [selectedType, setSelectedType] = useState(null);
const [selectedCategory, setSelectedCategory] = useState(null);
const [schema, setSchema] = useState(() =>
DocumentPayloadSchema({ isUpdateForm })
);
const methods = useForm({
resolver: zodResolver(schema),
defaultValues: defaultDocumentValues,
});
const {
register,
handleSubmit,
watch,
setValue,
reset,
formState: { errors },
} = methods;
const { mutate: UploadDocument, isPending: isUploading } = useUploadDocument(
() => {
showToast("Document Uploaded Successfully", "success");
closeModal();
}
);
const { mutate: UpdateDocument, isPending: isUpdating } = useUpdateDocument(
() => {
showToast("Document Updated Successfully", "success");
closeModal();
}
);
const onSubmit = (data) => {
const normalizeAttachment = (attachment) => {
if (!attachment) return null;
return {
...attachment,
fileSize: Math.ceil(attachment.fileSize / 1024),
};
};
const payload = {
...data,
attachment: normalizeAttachment(data.attachment),
};
if (ManageDoc?.document) {
const DocumentPayload = {
...payload,
id: DocData.id,
tags: MergedTagsWithExistenStatus(data?.tags, DocData?.tags),
};
UpdateDocument({ documentId: DocData?.id, DocumentPayload });
} else {
const DocumentPayload = { ...payload, entityId: Entity };
UploadDocument(DocumentPayload);
}
};
const {
data: DocData,
isLoading: isDocLoading,
isError: isDocError,
DocError,
} = useDocumentDetails(ManageDoc?.document);
const file = watch("attachment");
const documentTypeId = watch("documentTypeId");
// This hooks calling api base Entity(Employee) and Category
const { DocumentCategories, isLoading } =
useDocumentCategories(Document_Entity);
const categoryId = watch("documentCategoryId");
const { DocumentTypes, isLoading: isTypeLoading } = useDocumentTypes(
categoryId || null
);
const {data:DocumentTags} = useDocumentTags()
// Update schema whenever document type changes
useEffect(() => {
if (!documentTypeId) return;
const type = DocumentTypes?.find(
(t) => String(t.id) === String(documentTypeId)
);
if (!type) return;
setSelectedType(type)
const newSchema = DocumentPayloadSchema({
isMandatory: type.isMandatory ?? false,
regexExpression: type.regexExpression ?? null,
allowedContentType: type.allowedContentType ?? [
"application/pdf",
"image/jpeg",
"image/png",
],
maxSizeAllowedInMB: type.maxSizeAllowedInMB ?? 25,
isUpdateForm,
});
setSchema(() => newSchema);
methods.reset(methods.getValues(), { keepValues: true });
methods.formState.errors; // triggers revalidation
}, [documentTypeId, DocumentTypes, isUpdateForm]);
// File Upload
const onFileChange = async (e) => {
const uploaded = e.target.files[0];
if (!uploaded) return;
const base64Data = await toBase64(uploaded);
const parsedFile = {
fileName: uploaded.name,
base64Data,
contentType: uploaded.type,
fileSize: uploaded.size,
description: "",
isActive: true,
};
setValue("attachment", parsedFile, {
shouldDirty: true,
shouldValidate: true,
});
};
const removeFile = () => {
setValue("attachment", null, {
shouldDirty: true,
shouldValidate: true,
});
};
// build dynamic file accept string
const fileAccept =
selectedType?.allowedContentType
?.split(",")
.map((t) =>
t === "application/pdf"
? ".pdf"
: t === "image/jpeg"
? ".jpg,.jpeg"
: t === "image/png"
? ".png"
: ""
)
.join(",") || "";
useEffect(() => {
if (DocData) {
reset({
...defaultDocumentValues,
name: DocData?.name ?? "",
documentCategoryId: DocData?.documentType?.documentCategory?.id
? String(DocData.documentType.documentCategory.id)
: "",
documentTypeId: DocData?.documentType?.id
? String(DocData.documentType.id)
: "",
documentId: DocData?.documentId ?? "",
description: DocData?.description ?? "",
attachment: DocData?.attachment ?? null,
tags: DocData?.tags ?? [],
});
}
}, [DocData, reset]);
if (isDocLoading) return <div>Loading...</div>;
if (isDocError) return <div>{DocError.message}</div>;
const isPending = isUploading || isUpdating;
return (
<div className="p-2">
<p className="fw-bold fs-6">Upload New Document</p>
<FormProvider key={documentTypeId} {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="text-start">
{/* Document Name */}
<div className="mb-2">
<Label htmlFor="name" required>
Document Name
</Label>
<input
type="text"
className="form-control form-control-sm"
{...register("name")}
/>
{errors.name && (
<div className="danger-text">{errors.name.message}</div>
)}
</div>
{/* Category */}
<div className="mb-2">
<Label htmlFor="documentCategoryId">Document Category</Label>
<select
{...register("documentCategoryId")}
className="form-select form-select-sm"
>
{isLoading && (
<option disabled value="">
Loading...
</option>
)}
{!isLoading && <option value="">Select Category</option>}
{DocumentCategories?.map((type) => (
<option key={type.id} value={type.id}>
{type.name}
</option>
))}
</select>
{errors.documentCategoryId && (
<div className="danger-text">
{errors.documentCategoryId.message}
</div>
)}
</div>
{/* Type */}
{categoryId && (
<div className="mb-2">
<Label htmlFor="documentTypeId">Document Type</Label>
<select
{...register("documentTypeId")}
className="form-select form-select-sm"
>
{isTypeLoading && (
<option disabled value="">
Loading...
</option>
)}
{DocumentTypes?.map((type) => (
<option key={type.id} value={type.id}>
{type.name}
</option>
))}
</select>
{errors.documentTypeId && (
<div className="danger-text">
{errors.documentTypeId.message}
</div>
)}
</div>
)}
{/* Document ID */}
<div className="mb-2">
<Label
htmlFor="documentId"
required={selectedType?.isMandatory ?? false}
>
Document ID
</Label>
<input
type="text"
className="form-control form-control-sm"
{...register("documentId")}
/>
{errors.documentId && (
<div className="danger-text">{errors.documentId.message}</div>
)}
</div>
{/* Upload */}
<div className="row my-2">
<div className="col-md-12">
<Label htmlFor="attachment" required>Upload Document</Label>
<div
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
style={{ cursor: "pointer" }}
onClick={() => document.getElementById("attachment").click()}
>
<i className="bx bx-cloud-upload d-block bx-lg"></i>
<span className="text-muted d-block">
Click to select or click here to browse
</span>
<small className="text-muted">
({selectedType?.allowedContentType || "PDF/JPG/PNG"}, max{" "}
{selectedType?.maxSizeAllowedInMB ?? 25}MB)
</small>
<input
type="file"
id="attachment"
accept={selectedType?.allowedContentType}
style={{ display: "none" }}
onChange={(e) => {
onFileChange(e);
e.target.value = ""; // reset input
}}
/>
</div>
{errors.attachment && (
<small className="danger-text">
{errors.attachment.message
? errors.attachment.message
: errors.attachment.fileName?.message ||
errors.attachment.base64Data?.message ||
errors.attachment.contentType?.message ||
errors.attachment.fileSize?.message}
</small>
)}
{file?.base64Data && (
<div className="d-flex justify-content-between text-start p-1 mt-2">
<div>
<span className="mb-0 text-secondary small d-block">
{file.fileName}
</span>
<span className="text-body-secondary small d-block">
{(file.fileSize / 1024).toFixed(1)} KB
</span>
</div>
<i
className="bx bx-trash bx-sm cursor-pointer text-danger"
onClick={removeFile}
></i>
</div>
)}
</div>
</div>
<div className="mb-2">
<TagInput name="tags" label="Tags" placeholder="Tags.." options={DocumentTags} />
{errors.tags && (
<small className="danger-text">{errors.tags.message}</small>
)}
</div>
{/* Description */}
<div className="mb-2">
<Label htmlFor="description" required>Description</Label>
<textarea
rows="2"
className="form-control"
{...register("description")}
></textarea>
{errors.description && (
<div className="danger-text">{errors.description.message}</div>
)}
</div>
{/* Buttons */}
<div className="d-flex justify-content-center gap-3">
<button
type="submit"
className="btn btn-primary btn-sm"
disabled={isPending}
>
{isPending ? "Please Wait..." : " Submit"}
</button>
<button
type="reset"
className="btn btn-secondary btn-sm"
disabled={isPending}
onClick={closeModal}
>
Cancel
</button>
</div>
</form>
</FormProvider>
</div>
);
};
export default ManageDocument;

View File

@ -0,0 +1,47 @@
import React from "react";
const SkeletonLine = ({ height = 16, width = "100%", className = "" }) => (
<div
className={`skeleton mb-1 ${className}`}
style={{
height,
width,
borderRadius: 4,
}}
></div>
);
const VersionListSkeleton = ({ items = 5 }) => {
return (
<div className="list-group mx-0">
{[...Array(items)].map((_, idx) => (
<div
key={idx}
className="list-group-item py-2 border border-bottom border-top-0 border-start-0 border-end-0"
>
{/* Top row: document name + version/status */}
<div className="d-flex w-100 justify-content-between">
<SkeletonLine width="40%" height={16} />
<div className="d-flex gap-2">
<SkeletonLine width="60px" height={14} />
<SkeletonLine width="80px" height={14} />
</div>
</div>
{/* Upload by row */}
<div className="d-flex align-items-center gap-2 mt-2">
<SkeletonLine width="24px" height="24px" className="rounded-circle" />
<SkeletonLine width="120px" height={14} />
</div>
{/* Updated at row */}
<div className="d-flex gap-2 mt-2">
<SkeletonLine width="150px" height={14} />
</div>
</div>
))}
</div>
);
};
export default VersionListSkeleton;

View File

@ -0,0 +1,234 @@
import React, { useState } from "react";
import {
useDocumentDetails,
useDocumentVersionList,
useVerifyDocument,
} from "../../hooks/useDocument";
import { getDocuementsStatus, useDocumentContext } from "./Documents";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import Avatar from "../common/Avatar";
import {
DOWNLOAD_DOCUMENT,
ITEMS_PER_PAGE,
VERIFY_DOCUMENT,
} from "../../utils/constants";
import Pagination from "../common/Pagination";
import VersionListSkeleton from "./VersionListSkeleton";
import DocumentDetailsSkeleton from "./DocumentDetailsSkeleton ";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import DocumentVersionList from "./DocumentVersionList";
const ViewDocument = () => {
const { viewDoc, setViewDoc, setOpenDocument } = useDocumentContext();
const [currentPage, setCurrentPage] = useState(1);
const canVerifyDocument = useHasUserPermission(VERIFY_DOCUMENT);
const canDownloadDocument = useHasUserPermission(DOWNLOAD_DOCUMENT);
const { data, isLoading, isError, error } = useDocumentDetails(
viewDoc?.document
);
const {
data: versionList,
isError: isVersionError,
isLoading: versionLoding,
error: versionError,
} = useDocumentVersionList(
data?.parentAttachmentId,
ITEMS_PER_PAGE - 10,
currentPage
);
const paginate = (page) => {
if (page >= 1 && page <= (versionList?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
const { mutate: VerifyDoc, isPending } = useVerifyDocument();
const VerifyDocument = () => {
VerifyDoc({ documentId: viewDoc?.document, isVerify: true });
};
const RejectDocument = () => {
VerifyDoc({ documentId: viewDoc?.document, isVerify: false });
};
if (isLoading) return <DocumentDetailsSkeleton />;
if (isError)
return (
<div>
<p>{error?.response?.data?.message || error?.message}</p>
<p className="danger-text">{error?.response?.status}</p>
</div>
);
return (
<div className="p-1">
<p className="fw-bold fs-6">Document Details</p>
<div className="row mb-2">
<div className="col-12 col-md-6">
<div className="d-flex text-start">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Document Name:
</span>
<span className="text-muted">{data.name || "-"}</span>
</div>
</div>
<div className="col-12 col-md-6">
<div className="d-flex text-start">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Document ID:
</span>
<span className="text-muted">{data.documentId || "-"}</span>
</div>
</div>
</div>
{/* Row 2 */}
<div className="row mb-2">
<div className="col-12 col-md-6 text-start">
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Version:
</span>
<span className="text-muted">{data.version || "-"}</span>
</div>
</div>
<div className="col-12 col-md-6 text-start">
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Uploaded At:
</span>
<span className="text-muted">
{formatUTCToLocalTime(data.uploadedAt)}
</span>
</div>
</div>
</div>
{/* Row 3 */}
<div className="row mb-2 text-start">
<div className="col-12 col-md-6">
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Uploaded By:
</span>
<div className="d-flex align-items-center ms-1">
<Avatar
size="xs"
classAvatar="m-0"
firstName={data.uploadedBy?.firstName}
lastName={data.uploadedBy?.lastName}
/>
<span className="text-truncate ms-1">
{`${data.uploadedBy?.firstName ?? ""} ${
data.uploadedBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
</div>
</div>
{data.updatedAt && (
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Updated At:
</span>
<span className="text-muted">
{formatUTCToLocalTime(data.updatedAt) || "-"}
</span>
</div>
)}{" "}
<div className="col-12 col-md-6"></div>
</div>
{/* Row 4 */}
<div className="row mb-2 text-start">
<div className="col-12 col-md-6">
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Category:
</span>
<span className="text-muted">
{data.documentType?.documentCategory?.name || "-"}
</span>
</div>
</div>
<div className="col-12 col-md-6">
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Type:
</span>
<span className="text-muted">{data.documentType?.name || "-"}</span>
</div>
</div>
</div>
{/* Row 5 - Tags full width */}
<div className="row mb-2 text-start">
<div className="col-12">
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Tags:
</span>
<div className="d-flex flex-wrap gap-2">
{data.tags?.length > 0 ? (
data.tags.map((t, i) => (
<span
key={i}
className="badge rounded-pill bg-label-secondary"
>
{t.name}
</span>
))
) : (
<span className="text-muted">-</span>
)}
</div>
</div>
</div>
</div>
<div className="row mb-2 text-start">
<div className="col-12">
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Description:
</span>
<span className="text-muted">{data.description || "-"}</span>
</div>
</div>
</div>
{data.isVerified === null && (
<div className="d-flex justify-content-end">
<div className="d-flex text-start text-sm-end">
{" "}
{isPending ? (
"Please Wait..."
) : (
<div className="mx-2">
<a
onClick={VerifyDocument}
className="cursor-pointer text-primary"
>
Verify
</a>
<a
onClick={RejectDocument}
className="cursor-pointer text-danger mx-2"
>
Reject
</a>
</div>
)}
</div>
</div>
)}
<DocumentVersionList
versionLoding={versionLoding}
versionList={versionList}
isPending={isPending}
setOpenDocument={setOpenDocument}
VerifyDocument={VerifyDocument}
/>
</div>
);
};
export default ViewDocument;

View File

@ -126,7 +126,7 @@ const EmpAttendance = ({ employee }) => {
className="dataTables_length text-start py-2 d-flex justify-content-between " className="dataTables_length text-start py-2 d-flex justify-content-between "
id="DataTables_Table_0_length" id="DataTables_Table_0_length"
> >
<div className="col-md-3 my-0 "> <div className="col-md-4 my-0 ">
<DateRangePicker <DateRangePicker
DateDifference="30" DateDifference="30"
onRangeChange={setDateRange} onRangeChange={setDateRange}

View File

@ -59,20 +59,20 @@ const EmpBanner = ({ profile, loggedInUser }) => {
</h4> </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"> <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"> <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"> <span className="fw-medium">
{profile?.jobRole || <em>NA</em>} {profile?.jobRole || <em>NA</em>}
</span> </span>
</li> </li>
<li className="list-inline-item"> <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"> <span className="fw-medium">
{" "} {" "}
{profile?.phoneNumber || <em>NA</em>} {profile?.phoneNumber || <em>NA</em>}
</span> </span>
</li> </li>
<li className="list-inline-item"> <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"> <span className="fw-medium">
{" "} {" "}
Joined on{" "} Joined on{" "}
@ -85,18 +85,21 @@ const EmpBanner = ({ profile, loggedInUser }) => {
</li> </li>
</ul> </ul>
<ul className="list-inline mb-0 d-flex align-items-center flex-wrap justify-content-sm-start justify-content-center mt-4"> <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"> {profile?.isActive && ( // show only if active
<button <li className="list-inline-item">
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 <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()} onClick={() => openChangePassword()}
> >
Change Password Change Password

View File

@ -9,7 +9,6 @@ const EmpDashboard = ({ profile }) => {
refetch, refetch,
} = useProjectsAllocationByEmployee(profile?.id); } = useProjectsAllocationByEmployee(profile?.id);
console.log(projectList);
return ( return (
<> <>
<div className="row"> <div className="row">

View File

@ -1,10 +1,15 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { ComingSoonPage } from "../../pages/Misc/ComingSoonPage"; import { ComingSoonPage } from "../../pages/Misc/ComingSoonPage";
import DocumentPage from "../../pages/Documents/DocumentPage";
import Documents from "../Documents/Documents";
import { useParams } from "react-router-dom";
import { DOCUMENTS_ENTITIES } from "../../utils/constants";
const EmpDocuments = ({ profile, loggedInUser }) => { const EmpDocuments = ({ profile, loggedInUser }) => {
const {employeeId} = useParams()
return ( return (
<> <>
<ComingSoonPage/> <Documents Document_Entity={DOCUMENTS_ENTITIES.EmployeeEntity} Entity={employeeId} />
</> </>
); );
}; };

View File

@ -4,4 +4,4 @@ const EmployeeList = () => {
return <div>EmployeeList</div>; return <div>EmployeeList</div>;
}; };
export default EmployeeList; export default EmployeeList;

View File

@ -1,11 +1,18 @@
import React from "react"; import React from "react";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { VIEW_DOCUMENT } from "../../utils/constants";
const EmployeeNav = ({ onPillClick, activePill }) => { const EmployeeNav = ({ onPillClick, activePill }) => {
const tabs = [ const canViewDocuments = useHasUserPermission(VIEW_DOCUMENT)
const tabs = [
{ key: "profile", icon: "bx bx-user", label: "Profile" }, { key: "profile", icon: "bx bx-user", label: "Profile" },
{ key: "attendance", icon: "bx bx-group", label: "Attendances" }, { key: "attendance", icon: "bx bx-group", label: "Attendances" },
{ key: "documents", icon: "bx bx-user", label: "Documents" }, canViewDocuments && {
key: "documents",
icon: "bx bx-file",
label: "Documents",
},
{ key: "activities", icon: "bx bx-grid-alt", label: "Activities" }, { key: "activities", icon: "bx bx-grid-alt", label: "Activities" },
]; ].filter(Boolean);
return ( return (
<div className="col-md-12"> <div className="col-md-12">

View File

@ -71,7 +71,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
closePanel(); closePanel();
}; };
// Close popup when navigating to another component // Close popup when navigating to another component
const location = useLocation(); const location = useLocation();
useEffect(() => { useEffect(() => {
closePanel(); closePanel();
@ -105,6 +105,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
startField="startDate" startField="startDate"
endField="endDate" endField="endDate"
resetSignal={resetKey} resetSignal={resetKey}
defaultRange={false}
/> />
</div> </div>

View File

@ -8,6 +8,7 @@ import {
APPROVE_EXPENSE, APPROVE_EXPENSE,
EXPENSE_DRAFT, EXPENSE_DRAFT,
EXPENSE_REJECTEDBY, EXPENSE_REJECTEDBY,
ITEMS_PER_PAGE,
} from "../../utils/constants"; } from "../../utils/constants";
import { getColorNameFromHex, useDebounce } from "../../utils/appUtils"; import { getColorNameFromHex, useDebounce } from "../../utils/appUtils";
import { ExpenseTableSkeleton } from "./ExpenseSkeleton"; import { ExpenseTableSkeleton } from "./ExpenseSkeleton";
@ -22,12 +23,11 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
const IsExpenseEditable = useHasUserPermission(); const IsExpenseEditable = useHasUserPermission();
const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE); const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const pageSize = 20;
const debouncedSearch = useDebounce(searchText, 500); const debouncedSearch = useDebounce(searchText, 500);
const { mutate: DeleteExpense, isPending } = useDeleteExpense(); const { mutate: DeleteExpense, isPending } = useDeleteExpense();
const { data, isLoading, isError, isInitialLoading, error } = useExpenseList( const { data, isLoading, isError, isInitialLoading, error } = useExpenseList(
pageSize, ITEMS_PER_PAGE,
currentPage, currentPage,
filters, filters,
debouncedSearch debouncedSearch
@ -110,8 +110,9 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
label: "Submitted By", label: "Submitted By",
align: "text-start", align: "text-start",
getValue: (e) => getValue: (e) =>
`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""}`.trim() || `${e.createdBy?.firstName ?? ""} ${
"N/A", e.createdBy?.lastName ?? ""
}`.trim() || "N/A",
customRender: (e) => ( customRender: (e) => (
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<Avatar <Avatar
@ -185,26 +186,16 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
return ( return (
<> <>
{IsDeleteModalOpen && ( {IsDeleteModalOpen && (
<div <ConfirmModal
className={`modal fade show`} isOpen={IsDeleteModalOpen}
tabIndex="-1" type="delete"
role="dialog" header="Delete Expense"
style={{ message="Are you sure you want delete?"
display: "block", onSubmit={handleDelete}
backgroundColor: "rgba(0,0,0,0.5)", onClose={() => setIsDeleteModalOpen(false)}
}} loading={isPending}
aria-hidden="false" paramData={deletingId}
> />
<ConfirmModal
type="delete"
header="Delete Expense"
message="Are you sure you want delete?"
onSubmit={handleDelete}
onClose={() => setIsDeleteModalOpen(false)}
loading={isPending}
paramData={deletingId}
/>
</div>
)} )}
<div className="card px-0 px-sm-4"> <div className="card px-0 px-sm-4">

View File

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

View File

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

View File

@ -32,8 +32,8 @@ const WorkItem = ({
forWorkArea, forWorkArea,
deleteHandleTask, deleteHandleTask,
}) => { }) => {
const projectId = useSelector((store)=>store.localVariables.projectId) const projectId = useSelector((store) => store.localVariables.projectId);
const isTaskPlanning = /^\/activities\/task$/.test(location.pathname); const isTaskPlanning = /^\/activities\/task$/.test(location.pathname);
const [itemName, setItemName] = useState(""); const [itemName, setItemName] = useState("");
const [NewWorkItem, setNewWorkItem] = useState(); const [NewWorkItem, setNewWorkItem] = useState();
@ -135,25 +135,15 @@ const isTaskPlanning = /^\/activities\/task$/.test(location.pathname);
)} )}
{showModal2 && ( {showModal2 && (
<div <ConfirmModal
className={`modal fade ${showModal2 ? "show" : ""}`} isOpen={showModal2}
tabIndex="-1" type="delete"
role="dialog" header="Delete Activity"
style={{ message="Are you sure you want delete?"
display: showModal2 ? "block" : "none", onSubmit={handleSubmit}
backgroundColor: showModal2 ? "rgba(0,0,0,0.5)" : "transparent", onClose={closeModalDelete}
}} loading={isPending}
aria-hidden="false" />
>
<ConfirmModal
type={"delete"}
header={"Delete Activity"}
message={"Are you sure you want delete?"}
onSubmit={handleSubmit}
onClose={closeModalDelete}
loading={loadingDelete}
/>
</div>
)} )}
<tr key={NewWorkItem?.workItemId}> <tr key={NewWorkItem?.workItemId}>
@ -240,9 +230,7 @@ const isTaskPlanning = /^\/activities\/task$/.test(location.pathname);
</td> </td>
{(ManageInfra || {(ManageInfra ||
( (ManageAndAssignTak && PlannedWork !== CompletedWork)) && (
ManageAndAssignTak &&
PlannedWork !== CompletedWork)) && (
<td className="text-end align-items-middle border-top"> <td className="text-end align-items-middle border-top">
{/* Desktop (md and up): inline icons */} {/* Desktop (md and up): inline icons */}
<div className="d-none d-md-flex justify-content-end gap-1 px-2"> <div className="d-none d-md-flex justify-content-end gap-1 px-2">

View File

@ -0,0 +1,14 @@
import React from "react";
import Documents from "../Documents/Documents";
import { useSelectedProject } from "../../slices/apiDataManager";
import { DOCUMENTS_ENTITIES } from "../../utils/constants";
const ProjectDocuments = () => {
const selectedProject = useSelectedProject()
return (
<>
<Documents Document_Entity={DOCUMENTS_ENTITIES.ProjectEntity} Entity={selectedProject} />
</>
);
};
export default ProjectDocuments;

View File

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

View File

@ -1,84 +1,57 @@
import React from "react"; import React from "react";
import { hasUserPermission } from "../../utils/authUtils"; import { hasUserPermission } from "../../utils/authUtils";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { DIRECTORY_ADMIN, DIRECTORY_MANAGER, DIRECTORY_USER, VIEW_PROJECT_INFRA } from "../../utils/constants"; import {
DIRECTORY_ADMIN,
DIRECTORY_MANAGER,
DIRECTORY_USER,
VIEW_PROJECT_INFRA,
} from "../../utils/constants";
const ProjectNav = ({ onPillClick, activePill }) => { const ProjectNav = ({ onPillClick, activePill }) => {
const HasViewInfraStructure = useHasUserPermission( VIEW_PROJECT_INFRA ); const HasViewInfraStructure = useHasUserPermission(VIEW_PROJECT_INFRA);
const DirAdmin = useHasUserPermission(DIRECTORY_ADMIN); const DirAdmin = useHasUserPermission(DIRECTORY_ADMIN);
const DireManager = useHasUserPermission(DIRECTORY_MANAGER) const DireManager = useHasUserPermission(DIRECTORY_MANAGER);
const DirUser = useHasUserPermission(DIRECTORY_USER) const DirUser = useHasUserPermission(DIRECTORY_USER);
const ProjectTab = [
{ key: "profile", icon: "bx bx-user", label: "Profile" },
{ key: "teams", icon: "bx bx-group", label: "Teams" },
{
key: "infra",
icon: "bx bx-grid-alt",
label: "Infrastructure",
hidden: !HasViewInfraStructure,
},
{
key: "directory",
icon: "bx bxs-contact",
label: "Directory",
hidden: !(DirAdmin || DireManager || DirUser),
},
{ key: "documents", icon: "bx bx-folder-open", label: "Documents" },
{ key: "setting", icon: "bx bxs-cog", label: "Setting" },
];
return ( return (
<div className="nav-align-top"> <div className="nav-align-top">
<ul className="nav nav-tabs "> <ul className="nav nav-tabs">
<li className="nav-item"> {ProjectTab?.filter((tab) => !tab.hidden)?.map((tab) => (
<a <li key={tab.key} className="nav-item cursor-pointer">
className={`nav-link ${activePill === "profile" ? "active" : ""} fs-6`} <a
href="#"
onClick={(e) => { className={`nav-link ${
e.preventDefault(); activePill === tab.key ? "active cursor-pointer" : ""
onPillClick("profile"); } fs-6`}
}} onClick={(e) => {
> e.preventDefault();
<i className="bx bx-user bx-sm me-1_5"></i> <span className="d-none d-md-inline">Profile</span> onPillClick(tab.key);
</a> }}
</li> >
<li className="nav-item"> <i className={`${tab.icon} bx-sm me-1_5`}></i>
<a <span className="d-none d-md-inline ">{tab.label}</span>
className={`nav-link ${activePill === "teams" ? "active" : ""} fs-6`} </a>
href="#" </li>
onClick={(e) => { ))}
e.preventDefault();
onPillClick("teams");
}}
>
<i className="bx bx-group bx-sm me-1_5"></i><span className="d-none d-md-inline" > Teams</span>
</a>
</li>
<li className={`nav-item ${!HasViewInfraStructure && "d-none"} `}>
<a
className={`nav-link ${activePill === "infra" ? "active" : ""} fs-6`}
href="#"
onClick={(e) => {
e.preventDefault();
onPillClick("infra");
}}
>
<i className="bx bx-grid-alt bx-sm me-1_5"></i> <span className="d-none d-md-inline">Infrastructure</span>
</a>
</li>
{(DirAdmin || DireManager || DirUser) && (
<li className="nav-item">
<a
className={`nav-link ${activePill === "directory" ? "active" : ""} fs-6`}
href="#"
onClick={(e) => {
e.preventDefault(); // Prevent page reload
onPillClick("directory");
}}
>
<i className='bx bxs-contact bx-sm me-1_5'></i> <span className="d-none d-md-inline">Directory</span>
</a>
</li>
)}
<li className="nav-item">
<a
className={`nav-link ${
activePill === "imagegallary" ? "active" : ""
} fs-6`}
href="#"
onClick={(e) => {
e.preventDefault(); // Prevent page reload
onPillClick("imagegallary");
}}
>
<i className='bx bxs-cog bx-sm me-1_5'></i> <span className="d-none d-md-inline">project Setup</span>
</a>
</li>
</ul> </ul>
</div> </div>
); );

View File

@ -0,0 +1,181 @@
import React, { useEffect } from "react";
import {
useProjectLevelEmployeePermission,
useProjectLevelModules,
useUpdateProjectLevelEmployeePermission,
} from "../../hooks/useProjects";
import { useSelectedProject } from "../../slices/apiDataManager";
import { useEmployeesByProject } from "../../hooks/useEmployees";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import showToast from "../../services/toastService";
export const ProjectPermissionSchema = z.object({
employeeId: z.string().min(1, "Employee is required"),
selectedPermissions: z.array(z.string()).optional(),
});
const ProjectPermission = () => {
const selectedProject = useSelectedProject();
const { data: ProjectModules = [] } = useProjectLevelModules();
const { employees = [], loading } = useEmployeesByProject(selectedProject);
const {
register,
watch,
handleSubmit,
reset,
formState: { errors },
} = useForm({
resolver: zodResolver(ProjectPermissionSchema),
defaultValues: {
employeeId: "",
selectedPermissions: [],
},
});
const selectedEmployee = watch("employeeId");
const { data: selectedEmpPermissions } = useProjectLevelEmployeePermission(
selectedEmployee || "",
selectedProject
);
useEffect(() => {
if (!employees.length) return;
const enabledPerms =
selectedEmpPermissions?.permissions
?.filter((perm) => perm.isEnabled)
?.map((perm) => perm.id) || [];
reset({
employeeId: selectedEmployee || employees[0]?.id || "",
selectedPermissions: enabledPerms,
});
}, [selectedEmpPermissions, reset, selectedEmployee, employees]);
const { mutate: updatePermission, isPending } =
useUpdateProjectLevelEmployeePermission();
const onSubmit = (formData) => {
if (!formData.employeeId) {
showToast("Please select an employee", "warn");
return;
}
const existingPermissions = selectedEmpPermissions?.permissions || [];
const payloadPermissions =
existingPermissions.length > 0
? existingPermissions.map((perm) => ({
id: perm.id,
isEnabled: formData.selectedPermissions?.includes(perm.id) || false,
}))
: (formData.selectedPermissions || []).map((id) => ({
id,
isEnabled: true,
}));
if (payloadPermissions.length === 0) {
showToast("No permissions selected", "warn");
return;
}
const hasChanges = existingPermissions.some(
(perm) =>
perm.isEnabled !==
(formData.selectedPermissions?.includes(perm.id) || false)
);
if (!hasChanges && existingPermissions.length > 0) {
showToast("No changes detected", "info");
return;
}
const payload = {
employeeId: formData.employeeId,
projectId: selectedProject,
permission: payloadPermissions,
};
updatePermission(payload);
};
return (
<div className="row">
<form className="row" onSubmit={handleSubmit(onSubmit)}>
{/* Employee Dropdown */}
<div className="d-flex align-items-end gap-2">
<div className="text-start">
<label className="form-label">Select Employee</label>
<select
className="form-select form-select-sm"
{...register("employeeId")}
disabled={isPending}
>
{loading ? (
<option value="">Loading...</option>
) : (
<>
<option value="">-- Select --</option>
{employees.map((emp) => (
<option key={emp.id} value={emp.id}>
{emp.firstName} {emp.lastName}
</option>
))}
</>
)}
</select>
{errors.employeeId && (
<div className="text-danger small">
{errors.employeeId.message}
</div>
)}
</div>
<button className="btn btn-sm btn-primary" disabled={isPending || loading}>
{isPending ? "Please Wait..." : "Update Permission"}
</button>
</div>
{/* Permissions */}
{ProjectModules.map((feature) => (
<div key={feature.id} className="row my-2">
<div className="col-12 text-start fw-semibold mb-2">
{feature.name}
</div>
<div className="col-12">
<div className="row">
{feature.featurePermissions?.map((perm) => (
<div
className="col-12 col-sm-6 col-md-4 mb-2"
key={perm.id}
>
<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}
value={perm.id}
{...register("selectedPermissions")}
/>
{perm.name}
</label>
</div>
))}
</div>
</div>
<hr className="my-2" />
</div>
))}
</form>
</div>
);
};
export default ProjectPermission;

View File

@ -0,0 +1,74 @@
import React, { useState } from "react";
import { ComingSoonPage } from "../../pages/Misc/ComingSoonPage";
import ProjectPermission from "./ProjectPermission";
const ProjectSetting = () => {
const [activePill, setActivePill] = useState(() => {
return localStorage.getItem("lastActiveProjectSettingTab") || "Permissions";
});
const projectSettingTab = [
{ key: "Permissions", label: "Permissions" },
{ key: "Notification", label: "Notification" },
{ key: "SeparatedLink", label: "Separated link", isButton: true },
];
const handlePillClick = (pillKey) => {
setActivePill(pillKey);
localStorage.setItem("lastActiveProjectSettingTab", pillKey);
};
const renderContent = () => {
switch (activePill) {
case "Permissions":
return <ProjectPermission />;
case "Notification":
return <ComingSoonPage />;
default:
return <ComingSoonPage />;
}
};
return (
<div className="w-100">
<div className="card p-3">
<div className="col-4">
<div className="dropdown text-start">
<button
className="btn btn-sm btn-outline-primary dropdown-toggle"
type="button"
id="dropdownMenuButton"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{activePill || "Select Option"}
</button>
<ul className="dropdown-menu" aria-labelledby="dropdownMenuButton">
{projectSettingTab.map((item) =>
item.isButton ? (
<li key={item.key}>
<button className="dropdown-item">{item.label}</button>
</li>
) : (
<li key={item.key}>
<button
className="dropdown-item"
onClick={() => handlePillClick(item.key)}
>
{item.label}
</button>
</li>
)
)}
</ul>
</div>
</div>
<div className="mt-3">{renderContent()}</div>
</div>
</div>
);
};
export default ProjectSetting;

View File

@ -18,12 +18,12 @@ import {
useEmployeesByProjectAllocated, useEmployeesByProjectAllocated,
useManageProjectAllocation, useManageProjectAllocation,
} from "../../hooks/useProjects"; } from "../../hooks/useProjects";
import { useSelectedproject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
const Teams = () => { const Teams = () => {
// const {projectId} = useParams() // const {projectId} = useParams()
// const projectId = useSelector((store)=>store.localVariables.projectId) // const projectId = useSelector((store)=>store.localVariables.projectId)
const projectId = useSelectedproject(); const projectId = useSelectedProject();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { data, loading } = useMaster(); const { data, loading } = useMaster();
@ -249,26 +249,15 @@ const Teams = () => {
</div> </div>
{IsDeleteModal && ( {IsDeleteModal && (
<div <ConfirmModal
className={`modal fade ${IsDeleteModal ? "show" : ""}`} isOpen={IsDeleteModal}
tabIndex="-1" type="delete"
role="dialog" header="Removed Employee"
style={{ message="Are you sure you want delete?"
display: IsDeleteModal ? "block" : "none", onSubmit={() => removeAllocation(deleteEmployee)}
backgroundColor: IsDeleteModal ? "rgba(0,0,0,0.5)" : "transparent", onClose={closeDeleteModal}
}} loading={isPending}
aria-hidden="false" />
>
<ConfirmModal
type={"delete"}
header={"Removed Employee"}
message={"Are you sure you want delete?"}
onSubmit={removeAllocation}
onClose={closeDeleteModal}
loading={isPending}
paramData={deleteEmployee}
/>
</div>
)} )}
<div className="card card-action mb-6"> <div className="card card-action mb-6">

View File

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

View File

@ -48,7 +48,7 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
const data = getValues(); const data = getValues();
// onSubmitTenant(data); // onSubmitTenant(data);
// onNext(); // onNext();
const tenantPayload = {...data,onBoardingDate: moment.utc(data.onBoardingDate, "DD-MM-YYYY").toISOString() } const tenantPayload = { ...data, onBoardingDate: moment.utc(data.onBoardingDate, "DD-MM-YYYY").toISOString() }
CreateTenant(tenantPayload); CreateTenant(tenantPayload);
} }
}; };
@ -73,7 +73,7 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
</div> </div>
<div className="col-sm-6"> <div className="col-sm-6">
<Label htmlFor="officeNumber" required> <Label htmlFor="officeNumber" >
Office Number Office Number
</Label> </Label>
<input <input
@ -87,7 +87,7 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
</div> </div>
<div className="col-sm-6"> <div className="col-sm-6">
<Label htmlFor="domainName" required> <Label htmlFor="domainName" >
Domain Name Domain Name
</Label> </Label>
<input <input
@ -101,7 +101,7 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
</div> </div>
<div className="col-sm-6"> <div className="col-sm-6">
<Label htmlFor="taxId" required> <Label htmlFor="taxId" >
Tax ID Tax ID
</Label> </Label>
<input <input
@ -138,8 +138,10 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
</Label> </Label>
<select <select
className="form-select form-select-sm" id="organizationSize"
{...register("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) => ( {orgSize.map((org) => (
<option key={org.val} value={org.val}> <option key={org.val} value={org.val}>
@ -147,17 +149,20 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
</option> </option>
))} ))}
</select> </select>
{errors.organizationSize && ( {errors.organizationSize && (
<div className="danger-text">{errors.organizationSize.message}</div> <div className="danger-text">{errors.organizationSize.message}</div>
)} )}
</div> </div>
<div className="col-sm-6"> <div className="col-sm-6">
<Label htmlFor="industryId" required> <Label htmlFor="industryId" required>
Industry Industry
</Label> </Label>
<select <select
className="form-select form-select-sm" id="industryId"
className="form-select shadow-none border py-1 px-2 small"
{...register("industryId")} {...register("industryId")}
> >
{industryLoading ? ( {industryLoading ? (
@ -177,9 +182,9 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
<div className="col-sm-6"> <div className="col-sm-6">
<Label htmlFor="reference">Reference</Label> <Label htmlFor="reference">Reference</Label>
<select <select
className="form-select form-select-sm" id="reference"
className="form-select shadow-none border py-1 px-2 small"
{...register("reference")} {...register("reference")}
> >
{reference.map((org) => ( {reference.map((org) => (
@ -193,6 +198,7 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
)} )}
</div> </div>
<div className="col-sm-12"> <div className="col-sm-12">
<Label htmlFor="description">Description</Label> <Label htmlFor="description">Description</Label>
<textarea <textarea

View File

@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import React, { useState,useCallback } from "react"; import React, { useState, useCallback, useEffect } from "react";
import { FormProvider, useForm, useFormContext } from "react-hook-form"; import { FormProvider, useForm, useFormContext } from "react-hook-form";
import { defaultFilterValues, filterSchema } from "./TenantSchema"; import { defaultFilterValues, filterSchema } from "./TenantSchema";
import Label from "../common/Label"; import Label from "../common/Label";
@ -8,15 +8,16 @@ import { useIndustries } from "../../hooks/useTenant";
import { reference, TENANT_STATUS } from "../../utils/constants"; import { reference, TENANT_STATUS } from "../../utils/constants";
import { DateRangePicker1 } from "../common/DateRangePicker"; import { DateRangePicker1 } from "../common/DateRangePicker";
import moment from "moment"; import moment from "moment";
import { useLocation } from "react-router-dom";
const TenantFilterPanel = ({ onApply }) => {
const [resetKey, setResetKey] = useState(0);
const TenantFilterPanel = ({onApply}) => {
const [resetKey, setResetKey] = useState(0);
const methods = useForm({ const methods = useForm({
resolver: zodResolver(filterSchema), resolver: zodResolver(filterSchema),
defaultValues: defaultFilterValues, defaultValues: defaultFilterValues,
}); });
const { handleSubmit, reset } = methods; const { handleSubmit, reset } = methods;
const { data: industries = [], isLoading } = useIndustries(); const { data: industries = [], isLoading } = useIndustries();
@ -36,6 +37,13 @@ const [resetKey, setResetKey] = useState(0);
[onApply, handleClosePanel] [onApply, handleClosePanel]
); );
// Close popup when navigating to another component
const location = useLocation();
useEffect(() => {
handleClosePanel();
}, [location]);
const onClear = useCallback(() => { const onClear = useCallback(() => {
reset(defaultFilterValues); reset(defaultFilterValues);
setResetKey((prev) => prev + 1); // triggers DateRangePicker reset setResetKey((prev) => prev + 1); // triggers DateRangePicker reset
@ -48,44 +56,44 @@ const [resetKey, setResetKey] = useState(0);
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="text-start mb-1"> <div className="text-start mb-1">
<div className="text-start my-2"> <div className="text-start my-2">
<DateRangePicker1 <DateRangePicker1
placeholder="DD-MM-YYYY To DD-MM-YYYY" placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="startDate" startField="startDate"
endField="endDate" endField="endDate"
resetSignal={resetKey} resetSignal={resetKey}
defaultRange={false} defaultRange={false}
/> />
</div> </div>
<div className="text-strat mb-2"> <div className="text-strat mb-2">
<SelectMultiple <SelectMultiple
name="industryIds" name="industryIds"
label="Industries" label="Industries"
options={industries} options={industries}
labelKey="name" labelKey="name"
valueKey="id" valueKey="id"
/> />
</div> </div>
<div className="text-start mb-2"> <div className="text-start mb-2">
<SelectMultiple <SelectMultiple
name="references" name="references"
label="References" label="References"
options={reference} options={reference}
labelKey="name" labelKey="name"
valueKey="val" valueKey="val"
/> />
</div> </div>
<div className="text-start"> <div className="text-start">
<SelectMultiple <SelectMultiple
name="tenantStatusIds" name="tenantStatusIds"
label="Tenant Status" label="Tenant Status"
options={TENANT_STATUS} options={TENANT_STATUS}
labelKey="name" labelKey="name"
valueKey="id" valueKey="id"
/> />
</div> </div>
{/* <SelectMultiple {/* <SelectMultiple
name="references" name="references"
@ -100,7 +108,7 @@ const [resetKey, setResetKey] = useState(0);
type="button" type="button"
className="btn btn-secondary btn-xs" className="btn btn-secondary btn-xs"
onClick={onClear} onClick={onClear}
> >
Clear Clear
</button> </button>

View File

@ -2,34 +2,34 @@ import { z } from "zod";
export const newTenantSchema = z.object({ export const newTenantSchema = z.object({
firstName: z firstName: z
.string().trim() .string().trim()
.min(1, { message: "First Name is required!" }) .min(1, { message: "First Name is required!" })
.regex(/^[A-Za-z]+$/, { message: "First Name should contain only letters!" }), .regex(/^[A-Za-z]+$/, { message: "First Name should contain only letters!" }),
lastName: z lastName: z
.string().trim() .string().trim()
.min(1, { message: "Last Name is required!" }) .min(1, { message: "Last Name is required!" })
.regex(/^[A-Za-z]+$/, { message: "Last Name should contain only letters!" }), .regex(/^[A-Za-z]+$/, { message: "Last Name should contain only letters!" }),
email: z.string().trim().email("Invalid email address"), email: z.string().trim().email("Invalid email address"),
description: z.string().trim().optional(), description: z.string().trim().optional(),
domainName: z.string().trim().nonempty("Domain name is required"), domainName: z.string().trim().optional(),
billingAddress: z.string().trim().nonempty("Billing address is required"), billingAddress: z.string().trim().nonempty("Billing address is required"),
taxId: z.string().trim().nonempty("Tax ID is required"), taxId: z.string().trim().optional(),
logoImage: z.string().trim().optional(), logoImage: z.string().trim().optional(),
organizationName: z.string().trim().nonempty("Organization name is required"), organizationName: z.string().trim().nonempty("Organization name is required"),
officeNumber: z.string().trim().nonempty("Office number is required"), officeNumber: z.string().trim().optional(),
contactNumber: z.string().trim() contactNumber: z.string().trim()
.nonempty("Contact number is required") .nonempty("Contact number is required")
.regex(/^\+?[1-9]\d{7,14}$/, "Enter a valid contact number"), .regex(/^\+?[1-9]\d{7,14}$/, "Enter a valid contact number"),
onBoardingDate: z.preprocess((val) => { onBoardingDate: z.preprocess((val) => {
if (typeof val === "string" && val.includes("-")) { if (typeof val === "string" && val.includes("-")) {
const [day, month, year] = val.split("-"); const [day, month, year] = val.split("-");
return new Date(`${year}-${month}-${day}`); return new Date(`${year}-${month}-${day}`);
} }
return val; return val;
}, z.date({ }, z.date({
required_error: "Onboarding date is required", required_error: "Onboarding date is required",
invalid_type_error: "Invalid date format", invalid_type_error: "Invalid date format",
})), })),
organizationSize: z.string().nonempty("Organization size is required"), organizationSize: z.string().nonempty("Organization size is required"),
industryId: z.string().uuid("Invalid industry ID"), industryId: z.string().uuid("Invalid industry ID"),
reference: z.string().nonempty("Reference is required"), reference: z.string().nonempty("Reference is required"),
@ -71,8 +71,8 @@ export const getSubscriptionSchema = (minUsers) =>
export const subscriptionDefaultValues = { export const subscriptionDefaultValues = {
// tenantId: "", // tenantId: "",
planId: "", planId: "",
currencyId: "", currencyId: "",
maxUsers: 1, maxUsers: 1,
frequency: 1, frequency: 1,
isTrial: false, isTrial: false,
@ -84,8 +84,8 @@ export const filterSchema = z.object({
// createdByIds: z.array(z.string()).optional(), // createdByIds: z.array(z.string()).optional(),
tenantStatusIds: z.array(z.string()).optional(), tenantStatusIds: z.array(z.string()).optional(),
references: z.array(z.string()).optional(), references: z.array(z.string()).optional(),
startDate: z.string().optional(), startDate: z.string().optional(),
endDate: z.string().optional(), endDate: z.string().optional(),
}); });
export const defaultFilterValues = { export const defaultFilterValues = {
industryIds: [], industryIds: [],
@ -133,23 +133,23 @@ export const getStepFields = (stepIndex) => {
export const EditTenant = z.object({ export const EditTenant = z.object({
firstName: z firstName: z
.string().trim() .string().trim()
.min(1, { message: "First Name is required!" }) .min(1, { message: "First Name is required!" })
.regex(/^[A-Za-z]+$/, { message: "First Name should contain only letters!" }), .regex(/^[A-Za-z]+$/, { message: "First Name should contain only letters!" }),
lastName: z lastName: z
.string().trim() .string().trim()
.min(1, { message: "Last Name is required!" }) .min(1, { message: "Last Name is required!" })
.regex(/^[A-Za-z]+$/, { message: "Last Name should contain only letters!" }), .regex(/^[A-Za-z]+$/, { message: "Last Name should contain only letters!" }),
description: z.string().trim().optional(), description: z.string().trim().optional(),
domainName: z.string().trim().min(1, { message: "Domain Name is required!" }), domainName: z.string().trim().optional(),
billingAddress: z.string().trim().min(1, { message: "Billing Address is required!" }), billingAddress: z.string().trim().min(1, { message: "Billing Address is required!" }),
taxId: z.string().trim().min(1, { message: "Tax ID is required!" }), taxId: z.string().trim().optional(),
logoImage: z.string().optional(), logoImage: z.string().optional(),
officeNumber: z.string().trim().min(1, { message: "Office Number is required!" }), officeNumber: z.string().trim().optional(),
contactNumber: z.string().trim() contactNumber: z.string().trim()
.nonempty("Contact number is required") .nonempty("Contact number is required")
.regex(/^\+?[1-9]\d{7,14}$/, "Enter a valid contact number"), .regex(/^\+?[1-9]\d{7,14}$/, "Enter a valid contact number"),
organizationSize: z.string().min(1, { message: "Organization Size is required!" }), organizationSize: z.string().min(1, { message: "Organization Size is required!" }),
industryId: z.string().min(1,{ message: "Invalid Industry ID!" }), industryId: z.string().min(1, { message: "Invalid Industry ID!" }),
reference: z.string().optional(), reference: z.string().optional(),
}); });

View File

@ -1,70 +1,71 @@
import React, { useState } from 'react'; import React from "react";
const ConfirmModal = ({ type, onSubmit, onClose, message, loading ,header, paramData}) => { const ConfirmModal = ({
type,
const TypeofIcon = (type) => { onSubmit,
switch (type) { onClose,
case "delete": message,
return <i className='bx bx-x-circle text-danger ' style={{fontSize:"60px"}} ></i>; loading,
default: header,
return null; paramData,
isOpen = false,
}) => {
if (!isOpen) return null;
const TypeofIcon = () => {
if (type === "delete") {
return (
<i
className="bx bx-x-circle text-danger"
style={{ fontSize: "60px" }}
></i>
);
} }
return null;
}; };
const TypeofModal = (type) => { const modalSize = type === "delete" ? "sm" : "md";
switch (type) {
case "delete":
return "sm";
case "other":
return "md";
default:
return "sm";
}
};
return ( return (
<div className={`modal-dialog modal-${TypeofModal(type)} modal-simple modal-confirm`}> <div
<div className='modal-dialog modal-dialog-centered'> className="modal fade show"
style={{ display: "block", backgroundColor: "rgba(0,0,0,0.5)" }}
role="dialog"
aria-modal="true"
>
<div className={`modal-dialog modal-${modalSize} modal-dialog-top`}>
<div className="modal-content"> <div className="modal-content">
<div className="modal-body py-1 px-2"> <div className="modal-body py-1 px-2">
<div className="row"> <div className="d-flex justify-content-between mb-4 pt-2">
{header && <strong className="mb-0">{header}</strong>}
<div className="text-start mb-1">
<div className=' d-flex justify-content-between mb-4'>
{header && < strong className='mb-0 font-weight-bold'>{header }</strong>}
<button <button
type="button" type="button"
className="btn-close" className="btn-close "
aria-label="Close" aria-label="Close"
onClick={onClose} onClick={onClose}
/> />
</div> </div>
<div className='row'> <div className="row">
<div className='col-4 col-sm-2'> {TypeofIcon(type)}</div> <div className="col-4 col-sm-2">{TypeofIcon()}</div>
<div className='col-8 col-sm-10 py-sm-2 py-1 text-sm-start'> <div className="col-8 col-sm-10 py-sm-2 py-1 text-sm-start">
<span className='fs-6 text'>{message}</span> <span className="fs-6">{message}</span>
<div className='d-flex justify-content-end mt-4'> <div className="d-flex justify-content-end mt-4">
<button <button
className='btn btn-primary btn-sm' className="btn btn-primary btn-sm"
onClick={()=>onSubmit(paramData)} onClick={() => onSubmit(paramData)}
disabled={loading} disabled={loading}
> >
{loading ? "Please Wait..." : "Yes"} {loading ? "Please Wait..." : "Yes"}
</button> </button>
<button <button
className='btn btn-secondary ms-4 btn-sm' className="btn btn-secondary ms-4 btn-sm"
onClick={onClose} onClick={onClose}
disabled={loading} disabled={loading}
> >
No No
</button> </button>
</div> </div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -46,6 +46,12 @@ const DateRangePicker = ({
}; };
}, [onRangeChange, DateDifference, endDateMode]); }, [onRangeChange, DateDifference, endDateMode]);
const handleIconClick = () => {
if (inputRef.current) {
inputRef.current._flatpickr.open(); // directly opens flatpickr
}
};
return ( return (
<div className={`col-${sm} col-sm-${md} px-1`}> <div className={`col-${sm} col-sm-${md} px-1`}>
<input <input
@ -57,7 +63,7 @@ const DateRangePicker = ({
/> />
<i <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" }} style={{ right: "22px", bottom: "-8px" }}
></i> ></i>
</div> </div>
@ -77,7 +83,7 @@ export const DateRangePicker1 = ({
className = "", className = "",
allowText = false, allowText = false,
resetSignal, resetSignal,
defaultRange = true, defaultRange = true,
...rest ...rest
}) => { }) => {
const inputRef = useRef(null); const inputRef = useRef(null);

View File

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

View File

@ -2,111 +2,67 @@ import { useFormContext, useWatch } from "react-hook-form";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Label from "./Label"; import Label from "./Label";
const TagInput = ({ const TagInput = ({ label, name, placeholder, color = "#e9ecef", options = [] }) => {
label = "Tags", const { setValue, watch } = useFormContext();
name = "tags", const tags = watch(name) || [];
placeholder = "Start typing to add... like employee, manager",
color = "#e9ecef",
options = [],
}) => {
const [tags, setTags] = useState([]);
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const [suggestions, setSuggestions] = useState([]); const [suggestions, setSuggestions] = useState([]);
const { setValue, trigger, control } = useFormContext();
const watchedTags = useWatch({ control, name });
const handleAdd = (newTag) => {
const tagObj = typeof newTag === "string" ? { name: newTag, isActive: true } : newTag;
useEffect(() => { if (!tags.some((t) => t.name === tagObj.name)) {
if ( setValue(name, [...tags, tagObj], { shouldValidate: true });
Array.isArray(watchedTags) &&
JSON.stringify(tags) !== JSON.stringify(watchedTags)
) {
setTags(watchedTags);
}
}, [JSON.stringify(watchedTags)]);
useEffect(() => {
if (input.trim() === "") {
setSuggestions([]);
} else {
const filtered = options?.filter(
(opt) =>
opt?.name?.toLowerCase()?.includes(input.toLowerCase()) &&
!tags?.some((tag) => tag.name === opt.name)
);
setSuggestions(filtered);
} }
}, [input, options, tags]); };
const addTag = async (tagObj) => { const handleRemove = (tagName) => {
if (!tags.some((tag) => tag.name === tagObj.name)) { setValue(
const cleanedTag = { name,
id: tagObj.id ?? null, tags.filter((t) => t.name !== tagName),
name: tagObj.name, { shouldValidate: true }
}; );
const newTags = [...tags, cleanedTag]; };
setTags(newTags);
setValue(name, newTags, { shouldValidate: true }); const handleKeyDown = (e) => {
await trigger(name); if (e.key === "Enter" && input.trim()) {
e.preventDefault();
handleAdd(input.trim());
setInput(""); setInput("");
setSuggestions([]); setSuggestions([]);
} }
}; };
const removeTag = (indexToRemove) => { const handleChange = (e) => {
const newTags = tags.filter((_, i) => i !== indexToRemove); const val = e.target.value;
setTags(newTags); setInput(val);
setValue(name, newTags, { shouldValidate: true });
trigger(name);
};
const handleInputKeyDown = (e) => { if (val) {
if ((e.key === "Enter" || e.key === " ")&& input.trim() !== "") { setSuggestions(
e.preventDefault(); options
const existing = options.find( .filter((opt) => {
(opt) => opt.name.toLowerCase() === input.trim().toLowerCase() const label = typeof opt === "string" ? opt : opt.name;
); return (
const newTag = existing label.toLowerCase().includes(val.toLowerCase()) &&
? existing !tags.some((t) => t.name === label)
: { );
id: null, })
name: input.trim(), .map((opt) => ({
description: input.trim(), name: typeof opt === "string" ? opt : opt.name,
}; isActive: true,
addTag(newTag); }))
} else if (e.key === "Backspace" && input === "") {
setTags((prev) => prev.slice(0, -1));
}
};
const handleInputKey = (e) => {
const key = e.key?.toLowerCase();
if ((key === "enter" || key === " " || e.code === "Space") && input.trim() !== "") {
e.preventDefault();
const existing = options.find(
(opt) => opt.name.toLowerCase() === input.trim().toLowerCase()
); );
const newTag = existing } else {
? existing setSuggestions([]);
: {
id: null,
name: input.trim(),
description: input.trim(),
};
addTag(newTag);
} else if ((key === "backspace" || e.code === "Backspace") && input === "") {
setTags((prev) => prev.slice(0, -1));
} }
}; };
const handleSuggestionClick = (sugg) => {
const handleSuggestionClick = (suggestion) => { handleAdd(sugg);
addTag(suggestion); setInput("");
setSuggestions([]);
}; };
const backgroundColor = color || "#f8f9fa";
const iconColor = `var(--bs-${color})`;
return ( return (
<> <>
<Label htmlFor={name} className="form-label" required> <Label htmlFor={name} className="form-label" required>
@ -123,17 +79,16 @@ useEffect(() => {
key={index} key={index}
className="d-flex align-items-center" className="d-flex align-items-center"
style={{ style={{
color: iconColor, backgroundColor: color,
backgroundColor,
padding: "2px 6px", padding: "2px 6px",
borderRadius: "2px", borderRadius: "2px",
fontSize: "0.85rem", fontSize: "0.8rem",
}} }}
> >
{tag.name} {tag.name}
<i <i
className="bx bx-x bx-xs ms-1" className="bx bx-x bx-xs ms-1"
onClick={() => removeTag(index)} onClick={() => handleRemove(tag.name)}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
/> />
</span> </span>
@ -142,9 +97,8 @@ useEffect(() => {
<input <input
type="text" type="text"
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={handleChange}
onKeyDown={handleInputKeyDown} onKeyDown={handleKeyDown}
onKeyUp={handleInputKey}
placeholder={placeholder} placeholder={placeholder}
style={{ style={{
border: "none", border: "none",
@ -157,12 +111,13 @@ useEffect(() => {
{suggestions.length > 0 && ( {suggestions.length > 0 && (
<ul <ul
className="list-group position-absolute mt-1 bg-white w-50 shadow-sm " className="list-group position-absolute mt-1 bg-white w-50 shadow-sm"
style={{ style={{
zIndex: 1000, zIndex: 1000,
maxHeight: "150px", maxHeight: "150px",
overflowY: "auto", overflowY: "auto",
boxShadow:"0px 4px 10px rgba(0, 0, 0, 0.2)",borderRadius:"3px",border:"1px solid #ddd" borderRadius: "3px",
border: "1px solid #ddd",
}} }}
> >
{suggestions.map((sugg, i) => ( {suggestions.map((sugg, i) => (
@ -170,8 +125,7 @@ useEffect(() => {
key={i} key={i}
className="dropdown-item p-1 hoverBox" className="dropdown-item p-1 hoverBox"
onClick={() => handleSuggestionClick(sugg)} onClick={() => handleSuggestionClick(sugg)}
style={{cursor: "pointer", fontSize: "0.875rem"}} style={{ cursor: "pointer", fontSize: "0.875rem" }}
> >
{sugg.name} {sugg.name}
</li> </li>
@ -182,4 +136,7 @@ useEffect(() => {
</> </>
); );
}; };
export default TagInput; export default TagInput;

View File

@ -1,10 +1,10 @@
import React, { useEffect,useState } from 'react' import React, { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository'; import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice'; import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager'; import { getCachedData, cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService'; import showToast from '../../services/toastService';
import {useCreateContactCategory} from '../../hooks/masterHook/useMaster'; import {useCreateContactCategory} from '../../hooks/masterHook/useMaster';
import Label from '../common/Label'; import Label from '../common/Label';
@ -13,32 +13,32 @@ import Label from '../common/Label';
const schema = z.object({ const schema = z.object({
name: z.string().min(1, { message: "Category name is required" }), name: z.string().min(1, { message: "Category name is required" }),
description: z.string().min(1, { message: "Description 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 { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
reset, reset,
} = useForm({ } = useForm({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
name: "", name: "",
description: "", description: "",
}, },
}); });
const [descriptionLength, setDescriptionLength] = useState(0); const [descriptionLength, setDescriptionLength] = useState(0);
const maxDescriptionLength = 255; const maxDescriptionLength = 255;
const { mutate: createContactCategory, isPending: isLoading } = useCreateContactCategory(() => onClose?.()); const { mutate: createContactCategory, isPending: isLoading } = useCreateContactCategory(() => onClose?.());
const onSubmit = (payload) => { const onSubmit = (payload) => {
createContactCategory(payload); createContactCategory(payload);
}; };
// const onSubmit = (data) => { // const onSubmit = (data) => {
// setIsLoading(true) // setIsLoading(true)
// MasterRespository.createContactCategory(data).then((resp)=>{ // MasterRespository.createContactCategory(data).then((resp)=>{
@ -55,15 +55,15 @@ const onSubmit = (payload) => {
// setIsLoading(false) // setIsLoading(false)
// }) // })
// }; // };
const resetForm = () => { const resetForm = () => {
reset({ name: "", description: "" }); reset({ name: "", description: "" });
setDescriptionLength(0); setDescriptionLength(0);
}; };
useEffect(() => {
return () => resetForm();
}, []);
useEffect(() => {
return () => resetForm();
}, []);
return (<> return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}> <form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12 text-start"> <div className="col-12 col-md-12 text-start">
@ -88,10 +88,10 @@ useEffect(() => {
<div className="text-end small text-muted"> <div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left {maxDescriptionLength - descriptionLength} characters left
</div> </div>
{errors.description && ( {errors.description && (
<p className="text-danger">{errors.description.message}</p> <p className="text-danger">{errors.description.message}</p>
)} )}
</div> </div>
<div className="col-12 text-end"> <div className="col-12 text-end">
<button <button
@ -107,10 +107,10 @@ useEffect(() => {
</button> </button>
</div> </div>
</form> </form>
</> </>
) )
} }

View File

@ -1,10 +1,10 @@
import React, { useEffect,useState } from 'react' import React, { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository'; import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice'; import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager'; import { getCachedData, cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService'; import showToast from '../../services/toastService';
import {useCreateContactTag} from '../../hooks/masterHook/useMaster'; import {useCreateContactTag} from '../../hooks/masterHook/useMaster';
import Label from '../common/Label'; import Label from '../common/Label';
@ -13,31 +13,31 @@ import Label from '../common/Label';
const schema = z.object({ const schema = z.object({
name: z.string().min(1, { message: "Tag name is required" }), name: z.string().min(1, { message: "Tag name is required" }),
description: z.string().min(1, { message: "Description 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 CreateContactTag = ({ onClose }) => {
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
reset, reset,
} = useForm({ } = useForm({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
name: "", name: "",
description: "", description: "",
}, },
}); });
const [descriptionLength, setDescriptionLength] = useState(0); const [descriptionLength, setDescriptionLength] = useState(0);
const maxDescriptionLength = 255; const maxDescriptionLength = 255;
const { mutate: createContactTag, isPending: isLoading } = useCreateContactTag(() => onClose?.()); const { mutate: createContactTag, isPending: isLoading } = useCreateContactTag(() => onClose?.());
const onSubmit = (payload) => { const onSubmit = (payload) => {
createContactTag(payload); createContactTag(payload);
}; };
// const onSubmit = (data) => { // const onSubmit = (data) => {
// setIsLoading(true) // setIsLoading(true)
// MasterRespository.createContactTag(data).then((resp)=>{ // MasterRespository.createContactTag(data).then((resp)=>{
@ -55,15 +55,15 @@ const onSubmit = (payload) => {
// setIsLoading(false) // setIsLoading(false)
// }) // })
// }; // };
const resetForm = () => { const resetForm = () => {
reset({ name: "", description: "" }); reset({ name: "", description: "" });
setDescriptionLength(0); setDescriptionLength(0);
}; };
useEffect(() => {
return () => resetForm();
}, []);
useEffect(() => {
return () => resetForm();
}, []);
return (<> return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}> <form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12 text-start"> <div className="col-12 col-md-12 text-start">
@ -88,10 +88,10 @@ useEffect(() => {
<div className="text-end small text-muted"> <div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left {maxDescriptionLength - descriptionLength} characters left
</div> </div>
{errors.description && ( {errors.description && (
<p className="text-danger">{errors.description.message}</p> <p className="text-danger">{errors.description.message}</p>
)} )}
</div> </div>
<div className="col-12 text-end"> <div className="col-12 text-end">
<button <button
@ -107,10 +107,10 @@ useEffect(() => {
</button> </button>
</div> </div>
</form> </form>
</> </>
) )
} }

View File

@ -1,10 +1,10 @@
import React, { useEffect,useState } from 'react' import React, { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository'; import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice'; import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager'; import { getCachedData, cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService'; import showToast from '../../services/toastService';
import {useCreateJobRole} from '../../hooks/masterHook/useMaster'; import {useCreateJobRole} from '../../hooks/masterHook/useMaster';
import Label from '../common/Label'; import Label from '../common/Label';
@ -13,10 +13,10 @@ import Label from '../common/Label';
const schema = z.object({ const schema = z.object({
role: z.string().min(1, { message: "Role is required" }), role: z.string().min(1, { message: "Role is required" }),
description: z.string().min(1, { message: "Description 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 maxDescriptionLength = 255;
const [descriptionLength, setDescriptionLength] = useState(0); const [descriptionLength, setDescriptionLength] = useState(0);
@ -40,34 +40,34 @@ const CreateJobRole = ({onClose}) => {
setDescriptionLength(0); setDescriptionLength(0);
}; };
const { mutate: createJobRole, isPending:isLoading } = useCreateJobRole(() => { const { mutate: createJobRole, isPending: isLoading } = useCreateJobRole(() => {
onClose?.(); onClose?.();
} ); });
// const onSubmit = (data) => { // const onSubmit = (data) => {
// setIsLoading(true) // setIsLoading(true)
// const result = { // const result = {
// name: data.role, // name: data.role,
// description: data.description, // 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");
// onClose() // MasterRespository.createJobRole(result).then((resp)=>{
// }).catch((error)=>{ // setIsLoading(false)
// showToast(error.message, "error"); // resetForm()
// setIsLoading(false) // 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 onSubmit = (data) => {
const payload = { const payload = {
name: data.role, name: data.role,
@ -88,7 +88,7 @@ const CreateJobRole = ({onClose}) => {
}, []); }, []);
return (<> return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}> <form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
{/* <div className="col-12 col-md-12"> {/* <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> <label className="fs-5 text-dark text-center d-flex align-items-center justify-content-center flex-wrap">Create Job Role</label>
</div> */} </div> */}
@ -114,10 +114,10 @@ const CreateJobRole = ({onClose}) => {
<div className="text-end small text-muted"> <div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left {maxDescriptionLength - descriptionLength} characters left
</div> </div>
{errors.description && ( {errors.description && (
<p className="text-danger">{errors.description.message}</p> <p className="text-danger">{errors.description.message}</p>
)} )}
</div> </div>
<div className="col-12 text-end"> <div className="col-12 text-end">
<button <button
@ -133,10 +133,10 @@ const CreateJobRole = ({onClose}) => {
</button> </button>
</div> </div>
</form> </form>
</> </>
) )
} }

View File

@ -1,10 +1,10 @@
import React, { useEffect,useState } from 'react' import React, { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository'; import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice'; import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager'; import { getCachedData, cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService'; import showToast from '../../services/toastService';
import {useCreateWorkCategory} from '../../hooks/masterHook/useMaster'; import {useCreateWorkCategory} from '../../hooks/masterHook/useMaster';
import Label from '../common/Label'; import Label from '../common/Label';
@ -13,69 +13,69 @@ import Label from '../common/Label';
const schema = z.object({ const schema = z.object({
name: z.string().min(1, { message: "Category name is required" }), name: z.string().min(1, { message: "Category name is required" }),
description: z.string().min(1, { message: "Description 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 { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
reset, reset,
} = useForm({ } = useForm({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
name: "", name: "",
description: "", 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: "",
}); });
setDescriptionLength(0);
};
useEffect(() => { const [descriptionLength, setDescriptionLength] = useState(0);
return () => resetForm(); 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 (<> return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}> <form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
{/* <div className="col-12 col-md-12"> {/* <div className="col-12 col-md-12">
@ -104,10 +104,10 @@ useEffect(() => {
<div className="text-end small text-muted"> <div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left {maxDescriptionLength - descriptionLength} characters left
</div> </div>
{errors.description && ( {errors.description && (
<p className="text-danger">{errors.description.message}</p> <p className="text-danger">{errors.description.message}</p>
)} )}
</div> </div>
<div className="col-12 text-end"> <div className="col-12 text-end">
<button <button
@ -123,10 +123,10 @@ useEffect(() => {
</button> </button>
</div> </div>
</form> </form>
</> </>
) )
} }

View File

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

View File

@ -1,10 +1,10 @@
import React, { useEffect,useState } from 'react' import React, { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository'; import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice'; import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager'; import { getCachedData, cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService'; import showToast from '../../services/toastService';
import {useUpdateContactCategory} from '../../hooks/masterHook/useMaster'; import {useUpdateContactCategory} from '../../hooks/masterHook/useMaster';
import Label from '../common/Label'; import Label from '../common/Label';
@ -13,38 +13,38 @@ import Label from '../common/Label';
const schema = z.object({ const schema = z.object({
name: z.string().min(1, { message: "Category name is required" }), name: z.string().min(1, { message: "Category name is required" }),
description: z.string().min(1, { message: "Description 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 EditContactCategory = ({ data, onClose }) => {
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
reset, reset,
} = useForm({ } = useForm({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
name: data?.name || "", name: data?.name || "",
description: data?.description || "", description: data?.description || "",
}, },
}); });
const [descriptionLength, setDescriptionLength] = useState(data?.description?.length || 0); const [descriptionLength, setDescriptionLength] = useState(data?.description?.length || 0);
const maxDescriptionLength = 255; const maxDescriptionLength = 255;
const { mutate: updateContactCategory, isPending: isLoading } = useUpdateContactCategory(() => onClose?.()); const { mutate: updateContactCategory, isPending: isLoading } = useUpdateContactCategory(() => onClose?.());
const onSubmit = (formData) => { const onSubmit = (formData) => {
const payload = { const payload = {
id: data?.id, id: data?.id,
name: formData.name, name: formData.name,
description: formData.description, description: formData.description,
};
updateContactCategory({ id: data?.id, payload });
}; };
// const onSubmit = (formdata) => {
updateContactCategory({id:data?.id,payload});
};
// const onSubmit = (formdata) => {
// setIsLoading(true) // setIsLoading(true)
// const result = { // const result = {
// id:data?.id, // id:data?.id,
@ -52,8 +52,8 @@ const onSubmit = (formData) => {
// description: formdata.description, // description: formdata.description,
// }; // };
// MasterRespository.updateContactCategory(data?.id,result).then((resp)=>{ // MasterRespository.updateContactCategory(data?.id,result).then((resp)=>{
// setIsLoading(false) // setIsLoading(false)
// showToast("Contact Category Updated successfully.", "success"); // showToast("Contact Category Updated successfully.", "success");
@ -71,18 +71,18 @@ const onSubmit = (formData) => {
// showToast(error?.response?.data?.message, "error") // showToast(error?.response?.data?.message, "error")
// setIsLoading(false) // setIsLoading(false)
// }) // })
// }; // };
const resetForm = () => { const resetForm = () => {
reset({ name: "", description: "" }); reset({ name: "", description: "" });
setDescriptionLength(0); setDescriptionLength(0);
}; };
useEffect(() => {
return () => resetForm();
}, []);
useEffect(() => {
return () => resetForm();
}, []);
return (<> return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}> <form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12 text-start"> <div className="col-12 col-md-12 text-start">
@ -107,10 +107,10 @@ useEffect(() => {
<div className="text-end small text-muted"> <div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left {maxDescriptionLength - descriptionLength} characters left
</div> </div>
{errors.description && ( {errors.description && (
<p className="text-danger">{errors.description.message}</p> <p className="text-danger">{errors.description.message}</p>
)} )}
</div> </div>
<div className="col-12 text-end"> <div className="col-12 text-end">
<button <button
@ -126,10 +126,10 @@ useEffect(() => {
</button> </button>
</div> </div>
</form> </form>
</> </>
) )
} }

View File

@ -1,10 +1,10 @@
import React,{useState,useEffect} from 'react' import React, { useState, useEffect } from 'react'
import {useForm} from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository'; import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice'; import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager'; import { getCachedData, cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService'; import showToast from '../../services/toastService';
import {useUpdateContactTag} from '../../hooks/masterHook/useMaster'; import {useUpdateContactTag} from '../../hooks/masterHook/useMaster';
import Label from '../common/Label'; import Label from '../common/Label';
@ -13,38 +13,38 @@ import Label from '../common/Label';
const schema = z.object({ const schema = z.object({
name: z.string().min(1, { message: "Tag name is required" }), name: z.string().min(1, { message: "Tag name is required" }),
description: z.string().min(1, { message: "Description 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 EditContactTag = ({ data, onClose }) => {
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
reset reset
} = useForm({ } = useForm({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
name: data?.name || "", name: data?.name || "",
description: data?.description || "", description: data?.description || "",
}, },
}); });
const [descriptionLength, setDescriptionLength] = useState(data?.description?.length || 0); const [descriptionLength, setDescriptionLength] = useState(data?.description?.length || 0);
const maxDescriptionLength = 255; const maxDescriptionLength = 255;
const { mutate: updateContactTag, isPending: isLoading } = useUpdateContactTag(() => onClose?.()); const { mutate: updateContactTag, isPending: isLoading } = useUpdateContactTag(() => onClose?.());
const onSubmit = (formData) => { const onSubmit = (formData) => {
const payload = { const payload = {
id: data?.id, id: data?.id,
name: formData.name, name: formData.name,
description: formData.description, description: formData.description,
}; };
debugger debugger
updateContactTag({ id: data?.id, payload} ); updateContactTag({ id: data?.id, payload });
} }
// const onSubmit = (formdata) => { // const onSubmit = (formdata) => {
// setIsLoading(true) // setIsLoading(true)
// const result = { // const result = {
// id:data?.id, // id:data?.id,
@ -52,8 +52,8 @@ const onSubmit = (formData) => {
// description: formdata.description, // description: formdata.description,
// }; // };
// MasterRespository.updateContactTag(data?.id,result).then((resp)=>{ // MasterRespository.updateContactTag(data?.id,result).then((resp)=>{
// setIsLoading(false) // setIsLoading(false)
// showToast("Contact Tag Updated successfully.", "success"); // showToast("Contact Tag Updated successfully.", "success");
@ -71,18 +71,18 @@ const onSubmit = (formData) => {
// showToast(error?.response?.data?.message, "error") // showToast(error?.response?.data?.message, "error")
// setIsLoading(false) // setIsLoading(false)
// }) // })
// }; // };
const resetForm = () => { const resetForm = () => {
reset({ name: "", description: "" }); reset({ name: "", description: "" });
setDescriptionLength(0); setDescriptionLength(0);
}; };
useEffect(() => {
return () => resetForm();
}, []);
useEffect(() => {
return () => resetForm();
}, []);
return (<> return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}> <form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12 text-start"> <div className="col-12 col-md-12 text-start">
@ -107,10 +107,10 @@ useEffect(() => {
<div className="text-end small text-muted"> <div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left {maxDescriptionLength - descriptionLength} characters left
</div> </div>
{errors.description && ( {errors.description && (
<p className="text-danger">{errors.description.message}</p> <p className="text-danger">{errors.description.message}</p>
)} )}
</div> </div>
<div className="col-12 text-end"> <div className="col-12 text-end">
<button <button
@ -126,10 +126,10 @@ useEffect(() => {
</button> </button>
</div> </div>
</form> </form>
</> </>
) )
} }

View File

@ -1,9 +1,9 @@
import React, { useEffect,useState } from 'react' import React, { useEffect, useState } from 'react'
import { useForm ,Controller} from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import { set, z } from 'zod'; import { set, z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository'; import { MasterRespository } from '../../repositories/MastersRepository';
import { cacheData,getCachedData } from '../../slices/apiDataManager'; import { cacheData, getCachedData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService'; import showToast from '../../services/toastService';
import {useUpdateJobRole} from '../../hooks/masterHook/useMaster'; import {useUpdateJobRole} from '../../hooks/masterHook/useMaster';
import Label from '../common/Label'; import Label from '../common/Label';
@ -13,13 +13,13 @@ import Label from '../common/Label';
const schema = z.object({ const schema = z.object({
role: z.string().min(1, { message: "Role is required" }), role: z.string().min(1, { message: "Role is required" }),
description: z.string().min(1, { message: "Description 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 EditJobRole = ({ data, onClose }) => {
const [descriptionLength, setDescriptionLength] = useState(data?.description?.length || 0); const [descriptionLength, setDescriptionLength] = useState(data?.description?.length || 0);
const maxDescriptionLength = 255; const maxDescriptionLength = 255;
const { const {
@ -36,38 +36,38 @@ const [descriptionLength, setDescriptionLength] = useState(data?.description?.le
}, },
}); });
const { mutate: updateJobRole, isPendin:isLoading } = useUpdateJobRole(() => { const { mutate: updateJobRole, isPendin: isLoading } = useUpdateJobRole(() => {
onClose?.(); onClose?.();
}); });
// const onSubmit = (formdata) => { // const onSubmit = (formdata) => {
// setIsLoading(true) // setIsLoading(true)
// const result = { // const result = {
// id:data?.id, // id:data?.id,
// name: formdata?.role, // name: formdata?.role,
// description: formdata.description, // 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() // MasterRespository.updateJobRole(data?.id,result).then((resp)=>{
// }).catch((error)=>{ // setIsLoading(false)
// showToast(error.message, "error") // showToast("JobRole Update successfully.", "success");
// setIsLoading(false) // 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) => { const onSubmit = (formData) => {
updateJobRole({ updateJobRole({
@ -94,9 +94,9 @@ const [descriptionLength, setDescriptionLength] = useState(data?.description?.le
}); });
return () => sub.unsubscribe(); return () => sub.unsubscribe();
}, [watch]); }, [watch]);
return (<> return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}> <form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
{/* <div className="col-12 col-md-12"> {/* <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> <label className="fs-5 text-dark text-center d-flex align-items-center justify-content-center flex-wrap">Edit Job Role</label>
</div> */} </div> */}
@ -126,7 +126,7 @@ const [descriptionLength, setDescriptionLength] = useState(data?.description?.le
<p className="text-danger">{errors.description.message}</p> <p className="text-danger">{errors.description.message}</p>
)} )}
</div> </div>
<div className="col-12 text-end"> <div className="col-12 text-end">
<button <button
@ -142,10 +142,11 @@ const [descriptionLength, setDescriptionLength] = useState(data?.description?.le
</button> </button>
</div> </div>
</form> </form>
</> </>
) )
} }

View File

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

View File

@ -0,0 +1,156 @@
import React, { useEffect } from "react";
import { useForm, FormProvider } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useFeatures } from "../../hooks/useMasterRole";
import { DOCUMENTS_ENTITIES, EXPENSE_MANAGEMENT } from "../../utils/constants";
import {
useCreateDocumentCatgory,
useUpdateDocumentCategory,
} from "../../hooks/masterHook/useMaster";
export const Document_Entity = Object.entries(DOCUMENTS_ENTITIES).map(
([key, value]) => ({ key, value })
);
const ExpenseStatusSchema = z.object({
name: z.string().min(1, { message: "Name is required" }),
description: z.string().min(1, { message: "Description is required" }),
entityTypeId: z.string().min(1, { message: "Entity is required" }),
});
const ManageDocumentCategory = ({ data, onClose }) => {
const methods = useForm({
resolver: zodResolver(ExpenseStatusSchema),
defaultValues: {
name: "",
description: "",
entityTypeId: "",
},
});
const {
register,
handleSubmit,
reset,
formState: { errors },
} = methods;
const { masterFeatures, loading } = useFeatures();
const ExpenseFeature = masterFeatures?.find(
(appfeature) => appfeature.id === EXPENSE_MANAGEMENT
);
const { mutate: CreateDocumentCategory, isPending } =
useCreateDocumentCatgory(() => onClose?.());
const { mutate: UpdateDocumentCategory, isPending: Updating } =
useUpdateDocumentCategory(() => onClose?.());
const onSubmit = (payload) => {
if (data) {
UpdateDocumentCategory({
id: data.id,
payload: { ...payload, id: data.id },
});
} else {
CreateDocumentCategory(payload);
}
};
useEffect(() => {
if (data) {
reset({
name: data.name ?? "",
description: data.description ?? "",
entityTypeId: data.entityTypeId ?? "",
});
}
}, [data, reset]);
return (
<FormProvider {...methods}>
{loading ? (
<div>Loading...</div>
) : (
<div>
<div>
<p className="fw-semibold">
{data ? "Update Document Category" : "Add Document Category"}
</p>
</div>
<form
className="row g-2 text-start"
onSubmit={handleSubmit(onSubmit)}
>
<div className="col-12">
<label className="form-label">Category Name</label>
<input
type="text"
{...register("name")}
className={`form-control form-control-sm `}
/>
{errors.name && (
<p className="danger-text">{errors.name.message}</p>
)}
</div>
<div className="col-12">
<label className="form-label">Select Entity</label>
<select
className="form-select form-select-sm"
{...register("entityTypeId")}
>
<option value="" disabled>Select entity</option>
{Document_Entity.map((entity) => (
<option key={entity.key} value={entity.value}>
{entity.key}
</option>
))}
</select>
{errors.entityTypeId && (
<p className="danger-text">{errors.entityTypeId.message}</p>
)}
</div>
<div className="col-12">
<label className="form-label">Description</label>
<textarea
rows="3"
{...register("description")}
className={`form-control form-control-sm`}
/>
{errors.description && (
<p className="danger-text">{errors.description.message}</p>
)}
</div>
<div className="col-12 text-center">
<button
type="submit"
className="btn btn-sm btn-primary me-3"
disabled={isPending || Updating}
>
{isPending || Updating
? "Please Wait..."
: data
? "Update"
: "Submit"}
</button>
<button
type="button"
className="btn btn-sm btn-secondary"
onClick={onClose}
disabled={isPending || Updating}
>
Cancel
</button>
</div>
</form>
</div>
)}
</FormProvider>
);
};
export default ManageDocumentCategory;

View File

@ -0,0 +1,223 @@
import React, { useEffect } from "react";
import { useForm, FormProvider } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useCreateDocumentType, useDocumentCategories, useUpdateDocumentType } from "../../hooks/masterHook/useMaster";
import { DOCUMENTS_ENTITIES } from "../../utils/constants";
export const Document_Entity = Object.entries(DOCUMENTS_ENTITIES).map(
([key, value]) => ({ key, value })
);
const DocumentTypeSchema = z.object({
name: z.string().min(1, { message: "Name is required" }),
regexExpression: z.string().optional(),
allowedContentType: z.string().min(1, { message: "Allowed content type is required" }),
maxSizeAllowedInMB: z
.number({ invalid_type_error: "Must be a number" })
.min(1, { message: "Max size must be at least 1MB" }),
isValidationRequired: z.boolean().default(true),
isMandatory: z.boolean().default(true),
documentCategoryId: z.string().min(1, { message: "Document Category is required" }),
});
const ManageDocumentType = ({ data, onClose, documentCategories = [] }) => {
const methods = useForm({
resolver: zodResolver(DocumentTypeSchema),
defaultValues: {
name: "",
regexExpression: "",
allowedContentType: "",
maxSizeAllowedInMB: 25,
isValidationRequired: true,
isMandatory: true,
documentCategoryId: "",
entityTypeId:""
},
});
const {
register,
handleSubmit,
reset,watch,
formState: { errors },
} = methods;
const selectedDocumentEntity = watch("entityTypeId")
const {DocumentCategories,isLoading} = useDocumentCategories(selectedDocumentEntity)
const { mutate: createDocType, isPending: creating } = useCreateDocumentType(() =>
onClose?.()
);
const { mutate: updateDocType, isPending: updating } = useUpdateDocumentType(() =>
onClose?.()
);
const onSubmit = (payload) => {
const { entityTypeId, ...rest } = payload;
if (data) {
updateDocType({ id: data.id, payload: { ...rest, id: data.id } });
} else {
createDocType(rest);
}
};
useEffect(() => {
if (data) {
reset({
name: data.name ?? "",
regexExpression: data.regexExpression ?? "",
allowedContentType: data.allowedContentType ?? "",
maxSizeAllowedInMB: data.maxSizeAllowedInMB ?? 25,
isValidationRequired: data.isValidationRequired ?? true,
isMandatory: data.isMandatory ?? true,
documentCategoryId: data.documentCategory?.id ?? "",
entityTypeId:data.documentCategory?.entityTypeId ?? ""
});
}
}, [data]);
return (
<FormProvider {...methods}>
<form className="row g-2 text-start" onSubmit={handleSubmit(onSubmit)}>
<div className="text-center">
<p className="fw-semibold">
{data ? "Edit Document Type" : "Add Document Type"}
</p>
</div>
{/* Name */}
<div className="col-12">
<label className="form-label">Name</label>
<input
type="text"
{...register("name")}
className={`form-control form-control-sm `}
/>
{errors.name && <a className="text-danger">{errors.name.message}</a>}
</div>
{/* Regex Expression */}
<div className="col-12">
<label className="form-label">Regex Expression</label>
<input
type="text"
{...register("regexExpression")}
className="form-control form-control-sm"
/>
</div>
{/* Allowed Content Type */}
<div className="col-12">
<label className="form-label">Allowed Content Type</label>
<input
type="text"
{...register("allowedContentType")}
className={`form-control form-control form-control-sm`}
/>
{errors.allowedContentType && (
<a className="text-danger">{errors.allowedContentType.message}</a>
)}
</div>
{/* Max Size */}
<div className="col-12">
<label className="form-label">Max Size Allowed (MB)</label>
<input
type="number"
{...register("maxSizeAllowedInMB", { valueAsNumber: true })}
className={`form-control form-control-sm`}
/>
{errors.maxSizeAllowedInMB && (
<a className="text-danger">{errors.maxSizeAllowedInMB.message}</a>
)}
</div>
{/* Validation Required */}
<div className="col-6 form-check">
<input
type="checkbox"
className="form-check-input"
{...register("isValidationRequired")}
/>
<label className="form-check-label">Validation Required</label>
</div>
{/* Mandatory */}
<div className="col-6 form-check">
<input
type="checkbox"
className="form-check-input "
{...register("isMandatory")}
/>
<label className="form-check-label">Mandatory</label>
</div>
<div className="col-12">
<label className="form-label">Document Entity</label>
<select
{...register("entityTypeId")}
className={`form-select form-select-sm`}
>
<option value="">-- Select Category --</option>
{Document_Entity.map((entity) => (
<option key={entity.key} value={entity.value}>
{entity.key}
</option>
))}
</select>
{errors.entityTypeId && (
<a className="text-danger">{errors.entityTypeId.message}</a>
)}
</div>
{/* Category */}
<div className="col-12">
<label className="form-label">Document Category</label>
<select
{...register("documentCategoryId")}
className={`form-select form-select-sm`}
>
{isLoading && <option value="" disabled>Loading....</option> }
<option value="">-- Select Category --</option>
{!isLoading && DocumentCategories?.map((cat) => (
<option key={cat.id} value={cat.id}>
{cat.name}
</option>
))}
</select>
{errors.documentCategoryId && (
<a className="text-danger">{errors.documentCategoryId.message}</a>
)}
</div>
{/* Buttons */}
<div className="col-12 text-center">
<button
type="submit"
className="btn btn-sm btn-primary me-3"
disabled={creating || updating}
>
{creating || updating
? "Please Wait..."
: data
? "Update"
: "Submit"}
</button>
<button
type="button"
className="btn btn-sm btn-secondary"
onClick={onClose}
disabled={creating || updating}
>
Cancel
</button>
</div>
</form>
</FormProvider>
);
};
export default ManageDocumentType;

View File

@ -18,31 +18,31 @@ const ManagePaymentMode = ({ data = null, onClose }) => {
formState: { errors }, formState: { errors },
} = useForm({ } = useForm({
resolver: zodResolver(ExpnseSchema), resolver: zodResolver(ExpnseSchema),
defaultValues: { name: "", description: "" }, defaultValues: { name: "", description: "" },
}); });
const { mutate: CreatePaymentMode, isPending } = useCreatePaymentMode(() => const { mutate: CreatePaymentMode, isPending } = useCreatePaymentMode(() =>
onClose?.() onClose?.()
); );
const {mutate:UpdatePaymentMode,isPending:Updating} = useUpdatePaymentMode(()=>onClose?.()) const { mutate: UpdatePaymentMode, isPending: Updating } = useUpdatePaymentMode(() => onClose?.())
const onSubmit = (payload) => { const onSubmit = (payload) => {
if(data){ if (data) {
UpdatePaymentMode({id:data.id,payload:{...payload,id:data.id}}) UpdatePaymentMode({ id: data.id, payload: { ...payload, id: data.id } })
}else( } else (
CreatePaymentMode(payload) CreatePaymentMode(payload)
) )
}; };
useEffect(()=>{ useEffect(() => {
if(data){ if (data) {
reset({ reset({
name:data.name ?? "", name: data.name ?? "",
description:data.description ?? "" description: data.description ?? ""
}) })
} }
},[data]) }, [data])
return ( return (

View File

@ -1,140 +1,68 @@
import React, { useState, useEffect } from "react"; import React from "react";
import CreateRole from "./CreateRole"; import CreateRole from "./CreateRole";
import DeleteMaster from "./DeleteMaster";
import EditRole from "./EditRole"; import EditRole from "./EditRole";
import CreateJobRole from "./CreateJobRole"; import CreateJobRole from "./CreateJobRole";
import EditJobRole from "./EditJobRole"; import EditJobRole from "./EditJobRole";
import CreateActivity from "./CreateActivity"; import CreateActivity from "./CreateActivity";
import EditActivity from "./EditActivity"; import EditActivity from "./EditActivity";
import ConfirmModal from "../common/ConfirmModal";
import { MasterRespository } from "../../repositories/MastersRepository";
import { cacheData, getCachedData } from "../../slices/apiDataManager";
import showToast from "../../services/toastService";
import CreateWorkCategory from "./CreateWorkCategory"; import CreateWorkCategory from "./CreateWorkCategory";
import EditWorkCategory from "./EditWorkCategory"; import EditWorkCategory from "./EditWorkCategory";
import CreateCategory from "./CreateContactCategory"; import CreateCategory from "./CreateContactCategory";
import CreateContactTag from "./CreateContactTag"; import CreateContactTag from "./CreateContactTag";
import EditContactCategory from "./EditContactCategory"; import EditContactCategory from "./EditContactCategory";
import EditContactTag from "./EditContactTag"; import EditContactTag from "./EditContactTag";
import { useDeleteMasterItem } from "../../hooks/masterHook/useMaster";
import ManageExpenseType from "./ManageExpenseType"; import ManageExpenseType from "./ManageExpenseType";
import ManagePaymentMode from "./ManagePaymentMode"; import ManagePaymentMode from "./ManagePaymentMode";
import ManageExpenseStatus from "./ManageExpenseStatus"; import ManageExpenseStatus from "./ManageExpenseStatus";
import ManageDocumentCategory from "./ManageDocumentCategory";
import ManageDocumentType from "./ManageDocumentType";
const MasterModal = ({ modaldata, closeModal }) => { const MasterModal = ({ modaldata, closeModal }) => {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); if (!modaldata?.modalType || modaldata.modalType === "delete") {
const { mutate: deleteMasterItem, isPending } = useDeleteMasterItem();
const handleSelectedMasterDeleted = () => {
const { masterType, item, validateFn } = modaldata || {};
if (!masterType || !item?.id) {
showToast("Missing master type or item", "error");
return;
}
deleteMasterItem(
{ masterType, item, validateFn },
{ onSuccess: handleCloseDeleteModal }
);
};
const handleCloseDeleteModal = () => {
setIsDeleteModalOpen(false);
closeModal();
};
useEffect(() => {
if (modaldata?.modalType === "delete") {
setIsDeleteModalOpen(true);
}
}, [modaldata]);
if (!modaldata?.modalType) {
closeModal();
return null; return null;
} }
if (modaldata.modalType === "delete" && isDeleteModalOpen) { const { modalType, item, masterType } = modaldata;
return (
<div
className="modal fade show"
tabIndex="-1"
role="dialog"
style={{ display: "block", backgroundColor: "rgba(0,0,0,0.5)" }}
aria-hidden="false"
>
<ConfirmModal
type="delete"
header={`Delete ${modaldata.masterType}`}
message="Are you sure you want delete?"
onSubmit={handleSelectedMasterDeleted}
onClose={handleCloseDeleteModal}
/>
</div>
);
}
const renderModalContent = () => { const modalComponents = {
const { modalType, item, masterType } = modaldata; "Application Role": (
<CreateRole masmodalType={masterType} onClose={closeModal} />
const modalComponents = { ),
"Application Role": <CreateRole masmodalType={masterType} onClose={closeModal} />, "Edit-Application Role": (
"Edit-Application Role": <EditRole master={modaldata} onClose={closeModal} />, <EditRole master={modaldata} onClose={closeModal} />
"Job Role": <CreateJobRole onClose={closeModal} />, ),
"Edit-Job Role": <EditJobRole data={item} onClose={closeModal} />, "Job Role": <CreateJobRole onClose={closeModal} />,
"Activity": <CreateActivity onClose={closeModal} />, "Edit-Job Role": <EditJobRole data={item} onClose={closeModal} />,
"Edit-Activity": <EditActivity activityData={item} onClose={closeModal} />, "Activity": <CreateActivity onClose={closeModal} />,
"Work Category": <CreateWorkCategory onClose={closeModal} />, "Edit-Activity": <EditActivity activityData={item} onClose={closeModal} />,
"Edit-Work Category": <EditWorkCategory data={item} onClose={closeModal} />, "Work Category": <CreateWorkCategory onClose={closeModal} />,
"Contact Category": <CreateCategory data={item} onClose={closeModal} />, "Edit-Work Category": <EditWorkCategory data={item} onClose={closeModal} />,
"Edit-Contact Category": <EditContactCategory data={item} onClose={closeModal} />, "Contact Category": <CreateCategory data={item} onClose={closeModal} />,
"Contact Tag": <CreateContactTag data={item} onClose={closeModal} />, "Edit-Contact Category": (
"Edit-Contact Tag": <EditContactTag data={item} onClose={closeModal} />, <EditContactCategory data={item} onClose={closeModal} />
"Expense Type":<ManageExpenseType onClose={closeModal} />, ),
"Edit-Expense Type":<ManageExpenseType data={item} onClose={closeModal} />, "Contact Tag": <CreateContactTag data={item} onClose={closeModal} />,
"Payment Mode":<ManagePaymentMode onClose={closeModal}/>, "Edit-Contact Tag": <EditContactTag data={item} onClose={closeModal} />,
"Edit-Payment Mode":<ManagePaymentMode data={item} onClose={closeModal}/>, "Expense Type": <ManageExpenseType onClose={closeModal} />,
"Expense Status":<ManageExpenseStatus onClose={closeModal}/>, "Edit-Expense Type": <ManageExpenseType data={item} onClose={closeModal} />,
"Edit-Expense Status":<ManageExpenseStatus data={item} onClose={closeModal}/> "Payment Mode": <ManagePaymentMode onClose={closeModal} />,
}; "Edit-Payment Mode": <ManagePaymentMode data={item} onClose={closeModal} />,
"Expense Status": <ManageExpenseStatus onClose={closeModal} />,
return modalComponents[modalType] || null; "Edit-Expense Status": (
<ManageExpenseStatus data={item} onClose={closeModal} />
),
"Document Category": <ManageDocumentCategory onClose={closeModal} />,
"Edit-Document Category": (
<ManageDocumentCategory data={item} onClose={closeModal} />
),
"Document Type": <ManageDocumentType onClose={closeModal} />,
"Edit-Document Type": (
<ManageDocumentType data={item} onClose={closeModal} />
),
}; };
const isLargeModal = ["Application Role", "Edit-Application Role"].includes( return modalComponents[modalType] || null;
modaldata.modalType
);
return (
<div
className="modal fade show"
id="master-modal"
tabIndex="-1"
role="dialog"
aria-hidden="true"
aria-labelledby="modalToggleLabel"
style={{ display: "block" }}
>
<div className={`modal-dialog mx-sm-auto mx-1 ${isLargeModal ? "modal-lg" : "modal-md"} modal-simple`}>
<div className="modal-content">
<div className="modal-body p-sm-4 p-0">
<div className="d-flex justify-content-between">
<h6>{modaldata?.modalType}</h6>
<button
type="button"
className="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
onClick={closeModal}
/>
</div>
{renderModalContent()}
</div>
</div>
</div>
</div>
);
}; };
export default MasterModal; export default MasterModal;

View File

@ -171,6 +171,56 @@ const {
return { ExpenseStatus, loading, error }; return { ExpenseStatus, loading, error };
} }
export const useDocumentTypes =(category)=>{
const {
data: DocumentTypes = [],
error,
isError,
isLoading
} = useQuery({
queryKey: ["Document Type",category],
queryFn: async () => {
const res = await MasterRespository.getDocumentTypes(category)
return res.data;
},
enabled:!!category,
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Failed to fetch Expense Status",
"error"
);
},
});
return { DocumentTypes, isError, isLoading, error };
}
export const useDocumentCategories =(EntityType)=>{
const {
data: DocumentCategories = [],
error,
isError,
isLoading
} = useQuery({
queryKey: ["Document Category",EntityType],
queryFn: async () => {
const res = await MasterRespository.getDocumentCategories(EntityType)
return res.data;
},
enabled:!!EntityType,
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Failed to fetch Expense Status",
"error"
);
},
});
return { DocumentCategories, isError, isLoading, error };
}
// ===Application Masters Query================================================= // ===Application Masters Query=================================================
const fetchMasterData = async (masterType) => { const fetchMasterData = async (masterType) => {
@ -193,6 +243,10 @@ const fetchMasterData = async (masterType) => {
return (await MasterRespository.getPaymentMode()).data; return (await MasterRespository.getPaymentMode()).data;
case "Expense Status": case "Expense Status":
return (await MasterRespository.getExpenseStatus()).data; return (await MasterRespository.getExpenseStatus()).data;
case "Document Type":
return (await MasterRespository.getDocumentTypes()).data;
case "Document Category":
return (await MasterRespository.getDocumentCategories()).data;
case "Status": case "Status":
return [ return [
{ {
@ -634,6 +688,7 @@ export const useUpdatePaymentMode = (onSuccessCallback)=>{
} }
}) })
} }
// -------------------Expense Status---------------------------------- // -------------------Expense Status----------------------------------
export const useCreateExpenseStatus =(onSuccessCallback)=>{ export const useCreateExpenseStatus =(onSuccessCallback)=>{
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -677,6 +732,100 @@ export const useUpdateExpenseStatus = (onSuccessCallback)=>{
} }
}) })
} }
// --------------------Document-Category--------------------------------
export const useCreateDocumentCatgory =(onSuccessCallback)=>{
const queryClient = useQueryClient();
return useMutation( {
mutationFn: async ( payload ) =>
{
const resp = await MasterRespository.createDocumenyCategory(payload);
return resp.data;
},
onSuccess: ( data ) =>
{
queryClient.invalidateQueries( {queryKey:[ "masterData", "Document Category" ]} )
queryClient.invalidateQueries( {queryKey:[ "Document Category" ]} )
showToast( "Document Category added successfully", "success" );
if(onSuccessCallback) onSuccessCallback(data)
},
onError: ( error ) =>
{
showToast(error.message || "Something went wrong", "error");
}
})
}
export const useUpdateDocumentCategory =(onSuccessCallback)=>{
const queryClient = useQueryClient();
return useMutation( {
mutationFn: async ( {id,payload} ) =>
{
const resp = await MasterRespository.updateDocumentCategory(id,payload);
return resp.data;
},
onSuccess: ( data ) =>
{
queryClient.invalidateQueries( {queryKey:[ "masterData", "Document Category" ]} )
queryClient.invalidateQueries( {queryKey:[ "Document Category" ]} )
showToast( "Document Category Updated successfully", "success" );
if(onSuccessCallback) onSuccessCallback(data)
},
onError: ( error ) =>
{
showToast(error.message || "Something went wrong", "error");
}
})
}
// ------------------------------Document-Type-----------------------------------
export const useCreateDocumentType =(onSuccessCallback)=>{
const queryClient = useQueryClient();
return useMutation( {
mutationFn: async ( payload ) =>
{
const resp = await MasterRespository.createDocumentType(payload);
return resp.data;
},
onSuccess: ( data ) =>
{
queryClient.invalidateQueries( {queryKey:[ "masterData", "Document Type" ]} )
showToast( "Document Type added successfully", "success" );
if(onSuccessCallback) onSuccessCallback(data)
},
onError: ( error ) =>
{
showToast(error.message || "Something went wrong", "error");
}
})
}
export const useUpdateDocumentType =(onSuccessCallback)=>{
const queryClient = useQueryClient();
return useMutation( {
mutationFn: async ( {id,payload} ) =>
{
const resp = await MasterRespository.updateDocumentType(id,payload);
return resp.data;
},
onSuccess: ( data ) =>
{
queryClient.invalidateQueries( {queryKey:[ "masterData", "Document Type" ]} )
showToast( "Document Type Updated successfully", "success" );
if(onSuccessCallback) onSuccessCallback(data)
},
onError: ( error ) =>
{
showToast(error.message || "Something went wrong", "error");
}
})
}
// -Delete Master -------- // -Delete Master --------
export const useDeleteMasterItem = () => { export const useDeleteMasterItem = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();

View File

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

204
src/hooks/useDocument.js Normal file
View File

@ -0,0 +1,204 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import showToast from "../services/toastService";
import { DocumentRepository } from "../repositories/DocumentRepository";
// ----------------------Query-------------------------------
const cleanFilter = (filter) => {
const cleaned = { ...filter };
[
"uploadedByIds",
"documentCategoryIds",
"documentTypeIds",
"documentTagIds",
].forEach((key) => {
if (Array.isArray(cleaned[key]) && cleaned[key].length === 0) {
delete cleaned[key];
}
});
["startDate", "endDate", "isVerified"].forEach((key) => {
if (cleaned[key] === null) {
delete cleaned[key];
}
});
return cleaned;
};
export const useDocumentListByEntityId = (
entityTypeId,
entityId,
pageSize,
pageNumber,
filter,
searchString = "",
isActive
) => {
return useQuery({
queryKey: [
"DocumentList",
entityTypeId,
entityId,
pageSize,
pageNumber,
filter,
searchString,
isActive
],
queryFn: async () => {
const cleanedFilter = cleanFilter(filter);
const resp = await DocumentRepository.getDocumentList(
entityTypeId,
entityId,
pageSize,
pageNumber,
cleanedFilter,
searchString,
isActive
);
return resp.data
},
enabled: !!entityTypeId && !!entityId,
});
};
export const useDocumentFilterEntities = (entityTypeId) => {
return useQuery({
queryKey: ["DFilter", entityTypeId],
queryFn: async () =>
await DocumentRepository.getFilterEntities(entityTypeId),
});
};
export const useDocumentDetails = (documentId) => {
return useQuery({
queryKey: ["Document", documentId],
queryFn: async () => {
const resp = await DocumentRepository.getDocumentById(documentId);
return resp.data;
},
enabled: !!documentId,
});
};
export const useDocumentVersionList = (parentAttachmentId,pageSize,pageNumber) => {
return useQuery({
queryKey: ["DocumentVersionList", parentAttachmentId,pageSize,pageNumber],
queryFn: async () => {
const resp = await DocumentRepository.getDocumentVersionList(parentAttachmentId,pageSize,pageNumber);
return resp.data
},
enabled: !!parentAttachmentId,
});
};
export const useDocumentVersion = (id)=>{
return useQuery({
queryKey:["DocumentVersion",id],
queryFn:async()=> await DocumentRepository.getDocumentVersion(id),
enabled:!!id
})
}
export const useDocumentTags =()=>{
return useQuery({
queryKey:["DocumentTag"],
queryFn:async()=> {const resp = await DocumentRepository.getDocumentTags()
return resp.data
}
})
}
//----------------------- MUTATION -------------------------
export const useUploadDocument = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (DocumentPayload) =>
DocumentRepository.uploadDocument(DocumentPayload),
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ["DocumentList"] });
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
console.log(error);
showToast(
error.response.data.message ||
"Something went wrong please try again !",
"error"
);
},
});
};
export const useUpdateDocument = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ documentId, DocumentPayload }) =>
DocumentRepository.UpdateDocument(documentId, DocumentPayload),
onSuccess: (data, variables) => {
const { documentId } = variables;
queryClient.invalidateQueries({ queryKey: ["DocumentList"] });
queryClient.invalidateQueries({ queryKey: ["Document", documentId] });
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
console.log(error);
showToast(
error.response.data.message ||
"Something went wrong please try again !",
"error"
);
},
});
};
export const useVerifyDocument = ()=>{
const queryClient = useQueryClient();
return useMutation({
mutationFn:async({documentId,isVerify}) => await DocumentRepository.verifyDocument(documentId,isVerify),
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ["DocumentVersionList"] });
queryClient.invalidateQueries({ queryKey: ["DocumentList"] });
queryClient.invalidateQueries({ queryKey: ["Document"] });
showToast(
data.response.data.message ||
"Document Successfully Verified !",
"success"
);
},
onError: (error) => {
showToast(
error.response.data.message ||
"Something went wrong please try again !",
"error"
);
},
})
}
export const useActiveInActiveDocument = ()=>{
const queryClient = useQueryClient();
return useMutation({
mutationFn:async({documentId,isActive}) => await DocumentRepository.deleteDocument(documentId,isActive),
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ["DocumentList"] });
showToast(
data.response.data.message ||
"Document Successfully Verified !",
"success"
);
},
onError: (error) => {
showToast(
error.response.data.message ||
"Something went wrong please try again !",
"error"
);
},
})
}

View File

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

View File

@ -14,7 +14,6 @@ import {
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import showToast from "../services/toastService"; import showToast from "../services/toastService";
// ------------------------------Query------------------- // ------------------------------Query-------------------
export const useProjects = () => { export const useProjects = () => {
@ -153,7 +152,7 @@ export const useProjectName = () => {
isLoading, isLoading,
error, error,
refetch, refetch,
isError isError,
} = useQuery({ } = useQuery({
queryKey: ["basicProjectNameList"], queryKey: ["basicProjectNameList"],
queryFn: async () => { queryFn: async () => {
@ -164,7 +163,13 @@ export const useProjectName = () => {
showToast(error.message || "Error while Fetching project Name", "error"); showToast(error.message || "Error while Fetching project Name", "error");
}, },
}); });
return { projectNames: data, loading: isLoading, Error: error, refetch,isError }; return {
projectNames: data,
loading: isLoading,
Error: error,
refetch,
isError,
};
}; };
export const useProjectInfra = (projectId) => { export const useProjectInfra = (projectId) => {
@ -175,7 +180,7 @@ export const useProjectInfra = (projectId) => {
} = useQuery({ } = useQuery({
queryKey: ["ProjectInfra", projectId], queryKey: ["ProjectInfra", projectId],
queryFn: async () => { queryFn: async () => {
if(!projectId) return null; if (!projectId) return null;
const res = await ProjectRepository.getProjectInfraByproject(projectId); const res = await ProjectRepository.getProjectInfraByproject(projectId);
return res.data; return res.data;
}, },
@ -207,12 +212,7 @@ export const useProjectTasks = (workAreaId, IsExpandedArea = false) => {
return { ProjectTaskList, isLoading, error }; return { ProjectTaskList, isLoading, error };
}; };
export const useProjectTasksByEmployee = ( export const useProjectTasksByEmployee = (employeeId, fromDate, toDate) => {
employeeId,
fromDate,
toDate,
) => {
return useQuery({ return useQuery({
queryKey: ["TasksByEmployee", employeeId], queryKey: ["TasksByEmployee", employeeId],
queryFn: async () => { queryFn: async () => {
@ -230,6 +230,43 @@ export const useProjectTasksByEmployee = (
}); });
}; };
export const useProjectLevelEmployeeList = (ProjectId) => {
return useQuery({
queryKey: ["ProjectLevelEmployeeList", ProjectId],
queryFn: async () => {
const resp = await ProjectRepository.getProjectLevelEmployeeList(
ProjectId
);
return resp.data;
},
enabled: !!ProjectId,
});
};
export const useProjectLevelModules = () => {
return useQuery({
queryKey: ["ProjectLevelModules"],
queryFn: async () => {
const resp = await ProjectRepository.getProjectLevelModules();
return resp.data;
},
});
};
export const useProjectLevelEmployeePermission = (employeeId, projectId) => {
return useQuery({
queryKey: ["ProjectLevelEmployeePermission", employeeId, projectId],
queryFn: async () => {
const resp = await ProjectRepository.getProjectLevelEmployeePermissions(
employeeId,
projectId
);
return resp.data;
},
enabled: !!employeeId && !!projectId,
});
};
// -- -------------Mutation------------------------------- // -- -------------Mutation-------------------------------
export const useCreateProject = ({ onSuccessCallback }) => { export const useCreateProject = ({ onSuccessCallback }) => {
@ -558,3 +595,26 @@ export const useDeleteProjectTask = (onSuccessCallback) => {
}, },
}); });
}; };
export const useUpdateProjectLevelEmployeePermission = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload) =>
await ProjectRepository.updateProjectLevelEmployeePermission(payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: ["ProjectLevelEmployeePermission"],
});
showToast("Permission Updated successfully", "success");
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Failed to delete task",
"error"
);
if (onSuccessCallback) onSuccessCallback();
},
});
};

View File

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

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { useTaskList } from "../../hooks/useTasks"; import { useTaskList } from "../../hooks/useTasks";
import { useProjectName } from "../../hooks/useProjects"; import { useProjectName } from "../../hooks/useProjects";
@ -13,13 +13,13 @@ import SubTask from "../../components/Activities/SubTask";
import { formatNumber, formatUTCToLocalTime } from "../../utils/dateUtils"; import { formatNumber, formatUTCToLocalTime } from "../../utils/dateUtils";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { APPROVE_TASK, ASSIGN_REPORT_TASK } from "../../utils/constants"; import { APPROVE_TASK, ASSIGN_REPORT_TASK } from "../../utils/constants";
import { useSelectedproject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
import moment from "moment"; import moment from "moment";
import Loader from "../../components/common/Loader"; import Loader from "../../components/common/Loader";
const DailyTask = () => { const DailyTask = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const selectedProject = useSelectedproject(); const selectedProject = useSelectedProject();
const { projectNames } = useProjectName(); const { projectNames } = useProjectName();
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK); const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK); const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK);
@ -41,10 +41,20 @@ const DailyTask = () => {
// Ensure project is set // Ensure project is set
useEffect(() => { useEffect(() => {
if (!selectedProject && projectNames.length > 0) { if (!selectedProject && projectNames.length > 0) {
debugger
dispatch(setProjectId(projectNames[0].id)); dispatch(setProjectId(projectNames[0].id));
} }
}, [selectedProject, projectNames, dispatch]); }, [selectedProject, projectNames, dispatch]);
// 🔹 Reset filters when project changes
useEffect(() => {
setFilters({
selectedBuilding: "",
selectedFloors: [],
selectedActivities: [],
});
}, [selectedProject]);
// Memoized filtering // Memoized filtering
const filteredTasks = useMemo(() => { const filteredTasks = useMemo(() => {
if (!TaskList) return []; if (!TaskList) return [];
@ -91,8 +101,8 @@ const DailyTask = () => {
data-bs-content={` data-bs-content={`
<div class="border border-secondary rounded custom-popover p-2 px-3"> <div class="border border-secondary rounded custom-popover p-2 px-3">
${task.teamMembers ${task.teamMembers
.map( .map(
(m) => ` (m) => `
<div class="d-flex align-items-center gap-2 mb-2"> <div class="d-flex align-items-center gap-2 mb-2">
<div class="avatar avatar-xs"> <div class="avatar avatar-xs">
<span class="avatar-initial rounded-circle bg-label-primary"> <span class="avatar-initial rounded-circle bg-label-primary">
@ -101,8 +111,8 @@ const DailyTask = () => {
</div> </div>
<span>${m.firstName} ${m.lastName}</span> <span>${m.firstName} ${m.lastName}</span>
</div>` </div>`
) )
.join("")} .join("")}
</div> </div>
`} `}
> >
@ -163,6 +173,7 @@ const DailyTask = () => {
currentSelectedBuilding={filters.selectedBuilding} currentSelectedBuilding={filters.selectedBuilding}
currentSelectedFloors={filters.selectedFloors} currentSelectedFloors={filters.selectedFloors}
currentSelectedActivities={filters.selectedActivities} currentSelectedActivities={filters.selectedActivities}
selectedProject={selectedProject}
/> />
</div> </div>

View File

@ -4,21 +4,19 @@ import InfraPlanning from "../../components/Activities/InfraPlanning";
import { useProjectName } from "../../hooks/useProjects"; import { useProjectName } from "../../hooks/useProjects";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { setProjectId } from "../../slices/localVariablesSlice"; import { setProjectId } from "../../slices/localVariablesSlice";
import { useSelectedproject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
const TaskPlannng = () => { const TaskPlannng = () => {
const selectedProject = useSelectedproject(); const selectedProject = useSelectedProject();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { projectNames, loading: projectLoading } = useProjectName(); const { projectNames = [], loading: projectLoading } = useProjectName();
const initialized = useRef(false); useEffect(() => {
if (!selectedProject) {
dispatch(setProjectId(projectNames[0].id));
}
}, [projectNames, selectedProject?.id, dispatch]);
useEffect(() => {
if (!initialized.current && projectNames.length > 0 && !selectedProject?.id) {
dispatch(setProjectId(projectNames[0].id));
initialized.current = true;
}
}, [projectNames, selectedProject, dispatch]);
return ( return (
<div className="container-fluid"> <div className="container-fluid">

View File

@ -301,25 +301,15 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
</GlobalModel> </GlobalModel>
)} )}
{deleteContact && ( {deleteContact && (
<div <ConfirmModal
className={`modal fade ${deleteContact ? "show" : ""}`} isOpen={!!deleteContact}
tabIndex="-1" type="delete"
role="dialog" header="Delete Contact"
style={{ message="Are you sure you want delete?"
display: deleteContact ? "block" : "none", onSubmit={handleDeleteContact}
backgroundColor: deleteContact ? "rgba(0,0,0,0.5)" : "transparent", onClose={() => setDeleteContact(null)}
}} loading={IsDeleting}
aria-hidden="false" />
>
<ConfirmModal
type={"delete"}
header={"Delete Contact"}
message={"Are you sure you want delete?"}
onSubmit={handleDeleteContact}
onClose={() => setDeleteContact(null)}
loading={IsDeleting}
/>
</div>
)} )}
{openBucketModal && ( {openBucketModal && (
@ -383,12 +373,14 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
{/* Empty state AFTER list */} {/* Empty state AFTER list */}
{!loading && contacts?.length === 0 && ( {!loading && contacts?.length === 0 && (
<p className="mt-3 ms-3 text-muted" >No contact found</p> <p className="mt-3 ms-3 text-muted">No contact found</p>
)} )}
{!loading && {!loading &&
contacts?.length > 0 && contacts?.length > 0 &&
currentItems.length === 0 && ( currentItems.length === 0 && (
<p className="mt-3 ms-3 text-muted">No matching contact found</p> <p className="mt-3 ms-3 text-muted">
No matching contact found
</p>
)} )}
</div> </div>
</div> </div>
@ -419,11 +411,9 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
{!loading && contacts?.length === 0 && ( {!loading && contacts?.length === 0 && (
<p className="mt-3 ms-3 text-muted">No contact found</p> <p className="mt-3 ms-3 text-muted">No contact found</p>
)} )}
{!loading && {!loading && contacts?.length > 0 && currentItems.length === 0 && (
contacts?.length > 0 && <p className="mt-3 ms-3 text-muted">No matching contact found</p>
currentItems.length === 0 && ( )}
<p className="mt-3 ms-3 text-muted">No matching contact found</p>
)}
</div> </div>
)} )}
@ -446,7 +436,9 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
currentItems.length > ITEMS_PER_PAGE && ( currentItems.length > ITEMS_PER_PAGE && (
<nav aria-label="Page navigation"> <nav aria-label="Page navigation">
<ul className="pagination pagination-sm justify-content-end py-1"> <ul className="pagination pagination-sm justify-content-end py-1">
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}> <li
className={`page-item ${currentPage === 1 ? "disabled" : ""}`}
>
<button <button
className="page-link btn-xs" className="page-link btn-xs"
onClick={() => paginate(currentPage - 1)} onClick={() => paginate(currentPage - 1)}
@ -458,8 +450,9 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
{[...Array(totalPages)].map((_, index) => ( {[...Array(totalPages)].map((_, index) => (
<li <li
key={index} key={index}
className={`page-item ${currentPage === index + 1 ? "active" : "" className={`page-item ${
}`} currentPage === index + 1 ? "active" : ""
}`}
> >
<button <button
className="page-link" className="page-link"
@ -471,8 +464,9 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
))} ))}
<li <li
className={`page-item ${currentPage === totalPages ? "disabled" : "" className={`page-item ${
}`} currentPage === totalPages ? "disabled" : ""
}`}
> >
<button <button
className="page-link" className="page-link"
@ -489,4 +483,4 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
); );
}; };
export default Directory; export default Directory;

View File

@ -0,0 +1,41 @@
import React from 'react'
import Breadcrumb from '../../components/common/Breadcrumb'
const DocumentPage = () => {
return (
<div className=''>
<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"
className="form-control form-control-sm"
placeholder="Search Document"
/>
</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">
Refresh
< i className={`bx bx-refresh ms-1 `}></i>
</span>
<button
type="button"
title="Add New Document"
className="p-1 bg-primary rounded-circle cursor-pointer"
>
<i className="bx bx-plus fs-4 text-white"></i>
</button>
</div>
</div>
</div>
</div>
)
}
export default DocumentPage

View File

@ -85,6 +85,14 @@ const LoginPage = () => {
navigate("/auth/login-otp"); navigate("/auth/login-otp");
} }
}, [IsLoginWithOTP]); }, [IsLoginWithOTP]);
// useEffect(() => {
// const token = localStorage.getItem("jwtToken");
// if (token) {
// navigate("/dashboard");
// }
// }, [navigate]);
return ( return (
<AuthWrapper> <AuthWrapper>
<h4 className="mb-2">Welcome to PMS!</h4> <h4 className="mb-2">Welcome to PMS!</h4>
@ -210,7 +218,7 @@ const LoginPage = () => {
Login With Password Login With Password
</a> </a>
) : ( ) : (
<Link to="/auth/reqest/demo" className="registration-link"> <Link to="/market/enquire" className="registration-link">
Request a Demo Request a Demo
</Link> </Link>
)} )}

View File

@ -18,7 +18,7 @@ import {
VIEW_ALL_EMPLOYEES, VIEW_ALL_EMPLOYEES,
VIEW_TEAM_MEMBERS, VIEW_TEAM_MEMBERS,
} from "../../utils/constants"; } from "../../utils/constants";
import { clearCacheKey, useSelectedproject } from "../../slices/apiDataManager"; import { clearCacheKey, useSelectedProject } from "../../slices/apiDataManager";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import SuspendEmp from "../../components/Employee/SuspendEmp"; // Keep if you use SuspendEmp import SuspendEmp from "../../components/Employee/SuspendEmp"; // Keep if you use SuspendEmp
import { import {
@ -41,7 +41,7 @@ const EmployeeList = () => {
// const selectedProjectId = useSelector( // const selectedProjectId = useSelector(
// (store) => store.localVariables.projectId // (store) => store.localVariables.projectId
// ); // );
const selectedProjectId = useSelectedproject(); const selectedProjectId = useSelectedProject();
const { projectNames, loading: projectLoading, fetchData } = useProjectName(); const { projectNames, loading: projectLoading, fetchData } = useProjectName();
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -177,10 +177,12 @@ const EmployeeList = () => {
useEffect(() => { useEffect(() => {
if (!loading && Array.isArray(employees)) { if (!loading && Array.isArray(employees)) {
const sorted = [...employees].sort((a, b) => { const sorted = [...employees].sort((a, b) => {
const nameA = `${a.firstName || ""}${a.middleName || ""}${a.lastName || "" const nameA = `${a.firstName || ""}${a.middleName || ""}${
}`.toLowerCase(); a.lastName || ""
const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || "" }`.toLowerCase();
}`.toLowerCase(); const nameB = `${b.firstName || ""}${b.middleName || ""}${
b.lastName || ""
}`.toLowerCase();
return nameA?.localeCompare(nameB); return nameA?.localeCompare(nameB);
}); });
@ -259,37 +261,26 @@ const EmployeeList = () => {
)} )}
{IsDeleteModalOpen && ( {IsDeleteModalOpen && (
<div <ConfirmModal
className={`modal fade ${IsDeleteModalOpen ? "show" : ""}`} isOpen={IsDeleteModalOpen}
tabIndex="-1" type="delete"
role="dialog" header={
style={{ selectedEmpFordelete?.isActive
display: IsDeleteModalOpen ? "block" : "none", ? "Suspend Employee"
backgroundColor: IsDeleteModalOpen : "Reactivate Employee"
? "rgba(0,0,0,0.5)" }
: "transparent", message={`Are you sure you want to ${
}} selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
aria-hidden="false" } this employee?`}
> onSubmit={() =>
<ConfirmModal suspendEmployee({
type={"delete"} employeeId: selectedEmpFordelete.id,
header={ active: !selectedEmpFordelete.isActive,
selectedEmpFordelete?.isActive })
? "Suspend Employee" }
: "Reactivate Employee" onClose={() => setIsDeleteModalOpen(false)}
} loading={employeeLodaing}
message={`Are you sure you want to ${selectedEmpFordelete?.isActive ? "suspend" : "reactivate" />
} this employee?`}
onSubmit={() =>
suspendEmployee({
employeeId: selectedEmpFordelete.id,
active: !selectedEmpFordelete.isActive,
})
}
onClose={() => setIsDeleteModalOpen(false)}
loading={employeeLodaing}
/>
</div>
)} )}
<div className="container-fluid"> <div className="container-fluid">
@ -511,8 +502,9 @@ const EmployeeList = () => {
Status Status
</th> </th>
<th <th
className={`sorting_disabled ${!Manage_Employee && "d-none" className={`sorting_disabled ${
}`} !Manage_Employee && "d-none"
}`}
rowSpan="1" rowSpan="1"
colSpan="1" colSpan="1"
style={{ width: "50px" }} style={{ width: "50px" }}
@ -532,9 +524,9 @@ const EmployeeList = () => {
)} )}
{/* Conditional messages for no data or no search results */} {/* Conditional messages for no data or no search results */}
{!loading && {!loading &&
displayData?.length === 0 && displayData?.length === 0 &&
searchText && searchText &&
!showAllEmployees ? ( !showAllEmployees ? (
<tr> <tr>
<td colSpan={8}> <td colSpan={8}>
<small className="muted"> <small className="muted">
@ -544,8 +536,8 @@ const EmployeeList = () => {
</tr> </tr>
) : null} ) : null}
{!loading && {!loading &&
displayData?.length === 0 && displayData?.length === 0 &&
(!searchText || showAllEmployees) ? ( (!searchText || showAllEmployees) ? (
<tr> <tr>
<td <td
colSpan={8} colSpan={8}
@ -639,7 +631,9 @@ const EmployeeList = () => {
<div className="dropdown-menu dropdown-menu-end"> <div className="dropdown-menu dropdown-menu-end">
{/* View always visible */} {/* View always visible */}
<button <button
onClick={() => navigate(`/employee/${item.id}`)} onClick={() =>
navigate(`/employee/${item.id}`)
}
className="dropdown-item py-1" className="dropdown-item py-1"
> >
<i className="bx bx-detail bx-sm"></i> View <i className="bx bx-detail bx-sm"></i> View
@ -650,9 +644,12 @@ const EmployeeList = () => {
<> <>
<button <button
className="dropdown-item py-1" className="dropdown-item py-1"
onClick={() => handleEmployeeModel(item.id)} onClick={() =>
handleEmployeeModel(item.id)
}
> >
<i className="bx bx-edit bx-sm"></i> Edit <i className="bx bx-edit bx-sm"></i>{" "}
Edit
</button> </button>
{/* Suspend only when active */} {/* Suspend only when active */}
@ -661,7 +658,8 @@ const EmployeeList = () => {
className="dropdown-item py-1" className="dropdown-item py-1"
onClick={() => handleOpenDelete(item)} onClick={() => handleOpenDelete(item)}
> >
<i className="bx bx-task-x bx-sm"></i> Suspend <i className="bx bx-task-x bx-sm"></i>{" "}
Suspend
</button> </button>
)} )}
@ -670,11 +668,13 @@ const EmployeeList = () => {
type="button" type="button"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#managerole-modal" data-bs-target="#managerole-modal"
onClick={() => setEmpForManageRole(item.id)} onClick={() =>
setEmpForManageRole(item.id)
}
> >
<i className="bx bx-cog bx-sm"></i> Manage Role <i className="bx bx-cog bx-sm"></i>{" "}
Manage Role
</button> </button>
</> </>
)} )}
@ -684,7 +684,8 @@ const EmployeeList = () => {
className="dropdown-item py-1" className="dropdown-item py-1"
onClick={() => handleOpenDelete(item)} onClick={() => handleOpenDelete(item)}
> >
<i className="bx bx-refresh bx-sm me-1"></i> Re-activate <i className="bx bx-refresh bx-sm me-1"></i>{" "}
Re-activate
</button> </button>
)} )}
</div> </div>
@ -703,8 +704,9 @@ const EmployeeList = () => {
<nav aria-label="Page"> <nav aria-label="Page">
<ul className="pagination pagination-sm justify-content-end py-1"> <ul className="pagination pagination-sm justify-content-end py-1">
<li <li
className={`page-item ${currentPage === 1 ? "disabled" : "" className={`page-item ${
}`} currentPage === 1 ? "disabled" : ""
}`}
> >
<button <button
className="page-link btn-xs" className="page-link btn-xs"
@ -717,8 +719,9 @@ const EmployeeList = () => {
{[...Array(totalPages)]?.map((_, index) => ( {[...Array(totalPages)]?.map((_, index) => (
<li <li
key={index} key={index}
className={`page-item ${currentPage === index + 1 ? "active" : "" className={`page-item ${
}`} currentPage === index + 1 ? "active" : ""
}`}
> >
<button <button
className="page-link" className="page-link"
@ -730,8 +733,9 @@ const EmployeeList = () => {
))} ))}
<li <li
className={`page-item ${currentPage === totalPages ? "disabled" : "" className={`page-item ${
}`} currentPage === totalPages ? "disabled" : ""
}`}
> >
<button <button
className="page-link" className="page-link"

View File

@ -4,43 +4,45 @@ import MasterModal from "../../components/master/MasterModal";
import { mastersList } from "../../data/masters"; import { mastersList } from "../../data/masters";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice"; import { changeMaster } from "../../slices/localVariablesSlice";
import useMaster, { useMasterMenu } from "../../hooks/masterHook/useMaster" import useMaster, {
useDeleteMasterItem,
useMasterMenu,
} from "../../hooks/masterHook/useMaster";
import MasterTable from "./MasterTable"; import MasterTable from "./MasterTable";
import { getCachedData } from "../../slices/apiDataManager"; import { getCachedData } from "../../slices/apiDataManager";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { MANAGE_MASTER } from "../../utils/constants"; import { MANAGE_MASTER } from "../../utils/constants";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import GlobalModel from "../../components/common/GlobalModel";
import ConfirmModal from "../../components/common/ConfirmModal";
const MasterPage = () => { const MasterPage = () => {
const {data,isLoading,isError,error:menuError} = useMasterMenu() const { data, isLoading, isError, error: menuError } = useMasterMenu();
const [modalConfig, setModalConfig] = useState({ modalType: "", item: null, masterType: null }); const [modalConfig, setModalConfig] = useState(null);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState("");
const [filteredResults, setFilteredResults] = useState([]); const [filteredResults, setFilteredResults] = useState([]);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const hasMasterPermission = useHasUserPermission(MANAGE_MASTER); const hasMasterPermission = useHasUserPermission(MANAGE_MASTER);
const dispatch = useDispatch(); const dispatch = useDispatch();
const selectedMaster = useSelector((store) => store.localVariables.selectedMaster); const selectedMaster = useSelector(
(store) => store.localVariables.selectedMaster
);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: masterData = [], loading, error, RecallApi,isError:isMasterError } = useMaster(); const {
data: masterData = [],
loading,
error,
RecallApi,
isError: isMasterError,
} = useMaster();
const { mutate: deleteMasterItem, isPending } = useDeleteMasterItem();
const openModal = () => setIsCreateModalOpen(true); const openModal = () => setIsCreateModalOpen(true);
const closeModal = () => { const closeModal = () => {
setIsCreateModalOpen(false); setIsCreateModalOpen(false);
setModalConfig(null); setModalConfig(null);
// Clean up Bootstrap modal manually
const modalEl = document.getElementById('master-modal');
modalEl?.classList.remove('show');
if (modalEl) modalEl.style.display = 'none';
document.body.classList.remove('modal-open');
document.body.style.overflow = 'auto';
document.querySelectorAll('.modal-backdrop').forEach((el) => el.remove());
}; };
const handleModalData = (modalType, item, masterType = selectedMaster) => { const handleModalData = (modalType, item, masterType = selectedMaster) => {
@ -54,15 +56,17 @@ const MasterPage = () => {
if (!masterData?.length) return; if (!masterData?.length) return;
const results = masterData.filter((item) => const results = masterData.filter((item) =>
Object.values(item).some( Object.values(item).some((field) =>
(field) => field?.toString().toLowerCase().includes(value) field?.toString().toLowerCase().includes(value)
) )
); );
setFilteredResults(results); setFilteredResults(results);
}; };
const displayData = useMemo(() => { const displayData = useMemo(() => {
if (searchTerm) return filteredResults; if (searchTerm) return filteredResults;
return queryClient.getQueryData(["masterData", selectedMaster]) || masterData; return (
queryClient.getQueryData(["masterData", selectedMaster]) || masterData
);
}, [searchTerm, filteredResults, selectedMaster, masterData]); }, [searchTerm, filteredResults, selectedMaster, masterData]);
const columns = useMemo(() => { const columns = useMemo(() => {
@ -73,9 +77,12 @@ const MasterPage = () => {
})); }));
}, [displayData]); }, [displayData]);
useEffect(() => { useEffect(() => {
if (modalConfig) openModal(); if (modalConfig?.modalType && modalConfig?.modalType !== "delete") {
}, [modalConfig]); openModal();
}
}, [modalConfig]);
useEffect(() => { useEffect(() => {
return () => { return () => {
@ -84,15 +91,46 @@ const MasterPage = () => {
}; };
}, []); }, []);
if(isError || isMasterError) return <div className="d-flex flex-column align-items-center justify-content-center py-5"> if (isError || isMasterError)
<h4 className=" mb-3"><i className="fa-solid fa-triangle-exclamation fs-5" /> Oops, an error occurred</h4> return (
<p className="text-muted">{error?.message || menuError?.message}</p> <div className="d-flex flex-column align-items-center justify-content-center py-5">
</div> <h4 className=" mb-3">
<i className="fa-solid fa-triangle-exclamation fs-5" /> Oops, an error
occurred
</h4>
<p className="text-muted">{error?.message || menuError?.message}</p>
</div>
);
return ( return (
<> <>
{isCreateModalOpen && ( {modalConfig?.modalType === "delete" && (
<MasterModal modaldata={modalConfig} closeModal={closeModal} /> <ConfirmModal
type="delete"
header={`Delete ${modalConfig.masterType}`}
message="Are you sure you want to delete?"
onSubmit={() => {
deleteMasterItem(
{
masterType: modalConfig.masterType,
item: modalConfig.item,
validateFn: modalConfig.validateFn,
},
{ onSuccess: closeModal }
);
}}
onClose={closeModal}
loading={isPending}
isOpen={true}
/>
)}
{isCreateModalOpen && modalConfig?.modalType !== "delete" && (
<GlobalModel
isOpen={isCreateModalOpen}
closeModal={closeModal}
size={modalConfig?.masterType === "Application Role" ? "lg" : "md"}
>
<MasterModal modaldata={modalConfig} closeModal={closeModal} />
</GlobalModel>
)} )}
<div className="container-fluid"> <div className="container-fluid">
@ -119,17 +157,25 @@ const MasterPage = () => {
> >
<label> <label>
<select <select
onChange={(e) => dispatch(changeMaster(e.target.value))} onChange={(e) =>
dispatch(changeMaster(e.target.value))
}
name="DataTables_Table_0_length" name="DataTables_Table_0_length"
aria-controls="DataTables_Table_0" 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} value={selectedMaster}
> >
{isLoading && (<option value={null}>Loading...</option>)} {isLoading && (
{(!isLoading && data) && data?.map((item) => ( <option value={null}>Loading...</option>
)}
<option key={item.id} value={item.name}>{item.name}</option> {!isLoading &&
))} data &&
data?.map((item) => (
<option key={item.id} value={item.name}>
{item.name}
</option>
))}
</select> </select>
</label> </label>
</div> </div>
@ -152,10 +198,13 @@ const MasterPage = () => {
></input> ></input>
</label> </label>
</div> </div>
<div className={`dt-buttons btn-group flex-wrap ${!hasMasterPermission && 'd-none'}`}> <div
className={`dt-buttons btn-group flex-wrap ${
!hasMasterPermission && "d-none"
}`}
>
{" "} {" "}
<div className="input-group"> <div className="input-group">
<button <button
className={`btn btn-sm add-new btn-primary `} className={`btn btn-sm add-new btn-primary `}
tabIndex="0" tabIndex="0"
@ -164,13 +213,17 @@ const MasterPage = () => {
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#master-modal" data-bs-target="#master-modal"
onClick={() => { onClick={() => {
handleModalData(selectedMaster, "null", selectedMaster) handleModalData(
selectedMaster,
null,
selectedMaster
);
}} }}
> >
<span> <span>
<i className="bx bx-plus-circle me-2"></i> <i className="bx bx-plus-circle me-2"></i>
<span className=" d-sm-inline-block"> <span className=" d-sm-inline-block">
Add {selectedMaster} Add {selectedMaster}
</span> </span>
</span> </span>
</button>{" "} </button>{" "}
@ -180,13 +233,17 @@ const MasterPage = () => {
</div> </div>
</div> </div>
<MasterTable data={displayData} columns={columns} loading={loading} handleModalData={handleModalData} /> <MasterTable
data={displayData}
columns={columns}
loading={loading}
handleModalData={handleModalData}
/>
<div style={{ width: "1%" }}></div> <div style={{ width: "1%" }}></div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</> </>
); );

View File

@ -20,7 +20,14 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => {
"noOfPersonsRequired", "noOfPersonsRequired",
"color", "color",
"displayName", "displayName",
"permissionIds" "permissionIds",
"entityTypeId",
"regexExpression",
"isMandatory",
"maxFilesAllowed",
"maxSizeAllowedInMB",
"isValidationRequired",
"documentCategory"
]; ];
const safeData = Array.isArray(data) ? data : []; const safeData = Array.isArray(data) ? data : [];
@ -75,17 +82,16 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => {
{loading ? ( {loading ? (
<p>Loading...</p> <p>Loading...</p>
) : ( ) : (
<table <table className="datatables-users table border-top dataTable no-footer dtr-column w-100">
className="datatables-users table border-top dataTable no-footer dtr-column" <thead className="shadow-sm">
id="DataTables_Table_0"
aria-describedby="DataTables_Table_0_info"
style={{ width: "100%" }}
>
<thead>
<tr> <tr>
<th></th> <th></th>
<th className="text-start"> {selectedMaster === "Activity" ? "Activity" : "Name"}</th> <th className="text-start"> {selectedMaster === "Activity" ? "Activity" : "Name"}</th>
<th className="text-start"> {selectedMaster === "Activity" ? "Unit" : "Description"}</th> <th className="text-start"> {selectedMaster === "Activity"
? "Unit"
: selectedMaster === "Document Type"
? "Content Type"
: "Description"}</th>
<th className={` ${!hasMasterPermission && "d-none"}`}> <th className={` ${!hasMasterPermission && "d-none"}`}>
Actions Actions
</th> </th>
@ -151,19 +157,17 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => {
data-bs-target="#master-modal" data-bs-target="#master-modal"
className="btn p-0 dropdown-toggle hide-arrow" className="btn p-0 dropdown-toggle hide-arrow"
onClick={() => onClick={() =>
handleModalData(`Edit-${selectedMaster}`, item) handleModalData(`Edit-${selectedMaster}`, item, selectedMaster)
} }
> >
<i className="bx bxs-edit me-2 text-primary"></i> <i className="bx bxs-edit me-2 text-primary"></i>
</button> </button>
<button <button
aria-label="Delete" aria-label="Delete"
type="button" type="button"
className="btn p-0 dropdown-toggle hide-arrow" className="btn p-0 dropdown-toggle hide-arrow"
data-bs-toggle="modal" onClick={() => handleModalData("delete", item, selectedMaster)}
data-bs-target="#master-modal"
onClick={() => handleModalData("delete", item)}
> >
<i className="bx bx-trash me-1 text-danger"></i> <i className="bx bx-trash me-1 text-danger"></i>
</button> </button>

View File

@ -13,12 +13,10 @@ import {
cacheData, cacheData,
clearCacheKey, clearCacheKey,
getCachedData, getCachedData,
useSelectedproject, useSelectedProject,
} from "../../slices/apiDataManager"; } from "../../slices/apiDataManager";
import "./ProjectDetails.css"; import "./ProjectDetails.css";
import { import { useProjectDetails } from "../../hooks/useProjects";
useProjectDetails,
} from "../../hooks/useProjects";
import { ComingSoonPage } from "../Misc/ComingSoonPage"; import { ComingSoonPage } from "../Misc/ComingSoonPage";
import Directory from "../Directory/Directory"; import Directory from "../Directory/Directory";
import eventBus from "../../services/eventBus"; import eventBus from "../../services/eventBus";
@ -26,19 +24,22 @@ import ProjectProgressChart from "../../components/Dashboard/ProjectProgressChar
import { useProjectName } from "../../hooks/useProjects"; import { useProjectName } from "../../hooks/useProjects";
import AttendanceOverview from "../../components/Dashboard/AttendanceChart"; import AttendanceOverview from "../../components/Dashboard/AttendanceChart";
import { setProjectId } from "../../slices/localVariablesSlice"; import { setProjectId } from "../../slices/localVariablesSlice";
import ProjectDocument from "../../components/Project/ProjectDocuments";
import ProjectDocuments from "../../components/Project/ProjectDocuments";
import ProjectSetting from "../../components/Project/ProjectSetting";
const ProjectDetails = () => { const ProjectDetails = () => {
const projectId = useSelectedproject() const projectId = useSelectedProject()
const { projectNames, fetchData } = useProjectName(); const { projectNames, fetchData } = useProjectName();
const dispatch = useDispatch() const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
if (projectId == null) { if (projectId == null) {
dispatch(setProjectId(projectNames[0]?.id)); dispatch(setProjectId(projectNames[0]?.id));
} }
}, [projectNames]) }, [projectNames]);
const { const {
projects_Details, projects_Details,
@ -49,12 +50,15 @@ const ProjectDetails = () => {
// const [activePill, setActivePill] = useState("profile"); // const [activePill, setActivePill] = useState("profile");
const [activePill, setActivePill] = useState(() => { const [activePill, setActivePill] = useState(() => {
return localStorage.getItem("lastActiveProjectTab") || "profile"; return localStorage.getItem("lastActiveProjectTab") || "profile";
}); });
const handler = useCallback( const handler = useCallback(
(msg) => { (msg) => {
if (msg.keyword === "Update_Project" && projects_Details?.id === msg.response.id) { if (
msg.keyword === "Update_Project" &&
projects_Details?.id === msg.response.id
) {
refetch(); refetch();
} }
}, },
@ -66,11 +70,10 @@ const ProjectDetails = () => {
return () => eventBus.off("project", handler); return () => eventBus.off("project", handler);
}, [handler]); }, [handler]);
const handlePillClick = (pillKey) => { const handlePillClick = (pillKey) => {
setActivePill(pillKey); setActivePill(pillKey);
localStorage.setItem("lastActiveProjectTab", pillKey); // Save to localStorage localStorage.setItem("lastActiveProjectTab", pillKey); // Save to localStorage
}; };
const renderContent = () => { const renderContent = () => {
if (projectLoading || !projects_Details) return <Loader />; if (projectLoading || !projects_Details) return <Loader />;
@ -85,9 +88,14 @@ const ProjectDetails = () => {
<ProjectOverview project={projectId} /> <ProjectOverview project={projectId} />
</div> </div>
<div className="col-lg-8 col-md-7 mt-5"> <div className="col-lg-8 col-md-7 mt-5">
<ProjectProgressChart ShowAllProject="false" DefaultRange="1M" /> <ProjectProgressChart
<div className="mt-5"> <AttendanceOverview /></div> ShowAllProject="false"
DefaultRange="1M"
/>
<div className="mt-5">
{" "}
<AttendanceOverview />
</div>
</div> </div>
</div> </div>
</> </>
@ -103,14 +111,10 @@ const ProjectDetails = () => {
); );
case "infra": case "infra":
return ( return <ProjectInfra data={projects_Details} onDataChange={refetch} />;
<ProjectInfra data={projects_Details} onDataChange={refetch} />
);
case "workplan": case "workplan":
return ( return <WorkPlan data={projects_Details} onDataChange={refetch} />;
<WorkPlan data={projects_Details} onDataChange={refetch} />
);
case "directory": case "directory":
return ( return (
@ -118,6 +122,18 @@ const ProjectDetails = () => {
<Directory IsPage={false} prefernceContacts={projects_Details.id} /> <Directory IsPage={false} prefernceContacts={projects_Details.id} />
</div> </div>
); );
case "documents":
return (
<div className="row">
<ProjectDocuments />
</div>
);
case "setting":
return (
<div className="row">
<ProjectSetting />
</div>
);
default: default:
return <ComingSoonPage />; return <ComingSoonPage />;
@ -142,4 +158,4 @@ const ProjectDetails = () => {
); );
}; };
export default ProjectDetails; export default ProjectDetails;

View File

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

View File

@ -0,0 +1,26 @@
import { api } from "../utils/axiosClient";
export const DocumentRepository = {
uploadDocument:(data)=> api.post(`/api/Document/upload`,data),
getDocumentList:(entityTypeId,entityId,pageSize, pageNumber, filter,searchString,isActive)=>{
const payloadJsonString = JSON.stringify(filter);
return api.get(`/api/Document/list/${entityTypeId}/entity/${entityId}/?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}&isActive=${isActive}`)
},
getDocumentById:(id)=>api.get(`/api/Document/get/details/${id}`),
getFilterEntities:(entityTypeId)=>api.get(`/api/Document/get/filter/${entityTypeId}`),
UpdateDocument:(documentId,data)=>api.put(`/api/Document/edit/${documentId}`,data),
getDocumentVersionList:(parentAttachmentId,pageSize,pageNumber)=>api.get(`/api/Document/list/versions/${parentAttachmentId}/?pageSize=${pageSize}&pageNumber=${pageNumber}`),
getDocumentVersion:(id)=>api.get(`/api/Document/get/version/${id}`),
verifyDocument:(id,isVerify)=>api.post(`/api/Document/verify/${id}/?isVerify=${isVerify}`),
deleteDocument:(id,isActive)=>api.delete(`/api/Document/delete/${id}/?isActive=${isActive}`),
getDocumentTags:()=>api.get('/api/Document/get/tags')
}

View File

@ -1,6 +1,6 @@
import { api } from "../utils/axiosClient"; import { api } from "../utils/axiosClient";
export const MarketRepository = { 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"), getIndustries: () => api.get("api/market/industries"),
}; };

View File

@ -18,7 +18,7 @@ export const RolesRepository = {
}; };
export const MasterRespository = { export const MasterRespository = {
getMasterMenus:()=>api.get("/api/AppMenu/get/master-list"), getMasterMenus: () => api.get("/api/AppMenu/get/master-list"),
getRoles: () => api.get("/api/roles"), getRoles: () => api.get("/api/roles"),
createRole: (data) => api.post("/api/roles", data), createRole: (data) => api.post("/api/roles", data),
@ -48,6 +48,9 @@ export const MasterRespository = {
api.delete(`/api/Master/payment-mode/delete/${id}`, (isActive = false)), api.delete(`/api/Master/payment-mode/delete/${id}`, (isActive = false)),
"Expense Status": (id, isActive) => "Expense Status": (id, isActive) =>
api.delete(`/api/Master/expenses-status/delete/${id}`, (isActive = false)), api.delete(`/api/Master/expenses-status/delete/${id}`, (isActive = false)),
"Document Type": (id) => api.delete(`/api/Master/document-type/delete/${id}`),
"Document Category": (id) =>
api.delete(`/api/Master/document-category/delete/${id}`),
getWorkCategory: () => api.get(`/api/master/work-categories`), getWorkCategory: () => api.get(`/api/master/work-categories`),
createWorkCategory: (data) => api.post(`/api/master/work-category`, data), createWorkCategory: (data) => api.post(`/api/master/work-category`, data),
@ -58,7 +61,7 @@ export const MasterRespository = {
createContactCategory: (data) => createContactCategory: (data) =>
api.post(`/api/master/contact-category`, data), api.post(`/api/master/contact-category`, data),
updateContactCategory: (id, data) => updateContactCategory: (id, data) =>
api.post(`/api/master/contact-category/edit/${id}`, data), api.put(`/api/master/contact-category/edit/${id}`, data),
getContactTag: () => api.get(`/api/master/contact-tags`), getContactTag: () => api.get(`/api/master/contact-tags`),
createContactTag: (data) => api.post(`/api/master/contact-tag`, data), createContactTag: (data) => api.post(`/api/master/contact-tag`, data),
@ -81,4 +84,26 @@ export const MasterRespository = {
createExpenseStatus: (data) => api.post("/api/Master/expenses-status", data), createExpenseStatus: (data) => api.post("/api/Master/expenses-status", data),
updateExepnseStatus: (id, data) => updateExepnseStatus: (id, data) =>
api.put(`/api/Master/expenses-status/edit/${id}`, data), api.put(`/api/Master/expenses-status/edit/${id}`, data),
getDocumentCategories: (entityType) =>
api.get(
`/api/Master/document-category/list${
entityType ? `?entityTypeId=${entityType}` : ""
}`
),
createDocumenyCategory: (data) =>
api.post(`/api/Master/document-category`, data),
updateDocumentCategory: (id, data) =>
api.put(`/api/Master/document-category/edit/${id}`, data),
getDocumentTypes: (category) =>
api.get(
`/api/Master/document-type/list${
category ? `?documentCategoryId=${category}` : ""
}`
),
createDocumentType: (data) => api.post(`/api/Master/document-type`, data),
updateDocumentType: (id, data) =>
api.put(`/api/Master/document-type/edit/${id}`, data),
}; };

View File

@ -37,6 +37,14 @@ const ProjectRepository = {
api.get( api.get(
`/api/project/tasks-employee/${id}?fromDate=${fromDate}&toDate=${toDate}` `/api/project/tasks-employee/${id}?fromDate=${fromDate}&toDate=${toDate}`
), ),
// Permission Managment for Employee at Project Level
getProjectLevelEmployeeList:(projectId)=>api.get(`/api/Project/get/proejct-level/employees/${projectId}`),
getProjectLevelModules:()=>api.get(`/api/Project/get/proejct-level/modules`),
getProjectLevelEmployeePermissions:(employeeId,projectId)=>api.get(`/api/Project/get/project-level-permission/employee/${employeeId}/project/${projectId}`),
updateProjectLevelEmployeePermission:(data)=>api.post(`/api/Project/assign/project-level-permission`,data)
}; };
export const TasksRepository = { export const TasksRepository = {

View File

@ -44,6 +44,7 @@ import ExpensePage from "../pages/Expense/ExpensePage";
import TenantDetails from "../pages/Tenant/TenantDetails"; import TenantDetails from "../pages/Tenant/TenantDetails";
import SelfTenantDetails from "../pages/Tenant/SelfTenantDetails"; import SelfTenantDetails from "../pages/Tenant/SelfTenantDetails";
import SuperTenantDetails from "../pages/Tenant/SuperTenantDetails"; import SuperTenantDetails from "../pages/Tenant/SuperTenantDetails";
import DocumentPage from "../pages/Documents/DocumentPage";
const router = createBrowserRouter( const router = createBrowserRouter(
[ [
@ -52,7 +53,7 @@ const router = createBrowserRouter(
children: [ children: [
{ path: "/auth/login", element: <LoginPage /> }, { path: "/auth/login", element: <LoginPage /> },
{ path: "/auth/login-otp", element: <LoginWithOtp /> }, { path: "/auth/login-otp", element: <LoginWithOtp /> },
{ path: "/auth/reqest/demo", element: <RegisterPage /> }, { path: "/market/enquire", element: <RegisterPage /> },
{ path: "/auth/forgot-password", element: <ForgotPasswordPage /> }, { path: "/auth/forgot-password", element: <ForgotPasswordPage /> },
{ path: "/reset-password", element: <ResetPasswordPage /> }, { path: "/reset-password", element: <ResetPasswordPage /> },
{ path: "/legal-info", element: <LegalInfoCard /> }, { path: "/legal-info", element: <LegalInfoCard /> },
@ -69,6 +70,7 @@ const router = createBrowserRouter(
{ path: "/", element: <Dashboard /> }, { path: "/", element: <Dashboard /> },
{ path: "/dashboard", element: <Dashboard /> }, { path: "/dashboard", element: <Dashboard /> },
{ path: "/projects", element: <ProjectList /> }, { path: "/projects", element: <ProjectList /> },
{ path: "/document", element: <DocumentPage /> },
{ path: "/projects/details", element: <ProjectDetails /> }, { path: "/projects/details", element: <ProjectDetails /> },
{ path: "/project/manage/:projectId", element: <ManageProject /> }, { path: "/project/manage/:projectId", element: <ManageProject /> },
{ path: "/employees", element: <EmployeeList /> }, { path: "/employees", element: <EmployeeList /> },

View File

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

26
src/utils/FileIcon.jsx Normal file
View File

@ -0,0 +1,26 @@
const contentTypeIcons = {
"application/pdf": "fa-solid fa-file-pdf text-danger",
"application/msword": "fa-solid fa-file-word text-primary",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
"fa-solid fa-file-word text-primary",
"application/vnd.ms-excel": "fa-solid fa-file-excel text-success",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
"fa-solid fa-file-excel text-success",
"application/vnd.ms-powerpoint": "fa-solid fa-file-powerpoint text-warning",
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
"fa-solid fa-file-powerpoint text-warning",
"image/jpg": "fa-solid fa-file-image text-info",
"image/jpeg": "fa-solid fa-file-image text-info",
"image/png": "fa-solid fa-file-image text-info",
"image/gif": "fa-solid fa-file-image text-info",
"text/plain": "fa-solid fa-file-lines text-secondary",
"text/csv": "fa-solid fa-file-csv text-success",
"application/json": "fa-solid fa-file-code text-dark",
folder: "fa-solid fa-folder text-warning", // special for folders
default: "fa-solid fa-file text-muted",
};
export const FileIcon = ({ type, size = "fs-4", className = "" }) => {
const iconClass = contentTypeIcons[type] || contentTypeIcons.default;
return <i className={`${iconClass} ${size} ${className}`}></i>;
};

View File

@ -61,3 +61,11 @@ export const getIconByFileType = (type = "") => {
return "bx bx-file"; return "bx bx-file";
}; };
export const normalizeAllowedContentTypes = (allowedContentType) => {
if (!allowedContentType) return [];
if (Array.isArray(allowedContentType)) return allowedContentType;
if (typeof allowedContentType === "string") return allowedContentType.split(",");
return [];
};

View File

@ -39,12 +39,14 @@ export const VIEW_TASK = "9fcc5f87-25e3-4846-90ac-67a71ab92e3c"
export const ASSIGN_REPORT_TASK = "6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2" export const ASSIGN_REPORT_TASK = "6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"
// ------------------------Directory-------------------------------------
export const DIRECTORY_ADMIN = "4286a13b-bb40-4879-8c6d-18e9e393beda" export const DIRECTORY_ADMIN = "4286a13b-bb40-4879-8c6d-18e9e393beda"
export const DIRECTORY_MANAGER = "62668630-13ce-4f52-a0f0-db38af2230c5" export const DIRECTORY_MANAGER = "62668630-13ce-4f52-a0f0-db38af2230c5"
export const DIRECTORY_USER = "0f919170-92d4-4337-abd3-49b66fc871bb" export const DIRECTORY_USER = "0f919170-92d4-4337-abd3-49b66fc871bb"
// -----------------------Expense----------------------------------------
export const VIEW_SELF_EXPENSE = "385be49f-8fde-440e-bdbc-3dffeb8dd116" export const VIEW_SELF_EXPENSE = "385be49f-8fde-440e-bdbc-3dffeb8dd116"
export const VIEW_ALL_EXPNESE = "01e06444-9ca7-4df4-b900-8c3fa051b92f"; export const VIEW_ALL_EXPNESE = "01e06444-9ca7-4df4-b900-8c3fa051b92f";
@ -55,7 +57,6 @@ export const REVIEW_EXPENSE = "1f4bda08-1873-449a-bb66-3e8222bd871b";
export const APPROVE_EXPENSE = "eaafdd76-8aac-45f9-a530-315589c6deca"; export const APPROVE_EXPENSE = "eaafdd76-8aac-45f9-a530-315589c6deca";
export const PROCESS_EXPENSE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11" export const PROCESS_EXPENSE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"
export const EXPENSE_MANAGE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11" export const EXPENSE_MANAGE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"
@ -64,10 +65,19 @@ export const EXPENSE_REJECTEDBY = ["d1ee5eec-24b6-4364-8673-a8f859c60729","965ed
export const EXPENSE_DRAFT = "297e0d8f-f668-41b5-bfea-e03b354251c8" export const EXPENSE_DRAFT = "297e0d8f-f668-41b5-bfea-e03b354251c8"
// ----------------------------Tenant-------------------------
export const SUPPER_TENANT = "d032cb1a-3f30-462c-bef0-7ace73a71c0b" export const SUPPER_TENANT = "d032cb1a-3f30-462c-bef0-7ace73a71c0b"
export const MANAGE_TENANTS = "00e20637-ce8d-4417-bec4-9b31b5e65092" export const MANAGE_TENANTS = "00e20637-ce8d-4417-bec4-9b31b5e65092"
export const VIEW_TENANTS = "647145c6-2108-4c98-aab4-178602236e55" export const VIEW_TENANTS = "647145c6-2108-4c98-aab4-178602236e55"
export const ActiveTenant = "297e0d8f-f668-41b5-bfea-e03b354251c8" export const ActiveTenant = "297e0d8f-f668-41b5-bfea-e03b354251c8"
// ---------------------Documents---------------------------------
export const VIEW_DOCUMENT = "71189504-f1c8-4ca5-8db6-810497be2854";
export const UPLOAD_DOCUMENT = "3f6d1f67-6fa5-4b7c-b17b-018d4fe4aab8";
export const MODIFY_DOCUMENT = "c423fd81-6273-4b9d-bb5e-76a0fb343833";
export const DELETE_DOCUMENT = "40863a13-5a66-469d-9b48-135bc5dbf486";
export const DOWNLOAD_DOCUMENT = "404373d0-860f-490e-a575-1c086ffbce1d";
export const VERIFY_DOCUMENT = "13a1f30f-38d1-41bf-8e7a-b75189aab8e0";
// -------------------Application Role------------------------------ // -------------------Application Role------------------------------
// 1 - Expense Manage // 1 - Expense Manage
@ -79,6 +89,12 @@ export const TENANT_STATUS = [
{id:"35d7840a-164a-448b-95e6-efb2ec84a751",name:"Supspended"} {id:"35d7840a-164a-448b-95e6-efb2ec84a751",name:"Supspended"}
] ]
export const DOCUMENTS_ENTITIES = {
ProjectEntity : "c8fe7115-aa27-43bc-99f4-7b05fabe436e",
EmployeeEntity:"dbb9555a-7a0c-40f2-a9ed-f0463f1ceed7",
}
export const CONSTANT_TEXT = { export const CONSTANT_TEXT = {
} }