React Query Integration for Server State Sync in Clinet #245

Merged
admin merged 60 commits from react-query into main 2025-07-11 11:32:19 +00:00
35 changed files with 1892 additions and 1047 deletions
Showing only changes of commit 2f110b7ead - Show all commits

View File

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

View File

@ -4978,6 +4978,7 @@ fieldset:disabled .btn {
flex: 1 1 auto; 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 {

View File

@ -143,7 +143,7 @@ const Attendance = ({
</tbody> </tbody>
</table> </table>
{!loading > 20 && ( {!loading && filteredData.length > 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

View File

@ -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 ">

View File

@ -44,7 +44,7 @@ const Regularization = ({ handleRequest }) => {
const { currentPage, totalPages, currentItems, paginate } = usePagination( const { currentPage, totalPages, currentItems, paginate } = usePagination(
filteredData, filteredData,
10 20
); );
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" className="table-responsive text-nowrap pb-4"
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 > 10 && ( {!loading && totalPages > 1 && (
<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 mt-3">
<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"
@ -190,4 +190,4 @@ const Regularization = ({ handleRequest }) => {
); );
}; };
export default Regularization; export default Regularization;

View File

@ -17,7 +17,7 @@ const Dashboard = () => {
const { tasksCardData } = useDashboardTasksCardData(); const { tasksCardData } = useDashboardTasksCardData();
return ( return (
<div className="container-xxl flex-grow-1 container-p-y"> <div className="container-fluid mt-3">
<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">

View File

@ -69,6 +69,7 @@ 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",

View File

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

View File

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

View File

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

View File

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

View File

@ -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-xxl flex-grow-1 container-p-y"> <div className="container-fluid">
<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">

View File

@ -222,24 +222,24 @@ const { mutate: updateEmployee, isLoading } = useUpdateEmployee();
reset( reset(
currentEmployee currentEmployee
? { ? {
id: currentEmployee.id || null, id: currentEmployee.id || null,
firstName: currentEmployee.firstName || "", firstName: currentEmployee.firstName || "",
middleName: currentEmployee.middleName || "", middleName: currentEmployee.middleName || "",
lastName: currentEmployee.lastName || "", lastName: currentEmployee.lastName || "",
email: currentEmployee.email || "", email: currentEmployee.email || "",
currentAddress: currentEmployee.currentAddress || "", currentAddress: currentEmployee.currentAddress || "",
birthDate: formatDate(currentEmployee.birthDate) || "", birthDate: formatDate(currentEmployee.birthDate) || "",
joiningDate: formatDate(currentEmployee.joiningDate) || "", joiningDate: formatDate(currentEmployee.joiningDate) || "",
emergencyPhoneNumber: currentEmployee.emergencyPhoneNumber || "", emergencyPhoneNumber: currentEmployee.emergencyPhoneNumber || "",
emergencyContactPerson: emergencyContactPerson:
currentEmployee.emergencyContactPerson || "", currentEmployee.emergencyContactPerson || "",
aadharNumber: currentEmployee.aadharNumber || "", aadharNumber: currentEmployee.aadharNumber || "",
gender: currentEmployee.gender || "", gender: currentEmployee.gender || "",
panNumber: currentEmployee.panNumber || "", panNumber: currentEmployee.panNumber || "",
permanentAddress: currentEmployee.permanentAddress || "", permanentAddress: currentEmployee.permanentAddress || "",
phoneNumber: currentEmployee.phoneNumber || "", phoneNumber: currentEmployee.phoneNumber || "",
jobRoleId: currentEmployee.jobRoleId?.toString() || "", jobRoleId: currentEmployee.jobRoleId?.toString() || "",
} }
: {} // Empty object resets the form : {} // Empty object resets the form
); );
setCurrentAddressLength(currentEmployee?.currentAddress?.length || 0); setCurrentAddressLength(currentEmployee?.currentAddress?.length || 0);
@ -274,378 +274,378 @@ const { mutate: updateEmployee, isLoading } = useUpdateEmployee();
<div className="col-sm-4"> <div className="col-sm-4">
<div className="form-text text-start">Middle Name</div> <div className="form-text text-start">Middle Name</div>
<input <input
type="text" type="text"
{...register("middleName")} {...register("middleName")}
className="form-control form-control-sm" className="form-control form-control-sm"
id="middleName" id="middleName"
placeholder="Middle Name" placeholder="Middle Name"
/> />
{errors.middleName && ( {errors.middleName && (
<div <div
className="danger-text text-start " className="danger-text text-start "
style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
> >
{errors.middleName.message} {errors.middleName.message}
</div> </div>
)} )}
</div> </div>
<div className="col-sm-4"> <div className="col-sm-4">
<div className="form-text text-start">Last Name</div> <div className="form-text text-start">Last Name</div>
<input <input
type="text" type="text"
{...register("lastName")} {...register("lastName")}
className="form-control form-control-sm" className="form-control form-control-sm"
id="lastName" id="lastName"
placeholder="Last Name" placeholder="Last Name"
/> />
{errors.lastName && ( {errors.lastName && (
<div <div
className="danger-text text-start" className="danger-text text-start"
style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
> >
{errors.lastName.message} {errors.lastName.message}
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className="row mb-3"> <div className="row mb-3">
<div className="col-sm-6"> <div className="col-sm-6">
<div className="form-text text-start">Email</div> <div className="form-text text-start">Email</div>
<input <input
type="email" type="email"
id="email" id="email"
{...register("email")} {...register("email")}
className="form-control form-control-sm" className="form-control form-control-sm"
placeholder="example@domain.com" placeholder="example@domain.com"
maxLength={80} maxLength={80}
aria-describedby="Email" aria-describedby="Email"
disabled={!!currentEmployee?.email} disabled={!!currentEmployee?.email}
/> />
{errors.email && ( {errors.email && (
<div <div
className="danger-text text-start" className="danger-text text-start"
style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
> >
{errors.email.message} {errors.email.message}
</div> </div>
)} )}
</div> </div>
<div className="col-sm-6"> <div className="col-sm-6">
<div className="form-text text-start">Phone Number</div> <div className="form-text text-start">Phone Number</div>
<input <input
type="text" type="text"
keyboardType="numeric" keyboardType="numeric"
id="phoneNumber" id="phoneNumber"
{...register("phoneNumber")} {...register("phoneNumber")}
className="form-control form-control-sm" className="form-control form-control-sm"
placeholder="Phone Number" placeholder="Phone Number"
inputMode="numeric" inputMode="numeric"
maxLength={10} maxLength={10}
/> />
{errors.phoneNumber && ( {errors.phoneNumber && (
<div <div
className="danger-text text-start" className="danger-text text-start"
style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
> >
{errors.phoneNumber.message} {errors.phoneNumber.message}
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className="row mb-3"></div> <div className="row mb-3"></div>
<div className="row mb-3"> <div className="row mb-3">
<div className="col-sm-4"> <div className="col-sm-4">
<div className="form-text text-start">Gender</div> <div className="form-text text-start">Gender</div>
<div className="input-group input-group-merge "> <div className="input-group input-group-merge ">
<select <select
className="form-select form-select-sm " className="form-select form-select-sm "
{...register("gender")} {...register("gender")}
id="gender" id="gender"
aria-label="" aria-label=""
> >
<option disabled value=""> <option disabled value="">
Select Gender Select Gender
</option> </option>
<option value="Male">Male </option> <option value="Male">Male </option>
<option value="Female">Female</option> <option value="Female">Female</option>
<option value="Other">Other</option> <option value="Other">Other</option>
</select> </select>
</div> </div>
{errors.gender && ( {errors.gender && (
<div <div
className="danger-text text-start" className="danger-text text-start"
style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
> >
{errors.gender.message} {errors.gender.message}
</div> </div>
)} )}
</div> </div>
<div className="col-sm-4"> <div className="col-sm-4">
<div className="form-text text-start">Birth Date</div> <div className="form-text text-start">Birth Date</div>
<div className="input-group input-group-merge "> <div className="input-group input-group-merge ">
<input <input
className="form-control form-control-sm" className="form-control form-control-sm"
type="date" type="date"
{...register("birthDate")} {...register("birthDate")}
id="birthDate" id="birthDate"
/> />
</div> </div>
{errors.birthDate && ( {errors.birthDate && (
<div <div
className="danger-text text-start" className="danger-text text-start"
style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
> >
{errors.birthDate.message} {errors.birthDate.message}
</div> </div>
)} )}
</div> </div>
<div className="col-sm-4"> <div className="col-sm-4">
<div className="form-text text-start">Joining Date</div> <div className="form-text text-start">Joining Date</div>
<div className="input-group input-group-merge "> <div className="input-group input-group-merge ">
<input <input
className="form-control form-control-sm" className="form-control form-control-sm"
type="date" type="date"
{...register("joiningDate")} {...register("joiningDate")}
id="joiningDate" id="joiningDate"
/> />
</div> </div>
{errors.joiningDate && ( {errors.joiningDate && (
<div <div
className="danger-text text-start" className="danger-text text-start"
style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
> >
{errors.joiningDate.message} {errors.joiningDate.message}
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className="row mb-3"> <div className="row mb-3">
<div className="col-sm-6"> <div className="col-sm-6">
<div className="form-text text-start">Current Address</div> <div className="form-text text-start">Current Address</div>
<textarea <textarea
id="currentAddress" id="currentAddress"
className="form-control form-control-sm" className="form-control form-control-sm"
placeholder="Current Address" placeholder="Current Address"
aria-label="Current Address" aria-label="Current Address"
aria-describedby="basic-icon-default-message2" aria-describedby="basic-icon-default-message2"
{...register("currentAddress")} {...register("currentAddress")}
maxLength={500} maxLength={500}
onChange={(e) => { onChange={(e) => {
setCurrentAddressLength(e.target.value.length); setCurrentAddressLength(e.target.value.length);
// let react-hook-form still handle it // let react-hook-form still handle it
register("currentAddress").onChange(e); register("currentAddress").onChange(e);
}} }}
></textarea> ></textarea>
<div className="text-end muted"> <div className="text-end muted">
<small> <small>
{" "} {" "}
{500 - currentAddressLength} characters left {500 - currentAddressLength} characters left
</small> </small>
</div> </div>
{errors.currentAddress && ( {errors.currentAddress && (
<div <div
className="danger-text text-start" className="danger-text text-start"
style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
> >
{errors.currentAddress.message} {errors.currentAddress.message}
</div> </div>
)} )}
</div> </div>
<div className="col-sm-6"> <div className="col-sm-6">
<div className="form-text text-start"> <div className="form-text text-start">
Permanent Address Permanent Address
</div> </div>
<textarea <textarea
id="permanentAddress" id="permanentAddress"
className="form-control form-control-sm" className="form-control form-control-sm"
placeholder="Permanent Address" placeholder="Permanent Address"
aria-label="Permanent Address" aria-label="Permanent Address"
aria-describedby="basic-icon-default-message2" aria-describedby="basic-icon-default-message2"
{...register("permanentAddress")} {...register("permanentAddress")}
maxLength={500} maxLength={500}
onChange={(e) => { onChange={(e) => {
setPermanentAddressLength(e.target.value.length); setPermanentAddressLength(e.target.value.length);
register("permanentAddress").onChange(e); register("permanentAddress").onChange(e);
}} }}
></textarea> ></textarea>
<div className="text-end muted"> <div className="text-end muted">
<small> <small>
{500 - permanentAddressLength} characters left {500 - permanentAddressLength} characters left
</small> </small>
</div> </div>
{errors.permanentAddress && ( {errors.permanentAddress && (
<div <div
className="danger-text text-start" className="danger-text text-start"
style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
> >
{errors.permanentAddress.message} {errors.permanentAddress.message}
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className="row mb-3"> <div className="row mb-3">
{" "} {" "}
<div className="divider"> <div className="divider">
<div className="divider-text">Other Information</div> <div className="divider-text">Other Information</div>
</div> </div>
</div> </div>
<div className="row mb-3"> <div className="row mb-3">
<div className="col-sm-4"> <div className="col-sm-4">
<div className="form-text text-start">Role</div> <div className="form-text text-start">Role</div>
<div className="input-group input-group-merge "> <div className="input-group input-group-merge ">
<select <select
className="form-select form-select-sm" className="form-select form-select-sm"
{...register("jobRoleId")} {...register("jobRoleId")}
id="jobRoleId" id="jobRoleId"
aria-label="" aria-label=""
> >
<option disabled value=""> <option disabled value="">
Select Role Select Role
</option> </option>
{[...job_role] {[...job_role]
.sort((a, b) => a?.name?.localeCompare(b.name)) .sort((a, b) => a?.name?.localeCompare(b.name))
.map((item) => ( .map((item) => (
<option value={item?.id} key={item.id}> <option value={item?.id} key={item.id}>
{item?.name}{" "} {item?.name}{" "}
</option> </option>
))} ))}
</select> </select>
</div> </div>
{errors.jobRoleId && ( {errors.jobRoleId && (
<div <div
className="danger-text text-start" className="danger-text text-start"
style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
> >
{errors.jobRoleId.message} {errors.jobRoleId.message}
</div> </div>
)} )}
</div> </div>
<div className="col-sm-4"> <div className="col-sm-4">
<div className="form-text text-start"> <div className="form-text text-start">
Emergency Contact Person Emergency Contact Person
</div> </div>
<input <input
type="text" type="text"
{...register("emergencyContactPerson")} {...register("emergencyContactPerson")}
className="form-control form-control-sm" className="form-control form-control-sm"
id="emergencyContactPerson" id="emergencyContactPerson"
maxLength={50} maxLength={50}
placeholder="Contact Person" placeholder="Contact Person"
/> />
{errors.emergencyContactPerson && ( {errors.emergencyContactPerson && (
<div <div
className="danger-text text-start" className="danger-text text-start"
style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
> >
{errors.emergencyContactPerson.message} {errors.emergencyContactPerson.message}
</div> </div>
)} )}
</div> </div>
<div className="col-sm-4"> <div className="col-sm-4">
<div className="form-text text-start"> <div className="form-text text-start">
Emergency Phone Number Emergency Phone Number
</div> </div>
<input <input
type="text" type="text"
{...register("emergencyPhoneNumber")} {...register("emergencyPhoneNumber")}
className="form-control form-control-sm phone-mask" className="form-control form-control-sm phone-mask"
id="emergencyPhoneNumber" id="emergencyPhoneNumber"
placeholder="Phone Number" placeholder="Phone Number"
inputMode="numeric" inputMode="numeric"
maxLength={10} maxLength={10}
/> />
{errors.emergencyPhoneNumber && ( {errors.emergencyPhoneNumber && (
<div <div
className="danger-text text-start" className="danger-text text-start"
style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
> >
{errors.emergencyPhoneNumber.message} {errors.emergencyPhoneNumber.message}
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className="row mb-3 d-none"> <div className="row mb-3 d-none">
<div className="col-sm-6"> <div className="col-sm-6">
<div className="form-text text-start">AADHAR Number</div> <div className="form-text text-start">AADHAR Number</div>
<input <input
type="text" type="text"
{...register("aadharNumber")} {...register("aadharNumber")}
className="form-control form-control-sm" className="form-control form-control-sm"
id="aadharNumber" id="aadharNumber"
placeholder="AADHAR Number" placeholder="AADHAR Number"
maxLength={12} maxLength={12}
inputMode="numeric" inputMode="numeric"
/> />
{errors.aadharNumber && ( {errors.aadharNumber && (
<div className="danger-text text-start"> <div className="danger-text text-start">
{errors.aadharNumber.message} {errors.aadharNumber.message}
</div> </div>
)} )}
</div> </div>
<div className="col-sm-6 d-none"> <div className="col-sm-6 d-none">
<div className="form-text text-start">PAN Number</div> <div className="form-text text-start">PAN Number</div>
<input <input
type="text" type="text"
{...register("panNumber")} {...register("panNumber")}
className="form-control form-control-sm" className="form-control form-control-sm"
id="panNumber" id="panNumber"
placeholder="PAN Number" placeholder="PAN Number"
maxLength={10} maxLength={10}
/> />
{errors.panNumber && ( {errors.panNumber && (
<div <div
className="danger-text text-start" className="danger-text text-start"
style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
> >
{errors.panNumber.message} {errors.panNumber.message}
</div> </div>
)} )}
</div> </div>
</div> </div>
{employeeId && ( {employeeId && (
<div className="row mb-3 d-none"> <div className="row mb-3 d-none">
<div className="col-sm-12"> <div className="col-sm-12">
<input type="text" name="id" {...register("id")} /> <input type="text" name="id" {...register("id")} />
</div> </div>
</div> </div>
)} )}
<div className="row justify-content-start"> <div className="row justify-content-start">
<div className="col-sm-12"> <div className="col-sm-12">
<button <button
aria-label="manage employee" aria-label="manage employee"
type="submit" type="submit"
className="btn btn-sm btn-primary" className="btn btn-sm btn-primary"
disabled={isloading} disabled={isloading}
> >
{isloading {isloading
? "Please Wait..." ? "Please Wait..."
: employeeId : employeeId
? "Update" ? "Update"
: "Create"} : "Create"}
</button> </button>
<button
aria-label="manage employee"
type="reset"
className="btn btn-sm btn-primary ms-2"
disabled={isloading}
>
Clear
</button>
</div>
</div>
</form>
<button
aria-label="manage employee"
type="reset"
className="btn btn-sm btn-primary ms-2"
disabled={isloading}
>
Clear
</button>
</div>
</div>
</form>
</> </>
); );
}; };

View File

@ -103,7 +103,8 @@ 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) => {
@ -158,7 +159,7 @@ const Header = () => {
return ( return (
<nav <nav
className="layout-navbar container-xxl navbar navbar-expand-xl navbar-detached align-items-center bg-navbar-theme" className="layout-navbar container-fluid mb-3 navbar navbar-expand-xl navbar-detached align-items-center bg-navbar-theme"
id="layout-navbar" 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">
@ -175,11 +176,11 @@ const Header = () => {
> >
{projectNames?.length > 0 && ( {projectNames?.length > 0 && (
<div className=" align-items-center"> <div className=" align-items-center">
{!isProjectPath && ( {(!isProjectPath && !isDirectoryPath) && (
<> <>
<i <i
className="rounded-circle bx bx-building-house" className="rounded-circle bx bx-building-house bx-sm-lg bx-md"
style={{ fontSize: "xx-large" }}
></i> ></i>
<div className="btn-group"> <div className="btn-group">
<button <button
@ -199,7 +200,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
@ -210,7 +211,7 @@ const Header = () => {
> >
{project?.name}{" "} {project?.name}{" "}
{project?.shortName ? ( {project?.shortName ? (
<span className="text-primary fw-semibold"> <span className="text-primary fw-semibold ">
{" "} {" "}
({project?.shortName}) ({project?.shortName})
</span> </span>
@ -270,7 +271,7 @@ const Header = () => {
</div> </div>
<div className="dropdown-shortcuts-item col"> <div className="dropdown-shortcuts-item col">
<a <a
onClick={() => navigate(`/projectNames`)} onClick={() => navigate(`/projects`)}
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">

View File

@ -20,16 +20,17 @@ const taskSchema = z.object({
const defaultModel = { const defaultModel = {
id: null, id: null,
buildingID: "0", buildingID: "", // Changed from "0"
floorId: "0", floorId: "", // Changed from "0"
workAreaId: "0", workAreaId: "", // Changed from "0"
activityID: null, activityID: "", // Changed from null
workCategoryId: "", workCategoryId: "", // Kept as empty
plannedWork: 0, plannedWork: 0,
completedWork: 0, completedWork: 0,
comment:"" comment: ""
}; };
const TaskModel = ({ const TaskModel = ({
project, project,
onSubmit, onSubmit,
@ -86,7 +87,7 @@ const TaskModel = ({
reset((prev) => ({ reset((prev) => ({
...prev, ...prev,
floorId: value, floorId: value,
workAreaId: 0, workAreaId: "",
activityID: "", activityID: "",
workCategoryId: categoryData?.[0]?.id?.toString() ?? "", workCategoryId: categoryData?.[0]?.id?.toString() ?? "",
})); }));
@ -193,7 +194,7 @@ const TaskModel = ({
{...register("buildingID")} {...register("buildingID")}
onChange={handleBuildingChange} onChange={handleBuildingChange}
> >
<option value="0">Select Building</option> <option value="">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))
@ -225,7 +226,7 @@ const TaskModel = ({
{...register("floorId")} {...register("floorId")}
onChange={handleFloorChange} onChange={handleFloorChange}
> >
<option value="0">Select Floor</option> <option value="">Select Floor</option>
{selectedBuilding.floors {selectedBuilding.floors
?.filter( ?.filter(
(floor) => (floor) =>
@ -261,7 +262,7 @@ const TaskModel = ({
{...register("workAreaId")} {...register("workAreaId")}
onChange={handleWorkAreaChange} onChange={handleWorkAreaChange}
> >
<option value="0">Select Work Area</option> <option value="">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))
@ -441,4 +442,4 @@ const TaskModel = ({
); );
}; };
export default TaskModel; export default TaskModel;

View File

@ -5,8 +5,8 @@ const Breadcrumb = ({ data }) => {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb" >
<ol className="breadcrumb breadcrumb-custom-icon"> <ol className="breadcrumb breadcrumb-custom-icon my-3">
{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}>

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,7 @@ 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);
@ -31,11 +32,17 @@ 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 [listView, setListView] = useState(false); const [viewType, setViewType] = useState("notes");
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([]);
@ -71,8 +78,6 @@ 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) {
@ -249,12 +254,13 @@ 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-xxl flex-grow-1 container-p-y"> <div className="container-fluid">
{IsPage && ( {IsPage && (
<Breadcrumb <Breadcrumb
data={[ data={[
@ -326,84 +332,74 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
</GlobalModel> </GlobalModel>
)} )}
<div className="card p-2 card-minHeight"> <div className="card p-0 mb-0">
<DirectoryPageHeader <div className="card-body p-1 pb-0">
searchText={searchText} <DirectoryPageHeader
setSearchText={setSearchText} searchText={searchText}
setIsActive={setIsActive} setSearchText={setSearchText}
listView={listView} setIsActive={setIsActive}
setListView={setListView} viewType={viewType}
filteredBuckets={filteredBuckets} setViewType={setViewType}
tempSelectedBucketIds={tempSelectedBucketIds} filteredBuckets={filteredBuckets}
handleTempBucketChange={handleTempBucketChange} tempSelectedBucketIds={tempSelectedBucketIds}
filteredCategories={filteredCategories} handleTempBucketChange={handleTempBucketChange}
tempSelectedCategoryIds={tempSelectedCategoryIds} filteredCategories={filteredCategories}
handleTempCategoryChange={handleTempCategoryChange} tempSelectedCategoryIds={tempSelectedCategoryIds}
clearFilter={clearFilter} handleTempCategoryChange={handleTempCategoryChange}
applyFilter={applyFilter} clearFilter={clearFilter}
loading={loading} applyFilter={applyFilter}
IsActive={IsActive} loading={loading}
setOpenBucketModal={setOpenBucketModal} IsActive={IsActive}
/> setOpenBucketModal={setOpenBucketModal}
contactsToExport={contacts}
{/* Messages when listView is false */} notesToExport={notes}
{!listView && ( selectedNoteNames={selectedNoteNames}
<div className="d-flex flex-column justify-content-center align-items-center text-center "> setSelectedNoteNames={setSelectedNoteNames}
{loading && <p className="mt-10">Loading...</p>} notesForFilter={notes}
{!loading && contacts?.length === 0 && ( setFilterAppliedNotes={setFilterAppliedNotes}
/>
</div>
</div>
<div className="card-minHeight mt-0">
{(viewType === "card" || viewType === "list" || viewType === "notes") && (
<div className="d-flex flex-column justify-content-center align-items-center text-center">
{!loading && (viewType === "card" || viewType === "list") && contacts?.length === 0 && (
<p className="mt-10">No contact found</p> <p className="mt-10">No contact found</p>
)} )}
{!loading && contacts?.length > 0 && currentItems.length === 0 && ( {!loading &&
<p className="mt-10">No matching contact found</p> (viewType === "card" || viewType === "list") &&
)} contacts?.length > 0 &&
currentItems.length === 0 && (
<p className="mt-10">No matching contact found</p>
)}
</div> </div>
)} )}
{/* Table view (listView === true) */} {viewType === "list" && (
{listView ? ( <div className="card cursor-pointer mt-5">
<DirectoryListTableHeader> <div className="card-body p-2 pb-1">
{loading && ( <DirectoryListTableHeader>
<tr> {!loading &&
<td colSpan={10}> currentItems.map((contact) => (
{" "} <ListViewDirectory
<p className="mt-10">Loading...</p>{" "} key={contact.id}
</td> IsActive={IsActive}
</tr> contact={contact}
)} setSelectedContact={setSelectedContact}
setIsOpenModal={setIsOpenModal}
setOpen_contact={setOpen_contact}
setIsOpenModalNote={setIsOpenModalNote}
IsDeleted={setDeleteContact}
restore={handleDeleteContact}
/>
))}
</DirectoryListTableHeader>
</div>
</div>
)}
{!loading && contacts?.length === 0 && ( {viewType === "card" && (
<tr> <div className="row mt-4">
<td colSpan={10}>
<p className="mt-10">No contact found</p>
</td>
</tr>
)}
{!loading && currentItems.length === 0 && contacts?.length > 0 && (
<tr>
<td colSpan={10}>
<p className="mt-10">No matching contact found</p>
</td>
</tr>
)}
{!loading &&
currentItems.map((contact) => (
<ListViewDirectory
key={contact.id}
IsActive={IsActive}
contact={contact}
setSelectedContact={setSelectedContact}
setIsOpenModal={setIsOpenModal}
setOpen_contact={setOpen_contact}
setIsOpenModalNote={setIsOpenModalNote}
IsDeleted={setDeleteContact}
restore={handleDeleteContact}
/>
))}
</DirectoryListTableHeader>
) : (
<div className="row mt-5">
{!loading && {!loading &&
currentItems.map((contact) => ( currentItems.map((contact) => (
<div <div
@ -425,15 +421,26 @@ 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 <li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
className={`page-item ${currentPage === 1 ? "disabled" : ""}`}
>
<button <button
className="page-link btn-xs" className="page-link btn-xs"
onClick={() => paginate(currentPage - 1)} onClick={() => paginate(currentPage - 1)}
@ -445,9 +452,8 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
{[...Array(totalPages)].map((_, index) => ( {[...Array(totalPages)].map((_, index) => (
<li <li
key={index} key={index}
className={`page-item ${ className={`page-item ${currentPage === index + 1 ? "active" : ""
currentPage === index + 1 ? "active" : "" }`}
}`}
> >
<button <button
className="page-link" className="page-link"
@ -458,11 +464,7 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
</li> </li>
))} ))}
<li <li className={`page-item ${currentPage === totalPages ? "disabled" : ""}`}>
className={`page-item ${
currentPage === totalPages ? "disabled" : ""
}`}
>
<button <button
className="page-link" className="page-link"
onClick={() => paginate(currentPage + 1)} onClick={() => paginate(currentPage + 1)}
@ -478,4 +480,4 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
); );
}; };
export default Directory; export default Directory;

View File

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

View File

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

View File

@ -19,13 +19,13 @@ const LoginPage = () => {
const loginSchema = IsLoginWithOTP const loginSchema = IsLoginWithOTP
? z.object({ ? z.object({
username: z.string().email({ message: "Valid email required" }), username: z.string().email({ message: "Valid email required" }),
}) })
: z.object({ : z.object({
username: z.string().email({ message: "Valid email required" }), username: z.string().email({ message: "Valid email required" }),
password: z.string().min(1, { message: "Password required" }), password: z.string().min(1, { message: "Password required" }),
rememberMe: z.boolean(), rememberMe: z.boolean(),
}); });
const { const {
register, register,
@ -53,6 +53,7 @@ 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");
@ -114,18 +115,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"> <div className="input-group input-group-merge d-flex align-items-center border rounded px-2">
<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" className="form-control form-control-xl border-0 shadow-none"
placeholder="••••••••••••" placeholder="••••••••••••"
/> />
<button <button
type="button" type="button"
className="btn border-top border-end border-bottom" className="btn btn-link p-0 ms-2 "
onClick={() => setHidepass(!hidepass)} onClick={() => setHidepass(!hidepass)}
style={{ style={{
borderTopLeftRadius: 0, borderTopLeftRadius: 0,
@ -150,6 +151,7 @@ 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

View File

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

View File

@ -12,7 +12,7 @@ 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"; import SuspendEmp from "../../components/Employee/SuspendEmp"; // Keep if you use SuspendEmp
import { import {
exportToCSV, exportToCSV,
exportToExcel, exportToExcel,
@ -29,16 +29,19 @@ import GlobalModel from "../../components/common/GlobalModel";
import usePagination from "../../hooks/usePagination"; import usePagination from "../../hooks/usePagination";
const EmployeeList = () => { const EmployeeList = () => {
const selectedProjectId = useSelector((store) => store.localVariables.projectId); const selectedProjectId = useSelector(
const [selectedProject, setSelectedProject] = useState(() => selectedProjectId || ""); (store) => store.localVariables.projectId
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(showAllEmployees ? null : selectedProject, showInactive); useEmployeesAllOrByProjectId(
const [projectsList, setProjectsList] = useState(projects || []); showAllEmployees ? null : selectedProjectId, // Use selectedProjectId here
showInactive
);
const [employeeList, setEmployeeList] = useState([]); const [employeeList, setEmployeeList] = useState([]);
const [ modelConfig, setModelConfig ] = useState(); const [ modelConfig, setModelConfig ] = useState();
@ -46,7 +49,6 @@ const EmployeeList = () => {
// 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);
@ -66,14 +68,27 @@ const EmployeeList = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const handleSearch = (e) => { /**
const value = e.target.value.toLowerCase(); * Applies the search filter to a given array of employee data.
setSearchText(value); * @param {Array} data - The array of employee objects to filter.
* @param {string} text - The search text.
* @returns {Array} The filtered array.
*/
const applySearchFilter = (data, text) => {
if (!text) {
return data;
}
const lowercasedText = text.toLowerCase().trim(); // Ensure search text is trimmed and lowercase
if (!employeeList.length) return; return data.filter((item) => {
// **IMPROVED FULL NAME CONSTRUCTION**
const firstName = item.firstName || "";
const middleName = item.middleName || "";
const lastName = item.lastName || "";
// Join parts, then trim any excess spaces if a middle name is missing
const fullName = `${firstName} ${middleName} ${lastName}`.toLowerCase().trim().replace(/\s+/g, ' ');
const results = employeeList.filter((item) => {
const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
const email = item.email ? item.email.toLowerCase() : ""; const 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() : "";
@ -85,8 +100,12 @@ const EmployeeList = () => {
jobRole.includes(value) jobRole.includes(value)
); );
}); });
};
setFilteredData(results); const handleSearch = (e) => {
const value = e.target.value;
setSearchText(value);
setCurrentPage(1);
}; };
@ -108,11 +127,11 @@ const EmployeeList = () => {
modalElement.classList.remove("show"); modalElement.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(); document.querySelector(".modal-backdrop")?.remove(); // Use optional chaining for safety
} }
setShowModal(false); setShowModal(false);
clearCacheKey("employeeProfile"); clearCacheKey("employeeProfile");
recallEmployeeData(showInactive); recallEmployeeData(showInactive, showAllEmployees ? null : selectedProjectId); // Use selectedProjectId here
}; };
const handleShow = () => setShowModal(true); const handleShow = () => setShowModal(true);
const handleClose = () => setShowModal( false ); const handleClose = () => setShowModal( false );
@ -194,7 +213,7 @@ const EmployeeList = () => {
const handleToggle = (e) => { const handleToggle = (e) => {
setShowInactive(e.target.checked); setShowInactive(e.target.checked);
recallEmployeeData(e.target.checked); recallEmployeeData(e.target.checked, showAllEmployees ? null : selectedProjectId); // Use selectedProjectId here
}; };
const handleAllEmployeesToggle = (e) => { const handleAllEmployeesToggle = (e) => {
@ -207,8 +226,6 @@ const handleAllEmployeesToggle = (e) => {
} }
}; };
const handleEmployeeModel = (id) => { const handleEmployeeModel = (id) => {
setSelecedEmployeeId(id); setSelecedEmployeeId(id);
setShowModal(true); setShowModal(true);
@ -219,24 +236,19 @@ const handleAllEmployeesToggle = (e) => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
}; };
const handleProjectSelection = (e) => {
const newProjectId = e.target.value;
setSelectedProject(newProjectId);
if (newProjectId) {
setShowAllEmployees(false);
}
};
useEffect(() => { useEffect(() => {
setSelectedProject(selectedProjectId || ""); if (!showAllEmployees) {
}, [selectedProjectId]); recallEmployeeData(showInactive, selectedProjectId);
}
}, [selectedProjectId, showInactive, showAllEmployees, recallEmployeeData]);
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); recallEmployeeData(showInactive, showAllEmployees ? null : selectedProjectId); // Use selectedProjectId here
} }
},[employees] },[employees, showInactive, showAllEmployees, selectedProjectId] // Add all relevant dependencies
); );
useEffect(() => { useEffect(() => {
@ -288,7 +300,7 @@ const handleAllEmployeesToggle = (e) => {
</div> </div>
)} )}
<div className="container-xxl flex-grow-1 container-p-y"> <div className="container-fluid">
<Breadcrumb <Breadcrumb
data={[ data={[
{ label: "Home", link: "/dashboard" }, { label: "Home", link: "/dashboard" },
@ -343,7 +355,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 */} {/* Search Input - ALWAYS ENABLED */}
<div className="dataTables_filter"> <div className="dataTables_filter">
<label className="mb-0"> <label className="mb-0">
<input <input
@ -392,7 +404,7 @@ const handleAllEmployeesToggle = (e) => {
</ul> </ul>
</div> </div>
{/* Add Employee */} {/* Add Employee Button */}
{Manage_Employee && ( {Manage_Employee && (
<button <button
className="btn btn-sm btn-primary" className="btn btn-sm btn-primary"
@ -406,7 +418,6 @@ 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"
@ -501,7 +512,17 @@ const handleAllEmployeesToggle = (e) => {
</td> </td>
</tr> </tr>
)} )}
{!loading && employeeList?.length === 0 && ( {/* Conditional messages for no data or no search results */}
{!loading && displayData?.length === 0 && searchText && !showAllEmployees ? (
<tr>
<td colSpan={8}>
<small className="muted">
'{searchText}' employee not found
</small>{" "}
</td>
</tr>
) : null}
{!loading && displayData?.length === 0 && (!searchText || showAllEmployees) ? (
<tr> <tr>
<td <td
colSpan={8} colSpan={8}
@ -510,72 +531,58 @@ 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>
)}
{currentItems && {/* Render current items */}
!loading && {currentItems && !loading && currentItems.map((item) => (
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"> <Avatar
<Avatar firstName={item.firstName}
firstName={item.firstName} lastName={item.lastName}
lastName={item.lastName} ></Avatar>
></Avatar> <div className="d-flex flex-column">
<div className="d-flex flex-column"> <a
<a onClick={() =>
onClick={() => navigate(
navigate( `/employee/${item.id}?for=attendance`
`/employee/${item.id}?for=attendance` )
) }
} className="text-heading text-truncate cursor-pointer"
className="text-heading text-truncate cursor-pointer" >
> <span className="fw-normal">
<span className="fw-normal"> {item.firstName} {item.middleName}{" "}
{item.firstName} {item.middleName}{" "} {item.lastName}
{item.lastName} </span>
</span> </a>
</a>
</div>
</div> </div>
</td> </div>
<td className="text-start d-none d-sm-table-cell"> </td>
{item.email ? ( <td className="text-start d-none d-sm-table-cell">
<span className="text-truncate"> {item.email ? (
<i className="bx bxs-envelope text-primary me-2"></i>
{item.email}
</span>
) : (
<span className="text-truncate text-italic">
NA
</span>
)}
</td>
<td className="text-start d-none d-sm-table-cell">
<span className="text-truncate"> <span className="text-truncate">
<i className="bx bxs-phone-call text-primary me-2"></i> <i className="bx bxs-envelope text-primary me-2"></i>
{item.phoneNumber} {item.email}
</span> </span>
</td> ) : (
<td className=" d-none d-sm-table-cell text-start"> <span className="text-truncate text-italic">
<span className="text-truncate"> NA
<i className="bx bxs-wrench text-success me-2"></i>
{item.jobRole || "Not Assign Yet"}
</span> </span>
</td> )}
</td>
<td className="text-start d-none d-sm-table-cell">
<span className="text-truncate">
<i className="bx bxs-phone-call text-primary me-2"></i>
{item.phoneNumber}
</span>
</td>
<td className=" d-none d-sm-table-cell text-start">
<span className="text-truncate">
<i className="bx bxs-wrench text-success me-2"></i>
{item.jobRole || "Not Assign Yet"}
</span>
</td>
<td className=" d-none d-md-table-cell"> <td className=" d-none d-md-table-cell">
{moment(item.joiningDate)?.format("DD-MMM-YYYY")} {moment(item.joiningDate)?.format("DD-MMM-YYYY")}
@ -703,7 +710,6 @@ const handleAllEmployeesToggle = (e) => {
</ul> </ul>
</nav> </nav>
)} )}
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

@ -159,219 +159,227 @@ const ProjectList = () => {
</GlobalModel> </GlobalModel>
)} )}
<div className="container-xxl flex-grow-1 container-p-y"> <div className="container-fluid">
<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">
<div className="d-flex flex-wrap align-items-start">
<div className="flex-grow-1 me-2 mb-2">
<input
type="search"
className="form-control form-control-sm"
placeholder="Search projects..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setCurrentPage(1);
}}
/>
</div>
<div className="d-flex flex-wrap justify-content-between align-items-start mb-4"> <div className="d-flex gap-2 mb-2">
<div className="d-flex flex-wrap align-items-start"> <button
<div className="flex-grow-1 me-2 mb-2"> type="button"
<input className={`btn btn-sm p-1 ${
type="search" !listView ? "btn-primary" : "btn-outline-primary"
className="form-control form-control-sm" }`}
placeholder="Search projects..." onClick={() => setListView(false)}
value={searchTerm} data-bs-toggle="tooltip"
onChange={(e) => { data-bs-custom-class="tooltip"
setSearchTerm(e.target.value); title="Card View"
setCurrentPage(1); >
}} <i className="bx bx-grid-alt fs-5"></i>
/> </button>
<button
type="button"
className={`btn btn-sm p-1 ${
listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(true)}
data-bs-toggle="tooltip"
data-bs-custom-class="tooltip"
title="List View"
>
<i className="bx bx-list-ul fs-5"></i>
</button>
</div>
<div className="dropdown ms-3 mt-1">
<a
className="dropdown-toggle hide-arrow cursor-pointer p-1 mt-3 "
data-bs-toggle="dropdown"
aria-expanded="false"
data-bs-custom-class="tooltip"
title="Filter"
>
<i className="fa-solid fa-filter fs-4"></i>
</a>
<ul className="dropdown-menu p-2 text-capitalize">
{[
{
id: "b74da4c2-d07e-46f2-9919-e75e49b12731",
label: "Active",
},
{
id: "603e994b-a27f-4e5d-a251-f3d69b0498ba",
label: "On Hold",
},
{
id: "ef1c356e-0fe0-42df-a5d3-8daee355492d",
label: "Inactive",
},
{
id: "33deaef9-9af1-4f2a-b443-681ea0d04f81",
label: "Completed",
},
].map(({ id, label }) => (
<li key={id}>
<div className="form-check">
<input
className="form-check-input "
type="checkbox"
checked={selectedStatuses.includes(id)}
onChange={() => handleStatusChange(id)}
/>
<label className="form-check-label">{label}</label>
</div>
</li>
))}
</ul>
</div>
</div>
<div>
<button
type="button"
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip"
title="Add New Project"
className={`p-1 me-2 bg-primary rounded-circle ${
!HasManageProject && "d-none"
}`}
onClick={handleShow}
>
<i className="bx bx-plus fs-4 text-white"></i>
</button>
</div>
</div> </div>
<div className="d-flex gap-2 mb-2">
<button
type="button"
className={`btn btn-sm ${
!listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(false)}
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip"
title="Card View"
>
<i className="bx bx-grid-alt bx-sm"></i>
</button>
<button
type="button"
className={`btn btn-sm ${
listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(true)}
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip"
title="List View"
>
<i className="bx bx-list-ul bx-sm"></i>
</button>
</div>
<div className="dropdown ms-3">
<a
className="dropdown-toggle hide-arrow cursor-pointer"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i className="bx bx-filter bx-lg"></i>
</a>
<ul className="dropdown-menu p-2 text-capitalize">
{[
{
id: "b74da4c2-d07e-46f2-9919-e75e49b12731",
label: "Active",
},
{
id: "603e994b-a27f-4e5d-a251-f3d69b0498ba",
label: "On Hold",
},
{
id: "ef1c356e-0fe0-42df-a5d3-8daee355492d",
label: "Inactive",
},
{
id: "33deaef9-9af1-4f2a-b443-681ea0d04f81",
label: "Completed",
},
].map(({ id, label }) => (
<li key={id}>
<div className="form-check">
<input
className="form-check-input "
type="checkbox"
checked={selectedStatuses.includes(id)}
onChange={() => handleStatusChange(id)}
/>
<label className="form-check-label">{label}</label>
</div>
</li>
))}
</ul>
</div>
</div>
<div>
<button
type="button"
className={`btn btn-sm btn-primary ${
!HasManageProject && "d-none"
}`}
onClick={handleShow}
>
<i className="bx bx-plus-circle me-2"></i>
Create New Project
</button>
</div> </div>
</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="table-responsive text-nowrap py-2 "> <div className="card-body p-2">
<table className="table px-2"> <div className="table-responsive text-nowrap py-2 ">
<thead> <table className="table m-3">
<tr> <thead>
<th className="text-start" colSpan={5}>
Project Name
</th>
<th className="mx-2 text-start">Contact Person</th>
<th className="mx-2">START DATE</th>
<th className="mx-2">DEADLINE</th>
<th className="mx-2">Task</th>
<th className="mx-2">Progress</th>
<th className="mx-2">
<div className="dropdown">
<a
className="dropdown-toggle hide-arrow cursor-pointer"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Status <i className="bx bx-filter bx-sm"></i>
</a>
<ul className="dropdown-menu p-2 text-capitalize">
{[
{
id: "b74da4c2-d07e-46f2-9919-e75e49b12731",
label: "Active",
},
{
id: "603e994b-a27f-4e5d-a251-f3d69b0498ba",
label: "On Hold",
},
{
id: "ef1c356e-0fe0-42df-a5d3-8daee355492d",
label: "Inactive",
},
{
id: "33deaef9-9af1-4f2a-b443-681ea0d04f81",
label: "Completed",
},
].map(({ id, label }) => (
<li key={id}>
<div className="form-check">
<input
className="form-check-input "
type="checkbox"
checked={selectedStatuses.includes(id)}
onChange={() => handleStatusChange(id)}
/>
<label className="form-check-label">
{label}
</label>
</div>
</li>
))}
</ul>
</div>
</th>
<th
className={`mx-2 ${
HasManageProject ? "d-sm-table-cell" : "d-none"
}`}
>
Action
</th>
</tr>
</thead>
<tbody className="table-border-bottom-0 overflow-auto ">
{currentItems.length === 0 ? (
<tr> <tr>
<td colSpan="12" className="text-center py-4"> <th className="text-start" colSpan={5}>
No projects found Project Name
</td> </th>
<th className="mx-2 text-start">Contact Person</th>
<th className="mx-2">START DATE</th>
<th className="mx-2">DEADLINE</th>
<th className="mx-2">Task</th>
<th className="mx-2">Progress</th>
<th className="mx-2">
<div className="dropdown">
<a
className="dropdown-toggle hide-arrow cursor-pointer"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Status <i className="bx bx-filter bx-sm"></i>
</a>
<ul className="dropdown-menu p-2 text-capitalize">
{[
{
id: "b74da4c2-d07e-46f2-9919-e75e49b12731",
label: "Active",
},
{
id: "603e994b-a27f-4e5d-a251-f3d69b0498ba",
label: "On Hold",
},
{
id: "ef1c356e-0fe0-42df-a5d3-8daee355492d",
label: "Inactive",
},
{
id: "33deaef9-9af1-4f2a-b443-681ea0d04f81",
label: "Completed",
},
].map(({ id, label }) => (
<li key={id}>
<div className="form-check">
<input
className="form-check-input "
type="checkbox"
checked={selectedStatuses.includes(id)}
onChange={() => handleStatusChange(id)}
/>
<label className="form-check-label">
{label}
</label>
</div>
</li>
))}
</ul>
</div>
</th>
<th
className={`mx-2 ${
HasManageProject ? "d-sm-table-cell" : "d-none"
}`}
>
Action
</th>
</tr> </tr>
) : ( </thead>
currentItems.map((project) => ( <tbody className="table-border-bottom-0 overflow-auto ">
<ProjectListView {currentItems.length === 0 ? (
key={project.id} <tr>
projectData={project} <td colSpan="12" className="text-center py-4">
recall={sortingProject} No projects found
/> </td>
)) </tr>
)} ) : (
</tbody> currentItems.map((project) => (
</table> <ProjectListView
</div> key={project.id}
) : ( projectData={project}
currentItems.map((project) => ( recall={sortingProject}
/>
))
)}
</tbody>
</table>
</div>{" "}
</div>{" "}
</div>
) : (
<div className="row">
{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>

View File

@ -140,14 +140,14 @@ const ProjectListView = ({ projectData, recall }) => {
<tr className={`py-8 ${isPending ? "bg-light opacity-50 pointer-events-none" : ""} `}> <tr className={`py-8 ${isPending ? "bg-light opacity-50 pointer-events-none" : ""} `}>
<td className="text-start" colSpan={5}> <td className="text-start" colSpan={5}>
<strong <span
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}
</strong> </span>
</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">

View File

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

View File

@ -32,4 +32,7 @@ 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}`),
}; };

View File

@ -72,7 +72,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);
@ -107,9 +107,7 @@ 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() {

View File

@ -7,18 +7,22 @@ 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, // Your Web API URL baseURL: base_Url,
withCredentials: false, // Required if the API uses cookies withCredentials: false,
headers: { headers: {
"Content-Type": "application/json", // Specify the content type "Content-Type": "application/json",
}, },
}); });
// Auto retry failed requests (e.g., network issues)
axiosRetry(axiosClient, { retries: 3 }); axiosRetry(axiosClient, { retries: 3 });
// Request interceptor to add Bearer token // Request Interceptor Add Bearer token if required
axiosClient.interceptors.request.use( axiosClient.interceptors.request.use(
async (config) => { async (config) => {
if (config.authRequired) { const requiresAuth = config.authRequired !== false; // default to true
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}`;
@ -27,25 +31,24 @@ axiosClient.interceptors.request.use(
config._retry = false; config._retry = false;
} }
} }
return config; return config;
}, },
(error) => Promise.reject(error) (error) => Promise.reject(error)
); );
// // Response interceptor to handle responses globally (optional) // 🔄 Response Interceptor Handle 401, refresh token, etc.
// 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;
// Prevent infinite loop // Skip retry for public requests or already retried ones
if (!originalRequest || originalRequest._retry) { if (!originalRequest || originalRequest._retry || originalRequest.authRequired === false) {
return Promise.reject(error); return Promise.reject(error);
} }
// Only show one toast per request // Avoid showing multiple toasts
if (!originalRequest._toastShown) { if (!originalRequest._toastShown) {
originalRequest._toastShown = true; originalRequest._toastShown = true;
@ -61,7 +64,6 @@ 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");
@ -74,7 +76,7 @@ axiosClient.interceptors.response.use(
stopSignalR(); stopSignalR();
try { try {
// Refresh token // Refresh token call
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,
@ -82,16 +84,14 @@ axiosClient.interceptors.response.use(
const { token, refreshToken: newRefreshToken } = res.data.data; const { token, refreshToken: newRefreshToken } = res.data.data;
// Save new tokens // Save updated 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,11 +101,12 @@ 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 Call // Generic API function
const apiRequest = async (method, url, data = {}, config = {}) => { const apiRequest = async (method, url, data = {}, config = {}) => {
try { try {
const response = await axiosClient({ const response = await axiosClient({
@ -121,15 +122,16 @@ const apiRequest = async (method, url, data = {}, config = {}) => {
} }
}; };
// Exported API wrapper
export const api = { export const api = {
// For public routes like login, set authRequired: false // Public routes (no token required)
postPublic: (url, data = {}, customHeaders = {}) => postPublic: (url, data = {}, customHeaders = {}) =>
apiRequest("post", url, data, { apiRequest("post", url, data, {
headers: { ...customHeaders }, headers: { ...customHeaders },
authRequired: false, authRequired: false,
}), }),
// For protected routes, authRequired defaults to true // Authenticated routes
get: (url, params = {}, customHeaders = {}) => get: (url, params = {}, customHeaders = {}) =>
apiRequest("get", url, params, { apiRequest("get", url, params, {
headers: { ...customHeaders }, headers: { ...customHeaders },
@ -154,7 +156,8 @@ 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";
} }