Compare commits
No commits in common. "3d556a704e122ade81fc64db67eaf74e6430a774" and "a5e7766a2f9b1bb2b1e86e3d3f1b4e89c7b246eb" have entirely different histories.
3d556a704e
...
a5e7766a2f
@ -1,11 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" lang="en"
|
<html lang="en">
|
||||||
class="light-style layout-navbar-fixed layout-menu-fixed layout-compact"
|
|
||||||
dir="ltr"
|
|
||||||
data-theme="theme-default"
|
|
||||||
data-assets-path="/assets/"
|
|
||||||
data-template="vertical-menu-template"
|
|
||||||
data-style="light">
|
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|||||||
@ -213,8 +213,3 @@
|
|||||||
.ql-editor {
|
.ql-editor {
|
||||||
max-height: 200px;
|
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,7 +4978,6 @@ fieldset:disabled .btn {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x);
|
padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x);
|
||||||
color: var(--bs-card-color);
|
color: var(--bs-card-color);
|
||||||
word-break: break-word;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
|
|||||||
@ -143,7 +143,7 @@ const Attendance = ({
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{!loading && filteredData.length > 20 && (
|
{!loading > 20 && (
|
||||||
<nav aria-label="Page ">
|
<nav aria-label="Page ">
|
||||||
<ul className="pagination pagination-sm justify-content-end py-1">
|
<ul className="pagination pagination-sm justify-content-end py-1">
|
||||||
<li
|
<li
|
||||||
|
|||||||
@ -334,11 +334,11 @@ const AttendanceLog = ({
|
|||||||
{!loading && !isRefreshing && data.length === 0 && (
|
{!loading && !isRefreshing && data.length === 0 && (
|
||||||
<span>No employee logs</span>
|
<span>No employee logs</span>
|
||||||
)}
|
)}
|
||||||
{/* {error && !loading && !isRefreshing && (
|
{error && !loading && !isRefreshing && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6}>{error}</td>
|
<td colSpan={6}>{error}</td>
|
||||||
</tr>
|
</tr>
|
||||||
)} */}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!loading && !isRefreshing && processedData.length > 10 && (
|
{!loading && !isRefreshing && processedData.length > 10 && (
|
||||||
<nav aria-label="Page ">
|
<nav aria-label="Page ">
|
||||||
|
|||||||
@ -44,7 +44,7 @@ const Regularization = ({ handleRequest }) => {
|
|||||||
|
|
||||||
const { currentPage, totalPages, currentItems, paginate } = usePagination(
|
const { currentPage, totalPages, currentItems, paginate } = usePagination(
|
||||||
filteredData,
|
filteredData,
|
||||||
20
|
10
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
eventBus.on("regularization", handler);
|
eventBus.on("regularization", handler);
|
||||||
@ -67,8 +67,8 @@ const Regularization = ({ handleRequest }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="table-responsive text-nowrap pb-4"
|
className="table-responsive text-nowrap"
|
||||||
|
style={{ minHeight: "300px" }}
|
||||||
>
|
>
|
||||||
<table className="table mb-0">
|
<table className="table mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
@ -85,11 +85,11 @@ const Regularization = ({ handleRequest }) => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{/* {loading && (
|
{loading && (
|
||||||
<td colSpan={6} className="text-center py-5">
|
<td colSpan={6} className="text-center py-5">
|
||||||
Loading...
|
Loading...
|
||||||
</td>
|
</td>
|
||||||
)} */}
|
)}
|
||||||
|
|
||||||
{!loading &&
|
{!loading &&
|
||||||
(regularizes?.length > 0 ? (
|
(regularizes?.length > 0 ? (
|
||||||
@ -145,9 +145,9 @@ const Regularization = ({ handleRequest }) => {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{!loading && totalPages > 1 && (
|
{!loading > 10 && (
|
||||||
<nav aria-label="Page ">
|
<nav aria-label="Page ">
|
||||||
<ul className="pagination pagination-sm justify-content-end py-1 mt-3">
|
<ul className="pagination pagination-sm justify-content-end py-1">
|
||||||
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
|
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
|
||||||
<button
|
<button
|
||||||
className="page-link btn-xs"
|
className="page-link btn-xs"
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const Dashboard = () => {
|
|||||||
const { tasksCardData } = useDashboardTasksCardData();
|
const { tasksCardData } = useDashboardTasksCardData();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-fluid mt-3">
|
<div className="container-xxl flex-grow-1 container-p-y">
|
||||||
<div className="row gy-4">
|
<div className="row gy-4">
|
||||||
{/* Projects Card */}
|
{/* Projects Card */}
|
||||||
<div className="col-sm-6 col-lg-4">
|
<div className="col-sm-6 col-lg-4">
|
||||||
|
|||||||
@ -69,7 +69,6 @@ const ProjectProgressChart = () => {
|
|||||||
);
|
);
|
||||||
const lineChartCategoriesDates = sortedDashboardData.map((d) =>
|
const lineChartCategoriesDates = sortedDashboardData.map((d) =>
|
||||||
new Date(d.date).toLocaleDateString("en-US", {
|
new Date(d.date).toLocaleDateString("en-US", {
|
||||||
weekday:"short",
|
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
|
|||||||
@ -88,14 +88,8 @@ const ListViewDirectory = ({
|
|||||||
{contact.organization}
|
{contact.organization}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* <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%" }}>
|
<td className="px-2" style={{ width: "10%" }}>
|
||||||
<span className="text-truncate">
|
<span className="badge badge-outline-secondary">
|
||||||
{contact?.contactCategory?.name || "Other"}
|
{contact?.contactCategory?.name || "Other"}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -1,256 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,176 +0,0 @@
|
|||||||
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">
|
<div className="d-flex justify-content-end">
|
||||||
<span
|
<span
|
||||||
className={`btn btn-sm ${addNote ? "btn-secondary" : "btn-primary"}`}
|
className={`btn btn-sm ${addNote ? "btn-danger" : "btn-primary"}`}
|
||||||
onClick={() => setAddNote(!addNote)}
|
onClick={() => setAddNote(!addNote)}
|
||||||
>
|
>
|
||||||
{addNote ? "Hide Editor" : "Add a Note"}
|
{addNote ? "Hide Editor" : "Add a Note"}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import React from "react";
|
|||||||
const DemoTable = () => {
|
const DemoTable = () => {
|
||||||
return (
|
return (
|
||||||
<div className="content-wrapper">
|
<div className="content-wrapper">
|
||||||
<div className="container-fluid">
|
<div className="container-xxl flex-grow-1 container-p-y">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-datatable table-responsive">
|
<div className="card-datatable table-responsive">
|
||||||
<table className="datatables-basic table border-top">
|
<table className="datatables-basic table border-top">
|
||||||
|
|||||||
@ -172,7 +172,8 @@ const ManageEmployee = ({ employeeId, onClosed }) => {
|
|||||||
.then((response) => {
|
.then((response) => {
|
||||||
cacheData("employeeProfileInfo", data);
|
cacheData("employeeProfileInfo", data);
|
||||||
showToast(
|
showToast(
|
||||||
`Employee details ${data.id == null ? "created" : "updated"
|
`Employee details ${
|
||||||
|
data.id == null ? "created" : "updated"
|
||||||
} successfully.`,
|
} successfully.`,
|
||||||
"success"
|
"success"
|
||||||
);
|
);
|
||||||
@ -238,14 +239,7 @@ const ManageEmployee = ({ employeeId, onClosed }) => {
|
|||||||
<p>Loading Employee Data...</p>
|
<p>Loading Employee Data...</p>
|
||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-sm-5 p-2 position-relative">
|
<form onSubmit={handleSubmit( onSubmit )} className="p-sm-0 p-2">
|
||||||
{/* Cross button */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn-close position-absolute top-0 end-0 m-2"
|
|
||||||
aria-label="Close"
|
|
||||||
onClick={onClosed}
|
|
||||||
></button>
|
|
||||||
<div className="text-center"><p className="fs-6 fw-semibold"> {employee ? "Update Employee" : "Create Employee"}</p></div>
|
<div className="text-center"><p className="fs-6 fw-semibold"> {employee ? "Update Employee" : "Create Employee"}</p></div>
|
||||||
<div className="row mb-3">
|
<div className="row mb-3">
|
||||||
<div className="col-sm-4">
|
<div className="col-sm-4">
|
||||||
|
|||||||
@ -103,8 +103,7 @@ const Header = () => {
|
|||||||
}, [projectNames]);
|
}, [projectNames]);
|
||||||
|
|
||||||
/** Check if current page id project details page */
|
/** 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(
|
const handler = useCallback(
|
||||||
async (data) => {
|
async (data) => {
|
||||||
@ -146,7 +145,7 @@ const Header = () => {
|
|||||||
}, [handler]);
|
}, [handler]);
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className="layout-navbar container-fluid mb-3 navbar navbar-expand-xl navbar-detached align-items-center bg-navbar-theme"
|
className="layout-navbar container-xxl navbar navbar-expand-xl navbar-detached align-items-center bg-navbar-theme"
|
||||||
id="layout-navbar"
|
id="layout-navbar"
|
||||||
>
|
>
|
||||||
<div className="layout-menu-toggle navbar-nav align-items-xl-center me-3 me-xl-0 d-xl-none">
|
<div className="layout-menu-toggle navbar-nav align-items-xl-center me-3 me-xl-0 d-xl-none">
|
||||||
@ -163,11 +162,11 @@ const Header = () => {
|
|||||||
>
|
>
|
||||||
{projectNames?.length > 0 && (
|
{projectNames?.length > 0 && (
|
||||||
<div className=" align-items-center">
|
<div className=" align-items-center">
|
||||||
{(!isProjectPath && !isDirectoryPath) && (
|
{!isProjectPath && (
|
||||||
<>
|
<>
|
||||||
<i
|
<i
|
||||||
className="rounded-circle bx bx-building-house bx-sm-lg bx-md"
|
className="rounded-circle bx bx-building-house"
|
||||||
|
style={{ fontSize: "xx-large" }}
|
||||||
></i>
|
></i>
|
||||||
<div className="btn-group">
|
<div className="btn-group">
|
||||||
<button
|
<button
|
||||||
@ -187,7 +186,7 @@ const Header = () => {
|
|||||||
style={{ overflow: "auto", maxHeight: "300px" }}
|
style={{ overflow: "auto", maxHeight: "300px" }}
|
||||||
>
|
>
|
||||||
{[...projectNames]
|
{[...projectNames]
|
||||||
.sort((a, b) => a?.name?.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map((project) => (
|
.map((project) => (
|
||||||
<li key={project?.id}>
|
<li key={project?.id}>
|
||||||
<button
|
<button
|
||||||
@ -258,7 +257,7 @@ const Header = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="dropdown-shortcuts-item col">
|
<div className="dropdown-shortcuts-item col">
|
||||||
<a
|
<a
|
||||||
onClick={() => navigate(`/projects`)}
|
onClick={() => navigate(`/projectNames`)}
|
||||||
className="text-heading text-truncate cursor-pointer"
|
className="text-heading text-truncate cursor-pointer"
|
||||||
>
|
>
|
||||||
<span className="dropdown-shortcuts-icon rounded-circle mb-3">
|
<span className="dropdown-shortcuts-icon rounded-circle mb-3">
|
||||||
|
|||||||
@ -20,17 +20,16 @@ const taskSchema = z.object({
|
|||||||
|
|
||||||
const defaultModel = {
|
const defaultModel = {
|
||||||
id: null,
|
id: null,
|
||||||
buildingID: "", // Changed from "0"
|
buildingID: "0",
|
||||||
floorId: "", // Changed from "0"
|
floorId: "0",
|
||||||
workAreaId: "", // Changed from "0"
|
workAreaId: "0",
|
||||||
activityID: "", // Changed from null
|
activityID: null,
|
||||||
workCategoryId: "", // Kept as empty
|
workCategoryId: "",
|
||||||
plannedWork: 0,
|
plannedWork: 0,
|
||||||
completedWork: 0,
|
completedWork: 0,
|
||||||
comment:""
|
comment:""
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const TaskModel = ({
|
const TaskModel = ({
|
||||||
project,
|
project,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@ -88,7 +87,7 @@ const TaskModel = ({
|
|||||||
reset((prev) => ({
|
reset((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
floorId: value,
|
floorId: value,
|
||||||
workAreaId: "",
|
workAreaId: 0,
|
||||||
activityID: "",
|
activityID: "",
|
||||||
workCategoryId: categoryData?.[0]?.id?.toString() ?? "",
|
workCategoryId: categoryData?.[0]?.id?.toString() ?? "",
|
||||||
}));
|
}));
|
||||||
@ -195,7 +194,7 @@ const TaskModel = ({
|
|||||||
{...register("buildingID")}
|
{...register("buildingID")}
|
||||||
onChange={handleBuildingChange}
|
onChange={handleBuildingChange}
|
||||||
>
|
>
|
||||||
<option value="">Select Building</option>
|
<option value="0">Select Building</option>
|
||||||
{project.buildings
|
{project.buildings
|
||||||
?.filter((building) => building?.name) // Ensure valid name
|
?.filter((building) => building?.name) // Ensure valid name
|
||||||
?.sort((a, b) => a.name?.localeCompare(b.name))
|
?.sort((a, b) => a.name?.localeCompare(b.name))
|
||||||
@ -227,7 +226,7 @@ const TaskModel = ({
|
|||||||
{...register("floorId")}
|
{...register("floorId")}
|
||||||
onChange={handleFloorChange}
|
onChange={handleFloorChange}
|
||||||
>
|
>
|
||||||
<option value="">Select Floor</option>
|
<option value="0">Select Floor</option>
|
||||||
{selectedBuilding.floors
|
{selectedBuilding.floors
|
||||||
?.filter(
|
?.filter(
|
||||||
(floor) =>
|
(floor) =>
|
||||||
@ -263,7 +262,7 @@ const TaskModel = ({
|
|||||||
{...register("workAreaId")}
|
{...register("workAreaId")}
|
||||||
onChange={handleWorkAreaChange}
|
onChange={handleWorkAreaChange}
|
||||||
>
|
>
|
||||||
<option value="">Select Work Area</option>
|
<option value="0">Select Work Area</option>
|
||||||
{selectedFloor.workAreas
|
{selectedFloor.workAreas
|
||||||
?.filter((workArea) => workArea?.areaName)
|
?.filter((workArea) => workArea?.areaName)
|
||||||
?.sort((a, b) => a.areaName?.localeCompare(b.areaName))
|
?.sort((a, b) => a.areaName?.localeCompare(b.areaName))
|
||||||
|
|||||||
@ -6,7 +6,7 @@ const Breadcrumb = ({ data }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol className="breadcrumb breadcrumb-custom-icon my-3">
|
<ol className="breadcrumb breadcrumb-custom-icon">
|
||||||
{data.map((item, index) => (
|
{data.map((item, index) => (
|
||||||
item.link ? (
|
item.link ? (
|
||||||
<li className="breadcrumb-item cursor-pointer" key={index}>
|
<li className="breadcrumb-item cursor-pointer" key={index}>
|
||||||
|
|||||||
@ -132,7 +132,7 @@ const CreateRole = ({ modalType, onClose }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="col-12 col-md-12">
|
<div className="col-12 col-md-12 border">
|
||||||
|
|
||||||
|
|
||||||
{masterFeatures.map((feature, featureIndex) => (
|
{masterFeatures.map((feature, featureIndex) => (
|
||||||
|
|||||||
@ -180,7 +180,7 @@ const EditMaster = ({ master, onClose }) => {
|
|||||||
<div className="col-12 col-md-12 mx-2s " >
|
<div className="col-12 col-md-12 mx-2s " >
|
||||||
|
|
||||||
{masterFeatures.map((feature, featureIndex) => (
|
{masterFeatures.map((feature, featureIndex) => (
|
||||||
<div className="row my-1" key={feature.id} style={{ marginLeft: "0px" }}>
|
<div className="row my-3" key={feature.id} style={{ marginLeft: "0px" }}>
|
||||||
|
|
||||||
<div className="col-12 col-md-3 d-flex text-start align-items-center" style={{ wordWrap: 'break-word' }}>
|
<div className="col-12 col-md-3 d-flex text-start align-items-center" style={{ wordWrap: 'break-word' }}>
|
||||||
<span className="fs">{feature.name}</span>
|
<span className="fs">{feature.name}</span>
|
||||||
@ -240,7 +240,6 @@ const EditMaster = ({ master, onClose }) => {
|
|||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<hr className="hr my-1 py-1" />
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{errors.permissions && (
|
{errors.permissions && (
|
||||||
|
|||||||
@ -185,7 +185,7 @@ const AttendancePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="container-fluid">
|
<div className="container-xxl flex-grow-1 container-p-y">
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
data={[
|
data={[
|
||||||
{ label: "Home", link: "/dashboard" },
|
{ label: "Home", link: "/dashboard" },
|
||||||
|
|||||||
@ -215,7 +215,7 @@ const DailyTask = () => {
|
|||||||
</GlobalModel>
|
</GlobalModel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="container-fluid">
|
<div className="container-xxl flex-grow-1 container-p-y">
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
data={[
|
data={[
|
||||||
{ label: "Home", link: "/dashboard" },
|
{ label: "Home", link: "/dashboard" },
|
||||||
|
|||||||
@ -108,11 +108,11 @@ const TaskPlannng = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="container-fluid">
|
<div className="container-xxl flex-grow-1 container-p-y">
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
data={[
|
data={[
|
||||||
{ label: "Home", link: "/dashboard" },
|
{ label: "Home", link: "/dashboard" },
|
||||||
{ label: "Daily Task Planning" }
|
{ label: "Daily Task Planning", link: "/activities/task" },
|
||||||
]}
|
]}
|
||||||
></Breadcrumb>
|
></Breadcrumb>
|
||||||
{project_listLoader && <p>Loading..</p>}
|
{project_listLoader && <p>Loading..</p>}
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import DirectoryPageHeader from "./DirectoryPageHeader";
|
|||||||
import ManageBucket from "../../components/Directory/ManageBucket";
|
import ManageBucket from "../../components/Directory/ManageBucket";
|
||||||
import { useFab } from "../../Context/FabContext";
|
import { useFab } from "../../Context/FabContext";
|
||||||
import { DireProvider, useDir } from "../../Context/DireContext";
|
import { DireProvider, useDir } from "../../Context/DireContext";
|
||||||
import NotesCardViewDirectory from "../../components/Directory/NotesCardViewDirectory";
|
|
||||||
|
|
||||||
const Directory = ({ IsPage = true, prefernceContacts }) => {
|
const Directory = ({ IsPage = true, prefernceContacts }) => {
|
||||||
const [projectPrefernce, setPerfence] = useState(null);
|
const [projectPrefernce, setPerfence] = useState(null);
|
||||||
@ -32,17 +31,11 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
|||||||
const [ContactList, setContactList] = useState([]);
|
const [ContactList, setContactList] = useState([]);
|
||||||
const [contactCategories, setContactCategories] = useState([]);
|
const [contactCategories, setContactCategories] = useState([]);
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const [viewType, setViewType] = useState("notes");
|
const [listView, setListView] = useState(false);
|
||||||
const [selectedBucketIds, setSelectedBucketIds] = useState([]);
|
const [selectedBucketIds, setSelectedBucketIds] = useState([]);
|
||||||
const [deleteContact, setDeleteContact] = useState(null);
|
const [deleteContact, setDeleteContact] = useState(null);
|
||||||
const [IsDeleting, setDeleting] = useState(false);
|
const [IsDeleting, setDeleting] = useState(false);
|
||||||
const [openBucketModal, setOpenBucketModal] = 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 [tempSelectedBucketIds, setTempSelectedBucketIds] = useState([]);
|
||||||
const [tempSelectedCategoryIds, setTempSelectedCategoryIds] = useState([]);
|
const [tempSelectedCategoryIds, setTempSelectedCategoryIds] = useState([]);
|
||||||
@ -78,6 +71,8 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
|||||||
setIsOpenModal(false);
|
setIsOpenModal(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cacheData("Contacts", {data:updatedContacts,isActive:IsActive});
|
||||||
|
// setContactList(updatedContacts);
|
||||||
refetch(IsActive, prefernceContacts);
|
refetch(IsActive, prefernceContacts);
|
||||||
refetchBucket();
|
refetchBucket();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -254,13 +249,12 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
|||||||
|
|
||||||
return () => setActions([]);
|
return () => setActions([]);
|
||||||
}, [IsPage, buckets]);
|
}, [IsPage, buckets]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPerfence(prefernceContacts);
|
setPerfence(prefernceContacts);
|
||||||
}, [prefernceContacts]);
|
}, [prefernceContacts]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-fluid">
|
<div className="container-xxl flex-grow-1 container-p-y">
|
||||||
{IsPage && (
|
{IsPage && (
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
data={[
|
data={[
|
||||||
@ -332,14 +326,13 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
|||||||
</GlobalModel>
|
</GlobalModel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="card p-0 mb-0">
|
<div className="card p-2 card-minHeight">
|
||||||
<div className="card-body p-1 pb-0">
|
|
||||||
<DirectoryPageHeader
|
<DirectoryPageHeader
|
||||||
searchText={searchText}
|
searchText={searchText}
|
||||||
setSearchText={setSearchText}
|
setSearchText={setSearchText}
|
||||||
setIsActive={setIsActive}
|
setIsActive={setIsActive}
|
||||||
viewType={viewType}
|
listView={listView}
|
||||||
setViewType={setViewType}
|
setListView={setListView}
|
||||||
filteredBuckets={filteredBuckets}
|
filteredBuckets={filteredBuckets}
|
||||||
tempSelectedBucketIds={tempSelectedBucketIds}
|
tempSelectedBucketIds={tempSelectedBucketIds}
|
||||||
handleTempBucketChange={handleTempBucketChange}
|
handleTempBucketChange={handleTempBucketChange}
|
||||||
@ -351,34 +344,49 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
IsActive={IsActive}
|
IsActive={IsActive}
|
||||||
setOpenBucketModal={setOpenBucketModal}
|
setOpenBucketModal={setOpenBucketModal}
|
||||||
contactsToExport={contacts}
|
|
||||||
notesToExport={notes}
|
|
||||||
selectedNoteNames={selectedNoteNames}
|
|
||||||
setSelectedNoteNames={setSelectedNoteNames}
|
|
||||||
notesForFilter={notes}
|
|
||||||
setFilterAppliedNotes={setFilterAppliedNotes}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
{/* Messages when listView is false */}
|
||||||
<div className="card-minHeight mt-0">
|
{!listView && (
|
||||||
{(viewType === "card" || viewType === "list" || viewType === "notes") && (
|
|
||||||
<div className="d-flex flex-column justify-content-center align-items-center text-center ">
|
<div className="d-flex flex-column justify-content-center align-items-center text-center ">
|
||||||
{!loading && (viewType === "card" || viewType === "list") && contacts?.length === 0 && (
|
{loading && <p className="mt-10">Loading...</p>}
|
||||||
|
{!loading && contacts?.length === 0 && (
|
||||||
<p className="mt-10">No contact found</p>
|
<p className="mt-10">No contact found</p>
|
||||||
)}
|
)}
|
||||||
{!loading &&
|
{!loading && contacts?.length > 0 && currentItems.length === 0 && (
|
||||||
(viewType === "card" || viewType === "list") &&
|
|
||||||
contacts?.length > 0 &&
|
|
||||||
currentItems.length === 0 && (
|
|
||||||
<p className="mt-10">No matching contact found</p>
|
<p className="mt-10">No matching contact found</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{viewType === "list" && (
|
{/* Table view (listView === true) */}
|
||||||
<div className="card cursor-pointer mt-5">
|
{listView ? (
|
||||||
<div className="card-body p-2 pb-1">
|
|
||||||
<DirectoryListTableHeader>
|
<DirectoryListTableHeader>
|
||||||
|
{loading && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={10}>
|
||||||
|
{" "}
|
||||||
|
<p className="mt-10">Loading...</p>{" "}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!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 &&
|
{!loading &&
|
||||||
currentItems.map((contact) => (
|
currentItems.map((contact) => (
|
||||||
<ListViewDirectory
|
<ListViewDirectory
|
||||||
@ -394,12 +402,8 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</DirectoryListTableHeader>
|
</DirectoryListTableHeader>
|
||||||
</div>
|
) : (
|
||||||
</div>
|
<div className="row mt-5">
|
||||||
)}
|
|
||||||
|
|
||||||
{viewType === "card" && (
|
|
||||||
<div className="row mt-4">
|
|
||||||
{!loading &&
|
{!loading &&
|
||||||
currentItems.map((contact) => (
|
currentItems.map((contact) => (
|
||||||
<div
|
<div
|
||||||
@ -421,26 +425,15 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{viewType === "notes" && (
|
|
||||||
<div className="mt-0">
|
|
||||||
<NotesCardViewDirectory
|
|
||||||
notes={notes}
|
|
||||||
setNotesForFilter={setNotes}
|
|
||||||
searchText={searchText}
|
|
||||||
setIsOpenModalNote={setIsOpenModalNote}
|
|
||||||
filterAppliedNotes={filterAppliedNotes}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{!loading &&
|
{!loading &&
|
||||||
viewType !== "notes" &&
|
|
||||||
contacts?.length > 0 &&
|
contacts?.length > 0 &&
|
||||||
currentItems.length > ITEMS_PER_PAGE && (
|
currentItems.length > ITEMS_PER_PAGE && (
|
||||||
<nav aria-label="Page navigation">
|
<nav aria-label="Page navigation">
|
||||||
<ul className="pagination pagination-sm justify-content-end py-1">
|
<ul className="pagination pagination-sm justify-content-end py-1">
|
||||||
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
|
<li
|
||||||
|
className={`page-item ${currentPage === 1 ? "disabled" : ""}`}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className="page-link btn-xs"
|
className="page-link btn-xs"
|
||||||
onClick={() => paginate(currentPage - 1)}
|
onClick={() => paginate(currentPage - 1)}
|
||||||
@ -452,7 +445,8 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
|||||||
{[...Array(totalPages)].map((_, index) => (
|
{[...Array(totalPages)].map((_, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
className={`page-item ${
|
||||||
|
currentPage === index + 1 ? "active" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -464,7 +458,11 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<li className={`page-item ${currentPage === totalPages ? "disabled" : ""}`}>
|
<li
|
||||||
|
className={`page-item ${
|
||||||
|
currentPage === totalPages ? "disabled" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className="page-link"
|
className="page-link"
|
||||||
onClick={() => paginate(currentPage + 1)}
|
onClick={() => paginate(currentPage + 1)}
|
||||||
|
|||||||
@ -7,38 +7,33 @@ const DirectoryListTableHeader = ({ children }) => {
|
|||||||
<table className="table px-2">
|
<table className="table px-2">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colSpan={2} className="text-start">
|
<th colSpan={2}>
|
||||||
<div className="d-flex align-items-center gap-1">
|
<div className="d-flex align-items-center gap-1">
|
||||||
<span>Name</span>
|
<span>Name</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th className="px-2 text-start">
|
<th className="px-2 text-start">
|
||||||
<div className="d-flex align-items-center gap-1">
|
<div className="d-flex text-center align-items-center gap-1 justify-content-start">
|
||||||
<span>Email</span>
|
<span>Email</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th className="mx-2 text-start">
|
<th className="mx-2">
|
||||||
<div className="d-flex align-items-center gap-1">
|
<div className="d-flex align-items-center m-0 p-0 gap-1">
|
||||||
<span>Phone</span>
|
<span>Phone</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th colSpan={2} className="mx-2 ps-20 text-start">
|
<th colSpan={2} className="mx-2 ps-20">
|
||||||
<span>Organization</span>
|
Organization
|
||||||
</th>
|
|
||||||
<th className="mx-2 text-start">
|
|
||||||
<span>Category</span>
|
|
||||||
</th>
|
|
||||||
<th className="text-start">
|
|
||||||
<span>Action</span>
|
|
||||||
</th>
|
</th>
|
||||||
|
<th className="mx-2">Category</th>
|
||||||
|
<th>Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="table-border-bottom-0 overflow-auto text-start">
|
<tbody className="table-border-bottom-0 overflow-auto">
|
||||||
{children}
|
{children}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DirectoryListTableHeader;
|
export default DirectoryListTableHeader;
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { exportToCSV, exportToExcel, printTable, exportToPDF } from "../../utils/tableExportUtils";
|
|
||||||
|
|
||||||
const DirectoryPageHeader = ({
|
const DirectoryPageHeader = ({
|
||||||
searchText,
|
searchText,
|
||||||
setSearchText,
|
setSearchText,
|
||||||
setIsActive,
|
setIsActive,
|
||||||
viewType,
|
listView,
|
||||||
setViewType,
|
setListView,
|
||||||
filteredBuckets,
|
filteredBuckets,
|
||||||
tempSelectedBucketIds,
|
tempSelectedBucketIds,
|
||||||
handleTempBucketChange,
|
handleTempBucketChange,
|
||||||
@ -17,432 +16,95 @@ const DirectoryPageHeader = ({
|
|||||||
applyFilter,
|
applyFilter,
|
||||||
loading,
|
loading,
|
||||||
IsActive,
|
IsActive,
|
||||||
contactsToExport,
|
setOpenBucketModal,
|
||||||
notesToExport,
|
|
||||||
selectedNoteNames,
|
|
||||||
setSelectedNoteNames,
|
|
||||||
notesForFilter,
|
|
||||||
setFilterAppliedNotes
|
|
||||||
}) => {
|
}) => {
|
||||||
const [filtered, setFiltered] = useState(0);
|
const [filtered, setFiltered] = useState();
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
setFiltered(tempSelectedBucketIds?.length + tempSelectedCategoryIds?.length);
|
setFiltered(
|
||||||
|
tempSelectedBucketIds?.length + tempSelectedCategoryIds?.length
|
||||||
|
);
|
||||||
}, [tempSelectedBucketIds, tempSelectedCategoryIds]);
|
}, [tempSelectedBucketIds, tempSelectedCategoryIds]);
|
||||||
|
|
||||||
// New state to track active filters for notes
|
|
||||||
const [notesFilterCount, setNotesFilterCount] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Calculate the number of active filters for notes
|
|
||||||
setNotesFilterCount(selectedCreators.length + selectedOrgs.length);
|
|
||||||
}, [selectedCreators, selectedOrgs]);
|
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
// 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="row mx-0 px-0 align-items-center mt-0">
|
{/* <div className="row">vikas</div> */}
|
||||||
<div className="col-12 col-md-6 mb-0 px-1 d-flex align-items-center gap-4">
|
<div className="row mx-0 px-0 align-items-center mt-2">
|
||||||
<ul className="nav nav-tabs mb-0" role="tablist">
|
<div className="col-12 col-md-6 mb-2 px-1 d-flex align-items-center gap-4 ">
|
||||||
<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>
|
|
||||||
</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
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
className="form-control me-0"
|
className="form-control form-control-sm me-2"
|
||||||
placeholder={viewType === "notes" ? "Search Notes..." : "Search Contact..."}
|
placeholder="Search Contact..."
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
style={{ width: "200px", height: "30px" }}
|
style={{ width: "200px" }}
|
||||||
/>
|
/>
|
||||||
|
<div className="d-flex gap-2 ">
|
||||||
{/* Filter by funnel icon for Notes view */}
|
<button
|
||||||
{viewType === "notes" && (
|
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" }}>
|
<div className="dropdown" style={{ width: "fit-content" }}>
|
||||||
<a
|
<a
|
||||||
className="dropdown-toggle hide-arrow cursor-pointer d-flex align-items-center position-relative"
|
className="dropdown-toggle hide-arrow cursor-pointer d-flex align-items-center position-relative"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
>
|
>
|
||||||
<i className={`fa-solid fa-filter ms-1 fs-5 ${notesFilterCount > 0 ? "text-primary" : "text-muted"}`}></i>
|
<i
|
||||||
{notesFilterCount > 0 && (
|
className={`fa-solid fa-filter ms-1 fs-5 ${
|
||||||
<span className="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning" style={{ fontSize: "0.4rem" }}>
|
filtered > 0 ? "text-primary" : "text-muted"
|
||||||
{notesFilterCount}
|
}`}
|
||||||
</span>
|
></i>
|
||||||
)}
|
|
||||||
</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 && (
|
{filtered > 0 && (
|
||||||
<span className="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning" style={{ fontSize: "0.4rem" }}>
|
<span
|
||||||
|
className="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning"
|
||||||
|
style={{ fontSize: "0.4rem" }}
|
||||||
|
>
|
||||||
{filtered}
|
{filtered}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ul className="dropdown-menu p-3" style={{ width: "700px" }}>
|
<ul className="dropdown-menu p-3" style={{ width: "320px" }}>
|
||||||
|
<div>
|
||||||
<p className="text-muted m-0 h6 ">Filter by</p>
|
<p className="text-muted m-0 h6 ">Filter by</p>
|
||||||
|
|
||||||
<div className="d-flex flex-nowrap">
|
{/* Bucket Filter */}
|
||||||
<div className="mt-1 me-4" style={{ flexBasis: "50%" }}>
|
<div className="mt-1">
|
||||||
<p className="text-small mb-1 ">Buckets</p>
|
<p className="text-small mb-1 ">Buckets</p>
|
||||||
<div className="d-flex flex-wrap">
|
<div className="d-flex flex-wrap">
|
||||||
{filteredBuckets.map(({ id, name }) => (
|
{filteredBuckets.map(({ id, name }) => (
|
||||||
<div className="form-check me-3 mb-1" style={{ minWidth: "calc(50% - 15px)" }} key={id}>
|
<div
|
||||||
|
className="form-check me-3 mb-1"
|
||||||
|
style={{ minWidth: "33.33%" }}
|
||||||
|
key={id}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
className="form-check-input"
|
className="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -450,18 +112,27 @@ const DirectoryPageHeader = ({
|
|||||||
checked={tempSelectedBucketIds.includes(id)}
|
checked={tempSelectedBucketIds.includes(id)}
|
||||||
onChange={() => handleTempBucketChange(id)}
|
onChange={() => handleTempBucketChange(id)}
|
||||||
/>
|
/>
|
||||||
<label className="form-check-label text-nowrap text-small" htmlFor={`bucket-${id}`}>
|
<label
|
||||||
|
className="form-check-label text-nowrap text-small "
|
||||||
|
htmlFor={`bucket-${id}`}
|
||||||
|
>
|
||||||
{name}
|
{name}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1" style={{ flexBasis: "50%" }}>
|
<hr className="m-0" />
|
||||||
|
{/* Category Filter */}
|
||||||
|
<div className="mt-1">
|
||||||
<p className="text-small mb-1 ">Categories</p>
|
<p className="text-small mb-1 ">Categories</p>
|
||||||
<div className="d-flex flex-wrap">
|
<div className="d-flex flex-wrap">
|
||||||
{filteredCategories.map(({ id, name }) => (
|
{filteredCategories.map(({ id, name }) => (
|
||||||
<div className="form-check me-3 mb-1" style={{ minWidth: "calc(50% - 15px)" }} key={id}>
|
<div
|
||||||
|
className="form-check me-3 mb-1"
|
||||||
|
style={{ minWidth: "33.33%" }}
|
||||||
|
key={id}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
className="form-check-input"
|
className="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -469,87 +140,53 @@ const DirectoryPageHeader = ({
|
|||||||
checked={tempSelectedCategoryIds.includes(id)}
|
checked={tempSelectedCategoryIds.includes(id)}
|
||||||
onChange={() => handleTempCategoryChange(id)}
|
onChange={() => handleTempCategoryChange(id)}
|
||||||
/>
|
/>
|
||||||
<label className="form-check-label text-nowrap text-small" htmlFor={`cat-${id}`}>
|
<label
|
||||||
|
className="form-check-label text-nowrap text-small"
|
||||||
|
htmlFor={`cat-${id}`}
|
||||||
|
>
|
||||||
{name}
|
{name}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="d-flex justify-content-end gap-2 mt-1">
|
<div className="d-flex justify-content-end gap-2 mt-1">
|
||||||
<button
|
<button
|
||||||
className="btn btn-xs btn-secondary"
|
className="btn btn-xs btn-secondary"
|
||||||
onClick={(e) => {
|
onClick={clearFilter}
|
||||||
// e.stopPropagation();
|
|
||||||
clearFilter();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-xs btn-primary"
|
className="btn btn-xs btn-primary"
|
||||||
onClick={(e) => {
|
onClick={applyFilter}
|
||||||
applyFilter();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="col-12 col-md-6 mb-2 px-5 d-flex justify-content-end align-items-center gap-2">
|
<div className="col-12 col-md-6 mb-2 px-1 d-flex justify-content-end gap-2 align-items-center text-end">
|
||||||
{(viewType === "list" || viewType === "card") && (
|
<label className="switch switch-primary align-self-start mb-2">
|
||||||
<label className="switch switch-primary mb-0">
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="switch-input me-3"
|
className="switch-input me-3"
|
||||||
onChange={() => setIsActive(!IsActive)}
|
onChange={() => setIsActive(!IsActive)}
|
||||||
checked={!IsActive}
|
value={IsActive}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
<span className="switch-toggle-slider">
|
<span className="switch-toggle-slider">
|
||||||
<span className="switch-on"></span>
|
<span className="switch-on"></span>
|
||||||
<span className="switch-off"></span>
|
<span className="switch-off"></span>
|
||||||
</span>
|
</span>
|
||||||
<span className="ms-12">Show Inactive Contacts</span>
|
<span className=" list-inline-item ms-12 ">
|
||||||
|
Show Inactive Contacts
|
||||||
|
</span>
|
||||||
</label>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -53,7 +53,6 @@ const LoginPage = () => {
|
|||||||
navigate("/dashboard");
|
navigate("/dashboard");
|
||||||
} else {
|
} else {
|
||||||
await AuthRepository.sendOTP({ email: data.username });
|
await AuthRepository.sendOTP({ email: data.username });
|
||||||
showToast("OTP has been sent to your email.", "success");
|
|
||||||
localStorage.setItem("otpUsername", data.username);
|
localStorage.setItem("otpUsername", data.username);
|
||||||
localStorage.setItem("otpSentTime", now.toString());
|
localStorage.setItem("otpSentTime", now.toString());
|
||||||
navigate("/auth/login-otp");
|
navigate("/auth/login-otp");
|
||||||
@ -115,18 +114,18 @@ const LoginPage = () => {
|
|||||||
<label className="form-label" htmlFor="password">
|
<label className="form-label" htmlFor="password">
|
||||||
Password
|
Password
|
||||||
</label>
|
</label>
|
||||||
<div className="input-group input-group-merge d-flex align-items-center border rounded px-2">
|
<div className="input-group input-group-merge">
|
||||||
<input
|
<input
|
||||||
type={hidepass ? "password" : "text"}
|
type={hidepass ? "password" : "text"}
|
||||||
autoComplete="true"
|
autoComplete="true"
|
||||||
id="password"
|
id="password"
|
||||||
{...register("password")}
|
{...register("password")}
|
||||||
className="form-control form-control-xl border-0 shadow-none"
|
className="form-control"
|
||||||
placeholder="••••••••••••"
|
placeholder="••••••••••••"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-link p-0 ms-2 "
|
className="btn border-top border-end border-bottom"
|
||||||
onClick={() => setHidepass(!hidepass)}
|
onClick={() => setHidepass(!hidepass)}
|
||||||
style={{
|
style={{
|
||||||
borderTopLeftRadius: 0,
|
borderTopLeftRadius: 0,
|
||||||
@ -151,7 +150,6 @@ const LoginPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="mb-3 d-flex justify-content-between">
|
<div className="mb-3 d-flex justify-content-between">
|
||||||
<div className="form-check d-flex">
|
<div className="form-check d-flex">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@ -67,13 +67,9 @@ const ResetPasswordPage = () => {
|
|||||||
navigate("/auth/login", { replace: true });
|
navigate("/auth/login", { replace: true });
|
||||||
// setLoading(false);
|
// setLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugger;
|
showToast("Link is expries or Invalid ", "error");
|
||||||
setLoading(false);
|
|
||||||
if (error?.response?.status === 400) {
|
|
||||||
showToast("Please check valid Credentials", "error");
|
|
||||||
} else {
|
|
||||||
setTokenExpired(true);
|
setTokenExpired(true);
|
||||||
}
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,10 +77,7 @@ const ResetPasswordPage = () => {
|
|||||||
return (
|
return (
|
||||||
<AuthWrapper>
|
<AuthWrapper>
|
||||||
<h4 className="mb-2 ">Invalid Link 🔒</h4>
|
<h4 className="mb-2 ">Invalid Link 🔒</h4>
|
||||||
<p className="mb-4" style={{ fontSize: "12px" }}>
|
<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>
|
||||||
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">
|
<div className="text-center mb-4">
|
||||||
<Link to="/auth/forgot-password" className="btn btn-outline-primary">
|
<Link to="/auth/forgot-password" className="btn btn-outline-primary">
|
||||||
Go to Forgot Password
|
Go to Forgot Password
|
||||||
@ -149,6 +142,7 @@ const ResetPasswordPage = () => {
|
|||||||
borderTopLeftRadius: 0,
|
borderTopLeftRadius: 0,
|
||||||
borderBottomLeftRadius: 0,
|
borderBottomLeftRadius: 0,
|
||||||
borderLeft: 0,
|
borderLeft: 0,
|
||||||
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{hidepass ? (
|
{hidepass ? (
|
||||||
@ -191,6 +185,7 @@ const ResetPasswordPage = () => {
|
|||||||
borderTopLeftRadius: 0,
|
borderTopLeftRadius: 0,
|
||||||
borderBottomLeftRadius: 0,
|
borderBottomLeftRadius: 0,
|
||||||
borderLeft: 0,
|
borderLeft: 0,
|
||||||
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{hidepass1 ? (
|
{hidepass1 ? (
|
||||||
|
|||||||
@ -6,13 +6,13 @@ import Avatar from "../../components/common/Avatar";
|
|||||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||||
import ManageEmp from "../../components/Employee/ManageRole";
|
import ManageEmp from "../../components/Employee/ManageRole";
|
||||||
import { useEmployeesAllOrByProjectId } from "../../hooks/useEmployees";
|
import { useEmployeesAllOrByProjectId } from "../../hooks/useEmployees";
|
||||||
import { useProjects } from "../../hooks/useProjects"; // Keep if you use projects elsewhere
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
import { useProfile } from "../../hooks/useProfile"; // Keep if you use profile elsewhere
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
import { hasUserPermission } from "../../utils/authUtils"; // Keep if you use this elsewhere
|
import { hasUserPermission } from "../../utils/authUtils";
|
||||||
import { ITEMS_PER_PAGE, MANAGE_EMPLOYEES } from "../../utils/constants";
|
import { ITEMS_PER_PAGE, MANAGE_EMPLOYEES } from "../../utils/constants";
|
||||||
import { clearCacheKey } from "../../slices/apiDataManager";
|
import { clearCacheKey } from "../../slices/apiDataManager";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import SuspendEmp from "../../components/Employee/SuspendEmp"; // Keep if you use SuspendEmp
|
import SuspendEmp from "../../components/Employee/SuspendEmp";
|
||||||
import {
|
import {
|
||||||
exportToCSV,
|
exportToCSV,
|
||||||
exportToExcel,
|
exportToExcel,
|
||||||
@ -28,25 +28,23 @@ import { newlineChars } from "pdf-lib";
|
|||||||
import GlobalModel from "../../components/common/GlobalModel";
|
import GlobalModel from "../../components/common/GlobalModel";
|
||||||
|
|
||||||
const EmployeeList = () => {
|
const EmployeeList = () => {
|
||||||
const selectedProjectId = useSelector(
|
const selectedProjectId = useSelector((store) => store.localVariables.projectId);
|
||||||
(store) => store.localVariables.projectId
|
const [selectedProject, setSelectedProject] = useState(() => selectedProjectId || "");
|
||||||
);
|
const { projects, loading: projectLoading } = useProjects();
|
||||||
|
|
||||||
const [showInactive, setShowInactive] = useState(false);
|
const [showInactive, setShowInactive] = useState(false);
|
||||||
const [showAllEmployees, setShowAllEmployees] = useState(false);
|
const [showAllEmployees, setShowAllEmployees] = useState(false);
|
||||||
const Manage_Employee = useHasUserPermission(MANAGE_EMPLOYEES);
|
const Manage_Employee = useHasUserPermission(MANAGE_EMPLOYEES);
|
||||||
|
|
||||||
const { employees, loading, setLoading, error, recallEmployeeData } =
|
const { employees, loading, setLoading, error, recallEmployeeData } =
|
||||||
useEmployeesAllOrByProjectId(
|
useEmployeesAllOrByProjectId(showAllEmployees ? null : selectedProject, showInactive);
|
||||||
showAllEmployees ? null : selectedProjectId, // Use selectedProjectId here
|
const [projectsList, setProjectsList] = useState(projects || []);
|
||||||
showInactive
|
|
||||||
);
|
|
||||||
|
|
||||||
const [employeeList, setEmployeeList] = useState([]);
|
const [employeeList, setEmployeeList] = useState([]);
|
||||||
const [modelConfig, setModelConfig] = useState();
|
const [modelConfig, setModelConfig] = useState();
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage] = useState(ITEMS_PER_PAGE);
|
const [itemsPerPage] = useState(ITEMS_PER_PAGE);
|
||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
|
const [isEmployeeModalOpen, setIsEmployeeModalOpen] = useState(false);
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const [filteredData, setFilteredData] = useState([]);
|
const [filteredData, setFilteredData] = useState([]);
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
@ -57,65 +55,48 @@ const EmployeeList = () => {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
/**
|
const handleSearch = (e) => {
|
||||||
* Applies the search filter to a given array of employee data.
|
const value = e.target.value.toLowerCase();
|
||||||
* @param {Array} data - The array of employee objects to filter.
|
setSearchText(value);
|
||||||
* @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
|
|
||||||
|
|
||||||
return data.filter((item) => {
|
if (!employeeList.length) return;
|
||||||
// **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 email = item.email ? item.email.toLowerCase() : "";
|
||||||
const phoneNumber = item.phoneNumber ? item.phoneNumber.toLowerCase() : "";
|
const phoneNumber = item.phoneNumber ? item.phoneNumber.toLowerCase() : "";
|
||||||
const jobRole = item.jobRole ? item.jobRole.toLowerCase() : "";
|
const jobRole = item.jobRole ? item.jobRole.toLowerCase() : ""; // Get jobRole and convert to lowercase
|
||||||
|
|
||||||
return (
|
return (
|
||||||
fullName.includes(lowercasedText) ||
|
fullName.includes(value) ||
|
||||||
email.includes(lowercasedText) ||
|
email.includes(value) ||
|
||||||
phoneNumber.includes(lowercasedText) ||
|
phoneNumber.includes(value) ||
|
||||||
jobRole.includes(lowercasedText)
|
jobRole.includes(value) // Include jobRole in the search
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearch = (e) => {
|
setFilteredData(results);
|
||||||
const value = e.target.value;
|
|
||||||
setSearchText(value);
|
|
||||||
setCurrentPage(1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
|
|
||||||
if (!loading && Array.isArray(employees)) {
|
if (!loading && Array.isArray(employees)) {
|
||||||
|
// Sort by full name (firstName + lastName)
|
||||||
const sorted = [...employees].sort((a, b) => {
|
const sorted = [...employees].sort((a, b) => {
|
||||||
const nameA = `${a.firstName || ""}${a.middleName || ""}${a.lastName || ""}`.toLowerCase();
|
const nameA = `${a.firstName || ""}${a.middleName || ""}${b.lastName || ""
|
||||||
const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || ""}`.toLowerCase();
|
}`.toLowerCase();
|
||||||
|
const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || ""
|
||||||
|
}`.toLowerCase();
|
||||||
return nameA?.localeCompare(nameB);
|
return nameA?.localeCompare(nameB);
|
||||||
});
|
});
|
||||||
|
|
||||||
setEmployeeList(sorted);
|
setEmployeeList(sorted);
|
||||||
const results = applySearchFilter(sorted, searchText);
|
setFilteredData(sorted);
|
||||||
setFilteredData(results);
|
|
||||||
} else if (!loading && !employees) {
|
|
||||||
setEmployeeList([]);
|
|
||||||
setFilteredData([]);
|
|
||||||
}
|
}
|
||||||
}, [loading, employees, showAllEmployees, searchText, selectedProjectId]); // Add selectedProjectId to dependencies
|
}, [loading, employees, selectedProject, showAllEmployees]); // Add showAllEmployees to dependencies
|
||||||
|
|
||||||
const displayData = filteredData;
|
const displayData = searchText ? filteredData : employeeList;
|
||||||
const indexOfLastItem = currentPage * itemsPerPage;
|
const indexOfLastItem = currentPage * itemsPerPage;
|
||||||
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
||||||
const currentItems = Array.isArray(displayData)
|
const currentItems = Array.isArray(displayData)
|
||||||
@ -139,11 +120,11 @@ const EmployeeList = () => {
|
|||||||
modalElement.classList.remove("show");
|
modalElement.classList.remove("show");
|
||||||
modalElement.style.display = "none";
|
modalElement.style.display = "none";
|
||||||
document.body.classList.remove("modal-open");
|
document.body.classList.remove("modal-open");
|
||||||
document.querySelector(".modal-backdrop")?.remove(); // Use optional chaining for safety
|
document.querySelector(".modal-backdrop").remove();
|
||||||
}
|
}
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
clearCacheKey("employeeProfile");
|
clearCacheKey("employeeProfile");
|
||||||
recallEmployeeData(showInactive, showAllEmployees ? null : selectedProjectId); // Use selectedProjectId here
|
recallEmployeeData(showInactive);
|
||||||
};
|
};
|
||||||
const handleShow = () => setShowModal(true);
|
const handleShow = () => setShowModal(true);
|
||||||
const handleClose = () => setShowModal(false);
|
const handleClose = () => setShowModal(false);
|
||||||
@ -157,8 +138,8 @@ const EmployeeList = () => {
|
|||||||
clearCacheKey("allEmployeeList");
|
clearCacheKey("allEmployeeList");
|
||||||
clearCacheKey("allInactiveEmployeeList");
|
clearCacheKey("allInactiveEmployeeList");
|
||||||
clearCacheKey("employeeProfile");
|
clearCacheKey("employeeProfile");
|
||||||
// Recall data based on current filter states after deletion to refresh the table
|
setEmployeeList([]);
|
||||||
recallEmployeeData(showInactive, showAllEmployees ? null : selectedProjectId); // Use selectedProjectId here
|
recallEmployeeData(showInactive);
|
||||||
setemployeeLodaing(false);
|
setemployeeLodaing(false);
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
})
|
})
|
||||||
@ -207,7 +188,7 @@ const EmployeeList = () => {
|
|||||||
|
|
||||||
const handleToggle = (e) => {
|
const handleToggle = (e) => {
|
||||||
setShowInactive(e.target.checked);
|
setShowInactive(e.target.checked);
|
||||||
recallEmployeeData(e.target.checked, showAllEmployees ? null : selectedProjectId); // Use selectedProjectId here
|
recallEmployeeData(e.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAllEmployeesToggle = (e) => {
|
const handleAllEmployeesToggle = (e) => {
|
||||||
@ -220,6 +201,8 @@ const handleAllEmployeesToggle = (e) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleEmployeeModel = (id) => {
|
const handleEmployeeModel = (id) => {
|
||||||
setSelecedEmployeeId(id);
|
setSelecedEmployeeId(id);
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
@ -230,19 +213,24 @@ const handleAllEmployeesToggle = (e) => {
|
|||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleProjectSelection = (e) => {
|
||||||
if (!showAllEmployees) {
|
const newProjectId = e.target.value;
|
||||||
recallEmployeeData(showInactive, selectedProjectId);
|
setSelectedProject(newProjectId);
|
||||||
|
if (newProjectId) {
|
||||||
|
setShowAllEmployees(false);
|
||||||
}
|
}
|
||||||
}, [selectedProjectId, showInactive, showAllEmployees, recallEmployeeData]);
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedProject(selectedProjectId || "");
|
||||||
|
}, [selectedProjectId]);
|
||||||
|
|
||||||
const handler = useCallback(
|
const handler = useCallback(
|
||||||
(msg) => {
|
(msg) => {
|
||||||
if(employees.some((item) => item.id == msg.employeeId)){
|
if(employees.some((item) => item.id == msg.employeeId)){
|
||||||
setEmployeeList([]);
|
setEmployeeList([]);
|
||||||
recallEmployeeData(showInactive, showAllEmployees ? null : selectedProjectId); // Use selectedProjectId here
|
recallEmployeeData(showInactive);
|
||||||
}
|
}
|
||||||
},[employees, showInactive, showAllEmployees, selectedProjectId] // Add all relevant dependencies
|
},[employees]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -310,7 +298,7 @@ const handleAllEmployeesToggle = (e) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="container-fluid">
|
<div className="container-xxl flex-grow-1 container-p-y">
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
data={[
|
data={[
|
||||||
{ label: "Home", link: "/dashboard" },
|
{ label: "Home", link: "/dashboard" },
|
||||||
@ -365,7 +353,7 @@ const handleAllEmployeesToggle = (e) => {
|
|||||||
|
|
||||||
{/* Right side: Search + Export + Add Employee */}
|
{/* Right side: Search + Export + Add Employee */}
|
||||||
<div className="d-flex flex-wrap align-items-center justify-content-end gap-3 flex-grow-1">
|
<div className="d-flex flex-wrap align-items-center justify-content-end gap-3 flex-grow-1">
|
||||||
{/* Search Input - ALWAYS ENABLED */}
|
{/* Search */}
|
||||||
<div className="dataTables_filter">
|
<div className="dataTables_filter">
|
||||||
<label className="mb-0">
|
<label className="mb-0">
|
||||||
<input
|
<input
|
||||||
@ -414,7 +402,7 @@ const handleAllEmployeesToggle = (e) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add Employee Button */}
|
{/* Add Employee */}
|
||||||
{Manage_Employee && (
|
{Manage_Employee && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-primary"
|
className="btn btn-sm btn-primary"
|
||||||
@ -428,6 +416,7 @@ const handleAllEmployeesToggle = (e) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<table
|
<table
|
||||||
className="datatables-users table border-top dataTable no-footer dtr-column text-nowrap"
|
className="datatables-users table border-top dataTable no-footer dtr-column text-nowrap"
|
||||||
id="DataTables_Table_0"
|
id="DataTables_Table_0"
|
||||||
@ -522,17 +511,7 @@ const handleAllEmployeesToggle = (e) => {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{/* Conditional messages for no data or no search results */}
|
{!loading && employeeList?.length === 0 && (
|
||||||
{!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>
|
<tr>
|
||||||
<td
|
<td
|
||||||
colSpan={8}
|
colSpan={8}
|
||||||
@ -541,10 +520,23 @@ const handleAllEmployeesToggle = (e) => {
|
|||||||
No Data Found
|
No Data Found
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : null}
|
)}
|
||||||
|
{!loading &&
|
||||||
|
employeeList &&
|
||||||
|
currentItems.length === 0 &&
|
||||||
|
employeeList.length !== 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={8}>
|
||||||
|
<small className="muted">
|
||||||
|
'{searchText}' employee not found
|
||||||
|
</small>{" "}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Render current items */}
|
{currentItems &&
|
||||||
{currentItems && !loading && currentItems.map((item) => (
|
!loading &&
|
||||||
|
currentItems.map((item) => (
|
||||||
<tr className="odd" key={item.id}>
|
<tr className="odd" key={item.id}>
|
||||||
<td className="sorting_1" colSpan={2}>
|
<td className="sorting_1" colSpan={2}>
|
||||||
<div className="d-flex justify-content-start align-items-center user-name">
|
<div className="d-flex justify-content-start align-items-center user-name">
|
||||||
@ -573,6 +565,7 @@ const handleAllEmployeesToggle = (e) => {
|
|||||||
{item.email ? (
|
{item.email ? (
|
||||||
<span className="text-truncate">
|
<span className="text-truncate">
|
||||||
<i className="bx bxs-envelope text-primary me-2"></i>
|
<i className="bx bxs-envelope text-primary me-2"></i>
|
||||||
|
|
||||||
{item.email}
|
{item.email}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@ -598,21 +591,20 @@ const handleAllEmployeesToggle = (e) => {
|
|||||||
{moment(item.joiningDate)?.format("DD-MMM-YYYY")}
|
{moment(item.joiningDate)?.format("DD-MMM-YYYY")}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{/* Assuming 'isActive' property exists to determine status */}
|
{showInactive ? (
|
||||||
{item.isActive ? (
|
|
||||||
<span
|
|
||||||
className="badge bg-label-success"
|
|
||||||
text-capitalized=""
|
|
||||||
>
|
|
||||||
Active
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span
|
<span
|
||||||
className="badge bg-label-danger"
|
className="badge bg-label-danger"
|
||||||
text-capitalized=""
|
text-capitalized=""
|
||||||
>
|
>
|
||||||
Inactive
|
Inactive
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="badge bg-label-success"
|
||||||
|
text-capitalized=""
|
||||||
|
>
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
{Manage_Employee && (
|
{Manage_Employee && (
|
||||||
@ -721,6 +713,7 @@ const handleAllEmployeesToggle = (e) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -123,7 +123,7 @@ const EmployeeProfile = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="container-fluid">
|
<div className="container-xxl flex-grow-1 container-p-y">
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
data={[
|
data={[
|
||||||
{ label: "Home", link: "/dashboard" },
|
{ label: "Home", link: "/dashboard" },
|
||||||
|
|||||||
@ -103,7 +103,7 @@ const MasterPage = () => {
|
|||||||
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="container-fluid">
|
<div className="container-xxl flex-grow-1 container-p-y">
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
data={[
|
data={[
|
||||||
{ label: "Home", link: "/dashboard" },
|
{ label: "Home", link: "/dashboard" },
|
||||||
|
|||||||
@ -166,7 +166,7 @@ const ProjectDetails = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{}
|
{}
|
||||||
<div className="container-fluid">
|
<div className="container-xxl flex-grow-1 container-p-y">
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
data={[
|
data={[
|
||||||
{ label: "Home", link: "/dashboard" },
|
{ label: "Home", link: "/dashboard" },
|
||||||
|
|||||||
@ -125,7 +125,6 @@ const ProjectList = () => {
|
|||||||
indexOfLastItem
|
indexOfLastItem
|
||||||
);
|
);
|
||||||
const totalPages = Math.ceil(filteredProjects.length / itemsPerPage);
|
const totalPages = Math.ceil(filteredProjects.length / itemsPerPage);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tooltipTriggerList = Array.from(
|
const tooltipTriggerList = Array.from(
|
||||||
document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||||
@ -195,16 +194,15 @@ const ProjectList = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="container-fluid">
|
<div className="container-xxl flex-grow-1 container-p-y">
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
data={[
|
data={[
|
||||||
{ label: "Home", link: "/dashboard" },
|
{ label: "Home", link: "/dashboard" },
|
||||||
{ label: "Projects", link: null },
|
{ 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 mb-4">
|
||||||
<div className="d-flex flex-wrap justify-content-between align-items-start">
|
|
||||||
<div className="d-flex flex-wrap align-items-start">
|
<div className="d-flex flex-wrap align-items-start">
|
||||||
<div className="flex-grow-1 me-2 mb-2">
|
<div className="flex-grow-1 me-2 mb-2">
|
||||||
<input
|
<input
|
||||||
@ -222,39 +220,41 @@ const ProjectList = () => {
|
|||||||
<div className="d-flex gap-2 mb-2">
|
<div className="d-flex gap-2 mb-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn btn-sm p-1 ${
|
className={`btn btn-sm ${
|
||||||
!listView ? "btn-primary" : "btn-outline-primary"
|
!listView ? "btn-primary" : "btn-outline-primary"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setListView(false)}
|
onClick={() => setListView(false)}
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-offset="0,8"
|
||||||
|
data-bs-placement="top"
|
||||||
data-bs-custom-class="tooltip"
|
data-bs-custom-class="tooltip"
|
||||||
title="Card View"
|
title="Card View"
|
||||||
>
|
>
|
||||||
<i className="bx bx-grid-alt fs-5"></i>
|
<i className="bx bx-grid-alt bx-sm"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn btn-sm p-1 ${
|
className={`btn btn-sm ${
|
||||||
listView ? "btn-primary" : "btn-outline-primary"
|
listView ? "btn-primary" : "btn-outline-primary"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setListView(true)}
|
onClick={() => setListView(true)}
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-offset="0,8"
|
||||||
|
data-bs-placement="top"
|
||||||
data-bs-custom-class="tooltip"
|
data-bs-custom-class="tooltip"
|
||||||
title="List View"
|
title="List View"
|
||||||
>
|
>
|
||||||
<i className="bx bx-list-ul fs-5"></i>
|
<i className="bx bx-list-ul bx-sm"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="dropdown ms-3 mt-1">
|
<div className="dropdown ms-3">
|
||||||
<a
|
<a
|
||||||
className="dropdown-toggle hide-arrow cursor-pointer p-1 mt-3 "
|
className="dropdown-toggle hide-arrow cursor-pointer"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
data-bs-custom-class="tooltip"
|
|
||||||
title="Filter"
|
|
||||||
>
|
>
|
||||||
<i className="fa-solid fa-filter fs-4"></i>
|
<i className="bx bx-filter bx-lg"></i>
|
||||||
</a>
|
</a>
|
||||||
<ul className="dropdown-menu p-2 text-capitalize">
|
<ul className="dropdown-menu p-2 text-capitalize">
|
||||||
{[
|
{[
|
||||||
@ -294,32 +294,26 @@ const ProjectList = () => {
|
|||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="tooltip"
|
className={`btn btn-sm btn-primary ${
|
||||||
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"
|
!HasManageProject && "d-none"
|
||||||
}`}
|
}`}
|
||||||
onClick={handleShow}
|
onClick={handleShow}
|
||||||
>
|
>
|
||||||
<i className="bx bx-plus fs-4 text-white"></i>
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
|
Create New Project
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{loading && <p className="text-center">Loading...</p>}
|
{loading && <p className="text-center">Loading...</p>}
|
||||||
{!loading && filteredProjects.length === 0 && !listView && (
|
{!loading && filteredProjects.length === 0 && !listView && (
|
||||||
<p className="text-center text-muted">No projects found.</p>
|
<p className="text-center text-muted">No projects found.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="row">
|
||||||
{listView ? (
|
{listView ? (
|
||||||
<div className="card cursor-pointer">
|
|
||||||
<div className="card-body p-2">
|
|
||||||
<div className="table-responsive text-nowrap py-2 ">
|
<div className="table-responsive text-nowrap py-2 ">
|
||||||
<table className="table m-3">
|
<table className="table px-2">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-start" colSpan={5}>
|
<th className="text-start" colSpan={5}>
|
||||||
@ -402,20 +396,17 @@ const ProjectList = () => {
|
|||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>{" "}
|
|
||||||
</div>{" "}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="row">
|
currentItems.map((project) => (
|
||||||
{currentItems.map((project) => (
|
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
key={project.id}
|
key={project.id}
|
||||||
projectData={project}
|
projectData={project}
|
||||||
recall={sortingProject}
|
recall={sortingProject}
|
||||||
/>
|
/>
|
||||||
))}
|
))
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{!loading && totalPages > 1 && (
|
{!loading && totalPages > 1 && (
|
||||||
<nav>
|
<nav>
|
||||||
|
|||||||
@ -110,14 +110,14 @@ const ProjectListView = ({ projectData, recall }) => {
|
|||||||
|
|
||||||
<tr className="py-8">
|
<tr className="py-8">
|
||||||
<td className="text-start" colSpan={5}>
|
<td className="text-start" colSpan={5}>
|
||||||
<span
|
<strong
|
||||||
className="text-primary cursor-pointer"
|
className="text-primary cursor-pointer"
|
||||||
onClick={() => navigate(`/projects/${projectInfo.id}`)}
|
onClick={() => navigate(`/projects/${projectInfo.id}`)}
|
||||||
>
|
>
|
||||||
{projectInfo.shortName
|
{projectInfo.shortName
|
||||||
? `${projectInfo.name} (${projectInfo.shortName})`
|
? `${projectInfo.name} (${projectInfo.shortName})`
|
||||||
: projectInfo.name}
|
: projectInfo.name}
|
||||||
</span>
|
</strong>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-start small">{projectInfo.contactPerson}</td>
|
<td className="text-start small">{projectInfo.contactPerson}</td>
|
||||||
<td className="small text-center">
|
<td className="small text-center">
|
||||||
|
|||||||
@ -1,20 +1,18 @@
|
|||||||
import { api } from "../utils/axiosClient";
|
import { api } from "../utils/axiosClient";
|
||||||
|
|
||||||
const AuthRepository = {
|
const AuthRepository = {
|
||||||
// Public routes (no auth token required)
|
login: (data) => api.post("/api/auth/login", data),
|
||||||
login: (data) => api.postPublic("/api/auth/login", data),
|
refreshToken: (data) => api.post("/api/auth/refresh-token", 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),
|
logout: (data) => api.post("/api/auth/logout", data),
|
||||||
profile: () => api.get("/api/user/profile"),
|
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 ),
|
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)
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AuthRepository;
|
export default AuthRepository;
|
||||||
|
|||||||
@ -32,7 +32,4 @@ export const DirectoryRepository = {
|
|||||||
UpdateNote: (id, data) => api.put(`/api/directory/note/${id}`, data),
|
UpdateNote: (id, data) => api.put(`/api/directory/note/${id}`, data),
|
||||||
DeleteNote: (id, isActive) =>
|
DeleteNote: (id, isActive) =>
|
||||||
api.delete(`/api/directory/note/${id}?active=${isActive}`),
|
api.delete(`/api/directory/note/${id}?active=${isActive}`),
|
||||||
|
|
||||||
GetNotes: (pageSize, pageNumber) =>
|
|
||||||
api.get(`/api/directory/notes?pageSize=${pageSize}&pageNumber=${pageNumber}`),
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -70,7 +70,7 @@ export function startSignalR(loggedUser) {
|
|||||||
cacheData("hasReceived", false);
|
cacheData("hasReceived", false);
|
||||||
eventBus.emit("assign_project_one", data);
|
eventBus.emit("assign_project_one", data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.error("Error in cacheData:", e);
|
console.error("Error in cacheData:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
eventBus.emit("assign_project_all", data);
|
eventBus.emit("assign_project_all", data);
|
||||||
@ -96,7 +96,9 @@ export function startSignalR(loggedUser) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.start();
|
.start()
|
||||||
|
.then(() => console.log("SignalR connected"))
|
||||||
|
.catch((err) => console.error("SignalR error:", err));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stopSignalR() {
|
export function stopSignalR() {
|
||||||
|
|||||||
@ -7,22 +7,18 @@ import { BASE_URL } from "./constants";
|
|||||||
const base_Url = BASE_URL
|
const base_Url = BASE_URL
|
||||||
|
|
||||||
export const axiosClient = axios.create({
|
export const axiosClient = axios.create({
|
||||||
baseURL: base_Url,
|
baseURL: base_Url, // Your Web API URL
|
||||||
withCredentials: false,
|
withCredentials: false, // Required if the API uses cookies
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json", // Specify the content type
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto retry failed requests (e.g., network issues)
|
|
||||||
axiosRetry(axiosClient, { retries: 3 });
|
axiosRetry(axiosClient, { retries: 3 });
|
||||||
|
|
||||||
// Request Interceptor — Add Bearer token if required
|
// Request interceptor to add Bearer token
|
||||||
axiosClient.interceptors.request.use(
|
axiosClient.interceptors.request.use(
|
||||||
async (config) => {
|
async (config) => {
|
||||||
const requiresAuth = config.authRequired !== false; // default to true
|
if (config.authRequired) {
|
||||||
|
|
||||||
if (requiresAuth) {
|
|
||||||
const token = localStorage.getItem("jwtToken");
|
const token = localStorage.getItem("jwtToken");
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers["Authorization"] = `Bearer ${token}`;
|
config.headers["Authorization"] = `Bearer ${token}`;
|
||||||
@ -31,24 +27,25 @@ axiosClient.interceptors.request.use(
|
|||||||
config._retry = false;
|
config._retry = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => Promise.reject(error)
|
(error) => Promise.reject(error)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🔄 Response Interceptor — Handle 401, refresh token, etc.
|
// // Response interceptor to handle responses globally (optional)
|
||||||
|
// Add an interceptor to handle expired tokens
|
||||||
axiosClient.interceptors.response.use(
|
axiosClient.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
|
|
||||||
async (error) => {
|
async (error) => {
|
||||||
const originalRequest = error.config;
|
const originalRequest = error.config;
|
||||||
|
|
||||||
// Skip retry for public requests or already retried ones
|
// Prevent infinite loop
|
||||||
if (!originalRequest || originalRequest._retry || originalRequest.authRequired === false) {
|
if (!originalRequest || originalRequest._retry) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid showing multiple toasts
|
// Only show one toast per request
|
||||||
if (!originalRequest._toastShown) {
|
if (!originalRequest._toastShown) {
|
||||||
originalRequest._toastShown = true;
|
originalRequest._toastShown = true;
|
||||||
|
|
||||||
@ -64,6 +61,7 @@ axiosClient.interceptors.response.use(
|
|||||||
const isRefreshRequest = error.config.url.includes("refresh-token");
|
const isRefreshRequest = error.config.url.includes("refresh-token");
|
||||||
|
|
||||||
if (status === 401 && !isRefreshRequest) {
|
if (status === 401 && !isRefreshRequest) {
|
||||||
|
// Mark as retried to avoid loops
|
||||||
originalRequest._retry = true;
|
originalRequest._retry = true;
|
||||||
|
|
||||||
const refreshToken = localStorage.getItem("refreshToken");
|
const refreshToken = localStorage.getItem("refreshToken");
|
||||||
@ -76,7 +74,7 @@ axiosClient.interceptors.response.use(
|
|||||||
stopSignalR();
|
stopSignalR();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Refresh token call
|
// Refresh token
|
||||||
const res = await axiosClient.post("/api/Auth/refresh-token", {
|
const res = await axiosClient.post("/api/Auth/refresh-token", {
|
||||||
token: localStorage.getItem("jwtToken"),
|
token: localStorage.getItem("jwtToken"),
|
||||||
refreshToken,
|
refreshToken,
|
||||||
@ -84,14 +82,16 @@ axiosClient.interceptors.response.use(
|
|||||||
|
|
||||||
const { token, refreshToken: newRefreshToken } = res.data.data;
|
const { token, refreshToken: newRefreshToken } = res.data.data;
|
||||||
|
|
||||||
// Save updated tokens
|
// Save new tokens
|
||||||
localStorage.setItem("jwtToken", token);
|
localStorage.setItem("jwtToken", token);
|
||||||
localStorage.setItem("refreshToken", newRefreshToken);
|
localStorage.setItem("refreshToken", newRefreshToken);
|
||||||
|
|
||||||
startSignalR()
|
startSignalR()
|
||||||
// Set Authorization header
|
// Set Authorization header
|
||||||
originalRequest.headers["Authorization"] = `Bearer ${token}`;
|
originalRequest.headers["Authorization"] = `Bearer ${token}`;
|
||||||
return axiosClient(originalRequest);
|
|
||||||
|
// Optional: Instead of retrying, you may choose to reload app or go to home
|
||||||
|
return axiosClient(originalRequest); // <== only retry once
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
redirectToLogin();
|
redirectToLogin();
|
||||||
return Promise.reject(refreshError);
|
return Promise.reject(refreshError);
|
||||||
@ -101,12 +101,11 @@ axiosClient.interceptors.response.use(
|
|||||||
showToast("An unknown error occurred.", "error");
|
showToast("An unknown error occurred.", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generic API function
|
// Generic API Call
|
||||||
const apiRequest = async (method, url, data = {}, config = {}) => {
|
const apiRequest = async (method, url, data = {}, config = {}) => {
|
||||||
try {
|
try {
|
||||||
const response = await axiosClient({
|
const response = await axiosClient({
|
||||||
@ -122,16 +121,15 @@ const apiRequest = async (method, url, data = {}, config = {}) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Exported API wrapper
|
|
||||||
export const api = {
|
export const api = {
|
||||||
// Public routes (no token required)
|
// For public routes like login, set authRequired: false
|
||||||
postPublic: (url, data = {}, customHeaders = {}) =>
|
postPublic: (url, data = {}, customHeaders = {}) =>
|
||||||
apiRequest("post", url, data, {
|
apiRequest("post", url, data, {
|
||||||
headers: { ...customHeaders },
|
headers: { ...customHeaders },
|
||||||
authRequired: false,
|
authRequired: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Authenticated routes
|
// For protected routes, authRequired defaults to true
|
||||||
get: (url, params = {}, customHeaders = {}) =>
|
get: (url, params = {}, customHeaders = {}) =>
|
||||||
apiRequest("get", url, params, {
|
apiRequest("get", url, params, {
|
||||||
headers: { ...customHeaders },
|
headers: { ...customHeaders },
|
||||||
@ -156,8 +154,7 @@ export const api = {
|
|||||||
authRequired: true,
|
authRequired: true,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
//export default axiosClient;
|
||||||
// Redirect helper
|
|
||||||
function redirectToLogin() {
|
function redirectToLogin() {
|
||||||
window.location.href = "/auth/login";
|
window.location.href = "/auth/login";
|
||||||
}
|
}
|
||||||
@ -11,9 +11,9 @@ export const VIEW_PROJECTS = "6ea44136-987e-44ba-9e5d-1cf8f5837ebc"
|
|||||||
|
|
||||||
export const MANAGE_EMPLOYEES = "a97d366a-c2bb-448d-be93-402bd2324566"
|
export const MANAGE_EMPLOYEES = "a97d366a-c2bb-448d-be93-402bd2324566"
|
||||||
|
|
||||||
export const MANAGE_PROJECT_INFRA = "cf2825ad-453b-46aa-91d9-27c124d63373"
|
export const MANAGE_PROJECT_INFRA = "f2aee20a-b754-4537-8166-f9507b44585b"
|
||||||
|
|
||||||
export const VIEW_PROJECT_INFRA = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"
|
export const VIEW_PROJECT_INFRA = "c7b68e33-72f0-474f-bd96-77636427ecc8"
|
||||||
|
|
||||||
export const REGULARIZE_ATTENDANCE ="57802c4a-00aa-4a1f-a048-fd2f70dd44b6"
|
export const REGULARIZE_ATTENDANCE ="57802c4a-00aa-4a1f-a048-fd2f70dd44b6"
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user