Compare commits

...

87 Commits

Author SHA1 Message Date
Pramod Mahajan
80083aa5db updated some queries and mutation for prevent to duplicates calls 2025-07-02 19:42:43 +05:30
Pramod Mahajan
52c5e3d2d2 integrated reactquery for project infra 2025-07-02 13:20:17 +05:30
Pramod Mahajan
2f110b7ead Merge branch 'main' of https://git.marcoaiot.com/admin/marco.pms.web into react-query 2025-07-02 09:49:20 +05:30
Pramod Mahajan
0c8ff7b28c partially integrated react-query in project infra 2025-07-02 01:57:02 +05:30
c1a31e6b3e Merge pull request 'Issues_Jun_3W' (#226) from Issues_Jun_3W into main
Reviewed-on: #226
2025-07-01 10:24:37 +00:00
73b693f826 Merge pull request 'Missing Error Message When Required Fields Are Not Selected in Infrastructure > Create Task' (#225) from Kartik_Bug#521 into Issues_Jun_3W
Reviewed-on: #225
2025-07-01 09:36:03 +00:00
50f941aef6 Merge pull request '"OTP Sent Successfully" Message Displayed After Sending OTP' (#224) from Kartik_Bug#625 into Issues_Jun_3W
Reviewed-on: #224
2025-07-01 09:34:14 +00:00
520a0e679e Merge pull request 'Correction in Login Password text box design.' (#223) from Kartik_Bug#627 into Issues_Jun_3W
Reviewed-on: #223
2025-07-01 09:33:32 +00:00
85d8434c5f Merge pull request '401 Unauthorized Error on Attendance Menu.' (#222) from KartikBug#623 into Issues_Jun_3W
Reviewed-on: #222
2025-07-01 09:32:19 +00:00
2226d54f24 Missing Error Message When Required Fields Are Not Selected in Infrastructure > Create Task 2025-07-01 12:37:22 +05:30
5d99f11297 "OTP Sent Successfully" Message Displayed After Sending OTP 2025-07-01 12:33:57 +05:30
390a0b2b67 Correction in Login Password text box design. 2025-07-01 12:29:39 +05:30
2c0800f0e3 Unnecessary Blank Space Appears Below Employees in Regularization Tab. 2025-07-01 12:21:49 +05:30
e09413b23a 401 Unauthorized Error on Attendance Menu. 2025-07-01 12:19:25 +05:30
bb9c351189 Changes in Search text. 2025-06-30 18:29:03 +05:30
fb4275fb05 Merge branch 'Issues_Jun_3W' of https://git.marcoaiot.com/admin/marco.pms.web into Issues_Jun_3W 2025-06-30 17:12:28 +05:30
be0577e6d8 Changing link of ProjectNames to Projects. 2025-06-30 17:12:05 +05:30
5861288a9d Merge branch 'Issues_Jun_3W' of https://git.marcoaiot.com/admin/marco.pms.web into Issues_Jun_3W 2025-06-30 17:03:56 +05:30
7615bb9c24 Added line after each module 2025-06-30 17:03:52 +05:30
7dffae8957 Merge pull request 'Changes in the employee popup size.' (#221) from Kartik_Bug#524 into Issues_Jun_3W
Reviewed-on: #221
2025-06-30 11:16:51 +00:00
84563812b7 Adding cross button in Employee popup. 2025-06-30 16:42:58 +05:30
562a4ca46d "Changes in the employee popup size." 2025-06-30 16:27:12 +05:30
9c2b2d0b0e Merge pull request 'Changes in Directory in Filter, removing unnessary gaps and changing color of HideEditor.' (#220) from Kartik_Enhancement#624 into Issues_Jun_3W
Reviewed-on: #220
2025-06-30 10:49:47 +00:00
0b54e0b5f1 Changes in Directory Notes Filter. 2025-06-30 10:49:47 +00:00
9554caf25f Changes in Directory in Filter, removing unnessary gaps and changing color of HideEditor. 2025-06-30 10:49:47 +00:00
a96d3696bb Changed the Ids of view infra and manage infra 2025-06-30 13:15:30 +05:30
d86c8a2c0b Merge pull request '"Pagination overlaps on '+' (Add) button in Directory – bottom right corner"' (#218) from Kartik_Bug#611 into Issues_Jun_3W
Reviewed-on: #218
2025-06-30 06:43:00 +00:00
111325968d "Adding margin-bottom to the pagination section in Directory Notes." 2025-06-30 06:41:44 +00:00
baf6a4cf54 "Pagination overlaps on '+' (Add) button in Directory – bottom right corner" 2025-06-30 06:41:44 +00:00
a1b45c341a Merge pull request '"Directory filter popup closes on 'Clear' button click – it should remain open"' (#219) from Kartik_Bug#613 into Issues_Jun_3W
Reviewed-on: #219
2025-06-30 06:38:51 +00:00
da69ae6fec "In the Directory Notes, applying a filter changes the Clear button's state and color." 2025-06-30 06:38:51 +00:00
48d1592b8c "Directory filter popup closes on 'Clear' button click – it should remain open" 2025-06-30 06:38:51 +00:00
4700ba38cd Merge pull request '"Search field height mismatch in Directory – should match Card and List view height"' (#217) from Kartik_Bug#608 into Issues_Jun_3W
Reviewed-on: #217
2025-06-30 05:18:47 +00:00
67b694c697 "Search field height mismatch in Directory – should match Card and List view height" 2025-06-28 15:06:54 +05:30
2bdaed1d83 Merge pull request 'Enhanced the Directory module by adding Notes and Contacts tabs, with view-specific controls, export options, and search functionality for Notes.' (#215) from Kartik_Task#513 into Issues_Jun_3W
Reviewed-on: #215
2025-06-27 11:43:30 +00:00
af22b95316 Merge branch 'Kartik_Task#513' of https://git.marcoaiot.com/admin/marco.pms.web into Kartik_Task#513 2025-06-27 17:13:06 +05:30
58c17ffe37 remove divider line. 2025-06-27 17:13:02 +05:30
c7cd96f509 upadating directory filter in design. 2025-06-27 17:13:02 +05:30
92ad8485f6 Clear button is working at filter. 2025-06-27 17:13:02 +05:30
9396caea57 In Directory changing filter logic and adding popup. 2025-06-27 17:13:02 +05:30
d646711637 In Directory Inhance Filter in Notes add Clear button and space between checkbox and name. 2025-06-27 17:13:02 +05:30
a2761bdd4c Chnages in Directory for fetching data. 2025-06-27 17:13:02 +05:30
2df8187ade Changes in Directory. 2025-06-27 17:13:02 +05:30
35d5310cee Some Changes in Directory in Search functionality and Filter by Names. 2025-06-27 17:13:02 +05:30
52b796700d Adding filteration according to names. 2025-06-27 17:13:01 +05:30
3e0b4edf4a In directory Creating a tab view and add notes view. 2025-06-27 17:12:06 +05:30
b39aa2302d remove divider line. 2025-06-27 16:59:24 +05:30
45dadc88a2 upadating directory filter in design. 2025-06-27 16:57:57 +05:30
77cd8357cd Clear button is working at filter. 2025-06-27 16:04:15 +05:30
deb7100899 In Directory changing filter logic and adding popup. 2025-06-27 15:47:36 +05:30
Pramod Mahajan
f932a4c5a4 added condition for directory, for hide project dropdown 2025-06-27 11:45:29 +05:30
Pramod Mahajan
d1250739f9 fixed vertical scrolling layout 2025-06-27 10:59:41 +05:30
abc8bd8629 In Directory Inhance Filter in Notes add Clear button and space between checkbox and name. 2025-06-26 17:11:54 +05:30
d6d0fd9d14 Chnages in Directory for fetching data. 2025-06-26 11:30:07 +05:30
0a6d5746ed Changes in Directory. 2025-06-26 10:32:43 +05:30
7898d1ad81 Some Changes in Directory in Search functionality and Filter by Names. 2025-06-26 10:09:24 +05:30
8916e25940 Adding filteration according to names. 2025-06-25 17:58:11 +05:30
5e9e4a7bbf In directory Creating a tab view and add notes view. 2025-06-25 12:28:58 +05:30
Pramod Mahajan
237fea186b Merge branch 'Issues_Jun_3W' of https://git.marcoaiot.com/admin/marco.pms.web into Issues_Jun_3W 2025-06-24 16:45:05 +05:30
Pramod Mahajan
22c6072c32 Merge branch 'main' of https://git.marcoaiot.com/admin/marco.pms.web into Issues_Jun_3W 2025-06-24 16:31:54 +05:30
8f919436aa Merge pull request 'Ensured all table headers and cells are left-aligned for consistent layout and better readability.' (#214) from Kartik_Enhancement#517 into Issues_Jun_3W
Reviewed-on: #214
2025-06-23 12:30:16 +00:00
d8269cfbe8 Changes in search employee when double error message is shown. 2025-06-23 12:30:16 +00:00
589ba569f6 Correction in search function in Employees. 2025-06-23 12:30:16 +00:00
9f6d7580e3 Ensured all table headers and cells are left-aligned for consistent layout and better readability. 2025-06-23 12:30:16 +00:00
a682f20f81 Merge pull request 'Adding Export button in Directory.' (#213) from Kartik_Task#512_Exportbutton into Issues_Jun_3W
Reviewed-on: #213
2025-06-23 12:28:45 +00:00
Pramod Mahajan
79d6cd2f49 changed class for container "container-xxl" to "container-fluid" 2025-06-23 15:41:23 +05:30
056bd63869 Adding Export button in Directory. 2025-06-20 17:12:50 +05:30
089bfecfd3 Merge branch 'Issues_Jun_3W' of https://git.marcoaiot.com/admin/marco.pms.web into Issues_Jun_3W 2025-06-20 16:01:19 +05:30
6accbb3500 Add the weekday name (e.g., Monday) when hovering over a data point in the Project. 2025-06-20 15:55:17 +05:30
290b2d37d1 Remove the border from the 'Category' column in the list view of the directory. 2025-06-20 15:55:16 +05:30
882e3dd414 Project Address Text Overflow. 2025-06-20 15:55:16 +05:30
41af98e340 The current behavior is that when the "All Employees" filter is off, the system should display employees only for the globally selected project, not all employees. 2025-06-20 15:55:16 +05:30
c4c3e8d885 The search functionality in the Employees section does not work correctly after applying a filter. 2025-06-20 15:55:16 +05:30
Pramod Mahajan
5a13313bbd refactor: update axios client and auth repository for public/private route handling 2025-06-20 15:55:16 +05:30
38374caab3 Add cards in project list and diectory list views 2025-06-20 15:53:30 +05:30
246784c18a Merge pull request 'Add the weekday name (e.g., Monday) when hovering over a data point in the Project.' (#211) from Kartik_Task#511 into Issues_Jun_3W
Reviewed-on: #211
2025-06-20 09:13:07 +00:00
a6d3d2656b Add the weekday name (e.g., Monday) when hovering over a data point in the Project. 2025-06-20 09:13:07 +00:00
cffa683b20 Merge pull request 'Remove the border from the 'Category' column in the list view of the directory.' (#210) from Kartik_Bug#510 into Issues_Jun_3W
Reviewed-on: #210
2025-06-20 09:11:25 +00:00
4c1934518d Remove the border from the 'Category' column in the list view of the directory. 2025-06-20 09:11:25 +00:00
33b14208ec Merge pull request 'Project Address Text Overflow in the project card.' (#209) from Kartik_Bug#509 into Issues_Jun_3W
Reviewed-on: #209
2025-06-20 09:10:37 +00:00
a5161756c3 Project Address Text Overflow. 2025-06-20 09:10:37 +00:00
0131dfaa29 Merge pull request 'The search functionality in the Employees section does not work correctly after applying a filter.' (#208) from Kartik_Bug#508 into Issues_Jun_3W
Reviewed-on: #208
2025-06-20 09:09:18 +00:00
a2a8e90350 The current behavior is that when the "All Employees" filter is off, the system should display employees only for the globally selected project, not all employees. 2025-06-20 09:09:18 +00:00
33c3bb1282 The search functionality in the Employees section does not work correctly after applying a filter. 2025-06-20 09:09:18 +00:00
8e3ac744c0 Merge pull request 'pramod_Bug#494 : update axios client and auth repository for public/private route handling' (#207) from pramod_Bug#494 into Issues_Jun_3W
Reviewed-on: #207
2025-06-20 09:08:07 +00:00
Pramod Mahajan
39ee2d58ce refactor: update axios client and auth repository for public/private route handling 2025-06-20 09:08:07 +00:00
8ac9c64bf4 Add cards in project list and diectory list views 2025-06-20 14:33:51 +05:30
60 changed files with 3098 additions and 3567 deletions

View File

@ -213,3 +213,8 @@
.ql-editor {
max-height: 200px;
}
/* Remove Table Header Top Line */
thead tr {
border-top: 1px solid white;
}

View File

@ -4978,6 +4978,7 @@ fieldset:disabled .btn {
flex: 1 1 auto;
padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x);
color: var(--bs-card-color);
word-break: break-word;
}
.card-title {

View File

@ -143,7 +143,7 @@ const Attendance = ({
</tbody>
</table>
{!loading > 20 && (
{!loading && filteredData.length > 20 && (
<nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1">
<li

View File

@ -334,11 +334,11 @@ const AttendanceLog = ({
{!loading && !isRefreshing && data.length === 0 && (
<span>No employee logs</span>
)}
{error && !loading && !isRefreshing && (
{/* {error && !loading && !isRefreshing && (
<tr>
<td colSpan={6}>{error}</td>
</tr>
)}
)} */}
</div>
{!loading && !isRefreshing && processedData.length > 10 && (
<nav aria-label="Page ">

View File

@ -8,7 +8,7 @@ import WorkAreaModel from "../Project/Infrastructure/WorkAreaModel";
import TaskModel from "../Project/Infrastructure/TaskModel";
import ProjectRepository from "../../repositories/ProjectRepository";
import Breadcrumb from "../../components/common/Breadcrumb";
import {useProjectDetails, useProjects} from "../../hooks/useProjects";
import {useProjectDetails, useProjectInfra, useProjects} from "../../hooks/useProjects";
import {useHasUserPermission} from "../../hooks/useHasUserPermission";
import {MANAGE_PROJECT_INFRA} from "../../utils/constants";
import {useDispatch, useSelector} from "react-redux";
@ -21,11 +21,12 @@ const InfraPlanning = () =>
{
const {profile: LoggedUser, refetch : fetchData} = useProfile()
const dispatch = useDispatch()
// const {projects,loading:project_listLoader,error:projects_error} = useProjects()
const selectedProject = useSelector((store)=>store.localVariables.projectId)
const {projectInfra, isLoading, error} = useProjectInfra( selectedProject )
const ManageInfra = useHasUserPermission( MANAGE_PROJECT_INFRA )
const {projects_Details, loading: project_deatilsLoader, error: project_error,refetch} = useProjectDetails( selectedProject )
const reloadedData = useSelector( ( store ) => store.localVariables.reload )
@ -45,12 +46,9 @@ const InfraPlanning = () =>
<div className="card-body" style={{ padding: "0.5rem" }}>
<div className="align-items-center">
<div className="row ">
{project_deatilsLoader && ( <p>Loading...</p> )}
{( !project_deatilsLoader && projects_Details?.buildings.length === 0 ) && ( <p>No Result Found</p> )}
{(!project_deatilsLoader && projects_Details?.buildings?.length > 0) && (<InfraTable buildings={projects_Details?.buildings} projectId={projects_Details.id}/>)}
{isLoading && ( <p>Loading...</p> )}
{( !isLoading && projectInfra?.length === 0 ) && ( <p>No Result Found</p> )}
{(!isLoading && projectInfra?.length > 0) && (<InfraTable buildings={projectInfra} projectId={selectedProject}/>)}
</div>
</div>
</div>

View File

@ -44,7 +44,7 @@ const Regularization = ({ handleRequest }) => {
const { currentPage, totalPages, currentItems, paginate } = usePagination(
filteredData,
10
20
);
useEffect(() => {
eventBus.on("regularization", handler);
@ -67,8 +67,8 @@ const Regularization = ({ handleRequest }) => {
return (
<div
className="table-responsive text-nowrap"
style={{ minHeight: "300px" }}
className="table-responsive text-nowrap pb-4"
>
<table className="table mb-0">
<thead>
@ -85,11 +85,11 @@ const Regularization = ({ handleRequest }) => {
</tr>
</thead>
<tbody>
{loading && (
{/* {loading && (
<td colSpan={6} className="text-center py-5">
Loading...
</td>
)}
)} */}
{!loading &&
(regularizes?.length > 0 ? (
@ -145,9 +145,9 @@ const Regularization = ({ handleRequest }) => {
))}
</tbody>
</table>
{!loading > 10 && (
{!loading && totalPages > 1 && (
<nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1">
<ul className="pagination pagination-sm justify-content-end py-1 mt-3">
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
<button
className="page-link btn-xs"
@ -190,4 +190,4 @@ const Regularization = ({ handleRequest }) => {
);
};
export default Regularization;
export default Regularization;

View File

@ -17,7 +17,7 @@ const Dashboard = () => {
const { tasksCardData } = useDashboardTasksCardData();
return (
<div className="container-xxl flex-grow-1 container-p-y">
<div className="container-fluid mt-3">
<div className="row gy-4">
{/* Projects Card */}
<div className="col-sm-6 col-lg-4">

View File

@ -69,6 +69,7 @@ const ProjectProgressChart = () => {
);
const lineChartCategoriesDates = sortedDashboardData.map((d) =>
new Date(d.date).toLocaleDateString("en-US", {
weekday:"short",
month: "short",
day: "numeric",
year: "numeric",

View File

@ -88,10 +88,16 @@ const ListViewDirectory = ({
{contact.organization}
</td>
<td className="px-2" style={{ width: "10%" }}>
{/* <td className="px-2" style={{ width: "10%" }}>
<span className="badge badge-outline-secondary">
{contact?.contactCategory?.name || "Other"}
</span>
</td> */}
<td className="px-2" style={{ width: "10%" }}>
<span className="text-truncate">
{contact?.contactCategory?.name || "Other"}
</span>
</td>
<td className="align-middle text-center" style={{ width: "12%" }}>

View File

@ -0,0 +1,256 @@
import React, { useState } from "react";
import ReactQuill from "react-quill";
import moment from "moment";
import Avatar from "../common/Avatar";
import { DirectoryRepository } from "../../repositories/DirectoryRepository";
import showToast from "../../services/toastService";
import { cacheData, getCachedData } from "../../slices/apiDataManager";
import ConfirmModal from "../common/ConfirmModal"; // Make sure path is correct
import "../common/TextEditor/Editor.css";
import ProfileContactDirectory from "./ProfileContactDirectory";
import GlobalModel from "../common/GlobalModel";
const NoteCardDirectoryEditable = ({
noteItem,
contactId,
onNoteUpdate,
onNoteDelete,
}) => {
const [editing, setEditing] = useState(false);
const [editorValue, setEditorValue] = useState(noteItem.note);
const [isLoading, setIsLoading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isRestoring, setIsRestoring] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [open_contact, setOpen_contact] = useState(null);
const [isOpenModalNote, setIsOpenModalNote] = useState(false);
const handleUpdateNote = async () => {
try {
setIsLoading(true);
const payload = {
id: noteItem.id,
note: editorValue,
contactId,
};
const response = await DirectoryRepository.UpdateNote(noteItem.id, payload);
const cachedContactProfile = getCachedData("Contact Profile");
if (cachedContactProfile?.contactId === contactId) {
const updatedCache = {
...cachedContactProfile,
data: {
...cachedContactProfile.data,
notes: cachedContactProfile.data.notes.map((note) =>
note.id === noteItem.id ? response.data : note
),
},
};
cacheData("Contact Profile", updatedCache);
}
onNoteUpdate?.(response.data);
setEditing(false);
showToast("Note updated successfully", "success");
} catch (error) {
showToast("Failed to update note", "error");
} finally {
setIsLoading(false);
}
};
const suspendEmployee = async () => {
try {
setIsDeleting(true);
await DirectoryRepository.DeleteNote(noteItem.id, false);
onNoteDelete?.(noteItem.id);
setIsDeleteModalOpen(false);
showToast("Note deleted successfully", "success");
} catch (error) {
showToast("Failed to delete note", "error");
} finally {
setIsDeleting(false);
}
};
const contactProfile = (contactId) => {
DirectoryRepository.GetContactProfile(contactId).then((res) => {
setOpen_contact(res?.data);
setIsOpenModalNote(true);
});
};
const handleRestore = async () => {
try {
setIsRestoring(true);
await DirectoryRepository.DeleteNote(noteItem.id, true);
onNoteDelete?.(noteItem.id);
showToast("Note restored successfully", "success");
} catch (error) {
showToast("Failed to restore note", "error");
} finally {
setIsRestoring(false);
}
};
return (
<>
{isOpenModalNote && (
<GlobalModel
isOpen={isOpenModalNote}
closeModal={() => {
setOpen_contact(null);
setIsOpenModalNote(false);
}}
size="xl"
>
{open_contact && (
<ProfileContactDirectory
contact={open_contact}
setOpen_contact={setOpen_contact}
closeModal={() => setIsOpenModalNote(false)}
/>
)}
</GlobalModel>
)}
<div
className="card shadow-sm border-1 mb-3 p-4 rounded"
style={{
width: "100%",
background: noteItem.isActive ? "#fff" : "#f8f6f6",
}}
key={noteItem.id}
>
{/* Header */}
<div className="d-flex justify-content-between align-items-center mb-1">
<div className="d-flex align-items-center">
<Avatar
size="xxs"
firstName={noteItem?.createdBy?.firstName}
lastName={noteItem?.createdBy?.lastName}
className="m-0"
/>
<div>
<div className="d-flex ms-0 align-middle cursor-pointer" onClick={() =>contactProfile(noteItem.contactId)}>
<span>
<span className="fw-bold "> {noteItem?.contactName} </span> <span className="text-muted font-weight-normal">
({noteItem?.organizationName})
</span>
</span>
</div>
<div className="d-flex ms-0 align-middle">
</div>
<div className="d-flex ms-0 mt-2">
<span className="text-muted">
by <span className="fw-bold "> {noteItem?.createdBy?.firstName} {noteItem?.createdBy?.lastName} </span>
&nbsp; <span className="text-muted">
on {moment
.utc(noteItem?.createdAt)
.add(5, "hours")
.add(30, "minutes")
.format("MMMM DD, YYYY [at] hh:mm A")}
</span>
</span>
</div>
</div>
</div>
{/* Action Icons */}
<div>
{noteItem.isActive ? (
<>
<i
className="bx bxs-edit bx-sm me-2 text-primary cursor-pointer"
onClick={() => setEditing(true)}
title="Edit"
></i>
{!isDeleting ? (
<i
className="bx bx-trash bx-sm me-2 text-danger cursor-pointer"
onClick={() => setIsDeleteModalOpen(true)}
title="Delete"
></i>
) : (
<div className="spinner-border spinner-border-sm text-danger" />
)}
</>
) : isRestoring ? (
<i className="bx bx-loader-alt bx-spin text-primary"></i>
) : (
<i
className="bx bx-recycle me-2 text-success cursor-pointer"
onClick={handleRestore}
title="Restore"
></i>
)}
</div>
</div>
<hr className="mt-0 mb-2" />
{/* Editor or Content */}
{editing ? (
<>
<ReactQuill
value={editorValue}
onChange={setEditorValue}
theme="snow"
className="compact-editor"
/>
<div className="d-flex justify-content-end gap-3 mt-2">
<span
className="text-secondary cursor-pointer"
onClick={() => setEditing(false)}
>
Cancel
</span>
<span
className="text-primary cursor-pointer"
onClick={handleUpdateNote}
>
{isLoading ? "Saving..." : "Submit"}
</span>
</div>
</>
) : (
<div
className="mx-4 px-10 text-start"
dangerouslySetInnerHTML={{ __html: noteItem.note }}
/>
)}
</div>
{/* Delete Confirm Modal */}
{isDeleteModalOpen && (
<div
className={`modal fade ${isDeleteModalOpen ? "show" : ""}`}
tabIndex="-1"
role="dialog"
style={{
display: isDeleteModalOpen ? "block" : "none",
backgroundColor: "rgba(0,0,0,0.5)",
}}
aria-hidden="false"
>
<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>
)}
</>
);
};
export default NoteCardDirectoryEditable;

View File

@ -0,0 +1,176 @@
import React, { useEffect, useState, useMemo } from "react";
import { DirectoryRepository } from "../../repositories/DirectoryRepository";
import NoteCardDirectoryEditable from "./NoteCardDirectoryEditable";
const NotesCardViewDirectory = ({ notes, setNotesForFilter, searchText, filterAppliedNotes }) => {
const [allNotes, setAllNotes] = useState([]);
const [filteredNotes, setFilteredNotes] = useState([]);
const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [selectedCreators, setSelectedCreators] = useState([]);
const [selectedOrgs, setSelectedOrgs] = useState([]);
const pageSize = 20;
useEffect(() => {
fetchNotes();
}, []);
const fetchNotes = async () => {
setLoading(true);
try {
const response = await DirectoryRepository.GetNotes(1000, 1);
const fetchedNotes = response.data?.data || [];
setAllNotes(fetchedNotes);
setNotesForFilter(fetchedNotes)
const creatorsSet = new Set();
const orgsSet = new Set();
fetchedNotes.forEach((note) => {
const creator = `${note.createdBy?.firstName || ""} ${note.createdBy?.lastName || ""}`.trim();
if (creator) creatorsSet.add(creator);
const org = note.organizationName;
if (org) orgsSet.add(org);
});
} catch (error) {
console.error("Failed to fetch notes:", error);
} finally {
setLoading(false);
}
};
const applyCombinedFilter = () => {
const lowerSearch = searchText?.toLowerCase() || "";
const filtered = allNotes.filter((noteItem) => {
const creator = `${noteItem.createdBy?.firstName || ""} ${noteItem.createdBy?.lastName || ""}`.trim();
const org = noteItem.organizationName;
const matchesCreator = selectedCreators.length === 0 || selectedCreators.includes(creator);
const matchesOrg = selectedOrgs.length === 0 || selectedOrgs.includes(org);
const plainNote = noteItem?.note?.replace(/<[^>]+>/g, "").toLowerCase();
const stringValues = [];
const extractStrings = (obj) => {
for (const key in obj) {
const value = obj[key];
if (typeof value === "string") {
stringValues.push(value.toLowerCase());
} else if (typeof value === "object" && value !== null) {
extractStrings(value);
}
}
};
extractStrings(noteItem);
stringValues.push(plainNote, creator.toLowerCase());
const matchesSearch = stringValues.some((val) => val.includes(lowerSearch));
return matchesCreator && matchesOrg && matchesSearch;
});
setFilteredNotes(filtered);
setCurrentPage(1);
setTotalPages(Math.ceil(filtered.length / pageSize));
};
useEffect(() => {
applyCombinedFilter();
}, [searchText, allNotes]);
useEffect(() => {
setFilteredNotes(filterAppliedNotes);
}, [filterAppliedNotes])
const currentItems = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
return filteredNotes.slice(startIndex, startIndex + pageSize);
}, [filteredNotes, currentPage]);
const handlePageClick = (page) => {
if (page !== currentPage) {
setCurrentPage(page);
}
};
if (loading) return <p className="mt-10 text-center">Loading notes...</p>;
if (!filteredNotes.length) return <p className="mt-10 text-center">No matching notes found</p>;
return (
<div className="w-100 h-100 ">
{/* Filter Dropdown */}
<div className="dropdown mb-3 ms-2">
</div>
{/* Notes List */}
<div className="d-flex flex-column text-start" style={{ gap: "0rem", minHeight: "100%" }}>
{currentItems.map((noteItem) => (
<NoteCardDirectoryEditable
key={noteItem.id}
noteItem={noteItem}
contactId={noteItem.contactId}
onNoteUpdate={(updatedNote) => {
setAllNotes((prevNotes) =>
prevNotes.map((n) => (n.id === updatedNote.id ? updatedNote : n))
);
}}
onNoteDelete={() => fetchNotes()}
/>
))}
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="d-flex justify-content-end mt-2 align-items-center gap-2"
style={{ marginBottom: '70px' }}>
{/* Previous Button */}
<button
className="btn btn-sm rounded-circle border text-secondary"
onClick={() => handlePageClick(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
title="Previous"
style={{ width: "30px", height: "30px", padding: 0, fontSize: "0.75rem" }} // Adjusted width, height, and font size
>
«
</button>
{/* Page Number Buttons */}
{[...Array(totalPages)].map((_, i) => {
const page = i + 1;
return (
<button
key={page}
className={`btn rounded-circle border ${page === currentPage ? "btn-primary text-white" : "btn-light text-secondary"}`}
style={{ width: "30px", height: "30px", padding: 0, fontSize: "0.75rem", lineHeight: "1" }} // Adjusted width, height, and font size
onClick={() => handlePageClick(page)}
>
{page}
</button>
);
})}
{/* Next Button */}
<button
className="btn btn-sm rounded-circle border text-secondary"
onClick={() => handlePageClick(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
title="Next"
style={{ width: "30px", height: "30px", padding: 0, fontSize: "0.75rem" }} // Adjusted width, height, and font size
>
»
</button>
</div>
)}
</div>
);
};
export default NotesCardViewDirectory;

View File

@ -135,7 +135,7 @@ const NotesDirectory = ({
<div className="d-flex justify-content-end">
<span
className={`btn btn-sm ${addNote ? "btn-danger" : "btn-primary"}`}
className={`btn btn-sm ${addNote ? "btn-secondary" : "btn-primary"}`}
onClick={() => setAddNote(!addNote)}
>
{addNote ? "Hide Editor" : "Add a Note"}

View File

@ -3,7 +3,7 @@ import React from "react";
const DemoTable = () => {
return (
<div className="content-wrapper">
<div className="container-xxl flex-grow-1 container-p-y">
<div className="container-fluid">
<div className="card">
<div className="card-datatable table-responsive">
<table className="datatables-basic table border-top">

View File

@ -22,7 +22,7 @@ const mobileNumberRegex = /^[0-9]\d{9}$/;
const ManageEmployee = ({ employeeId, onClosed,IsAllEmployee }) => {
const dispatch = useDispatch();
const { mutate: updateEmployee, isLoading } = useUpdateEmployee();
const { mutate: updateEmployee, isPending } = useUpdateEmployee();
const {
employee,
@ -163,40 +163,6 @@ const { mutate: updateEmployee, isLoading } = useUpdateEmployee();
});
const AadharNumberValue = watch("aadharNumber") || "";
// const onSubmit = (data) => {
// setLoading(true);
// if (data.email == "") {
// data.email = null;
// }
// EmployeeRepository.manageEmployee(data)
// .then((response) => {
// cacheData("employeeProfileInfo", data);
// showToast(
// `Employee details ${
// data.id == null ? "created" : "updated"
// } successfully.`,
// "success"
// );
// clearCacheKey("employeeListByProject");
// clearCacheKey("allEmployeeList");
// clearCacheKey("allInactiveEmployeeList");
// clearCacheKey("employeeProfile");
// setLoading(false);
// reset();
// // navigation("/employees");
// onClosed();
// })
// .catch((error) => {
// const message =
// error?.response?.data?.message ||
// error?.message ||
// "Error occured during api calling";
// showToast(message, "error");
// setLoading(false);
// });
// };
const onSubmit = (data) => {
if (data.email === "") {
@ -222,25 +188,25 @@ const { mutate: updateEmployee, isLoading } = useUpdateEmployee();
reset(
currentEmployee
? {
id: currentEmployee.id || null,
firstName: currentEmployee.firstName || "",
middleName: currentEmployee.middleName || "",
lastName: currentEmployee.lastName || "",
email: currentEmployee.email || "",
currentAddress: currentEmployee.currentAddress || "",
birthDate: formatDate(currentEmployee.birthDate) || "",
joiningDate: formatDate(currentEmployee.joiningDate) || "",
emergencyPhoneNumber: currentEmployee.emergencyPhoneNumber || "",
emergencyContactPerson:
currentEmployee.emergencyContactPerson || "",
aadharNumber: currentEmployee.aadharNumber || "",
gender: currentEmployee.gender || "",
panNumber: currentEmployee.panNumber || "",
permanentAddress: currentEmployee.permanentAddress || "",
phoneNumber: currentEmployee.phoneNumber || "",
jobRoleId: currentEmployee.jobRoleId?.toString() || "",
}
: {} // Empty object resets the form
id: currentEmployee.id || null,
firstName: currentEmployee.firstName || "",
middleName: currentEmployee.middleName || "",
lastName: currentEmployee.lastName || "",
email: currentEmployee.email || "",
currentAddress: currentEmployee.currentAddress || "",
birthDate: formatDate(currentEmployee.birthDate) || "",
joiningDate: formatDate(currentEmployee.joiningDate) || "",
emergencyPhoneNumber: currentEmployee.emergencyPhoneNumber || "",
emergencyContactPerson:
currentEmployee.emergencyContactPerson || "",
aadharNumber: currentEmployee.aadharNumber || "",
gender: currentEmployee.gender || "",
panNumber: currentEmployee.panNumber || "",
permanentAddress: currentEmployee.permanentAddress || "",
phoneNumber: currentEmployee.phoneNumber || "",
jobRoleId: currentEmployee.jobRoleId?.toString() || "",
}
: {}
);
setCurrentAddressLength(currentEmployee?.currentAddress?.length || 0);
setPermanentAddressLength(currentEmployee?.permanentAddress?.length || 0);
@ -274,378 +240,378 @@ const { mutate: updateEmployee, isLoading } = useUpdateEmployee();
<div className="col-sm-4">
<div className="form-text text-start">Middle Name</div>
<input
type="text"
{...register("middleName")}
className="form-control form-control-sm"
id="middleName"
placeholder="Middle Name"
/>
{errors.middleName && (
<div
className="danger-text text-start "
style={{ fontSize: "12px" }}
>
{errors.middleName.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">Last Name</div>
<input
type="text"
{...register("lastName")}
className="form-control form-control-sm"
id="lastName"
placeholder="Last Name"
/>
{errors.lastName && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.lastName.message}
</div>
)}
</div>
</div>
<div className="row mb-3">
<div className="col-sm-6">
<div className="form-text text-start">Email</div>
<input
type="email"
id="email"
{...register("email")}
className="form-control form-control-sm"
placeholder="example@domain.com"
maxLength={80}
aria-describedby="Email"
disabled={!!currentEmployee?.email}
/>
{errors.email && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.email.message}
</div>
)}
</div>
<div className="col-sm-6">
<div className="form-text text-start">Phone Number</div>
<input
type="text"
keyboardType="numeric"
id="phoneNumber"
{...register("phoneNumber")}
className="form-control form-control-sm"
placeholder="Phone Number"
inputMode="numeric"
maxLength={10}
/>
{errors.phoneNumber && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.phoneNumber.message}
</div>
)}
</div>
</div>
<div className="row mb-3"></div>
<div className="row mb-3">
<div className="col-sm-4">
<div className="form-text text-start">Gender</div>
<input
type="text"
{...register("middleName")}
className="form-control form-control-sm"
id="middleName"
placeholder="Middle Name"
/>
{errors.middleName && (
<div
className="danger-text text-start "
style={{ fontSize: "12px" }}
>
{errors.middleName.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">Last Name</div>
<input
type="text"
{...register("lastName")}
className="form-control form-control-sm"
id="lastName"
placeholder="Last Name"
/>
{errors.lastName && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.lastName.message}
</div>
)}
</div>
</div>
<div className="row mb-3">
<div className="col-sm-6">
<div className="form-text text-start">Email</div>
<input
type="email"
id="email"
{...register("email")}
className="form-control form-control-sm"
placeholder="example@domain.com"
maxLength={80}
aria-describedby="Email"
disabled={!!currentEmployee?.email}
/>
{errors.email && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.email.message}
</div>
)}
</div>
<div className="col-sm-6">
<div className="form-text text-start">Phone Number</div>
<input
type="text"
keyboardType="numeric"
id="phoneNumber"
{...register("phoneNumber")}
className="form-control form-control-sm"
placeholder="Phone Number"
inputMode="numeric"
maxLength={10}
/>
{errors.phoneNumber && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.phoneNumber.message}
</div>
)}
</div>
</div>
<div className="row mb-3"></div>
<div className="row mb-3">
<div className="col-sm-4">
<div className="form-text text-start">Gender</div>
<div className="input-group input-group-merge ">
<select
className="form-select form-select-sm "
{...register("gender")}
id="gender"
aria-label=""
>
<option disabled value="">
Select Gender
</option>
<option value="Male">Male </option>
<option value="Female">Female</option>
<option value="Other">Other</option>
</select>
</div>
{errors.gender && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.gender.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">Birth Date</div>
<div className="input-group input-group-merge ">
<select
className="form-select form-select-sm "
{...register("gender")}
id="gender"
aria-label=""
>
<option disabled value="">
Select Gender
</option>
<option value="Male">Male </option>
<option value="Female">Female</option>
<option value="Other">Other</option>
</select>
</div>
{errors.gender && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.gender.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">Birth Date</div>
<div className="input-group input-group-merge ">
<input
className="form-control form-control-sm"
type="date"
{...register("birthDate")}
id="birthDate"
/>
</div>
{errors.birthDate && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.birthDate.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">Joining Date</div>
<div className="input-group input-group-merge ">
<input
className="form-control form-control-sm"
type="date"
{...register("birthDate")}
id="birthDate"
/>
</div>
{errors.birthDate && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.birthDate.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">Joining Date</div>
<div className="input-group input-group-merge ">
<input
className="form-control form-control-sm"
type="date"
{...register("joiningDate")}
id="joiningDate"
/>
</div>
{errors.joiningDate && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.joiningDate.message}
</div>
)}
</div>
</div>
<div className="row mb-3">
<div className="col-sm-6">
<div className="form-text text-start">Current Address</div>
<div className="input-group input-group-merge ">
<input
className="form-control form-control-sm"
type="date"
{...register("joiningDate")}
id="joiningDate"
/>
</div>
{errors.joiningDate && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.joiningDate.message}
</div>
)}
</div>
</div>
<div className="row mb-3">
<div className="col-sm-6">
<div className="form-text text-start">Current Address</div>
<textarea
id="currentAddress"
className="form-control form-control-sm"
placeholder="Current Address"
aria-label="Current Address"
aria-describedby="basic-icon-default-message2"
{...register("currentAddress")}
maxLength={500}
onChange={(e) => {
setCurrentAddressLength(e.target.value.length);
// let react-hook-form still handle it
register("currentAddress").onChange(e);
}}
></textarea>
<div className="text-end muted">
<small>
{" "}
{500 - currentAddressLength} characters left
</small>
</div>
{errors.currentAddress && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.currentAddress.message}
</div>
)}
</div>
<div className="col-sm-6">
<div className="form-text text-start">
Permanent Address
</div>
<textarea
id="currentAddress"
className="form-control form-control-sm"
placeholder="Current Address"
aria-label="Current Address"
aria-describedby="basic-icon-default-message2"
{...register("currentAddress")}
maxLength={500}
onChange={(e) => {
setCurrentAddressLength(e.target.value.length);
// let react-hook-form still handle it
register("currentAddress").onChange(e);
}}
></textarea>
<div className="text-end muted">
<small>
{" "}
{500 - currentAddressLength} characters left
</small>
</div>
{errors.currentAddress && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.currentAddress.message}
</div>
)}
</div>
<div className="col-sm-6">
<div className="form-text text-start">
Permanent Address
</div>
<textarea
id="permanentAddress"
className="form-control form-control-sm"
placeholder="Permanent Address"
aria-label="Permanent Address"
aria-describedby="basic-icon-default-message2"
{...register("permanentAddress")}
maxLength={500}
onChange={(e) => {
setPermanentAddressLength(e.target.value.length);
register("permanentAddress").onChange(e);
}}
></textarea>
<div className="text-end muted">
<small>
{500 - permanentAddressLength} characters left
</small>
</div>
{errors.permanentAddress && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.permanentAddress.message}
</div>
)}
</div>
</div>
<div className="row mb-3">
{" "}
<div className="divider">
<div className="divider-text">Other Information</div>
</div>
</div>
<div className="row mb-3">
<div className="col-sm-4">
<div className="form-text text-start">Role</div>
<div className="input-group input-group-merge ">
<select
className="form-select form-select-sm"
{...register("jobRoleId")}
id="jobRoleId"
aria-label=""
>
<option disabled value="">
Select Role
</option>
{[...job_role]
.sort((a, b) => a?.name?.localeCompare(b.name))
.map((item) => (
<option value={item?.id} key={item.id}>
{item?.name}{" "}
</option>
))}
</select>
</div>
{errors.jobRoleId && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.jobRoleId.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">
Emergency Contact Person
</div>
<input
type="text"
{...register("emergencyContactPerson")}
className="form-control form-control-sm"
id="emergencyContactPerson"
maxLength={50}
placeholder="Contact Person"
/>
{errors.emergencyContactPerson && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.emergencyContactPerson.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">
Emergency Phone Number
</div>
<input
type="text"
{...register("emergencyPhoneNumber")}
className="form-control form-control-sm phone-mask"
id="emergencyPhoneNumber"
placeholder="Phone Number"
inputMode="numeric"
maxLength={10}
/>
{errors.emergencyPhoneNumber && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.emergencyPhoneNumber.message}
</div>
)}
</div>
</div>
<div className="row mb-3 d-none">
<div className="col-sm-6">
<div className="form-text text-start">AADHAR Number</div>
<textarea
id="permanentAddress"
className="form-control form-control-sm"
placeholder="Permanent Address"
aria-label="Permanent Address"
aria-describedby="basic-icon-default-message2"
{...register("permanentAddress")}
maxLength={500}
onChange={(e) => {
setPermanentAddressLength(e.target.value.length);
register("permanentAddress").onChange(e);
}}
></textarea>
<div className="text-end muted">
<small>
{500 - permanentAddressLength} characters left
</small>
</div>
{errors.permanentAddress && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.permanentAddress.message}
</div>
)}
</div>
</div>
<div className="row mb-3">
{" "}
<div className="divider">
<div className="divider-text">Other Information</div>
</div>
</div>
<div className="row mb-3">
<div className="col-sm-4">
<div className="form-text text-start">Role</div>
<div className="input-group input-group-merge ">
<select
className="form-select form-select-sm"
{...register("jobRoleId")}
id="jobRoleId"
aria-label=""
>
<option disabled value="">
Select Role
</option>
{[...job_role]
.sort((a, b) => a?.name?.localeCompare(b.name))
.map((item) => (
<option value={item?.id} key={item.id}>
{item?.name}{" "}
</option>
))}
</select>
</div>
{errors.jobRoleId && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.jobRoleId.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">
Emergency Contact Person
</div>
<input
type="text"
{...register("emergencyContactPerson")}
className="form-control form-control-sm"
id="emergencyContactPerson"
maxLength={50}
placeholder="Contact Person"
/>
{errors.emergencyContactPerson && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.emergencyContactPerson.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">
Emergency Phone Number
</div>
<input
type="text"
{...register("emergencyPhoneNumber")}
className="form-control form-control-sm phone-mask"
id="emergencyPhoneNumber"
placeholder="Phone Number"
inputMode="numeric"
maxLength={10}
/>
{errors.emergencyPhoneNumber && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.emergencyPhoneNumber.message}
</div>
)}
</div>
</div>
<div className="row mb-3 d-none">
<div className="col-sm-6">
<div className="form-text text-start">AADHAR Number</div>
<input
type="text"
{...register("aadharNumber")}
className="form-control form-control-sm"
id="aadharNumber"
placeholder="AADHAR Number"
maxLength={12}
inputMode="numeric"
/>
{errors.aadharNumber && (
<div className="danger-text text-start">
{errors.aadharNumber.message}
</div>
)}
</div>
<div className="col-sm-6 d-none">
<div className="form-text text-start">PAN Number</div>
<input
type="text"
{...register("aadharNumber")}
className="form-control form-control-sm"
id="aadharNumber"
placeholder="AADHAR Number"
maxLength={12}
inputMode="numeric"
/>
{errors.aadharNumber && (
<div className="danger-text text-start">
{errors.aadharNumber.message}
</div>
)}
</div>
<div className="col-sm-6 d-none">
<div className="form-text text-start">PAN Number</div>
<input
type="text"
{...register("panNumber")}
className="form-control form-control-sm"
id="panNumber"
placeholder="PAN Number"
maxLength={10}
/>
{errors.panNumber && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.panNumber.message}
</div>
)}
</div>
</div>
<input
type="text"
{...register("panNumber")}
className="form-control form-control-sm"
id="panNumber"
placeholder="PAN Number"
maxLength={10}
/>
{errors.panNumber && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.panNumber.message}
</div>
)}
</div>
</div>
{employeeId && (
<div className="row mb-3 d-none">
<div className="col-sm-12">
<input type="text" name="id" {...register("id")} />
</div>
</div>
)}
{employeeId && (
<div className="row mb-3 d-none">
<div className="col-sm-12">
<input type="text" name="id" {...register("id")} />
</div>
</div>
)}
<div className="row justify-content-start">
<div className="col-sm-12">
<button
aria-label="manage employee"
type="submit"
className="btn btn-sm btn-primary"
disabled={isloading}
>
{isloading
? "Please Wait..."
: employeeId
? "Update"
: "Create"}
</button>
<div className="row justify-content-start">
<div className="col-sm-12">
<button
aria-label="manage employee"
type="submit"
className="btn btn-sm btn-primary"
disabled={isPending}
>
{isPending
? "Please Wait..."
: employeeId
? "Update"
: "Create"}
</button>
<button
aria-label="manage employee"
type="reset"
className="btn btn-sm btn-primary ms-2"
disabled={isPending}
>
Clear
</button>
</div>
</div>
</form>
<button
aria-label="manage employee"
type="reset"
className="btn btn-sm btn-primary ms-2"
disabled={isloading}
>
Clear
</button>
</div>
</div>
</form>
</>
);
};

View File

@ -22,7 +22,7 @@ import { MANAGE_PROJECT } from "../../utils/constants";
const Header = () => {
const {profile} = useProfile();
const location = useLocation();
const dispatch = useDispatch(changeMaster("Job Role"));
const dispatch = useDispatch();
const { data, loading } = useMaster();
const navigate = useNavigate();
const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT);
@ -103,7 +103,8 @@ const Header = () => {
}, [projectNames]);
/** Check if current page id project details page */
const isProjectPath = /^\/projects\/[a-f0-9-]{36}$/.test(location.pathname);
const isProjectPath = /^\/projects\/[a-f0-9-]{36}$/.test(location.pathname)
const isDirectoryPath = /^\/directory$/.test(location.pathname);
const handler = useCallback(
async (data) => {
@ -144,7 +145,10 @@ const Header = () => {
// return () => eventBus.off("project", newProjectHandler);
// }, [handler]);
useDispatch( () =>
{
dispatch(changeMaster("Job Role"))
},[])
useEffect(() => {
eventBus.on("assign_project_one", handler);
eventBus.on("project", newProjectHandler);
@ -158,7 +162,7 @@ const Header = () => {
return (
<nav
className="layout-navbar container-xxl navbar navbar-expand-xl navbar-detached align-items-center bg-navbar-theme"
className="layout-navbar container-fluid mb-3 navbar navbar-expand-xl navbar-detached align-items-center bg-navbar-theme"
id="layout-navbar"
>
<div className="layout-menu-toggle navbar-nav align-items-xl-center me-3 me-xl-0 d-xl-none">
@ -175,11 +179,11 @@ const Header = () => {
>
{projectNames?.length > 0 && (
<div className=" align-items-center">
{!isProjectPath && (
{(!isProjectPath && !isDirectoryPath) && (
<>
<i
className="rounded-circle bx bx-building-house"
style={{ fontSize: "xx-large" }}
className="rounded-circle bx bx-building-house bx-sm-lg bx-md"
></i>
<div className="btn-group">
<button
@ -199,7 +203,7 @@ const Header = () => {
style={{ overflow: "auto", maxHeight: "300px" }}
>
{[...projectNames]
.sort((a, b) => a.name.localeCompare(b.name))
.sort((a, b) => a?.name?.localeCompare(b.name))
.map((project) => (
<li key={project?.id}>
<button
@ -210,7 +214,7 @@ const Header = () => {
>
{project?.name}{" "}
{project?.shortName ? (
<span className="text-primary fw-semibold">
<span className="text-primary fw-semibold ">
{" "}
({project?.shortName})
</span>
@ -270,7 +274,7 @@ const Header = () => {
</div>
<div className="dropdown-shortcuts-item col">
<a
onClick={() => navigate(`/projectNames`)}
onClick={() => navigate(`/projects`)}
className="text-heading text-truncate cursor-pointer"
>
<span className="dropdown-shortcuts-icon rounded-circle mb-3">

View File

@ -1,16 +1,14 @@
import React, { useEffect, useState } from "react";
import moment from "moment";
import { getProjectStatusName } from "../../utils/projectStatus";
const AboutProject = ({ data }) => {
const [CurrentProject, setCurrentProject] = useState(data);
useEffect(() => {
setCurrentProject(data);
}, [data]);
import {useProjectDetails} from "../../hooks/useProjects";
import {useParams} from "react-router-dom";
const AboutProject = () => {
const {projectId} = useParams();
const {projects_Details,isLoading,error} = useProjectDetails(projectId)
return (
<>
{data && (
{projects_Details && (
<div className="card mb-6">
<div className="card-body">
<small className="card-text text-uppercase text-muted small">
@ -21,8 +19,8 @@ const AboutProject = ({ data }) => {
<i className="bx bx-check"></i>
<span className="fw-medium mx-2">Start Date:</span>{" "}
<span>
{data.startDate
? moment(data.startDate).format("DD-MMM-YYYY")
{projects_Details.startDate
? moment(projects_Details.startDate).format("DD-MMM-YYYY")
: "N/A"}
</span>
</li>
@ -30,31 +28,32 @@ const AboutProject = ({ data }) => {
<i className="bx bx-stop-circle"></i>{" "}
<span className="fw-medium mx-2">End Date:</span>{" "}
<span>
{data.endDate
? moment(data.endDate).format("DD-MMM-YYYY")
{projects_Details.endDate
? moment(projects_Details.endDate).format("DD-MMM-YYYY")
: "N/A"}
</span>
</li>
<li className="d-flex align-items-center mb-2">
<i className="bx bx-trophy"></i>
<span className="fw-medium mx-2">Status:</span>{" "}
<span>{getProjectStatusName(data.projectStatusId)}</span>
<span>{projects_Details?.projectStatus?.status
}</span>
</li>
<li className="d-flex align-items-center mb-4">
<i className="bx bx-user"></i>
<span className="fw-medium mx-2">Contact:</span>{" "}
<span>{data.contactPerson}</span>
<span>{projects_Details.contactPerson}</span>
</li>
<li className="d-flex flex-column align-items-start mb-4">
<div className="d-flex align-items-center">
<i className="bx bx-flag"></i>
<span className="fw-medium mx-2">Address:</span>
{data.projectAddress?.length <= 20 && (
<span>{data.projectAddress}</span>
{projects_Details.projectAddress?.length <= 20 && (
<span>{projects_Details.projectAddress}</span>
)}
</div>
{data.projectAddress?.length > 20 && (
<div className="ms-4 text-start">{data.projectAddress}</div>
{projects_Details.projectAddress?.length > 20 && (
<div className="ms-4 text-start">{projects_Details.projectAddress}</div>
)}
</li>
</ul>
@ -62,7 +61,7 @@ const AboutProject = ({ data }) => {
</div>
)}
{!data && <span>loading...</span>}
{isLoading && <span>loading...</span>}
</>
);
};

View File

@ -15,10 +15,8 @@ import { useCreateTask } from "../../hooks/useTasks";
const AssignTask = ({ assignData, onClose, setAssigned }) => {
const maxPlanned =
assignData?.workItem?.workItem?.plannedWork -
assignData?.workItem?.workItem?.completedWork;
// Zod schema for form validation
assignData?.workItem?.plannedWork -
assignData?.workItem?.completedWork;
const schema = z.object({
selectedEmployees: z
.array(z.string())
@ -51,9 +49,7 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
const infoRef = useRef(null);
const infoRef1 = useRef(null);
// Initialize Bootstrap Popovers on component mount
useEffect(() => {
// Check if Bootstrap is available globally
if (typeof bootstrap !== "undefined") {
if (infoRef.current) {
new bootstrap.Popover(infoRef.current, {
@ -75,8 +71,7 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
} else {
console.warn("Bootstrap is not available. Popovers might not function.");
}
}, []); // Empty dependency array ensures this runs once on mount
// Redux state and hooks
}, []);
const selectedProject = useSelector(
(store) => store.localVariables.projectId
);
@ -86,14 +81,11 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
recallEmployeeData,
} = useEmployeesAllOrByProjectId(selectedProject, false);
const dispatch = useDispatch();
const { loading } = useMaster(); // Assuming this is for jobRoleData loading
const jobRoleData = getCachedData("Job Role");
const { loading } = useMaster();
const {data:jobRoleData} = useMaster();
// Local component states
const [selectedRole, setSelectedRole] = useState("all");
const [displayedSelection, setDisplayedSelection] = useState(""); // This state is not updated in the provided code, consider if it's still needed or how it should be updated
// React Hook Form setup
const [displayedSelection, setDisplayedSelection] = useState("");
const {
handleSubmit,
control,
@ -101,50 +93,43 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
watch,
formState: { errors },
reset,
trigger, // <--- IMPORTANT: Destructure 'trigger' here
trigger,
} = useForm({
defaultValues: {
selectedEmployees: [],
description: "",
plannedTask: "",
},
resolver: zodResolver(schema), // Integrate Zod schema with react-hook-form
resolver: zodResolver(schema),
});
// Handler for employee checkbox changes
const handleCheckboxChange = (event, user) => {
const isChecked = event.target.checked;
let updatedSelectedEmployees = watch("selectedEmployees") || []; // Get current selected employees from form state
let updatedSelectedEmployees = watch("selectedEmployees") || [];
if (isChecked) {
// Add employee if checked and not already in the list
if (!updatedSelectedEmployees.includes(user.id)) {
updatedSelectedEmployees = [...updatedSelectedEmployees, user.id];
}
} else {
// Remove employee if unchecked
updatedSelectedEmployees = updatedSelectedEmployees.filter(
updatedSelectedEmployees = updatedSelectedEmployees?.filter(
(id) => id !== user.id
);
}
// Update the form state with the new list of selected employees
setValue("selectedEmployees", updatedSelectedEmployees);
trigger("selectedEmployees"); // <--- IMPORTANT: Trigger validation here
trigger("selectedEmployees");
};
// Effect to dispatch action for Job Role master data
useEffect(() => {
dispatch(changeMaster("Job Role"));
// Cleanup function to reset selected role when component unmounts or dispatch changes
return () => setSelectedRole("all");
}, [dispatch]);
// Handler for role filter change
const handleRoleChange = (event) => {
setSelectedRole(event.target.value);
};
// Filter employees based on selected role
const filteredEmployees =
selectedRole === "all"
? employees
@ -152,36 +137,6 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
(emp) => String(emp.jobRoleId || "") === selectedRole
);
// Form submission handler
// const onSubmit = async (data) => {
// const selectedEmployeeIds = data.selectedEmployees;
// setIsSubmitting(true);
// // Prepare taskTeam data (only IDs are needed for the backend based on previous context)
// const taskTeamWithDetails = selectedEmployeeIds
// .map((empId) => {
// return empId; // Return just the ID as per previous discussions
// })
// .filter(Boolean); // Ensure no nulls if employee not found (though unlikely with current logic)
// // Format data for API call
// const formattedData = {
// taskTeam: taskTeamWithDetails,
// plannedTask: data.plannedTask,
// description: data.description,
// assignmentDate: new Date().toISOString(), // Current date/time
// workItemId: assignData?.workItem?.workItem.id,
// };
// try {
// await TasksRepository.assignTask(formattedData);
// setIsSubmitting( false );
// showToast("Task Assined Successfully.", "success");
// closedModel();
// } catch (error) {
// setIsSubmitting(false);
// showToast("Something went wrong. Please try again.", "error");
// }
// };
const onSubmit = (data) => {
const selectedEmployeeIds = data.selectedEmployees;
@ -192,13 +147,12 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
plannedTask: data.plannedTask,
description: data.description,
assignmentDate: new Date().toISOString(),
workItemId: assignData?.workItem?.workItem.id,
workItemId: assignData?.workItem.id,
};
assignTask(formattedData);
};
// Handler to close the modal and reset form
const closedModel = () => {
reset();
onClose();
@ -212,10 +166,10 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
<span className="text-dark text-start d-flex align-items-center flex-wrap form-text">
<span className="me-2 m-0 font-bold">Work Location :</span>
{[
assignData?.building?.name,
assignData?.building?.buildingName,
assignData?.floor?.floorName,
assignData?.workArea?.areaName,
assignData?.workItem?.workItem?.activityMaster?.activityName,
assignData?.workItem?.activityMaster?.activityName,
]
.filter(Boolean) // Filter out any undefined/null values
.map((item, index, array) => (
@ -378,7 +332,7 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
"selectedEmployees",
updatedSelected
);
trigger("selectedEmployees"); // <--- IMPORTANT: Trigger validation on removing badge
trigger("selectedEmployees");
}}
>
<i className="icon-base bx bx-x icon-md "></i>
@ -395,11 +349,10 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
{!loading && errors.selectedEmployees && (
<div className="danger-text mt-1">
<p>{errors.selectedEmployees.message}</p>{" "}
{/* Use message from Zod schema */}
</div>
)}
{/* Pending Task of Activity section */}
<div className="col-md text-start mx-0 px-0">
<div className="form-check form-check-inline mt-3 px-1">
<label
@ -412,12 +365,12 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
htmlFor="inlineCheckbox1"
>
<strong>
{assignData?.workItem?.workItem?.plannedWork -
assignData?.workItem?.workItem?.completedWork}
{assignData?.workItem?.plannedWork -
assignData?.workItem?.completedWork}
</strong>{" "}
<u>
{
assignData?.workItem?.workItem?.activityMaster
assignData?.workItem?.activityMaster
?.unitOfMeasurement
}
</u>

View File

@ -4,7 +4,8 @@ const Building = ({
toggleBuilding,
expandedBuildings,
getContent,
}) => {
} ) =>
{
return (
<React.Fragment key={building.id}>
<tr className="overflow-auto">
@ -20,7 +21,7 @@ const Building = ({
>
<div className="row table-responsive">
<h6 style={{ marginBottom: "0px", fontSize: "14px" }}>
{building.name} &nbsp;
{building.buildingName} &nbsp;
{expandedBuildings.includes(building.id) ? (
<i className="bx bx-chevron-down"></i>
) : (

View File

@ -1,14 +1,13 @@
import React, { useState, useEffect } from "react";
import React, { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import ProjectRepository from "../../../repositories/ProjectRepository";
import { useSelector } from "react-redux";
import { useProjectDetails } from "../../../hooks/useProjects";
import { getCachedData } from "../../../slices/apiDataManager";
import showToast from "../../../services/toastService";
import { useManageProjectInfra } from "../../../hooks/useProjects";
import useSelect from "../../common/useSelect";
// Zod validation schema
const buildingSchema = z.object({
Id: z.string().optional(),
name: z.string().min(1, "Building name is required"),
@ -18,194 +17,165 @@ const buildingSchema = z.object({
.max(160, "Description cannot exceed 160 characters"),
});
const BuildingModel = ({
project,
onClose,
onSubmit,
clearTrigger,
onClearComplete,
editingBuilding = null,
}) => {
const BuildingModel = ({ project, onClose, editingBuilding = null }) => {
const selectedProject = useSelector(
(store) => store.localVariables.projectId
);
const [buildings, setBuildings] = useState([]);
const projects_Details = getCachedData("projectInfo");
const [formData, setFormData] = useState({
id: "",
name: "",
description: "",
projectId: project?.id,
});
useEffect(() => {
if (clearTrigger) {
setFormData({ id: null, name: "", description: "", projectId: project.id });
onClearComplete();
} else if (editingBuilding) {
setFormData({ ...editingBuilding, projectId: project.id });
}
return () => {
setValue("name", "");
};
}, [clearTrigger, onClearComplete, editingBuilding, project?.id]);
const {
register,
handleSubmit,
formState: { errors },
setValue,
watch,
reset,
getValues,
} = useForm({
resolver: zodResolver(buildingSchema),
defaultValues: formData, // Set default values from formData state
});
const handleBuildingChange = (e) => {
const selectedBuilding = project.buildings.find(
(b) => b.id === +e.target.value
);
if (selectedBuilding) {
setFormData({ ...selectedBuilding, projectId: project.id });
setValue("name", selectedBuilding.name); // Update name field
setValue("description", selectedBuilding.description); // Update description field
} else {
setFormData({ id: null, name: "", description: "", projectId: project.id });
setValue("name", "");
setValue("description", "");
}
};
const onSubmitHandler = async (data) => {
if (String(data.Id) === "0") {
data.Id = null;
}
onSubmit({ ...data, projectId: project.id });
reset({
defaultValues: {
Id: "0",
name: "",
description: "",
});
if (data.Id !== null) {
showToast("Building updated successfully.", "success");
} else {
showToast("Building created successfully.", "success");
}
};
},
});
const watchedId = watch("Id");
const { mutate: ManageBuilding, isPending } = useManageProjectInfra({
onSuccessCallback: (data, variables) => {
showToast(
watchedId != "0"
? "Building updated Successfully"
: "Building created Successfully",
"success"
);
reset({ Id: "0", name: "", description: "" });
// onClose?.();
},
});
const sortedBuildings = useMemo(() => {
return (project || [])
.filter((b) => b?.buildingName)
.sort((a, b) => a.buildingName.localeCompare(b?.buildingName));
}, [project]);
useEffect(() => {
if(projects_Details){
setBuildings(projects_Details.data?.buildings);
if (!watchedId || watchedId === "0") {
setValue("name", "");
setValue("description", "");
} else {
const selected = sortedBuildings.find((b) => String(b.id) === watchedId);
if (selected) {
setValue("name", selected.buildingName || "");
setValue("description", selected.description || "");
}
}
}, [projects_Details]);
}, [watchedId, sortedBuildings, setValue]);
useEffect(() => {
if (editingBuilding) {
reset({
Id: String(editingBuilding.id),
name: editingBuilding.name,
description: editingBuilding.description,
});
}
}, [editingBuilding]);
const onSubmitHandler = (data) => {
const payload = {
...data,
Id: data.Id === "0" ? null : data.Id,
projectId: selectedProject,
};
let infraObject = [
{
building: payload,
floor: null,
workArea: null,
},
];
ManageBuilding({ infraObject, projectId: selectedProject });
};
return (
<div className="modal-dialog modal-lg modal-simple modal-edit-user">
<div className="modal-content">
<div className="modal-body">
<button
type="button"
className="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
onClick={() => {
onClose();
reset(); // Call reset here
}}
></button>
<h5 className="text-center mb-2">
Manage Buildings - {project?.name}
</h5>
<form onSubmit={handleSubmit(onSubmitHandler)} className="row g-2">
<div className="col-12">
<label className="form-label">Select Building</label>
<select
{...register("Id")}
className="select2 form-select form-select-sm"
onChange={(e) => {
handleBuildingChange(e);
}}
>
<option value="0">Add New Building</option>
{project?.buildings?.length > 0 ? (
project.buildings
.filter((building) => building?.name)
.sort((a, b) => {
const nameA = a.name || "";
const nameB = b.name || "";
return nameA?.localeCompare(nameB);
})
.map((building) => (
<option key={building.id} value={building.id}>
{building.name}
</option>
))
) : (
<option disabled>No buildings found</option>
)}
</select>
{errors.Id && (
<span className="danger-text">{errors.Id.message}</span>
)}
</div>
<div className="col-12">
<label className="form-label">
{formData.id ? "Rename Building Name" : "New Building Name"}
</label>
<input
{...register("name")}
type="text"
className="form-control form-control-sm"
/>
{errors.name && (
<span className="danger-text">{errors.name.message}</span>
)}
</div>
<div className="col-12">
<label className="form-label">Description</label>
<textarea
{...register("description")}
maxLength="160"
rows="5"
className="form-control form-control-sm"
/>
{errors.description && (
<span className="danger-text">
{errors.description.message}
</span>
)}
</div>
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{formData.id && getValues("name")
? "Edit Building"
: "Add Building"}
</button>
<button
type="reset"
className="btn btn-sm btn-label-secondary"
data-bs-dismiss="modal"
aria-label="Close"
onClick={() => {
onClose();
reset(); // Call reset here
}}
>
Cancel
</button>
</div>
</form>
</div>
<form onSubmit={handleSubmit(onSubmitHandler)} className="row g-2">
<h5 className="text-center mb-2">Manage Buildings </h5>
<div className="col-12">
<label className="form-label">Select Building</label>
<select
{...register("Id")}
className="select2 form-select form-select-sm"
>
<option value="0">Add New Building</option>
{sortedBuildings.length > 0 ? (
sortedBuildings.map((b) => (
<option key={b.id} value={b.id}>
{b.buildingName}
</option>
))
) : (
<option disabled>No buildings found</option>
)}
</select>
{errors.Id && <span className="danger-text">{errors.Id.message}</span>}
</div>
</div>
{/* Name */}
<div className="col-12">
<label className="form-label">
{watchedId !== "0" ? "Rename Building Name" : "New Building Name"}
</label>
<input
{...register("name")}
type="text"
className="form-control form-control-sm"
/>
{errors.name && (
<span className="danger-text">{errors.name.message}</span>
)}
</div>
{/* Description */}
<div className="col-12">
<label className="form-label">Description</label>
<textarea
{...register("description")}
rows="5"
maxLength="160"
className="form-control form-control-sm"
/>
{errors.description && (
<span className="danger-text">{errors.description.message}</span>
)}
</div>
<div className="col-12 text-center">
<button
type="submit"
className="btn btn-sm btn-primary me-3"
disabled={isPending}
>
{isPending
? "Please wait..."
: watchedId !== "0"
? "Edit Building"
: "Add Building"}
</button>
<button
type="reset"
className="btn btn-sm btn-label-secondary"
disabled={isPending}
onClick={() => {
onClose();
reset();
}}
>
Cancel
</button>
</div>
</form>
);
};
export default BuildingModel;

View File

@ -1,17 +1,15 @@
import React, { useState, useEffect } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useActivitiesMaster, useWorkCategoriesMaster } from "../../../hooks/masterHook/useMaster";
import { useProjectDetails } from "../../../hooks/useProjects";
import { useDispatch, useSelector } from "react-redux";
import ProjectRepository from "../../../repositories/ProjectRepository";
import {
cacheData,
clearCacheKey,
getCachedData,
} from "../../../slices/apiDataManager";
useActivitiesMaster,
useWorkCategoriesMaster,
} from "../../../hooks/masterHook/useMaster";
import { useManageTask } from "../../../hooks/useProjects";
import { cacheData, getCachedData } from "../../../slices/apiDataManager";
import { refreshData } from "../../../slices/localVariablesSlice";
import showToast from "../../../services/toastService";
@ -20,18 +18,13 @@ const taskSchema = z
activityID: z.string().min(1, "Activity is required"),
workCategoryId: z.string().min(1, "Work Category is required"),
plannedWork: z.number().min(1, "Planned Work must be greater than 0"),
completedWork: z.number().min( 0, "Completed Work must be greater than 0" ),
comment:z.string()
completedWork: z.number().min(0, "Completed Work must be ≥ 0"),
comment: z.string(),
})
.refine(
(data) =>
data.completedWork === undefined ||
data.completedWork <= data.plannedWork,
{
message: "Completed Work cannot be greater than Planned Work",
path: ["completedWork"], // error will show next to this field
}
);
.refine((data) => data.completedWork <= data.plannedWork, {
message: "Completed Work cannot be greater than Planned Work",
path: ["completedWork"],
});
const EditActivityModal = ({
workItem,
@ -39,29 +32,12 @@ const EditActivityModal = ({
building,
floor,
onClose,
}) => {
const selectedProject = useSelector(
(store) => store.localVariables.projectId
);
const defaultModel = {
activityID: 0,
workCategoryId: 0,
plannedWork: 0,
completedWork: 0,
comment:""
};
const { projects_Details, refetch } = useProjectDetails(selectedProject);
const [ActivityUnit, setActivityUnit] = useState("");
const { activities, loading, error } = useActivitiesMaster();
const { categories, categoryLoading, categoryError } =
useWorkCategoriesMaster();
const [formData, setFormData] = useState(defaultModel);
} ) =>
{
const { activities, loading: loadingActivities } = useActivitiesMaster();
const { categories, loading: loadingCategories } = useWorkCategoriesMaster();
const [selectedActivity, setSelectedActivity] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [activityData, setActivityData] = useState([]);
const [categoryData, setCategoryData] = useState([]);
const dispatch = useDispatch();
const {
register,
@ -73,359 +49,207 @@ const EditActivityModal = ({
watch,
} = useForm({
resolver: zodResolver(taskSchema),
defaultValues: defaultModel,
defaultValues: {
activityID: "",
workCategoryId: "",
plannedWork: 0,
completedWork: 0,
comment: "",
},
});
const { mutate: UpdateTask, isPending } = useManageTask({
onSuccessCallback: () => onClose?.()
});
const handleActivityChange = (e) => {
const selectedId = String(e.target.value);
const selected = activityData.find((a) => a.id === selectedId);
const activityID = watch("activityID");
const sortedActivities = useMemo(
() =>
[...(activities || [])].sort((a, b) =>
a.activityName?.localeCompare(b.activityName)
),
[activities]
);
const sortedCategories = useMemo(
() => [...(categories || [])].sort((a, b) => a.name?.localeCompare(b.name)),
[categories]
);
useEffect(() => {
if (!workItem) return;
reset({
activityID: String(
workItem?.workItem?.activityId || workItem?.activityId || ""
),
workCategoryId: String(
workItem?.workItem?.workCategoryId || workItem?.workCategoryId || ""
),
plannedWork:
workItem?.workItem?.plannedWork || workItem?.plannedWork || 0,
completedWork:
workItem?.workItem?.completedWork || workItem?.completedWork || 0,
comment: workItem?.workItem?.description || workItem?.description || "",
});
}, [workItem?.id]);
useEffect(() => {
const selected = activities?.find((a) => a.id === activityID);
setSelectedActivity(selected || null);
setValue("activityID", selectedId);
};
}, [activityID, activities]);
const handleCategoryChange = (e) => {
const selectedId = String(e.target.value);
const category = categoryData.find((b) => b.id === selectedId);
setSelectedCategory(category || null);
setValue("workCategoryId", selectedId);
};
const onSubmitForm = async ( data ) =>
const onSubmitForm = (data) =>
{
setIsSubmitting(true)
const updatedProject = { ...projects_Details };
const finalData = {
const payload = {
...data,
id: workItem?.workItem?.id ?? workItem?.id,
buildingID: building?.id,
floorId: floor?.id,
workAreaId: workArea?.id,
};
ProjectRepository.manageProjectTasks([finalData])
.then((response) => {
if (response?.data[0]) {
const { workItemId, workItem } = response.data[0];
let finalUpdatedWorkItem = null;
const newProject = {
...updatedProject,
buildings: updatedProject.buildings.map((building) =>
building.id === finalData.buildingID
? {
...building,
floors: building.floors.map((floor) =>
floor.id === finalData.floorId
? {
...floor,
workAreas: floor.workAreas.map((workArea) =>
workArea.id === workItem?.workAreaId
? {
...workArea,
workItems: (() => {
const exists = workArea.workItems.some(
(item) =>
String(
item?.workItem?.id ?? item?.id
) === String(finalData.id)
);
finalUpdatedWorkItem = workItem;
return exists
? workArea.workItems.map((item) =>
String(
item?.workItem?.id ?? item?.id
) === String(finalData.id)
? workItem
: item
)
: [...workArea.workItems, workItem];
})(),
}
: workArea
),
}
: floor
),
}
: building
),
};
cacheData("projectInfo", {
projectId: newProject.id,
data: newProject,
});
resetForm();
dispatch( refreshData( true ) );
setIsSubmitting(false)
showToast("Activity Updated Successfully","success")
onClose();
}
})
.catch( ( error ) =>
{
setIsSubmitting(false)
const message = error.response.data.message || error.message || "Error Occured During Api Call"
showToast( message, "error" );
});
};
const resetForm = () => {
setFormData(defaultModel);
setSelectedActivity(null);
reset(defaultModel);
};
useEffect(() => {
reset({
activityID: workItem?.workItem?.activityId || workItem?.activityId || 0,
workCategoryId:
workItem?.workItem?.workCategoryId || workItem?.workCategoryId || 0,
plannedWork:
workItem?.workItem?.plannedWork || workItem?.plannedWork || 0,
completedWork:
workItem?.workItem?.completedWork || workItem?.completedWork || 0,
comment:
workItem?.workItem?.description || workItem?.description || ""
});
return () => reset();
}, [activities, workItem]);
const ISselectedActivity = watch("activityID");
useEffect(() => {
if (ISselectedActivity) {
const selected = activities.find((a) => a.id === ISselectedActivity);
setSelectedActivity(selected || null);
setActivityUnit(selected?.unitOfMeasurement);
}
}, [ISselectedActivity, activities]);
UpdateTask([payload])
}
return (
<div className="modal-dialog modal-lg modal-simple modal-edit-user">
<div className="modal-content">
<div className="modal-body">
<div className="row">
<button
type="button"
className="btn-close"
aria-label="Close"
onClick={onClose}
/>
<div className="text-center mb-1">
<h5 className="mb-1">Manage Task</h5>
</div>
<form className="row g-2" onSubmit={handleSubmit(onSubmitForm)}>
{/* Select Building */}
<div className="col-6 col-md-6">
<label className="form-label" htmlFor="buildingID">
Select Building
</label>
<input
type="text"
className="form-control form-control-sm"
value={building?.name}
disabled
/>
</div>
<form className="row g-2 p-2 p-md-1" onSubmit={handleSubmit(onSubmitForm)}>
<div className="text-center mb-1">
<h5 className="mb-1">Manage Task</h5>
</div>
{/* Select Floor */}
<div className="col-6 col-md-6">
<label className="form-label" htmlFor="floorId">
Select Floor
</label>
<div className="row g-2">
<div className="col-12 col-md-6">
<label className="form-label">Select Building</label>
<input
className="form-control form-control-sm"
value={building?.buildingName}
disabled
/>
</div>
<input
type="text"
className="form-control form-control-sm"
value={floor?.floorName}
disabled
/>
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="workAreaId">
Select Work Area
</label>
<input
type="text"
className="form-control form-control-sm"
value={workArea?.areaName}
disabled
/>
</div>
{/* Select Activity */}
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="activityID">
Select Activity
</label>
<select
id="activityID"
className="form-select form-select-sm"
{...register("activityID")}
>
{loading ? (
<option value="">Loading...</option>
) : (
<option disabled>Select Activity</option>
)}
{activities &&
activities.length > 0 &&
activities
.slice()
.sort((a, b) =>
(a.activityName || "")?.localeCompare(
b.activityName || ""
)
)
.map((activity) => (
<option key={activity.id} value={activity.id}>
{activity.activityName}
</option>
))}
{!loading && activities.length === 0 && (
<option disabled>No activities available</option>
)}
</select>
{errors.activityID && (
<p className="danger-text">{errors.activityID.message}</p>
)}
</div>
{/* Select Category */}
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="activityID">
Select Work Category
</label>
<select
id="workCategoryId"
className="form-select form-select-sm"
{...register("workCategoryId")}
>
{loading ? (
<option value="">Loading...</option>
) : (
<option disabled>Select Category</option>
)}
{categories &&
categories.length > 0 &&
categories
.slice()
.sort((a, b) =>
(a.name || "")?.localeCompare(
b.name || ""
)
)
.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
{!loading && categories.length === 0 && (
<option disabled>No categories available</option>
)}
</select>
{errors.workCategoryId && (
<p className="danger-text">{errors.workCategoryId.message}</p>
)}
</div>
{/* Planned Work */}
{/* {ISselectedActivity && ( */}
<div className="col-5 col-md-5">
<label className="form-label" htmlFor="plannedWork">
Planned Work
</label>
<input
{...register("plannedWork", { valueAsNumber: true })}
type="number"
className="form-control form-control-sm me-2"
placeholder="Planned Work"
/>
{errors.plannedWork && (
<p className="danger-text">{errors.plannedWork.message}</p>
)}
</div>
{/* )} */}
{/* Completed Work */}
{/* {ISselectedActivity && ( */}
<div className="col-5 col-md-5">
<label className="form-label" htmlFor="completedWork">
Completed Work
</label>
<input
{...register("completedWork", { valueAsNumber: true })}
type="number"
className="form-control form-control-sm me-2"
placeholder="Completed Work"
disabled={getValues("completedWork") > 0}
/>
{errors.completedWork && (
<p className="danger-text">{errors.completedWork.message}</p>
)}
</div>
{/* )} */}
{/* Unit */}
{/* {ISselectedActivity && ( */}
<div className="col-2 col-md-2">
<label className="form-label" htmlFor="unit">
Unit
</label>
<input
type="text"
disabled
className="form-control form-control-sm me-2"
value={selectedActivity?.unitOfMeasurement || ""}
/>
</div>
{/* )} */}
<div className="col-12">
<label
className="form-text fs-7 m-1 text-lg text-dark"
htmlFor="descriptionTextarea"
>
Comment
</label>
<textarea
{...register("comment")}
className="form-control"
id="descriptionTextarea"
rows="2"
/>
{errors.comment && (
<div className="danger-text">
{errors.comment.message}
</div>
)}
</div>
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3" disabled={activities.length === 0 || isSubmitting}>
{isSubmitting ? "Please Wait.." : "Edit Task"}
</button>
<button
type="button"
className="btn btn-sm btn-label-secondary"
onClick={onClose}
>
Cancel
</button>
</div>
</form>
</div>
<div className="col-12 col-md-6">
<label className="form-label">Select Floor</label>
<input
className="form-control form-control-sm"
value={floor?.floorName}
disabled
/>
</div>
</div>
</div>
<div className="col-12">
<label className="form-label">Select Work Area</label>
<input
className="form-control form-control-sm"
value={workArea?.areaName}
disabled
/>
</div>
<div className="col-12">
<label className="form-label">Select Activity</label>
<select
{...register("activityID")}
className="form-select form-select-sm"
>
<option disabled>Select Activity</option>
{loadingActivities ? (
<option>Loading...</option>
) : (
sortedActivities.map((a) => (
<option key={a.id} value={a.id}>
{a.activityName}
</option>
))
)}
</select>
{errors.activityID && (
<p className="danger-text">{errors.activityID.message}</p>
)}
</div>
<div className="col-12">
<label className="form-label">Select Work Category</label>
<select
{...register("workCategoryId")}
className="form-select form-select-sm"
>
<option disabled>Select Category</option>
{loadingCategories ? (
<option>Loading...</option>
) : (
sortedCategories.map((c) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))
)}
</select>
{errors.workCategoryId && (
<p className="danger-text">{errors.workCategoryId.message}</p>
)}
</div>
<div className="col-5">
<label className="form-label">Planned Work</label>
<input
{...register("plannedWork", { valueAsNumber: true })}
type="number"
className="form-control form-control-sm"
/>
{errors.plannedWork && (
<p className="danger-text">{errors.plannedWork.message}</p>
)}
</div>
<div className="col-5">
<label className="form-label">Completed Work</label>
<input
{...register("completedWork", { valueAsNumber: true })}
type="number"
disabled={getValues("completedWork") > 0}
className="form-control form-control-sm"
/>
{errors.completedWork && (
<p className="danger-text">{errors.completedWork.message}</p>
)}
</div>
<div className="col-2">
<label className="form-label">Unit</label>
<input
className="form-control form-control-sm"
disabled
value={selectedActivity?.unitOfMeasurement || ""}
/>
</div>
<div className="col-12">
<label className="form-label">Comment</label>
<textarea {...register("comment")} rows="2" className="form-control" />
{errors.comment && (
<div className="danger-text">{errors.comment.message}</div>
)}
</div>
<div className="col-12 text-center">
<button
type="submit"
className="btn btn-sm btn-primary me-2"
disabled={isPending}
>
{isPending ? "Please Wait..." : "Edit Task"}
</button>
<button
type="button"
className="btn btn-sm btn-label-secondary"
onClick={onClose}
disabled={isPending}
>
Cancel
</button>
</div>
</form>
);
};

View File

@ -16,13 +16,7 @@ const Floor = ({ floor, workAreas, forBuilding }) => {
<tr>
<td colSpan="4" className="text-start table-cell">
<div className="row ps-2">
{/* <div className="col-1 col-md-1 d-flex justify-content-between align-items-center " >
<button
className="btn me-2"
>
</button>
</div> */}
<div className="col-12 ps-8">
<div className="row">
<div className="d-flex col-5">

View File

@ -1,236 +1,189 @@
import React, { useState, useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import showToast from "../../../services/toastService";
import { useManageProjectInfra } from "../../../hooks/useProjects";
import { useSelector } from "react-redux";
// Schema
const floorSchema = z.object({
buildingId: z
.string()
.refine((val) => val !== "0", {
message: "Building is required",
}),
.refine((val) => val !== "0", { message: "Building is required" }),
id: z.string().optional(),
floorName: z.string().min(1, "Floor Name is required"),
});
const defaultModel = {
const defaultValues = {
id: "0",
floorName: "",
buildingId: "0",
};
const FloorModel = ({
project,
onClose,
onSubmit,
clearTrigger,
onClearComplete,
}) => {
const [formData, setFormData] = useState(defaultModel);
const [selectedBuilding, setSelectedBuilding] = useState({});
const [buildings, setBuildings] = useState(project?.buildings || []);
const FloorModel = ({ project, onClose, onSubmit }) => {
const selectedProject = useSelector(
(store) => store.localVariables.projectId
);
const [selectedBuilding, setSelectedBuilding] = useState(null);
const {
register,
handleSubmit,
setValue,
reset,
watch,
formState: { errors },
} = useForm({
defaultValues,
resolver: zodResolver(floorSchema),
defaultValues: defaultModel,
});
const watchId = watch("id");
const watchBuildingId = watch("buildingId");
const { mutate: ManageFloor, isPending } = useManageProjectInfra({
onSuccessCallback: (data, variables) => {
showToast(
watchId != "0"
? "Floor updated Successfully"
: "Floor created Successfully",
"success"
);
reset({ Id: "0", name: "", description: "" });
// onClose?.();
},
});
useEffect(() => {
if (clearTrigger) {
reset(defaultModel);
onClearComplete();
}
}, [clearTrigger, onClearComplete, reset]);
reset(defaultValues);
}, []);
useEffect(() => {
const building = project?.find((b) => b.id === watchBuildingId);
setSelectedBuilding(building || null);
}, [watchBuildingId, project]);
const handleBuildigChange = (e) => {
const buildingId = e.target.value;
const building = buildings.find((b) => b.id === String(buildingId));
if (building) {
setSelectedBuilding(building);
setFormData({
id: "",
floorName: "",
buildingId: building.id,
});
setValue("buildingId", building.id, { shouldValidate: true }); // trigger validation
setValue("id", "0");
} else {
setSelectedBuilding({});
setFormData({
id: "",
floorName: "",
buildingId: "0",
});
setValue("buildingId", "0", { shouldValidate: true }); // trigger validation
}
const handleBuildingChange = (e) => {
const id = e.target.value;
setValue("buildingId", id, { shouldValidate: true });
setValue("id", "0");
setValue("floorName", "");
};
const handleFloorChange = (e) => {
const id = e.target.value;
const floor = selectedBuilding.floors?.find((b) => b.id === String(id));
setValue("id", id);
const floor = selectedBuilding?.floors?.find((f) => f.id === id);
if (floor) {
setFormData({
id: floor.id,
floorName: floor.floorName,
buildingId: selectedBuilding.id,
});
setValue("floorName", floor.floorName);
} else {
setFormData({
id: "0",
floorName: "",
buildingId: selectedBuilding.id,
});
setValue("floorName", "");
}
};
const onFormSubmit = (data) => {
if (data.id === "0") {
data.id = null;
}
const isEdit = data.id !== "0";
const payload = {
...data,
id: isEdit ? data.id : null,
};
let infraObject = [
{
building: null,
floor: payload,
workArea: null,
},
];
onSubmit(data);
reset({ floorName: "" });
if (data.id !== null) {
showToast("Floor updated successfully.", "success");
} else {
showToast("Floor created successfully.", "success");
}
ManageFloor({ infraObject, projectId: selectedProject });
};
return (
<div className="modal-dialog modal-lg modal-simple modal-edit-user">
<div className="modal-content">
<div className="modal-body">
<div className="row">
<button
type="button"
className="btn-close"
aria-label="Close"
onClick={onClose}
/>
<div className="text-center mb-1">
<h5 className="mb-1">Manage Floors - {project.name}</h5>
</div>
<form className="row g-2" onSubmit={handleSubmit(onFormSubmit)}>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="buildingId">
Select Building
</label>
<select
id="buildingId"
className="select2 form-select form-select-sm"
aria-label="Select Building"
{...register("buildingId")}
onChange={handleBuildigChange}
>
<option value="0">Select Building</option>
{buildings?.length > 0 &&
buildings
.filter((building) => building?.name)
.sort((a, b) =>
(a.name || "")?.localeCompare(b.name || "")
)
.map((building) => (
<option key={building.id} value={building.id}>
{building.name}
</option>
))}
{buildings?.length === 0 && (
<option disabled>No buildings found</option>
)}
</select>
{errors.buildingId && (
<p className="text-danger">{errors.buildingId.message}</p>
)}
</div>
{formData.buildingId !== "0" && (
<>
<div className="col-12 col-md-12">
<label className="form-label">Select Floor</label>
<select
id="id"
className="select2 form-select form-select-sm"
aria-label="Select Floor"
{...register("id")}
onChange={handleFloorChange}
>
<option value="0">Add New Floor</option>
{selectedBuilding?.floors?.length > 0 &&
[...selectedBuilding.floors]
.filter((floor) => floor?.floorName)
.sort((a, b) =>
(a.floorName || "")?.localeCompare(
b.floorName || ""
)
)
.map((floor) => (
<option key={floor.id} value={floor.id}>
{floor.floorName}
</option>
))}
{selectedBuilding?.floors?.length === 0 && (
<option disabled>No floors found</option>
)}
</select>
{errors.id && (
<p className="text-danger">{errors.id.message}</p>
)}
</div>
<div className="col-12 col-md-12">
<label className="form-label">
{formData.id !== "0" ? "Modify " : "Enter "} Floor Name
</label>
<input
type="text"
id="floorName"
className="form-control form-control-sm me-2"
placeholder="Floor Name"
{...register("floorName")}
/>
{errors.floorName && (
<p className="text-danger">
{errors.floorName.message}
</p>
)}
</div>
</>
)}
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{formData.id !== "0" && formData.id !== ""
? "Edit Floor"
: "Add Floor"}
</button>
<button
type="button"
className="btn btn-sm btn-label-secondary"
data-bs-dismiss="modal"
aria-label="Close"
onClick={onClose}
>
Cancel
</button>
</div>
</form>
</div>
</div>
<form className="row g-2" onSubmit={handleSubmit(onFormSubmit)}>
<div className="text-center mb-1">
<h5 className="mb-1">Manage Floor</h5>
</div>
</div>
<div className="col-12">
<label className="form-label">Select Building</label>
<select
{...register("buildingId")}
className="form-select form-select-sm"
onChange={handleBuildingChange}
>
<option value="0">Select Building</option>
{project?.length > 0 &&
project
.filter((b) => b.buildingName)
.sort((a, b) => a.buildingName.localeCompare(b.buildingName))
.map((b) => (
<option key={b.id} value={b.id}>
{b.buildingName}
</option>
))}
</select>
{errors.buildingId && (
<p className="danger-text">{errors.buildingId.message}</p>
)}
</div>
{watchBuildingId !== "0" && (
<>
<div className="col-12">
<label className="form-label">Select Floor</label>
<select
{...register("id")}
className="form-select form-select-sm"
onChange={handleFloorChange}
>
<option value="0">Add New Floor</option>
{selectedBuilding?.floors?.length > 0 &&
selectedBuilding.floors
.filter((f) => f.floorName)
.sort((a, b) => a.floorName.localeCompare(b.floorName))
.map((f) => (
<option key={f.id} value={f.id}>
{f.floorName}
</option>
))}
</select>
</div>
<div className="col-12">
<label className="form-label">
{watchId !== "0" ? "Edit Floor Name" : "New Floor Name"}
</label>
<input
{...register("floorName")}
className="form-control form-control-sm"
placeholder="Floor Name"
/>
{errors.floorName && (
<p className="danger-text">{errors.floorName.message}</p>
)}
</div>
</>
)}
<div className="col-12 text-center">
<button
type="submit"
className="btn btn-sm btn-primary me-3"
disabled={isPending}
>
{isPending
? "Please Wait"
: watchId !== "0"
? "Edit Floor"
: "Add Floor"}
</button>
<button
type="button"
className="btn btn-sm btn-label-secondary"
disabled={isPending}
onClick={onClose}
>
Cancel
</button>
</div>
</form>
);
};

View File

@ -100,14 +100,7 @@ const InfraTable = ({ buildings, projectId, signalRHandler }) => {
No floors have been added yet. Start by adding floors to manage
this building.
</p>
{/* <button
type="button"
className="btn btn-xs btn-primary"
onClick={() => handleAddFloor(building)}
>
<i className="bx bx-plus-circle me-2"></i>
Add Floors
</button> */}
</div>
</td>
</tr>

View File

@ -1,11 +1,12 @@
import React, { useState, useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import {
useActivitiesMaster,
useWorkCategoriesMaster,
} from "../../../hooks/masterHook/useMaster";
import { useManageTask } from "../../../hooks/useProjects";
const taskSchema = z.object({
buildingID: z.string().min(1, "Building is required"),
@ -14,430 +15,262 @@ const taskSchema = z.object({
activityID: z.string().min(1, "Activity is required"),
workCategoryId: z.string().min(1, "Work Category is required"),
plannedWork: z.number().min(1, "Planned Work must be greater than 0"),
completedWork: z.number().min( 0, "Completed Work must be greater than 0" ),
comment:z.string(),
completedWork: z.number().min(0, "Completed Work must be 0 or more"),
comment: z.string(),
});
const defaultModel = {
id: null,
buildingID: "0",
floorId: "0",
workAreaId: "0",
activityID: null,
buildingID: "",
floorId: "",
workAreaId: "",
activityID: "",
workCategoryId: "",
plannedWork: 0,
completedWork: 0,
comment:""
comment: "",
};
const TaskModel = ({
project,
onSubmit,
clearTrigger,
onClearComplete,
onClose,
}) => {
const [formData, setFormData] = useState(defaultModel);
const [selectedBuilding, setSelectedBuilding] = useState(null);
const [selectedFloor, setSelectedFloor] = useState(null);
const [selectedWorkArea, setSelectedWorkArea] = useState(null);
const [selectedActivity, setSelectedActivity] = useState(null);
const [selectedCategory, setSelectedCategory] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [activityData, setActivityData] = useState([]);
const [categoryData, setCategoryData] = useState([]);
const { activities, loading, error } = useActivitiesMaster();
const { categories, categoryLoading, categoryError } =
useWorkCategoriesMaster();
const TaskModel = ({ project, onSubmit, onClose }) => {
const { activities, loading: activityLoading } = useActivitiesMaster();
const { categories, categoryLoading } = useWorkCategoriesMaster();
const {
register,
handleSubmit,
formState: { errors },
reset,
watch,
setValue,
reset,
formState: { errors },
} = useForm({
resolver: zodResolver(taskSchema),
defaultValues: defaultModel,
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [activityData, setActivityData] = useState([]);
const [categoryData, setCategoryData] = useState([]);
const watchBuildingId = watch("buildingID");
const watchFloorId = watch("floorId");
const watchWorkAreaId = watch("workAreaId");
const watchActivityId = watch("activityID");
const watchCategoryId = watch("workCategoryId");
const selectedBuilding = project?.find((b) => b.id === watchBuildingId);
const selectedFloor = selectedBuilding?.floors?.find(
(f) => f.id === watchFloorId
);
const selectedWorkArea = selectedFloor?.workAreas?.find(
(w) => w.id === watchWorkAreaId
);
const selectedActivity = activityData?.find((a) => a.id === watchActivityId);
const selectedCategory = categoryData?.find((c) => c.id === watchCategoryId);
const { mutate: CreateTask, isPending } = useManageTask({
onSuccessCallback: () => onClose?.(),
});
useEffect(() => {
resetForm();
reset(defaultModel);
}, []);
const handleBuildingChange = (e) => {
const { value } = e.target;
const building = project.buildings.find((b) => b.id === String(value));
setSelectedBuilding(building);
setSelectedFloor(null);
setSelectedWorkArea(null);
setSelectedActivity(null);
reset({
...defaultModel,
buildingID: value,
});
};
const handleFloorChange = (e) => {
const { value } = e.target;
const floor = selectedBuilding.floors.find((b) => b.id === String(value));
setSelectedFloor(floor);
setSelectedWorkArea(null);
setSelectedActivity(null);
reset((prev) => ({
...prev,
floorId: value,
workAreaId: 0,
activityID: "",
workCategoryId: categoryData?.[0]?.id?.toString() ?? "",
}));
};
const handleWorkAreaChange = (e) => {
const { value } = e.target;
const workArea = selectedFloor.workAreas.find(
(b) => b.id === String(value)
);
setSelectedWorkArea(workArea);
reset((prev) => ({
...prev,
workAreaId: String(value),
}));
};
const handleActivityChange = (e) => {
const { value } = e.target;
const activity = activityData.find((b) => b.id === String(value));
setSelectedActivity(activity);
reset((prev) => ({
...prev,
activityID: String(value),
}));
};
const handleCategoryChange = (e) => {
const { value } = e.target;
const category = categoryData.find((b) => b.id === String(value));
setSelectedCategory(category);
reset((prev) => ({
...prev,
workCategoryId: String(value),
}));
};
const onSubmitForm = async (data) => {
setIsSubmitting(true);
await onSubmit(data);
setValue("plannedWork", 0);
setValue("completedWork", 0);
setValue("activityID", 0);
setValue("workCategoryId", categoryData?.[0]?.id?.toString() ?? "");
setIsSubmitting(false);
};
const resetForm = () => {
setFormData(defaultModel);
setSelectedBuilding(null);
setSelectedFloor(null);
setSelectedWorkArea(null);
setSelectedActivity(null);
setSelectedCategory(categoryData?.[0]?.id?.toString() ?? "");
reset(defaultModel);
};
useEffect(() => {
if (!loading && Array.isArray(activities) && activities.length > 0) {
if (Array.isArray(activities) && activities.length > 0) {
setActivityData(activities);
}
}, [activities, loading]);
}, [activities]);
useEffect(() => {
if (
!categoryLoading &&
Array.isArray(categories) &&
categories.length > 0
) {
const newCategories = categories?.slice()?.sort((a, b) => {
const nameA = a?.name || "";
const nameB = b?.name || "";
return nameA?.localeCompare(nameB);
});
setCategoryData(newCategories);
setSelectedCategory(newCategories[0])
if (Array.isArray(categories) && categories.length > 0) {
const sorted = [...categories].sort((a, b) =>
(a?.name || "").localeCompare(b?.name || "")
);
setCategoryData(sorted);
setValue("workCategoryId", sorted?.[0]?.id?.toString() ?? "");
}
}, [categories, categoryLoading]);
}, [categories]);
const onSubmitForm = async (data) => {
const payload = [data];
CreateTask(payload);
};
return (
<div className="modal-dialog modal-lg modal-simple modal-edit-user">
<div className="modal-content">
<div className="modal-body">
<div className="row">
<button
type="button"
className="btn-close"
aria-label="Close"
onClick={onClose}
/>
<div className="text-center mb-1">
<h5 className="mb-1">Manage Task</h5>
</div>
<form className="row g-2" onSubmit={handleSubmit(onSubmitForm)}>
{/* Select Building */}
<div className="col-6 col-md-6">
<label className="form-label" htmlFor="buildingID">
Select Building
</label>
<select
id="buildingID"
className="form-select form-select-sm"
{...register("buildingID")}
onChange={handleBuildingChange}
>
<option value="0">Select Building</option>
{project.buildings
?.filter((building) => building?.name) // Ensure valid name
?.sort((a, b) => a.name?.localeCompare(b.name))
?.map((building) => (
<option key={building.id} value={building.id}>
{building.name}
</option>
))}
{project.buildings?.filter((building) => building?.name)
.length === 0 && (
<option disabled>No buildings found</option>
)}
</select>
{errors.buildingID && (
<p className="danger-text">{errors.buildingID.message}</p>
)}
</div>
{/* Select Floor */}
{selectedBuilding && (
<div className="col-6 col-md-6">
<label className="form-label" htmlFor="floorId">
Select Floor
</label>
<select
id="floorId"
className="form-select form-select-sm"
{...register("floorId")}
onChange={handleFloorChange}
>
<option value="0">Select Floor</option>
{selectedBuilding.floors
?.filter(
(floor) =>
floor?.floorName && Array.isArray(floor.workAreas)
)
?.sort((a, b) => a.floorName?.localeCompare(b.floorName))
?.map((floor) => (
<option key={floor.id} value={floor.id}>
{floor.floorName} - ({floor.workAreas.length} Work
Areas)
</option>
))}
{selectedBuilding.floors?.filter(
(floor) =>
floor?.floorName && Array.isArray(floor.workAreas)
).length === 0 && <option disabled>No floors found</option>}
</select>
{errors.floorId && (
<p className="danger-text">{errors.floorId.message}</p>
)}
</div>
)}
{selectedFloor && (
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="workAreaId">
Select Work Area
</label>
<select
id="workAreaId"
className="form-select form-select-sm"
{...register("workAreaId")}
onChange={handleWorkAreaChange}
>
<option value="0">Select Work Area</option>
{selectedFloor.workAreas
?.filter((workArea) => workArea?.areaName)
?.sort((a, b) => a.areaName?.localeCompare(b.areaName))
?.map((workArea) => (
<option key={workArea.id} value={workArea.id}>
{workArea.areaName}
</option>
))}
{selectedFloor.workAreas?.filter(
(workArea) => workArea?.areaName
).length === 0 && (
<option disabled>No work areas found</option>
)}
</select>
{errors.workAreaId && (
<p className="danger-text">{errors.workAreaId.message}</p>
)}
</div>
)}
{selectedWorkArea && (
<div className="col-12 col-md-12">
<label className="form-label">Select Activity</label>
<select
id="activityID"
className="form-select form-select-sm"
{...register("activityID")}
onChange={handleActivityChange}
>
<option value="">Select Activity</option>
{activityData &&
activityData.length > 0 &&
activityData
?.slice()
?.sort((a, b) => {
const nameA = a?.activityName || "";
const nameB = b?.activityName || "";
return nameA?.localeCompare(nameB);
})
?.map((activity) => (
<option key={activity.id} value={activity.id}>
{activity.activityName ||
`Unnamed (id: ${activity.id})`}
</option>
))}
{!loading && activities.length === 0 && (
<option disabled>No activities available</option>
)}
{loading && <option disabled>Loading...</option>}
</select>
{errors.activityID && (
<p className="danger-text">{errors.activityID.message}</p>
)}
</div>
)}
{selectedWorkArea && (
<div className="col-12 col-md-12">
<label className="form-label">Select Work Category</label>
<select
id="workCategoryId"
className="form-select form-select-sm"
{...register("workCategoryId")}
onChange={handleCategoryChange}
>
{categoryData &&
categoryData.length > 0 &&
categoryData
?.map((category) => (
<option key={category.id} value={category.id}>
{category.name || `Unnamed (id: ${category.id})`}
</option>
))}
{!categoryLoading && categories.length === 0 && (
<option disabled>No activities available</option>
)}
{categoryLoading && <option disabled>Loading...</option>}
</select>
{errors.workCategoryId && (
<p className="danger-text">
{errors.workCategoryId.message}
</p>
)}
</div>
)}
{selectedActivity && selectedCategory && (
<div className="col-5 col-md-5">
<label className="form-label" htmlFor="plannedWork">
{formData.id !== "0" ? "Modify " : "Enter "} Planned Work
</label>
<input
{...register("plannedWork", { valueAsNumber: true })}
type="number"
className="form-control form-control-sm me-2"
placeholder="Planned Work"
/>
{errors.plannedWork && (
<p className="danger-text">{errors.plannedWork.message}</p>
)}
</div>
)}
{selectedActivity && selectedCategory && (
<div className="col-5 col-md-5">
<label className="form-label" htmlFor="completedWork">
{formData.id !== "0" ? "Modify " : "Enter "} Completed Work
</label>
<input
{...register("completedWork", { valueAsNumber: true })}
type="number"
className="form-control form-control-sm me-2"
placeholder="Completed Work"
/>
{errors.completedWork && (
<p className="danger-text">
{errors.completedWork.message}
</p>
)}
</div>
)}
{selectedActivity && selectedCategory && (
<div className="col-2 col-md-2">
<label className="form-label" htmlFor="unit">
Unit
</label>
<input
type="text"
disabled
className="form-control form-control-sm me-2"
value={selectedActivity?.unitOfMeasurement || ""}
/>
</div>
)}
{selectedActivity && selectedCategory && (
<div className="col-12">
<label
className="form-text fs-7 m-1 text-lg text-dark"
htmlFor="descriptionTextarea"
>
Comment
</label>
<textarea
{...register("comment")}
className="form-control"
id="descriptionTextarea"
rows="2"
/>
{errors.comment && (
<div className="danger-text">
{errors.comment.message}
</div>
)}
</div>
)}
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isSubmitting ? "Please Wait.." : "Add Task"}
</button>
<button
type="button"
className="btn btn-sm btn-label-secondary"
onClick={onClose}
>
Cancel
</button>
</div>
</form>
</div>
</div>
<form className="row g-2" onSubmit={handleSubmit(onSubmitForm)}>
<div className="text-center mb-1">
<h5 className="mb-1">Manage Task</h5>
</div>
</div>
<div className="col-6">
<label className="form-label">Select Building</label>
<select
className="form-select form-select-sm"
{...register("buildingID")}
>
<option value="">Select Building</option>
{project
?.filter((b) => b?.buildingName)
?.sort((a, b) => a?.buildingName.localeCompare(b.buildingName))
?.map((b) => (
<option key={b.id} value={b.id}>
{b.buildingName}
</option>
))}
</select>
{errors.buildingID && (
<p className="danger-text">{errors.buildingID.message}</p>
)}
</div>
{selectedBuilding && (
<div className="col-6">
<label className="form-label">Select Floor</label>
<select
className="form-select form-select-sm"
{...register("floorId")}
>
<option value="">Select Floor</option>
{selectedBuilding.floors
?.sort((a, b) => a.floorName.localeCompare(b.floorName))
?.map((f) => (
<option key={f.id} value={f.id}>
{f.floorName}
</option>
))}
</select>
{errors.floorId && (
<p className="danger-text">{errors.floorId.message}</p>
)}
</div>
)}
{selectedFloor && (
<div className="col-12">
<label className="form-label">Select Work Area</label>
<select
className="form-select form-select-sm"
{...register("workAreaId")}
>
<option value="">Select Work Area</option>
{selectedFloor.workAreas
?.sort((a, b) => a.areaName.localeCompare(b.areaName))
?.map((w) => (
<option key={w.id} value={w.id}>
{w.areaName}
</option>
))}
</select>
{errors.workAreaId && (
<p className="danger-text">{errors.workAreaId.message}</p>
)}
</div>
)}
{selectedWorkArea && (
<div className="col-12">
<label className="form-label">Select Activity</label>
<select
className="form-select form-select-sm"
{...register("activityID")}
>
<option value="">Select Activity</option>
{activityData.map((a) => (
<option key={a.id} value={a.id}>
{a.activityName}
</option>
))}
</select>
{errors.activityID && (
<p className="danger-text">{errors.activityID.message}</p>
)}
</div>
)}
{selectedWorkArea && (
<div className="col-12">
<label className="form-label">Select Work Category</label>
<select
className="form-select form-select-sm"
{...register("workCategoryId")}
>
{categoryData.map((c) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))}
</select>
{errors.workCategoryId && (
<p className="danger-text">{errors.workCategoryId.message}</p>
)}
</div>
)}
{selectedActivity && selectedCategory && (
<>
<div className="col-5">
<label className="form-label">Planned Work</label>
<input
type="number"
className="form-control form-control-sm"
{...register("plannedWork", { valueAsNumber: true })}
/>
{errors.plannedWork && (
<p className="danger-text">{errors.plannedWork.message}</p>
)}
</div>
<div className="col-5">
<label className="form-label">Completed Work</label>
<input
type="number"
className="form-control form-control-sm"
{...register("completedWork", { valueAsNumber: true })}
/>
{errors.completedWork && (
<p className="danger-text">{errors.completedWork.message}</p>
)}
</div>
<div className="col-2">
<label className="form-label">Unit</label>
<input
type="text"
className="form-control form-control-sm"
disabled
value={selectedActivity?.unitOfMeasurement || ""}
/>
</div>
</>
)}
{selectedActivity && selectedCategory && (
<div className="col-12">
<label className="form-label">Comment</label>
<textarea
className="form-control"
rows="2"
{...register("comment")}
/>
{errors.comment && (
<p className="danger-text">{errors.comment.message}</p>
)}
</div>
)}
<div className="col-12 text-center">
<button
type="submit"
className="btn btn-sm btn-primary me-3"
disabled={isSubmitting}
>
{isSubmitting ? "Please Wait..." : "Add Task"}
</button>
<button
type="button"
className="btn btn-sm btn-label-secondary"
onClick={onClose}
>
Cancel
</button>
</div>
</form>
);
};

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import WorkItem from "./WorkItem";
import { useProjectDetails } from "../../../hooks/useProjects";
import { cacheData, getCachedData } from "../../../slices/apiDataManager";
import { useProjectDetails, useProjectTasks } from "../../../hooks/useProjects";
import { cacheData } from "../../../slices/apiDataManager";
import { useDispatch, useSelector } from "react-redux";
import { refreshData } from "../../../slices/localVariablesSlice";
import ProjectRepository from "../../../repositories/ProjectRepository";
@ -13,222 +13,122 @@ import {
MANAGE_TASK,
} from "../../../utils/constants";
import { useParams } from "react-router-dom";
import ProgressDonutChart from "../../Charts/ProgressDonutChart";
import ProgressBar from "../../common/ProgressBar";
import { componentsToColor } from "pdf-lib";
const WorkArea = ( {workArea, floor, forBuilding} ) =>
{
const selectedProject = useSelector( ( store ) => store.localVariables.projectId )
const { projects_Details, loading, error, refetch } = useProjectDetails(
selectedProject
);
const [workItems, setWorkItems] = useState([]);
const WorkArea = ({ workArea, floor, forBuilding }) => {
const selectedProject = useSelector((store) => store.localVariables.projectId);
const { projects_Details, loading } = useProjectDetails(selectedProject);
const [IsExpandedArea, setIsExpandedArea] = useState(false);
const dispatch = useDispatch();
const [Project, setProject] = useState();
const { projectId } = useParams();
const ManageTasks = useHasUserPermission(MANAGE_TASK);
const ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
const ManageAndAssignTak = useHasUserPermission(ASSIGN_REPORT_TASK);
const { ProjectTaskList, isLoading } = useProjectTasks(workArea.id, IsExpandedArea);
const [workAreaStatus, setWorkAreaStatus] = useState({
completed: 0,
planned: 100,
});
useEffect(() => {
const totalCompleted = workItems.reduce(
(sum, i) => sum + (i.workItem?.completedWork || 0),
setProject(projects_Details);
}, [projects_Details]);
useEffect(() => {
const totalCompleted = ProjectTaskList?.reduce(
(sum, i) => sum + (i?.workItem?.completedWork || 0),
0
);
const totalPlanned = workItems.reduce(
(sum, i) => sum + (i.workItem?.plannedWork || 0),
const totalPlanned = ProjectTaskList?.reduce(
(sum, i) => sum + (i?.workItem?.plannedWork || 0),
0
);
const percent =
totalPlanned > 0 ? (totalCompleted / totalPlanned) * 100 : 0;
//setPercentComplete(Math.min(percent, 100)); // cap at 100%
setWorkAreaStatus({ completed: totalCompleted, planned: totalPlanned });
}, [workItems]);
}, [ProjectTaskList]);
useEffect(() => {
// const project = getCachedData("projectInfo");
setProject( projects_Details );
const collapseElement = document.getElementById(`collapse-${workArea.id}`);
if (!projects_Details || !forBuilding?.id || !floor?.id || !workArea?.id) return;
const building = projects_Details.buildings?.find((b) => b.id === forBuilding.id);
const floors = building?.floors?.find((f) => f.id === floor.id);
const workAreas = floor?.workAreas?.find((wa) => wa.id === workArea.id);
setWorkItems(workAreas?.workItems || []);
}, [workArea, floor, floor,loading]);
const handleShown = () => setIsExpandedArea(true);
const handleHidden = () => setIsExpandedArea(false);
const HanldeDeleteActivity = async (workItemId) => {
try {
const updatedProject = { ...Project.data };
const response = await ProjectRepository.deleteProjectTask(workItemId);
const newProject = {
...updatedProject,
buildings: updatedProject?.buildings.map((building) =>
building?.id === building?.id
? {
...building,
floors: building?.floors?.map((floor) =>
floor.id === floor?.id
? {
...floor,
workAreas: floor.workAreas.map((workArea) =>
workArea.id === workArea?.id
? {
...workArea,
workItems: workArea.workItems.filter(
(item) =>
String(item?.workItem?.id ?? item?.id) !==
String(workItemId)
),
}
: workArea
),
}
: floor
),
}
: building
),
};
cacheData("projectInfo", {
projectId: newProject.id,
data: newProject,
});
dispatch(refreshData(true));
showToast("Activity Deleted Successfully", "success");
} catch (error) {
const message =
error.response?.data?.message ||
error.message ||
"An unexpected error occurred";
showToast(message, "error");
}
};
useEffect(() => {
const toggleButtons = document.querySelectorAll(".accordion-button");
toggleButtons.forEach((btn) => {
const icon = btn.querySelector(".toggle-icon");
btn.addEventListener("click", () => {
setTimeout(() => {
if (btn.classList.contains("collapsed")) {
icon.classList.remove("bx-minus-circle");
icon.classList.add("bx-plus-circle");
} else {
icon.classList.remove("bx-plus-circle");
icon.classList.add("bx-minus-circle");
}
}, 300); // allow Bootstrap collapse to complete
});
});
collapseElement?.addEventListener("shown.bs.collapse", handleShown);
collapseElement?.addEventListener("hidden.bs.collapse", handleHidden);
return () => {
toggleButtons.forEach((btn) => {
btn.removeEventListener("click", () => {});
});
collapseElement?.removeEventListener("shown.bs.collapse", handleShown);
collapseElement?.removeEventListener("hidden.bs.collapse", handleHidden);
};
}, []);
}, [workArea.id]);
return (
<React.Fragment key={workArea.id}>
<tr>
<td colSpan="4" className="p-0">
<div
className="accordion border-none"
id={`accordion-${workArea.id}`}
>
<div className="accordion border-none" id={`accordion-${workArea.id}`}>
<div className="accordion-item background border-0">
{/* Accordion Header */}
<p
className="accordion-header mb-0"
id={`heading-${workArea.id}`}
>
<p className="accordion-header mb-0" id={`heading-${workArea.id}`}>
<button
className={`accordion-button text-start px-2 py-2 custom-accordion-btn ${
workItems && workItems.length > 0 ? "collapsed" : "disabled"
}`}
className="accordion-button text-start px-2 py-2 custom-accordion-btn collapsed"
type="button"
data-bs-toggle={
workItems && workItems.length > 0 ? "collapse" : ""
}
data-bs-target={
workItems && workItems.length > 0
? `#collapse-${workArea.id}`
: undefined
}
data-bs-toggle="collapse"
data-bs-target={`#collapse-${workArea.id}`}
aria-expanded="false"
aria-controls={`collapse-${workArea.id}`}
disabled={!(workItems && workItems.length > 0)}
>
<i
className={`bx me-2 toggle-icon ${
workItems && workItems.length > 0
? "bx-plus-circle"
: "bx-block"
IsExpandedArea ? "bx-minus-circle" : "bx-plus-circle"
}`}
style={{
fontSize: "1.2rem",
color:
workItems && workItems.length > 0 ? "" : "transparent",
color: "black",
}}
></i>
<div className="d-flex justify-content-start row w-100 align-items-center">
<div className="d-flex col-5">
<span className="fw-semibold text-primary small">
Floor:
</span>
<span className="fw-normal text-darkgreen small px-2">
<span className="fw-semibold text-primary small">Floor:</span>
<span className="fw-normal text-darkgreen small px-2">
{floor.floorName}
</span>
</div>
<div className="text-start col-5">
<span className="fw-semibold text-primary small">
Work Area:
</span>
<span className="fw-normal text-darkgreen small px-2">
<span className="fw-semibold text-primary small">Work Area:</span>
<span className="fw-normal text-darkgreen small px-2">
{workArea.areaName}
</span>
</div>
{workArea?.workItems?.length > 0 && (
{ProjectTaskList?.length > 0 && (
<div className="col-2">
<ProgressBar
completedWork={workAreaStatus.completed}
plannedWork={workAreaStatus.planned}
className="m-0 text-info"
></ProgressBar>
/>
</div>
)}
</div>
</button>
</p>
{/* Accordion Body */}
{workItems && workItems.length > 0 && (
<div
id={`collapse-${workArea.id}`}
className="accordion-collapse collapse"
aria-labelledby={`heading-${workArea.id}`}
>
<div className="accordion-body px-1">
<div
id={`collapse-${workArea.id}`}
className="accordion-collapse collapse"
aria-labelledby={`heading-${workArea.id}`}
>
<div className="accordion-body px-1">
{isLoading ? (
<div className="text-center py-2 text-muted">Loading activities...</div>
) : ProjectTaskList?.length > 0 ? (
<table className="table table-sm mx-1">
<thead>
<tr>
<th className="infra-activity-table-header-first">
Activity
</th>
<th className="infra-activity-table-header-first">Activity</th>
<th className="infra-activity-table-header d-sm-table-cell d-md-none">
Status
</th>
@ -241,11 +141,8 @@ const WorkArea = ( {workArea, floor, forBuilding} ) =>
<th className="infra-activity-table-header d-none d-md-table-cell">
Today's Planned
</th>
<th className="infra-activity-table-header">
Progress
</th>
{(ManageInfra ||
(!projectId && ManageAndAssignTak)) && (
<th className="infra-activity-table-header">Progress</th>
{(ManageInfra || (!projectId && ManageAndAssignTak)) && (
<th className="infra-activity-table-header text-end">
<span className="px-2">Actions</span>
</th>
@ -253,21 +150,24 @@ const WorkArea = ( {workArea, floor, forBuilding} ) =>
</tr>
</thead>
<tbody className="table-border-bottom-0">
{workArea.workItems.map((workItem) => (
{ProjectTaskList.map((workItem,index) => (
<WorkItem
key={workItem.workItemId}
key={workItem.workItemId || `fallback-${index}`}
workItem={workItem}
forBuilding={forBuilding}
forFloor={floor}
forWorkArea={workArea}
deleteHandleTask={HanldeDeleteActivity}
/>
))}
</tbody>
</table>
</div>
) : (
<div className="text-center text-muted py-3">
No activities available for this work area.
</div>
)}
</div>
)}
</div>
</div>
</div>
</td>
@ -275,4 +175,5 @@ const WorkArea = ( {workArea, floor, forBuilding} ) =>
</React.Fragment>
);
};
export default WorkArea;

View File

@ -1,39 +1,29 @@
import React, { useState, useEffect } from "react";
import { set, useForm } from "react-hook-form";
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import showToast from "../../../services/toastService";
import { useManageProjectInfra } from "../../../hooks/useProjects";
import { useSelector } from "react-redux";
// Zod schema for form validation
const workAreaSchema = z.object({
id: z.string().nonempty("Floor is required"),
buildingId: z.string().nonempty("Building is required"),
floorId: z.string().nonempty("Floor is required"),
areaName: z
.string()
.nonempty("Work Area Name is required")
.min(3, "Name must be at least 3 characters long"),
id: z.string().optional(),
buildingId: z.string().min(1, "Building is required"),
floorId: z.string().min(1, "Floor is required"),
areaName: z.string().min(3, "Work Area Name must be at least 3 characters"),
});
// Default form data
const defaultModel = {
id: "0",
areaName: "",
buildingId: "0",
floorId: "0",
areaName: "",
};
const WorkAreaModel = ({
project,
onSubmit,
clearTrigger,
onClearComplete,
onClose,
}) => {
const WorkAreaModel = ({ project, onSubmit, onClose }) => {
const [selectedBuilding, setSelectedBuilding] = useState(null);
const [selectedFloor, setSelectedFloor] = useState(null);
const [selectdWorkArea, setWorkArea] = useState();
const selectedProject = useSelector((store)=>store.localVariables.projectId)
const {
register,
handleSubmit,
@ -42,259 +32,169 @@ const WorkAreaModel = ({
reset,
watch,
} = useForm({
resolver: zodResolver(workAreaSchema), // Use Zod resolver for validation
resolver: zodResolver(workAreaSchema),
defaultValues: defaultModel,
});
const floorId = watch("floorId"); // Watch the floorId for conditional rendering
const watchBuildingId = watch("buildingId");
const watchFloorId = watch("floorId");
const watchWorkAreaId = watch("id");
const { mutate: ManageWorkArea, isPending } = useManageProjectInfra({
onSuccessCallback: (data, variables) => {
showToast(
watchWorkAreaId != "0"
? "Wrok Area updated Successfully"
: "Work Area created Successfully",
"success"
);
reset({ id: "0", buildingId: "0", areaName: "", floorId: "0" });
// onClose?.();
},
});
useEffect(() => {
if (clearTrigger) {
reset(defaultModel); // Reset form to initial state
setSelectedBuilding(null);
setSelectedFloor(null);
onClearComplete();
}
}, [clearTrigger, onClearComplete, reset]);
const building = project?.find((b) => b.id === watchBuildingId);
setSelectedBuilding(building || null);
const handleWorkAreaChange = (e) => {
const { value } = e.target;
if (value === "0") {
setValue("id", "0"); // Create New Work Area
if (building) {
const floor = building.floors?.find((f) => f.id === watchFloorId);
setSelectedFloor(floor || null);
setValue("areaName", "");
setWorkArea(String(0));
} else {
const workArea = selectedFloor?.workAreas.find(
(b) => b.id === String(value)
);
if (workArea) {
setValue("id", String(workArea.id)); // Set id as a string
setValue("areaName", workArea.areaName);
setWorkArea(String(workArea.id));
}
}
};
const handleFloorChange = (e) => {
const { value } = e.target;
const floor = selectedBuilding?.floors.find((b) => b.id === String(value));
if (floor) {
setSelectedFloor(floor);
setValue("floorId", floor.id); // Update floorId
setValue("id", "0"); // Reset Work Area ID for new area creation
setValue("areaName", ""); // Reset Work Area Name when changing floor
} else {
setSelectedFloor(null);
setValue("floorId", "0");
setValue("id", "0"); // Reset Work Area ID
setValue("areaName", ""); // Reset Work Area Name
setValue("areaName", "");
}
}, [watchBuildingId, watchFloorId]);
const handleWrokAreaChange = (e) => {
const workAreaId = e.target.value;
setValue("id", workAreaId);
const area = selectedFloor?.workAreas.find((w) => w.id === workAreaId);
if (area) {
setValue("areaName", area.areaName);
} else {
setValue("areaName", "");
}
};
const handleBuildingChange = (e) => {
const { value } = e.target;
const building = project?.buildings.find((b) => b.id === String(value));
setSelectedBuilding(building);
setSelectedFloor(null); // Reset selected floor on building change
reset(defaultModel); // Reset the form when a new building is selected
};
useEffect(() => {
reset(defaultModel);
}, []);
const onSubmitForm = (data) => {
let WorkArea = {
id: data.id == "0" ? null : data.id,
const onSubmitForm = ( data ) =>
{
const payload = {
id: data.id === "0" ? null : data.id,
areaName: data.areaName,
floorId: data.floorId,
buildingId: data.buildingId,
};
onSubmit(WorkArea);
let infraObject = [
{
building: null,
floor: null,
workArea: payload,
},
];
reset({
id: "0",
areaName: "",
});
if (WorkArea.id !== null) {
showToast("WorkArea updated successfully.", "success");
} else {
showToast("WorkArea created successfully.", "success");
}
};
const handleCancel = () => {
reset(defaultModel);
setSelectedFloor(null);
setSelectedBuilding(null);
onClose();
ManageWorkArea({ infraObject, projectId: selectedProject });
};
return (
<div className="modal-dialog modal-lg modal-simple modal-edit-user">
<div className="modal-content">
<div className="modal-body">
<div className="row">
<button
type="button"
className="btn-close"
aria-label="Close"
onClick={onClose}
/>
<div className="text-center mb-1">
<h5 className="mb-1">Manage Work Area</h5>
</div>
<form className="row g-2" onSubmit={handleSubmit(onSubmitForm)}>
{/* Building Selection */}
<div className="col-6 col-md-6">
<label className="form-label" htmlFor="buildingId">
Select Building
</label>
<select
id="buildingId"
name="buildingId"
className="select2 form-select form-select-sm"
{...register("buildingId")}
onChange={handleBuildingChange}
>
<option value="0">Select Building</option>
{project?.buildings
?.filter((building) => building?.name)
?.sort((a, b) => {
const nameA = a.name || "";
const nameB = b.name || "";
return nameA?.localeCompare(nameB);
})
?.map((building) => (
<option key={building.id} value={building.id}>
{building.name}
</option>
))}
{project?.buildings?.filter((building) => building?.name)
.length === 0 && (
<option disabled>No buildings found</option>
)}
</select>
{errors.buildingId && <span>{errors.buildingId.message}</span>}
</div>
{/* Floor Selection */}
{selectedBuilding && selectedBuilding.buildingId !== "0" && (
<div className="col-6 col-md-6">
<label className="form-label" htmlFor="floorId">
Select Floor
</label>
<select
id="floorId"
name="floorId"
className="select2 form-select form-select-sm"
{...register("floorId")}
onChange={handleFloorChange}
>
<option value="0">Select Floor</option>
{selectedBuilding.floors
?.filter((floor) => floor?.floorName)
?.sort((a, b) => {
const nameA = a.floorName || "";
const nameB = b.floorName || "";
return nameA?.localeCompare(nameB);
})
?.map((floor) => (
<option key={floor.id} value={floor.id}>
{floor.floorName}
</option>
))}
{selectedBuilding.floors?.filter(
(floor) => floor?.floorName
).length === 0 && <option disabled>No floors found</option>}
</select>
{errors.floorId && <span>{errors.floorId.message}</span>}
</div>
)}
{/* Work Area Selection or Creation */}
{floorId !== "0" && (
<>
<div className="col-12 col-md-12">
<label className="form-label">Select Work Area</label>
<select
id="workAreaId"
name="workAreaId"
className="select2 form-select form-select-sm"
{...register("id")}
onChange={handleWorkAreaChange}
>
<option value="0">Create New Work Area</option>
{selectedFloor?.workAreas
?.filter((workArea) => workArea?.areaName)
?.sort((a, b) => {
const nameA = a.areaName || "";
const nameB = b.areaName || "";
return nameA?.localeCompare(nameB);
})
?.map((workArea) => (
<option key={workArea.id} value={workArea.id}>
{workArea.areaName}
</option>
))}
{selectedFloor?.workAreas?.filter(
(workArea) => workArea?.areaName
).length === 0 && (
<option disabled>No work areas found</option>
)}
</select>
{errors.id && <span>{errors.id.message}</span>}
</div>
{/* Work Area Name Input */}
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="areaName">
{watch("id") === "0"
? "Enter Work Area Name"
: "Modify Work Area Name"}
</label>
<input
type="text"
id="areaName"
name="areaName"
className="form-control form-control-sm"
placeholder="Work Area"
{...register("areaName")}
/>
{errors.areaName && (
<span className="danger-text">
{errors.areaName.message}
</span>
)}
</div>
{/* Submit and Cancel Buttons */}
<div className="col-12 text-center">
<button
type="submit"
className="btn btn-sm btn-primary me-3"
>
{watch("id") === "0" ? "Add Work Area" : "Edit Work Area"}
</button>
<button
type="button"
className="btn btn-sm btn-label-secondary"
onClick={handleCancel}
data-bs-dismiss="modal"
aria-label="Close"
>
Cancel
</button>
</div>
</>
)}
</form>
</div>
</div>
<form className="row g-2 p-2 p-md-1" onSubmit={handleSubmit(onSubmitForm)}>
<div className="text-center mb-1">
<h5 className="mb-1">Manage Work Area</h5>
</div>
</div>
<div className="col-12 col-sm-6">
<label className="form-label">Select Building</label>
<select
{...register("buildingId")}
className="form-select form-select-sm"
>
<option value="0">Select Building</option>
{project?.map((b) => (
<option key={b.id} value={b.id}>
{b.buildingName}
</option>
))}
</select>
{errors.buildingId && (
<p className="danger-text">{errors.buildingId.message}</p>
)}
</div>
{watchBuildingId !== "0" && (
<div className="col-12 col-sm-6">
<label className="form-label">Select Floor</label>
<select
{...register("floorId")}
className="form-select form-select-sm"
>
<option value="0">
{selectedBuilding?.floor?.length > 0
? "NO Floor Found"
: "Select Floor"}
</option>
{selectedBuilding?.floors?.map((f) => (
<option key={f.id} value={f.id}>
{f.floorName}
</option>
))}
</select>
{errors.floorId && (
<p className="danger-text">{errors.floorId.message}</p>
)}
</div>
)}
{watchFloorId !== "0" && (
<>
<div className="col-12">
<label className="form-label">Select Work Area</label>
<select
{...register("id")}
className="form-select form-select-sm"
onChange={handleWrokAreaChange}
>
<option value="0">Create New Work Area</option>
{selectedFloor?.workAreas?.length > 0 &&
selectedFloor?.workAreas?.map((w) => (
<option key={w.id} value={w.id}>
{w.areaName}
</option>
))}
</select>
</div>
<div className="col-12">
<label className="form-label">
{watchWorkAreaId === "0"
? "Enter Work Area Name"
: "Edit Work Area Name"}
</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="Work Area"
{...register("areaName")}
/>
{errors.areaName && (
<p className="danger-text">{errors.areaName.message}</p>
)}
</div>
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3" disabled={isPending}>
{isPending ? "Please Wait.." : watchWorkAreaId === "0" ? "Add Work Area" : "Update Work Area"}
</button>
<button type="button" className="btn btn-sm btn-label-secondary" disabled={isPending} onClick={onClose}>
Cancel
</button>
</div>
</>
)}
</form>
);
};

View File

@ -10,16 +10,16 @@ import {
} from "../../../utils/constants";
import ConfirmModal from "../../common/ConfirmModal";
import ProjectRepository from "../../../repositories/ProjectRepository";
import { useProjectDetails } from "../../../hooks/useProjects";
import { useDeleteProjectTask, useProjectDetails } from "../../../hooks/useProjects";
import showToast from "../../../services/toastService";
import {
cacheData,
clearCacheKey,
getCachedData,
} from "../../../slices/apiDataManager";
import { useDispatch } from "react-redux";
import { refreshData } from "../../../slices/localVariablesSlice";
import GlobalModel from "../../common/GlobalModel";
import {useDeleteMasterItem} from "../../../hooks/masterHook/useMaster";
const WorkItem = ({
workItem,
@ -39,18 +39,24 @@ const WorkItem = ({
const ManageAndAssignTak = useHasUserPermission(ASSIGN_REPORT_TASK);
const [loadingDelete, setLoadingDelete] = useState(false);
const project = getCachedData("projectInfo");
const dispatch = useDispatch();
const openModal = () => setIsModalOpen(true);
const closeModal = () => setIsModalOpen(false);
const closeModal = () => setIsModalOpen( false );
const showModalDelete = () => setShowModal2(true);
const closeModalDelete = () => setShowModal2(false);
const getProgress = (planned, completed) => {
return (completed * 100) / planned + "%";
};
const {mutate:DeleteTask,isPending } = useDeleteProjectTask(() => {
closeModalDelete?.();
});
const handleAssignTask = () => {
setItemName("");
};
useEffect(() => {
setNewWorkItem(workItem);
}, [workItem]);
@ -79,17 +85,15 @@ const WorkItem = ({
tooltipTriggerList.forEach((el) => new bootstrap.Tooltip(el));
}, []);
const showModal1 = () => setShowModal(true);
const closeModal1 = () => setShowModal(false);
const showModalDelete = () => setShowModal2(true);
const closeModalDelete = () => setShowModal2(false);
const handleSubmit = async () => {
setLoadingDelete(true);
let WorkItemId = workItem.workItemId || workItem.id;
deleteHandleTask(WorkItemId);
setLoadingDelete(false);
closeModalDelete();
DeleteTask({workItemId:WorkItemId,workAreaId:forWorkArea?.id})
};
const PlannedWork =
@ -105,21 +109,15 @@ const WorkItem = ({
)}
{showModal && (
<div
className={`modal fade ${showModal ? "show" : ""}`}
tabIndex="-1"
role="dialog"
style={{ display: showModal ? "block" : "none" }}
aria-hidden={!showModal}
>
<GlobalModel isOpen={showModal} size="lg" closeModal={()=>setShowModal(false)}>
<EditActivityModal
workItem={workItem}
workArea={forWorkArea}
building={forBuilding}
floor={forFloor}
onClose={closeModal1}
onClose={()=>setShowModal(false)}
/>
</div>
</GlobalModel>
)}
{showModal2 && (
@ -169,7 +167,7 @@ const WorkItem = ({
: "NA"}
</td>
{/* Category - visible on medium and above */}
<td className="text-center table-cell-small d-none d-md-table-cell">
<span className="fw-light">
{hasWorkItem
@ -204,7 +202,6 @@ const WorkItem = ({
: "NA"}
</td>
{/* Progress Bar - always visible */}
<td className="text-center " style={{ width: "15%" }}>
<div className="progress p-0">
<div
@ -231,7 +228,6 @@ const WorkItem = ({
</div>
</td>
{/* Actions - always visible */}
{(ManageInfra ||
(!projectId &&
ManageAndAssignTak &&
@ -255,7 +251,7 @@ const WorkItem = ({
<i
className="bx bxs-edit text-secondary cursor-pointer"
title="Edit"
onClick={showModal1}
onClick={()=>setShowModal(true)}
role="button"
></i>
<i
@ -297,7 +293,7 @@ const WorkItem = ({
<li>
<a
className="dropdown-item d-flex align-items-center"
onClick={showModal1}
onClick={()=>setShowModal(true) }
>
<i className="bx bxs-edit text-secondary me-2"></i> Edit
</a>

View File

@ -3,7 +3,6 @@ import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
// const currentDate = new Date().toISOString().split("T")[0];
const currentDate = new Date().toLocaleDateString('en-CA');
const formatDate = (date) => {
if (!date) {
@ -13,7 +12,6 @@ const formatDate = (date) => {
if (isNaN(d.getTime())) {
return currentDate;
}
// return d.toISOString().split("T")[0];
return d.toLocaleDateString('en-CA');
};
const ManageProjectInfo = ({ project, handleSubmitForm, onClose }) => {
@ -95,7 +93,7 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose }) => {
projectAddress: project?.projectAddress || "",
startDate: formatDate(project?.startDate) || "",
endDate: formatDate(project?.endDate) || "",
projectStatusId: String(project.projectStatusId) || "00000000-0000-0000-0000-000000000000",
projectStatusId: String(project?.projectStatus?.id) || "00000000-0000-0000-0000-000000000000",
}
: {}
);
@ -116,7 +114,7 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose }) => {
projectAddress: project?.projectAddress || "",
startDate: formatDate(project?.startDate) || currentDate,
endDate: formatDate(project?.endDate) || currentDate,
projectStatusId: String(project?.projectStatusId || "00000000-0000-0000-0000-000000000000"),
projectStatusId: String(project?.projectStatus?.id || "00000000-0000-0000-0000-000000000000"),
});
onClose();
};

View File

@ -69,45 +69,6 @@ const ProjectCard = ({ projectData, recall }) => {
}
};
// const handleFormSubmit = (updatedProject) => {
// if (projectInfo?.id) {
// ProjectRepository.updateProject(projectInfo.id, updatedProject)
// .then((response) => {
// const updatedProjectData = {
// ...projectInfo,
// ...response.data,
// building: projectDetails?.building,
// };
// setProjectInfo(updatedProject);
// if (getCachedData(`projectinfo-${projectInfo.id}`)) {
// cacheData(`projectinfo-${projectInfo.id}`, updatedProjectData);
// }
// const projects_list = getCachedData("projectslist");
// if (projects_list) {
// const updatedProjectsList = projects_list.map((project) =>
// project.id === projectInfo.id
// ? {
// ...project,
// ...response.data,
// // tenant: project.tenant
// }
// : project
// );
// cacheData("projectslist", updatedProjectsList);
// }
// recall(getCachedData("projectslist"));
// showToast("Project updated successfully.", "success");
// setShowModal(false);
// })
// .catch((error) => {
// showToast(error.message, "error");
// });
// }
// };
return (
<>

View File

@ -8,7 +8,6 @@ import TaskModel from "./Infrastructure/TaskModel";
import ProjectRepository, {
TasksRepository,
} from "../../repositories/ProjectRepository";
import ProjectModal from "./ProjectModal";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { MANAGE_PROJECT_INFRA } from "../../utils/constants";
import InfraTable from "./Infrastructure/InfraTable";
@ -17,328 +16,32 @@ import {
clearCacheKey,
getCachedData,
} from "../../slices/apiDataManager";
import { useProjectDetails } from "../../hooks/useProjects";
import { useProjectDetails, useProjectInfra } from "../../hooks/useProjects";
import { useDispatch, useSelector } from "react-redux";
import { refreshData } from "../../slices/localVariablesSlice";
import eventBus from "../../services/eventBus";
import {useParams} from "react-router-dom";
import GlobalModel from "../common/GlobalModel";
const ProjectInfra = ({ data, onDataChange, eachSiteEngineer }) => {
const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
{
const {projectId} = useParams()
const reloadedData = useSelector((store) => store.localVariables.reload);
const [expandedBuildings, setExpandedBuildings] = useState([]);
const [ expandedBuildings, setExpandedBuildings ] = useState( [] );
const {projectInfra,isLoading,error} = useProjectInfra(projectId)
const { projects_Details, refetch, loading } = useProjectDetails(data?.id);
const [project, setProject] = useState(projects_Details);
const [modalConfig, setModalConfig] = useState({ type: null, data: null });
const [isModalOpen, setIsModalOpen] = useState(false);
const [ project, setProject ] = useState( projects_Details );
const ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
const [isBuildingModalOpen, setIsBuildingModalOpen] = useState(false);
const [isFloorModalOpen, setIsFloorModalOpen] = useState(false);
const [isWorkAreaModelOpen, setIsWorkAreaModalOpen] = useState(false);
const [isTaskModelOpen, setIsTaskModalOpen] = useState(false);
const [isAssignRoleModal, setIsAssingRoleModal] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [clearFormTrigger, setClearFormTrigger] = useState(false);
const [CurrentBuilding, setCurrentBuilding] = useState("");
const [showModal, setShowModal] = useState(false);
const [showModalFloor, setshowModalFloor] = useState(false);
const [showModalWorkArea, setshowModalWorkArea] = useState(false);
const [showModalTask, setshowModalTask] = useState(false);
const [showModalBuilding, setshowModalBuilding] = useState(false);
const dispatch = useDispatch();
useEffect(() => {
setProject(projects_Details);
setProject(projectInfra);
}, [data, projects_Details]);
const openFloorModel = (projectData) => {
setIsFloorModalOpen(true);
};
const closeFloorModel = () => {
setIsFloorModalOpen(false);
};
const openAssignModel = (assignData) => {
setCurrentBuilding(assignData);
setIsAssingRoleModal(true);
};
const openBuildingModel = (projectData) => {
setIsBuildingModalOpen(true);
};
const closeBuildingModel = () => {
setIsBuildingModalOpen(false);
};
const handleBuildingModelFormSubmit = (buildingmodel) => {
if (buildingmodel.id == "" || buildingmodel.id == 0)
delete buildingmodel.id;
let data = [
{
building: buildingmodel,
floor: null,
workArea: null,
},
];
submitData(data);
};
const handleFloorModelFormSubmit = (updatedFloor) => {
if (updatedFloor.id == "") delete updatedFloor.id;
submitData([
{
building: null,
floor: updatedFloor,
workArea: null,
},
]);
};
const openWorkAreaModel = (projectData) => {
setIsWorkAreaModalOpen(true);
};
const closeWorkAreaModel = () => {
setIsWorkAreaModalOpen(false);
};
const handleWorkAreaModelFormSubmit = (updatedModel) => {
if (updatedModel.id == "") delete updatedModel.id;
submitData([
{
building: null,
floor: null,
workArea: updatedModel,
},
]);
};
const openTaskModel = (projectData) => {
setIsTaskModalOpen(true);
};
const closeTaskModel = () => {
setIsTaskModalOpen(false);
};
const handleTaskModelFormSubmit = (updatedModel) => {
if (updatedModel.id == "") updatedModel.id = null;
const updatedProject = { ...project };
ProjectRepository.manageProjectTasks([updatedModel])
.then((response) => {
onDataChange("task-change");
showToast("Details updated successfully.", "success");
// setClearFormTrigger( true );
if (response?.data[0]) {
const { workItemId, workItem } = response.data[0];
const updatedBuildings = updatedProject.buildings.map((building) =>
building.id == updatedModel.buildingID
? {
...building,
floors: building.floors.map((floor) =>
floor.id == updatedModel.floorId
? {
...floor,
workAreas: floor.workAreas.map((workArea) =>
workArea.id === workItem?.workAreaId
? {
...workArea,
workItems: workArea.workItems.some(
(existingItem) =>
existingItem.workItemId ===
workItem.workItemId
)
? [...workArea.workItems] // Create a new array to trigger re-render
: [...workArea.workItems, workItem],
}
: workArea
),
}
: floor
),
}
: building
);
updatedProject.buildings = updatedBuildings;
// workItem update, but having local state issue there for needed to calling api
clearCacheKey("projectInfo");
refetch();
cacheData("projectInfo", {
projectId: updatedProject.id,
data: updatedProject,
});
setProject(updatedProject);
// closeTaskModel()
}
})
.catch((error) => {
showToast(error.message, "error");
});
};
const submitData = async (infraObject) => {
try {
let response = await ProjectRepository.manageProjectInfra(infraObject);
const entity = response.data;
const updatedProject = { ...project };
// Handle the building data
if (entity.building) {
const { id, name, description } = entity.building;
const updatedBuildings = updatedProject?.buildings?.map((building) =>
building.id === id ? { ...building, name, description } : building
);
// Add building if it doesn't exist
if (!updatedProject.buildings.some((building) => building.id === id)) {
updatedBuildings.push({
id: id,
name,
description,
floors: [],
});
}
updatedProject.buildings = updatedBuildings;
// Update the cache for buildings
cacheData("projectInfo", {
projectId: updatedProject.id,
data: updatedProject,
});
setProject((prevProject) => ({
...prevProject,
buildings: updatedBuildings,
}));
// closeBuildingModel()
}
// Handle the floor data
else if (entity.floor) {
const { buildingId, id, floorName } = entity.floor;
const updatedBuildings = updatedProject?.buildings?.map((building) =>
building.id == buildingId
? {
...building,
floors: building.floors
.map((floor) =>
floor.id === id
? {
...floor,
floorName, // Update the floor name only
// Keep other properties as they are (including workArea)
}
: floor
)
// Add the new floor if it doesn't already exist
.concat(
!building.floors.some((floor) => floor.id === id)
? [{ id: id, floorName, workAreas: [] }] // New floor added with workArea set to null
: []
),
}
: building
);
updatedProject.buildings = updatedBuildings;
// Cache the updated project
cacheData("projectInfo", {
projectId: updatedProject.id,
data: updatedProject,
});
setProject(updatedProject);
// closeFloorModel()
}
// Handle the work area data
else if (entity.workArea) {
let buildingId = infraObject[0].workArea.buildingId;
const { floorId, areaName, id } = entity.workArea;
// Check if the workArea exists, otherwise create a new one
const updatedBuildings = updatedProject.buildings.map((building) =>
building.id == buildingId
? {
...building,
floors: building.floors.map((floor) =>
floor.id == floorId
? {
...floor,
workAreas: floor.workAreas.some(
(workArea) => workArea.id === id
)
? floor.workAreas.map((workArea) =>
workArea.id === id
? { ...workArea, areaName }
: workArea
)
: [
...floor.workAreas,
{ id, areaName, workItems: [] },
],
}
: floor
),
}
: building
);
updatedProject.buildings = updatedBuildings;
// Update the cache for work areas
cacheData("projectInfo", {
projectId: updatedProject.id,
data: updatedProject,
});
setProject(updatedProject);
// closeWorkAreaModel()
}
// Handle the task (workItem) data
else {
console.error("Unsupported data type for submitData", entity);
}
} catch (Err) {
showToast("Somthing wrong", "error");
}
};
const toggleBuilding = (id) => {
setExpandedBuildings((prev) =>
prev.includes(id) ? prev.filter((bid) => bid !== id) : [...prev, id]
);
};
const handleModalData = (type, modaldata) => {
setModalConfig({ type: type, data: modaldata });
};
const openModal = () => {
const modalElement = document.getElementById("building-model");
const modal = new Modal(modalElement, {
backdrop: false,
keyboard: true,
focus: true,
});
modal.show();
};
const closeModal = () => {
setIsModalOpen(false);
setModalConfig(null);
const modalElement = document.getElementById("building-model");
if (modalElement) {
modalElement.classList.remove("show"); // Remove modal visibility class
modalElement.style.display = "none"; // Hide the modal element
}
document.body.classList.remove("modal-open"); // Remove modal-open class from body
// Remove the modal backdrop
const backdropElement = document.querySelector(".modal-backdrop");
if (backdropElement) {
backdropElement.classList.remove("modal-backdrop"); // Remove backdrop class
backdropElement.style.display = "none"; // Hide the backdrop element
}
document.body.style.overflow = "auto";
};
const handleShow = () => setShowModal(true);
const handleClose = () => setShowModal(false);
useEffect(() => {
if (reloadedData) {
refetch();
@ -352,82 +55,30 @@ const ProjectInfra = ({ data, onDataChange, eachSiteEngineer }) => {
return (
<>
<div
className={`modal fade ${showModal ? "show" : ""}`}
tabIndex="-1"
role="dialog"
style={{ display: showModal ? "block" : "none" }}
aria-hidden={!showModal}
>
{showModalBuilding && <GlobalModel isOpen={showModalBuilding} size="md" closeModal={() => setshowModalBuilding( false )}>
<BuildingModel
project={project}
onClose={handleClose}
onSubmit={handleBuildingModelFormSubmit}
clearTrigger={clearFormTrigger}
onClearComplete={() => setClearFormTrigger(false)}
></BuildingModel>
</div>
{isFloorModalOpen && (
<div
className="modal fade show"
id="floor-model"
tabIndex="-1"
role="dialog"
style={{ display: "block" }}
aria-hidden="false"
>
<FloorModel
project={project}
onClose={closeFloorModel}
onSubmit={handleFloorModelFormSubmit}
clearTrigger={clearFormTrigger}
onClearComplete={() => setClearFormTrigger(false)}
project={projectInfra}
onClose={() => setshowModalBuilding( false )}
/>
</GlobalModel>}
{showModalFloor && <GlobalModel isOpen={showModalFloor} size="md" closeModal={()=>setshowModalFloor(false)}>
<FloorModel
project={projectInfra}
onClose={()=>setshowModalFloor(false)}
/>
</div>
)}
{isWorkAreaModelOpen && (
<div
className="modal fade show"
id="work-area-model"
tabIndex="-1"
role="dialog"
style={{ display: "block" }}
aria-hidden="false"
>
</GlobalModel>}
{showModalWorkArea && <GlobalModel isOpen={showModalWorkArea} size="lg" closeModal={()=>setshowModalWorkArea(false)} >
<WorkAreaModel
project={project}
onClose={closeWorkAreaModel}
onSubmit={handleWorkAreaModelFormSubmit}
clearTrigger={clearFormTrigger}
onClearComplete={() => setClearFormTrigger(false)}
project={projectInfra}
onClose={()=>setshowModalWorkArea(false)}
/>
</div>
)}
{isTaskModelOpen && (
<div
className="modal fade show"
id="task-model"
tabIndex="-1"
role="dialog"
style={{ display: "block" }}
aria-hidden="false"
>
<TaskModel
project={project}
onClose={closeTaskModel}
onSubmit={handleTaskModelFormSubmit}
clearTrigger={clearFormTrigger}
onClearComplete={() => setClearFormTrigger(false)}
</GlobalModel>}
{showModalTask && ( <GlobalModel isOpen={showModalTask} size="lg" closeModal={()=>setshowModalTask(false)}>
<TaskModel
project={projectInfra}
onClose={()=>setshowModalTask(false)}
/>
</div>
)}
{isModalOpen && (
<ProjectModal modalConfig={modalConfig} closeModal={closeModal} />
)}
</GlobalModel>)}
<div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4">
<div className="card">
<div className="card-body" style={{ padding: "0.5rem" }}>
@ -441,15 +92,15 @@ const ProjectInfra = ({ data, onDataChange, eachSiteEngineer }) => {
<button
type="button"
className="link-button link-button-sm m-1 "
onClick={handleShow}
onClick={()=>setshowModalBuilding(true)}
>
<i className="bx bx-plus-circle me-2"></i>
<i className="bx bx-plus-circle me-2"></i>
Manage Building
</button>
<button
type="button"
className="link-button m-1"
onClick={() => openFloorModel()}
onClick={()=>setshowModalFloor(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Floors
@ -457,7 +108,7 @@ const ProjectInfra = ({ data, onDataChange, eachSiteEngineer }) => {
<button
type="button"
className="link-button m-1"
onClick={() => openWorkAreaModel()}
onClick={() => setshowModalWorkArea(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Work Areas
@ -465,7 +116,7 @@ const ProjectInfra = ({ data, onDataChange, eachSiteEngineer }) => {
<button
type="button"
className="link-button m-1"
onClick={() => openTaskModel()}
onClick={()=>setshowModalTask(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Create Tasks
@ -473,15 +124,16 @@ const ProjectInfra = ({ data, onDataChange, eachSiteEngineer }) => {
</div>
</div>
<div className="row ">
{loading && <p>Loading....</p>}
{project && project.buildings?.length > 0 && (
{isLoading && <p>Loading....</p>}
{projectInfra && projectInfra?.length > 0 && (
<InfraTable
buildings={project?.buildings}
projectId={project.id}
handleFloor={submitData}
signalRHandler = {signalRHandler}
buildings={projectInfra}
projectId={projectId}
// handleFloor={submitData}
signalRHandler ={signalRHandler}
/>
)}
{!isLoading && projectInfra?.length == 0 && <div className="mt-5"><p>No Infra Avaiable</p></div>}
</div>
</div>
</div>

View File

@ -23,7 +23,6 @@ const ProjectModal = ({modalConfig,closeModal}) => {
></button>
<div className="text-center mb-2"></div>
{/* Modal Component */}
{modalConfig?.type === "assignRole" && <AssignRole assignData={modalConfig?.data} onClose={closeModal} />}

View File

@ -17,7 +17,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
className={`nav-link ${activePill === "profile" ? "active" : ""}`}
href="#"
onClick={(e) => {
e.preventDefault(); // Prevent page reload
e.preventDefault();
onPillClick("profile");
}}
>
@ -29,7 +29,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
className={`nav-link ${activePill === "teams" ? "active" : ""}`}
href="#"
onClick={(e) => {
e.preventDefault(); // Prevent page reload
e.preventDefault();
onPillClick("teams");
}}
>
@ -41,27 +41,14 @@ const ProjectNav = ({ onPillClick, activePill }) => {
className={`nav-link ${activePill === "infra" ? "active" : ""}`}
href="#"
onClick={(e) => {
e.preventDefault(); // Prevent page reload
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>
{/* <li className="nav-item">
<a
className={`nav-link ${
activePill === "workplan" ? "active" : ""
}`}
href="#"
onClick={(e) => {
e.preventDefault(); // Prevent page reload
onPillClick("workplan");
}}
>
<i className="bx bx-link bx-sm me-1_5"></i> Work Plan
</a>
</li> */}
<li className="nav-item">
<a
className={`nav-link ${

View File

@ -1,9 +1,9 @@
import React from "react";
import {useEmployeesByProjectAllocated, useProjects} from "../../hooks/useProjects";
const ProjectOverview = ({project}) =>
{
const {projects} = useProjects()
const ProjectOverview = ({project}) =>{
const {projects} = useProjects()
const project_detail = projects.find( ( pro ) => pro.id == project )
return (
<div className="card mb-6">

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback } from "react";
import MapUsers from "./MapUsers";
import { Link, NavLink, useNavigate } from "react-router-dom";
import { Link, NavLink, useNavigate, useParams } from "react-router-dom";
import showToast from "../../services/toastService";
import Avatar from "../common/Avatar";
@ -16,7 +16,9 @@ import ConfirmModal from "../common/ConfirmModal";
import eventBus from "../../services/eventBus";
import {useEmployeesByProjectAllocated, useManageProjectAllocation} from "../../hooks/useProjects";
const Teams = ({ project }) => {
const Teams = () =>
{
const {projectId} = useParams()
const dispatch = useDispatch();
const { data, loading } = useMaster();
@ -35,7 +37,7 @@ const Teams = ({ project }) => {
const HasAssignUserPermission = useHasUserPermission( ASSIGN_TO_PROJECT );
const [ IsDeleteModal, setIsDeleteModal ] = useState( false )
const {projectEmployees, loading:employeeLodaing, refetch} = useEmployeesByProjectAllocated( project.id )
const {projectEmployees, loading:employeeLodaing, refetch} = useEmployeesByProjectAllocated( projectId )
const {
mutate: submitAllocations,
isPending,
@ -216,11 +218,11 @@ const {
const handler = useCallback(
(msg) => {
if (msg.projectIds.some((item) => item === project.id)) {
if (msg.projectIds.some((item) => item === projectId)) {
refetch();
}
},
[project.id, refetch]
[projectId, refetch]
);
useEffect(() => {
@ -251,7 +253,7 @@ const {
aria-hidden="true"
>
<MapUsers
projectId={project?.id}
projectId={projectId}
onClose={onModelClose}
empJobRoles={empJobRoles}
onSubmit={handleEmpAlicationFormSubmit}

View File

@ -5,8 +5,8 @@ const Breadcrumb = ({ data }) => {
const navigate = useNavigate();
return (
<nav aria-label="breadcrumb">
<ol className="breadcrumb breadcrumb-custom-icon">
<nav aria-label="breadcrumb" >
<ol className="breadcrumb breadcrumb-custom-icon my-3">
{data.map((item, index) => (
item.link ? (
<li className="breadcrumb-item cursor-pointer" key={index}>

View File

@ -199,10 +199,10 @@ useEffect(() => {
)}
</div>
<div className="col-12 col-md-12 mx-2s " >
<div className="col-12 col-md-12 mx-2s" >
{masterFeatures.map((feature, featureIndex) => (
<div className="row my-3" key={feature.id} style={{ marginLeft: "0px" }}>
<div className="row my-1" key={feature.id} style={{ marginLeft: "0px" }}>
<div className="col-12 col-md-3 d-flex text-start align-items-center" style={{ wordWrap: 'break-word' }}>
<span className="fs">{feature.name}</span>
@ -210,7 +210,7 @@ useEffect(() => {
<div className="col-12 col-md-1">
</div>
<div className="col-12 col-md-8 d-flex justify-content-start align-items-center flex-wrap">
<div className="col-12 col-md-8 d-flex justify-content-start align-items-center flex-wrap ">
{feature.featurePermissions.map((perm, permIndex) => {
const refIndex = (featureIndex * 10) + permIndex;
return (
@ -262,6 +262,7 @@ useEffect(() => {
</div>
<hr className="hr my-1 py-1" />
</div>
))}
{errors.permissions && (

View File

@ -1,4 +1,4 @@
import {useState,useEffect} from "react"
import {useState,useEffect, useCallback} from "react"
import { MasterRespository } from "../../repositories/MastersRepository";
import { cacheData,getCachedData } from "../../slices/apiDataManager";
import { useSelector } from "react-redux";
@ -344,14 +344,14 @@ const fetchMasterData = async (masterType) => {
const useMaster = () => {
const selectedMaster = useSelector((store) => store.localVariables.selectedMaster);
const queryFn = useCallback(() => fetchMasterData(selectedMaster), [selectedMaster]);
const {
data = [],
isLoading,
error,
} = useQuery({
queryKey: ["masterData", selectedMaster],
queryFn: () => fetchMasterData(selectedMaster),
queryFn,
enabled: !!selectedMaster,
staleTime: 1000 * 60 * 10,
refetchOnWindowFocus: false,

View File

@ -10,260 +10,11 @@ import {store} from "../store/store";
import {queryClient} from "../layouts/AuthLayout";
// export const useAllEmployees = (showInactive) => {
// const [employeesList, setEmployeeList] = useState([]);
// const [loading, setLoading] = useState(false);
// const [error, setError] = useState();
// const fetchData = async () => {
// try {
// let EmployeeList_cached = getCachedData("AllEmployees");
// if (!EmployeeList_cached) {
// setLoading(true);
// const response = await EmployeeRepository.getAllEmployeeList(showInactive);
// cacheData("AllEmployees", response.data);
// setEmployeeList(response.data);
// setLoading(false);
// } else {
// setEmployeeList(EmployeeList_cached);
// setLoading(false);
// }
// } catch (error) {
// setError("Failed to fetch data.");
// setLoading(false);
// }
// };
// useEffect(() => {
// fetchData();
// }, []);
// return { employeesList, loading, error };
// };
// export const useEmployees = (selectedProject) => {
// const [employees, setEmployeeList] = useState([]);
// const [loading, setLoading] = useState(true);
// const [projects, setProjects] = useState([]);
// const fetchData = async (projectid) => {
// try {
// let EmployeeByProject_Cache = getCachedData("employeeListByProject");
// if (
// !EmployeeByProject_Cache ||
// !EmployeeByProject_Cache.projectId === projectid
// ) {
// EmployeeRepository.getEmployeeListByproject(projectid)
// .then((response) => {
// setEmployeeList(response);
// cacheData("employeeListByProject", {
// data: response,
// projectId: projectid,
// });
// })
// .catch((error) => {
// setError("Failed to fetch data.");
// });
// } else {
// setEmployeeList(EmployeeByProject_Cache.data);
// }
// setLoading(false);
// } catch (err) {
// setError("Failed to fetch data.");
// setLoading(false);
// }
// };
// useEffect(() => {
// if (selectedProject) {
// fetchData(selectedProject);
// }
// }, [selectedProject]);
// return { employees, loading, projects, reCallAllEmployee };
// };
// export const useEmployeeRoles = (employeeId) => {
// const [loading, setLoading] = useState(true);
// const [error, setError] = useState();
// const [employeeRoles, setEmployeeRoles] = useState([]);
// const fetchData = async (employeeid) => {
// try {
// let response = await RolesRepository.getEmployeeRoles(employeeid);
// setEmployeeRoles(response.data);
// cacheData("employeelist", response.data);
// } catch (err) {
// setError("Failed to fetch data.");
// setEmployeeRoles([]);
// } finally {
// setLoading(false);
// }
// };
// useEffect(() => {
// if (employeeId) {
// fetchData(employeeId);
// }
// }, [employeeId]);
// return { employeeRoles, loading, error };
// };
// export const useEmployeesByProject = (projectId) => {
// const [loading, setLoading] = useState(false);
// const [error, setError] = useState();
// const [employees, setEmployees] = useState([]);
// const fetchData = async () => {
// const Employees_cache = getCachedData("employeeListByProject");
// if (!Employees_cache || Employees_cache.projectId !== projectId) {
// setEmployees(true);
// ProjectRepository.getEmployeesByProject(projectId)
// .then((response) => {
// setEmployees(response.data);
// cacheData("employeeListByProject", {
// data: response.data,
// projectId,
// });
// setLoading(false);
// })
// .catch((error) => {
// setError("Failed to fetch data.");
// setLoading(false);
// });
// } else {
// setEmployees(Employees_cache.data);
// setLoading(false);
// }
// };
// useEffect(() => {
// fetchData(projectId);
// }, [projectId]);
// return { employees, loading, error, recallProjectEmplloyee: fetchData };
// };
// export const useEmployeesAllOrByProjectId = (projectId, showInactive) => {
// const [employees, setEmployees] = useState([]);
// const [loading, setLoading] = useState(false);
// const [error, setError] = useState(null);
// const fetchData = async (showInactive) => {
// if ( projectId )
// {
// const Employees_cache = getCachedData("employeeListByProject");
// if (!Employees_cache || Employees_cache.projectId !== projectId) {
// setLoading(true);
// setError(null);
// try {
// const response = await ProjectRepository.getEmployeesByProject(
// projectId
// );
// setEmployees(response.data);
// cacheData("employeeListByProject", {
// data: response.data,
// projectId,
// });
// setLoading(false);
// } catch (err) {
// setError("Failed to fetch data.");
// setLoading(false);
// }
// } else {
// setEmployees(Employees_cache.data);
// setLoading(false);
// }
// } else
// {
// const cacheKey = showInactive
// ? "allInactiveEmployeeList"
// : "allEmployeeList";
// try {
// const response = await EmployeeRepository.getAllEmployeeList(
// showInactive
// );
// setEmployees(response.data);
// cacheData(cacheKey, { data: response.data });
// setLoading(false);
// } catch (err) {
// setError("Failed to fetch data.");
// setLoading(false);
// }
// }
// };
// useEffect(() => {
// fetchData(showInactive); // Fetch data when the component mounts or projectId changes
// }, [projectId]); // Re-fetch when projectId changes
// return {
// employees,
// loading,
// error,
// recallEmployeeData: fetchData,
// };
// };
// export const useEmployeeProfile = (employeeId) => {
// const [loading, setLoading] = useState(true);
// const [error, setError] = useState();
// const [employee, setEmployees] = useState(null);
// const fetchData = async () => {
// if (!employeeId) {
// // Reset the state if no employeeId (e.g., opening for 'add' mode)
// setEmployees(null);
// setLoading(false);
// return;
// }
// const Employee_cache = getCachedData("employeeProfile");
// if (!Employee_cache || Employee_cache.employeeId !== employeeId) {
// EmployeeRepository.getEmployeeProfile(employeeId)
// .then((response) => {
// setEmployees(response.data);
// cacheData("employeeProfile", { data: response.data, employeeId });
// setLoading(false);
// })
// .catch((error) => {
// setError("Failed to fetch data.");
// setLoading(false);
// });
// } else {
// setEmployees(Employee_cache.data);
// setLoading(false);
// }
// };
// useEffect(() => {
// fetchData();
// }, [employeeId]);
// return { employee, loading, error };
// };
// Query ---------------------------------------------------------------------------
export const useAllEmployees = ( showInactive ) =>
{
const {
@ -413,12 +164,7 @@ export const useEmployeeProfile = ( employeeId ) =>
const res = await EmployeeRepository.getEmployeeProfile(employeeId);
return res.data;
},
enabled: isEnabled,
// initialData: () => {
// if (!queryClient) return null;
// return queryClient.getQueryData(['employeeProfile', employeeId]) || null;
// },
enabled: isEnabled,
});
return {

View File

@ -236,7 +236,11 @@ export const useEmployeesByProjectAllocated = (selectedProject) =>
const res = await ProjectRepository.getProjectAllocation( selectedProject );
return res.data || res
},
enabled:!!selectedProject
enabled: !!selectedProject,
onError: ( error ) =>
{
showToast(error.message || "Error while Fetching project Allocated Employees", "error");
}
} )
return {
@ -256,7 +260,11 @@ export const useProjectDetails = ( projectId,isAuto = true ) =>
const res = await ProjectRepository.getProjectByprojectId( projectId );
return res.data || res;
},
enabled:!!projectId && isAuto
enabled: !!projectId && isAuto,
onError: ( error ) =>
{
showToast(error.message || "Error while Fetching project Details", "error");
}
} )
return { projects_Details, loading:isLoading, error, refetch };
}
@ -270,7 +278,11 @@ export const useProjectsByEmployee = (employeeId) =>
const res = await ProjectRepository.getProjectsByEmployee( employeeId );
return res.data || res;
},
enabled: !!employeeId
enabled: !!employeeId,
onError: ( error ) =>
{
showToast(error.message || "Error while Fetching project Employee", "error");
}
})
return {projectList, loading:isLoading,error,refetch }
}
@ -284,13 +296,57 @@ export const useProjectName = () =>
const res = await ProjectRepository.projectNameList();
return res.data || res;
},
onError: ( error ) =>
{
showToast(error.message || "Error while Fetching project Name", "error");
}
} )
return {projectNames:data,loading:isLoading,Error:error,refetch}
}
export const useProjectInfra = (projectId) => {
const {
data: projectInfra,
isLoading,
error,
} = useQuery({
queryKey: ["ProjectInfra", projectId],
queryFn: async () => {
const res = await ProjectRepository.getProjectInfraByproject(projectId);
return res.data;
},
enabled: !!projectId ,
onError: (error) => {
showToast(error.message || "Error while fetching project infra", "error");
},
});
// -- Mutation-------------------------------
return { projectInfra, isLoading, error };
};
export const useProjectTasks = (workAreaId,IsExpandedArea=false) =>
{
const { data:ProjectTaskList,isLoading,error } = useQuery( {
queryKey: [ "WorkItems",workAreaId ],
queryFn: async () =>
{
const res = await ProjectRepository.getProjectTasksByWorkArea(workAreaId);
return res.data;
},
enabled: !!workAreaId && !!IsExpandedArea,
onError: ( error ) =>
{
showToast(error.message || "Error while Fetching project Tasks", "error");
}
} )
return {ProjectTaskList,isLoading,error}
}
// -- -------------Mutation-------------------------------
@ -337,14 +393,13 @@ export const useUpdateProject = ({ onSuccessCallback }) => {
return await ProjectRepository.updateProject(projectId, updatedData);
},
onSuccess: (data, variables) => {
onSuccess: ( data, variables ) =>
{
const { projectId } = variables;
// Invalidate queries
queryClient.invalidateQueries(["ProjectsList"]);
queryClient.invalidateQueries(["projectinfo", projectId]);
// Emit update event
eventBus.emit("project", {
keyword: "Update_Project",
response: data,
@ -371,21 +426,22 @@ export const useUpdateProject = ({ onSuccessCallback }) => {
};
export const useManageProjectInfra = () => {
export const useManageProjectInfra = ( {onSuccessCallback} ) =>
{
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({infraObject,projectId}) => {
mutationFn: async ( {infraObject, projectId} ) =>
{
return await ProjectRepository.manageProjectInfra(infraObject);
},
onSuccess: ( response, variables ) =>
onSuccess: ( data, variables ) =>
{
const { projectId } = variables;
showToast( "Details updated successfully.", "success" );
queryClient.invalidateQueries(["projectinfo", projectId]);
const { projectId } = variables;
queryClient.invalidateQueries(["ProjectInfra", projectId]);
if (onSuccessCallback) onSuccessCallback(data,variables);
},
onError: (error) => {
showToast(error.message || "Failed to update task details", "error");
showToast(error.message || "Failed to update Project Infra", "error");
},
});
};
@ -432,4 +488,58 @@ export const useManageProjectAllocation = ({
isSuccess,
isError,
};
};
};
export const useManageTask = ({onSuccessCallback}) =>
{
const queryClient = useQueryClient();
return useMutation( {
mutationFn: async ( payload ) => await ProjectRepository.manageProjectTasks( payload ),
onSuccess: ( data, variables ) =>
{
if ( variables[ 0 ]?.id )
{
showToast( 'Activity Updated Successfully', 'success' );
} else
{
showToast( 'Activity Created Successfully', 'success' );
}
queryClient.invalidateQueries(["WorkItems"])
if (onSuccessCallback) onSuccessCallback(data);
},
onError: (error) =>
{
const message =
error?.response?.data?.message || error.message || 'Error occurred during API call';
showToast(message, 'error');
}
})
}
export const useDeleteProjectTask = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ( {workItemId, workAreaId} ) =>
{
return await ProjectRepository.deleteProjectTask(workItemId);
},
onSuccess: ( _, variables ) =>
{
showToast("Task deleted successfully", "success");
queryClient.invalidateQueries([ "WorkItems",variables.workAreaId]);
if (onSuccessCallback) onSuccessCallback();
},
onError: (error) => {
showToast(
error?.response?.data?.message || error.message || "Failed to delete task",
"error"
);
if (onSuccessCallback) onSuccessCallback();
},
});
};

View File

@ -239,11 +239,15 @@ export const useSubmitTaskComment = ({ actionAllow, onSuccessCallback }) => {
return response.data;
},
onSuccess: ( data ) =>
onSuccess: ( data,variables ) =>
{
const workAreaId = variables?.commentsData?.workItem?.workArea?.id;
queryClient.invalidateQueries({ queryKey: ["taskList"] });
if (actionAllow) {
showToast("Review submitted successfully.", "success");
showToast( "Review submitted successfully.", "success" );
queryClient.invalidateQueries({ queryKey: [ "WorkItems", workAreaId ] });
} else
{
showToast("Comment sent successfully.", "success");
@ -270,7 +274,7 @@ export const useCreateTask = ( {onSuccessCallback, onErrorCallback} = {} ) =>
},
onSuccess: ( _, variables ) =>
{
queryClient.invalidateQueries({ queryKey: ["taskList"] });
queryClient.invalidateQueries(["taskList"]);
showToast("Task Assigned Successfully.", "success");
if (onSuccessCallback) onSuccessCallback(variables);
},

View File

@ -185,7 +185,7 @@ const AttendancePage = () => {
</div>
)}
<div className="container-xxl flex-grow-1 container-p-y">
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },

View File

@ -214,7 +214,7 @@ const DailyTask = () => {
</GlobalModel>
)}
<div className="container-xxl flex-grow-1 container-p-y">
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },

View File

@ -7,11 +7,11 @@ const TaskPlannng = () => {
return (
<>
<div className="container-xxl flex-grow-1 container-p-y">
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Daily Task Planning", link: "/activities/task" },
{ label: "Daily Task Planning" }
]}
></Breadcrumb>
<InfraPlanning/>

View File

@ -20,6 +20,7 @@ import DirectoryPageHeader from "./DirectoryPageHeader";
import ManageBucket from "../../components/Directory/ManageBucket";
import { useFab } from "../../Context/FabContext";
import { DireProvider, useDir } from "../../Context/DireContext";
import NotesCardViewDirectory from "../../components/Directory/NotesCardViewDirectory";
const Directory = ({ IsPage = true, prefernceContacts }) => {
const [projectPrefernce, setPerfence] = useState(null);
@ -31,11 +32,17 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
const [ContactList, setContactList] = useState([]);
const [contactCategories, setContactCategories] = useState([]);
const [searchText, setSearchText] = useState("");
const [listView, setListView] = useState(false);
const [viewType, setViewType] = useState("notes");
const [selectedBucketIds, setSelectedBucketIds] = useState([]);
const [deleteContact, setDeleteContact] = useState(null);
const [IsDeleting, setDeleting] = useState(false);
const [openBucketModal, setOpenBucketModal] = useState(false);
const [notes, setNotes] = useState([]);
const [filterAppliedNotes, setFilterAppliedNotes] = useState([]);
// const [selectedOrgs, setSelectedOrgs] = useState([]);
// Changed to an array for multiple selections
const [selectedNoteNames, setSelectedNoteNames] = useState([]);
const [tempSelectedBucketIds, setTempSelectedBucketIds] = useState([]);
const [tempSelectedCategoryIds, setTempSelectedCategoryIds] = useState([]);
@ -71,8 +78,6 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
setIsOpenModal(false);
}
// cacheData("Contacts", {data:updatedContacts,isActive:IsActive});
// setContactList(updatedContacts);
refetch(IsActive, prefernceContacts);
refetchBucket();
} catch (error) {
@ -249,12 +254,13 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
return () => setActions([]);
}, [IsPage, buckets]);
useEffect(() => {
setPerfence(prefernceContacts);
}, [prefernceContacts]);
return (
<div className="container-xxl flex-grow-1 container-p-y">
<div className="container-fluid">
{IsPage && (
<Breadcrumb
data={[
@ -326,84 +332,74 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
</GlobalModel>
)}
<div className="card p-2 card-minHeight">
<DirectoryPageHeader
searchText={searchText}
setSearchText={setSearchText}
setIsActive={setIsActive}
listView={listView}
setListView={setListView}
filteredBuckets={filteredBuckets}
tempSelectedBucketIds={tempSelectedBucketIds}
handleTempBucketChange={handleTempBucketChange}
filteredCategories={filteredCategories}
tempSelectedCategoryIds={tempSelectedCategoryIds}
handleTempCategoryChange={handleTempCategoryChange}
clearFilter={clearFilter}
applyFilter={applyFilter}
loading={loading}
IsActive={IsActive}
setOpenBucketModal={setOpenBucketModal}
/>
{/* Messages when listView is false */}
{!listView && (
<div className="d-flex flex-column justify-content-center align-items-center text-center ">
{loading && <p className="mt-10">Loading...</p>}
{!loading && contacts?.length === 0 && (
<div className="card p-0 mb-0">
<div className="card-body p-1 pb-0">
<DirectoryPageHeader
searchText={searchText}
setSearchText={setSearchText}
setIsActive={setIsActive}
viewType={viewType}
setViewType={setViewType}
filteredBuckets={filteredBuckets}
tempSelectedBucketIds={tempSelectedBucketIds}
handleTempBucketChange={handleTempBucketChange}
filteredCategories={filteredCategories}
tempSelectedCategoryIds={tempSelectedCategoryIds}
handleTempCategoryChange={handleTempCategoryChange}
clearFilter={clearFilter}
applyFilter={applyFilter}
loading={loading}
IsActive={IsActive}
setOpenBucketModal={setOpenBucketModal}
contactsToExport={contacts}
notesToExport={notes}
selectedNoteNames={selectedNoteNames}
setSelectedNoteNames={setSelectedNoteNames}
notesForFilter={notes}
setFilterAppliedNotes={setFilterAppliedNotes}
/>
</div>
</div>
<div className="card-minHeight mt-0">
{(viewType === "card" || viewType === "list" || viewType === "notes") && (
<div className="d-flex flex-column justify-content-center align-items-center text-center">
{!loading && (viewType === "card" || viewType === "list") && contacts?.length === 0 && (
<p className="mt-10">No contact found</p>
)}
{!loading && contacts?.length > 0 && currentItems.length === 0 && (
<p className="mt-10">No matching contact found</p>
)}
{!loading &&
(viewType === "card" || viewType === "list") &&
contacts?.length > 0 &&
currentItems.length === 0 && (
<p className="mt-10">No matching contact found</p>
)}
</div>
)}
{/* Table view (listView === true) */}
{listView ? (
<DirectoryListTableHeader>
{loading && (
<tr>
<td colSpan={10}>
{" "}
<p className="mt-10">Loading...</p>{" "}
</td>
</tr>
)}
{viewType === "list" && (
<div className="card cursor-pointer mt-5">
<div className="card-body p-2 pb-1">
<DirectoryListTableHeader>
{!loading &&
currentItems.map((contact) => (
<ListViewDirectory
key={contact.id}
IsActive={IsActive}
contact={contact}
setSelectedContact={setSelectedContact}
setIsOpenModal={setIsOpenModal}
setOpen_contact={setOpen_contact}
setIsOpenModalNote={setIsOpenModalNote}
IsDeleted={setDeleteContact}
restore={handleDeleteContact}
/>
))}
</DirectoryListTableHeader>
</div>
</div>
)}
{!loading && contacts?.length === 0 && (
<tr>
<td colSpan={10}>
<p className="mt-10">No contact found</p>
</td>
</tr>
)}
{!loading && currentItems.length === 0 && contacts?.length > 0 && (
<tr>
<td colSpan={10}>
<p className="mt-10">No matching contact found</p>
</td>
</tr>
)}
{!loading &&
currentItems.map((contact) => (
<ListViewDirectory
key={contact.id}
IsActive={IsActive}
contact={contact}
setSelectedContact={setSelectedContact}
setIsOpenModal={setIsOpenModal}
setOpen_contact={setOpen_contact}
setIsOpenModalNote={setIsOpenModalNote}
IsDeleted={setDeleteContact}
restore={handleDeleteContact}
/>
))}
</DirectoryListTableHeader>
) : (
<div className="row mt-5">
{viewType === "card" && (
<div className="row mt-4">
{!loading &&
currentItems.map((contact) => (
<div
@ -425,15 +421,26 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
</div>
)}
{viewType === "notes" && (
<div className="mt-0">
<NotesCardViewDirectory
notes={notes}
setNotesForFilter={setNotes}
searchText={searchText}
setIsOpenModalNote={setIsOpenModalNote}
filterAppliedNotes={filterAppliedNotes}
/>
</div>
)}
{/* Pagination */}
{!loading &&
viewType !== "notes" &&
contacts?.length > 0 &&
currentItems.length > ITEMS_PER_PAGE && (
<nav aria-label="Page navigation">
<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
className="page-link btn-xs"
onClick={() => paginate(currentPage - 1)}
@ -445,9 +452,8 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
{[...Array(totalPages)].map((_, index) => (
<li
key={index}
className={`page-item ${
currentPage === index + 1 ? "active" : ""
}`}
className={`page-item ${currentPage === index + 1 ? "active" : ""
}`}
>
<button
className="page-link"
@ -458,11 +464,7 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
</li>
))}
<li
className={`page-item ${
currentPage === totalPages ? "disabled" : ""
}`}
>
<li className={`page-item ${currentPage === totalPages ? "disabled" : ""}`}>
<button
className="page-link"
onClick={() => paginate(currentPage + 1)}
@ -478,4 +480,4 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
);
};
export default Directory;
export default Directory;

View File

@ -7,33 +7,38 @@ const DirectoryListTableHeader = ({ children }) => {
<table className="table px-2">
<thead>
<tr>
<th colSpan={2}>
<th colSpan={2} className="text-start">
<div className="d-flex align-items-center gap-1">
<span>Name</span>
</div>
</th>
<th className="px-2 text-start">
<div className="d-flex text-center align-items-center gap-1 justify-content-start">
<div className="d-flex align-items-center gap-1">
<span>Email</span>
</div>
</th>
<th className="mx-2">
<div className="d-flex align-items-center m-0 p-0 gap-1">
<th className="mx-2 text-start">
<div className="d-flex align-items-center gap-1">
<span>Phone</span>
</div>
</th>
<th colSpan={2} className="mx-2 ps-20">
Organization
<th colSpan={2} className="mx-2 ps-20 text-start">
<span>Organization</span>
</th>
<th className="mx-2 text-start">
<span>Category</span>
</th>
<th className="text-start">
<span>Action</span>
</th>
<th className="mx-2">Category</th>
<th>Action</th>
</tr>
</thead>
<tbody className="table-border-bottom-0 overflow-auto">
<tbody className="table-border-bottom-0 overflow-auto text-start">
{children}
</tbody>
</table>
</div>
);
};
export default DirectoryListTableHeader;

View File

@ -1,196 +1,559 @@
import React, { useEffect, useState } from "react";
import { exportToCSV, exportToExcel, printTable, exportToPDF } from "../../utils/tableExportUtils";
const DirectoryPageHeader = ({
searchText,
setSearchText,
setIsActive,
listView,
setListView,
filteredBuckets,
tempSelectedBucketIds,
handleTempBucketChange,
filteredCategories,
tempSelectedCategoryIds,
handleTempCategoryChange,
clearFilter,
applyFilter,
loading,
IsActive,
setOpenBucketModal,
searchText,
setSearchText,
setIsActive,
viewType,
setViewType,
filteredBuckets,
tempSelectedBucketIds,
handleTempBucketChange,
filteredCategories,
tempSelectedCategoryIds,
handleTempCategoryChange,
clearFilter,
applyFilter,
loading,
IsActive,
contactsToExport,
notesToExport,
selectedNoteNames,
setSelectedNoteNames,
notesForFilter,
setFilterAppliedNotes
}) => {
const [filtered, setFiltered] = useState();
const [filtered, setFiltered] = useState(0);
const [filteredNotes, setFilteredNotes] = useState([]);
const [noteCreators, setNoteCreators] = useState([]);
const [allCreators, setAllCreators] = useState([]);
const [allOrganizations, setAllOrganizations] = useState([]);
const [filteredOrganizations, setFilteredOrganizations] = useState([]);
const [selectedCreators, setSelectedCreators] = useState([]);
const [selectedOrgs, setSelectedOrgs] = useState([]);
useEffect(() => {
setFiltered(
tempSelectedBucketIds?.length + tempSelectedCategoryIds?.length
);
}, [tempSelectedBucketIds, tempSelectedCategoryIds]);
return (
<>
{/* <div className="row">vikas</div> */}
<div className="row mx-0 px-0 align-items-center mt-2">
<div className="col-12 col-md-6 mb-2 px-1 d-flex align-items-center gap-4 ">
<input
type="search"
className="form-control form-control-sm me-2"
placeholder="Search Contact..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
style={{ width: "200px" }}
/>
<div className="d-flex gap-2 ">
<button
type="button"
className={`btn btn-xs ${
!listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(false)}
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip"
title="Card View"
>
<i className="bx bx-grid-alt"></i>
</button>
<button
type="button"
className={`btn btn-xs ${
listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(true)}
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip"
title="List View"
>
<i className="bx bx-list-ul "></i>
</button>
</div>
<div className="dropdown" style={{ width: "fit-content" }}>
<div className="dropdown" style={{ width: "fit-content" }}>
<a
className="dropdown-toggle hide-arrow cursor-pointer d-flex align-items-center position-relative"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i
className={`fa-solid fa-filter ms-1 fs-5 ${
filtered > 0 ? "text-primary" : "text-muted"
}`}
></i>
useEffect(() => {
setFiltered(tempSelectedBucketIds?.length + tempSelectedCategoryIds?.length);
}, [tempSelectedBucketIds, tempSelectedCategoryIds]);
{filtered > 0 && (
<span
className="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning"
style={{ fontSize: "0.4rem" }}
>
{filtered}
</span>
)}
</a>
// New state to track active filters for notes
const [notesFilterCount, setNotesFilterCount] = useState(0);
<ul className="dropdown-menu p-3" style={{ width: "320px" }}>
<div>
<p className="text-muted m-0 h6 ">Filter by</p>
useEffect(() => {
// Calculate the number of active filters for notes
setNotesFilterCount(selectedCreators.length + selectedOrgs.length);
}, [selectedCreators, selectedOrgs]);
{/* Bucket Filter */}
<div className="mt-1">
<p className="text-small mb-1 ">Buckets</p>
<div className="d-flex flex-wrap">
{filteredBuckets.map(({ id, name }) => (
<div
className="form-check me-3 mb-1"
style={{ minWidth: "33.33%" }}
key={id}
>
<input
className="form-check-input"
type="checkbox"
id={`bucket-${id}`}
checked={tempSelectedBucketIds.includes(id)}
onChange={() => handleTempBucketChange(id)}
/>
<label
className="form-check-label text-nowrap text-small "
htmlFor={`bucket-${id}`}
>
{name}
</label>
</div>
))}
</div>
</div>
<hr className="m-0" />
{/* Category Filter */}
<div className="mt-1">
<p className="text-small mb-1 ">Categories</p>
<div className="d-flex flex-wrap">
{filteredCategories.map(({ id, name }) => (
<div
className="form-check me-3 mb-1"
style={{ minWidth: "33.33%" }}
key={id}
>
<input
className="form-check-input"
type="checkbox"
id={`cat-${id}`}
checked={tempSelectedCategoryIds.includes(id)}
onChange={() => handleTempCategoryChange(id)}
/>
<label
className="form-check-label text-nowrap text-small"
htmlFor={`cat-${id}`}
>
{name}
</label>
</div>
))}
</div>
</div>
useEffect(() => {
if (viewType === "notes") {
if (notesToExport && notesToExport.length > 0) {
const uniqueNames = [...new Set(notesToExport.map(note => {
const firstName = note.createdBy?.firstName || "";
const lastName = note.createdBy?.lastName || "";
return `${firstName} ${lastName}`.trim();
}).filter(name => name !== ""))];
setNoteCreators(uniqueNames.sort());
} else {
setNoteCreators([]);
}
} else {
setNoteCreators([]);
}
}, [notesToExport, viewType]);
<div className="d-flex justify-content-end gap-2 mt-1">
<button
className="btn btn-xs btn-secondary"
onClick={clearFilter}
>
Clear
</button>
<button
className="btn btn-xs btn-primary"
onClick={applyFilter}
>
Apply Filter
</button>
</div>
// Separate effect to clear selection only when switching away from notes
useEffect(() => {
if (viewType !== "notes" && selectedNoteNames.length > 0) {
setSelectedNoteNames([]);
}
}, [viewType]);
useEffect(() => {
const creatorsSet = new Set();
const orgsSet = new Set();
notesForFilter.forEach((note) => {
const creator = `${note.createdBy?.firstName || ""} ${note.createdBy?.lastName || ""}`.trim();
if (creator) creatorsSet.add(creator);
const org = note.organizationName;
if (org) orgsSet.add(org);
});
setAllCreators([...creatorsSet].sort());
setAllOrganizations([...orgsSet].sort());
setFilteredOrganizations([...orgsSet].sort());
}, [notesForFilter])
const handleToggleNoteName = (name) => {
setSelectedNoteNames(prevSelectedNames => {
if (prevSelectedNames.includes(name)) {
return prevSelectedNames.filter(n => n !== name);
} else {
return [...prevSelectedNames, name];
}
});
};
const updateFilteredOrganizations = () => {
if (selectedCreators.length === 0) {
setFilteredOrganizations(allOrganizations);
return;
}
const filteredOrgsSet = new Set();
notesForFilter.forEach((note) => {
const creator = `${note.createdBy?.firstName || ""} ${note.createdBy?.lastName || ""}`.trim();
if (selectedCreators.includes(creator)) {
if (note.organizationName) {
filteredOrgsSet.add(note.organizationName);
}
}
});
setFilteredOrganizations([...filteredOrgsSet].sort());
};
const handleToggleCreator = (name) => {
const updated = selectedCreators.includes(name)
? selectedCreators.filter((n) => n !== name)
: [...selectedCreators, name];
setSelectedCreators(updated);
};
const handleToggleOrg = (name) => {
const updated = selectedOrgs.includes(name)
? selectedOrgs.filter((n) => n !== name)
: [...selectedOrgs, name];
setSelectedOrgs(updated);
};
const handleExport = (type) => {
let dataToExport = [];
if (viewType === "notes") {
if (!notesToExport || notesToExport.length === 0) {
console.warn("No notes to export.");
return;
}
const decodeHtmlEntities = (html) => {
const textarea = document.createElement("textarea");
textarea.innerHTML = html;
return textarea.value;
};
const cleanNoteText = (html) => {
if (!html) return "";
const stripped = html.replace(/<[^>]+>/g, "");
const decoded = decodeHtmlEntities(stripped);
return decoded.replace(/\u00A0/g, " ").replace(/\s+/g, " ").trim();
};
const cleanName = (name) => {
if (!name) return "";
return name.replace(/\u00A0/g, " ").replace(/\s+/g, " ").trim();
};
dataToExport = notesToExport.map(note => ({
"Name": cleanName(`${note.createdBy?.firstName || ""} ${note.createdBy?.lastName || ""}`),
"Notes": cleanNoteText(note.note),
"Created At": note.createdAt
? new Date(note.createdAt).toLocaleString("en-IN")
: "",
"Updated At": note.updatedAt
? new Date(note.updatedAt).toLocaleString("en-IN")
: "",
"Updated By": cleanName(
`${note.updatedBy?.firstName || ""} ${note.updatedBy?.lastName || ""}`
),
}));
} else {
if (!contactsToExport || contactsToExport.length === 0) {
console.warn("No contacts to export.");
return;
}
dataToExport = contactsToExport.map(contact => ({
Name: contact.name || '',
Organization: contact.organization || '',
Email: contact.contactEmails?.map(email => email.emailAddress).join(', ') || '',
Phone: contact.contactPhones?.map(phone => phone.phoneNumber).join(', ') || '',
Category: contact.contactCategory?.name || '',
Tags: contact.tags?.map(tag => tag.name).join(', ') || '',
}));
}
const today = new Date();
const formattedDate = `${today.getFullYear()}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`;
const filename =
viewType === "notes"
? `Directory_Notes_${formattedDate}`
: `Directory_Contacts_${formattedDate}`;
switch (type) {
case "csv":
exportToCSV(dataToExport, filename);
break;
case "excel":
exportToExcel(dataToExport, filename);
break;
case "pdf":
exportToPDF(dataToExport, filename);
break;
case "print":
printTable(dataToExport, filename);
break;
default:
break;
}
};
const applyCombinedFilter = () => {
const lowerSearch = searchText?.toLowerCase() || "";
const filtered = notesForFilter.filter((noteItem) => {
const creator = `${noteItem.createdBy?.firstName || ""} ${noteItem.createdBy?.lastName || ""}`.trim();
const org = noteItem.organizationName;
const matchesCreator = selectedCreators.length === 0 || selectedCreators.includes(creator);
const matchesOrg = selectedOrgs.length === 0 || selectedOrgs.includes(org);
const plainNote = noteItem?.note?.replace(/<[^>]+>/g, "").toLowerCase();
const stringValues = [];
const extractStrings = (obj) => {
for (const key in obj) {
const value = obj[key];
if (typeof value === "string") {
stringValues.push(value.toLowerCase());
} else if (typeof value === "object" && value !== null) {
extractStrings(value);
}
}
};
extractStrings(noteItem);
stringValues.push(plainNote, creator.toLowerCase());
const matchesSearch = stringValues.some((val) => val.includes(lowerSearch));
return matchesCreator && matchesOrg && matchesSearch;
});
setFilteredNotes(filtered);
setFilterAppliedNotes(filtered);
};
return (
<>
<div className="row mx-0 px-0 align-items-center mt-0">
<div className="col-12 col-md-6 mb-0 px-1 d-flex align-items-center gap-4">
<ul className="nav nav-tabs mb-0" role="tablist">
<li className="nav-item" role="presentation">
<button
className={`nav-link ${viewType === "notes" ? "active" : ""}`}
onClick={() => setViewType("notes")}
type="button"
>
<i className="bx bx-note me-1"></i> Notes
</button>
</li>
<li className="nav-item" role="presentation">
<button
className={`nav-link ${viewType === "card" ? "active" : ""}`}
onClick={() => setViewType("card")}
type="button"
>
<i className="bx bx-user me-1"></i> Contacts
</button>
</li>
</ul>
</div>
</ul>
</div>
</div>
</div>
<div className="col-12 col-md-6 mb-2 px-1 d-flex justify-content-end gap-2 align-items-center text-end">
<label className="switch switch-primary align-self-start mb-2">
<input
type="checkbox"
className="switch-input me-3"
onChange={() => setIsActive(!IsActive)}
value={IsActive}
disabled={loading}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className=" list-inline-item ms-12 ">
Show Inactive Contacts
</span>
</label>
</div>
</div>
</>
);
<hr className="my-0 mb-2" style={{ borderTop: "1px solid #dee2e6" }} />
<div className="row mx-0 px-0 align-items-center mt-0">
<div className="col-12 col-md-6 mb-2 px-5 d-flex align-items-center gap-4">
<input
type="search"
className="form-control me-0"
placeholder={viewType === "notes" ? "Search Notes..." : "Search Contact..."}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
style={{ width: "200px", height: "30px" }}
/>
{/* Filter by funnel icon for Notes view */}
{viewType === "notes" && (
<div className="dropdown" style={{ width: "fit-content" }}>
<a
className="dropdown-toggle hide-arrow cursor-pointer d-flex align-items-center position-relative"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i className={`fa-solid fa-filter ms-1 fs-5 ${notesFilterCount > 0 ? "text-primary" : "text-muted"}`}></i>
{notesFilterCount > 0 && (
<span className="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning" style={{ fontSize: "0.4rem" }}>
{notesFilterCount}
</span>
)}
</a>
<div className="dropdown-menu p-0" style={{ minWidth: "700px" }}>
{/* Scrollable Filter Content */}
<div
className="p-3"
style={{
maxHeight: "300px",
overflowY: "auto",
overflowX: "hidden",
whiteSpace: "normal"
}}
>
<div className="d-flex gap-3">
{/* Created By */}
<div style={{ flex: 0.50, maxHeight: "260px", overflowY: "auto" }}>
<div style={{ position: "sticky", top: 0, background: "#fff", zIndex: 1 }}>
<p className="text-muted mb-2 pt-2">Created By</p>
</div>
{allCreators.map((name, idx) => (
<div className="form-check mb-1" key={`creator-${idx}`}>
<input
className="form-check-input"
type="checkbox"
id={`creator-${idx}`}
checked={selectedCreators.includes(name)}
onChange={() => handleToggleCreator(name)}
/>
<label className="form-check-label text-nowrap" htmlFor={`creator-${idx}`}>
{name}
</label>
</div>
))}
</div>
{/* Organization */}
<div style={{ flex: 1, maxHeight: "260px", overflowY: "auto",overflowX: "hidden", }}>
<div style={{ position: "sticky", top: 0, background: "#fff", zIndex: 1 }}>
<p className="text-muted mb-2 pt-2">Organization</p>
</div>
{filteredOrganizations.map((org, idx) => (
<div className="form-check mb-1" key={`org-${idx}`}>
<input
className="form-check-input"
type="checkbox"
id={`org-${idx}`}
checked={selectedOrgs.includes(org)}
onChange={() => handleToggleOrg(org)}
/>
<label className="form-check-label text-nowrap" htmlFor={`org-${idx}`}>
{org}
</label>
</div>
))}
</div>
</div>
</div>
{/* Sticky Footer Buttons */}
<div
className="d-flex justify-content-end gap-2 p-2 "
style={{
background: "#fff",
position: "sticky",
bottom: 0
}}
>
<button
className="btn btn-xs btn-secondary"
onClick={() => {
setSelectedCreators([]);
setSelectedOrgs([]);
setFilteredOrganizations(allOrganizations);
setFilterAppliedNotes(notesForFilter);
}}
>
Clear
</button>
<button
className="btn btn-xs btn-primary"
onClick={() => {
applyCombinedFilter();
}}
>
Apply Filter
</button>
</div>
</div>
</div>
)}
{(viewType === "card" || viewType === "list") && (
<div className="d-flex gap-2">
<button
type="button"
className={`btn btn-xs ${viewType === "card" ? "btn-primary" : "btn-outline-primary"}`}
onClick={() => setViewType("card")}
>
<i className="bx bx-grid-alt"></i>
</button>
<button
type="button"
className={`btn btn-xs ${viewType === "list" ? "btn-primary" : "btn-outline-primary"}`}
onClick={() => setViewType("list")}
>
<i className="bx bx-list-ul me-1"></i>
</button>
</div>
)}
{/* Filter by funnel icon for Contacts view (retains numerical badge) */}
{viewType !== "notes" && (
<div className="dropdown-center" style={{ width: "fit-content" }}>
<a
className="dropdown-toggle hide-arrow cursor-pointer d-flex align-items-center position-relative"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i className={`fa-solid fa-filter ms-1 fs-5 ${filtered > 0 ? "text-primary" : "text-muted"}`}></i>
{filtered > 0 && (
<span className="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning" style={{ fontSize: "0.4rem" }}>
{filtered}
</span>
)}
</a>
<ul className="dropdown-menu p-3" style={{ width: "700px" }}>
<p className="text-muted m-0 h6">Filter by</p>
<div className="d-flex flex-nowrap">
<div className="mt-1 me-4" style={{ flexBasis: "50%" }}>
<p className="text-small mb-1">Buckets</p>
<div className="d-flex flex-wrap">
{filteredBuckets.map(({ id, name }) => (
<div className="form-check me-3 mb-1" style={{ minWidth: "calc(50% - 15px)" }} key={id}>
<input
className="form-check-input"
type="checkbox"
id={`bucket-${id}`}
checked={tempSelectedBucketIds.includes(id)}
onChange={() => handleTempBucketChange(id)}
/>
<label className="form-check-label text-nowrap text-small" htmlFor={`bucket-${id}`}>
{name}
</label>
</div>
))}
</div>
</div>
<div className="mt-1" style={{ flexBasis: "50%" }}>
<p className="text-small mb-1">Categories</p>
<div className="d-flex flex-wrap">
{filteredCategories.map(({ id, name }) => (
<div className="form-check me-3 mb-1" style={{ minWidth: "calc(50% - 15px)" }} key={id}>
<input
className="form-check-input"
type="checkbox"
id={`cat-${id}`}
checked={tempSelectedCategoryIds.includes(id)}
onChange={() => handleTempCategoryChange(id)}
/>
<label className="form-check-label text-nowrap text-small" htmlFor={`cat-${id}`}>
{name}
</label>
</div>
))}
</div>
</div>
</div>
<div className="d-flex justify-content-end gap-2 mt-1">
<button
className="btn btn-xs btn-secondary"
onClick={(e) => {
// e.stopPropagation();
clearFilter();
}}
>
Clear
</button>
<button
className="btn btn-xs btn-primary"
onClick={(e) => {
applyFilter();
}}
>
Apply Filter
</button>
</div>
</ul>
</div>
)}
</div>
<div className="col-12 col-md-6 mb-2 px-5 d-flex justify-content-end align-items-center gap-2">
{(viewType === "list" || viewType === "card") && (
<label className="switch switch-primary mb-0">
<input
type="checkbox"
className="switch-input me-3"
onChange={() => setIsActive(!IsActive)}
checked={!IsActive}
disabled={loading}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className="ms-12">Show Inactive Contacts</span>
</label>
)}
<div className="btn-group">
<button
className="btn btn-sm btn-label-secondary dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i className="bx bx-export me-2 bx-sm"></i>Export
</button>
<ul className="dropdown-menu">
<li>
<a className="dropdown-item" href="#" onClick={(e) => { e.preventDefault(); handleExport("csv"); }}>
<i className="bx bx-file me-1"></i> CSV
</a>
</li>
<li>
<a className="dropdown-item" href="#" onClick={(e) => { e.preventDefault(); handleExport("excel"); }}>
<i className="bx bxs-file-export me-1"></i> Excel
</a>
</li>
{viewType !== "notes" && (
<li>
<a className="dropdown-item" href="#" onClick={(e) => { e.preventDefault(); handleExport("pdf"); }}>
<i className="bx bxs-file-pdf me-1"></i> PDF
</a>
</li>
)}
</ul>
</div>
</div>
</div>
</>
);
};
export default DirectoryPageHeader;
export default DirectoryPageHeader;

View File

@ -19,13 +19,13 @@ const LoginPage = () => {
const loginSchema = IsLoginWithOTP
? z.object({
username: z.string().email({ message: "Valid email required" }),
})
username: z.string().email({ message: "Valid email required" }),
})
: z.object({
username: z.string().email({ message: "Valid email required" }),
password: z.string().min(1, { message: "Password required" }),
rememberMe: z.boolean(),
});
username: z.string().email({ message: "Valid email required" }),
password: z.string().min(1, { message: "Password required" }),
rememberMe: z.boolean(),
});
const {
register,
@ -53,6 +53,7 @@ const LoginPage = () => {
navigate("/dashboard");
} else {
await AuthRepository.sendOTP({ email: data.username });
showToast("OTP has been sent to your email.", "success");
localStorage.setItem("otpUsername", data.username);
localStorage.setItem("otpSentTime", now.toString());
navigate("/auth/login-otp");
@ -114,18 +115,18 @@ const LoginPage = () => {
<label className="form-label" htmlFor="password">
Password
</label>
<div className="input-group input-group-merge">
<div className="input-group input-group-merge d-flex align-items-center border rounded px-2">
<input
type={hidepass ? "password" : "text"}
autoComplete="true"
id="password"
{...register("password")}
className="form-control"
className="form-control form-control-xl border-0 shadow-none"
placeholder="••••••••••••"
/>
<button
type="button"
className="btn border-top border-end border-bottom"
className="btn btn-link p-0 ms-2 "
onClick={() => setHidepass(!hidepass)}
style={{
borderTopLeftRadius: 0,
@ -150,6 +151,7 @@ const LoginPage = () => {
)}
</div>
<div className="mb-3 d-flex justify-content-between">
<div className="form-check d-flex">
<input

View File

@ -67,9 +67,13 @@ const ResetPasswordPage = () => {
navigate("/auth/login", { replace: true });
// setLoading(false);
} catch (error) {
showToast("Link is expries or Invalid ", "error");
setTokenExpired(true);
debugger;
setLoading(false);
if (error?.response?.status === 400) {
showToast("Please check valid Credentials", "error");
} else {
setTokenExpired(true);
}
}
};
@ -77,7 +81,10 @@ const ResetPasswordPage = () => {
return (
<AuthWrapper>
<h4 className="mb-2 ">Invalid Link 🔒</h4>
<p className="mb-4" style={{fontSize: "12px"}}>This link appears to be invalid or expired. Please use the 'Forgot Password' feature to set your new password.</p>
<p className="mb-4" style={{ fontSize: "12px" }}>
This link appears to be invalid or expired. Please use the 'Forgot
Password' feature to set your new password.
</p>
<div className="text-center mb-4">
<Link to="/auth/forgot-password" className="btn btn-outline-primary">
Go to Forgot Password
@ -142,7 +149,6 @@ const ResetPasswordPage = () => {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderLeft: 0,
}}
>
{hidepass ? (
@ -185,7 +191,6 @@ const ResetPasswordPage = () => {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderLeft: 0,
}}
>
{hidepass1 ? (

View File

@ -12,7 +12,7 @@ import { hasUserPermission } from "../../utils/authUtils";
import { ITEMS_PER_PAGE, MANAGE_EMPLOYEES } from "../../utils/constants";
import { clearCacheKey } from "../../slices/apiDataManager";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import SuspendEmp from "../../components/Employee/SuspendEmp";
import SuspendEmp from "../../components/Employee/SuspendEmp"; // Keep if you use SuspendEmp
import {
exportToCSV,
exportToExcel,
@ -29,16 +29,19 @@ import GlobalModel from "../../components/common/GlobalModel";
import usePagination from "../../hooks/usePagination";
const EmployeeList = () => {
const selectedProjectId = useSelector((store) => store.localVariables.projectId);
const [selectedProject, setSelectedProject] = useState(() => selectedProjectId || "");
const { projects, loading: projectLoading } = useProjects();
const selectedProjectId = useSelector(
(store) => store.localVariables.projectId
);
const [showInactive, setShowInactive] = useState(false);
const [showAllEmployees, setShowAllEmployees] = useState(false);
const [showAllEmployees, setShowAllEmployees] = useState(false);
const Manage_Employee = useHasUserPermission(MANAGE_EMPLOYEES);
const { employees, loading, setLoading, error, recallEmployeeData } =
useEmployeesAllOrByProjectId(showAllEmployees ? null : selectedProject, showInactive);
const [projectsList, setProjectsList] = useState(projects || []);
useEmployeesAllOrByProjectId(
showAllEmployees ? null : selectedProjectId, // Use selectedProjectId here
showInactive
);
const [employeeList, setEmployeeList] = useState([]);
const [ modelConfig, setModelConfig ] = useState();
@ -46,7 +49,6 @@ const EmployeeList = () => {
// const [currentPage, setCurrentPage] = useState(1);
// const [itemsPerPage] = useState(ITEMS_PER_PAGE);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isEmployeeModalOpen, setIsEmployeeModalOpen] = useState(false);
const [searchText, setSearchText] = useState("");
const [filteredData, setFilteredData] = useState([]);
const [showModal, setShowModal] = useState(false);
@ -66,14 +68,27 @@ const EmployeeList = () => {
const navigate = useNavigate();
const handleSearch = (e) => {
const value = e.target.value.toLowerCase();
setSearchText(value);
/**
* Applies the search filter to a given array of employee data.
* @param {Array} data - The array of employee objects to filter.
* @param {string} text - The search text.
* @returns {Array} The filtered array.
*/
const applySearchFilter = (data, text) => {
if (!text) {
return data;
}
const lowercasedText = text.toLowerCase().trim(); // Ensure search text is trimmed and lowercase
if (!employeeList.length) return;
return data.filter((item) => {
// **IMPROVED FULL NAME CONSTRUCTION**
const firstName = item.firstName || "";
const middleName = item.middleName || "";
const lastName = item.lastName || "";
// Join parts, then trim any excess spaces if a middle name is missing
const fullName = `${firstName} ${middleName} ${lastName}`.toLowerCase().trim().replace(/\s+/g, ' ');
const results = employeeList.filter((item) => {
const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
const email = item.email ? item.email.toLowerCase() : "";
const phoneNumber = item.phoneNumber ? item.phoneNumber.toLowerCase() : "";
const jobRole = item.jobRole ? item.jobRole.toLowerCase() : "";
@ -85,8 +100,12 @@ const EmployeeList = () => {
jobRole.includes(value)
);
});
};
setFilteredData(results);
const handleSearch = (e) => {
const value = e.target.value;
setSearchText(value);
setCurrentPage(1);
};
@ -108,11 +127,11 @@ const EmployeeList = () => {
modalElement.classList.remove("show");
modalElement.style.display = "none";
document.body.classList.remove("modal-open");
document.querySelector(".modal-backdrop").remove();
document.querySelector(".modal-backdrop")?.remove();
}
setShowModal(false);
clearCacheKey("employeeProfile");
recallEmployeeData(showInactive);
recallEmployeeData(showInactive, showAllEmployees ? null : selectedProjectId); // Use selectedProjectId here
};
const handleShow = () => setShowModal(true);
const handleClose = () => setShowModal( false );
@ -133,32 +152,9 @@ const EmployeeList = () => {
setEmployeeList(sorted);
setFilteredData(sorted);
}
}, [loading, employees, selectedProject, showAllEmployees]);
}, [loading, employees, selectedProjectId, showAllEmployees]);
// const suspendEmployee = (id) => {
// setemployeeLodaing(true);
// EmployeeRepository.deleteEmployee(id)
// .then((response) => {
// showToast("Employee deleted successfully.", "success");
// clearCacheKey("employeeListByProject");
// clearCacheKey("allEmployeeList");
// clearCacheKey("allInactiveEmployeeList");
// clearCacheKey("employeeProfile");
// setEmployeeList([]);
// recallEmployeeData(showInactive);
// setemployeeLodaing(false);
// setIsDeleteModalOpen(false);
// })
// .catch((error) => {
// const message =
// error.response?.data?.message ||
// error.message ||
// "An unexpected error occurred";
// showToast(message, "error");
// setemployeeLodaing(false);
// setIsDeleteModalOpen(false);
// });
// };
const handleConfigData = (config) => {
setModelConfig(config);
@ -194,7 +190,7 @@ const EmployeeList = () => {
const handleToggle = (e) => {
setShowInactive(e.target.checked);
recallEmployeeData(e.target.checked);
recallEmployeeData(e.target.checked, showAllEmployees ? null : selectedProjectId); // Use selectedProjectId here
};
const handleAllEmployeesToggle = (e) => {
@ -207,8 +203,6 @@ const handleAllEmployeesToggle = (e) => {
}
};
const handleEmployeeModel = (id) => {
setSelecedEmployeeId(id);
setShowModal(true);
@ -219,24 +213,19 @@ const handleAllEmployeesToggle = (e) => {
setIsDeleteModalOpen(true);
};
const handleProjectSelection = (e) => {
const newProjectId = e.target.value;
setSelectedProject(newProjectId);
if (newProjectId) {
setShowAllEmployees(false);
}
};
useEffect(() => {
setSelectedProject(selectedProjectId || "");
}, [selectedProjectId]);
if (!showAllEmployees) {
recallEmployeeData(showInactive, selectedProjectId);
}
}, [selectedProjectId, showInactive, showAllEmployees, recallEmployeeData]);
const handler = useCallback(
(msg) => {
if(employees.some((item) => item.id == msg.employeeId)){
setEmployeeList([]);
recallEmployeeData(showInactive);
recallEmployeeData(showInactive, showAllEmployees ? null : selectedProjectId); // Use selectedProjectId here
}
},[employees]
},[employees, showInactive, showAllEmployees, selectedProjectId] // Add all relevant dependencies
);
useEffect(() => {
@ -288,7 +277,7 @@ const handleAllEmployeesToggle = (e) => {
</div>
)}
<div className="container-xxl flex-grow-1 container-p-y">
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
@ -343,7 +332,7 @@ const handleAllEmployeesToggle = (e) => {
{/* Right side: Search + Export + Add Employee */}
<div className="d-flex flex-wrap align-items-center justify-content-end gap-3 flex-grow-1">
{/* Search */}
{/* Search Input - ALWAYS ENABLED */}
<div className="dataTables_filter">
<label className="mb-0">
<input
@ -392,7 +381,7 @@ const handleAllEmployeesToggle = (e) => {
</ul>
</div>
{/* Add Employee */}
{/* Add Employee Button */}
{Manage_Employee && (
<button
className="btn btn-sm btn-primary"
@ -406,7 +395,6 @@ const handleAllEmployeesToggle = (e) => {
</div>
</div>
<table
className="datatables-users table border-top dataTable no-footer dtr-column text-nowrap"
id="DataTables_Table_0"
@ -501,7 +489,17 @@ const handleAllEmployeesToggle = (e) => {
</td>
</tr>
)}
{!loading && employeeList?.length === 0 && (
{/* Conditional messages for no data or no search results */}
{!loading && displayData?.length === 0 && searchText && !showAllEmployees ? (
<tr>
<td colSpan={8}>
<small className="muted">
'{searchText}' employee not found
</small>{" "}
</td>
</tr>
) : null}
{!loading && displayData?.length === 0 && (!searchText || showAllEmployees) ? (
<tr>
<td
colSpan={8}
@ -510,72 +508,58 @@ const handleAllEmployeesToggle = (e) => {
No Data Found
</td>
</tr>
)}
{!loading &&
employeeList &&
currentItems.length === 0 &&
employeeList.length !== 0 && (
<tr>
<td colSpan={8}>
<small className="muted">
'{searchText}' employee not found
</small>{" "}
</td>
</tr>
)}
) : null}
{currentItems &&
!loading &&
currentItems.map((item) => (
<tr className="odd" key={item.id}>
<td className="sorting_1" colSpan={2}>
<div className="d-flex justify-content-start align-items-center user-name">
<Avatar
firstName={item.firstName}
lastName={item.lastName}
></Avatar>
<div className="d-flex flex-column">
<a
onClick={() =>
navigate(
`/employee/${item.id}?for=attendance`
)
}
className="text-heading text-truncate cursor-pointer"
>
<span className="fw-normal">
{item.firstName} {item.middleName}{" "}
{item.lastName}
</span>
</a>
</div>
{/* Render current items */}
{currentItems && !loading && currentItems.map((item) => (
<tr className="odd" key={item.id}>
<td className="sorting_1" colSpan={2}>
<div className="d-flex justify-content-start align-items-center user-name">
<Avatar
firstName={item.firstName}
lastName={item.lastName}
></Avatar>
<div className="d-flex flex-column">
<a
onClick={() =>
navigate(
`/employee/${item.id}?for=attendance`
)
}
className="text-heading text-truncate cursor-pointer"
>
<span className="fw-normal">
{item.firstName} {item.middleName}{" "}
{item.lastName}
</span>
</a>
</div>
</td>
<td className="text-start d-none d-sm-table-cell">
{item.email ? (
<span className="text-truncate">
<i className="bx bxs-envelope text-primary me-2"></i>
{item.email}
</span>
) : (
<span className="text-truncate text-italic">
NA
</span>
)}
</td>
<td className="text-start d-none d-sm-table-cell">
</div>
</td>
<td className="text-start d-none d-sm-table-cell">
{item.email ? (
<span className="text-truncate">
<i className="bx bxs-phone-call text-primary me-2"></i>
{item.phoneNumber}
<i className="bx bxs-envelope text-primary me-2"></i>
{item.email}
</span>
</td>
<td className=" d-none d-sm-table-cell text-start">
<span className="text-truncate">
<i className="bx bxs-wrench text-success me-2"></i>
{item.jobRole || "Not Assign Yet"}
) : (
<span className="text-truncate text-italic">
NA
</span>
</td>
)}
</td>
<td className="text-start d-none d-sm-table-cell">
<span className="text-truncate">
<i className="bx bxs-phone-call text-primary me-2"></i>
{item.phoneNumber}
</span>
</td>
<td className=" d-none d-sm-table-cell text-start">
<span className="text-truncate">
<i className="bx bxs-wrench text-success me-2"></i>
{item.jobRole || "Not Assign Yet"}
</span>
</td>
<td className=" d-none d-md-table-cell">
{moment(item.joiningDate)?.format("DD-MMM-YYYY")}
@ -703,7 +687,6 @@ const handleAllEmployeesToggle = (e) => {
</ul>
</nav>
)}
</div>
</div>
</div>

View File

@ -123,7 +123,7 @@ const EmployeeProfile = () => {
</div>
</div>
)}
<div className="container-xxl flex-grow-1 container-p-y">
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },

View File

@ -91,7 +91,7 @@ useEffect(() => {
)}
<div className="container-xxl flex-grow-1 container-p-y">
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },

View File

@ -78,14 +78,10 @@ const ProjectDetails = () => {
return (
<div className="row">
<div className="col-xl-4 col-lg-5 col-md-5 mt-5">
{/* About User */}
<AboutProject data={projects_Details}></AboutProject>
{/* About User */}
<AboutProject ></AboutProject>
</div>
<div className="col-xl-4 col-lg-5 col-md-5 mt-5">
{/* Profile Overview */}
<ProjectOverview project={projectId} />
{/* Profile Overview */}
</div>
</div>
);
@ -95,7 +91,7 @@ const ProjectDetails = () => {
<div className="row">
<div className="col-lg-12 col-xl-12">
{/* Teams */}
<Teams project={projects_Details}></Teams>
<Teams ></Teams>
{/* Teams */}
</div>
</div>
@ -135,26 +131,12 @@ const ProjectDetails = () => {
useEffect(() => {
dispatch(setProjectId(projectId));
// setProject(projects_Details);
// setProjectDetails(projects_Details);
}, [projects_Details, projectId]);
const handler = useCallback(
(msg) => {
if (msg.keyword === "Update_Project" && project.id === msg.response.id) {
// clearCacheKey("projectInfo")
// ProjectRepository.getProjectByprojectId(projectId)
// .then((response) => {
// setProjectDetails(response.data);
// setProject(response.data);
// cacheData("projectInfo", { projectId, data: response.data });
// setLoading(false);
// })
// .catch((error) => {
// console.error(error);
// setError("Failed to fetch data.");
// setLoading(false);
// });
refetch()
}
},
@ -168,7 +150,7 @@ const ProjectDetails = () => {
return (
<>
{}
<div className="container-xxl flex-grow-1 container-p-y">
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },

View File

@ -4,7 +4,6 @@ import ManageProjectInfo from "../../components/Project/ManageProjectInfo";
import Breadcrumb from "../../components/common/Breadcrumb";
import ProjectRepository from "../../repositories/ProjectRepository";
import { useProjects, useCreateProject } from "../../hooks/useProjects";
import { useDispatch } from "react-redux";
import showToast from "../../services/toastService";
// import {
// getCachedData,
@ -32,7 +31,6 @@ const ProjectList = () => {
const [HasManageProject, setHasManageProject] = useState(
HasManageProjectPermission
);
const dispatch = useDispatch();
const { mutate: createProject } = useCreateProject({
onSuccessCallback: () => {
setShowModal(false);
@ -82,24 +80,7 @@ const ProjectList = () => {
}
}, [loginUser, HasManageProjectPermission]);
// const handleSubmitForm = (newProject, setloading, reset) => {
// ProjectRepository.manageProject(newProject)
// .then((response) => {
// const cachedProjects = getCachedData("projectslist") || [];
// const updatedProjects = [...cachedProjects, response.data];
// cacheData("projectslist", updatedProjects);
// setProjectList((prev) => [...prev, response.data]);
// setloading(false);
// reset();
// sortingProject(getCachedData("projectslist"));
// showToast("Project Created successfully.", "success");
// setShowModal(false);
// })
// .catch((error) => {
// showToast(error.message, "error");
// setShowModal(false);
// });
// };
const handleSubmitForm = (newProject, setloading, reset) => {
setloading(true);
@ -159,219 +140,227 @@ const ProjectList = () => {
</GlobalModel>
)}
<div className="container-xxl flex-grow-1 container-p-y">
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Projects", link: null },
]}
/>
<div className="card cursor-pointer mb-5">
<div className="card-body p-2 pb-1">
<div className="d-flex flex-wrap justify-content-between align-items-start">
<div className="d-flex flex-wrap align-items-start">
<div className="flex-grow-1 me-2 mb-2">
<input
type="search"
className="form-control form-control-sm"
placeholder="Search projects..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setCurrentPage(1);
}}
/>
</div>
<div className="d-flex flex-wrap justify-content-between align-items-start mb-4">
<div className="d-flex flex-wrap align-items-start">
<div className="flex-grow-1 me-2 mb-2">
<input
type="search"
className="form-control form-control-sm"
placeholder="Search projects..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setCurrentPage(1);
}}
/>
<div className="d-flex gap-2 mb-2">
<button
type="button"
className={`btn btn-sm p-1 ${
!listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(false)}
data-bs-toggle="tooltip"
data-bs-custom-class="tooltip"
title="Card View"
>
<i className="bx bx-grid-alt fs-5"></i>
</button>
<button
type="button"
className={`btn btn-sm p-1 ${
listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(true)}
data-bs-toggle="tooltip"
data-bs-custom-class="tooltip"
title="List View"
>
<i className="bx bx-list-ul fs-5"></i>
</button>
</div>
<div className="dropdown ms-3 mt-1">
<a
className="dropdown-toggle hide-arrow cursor-pointer p-1 mt-3 "
data-bs-toggle="dropdown"
aria-expanded="false"
data-bs-custom-class="tooltip"
title="Filter"
>
<i className="fa-solid fa-filter fs-4"></i>
</a>
<ul className="dropdown-menu p-2 text-capitalize">
{[
{
id: "b74da4c2-d07e-46f2-9919-e75e49b12731",
label: "Active",
},
{
id: "603e994b-a27f-4e5d-a251-f3d69b0498ba",
label: "On Hold",
},
{
id: "ef1c356e-0fe0-42df-a5d3-8daee355492d",
label: "Inactive",
},
{
id: "33deaef9-9af1-4f2a-b443-681ea0d04f81",
label: "Completed",
},
].map(({ id, label }) => (
<li key={id}>
<div className="form-check">
<input
className="form-check-input "
type="checkbox"
checked={selectedStatuses.includes(id)}
onChange={() => handleStatusChange(id)}
/>
<label className="form-check-label">{label}</label>
</div>
</li>
))}
</ul>
</div>
</div>
<div>
<button
type="button"
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip"
title="Add New Project"
className={`p-1 me-2 bg-primary rounded-circle ${
!HasManageProject && "d-none"
}`}
onClick={handleShow}
>
<i className="bx bx-plus fs-4 text-white"></i>
</button>
</div>
</div>
<div className="d-flex gap-2 mb-2">
<button
type="button"
className={`btn btn-sm ${
!listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(false)}
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip"
title="Card View"
>
<i className="bx bx-grid-alt bx-sm"></i>
</button>
<button
type="button"
className={`btn btn-sm ${
listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(true)}
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip"
title="List View"
>
<i className="bx bx-list-ul bx-sm"></i>
</button>
</div>
<div className="dropdown ms-3">
<a
className="dropdown-toggle hide-arrow cursor-pointer"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i className="bx bx-filter bx-lg"></i>
</a>
<ul className="dropdown-menu p-2 text-capitalize">
{[
{
id: "b74da4c2-d07e-46f2-9919-e75e49b12731",
label: "Active",
},
{
id: "603e994b-a27f-4e5d-a251-f3d69b0498ba",
label: "On Hold",
},
{
id: "ef1c356e-0fe0-42df-a5d3-8daee355492d",
label: "Inactive",
},
{
id: "33deaef9-9af1-4f2a-b443-681ea0d04f81",
label: "Completed",
},
].map(({ id, label }) => (
<li key={id}>
<div className="form-check">
<input
className="form-check-input "
type="checkbox"
checked={selectedStatuses.includes(id)}
onChange={() => handleStatusChange(id)}
/>
<label className="form-check-label">{label}</label>
</div>
</li>
))}
</ul>
</div>
</div>
<div>
<button
type="button"
className={`btn btn-sm btn-primary ${
!HasManageProject && "d-none"
}`}
onClick={handleShow}
>
<i className="bx bx-plus-circle me-2"></i>
Create New Project
</button>
</div>
</div>
{loading && <p className="text-center">Loading...</p>}
{!loading && filteredProjects.length === 0 && !listView && (
<p className="text-center text-muted">No projects found.</p>
)}
<div className="row">
{listView ? (
<div className="table-responsive text-nowrap py-2 ">
<table className="table px-2">
<thead>
<tr>
<th className="text-start" colSpan={5}>
Project Name
</th>
<th className="mx-2 text-start">Contact Person</th>
<th className="mx-2">START DATE</th>
<th className="mx-2">DEADLINE</th>
<th className="mx-2">Task</th>
<th className="mx-2">Progress</th>
<th className="mx-2">
<div className="dropdown">
<a
className="dropdown-toggle hide-arrow cursor-pointer"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Status <i className="bx bx-filter bx-sm"></i>
</a>
<ul className="dropdown-menu p-2 text-capitalize">
{[
{
id: "b74da4c2-d07e-46f2-9919-e75e49b12731",
label: "Active",
},
{
id: "603e994b-a27f-4e5d-a251-f3d69b0498ba",
label: "On Hold",
},
{
id: "ef1c356e-0fe0-42df-a5d3-8daee355492d",
label: "Inactive",
},
{
id: "33deaef9-9af1-4f2a-b443-681ea0d04f81",
label: "Completed",
},
].map(({ id, label }) => (
<li key={id}>
<div className="form-check">
<input
className="form-check-input "
type="checkbox"
checked={selectedStatuses.includes(id)}
onChange={() => handleStatusChange(id)}
/>
<label className="form-check-label">
{label}
</label>
</div>
</li>
))}
</ul>
</div>
</th>
<th
className={`mx-2 ${
HasManageProject ? "d-sm-table-cell" : "d-none"
}`}
>
Action
</th>
</tr>
</thead>
<tbody className="table-border-bottom-0 overflow-auto ">
{currentItems.length === 0 ? (
{listView ? (
<div className="card cursor-pointer">
<div className="card-body p-2">
<div className="table-responsive text-nowrap py-2 ">
<table className="table m-3">
<thead>
<tr>
<td colSpan="12" className="text-center py-4">
No projects found
</td>
<th className="text-start" colSpan={5}>
Project Name
</th>
<th className="mx-2 text-start">Contact Person</th>
<th className="mx-2">START DATE</th>
<th className="mx-2">DEADLINE</th>
<th className="mx-2">Task</th>
<th className="mx-2">Progress</th>
<th className="mx-2">
<div className="dropdown">
<a
className="dropdown-toggle hide-arrow cursor-pointer"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Status <i className="bx bx-filter bx-sm"></i>
</a>
<ul className="dropdown-menu p-2 text-capitalize">
{[
{
id: "b74da4c2-d07e-46f2-9919-e75e49b12731",
label: "Active",
},
{
id: "603e994b-a27f-4e5d-a251-f3d69b0498ba",
label: "On Hold",
},
{
id: "ef1c356e-0fe0-42df-a5d3-8daee355492d",
label: "Inactive",
},
{
id: "33deaef9-9af1-4f2a-b443-681ea0d04f81",
label: "Completed",
},
].map(({ id, label }) => (
<li key={id}>
<div className="form-check">
<input
className="form-check-input "
type="checkbox"
checked={selectedStatuses.includes(id)}
onChange={() => handleStatusChange(id)}
/>
<label className="form-check-label">
{label}
</label>
</div>
</li>
))}
</ul>
</div>
</th>
<th
className={`mx-2 ${
HasManageProject ? "d-sm-table-cell" : "d-none"
}`}
>
Action
</th>
</tr>
) : (
currentItems.map((project) => (
<ProjectListView
key={project.id}
projectData={project}
recall={sortingProject}
/>
))
)}
</tbody>
</table>
</div>
) : (
currentItems.map((project) => (
</thead>
<tbody className="table-border-bottom-0 overflow-auto ">
{currentItems.length === 0 ? (
<tr>
<td colSpan="12" className="text-center py-4">
No projects found
</td>
</tr>
) : (
currentItems.map((project) => (
<ProjectListView
key={project.id}
projectData={project}
recall={sortingProject}
/>
))
)}
</tbody>
</table>
</div>{" "}
</div>{" "}
</div>
) : (
<div className="row">
{currentItems.map((project) => (
<ProjectCard
key={project.id}
projectData={project}
recall={sortingProject}
/>
))
)}
</div>
))}
</div>
)}
{!loading && totalPages > 1 && (
<nav>

View File

@ -140,14 +140,14 @@ const ProjectListView = ({ projectData, recall }) => {
<tr className={`py-8 ${isPending ? "bg-light opacity-50 pointer-events-none" : ""} `}>
<td className="text-start" colSpan={5}>
<strong
<span
className="text-primary cursor-pointer"
onClick={() => navigate(`/projects/${projectInfo.id}`)}
>
{projectInfo.shortName
? `${projectInfo.name} (${projectInfo.shortName})`
: projectInfo.name}
</strong>
</span>
</td>
<td className="text-start small">{projectInfo.contactPerson}</td>
<td className="small text-center">

View File

@ -1,18 +1,20 @@
import { api } from "../utils/axiosClient";
const AuthRepository = {
login: (data) => api.post("/api/auth/login", data),
refreshToken: (data) => api.post("/api/auth/refresh-token", data),
// Public routes (no auth token required)
login: (data) => api.postPublic("/api/auth/login", data),
refreshToken: (data) => api.postPublic("/api/auth/refresh-token", data),
forgotPassword: (data) => api.postPublic("/api/auth/forgot-password", data),
resetPassword: (data) => api.postPublic("/api/auth/reset-password", data),
sendOTP: (data) => api.postPublic("/api/auth/send-otp", data),
verifyOTP: (data) => api.postPublic("/api/auth/login-otp", data),
register: (data) => api.postPublic("/api/auth/register", data),
sendMail: (data) => api.postPublic("/api/auth/sendmail", data),
// Protected routes (require auth token)
logout: (data) => api.post("/api/auth/logout", data),
profile: () => api.get(`/api/user/profile`),
register: (data) => api.post("api/auth/register", data),
resetPassword: (data) => api.post("/api/auth/reset-password", data),
forgotPassword: (data) => api.post("/api/auth/forgot-password", data),
sendMail: (data) => api.post("/api/auth/sendmail", data),
changepassword: ( data ) => api.post( "/api/auth/change-password", data ),
sendOTP: ( data ) => api.post( 'api/auth/send-otp', data ),
verifyOTP:(data)=>api.post("api/auth/login-otp",data)
profile: () => api.get("/api/user/profile"),
changepassword: (data) => api.post("/api/auth/change-password", data),
};
export default AuthRepository;

View File

@ -32,4 +32,7 @@ export const DirectoryRepository = {
UpdateNote: (id, data) => api.put(`/api/directory/note/${id}`, data),
DeleteNote: (id, isActive) =>
api.delete(`/api/directory/note/${id}?active=${isActive}`),
GetNotes: (pageSize, pageNumber) =>
api.get(`/api/directory/notes?pageSize=${pageSize}&pageNumber=${pageNumber}`),
};

View File

@ -23,7 +23,11 @@ const ProjectRepository = {
deleteProject: ( id ) => api.delete( `/projects/${ id }` ),
getProjectsByEmployee: ( id ) => api.get( `/api/project/assigned-projects/${ id }` ),
updateProjectsByEmployee:(id,data)=>api.post(`/api/project/assign-projects/${id}`,data),
projectNameList:()=>api.get("/api/project/list/basic")
projectNameList: () => api.get( "/api/project/list/basic" ),
getProjectDetails:(id)=>api.get(`/api/project/details/${id}`),
getProjectInfraByproject: ( id ) => api.get( `/api/project/infra-details/${ id }` ),
getProjectTasksByWorkArea:(id)=>api.get(`/api/project/tasks/${id}`)
};
export const TasksRepository = {

View File

@ -72,7 +72,7 @@ export function startSignalR(loggedUser) {
cacheData("hasReceived", false);
eventBus.emit("assign_project_one", data);
} catch (e) {
console.error("Error in cacheData:", e);
// console.error("Error in cacheData:", e);
}
}
eventBus.emit("assign_project_all", data);
@ -107,9 +107,7 @@ export function startSignalR(loggedUser) {
});
connection
.start()
.then(() => console.log("SignalR connected"))
.catch((err) => console.error("SignalR error:", err));
.start();
}
export function stopSignalR() {

View File

@ -7,18 +7,22 @@ import { BASE_URL } from "./constants";
const base_Url = BASE_URL
export const axiosClient = axios.create({
baseURL: base_Url, // Your Web API URL
withCredentials: false, // Required if the API uses cookies
baseURL: base_Url,
withCredentials: false,
headers: {
"Content-Type": "application/json", // Specify the content type
"Content-Type": "application/json",
},
});
// Auto retry failed requests (e.g., network issues)
axiosRetry(axiosClient, { retries: 3 });
// Request interceptor to add Bearer token
// Request Interceptor Add Bearer token if required
axiosClient.interceptors.request.use(
async (config) => {
if (config.authRequired) {
const requiresAuth = config.authRequired !== false; // default to true
if (requiresAuth) {
const token = localStorage.getItem("jwtToken");
if (token) {
config.headers["Authorization"] = `Bearer ${token}`;
@ -27,25 +31,24 @@ axiosClient.interceptors.request.use(
config._retry = false;
}
}
return config;
},
(error) => Promise.reject(error)
);
// // Response interceptor to handle responses globally (optional)
// Add an interceptor to handle expired tokens
// 🔄 Response Interceptor Handle 401, refresh token, etc.
axiosClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Prevent infinite loop
if (!originalRequest || originalRequest._retry) {
// Skip retry for public requests or already retried ones
if (!originalRequest || originalRequest._retry || originalRequest.authRequired === false) {
return Promise.reject(error);
}
// Only show one toast per request
// Avoid showing multiple toasts
if (!originalRequest._toastShown) {
originalRequest._toastShown = true;
@ -61,7 +64,6 @@ axiosClient.interceptors.response.use(
const isRefreshRequest = error.config.url.includes("refresh-token");
if (status === 401 && !isRefreshRequest) {
// Mark as retried to avoid loops
originalRequest._retry = true;
const refreshToken = localStorage.getItem("refreshToken");
@ -74,7 +76,7 @@ axiosClient.interceptors.response.use(
stopSignalR();
try {
// Refresh token
// Refresh token call
const res = await axiosClient.post("/api/Auth/refresh-token", {
token: localStorage.getItem("jwtToken"),
refreshToken,
@ -82,16 +84,14 @@ axiosClient.interceptors.response.use(
const { token, refreshToken: newRefreshToken } = res.data.data;
// Save new tokens
// Save updated tokens
localStorage.setItem("jwtToken", token);
localStorage.setItem("refreshToken", newRefreshToken);
startSignalR()
// Set Authorization header
originalRequest.headers["Authorization"] = `Bearer ${token}`;
// Optional: Instead of retrying, you may choose to reload app or go to home
return axiosClient(originalRequest); // <== only retry once
return axiosClient(originalRequest);
} catch (refreshError) {
redirectToLogin();
return Promise.reject(refreshError);
@ -101,11 +101,12 @@ axiosClient.interceptors.response.use(
showToast("An unknown error occurred.", "error");
}
}
return Promise.reject(error);
}
);
// Generic API Call
// Generic API function
const apiRequest = async (method, url, data = {}, config = {}) => {
try {
const response = await axiosClient({
@ -121,15 +122,16 @@ const apiRequest = async (method, url, data = {}, config = {}) => {
}
};
// Exported API wrapper
export const api = {
// For public routes like login, set authRequired: false
// Public routes (no token required)
postPublic: (url, data = {}, customHeaders = {}) =>
apiRequest("post", url, data, {
headers: { ...customHeaders },
authRequired: false,
}),
// For protected routes, authRequired defaults to true
// Authenticated routes
get: (url, params = {}, customHeaders = {}) =>
apiRequest("get", url, params, {
headers: { ...customHeaders },
@ -154,7 +156,8 @@ export const api = {
authRequired: true,
}),
};
//export default axiosClient;
// Redirect helper
function redirectToLogin() {
window.location.href = "/auth/login";
}
}

View File

@ -11,9 +11,9 @@ export const VIEW_PROJECTS = "6ea44136-987e-44ba-9e5d-1cf8f5837ebc"
export const MANAGE_EMPLOYEES = "a97d366a-c2bb-448d-be93-402bd2324566"
export const MANAGE_PROJECT_INFRA = "f2aee20a-b754-4537-8166-f9507b44585b"
export const MANAGE_PROJECT_INFRA = "cf2825ad-453b-46aa-91d9-27c124d63373"
export const VIEW_PROJECT_INFRA = "c7b68e33-72f0-474f-bd96-77636427ecc8"
export const VIEW_PROJECT_INFRA = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"
export const REGULARIZE_ATTENDANCE ="57802c4a-00aa-4a1f-a048-fd2f70dd44b6"