React Query Integration for Server State Sync in Clinet #245
@ -213,3 +213,8 @@
|
||||
.ql-editor {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
/* Remove Table Header Top Line */
|
||||
thead tr {
|
||||
border-top: 1px solid white;
|
||||
}
|
||||
|
1
public/assets/vendor/css/core.css
vendored
1
public/assets/vendor/css/core.css
vendored
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 ">
|
||||
|
@ -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;
|
@ -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">
|
||||
|
@ -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",
|
||||
|
@ -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%" }}>
|
||||
|
256
src/components/Directory/NoteCardDirectoryEditable.jsx
Normal file
256
src/components/Directory/NoteCardDirectoryEditable.jsx
Normal 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>
|
||||
<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;
|
176
src/components/Directory/NotesCardViewDirectory.jsx
Normal file
176
src/components/Directory/NotesCardViewDirectory.jsx
Normal 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;
|
@ -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"}
|
||||
|
@ -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">
|
||||
|
@ -222,24 +222,24 @@ 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() || "",
|
||||
}
|
||||
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
|
||||
);
|
||||
setCurrentAddressLength(currentEmployee?.currentAddress?.length || 0);
|
||||
@ -274,378 +274,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={isloading}
|
||||
>
|
||||
{isloading
|
||||
? "Please Wait..."
|
||||
: employeeId
|
||||
? "Update"
|
||||
: "Create"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="manage employee"
|
||||
type="reset"
|
||||
className="btn btn-sm btn-primary ms-2"
|
||||
disabled={isloading}
|
||||
>
|
||||
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>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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) => {
|
||||
@ -158,7 +159,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 +176,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 +200,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 +211,7 @@ const Header = () => {
|
||||
>
|
||||
{project?.name}{" "}
|
||||
{project?.shortName ? (
|
||||
<span className="text-primary fw-semibold">
|
||||
<span className="text-primary fw-semibold ">
|
||||
{" "}
|
||||
({project?.shortName})
|
||||
</span>
|
||||
@ -270,7 +271,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">
|
||||
|
@ -20,16 +20,17 @@ const taskSchema = z.object({
|
||||
|
||||
const defaultModel = {
|
||||
id: null,
|
||||
buildingID: "0",
|
||||
floorId: "0",
|
||||
workAreaId: "0",
|
||||
activityID: null,
|
||||
workCategoryId: "",
|
||||
buildingID: "", // Changed from "0"
|
||||
floorId: "", // Changed from "0"
|
||||
workAreaId: "", // Changed from "0"
|
||||
activityID: "", // Changed from null
|
||||
workCategoryId: "", // Kept as empty
|
||||
plannedWork: 0,
|
||||
completedWork: 0,
|
||||
comment:""
|
||||
comment: ""
|
||||
};
|
||||
|
||||
|
||||
const TaskModel = ({
|
||||
project,
|
||||
onSubmit,
|
||||
@ -86,7 +87,7 @@ const TaskModel = ({
|
||||
reset((prev) => ({
|
||||
...prev,
|
||||
floorId: value,
|
||||
workAreaId: 0,
|
||||
workAreaId: "",
|
||||
activityID: "",
|
||||
workCategoryId: categoryData?.[0]?.id?.toString() ?? "",
|
||||
}));
|
||||
@ -193,7 +194,7 @@ const TaskModel = ({
|
||||
{...register("buildingID")}
|
||||
onChange={handleBuildingChange}
|
||||
>
|
||||
<option value="0">Select Building</option>
|
||||
<option value="">Select Building</option>
|
||||
{project.buildings
|
||||
?.filter((building) => building?.name) // Ensure valid name
|
||||
?.sort((a, b) => a.name?.localeCompare(b.name))
|
||||
@ -225,7 +226,7 @@ const TaskModel = ({
|
||||
{...register("floorId")}
|
||||
onChange={handleFloorChange}
|
||||
>
|
||||
<option value="0">Select Floor</option>
|
||||
<option value="">Select Floor</option>
|
||||
{selectedBuilding.floors
|
||||
?.filter(
|
||||
(floor) =>
|
||||
@ -261,7 +262,7 @@ const TaskModel = ({
|
||||
{...register("workAreaId")}
|
||||
onChange={handleWorkAreaChange}
|
||||
>
|
||||
<option value="0">Select Work Area</option>
|
||||
<option value="">Select Work Area</option>
|
||||
{selectedFloor.workAreas
|
||||
?.filter((workArea) => workArea?.areaName)
|
||||
?.sort((a, b) => a.areaName?.localeCompare(b.areaName))
|
||||
@ -441,4 +442,4 @@ const TaskModel = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskModel;
|
||||
export default TaskModel;
|
@ -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}>
|
||||
|
@ -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 && (
|
||||
|
@ -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" },
|
||||
|
@ -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" },
|
||||
|
@ -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/>
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -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;
|
@ -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
|
||||
|
@ -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 ? (
|
||||
|
@ -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(); // Use optional chaining for safety
|
||||
}
|
||||
setShowModal(false);
|
||||
clearCacheKey("employeeProfile");
|
||||
recallEmployeeData(showInactive);
|
||||
recallEmployeeData(showInactive, showAllEmployees ? null : selectedProjectId); // Use selectedProjectId here
|
||||
};
|
||||
const handleShow = () => setShowModal(true);
|
||||
const handleClose = () => setShowModal( false );
|
||||
@ -194,7 +213,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 +226,6 @@ const handleAllEmployeesToggle = (e) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleEmployeeModel = (id) => {
|
||||
setSelecedEmployeeId(id);
|
||||
setShowModal(true);
|
||||
@ -219,24 +236,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 +300,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 +355,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 +404,7 @@ const handleAllEmployeesToggle = (e) => {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Add Employee */}
|
||||
{/* Add Employee Button */}
|
||||
{Manage_Employee && (
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
@ -406,7 +418,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 +512,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 +531,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 +710,6 @@ const handleAllEmployeesToggle = (e) => {
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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" },
|
||||
|
@ -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" },
|
||||
|
@ -150,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" },
|
||||
|
@ -159,219 +159,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>
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
|
@ -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}`),
|
||||
};
|
||||
|
@ -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() {
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user