Merge pull request 'Issues_Jun_3W' (#226) from Issues_Jun_3W into main

Reviewed-on: #226
This commit is contained in:
ashutosh.nehete 2025-07-01 10:24:37 +00:00
commit c1a31e6b3e
38 changed files with 2033 additions and 1174 deletions

View File

@ -1,5 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" lang="en"
class="light-style layout-navbar-fixed layout-menu-fixed layout-compact"
dir="ltr"
data-theme="theme-default"
data-assets-path="/assets/"
data-template="vertical-menu-template"
data-style="light">
<head>
<meta charset="UTF-8" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -172,8 +172,7 @@ const ManageEmployee = ({ employeeId, onClosed }) => {
.then((response) => {
cacheData("employeeProfileInfo", data);
showToast(
`Employee details ${
data.id == null ? "created" : "updated"
`Employee details ${data.id == null ? "created" : "updated"
} successfully.`,
"success"
);
@ -207,24 +206,24 @@ const ManageEmployee = ({ employeeId, onClosed }) => {
reset(
currentEmployee
? {
id: currentEmployee.id || null,
firstName: currentEmployee.firstName || "",
middleName: currentEmployee.middleName || "",
lastName: currentEmployee.lastName || "",
email: currentEmployee.email || "",
currentAddress: currentEmployee.currentAddress || "",
birthDate: formatDate(currentEmployee.birthDate) || "",
joiningDate: formatDate(currentEmployee.joiningDate) || "",
emergencyPhoneNumber: currentEmployee.emergencyPhoneNumber || "",
emergencyContactPerson:
currentEmployee.emergencyContactPerson || "",
aadharNumber: currentEmployee.aadharNumber || "",
gender: currentEmployee.gender || "",
panNumber: currentEmployee.panNumber || "",
permanentAddress: currentEmployee.permanentAddress || "",
phoneNumber: currentEmployee.phoneNumber || "",
jobRoleId: currentEmployee.jobRoleId?.toString() || "",
}
id: currentEmployee.id || null,
firstName: currentEmployee.firstName || "",
middleName: currentEmployee.middleName || "",
lastName: currentEmployee.lastName || "",
email: currentEmployee.email || "",
currentAddress: currentEmployee.currentAddress || "",
birthDate: formatDate(currentEmployee.birthDate) || "",
joiningDate: formatDate(currentEmployee.joiningDate) || "",
emergencyPhoneNumber: currentEmployee.emergencyPhoneNumber || "",
emergencyContactPerson:
currentEmployee.emergencyContactPerson || "",
aadharNumber: currentEmployee.aadharNumber || "",
gender: currentEmployee.gender || "",
panNumber: currentEmployee.panNumber || "",
permanentAddress: currentEmployee.permanentAddress || "",
phoneNumber: currentEmployee.phoneNumber || "",
jobRoleId: currentEmployee.jobRoleId?.toString() || "",
}
: {} // Empty object resets the form
);
setCurrentAddressLength(currentEmployee?.currentAddress?.length || 0);
@ -233,410 +232,417 @@ const ManageEmployee = ({ employeeId, onClosed }) => {
return (
<>
{/* <div className="c">
{/* <div className="c">
{!currentEmployee && empLoading && employeeId && (
<p>Loading Employee Data...</p>
)} */}
<form onSubmit={handleSubmit( onSubmit )} className="p-sm-0 p-2">
<form onSubmit={handleSubmit(onSubmit)} className="p-sm-5 p-2 position-relative">
{/* Cross button */}
<button
type="button"
className="btn-close position-absolute top-0 end-0 m-2"
aria-label="Close"
onClick={onClosed}
></button>
<div className="text-center"><p className="fs-6 fw-semibold"> {employee ? "Update Employee" : "Create Employee"}</p></div>
<div className="row mb-3">
<div className="col-sm-4">
{" "}
<div className="form-text text-start">First Name</div>
<input
type="text"
name="FirstName"
{...register("firstName")}
className="form-control form-control-sm"
id="firstName"
placeholder="First Name"
/>
{errors.firstName && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.firstName.message}
</div>
)}
</div>{" "}
<div className="col-sm-4">
<div className="form-text text-start">Middle Name</div>
<div className="row mb-3">
<div className="col-sm-4">
{" "}
<div className="form-text text-start">First Name</div>
<input
type="text"
name="FirstName"
{...register("firstName")}
className="form-control form-control-sm"
id="firstName"
placeholder="First Name"
/>
{errors.firstName && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.firstName.message}
</div>
)}
</div>{" "}
<div className="col-sm-4">
<div className="form-text text-start">Middle Name</div>
<input
type="text"
{...register("middleName")}
className="form-control form-control-sm"
id="middleName"
placeholder="Middle Name"
/>
{errors.middleName && (
<div
className="danger-text text-start "
style={{ fontSize: "12px" }}
>
{errors.middleName.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">Last Name</div>
<input
type="text"
{...register("lastName")}
className="form-control form-control-sm"
id="lastName"
placeholder="Last Name"
/>
{errors.lastName && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.lastName.message}
</div>
)}
</div>
</div>
<div className="row mb-3">
<div className="col-sm-6">
<div className="form-text text-start">Email</div>
<input
type="email"
id="email"
{...register("email")}
className="form-control form-control-sm"
placeholder="example@domain.com"
maxLength={80}
aria-describedby="Email"
disabled={!!currentEmployee?.email}
/>
{errors.email && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.email.message}
</div>
)}
</div>
<div className="col-sm-6">
<div className="form-text text-start">Phone Number</div>
<input
type="text"
keyboardType="numeric"
id="phoneNumber"
{...register("phoneNumber")}
className="form-control form-control-sm"
placeholder="Phone Number"
inputMode="numeric"
maxLength={10}
/>
{errors.phoneNumber && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.phoneNumber.message}
</div>
)}
</div>
</div>
<div className="row mb-3"></div>
<div className="row mb-3">
<div className="col-sm-4">
<div className="form-text text-start">Gender</div>
<input
type="text"
{...register("middleName")}
className="form-control form-control-sm"
id="middleName"
placeholder="Middle Name"
/>
{errors.middleName && (
<div
className="danger-text text-start "
style={{ fontSize: "12px" }}
>
{errors.middleName.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">Last Name</div>
<input
type="text"
{...register("lastName")}
className="form-control form-control-sm"
id="lastName"
placeholder="Last Name"
/>
{errors.lastName && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.lastName.message}
</div>
)}
</div>
</div>
<div className="row mb-3">
<div className="col-sm-6">
<div className="form-text text-start">Email</div>
<input
type="email"
id="email"
{...register("email")}
className="form-control form-control-sm"
placeholder="example@domain.com"
maxLength={80}
aria-describedby="Email"
disabled={!!currentEmployee?.email}
/>
{errors.email && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.email.message}
</div>
)}
</div>
<div className="col-sm-6">
<div className="form-text text-start">Phone Number</div>
<input
type="text"
keyboardType="numeric"
id="phoneNumber"
{...register("phoneNumber")}
className="form-control form-control-sm"
placeholder="Phone Number"
inputMode="numeric"
maxLength={10}
/>
{errors.phoneNumber && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.phoneNumber.message}
</div>
)}
</div>
</div>
<div className="row mb-3"></div>
<div className="row mb-3">
<div className="col-sm-4">
<div className="form-text text-start">Gender</div>
<div className="input-group input-group-merge ">
<select
className="form-select form-select-sm "
{...register("gender")}
id="gender"
aria-label=""
>
<option disabled value="">
Select Gender
</option>
<option value="Male">Male </option>
<option value="Female">Female</option>
<option value="Other">Other</option>
</select>
</div>
{errors.gender && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.gender.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">Birth Date</div>
<div className="input-group input-group-merge ">
<select
className="form-select form-select-sm "
{...register("gender")}
id="gender"
aria-label=""
>
<option disabled value="">
Select Gender
</option>
<option value="Male">Male </option>
<option value="Female">Female</option>
<option value="Other">Other</option>
</select>
</div>
{errors.gender && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.gender.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">Birth Date</div>
<div className="input-group input-group-merge ">
<input
className="form-control form-control-sm"
type="date"
{...register("birthDate")}
id="birthDate"
/>
</div>
{errors.birthDate && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.birthDate.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">Joining Date</div>
<div className="input-group input-group-merge ">
<input
className="form-control form-control-sm"
type="date"
{...register("birthDate")}
id="birthDate"
/>
</div>
{errors.birthDate && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.birthDate.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">Joining Date</div>
<div className="input-group input-group-merge ">
<input
className="form-control form-control-sm"
type="date"
{...register("joiningDate")}
id="joiningDate"
/>
</div>
{errors.joiningDate && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.joiningDate.message}
</div>
)}
</div>
</div>
<div className="row mb-3">
<div className="col-sm-6">
<div className="form-text text-start">Current Address</div>
<div className="input-group input-group-merge ">
<input
className="form-control form-control-sm"
type="date"
{...register("joiningDate")}
id="joiningDate"
/>
</div>
{errors.joiningDate && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.joiningDate.message}
</div>
)}
</div>
</div>
<div className="row mb-3">
<div className="col-sm-6">
<div className="form-text text-start">Current Address</div>
<textarea
id="currentAddress"
className="form-control form-control-sm"
placeholder="Current Address"
aria-label="Current Address"
aria-describedby="basic-icon-default-message2"
{...register("currentAddress")}
maxLength={500}
onChange={(e) => {
setCurrentAddressLength(e.target.value.length);
// let react-hook-form still handle it
register("currentAddress").onChange(e);
}}
></textarea>
<div className="text-end muted">
<small>
{" "}
{500 - currentAddressLength} characters left
</small>
</div>
{errors.currentAddress && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.currentAddress.message}
</div>
)}
</div>
<div className="col-sm-6">
<div className="form-text text-start">
Permanent Address
</div>
<textarea
id="currentAddress"
className="form-control form-control-sm"
placeholder="Current Address"
aria-label="Current Address"
aria-describedby="basic-icon-default-message2"
{...register("currentAddress")}
maxLength={500}
onChange={(e) => {
setCurrentAddressLength(e.target.value.length);
// let react-hook-form still handle it
register("currentAddress").onChange(e);
}}
></textarea>
<div className="text-end muted">
<small>
{" "}
{500 - currentAddressLength} characters left
</small>
</div>
{errors.currentAddress && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.currentAddress.message}
</div>
)}
</div>
<div className="col-sm-6">
<div className="form-text text-start">
Permanent Address
</div>
<textarea
id="permanentAddress"
className="form-control form-control-sm"
placeholder="Permanent Address"
aria-label="Permanent Address"
aria-describedby="basic-icon-default-message2"
{...register("permanentAddress")}
maxLength={500}
onChange={(e) => {
setPermanentAddressLength(e.target.value.length);
register("permanentAddress").onChange(e);
}}
></textarea>
<div className="text-end muted">
<small>
{500 - permanentAddressLength} characters left
</small>
</div>
{errors.permanentAddress && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.permanentAddress.message}
</div>
)}
</div>
</div>
<div className="row mb-3">
{" "}
<div className="divider">
<div className="divider-text">Other Information</div>
</div>
</div>
<div className="row mb-3">
<div className="col-sm-4">
<div className="form-text text-start">Role</div>
<div className="input-group input-group-merge ">
<select
className="form-select form-select-sm"
{...register("jobRoleId")}
id="jobRoleId"
aria-label=""
>
<option disabled value="">
Select Role
</option>
{[...job_role]
.sort((a, b) => a?.name?.localeCompare(b.name))
.map((item) => (
<option value={item?.id} key={item.id}>
{item?.name}{" "}
</option>
))}
</select>
</div>
{errors.jobRoleId && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.jobRoleId.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">
Emergency Contact Person
</div>
<input
type="text"
{...register("emergencyContactPerson")}
className="form-control form-control-sm"
id="emergencyContactPerson"
maxLength={50}
placeholder="Contact Person"
/>
{errors.emergencyContactPerson && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.emergencyContactPerson.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">
Emergency Phone Number
</div>
<input
type="text"
{...register("emergencyPhoneNumber")}
className="form-control form-control-sm phone-mask"
id="emergencyPhoneNumber"
placeholder="Phone Number"
inputMode="numeric"
maxLength={10}
/>
{errors.emergencyPhoneNumber && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.emergencyPhoneNumber.message}
</div>
)}
</div>
</div>
<div className="row mb-3 d-none">
<div className="col-sm-6">
<div className="form-text text-start">AADHAR Number</div>
<textarea
id="permanentAddress"
className="form-control form-control-sm"
placeholder="Permanent Address"
aria-label="Permanent Address"
aria-describedby="basic-icon-default-message2"
{...register("permanentAddress")}
maxLength={500}
onChange={(e) => {
setPermanentAddressLength(e.target.value.length);
register("permanentAddress").onChange(e);
}}
></textarea>
<div className="text-end muted">
<small>
{500 - permanentAddressLength} characters left
</small>
</div>
{errors.permanentAddress && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.permanentAddress.message}
</div>
)}
</div>
</div>
<div className="row mb-3">
{" "}
<div className="divider">
<div className="divider-text">Other Information</div>
</div>
</div>
<div className="row mb-3">
<div className="col-sm-4">
<div className="form-text text-start">Role</div>
<div className="input-group input-group-merge ">
<select
className="form-select form-select-sm"
{...register("jobRoleId")}
id="jobRoleId"
aria-label=""
>
<option disabled value="">
Select Role
</option>
{[...job_role]
.sort((a, b) => a?.name?.localeCompare(b.name))
.map((item) => (
<option value={item?.id} key={item.id}>
{item?.name}{" "}
</option>
))}
</select>
</div>
{errors.jobRoleId && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.jobRoleId.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">
Emergency Contact Person
</div>
<input
type="text"
{...register("emergencyContactPerson")}
className="form-control form-control-sm"
id="emergencyContactPerson"
maxLength={50}
placeholder="Contact Person"
/>
{errors.emergencyContactPerson && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.emergencyContactPerson.message}
</div>
)}
</div>
<div className="col-sm-4">
<div className="form-text text-start">
Emergency Phone Number
</div>
<input
type="text"
{...register("emergencyPhoneNumber")}
className="form-control form-control-sm phone-mask"
id="emergencyPhoneNumber"
placeholder="Phone Number"
inputMode="numeric"
maxLength={10}
/>
{errors.emergencyPhoneNumber && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.emergencyPhoneNumber.message}
</div>
)}
</div>
</div>
<div className="row mb-3 d-none">
<div className="col-sm-6">
<div className="form-text text-start">AADHAR Number</div>
<input
type="text"
{...register("aadharNumber")}
className="form-control form-control-sm"
id="aadharNumber"
placeholder="AADHAR Number"
maxLength={12}
inputMode="numeric"
/>
{errors.aadharNumber && (
<div className="danger-text text-start">
{errors.aadharNumber.message}
</div>
)}
</div>
<div className="col-sm-6 d-none">
<div className="form-text text-start">PAN Number</div>
<input
type="text"
{...register("aadharNumber")}
className="form-control form-control-sm"
id="aadharNumber"
placeholder="AADHAR Number"
maxLength={12}
inputMode="numeric"
/>
{errors.aadharNumber && (
<div className="danger-text text-start">
{errors.aadharNumber.message}
</div>
)}
</div>
<div className="col-sm-6 d-none">
<div className="form-text text-start">PAN Number</div>
<input
type="text"
{...register("panNumber")}
className="form-control form-control-sm"
id="panNumber"
placeholder="PAN Number"
maxLength={10}
/>
{errors.panNumber && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.panNumber.message}
</div>
)}
</div>
</div>
<input
type="text"
{...register("panNumber")}
className="form-control form-control-sm"
id="panNumber"
placeholder="PAN Number"
maxLength={10}
/>
{errors.panNumber && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.panNumber.message}
</div>
)}
</div>
</div>
{employeeId && (
<div className="row mb-3 d-none">
<div className="col-sm-12">
<input type="text" name="id" {...register("id")} />
</div>
</div>
)}
{employeeId && (
<div className="row mb-3 d-none">
<div className="col-sm-12">
<input type="text" name="id" {...register("id")} />
</div>
</div>
)}
<div className="row justify-content-start">
<div className="col-sm-12">
<button
aria-label="manage employee"
type="submit"
className="btn btn-sm btn-primary"
disabled={isloading}
>
{isloading
? "Please Wait..."
: employeeId
? "Update"
: "Create"}
</button>
<div className="row justify-content-start">
<div className="col-sm-12">
<button
aria-label="manage employee"
type="submit"
className="btn btn-sm btn-primary"
disabled={isloading}
>
{isloading
? "Please Wait..."
: employeeId
? "Update"
: "Create"}
</button>
<button
aria-label="manage employee"
type="reset"
className="btn btn-sm btn-primary ms-2"
disabled={isloading}
>
Clear
</button>
</div>
</div>
</form>
<button
aria-label="manage employee"
type="reset"
className="btn btn-sm btn-primary ms-2"
disabled={isloading}
>
Clear
</button>
</div>
</div>
</form>
</>
);
};

View File

@ -103,7 +103,8 @@ const Header = () => {
}, [projectNames]);
/** Check if current page id project details page */
const isProjectPath = /^\/projects\/[a-f0-9-]{36}$/.test(location.pathname);
const isProjectPath = /^\/projects\/[a-f0-9-]{36}$/.test(location.pathname)
const isDirectoryPath = /^\/directory$/.test(location.pathname);
const handler = useCallback(
async (data) => {
@ -145,7 +146,7 @@ const Header = () => {
}, [handler]);
return (
<nav
className="layout-navbar container-xxl navbar navbar-expand-xl navbar-detached align-items-center bg-navbar-theme"
className="layout-navbar container-fluid mb-3 navbar navbar-expand-xl navbar-detached align-items-center bg-navbar-theme"
id="layout-navbar"
>
<div className="layout-menu-toggle navbar-nav align-items-xl-center me-3 me-xl-0 d-xl-none">
@ -162,11 +163,11 @@ const Header = () => {
>
{projectNames?.length > 0 && (
<div className=" align-items-center">
{!isProjectPath && (
{(!isProjectPath && !isDirectoryPath) && (
<>
<i
className="rounded-circle bx bx-building-house"
style={{ fontSize: "xx-large" }}
className="rounded-circle bx bx-building-house bx-sm-lg bx-md"
></i>
<div className="btn-group">
<button
@ -186,7 +187,7 @@ const Header = () => {
style={{ overflow: "auto", maxHeight: "300px" }}
>
{[...projectNames]
.sort((a, b) => a.name.localeCompare(b.name))
.sort((a, b) => a?.name?.localeCompare(b.name))
.map((project) => (
<li key={project?.id}>
<button
@ -197,7 +198,7 @@ const Header = () => {
>
{project?.name}{" "}
{project?.shortName ? (
<span className="text-primary fw-semibold">
<span className="text-primary fw-semibold ">
{" "}
({project?.shortName})
</span>
@ -257,7 +258,7 @@ const Header = () => {
</div>
<div className="dropdown-shortcuts-item col">
<a
onClick={() => navigate(`/projectNames`)}
onClick={() => navigate(`/projects`)}
className="text-heading text-truncate cursor-pointer"
>
<span className="dropdown-shortcuts-icon rounded-circle mb-3">

View File

@ -20,16 +20,17 @@ const taskSchema = z.object({
const defaultModel = {
id: null,
buildingID: "0",
floorId: "0",
workAreaId: "0",
activityID: null,
workCategoryId: "",
buildingID: "", // Changed from "0"
floorId: "", // Changed from "0"
workAreaId: "", // Changed from "0"
activityID: "", // Changed from null
workCategoryId: "", // Kept as empty
plannedWork: 0,
completedWork: 0,
comment:""
comment: ""
};
const TaskModel = ({
project,
onSubmit,
@ -87,7 +88,7 @@ const TaskModel = ({
reset((prev) => ({
...prev,
floorId: value,
workAreaId: 0,
workAreaId: "",
activityID: "",
workCategoryId: categoryData?.[0]?.id?.toString() ?? "",
}));
@ -194,7 +195,7 @@ const TaskModel = ({
{...register("buildingID")}
onChange={handleBuildingChange}
>
<option value="0">Select Building</option>
<option value="">Select Building</option>
{project.buildings
?.filter((building) => building?.name) // Ensure valid name
?.sort((a, b) => a.name?.localeCompare(b.name))
@ -226,7 +227,7 @@ const TaskModel = ({
{...register("floorId")}
onChange={handleFloorChange}
>
<option value="0">Select Floor</option>
<option value="">Select Floor</option>
{selectedBuilding.floors
?.filter(
(floor) =>
@ -262,7 +263,7 @@ const TaskModel = ({
{...register("workAreaId")}
onChange={handleWorkAreaChange}
>
<option value="0">Select Work Area</option>
<option value="">Select Work Area</option>
{selectedFloor.workAreas
?.filter((workArea) => workArea?.areaName)
?.sort((a, b) => a.areaName?.localeCompare(b.areaName))
@ -442,4 +443,4 @@ const TaskModel = ({
);
};
export default TaskModel;
export default TaskModel;

View File

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

View File

@ -132,7 +132,7 @@ const CreateRole = ({ modalType, onClose }) => {
</div>
<div className="col-12 col-md-12 border">
<div className="col-12 col-md-12">
{masterFeatures.map((feature, featureIndex) => (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,13 +6,13 @@ import Avatar from "../../components/common/Avatar";
import Breadcrumb from "../../components/common/Breadcrumb";
import ManageEmp from "../../components/Employee/ManageRole";
import { useEmployeesAllOrByProjectId } from "../../hooks/useEmployees";
import { useProjects } from "../../hooks/useProjects";
import { useProfile } from "../../hooks/useProfile";
import { hasUserPermission } from "../../utils/authUtils";
import { useProjects } from "../../hooks/useProjects"; // Keep if you use projects elsewhere
import { useProfile } from "../../hooks/useProfile"; // Keep if you use profile elsewhere
import { hasUserPermission } from "../../utils/authUtils"; // Keep if you use this elsewhere
import { ITEMS_PER_PAGE, MANAGE_EMPLOYEES } from "../../utils/constants";
import { clearCacheKey } from "../../slices/apiDataManager";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import SuspendEmp from "../../components/Employee/SuspendEmp";
import SuspendEmp from "../../components/Employee/SuspendEmp"; // Keep if you use SuspendEmp
import {
exportToCSV,
exportToExcel,
@ -28,23 +28,25 @@ import { newlineChars } from "pdf-lib";
import GlobalModel from "../../components/common/GlobalModel";
const EmployeeList = () => {
const selectedProjectId = useSelector((store) => store.localVariables.projectId);
const [selectedProject, setSelectedProject] = useState(() => selectedProjectId || "");
const { projects, loading: projectLoading } = useProjects();
const selectedProjectId = useSelector(
(store) => store.localVariables.projectId
);
const [showInactive, setShowInactive] = useState(false);
const [showAllEmployees, setShowAllEmployees] = useState(false);
const [showAllEmployees, setShowAllEmployees] = useState(false);
const Manage_Employee = useHasUserPermission(MANAGE_EMPLOYEES);
const { employees, loading, setLoading, error, recallEmployeeData } =
useEmployeesAllOrByProjectId(showAllEmployees ? null : selectedProject, showInactive);
const [projectsList, setProjectsList] = useState(projects || []);
useEmployeesAllOrByProjectId(
showAllEmployees ? null : selectedProjectId, // Use selectedProjectId here
showInactive
);
const [employeeList, setEmployeeList] = useState([]);
const [modelConfig, setModelConfig] = useState();
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(ITEMS_PER_PAGE);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isEmployeeModalOpen, setIsEmployeeModalOpen] = useState(false);
const [searchText, setSearchText] = useState("");
const [filteredData, setFilteredData] = useState([]);
const [showModal, setShowModal] = useState(false);
@ -55,48 +57,65 @@ const EmployeeList = () => {
const navigate = useNavigate();
const handleSearch = (e) => {
const value = e.target.value.toLowerCase();
setSearchText(value);
/**
* Applies the search filter to a given array of employee data.
* @param {Array} data - The array of employee objects to filter.
* @param {string} text - The search text.
* @returns {Array} The filtered array.
*/
const applySearchFilter = (data, text) => {
if (!text) {
return data;
}
const lowercasedText = text.toLowerCase().trim(); // Ensure search text is trimmed and lowercase
if (!employeeList.length) return;
return data.filter((item) => {
// **IMPROVED FULL NAME CONSTRUCTION**
const firstName = item.firstName || "";
const middleName = item.middleName || "";
const lastName = item.lastName || "";
// Join parts, then trim any excess spaces if a middle name is missing
const fullName = `${firstName} ${middleName} ${lastName}`.toLowerCase().trim().replace(/\s+/g, ' ');
const results = employeeList.filter((item) => {
const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
const email = item.email ? item.email.toLowerCase() : "";
const phoneNumber = item.phoneNumber ? item.phoneNumber.toLowerCase() : "";
const jobRole = item.jobRole ? item.jobRole.toLowerCase() : ""; // Get jobRole and convert to lowercase
const jobRole = item.jobRole ? item.jobRole.toLowerCase() : "";
return (
fullName.includes(value) ||
email.includes(value) ||
phoneNumber.includes(value) ||
jobRole.includes(value) // Include jobRole in the search
fullName.includes(lowercasedText) ||
email.includes(lowercasedText) ||
phoneNumber.includes(lowercasedText) ||
jobRole.includes(lowercasedText)
);
});
};
setFilteredData(results);
const handleSearch = (e) => {
const value = e.target.value;
setSearchText(value);
setCurrentPage(1);
};
useEffect(() => {
setCurrentPage(1);
if (!loading && Array.isArray(employees)) {
// Sort by full name (firstName + lastName)
const sorted = [...employees].sort((a, b) => {
const nameA = `${a.firstName || ""}${a.middleName || ""}${b.lastName || ""
}`.toLowerCase();
const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || ""
}`.toLowerCase();
const nameA = `${a.firstName || ""}${a.middleName || ""}${a.lastName || ""}`.toLowerCase();
const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || ""}`.toLowerCase();
return nameA?.localeCompare(nameB);
});
setEmployeeList(sorted);
setFilteredData(sorted);
const results = applySearchFilter(sorted, searchText);
setFilteredData(results);
} else if (!loading && !employees) {
setEmployeeList([]);
setFilteredData([]);
}
}, [loading, employees, selectedProject, showAllEmployees]); // Add showAllEmployees to dependencies
}, [loading, employees, showAllEmployees, searchText, selectedProjectId]); // Add selectedProjectId to dependencies
const displayData = searchText ? filteredData : employeeList;
const displayData = filteredData;
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = Array.isArray(displayData)
@ -120,11 +139,11 @@ const EmployeeList = () => {
modalElement.classList.remove("show");
modalElement.style.display = "none";
document.body.classList.remove("modal-open");
document.querySelector(".modal-backdrop").remove();
document.querySelector(".modal-backdrop")?.remove(); // Use optional chaining for safety
}
setShowModal(false);
clearCacheKey("employeeProfile");
recallEmployeeData(showInactive);
recallEmployeeData(showInactive, showAllEmployees ? null : selectedProjectId); // Use selectedProjectId here
};
const handleShow = () => setShowModal(true);
const handleClose = () => setShowModal(false);
@ -138,8 +157,8 @@ const EmployeeList = () => {
clearCacheKey("allEmployeeList");
clearCacheKey("allInactiveEmployeeList");
clearCacheKey("employeeProfile");
setEmployeeList([]);
recallEmployeeData(showInactive);
// Recall data based on current filter states after deletion to refresh the table
recallEmployeeData(showInactive, showAllEmployees ? null : selectedProjectId); // Use selectedProjectId here
setemployeeLodaing(false);
setIsDeleteModalOpen(false);
})
@ -188,7 +207,7 @@ const EmployeeList = () => {
const handleToggle = (e) => {
setShowInactive(e.target.checked);
recallEmployeeData(e.target.checked);
recallEmployeeData(e.target.checked, showAllEmployees ? null : selectedProjectId); // Use selectedProjectId here
};
const handleAllEmployeesToggle = (e) => {
@ -201,8 +220,6 @@ const handleAllEmployeesToggle = (e) => {
}
};
const handleEmployeeModel = (id) => {
setSelecedEmployeeId(id);
setShowModal(true);
@ -213,24 +230,19 @@ const handleAllEmployeesToggle = (e) => {
setIsDeleteModalOpen(true);
};
const handleProjectSelection = (e) => {
const newProjectId = e.target.value;
setSelectedProject(newProjectId);
if (newProjectId) {
setShowAllEmployees(false);
}
};
useEffect(() => {
setSelectedProject(selectedProjectId || "");
}, [selectedProjectId]);
if (!showAllEmployees) {
recallEmployeeData(showInactive, selectedProjectId);
}
}, [selectedProjectId, showInactive, showAllEmployees, recallEmployeeData]);
const handler = useCallback(
(msg) => {
if(employees.some((item) => item.id == msg.employeeId)){
setEmployeeList([]);
recallEmployeeData(showInactive);
recallEmployeeData(showInactive, showAllEmployees ? null : selectedProjectId); // Use selectedProjectId here
}
},[employees]
},[employees, showInactive, showAllEmployees, selectedProjectId] // Add all relevant dependencies
);
useEffect(() => {
@ -298,7 +310,7 @@ const handleAllEmployeesToggle = (e) => {
</div>
)}
<div className="container-xxl flex-grow-1 container-p-y">
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
@ -353,7 +365,7 @@ const handleAllEmployeesToggle = (e) => {
{/* Right side: Search + Export + Add Employee */}
<div className="d-flex flex-wrap align-items-center justify-content-end gap-3 flex-grow-1">
{/* Search */}
{/* Search Input - ALWAYS ENABLED */}
<div className="dataTables_filter">
<label className="mb-0">
<input
@ -402,7 +414,7 @@ const handleAllEmployeesToggle = (e) => {
</ul>
</div>
{/* Add Employee */}
{/* Add Employee Button */}
{Manage_Employee && (
<button
className="btn btn-sm btn-primary"
@ -416,7 +428,6 @@ const handleAllEmployeesToggle = (e) => {
</div>
</div>
<table
className="datatables-users table border-top dataTable no-footer dtr-column text-nowrap"
id="DataTables_Table_0"
@ -511,7 +522,17 @@ const handleAllEmployeesToggle = (e) => {
</td>
</tr>
)}
{!loading && employeeList?.length === 0 && (
{/* Conditional messages for no data or no search results */}
{!loading && displayData?.length === 0 && searchText && !showAllEmployees ? (
<tr>
<td colSpan={8}>
<small className="muted">
'{searchText}' employee not found
</small>{" "}
</td>
</tr>
) : null}
{!loading && displayData?.length === 0 && (!searchText || showAllEmployees) ? (
<tr>
<td
colSpan={8}
@ -520,150 +541,137 @@ const handleAllEmployeesToggle = (e) => {
No Data Found
</td>
</tr>
)}
{!loading &&
employeeList &&
currentItems.length === 0 &&
employeeList.length !== 0 && (
<tr>
<td colSpan={8}>
<small className="muted">
'{searchText}' employee not found
</small>{" "}
</td>
</tr>
)}
) : null}
{currentItems &&
!loading &&
currentItems.map((item) => (
<tr className="odd" key={item.id}>
<td className="sorting_1" colSpan={2}>
<div className="d-flex justify-content-start align-items-center user-name">
<Avatar
firstName={item.firstName}
lastName={item.lastName}
></Avatar>
<div className="d-flex flex-column">
<a
{/* Render current items */}
{currentItems && !loading && currentItems.map((item) => (
<tr className="odd" key={item.id}>
<td className="sorting_1" colSpan={2}>
<div className="d-flex justify-content-start align-items-center user-name">
<Avatar
firstName={item.firstName}
lastName={item.lastName}
></Avatar>
<div className="d-flex flex-column">
<a
onClick={() =>
navigate(
`/employee/${item.id}?for=attendance`
)
}
className="text-heading text-truncate cursor-pointer"
>
<span className="fw-normal">
{item.firstName} {item.middleName}{" "}
{item.lastName}
</span>
</a>
</div>
</div>
</td>
<td className="text-start d-none d-sm-table-cell">
{item.email ? (
<span className="text-truncate">
<i className="bx bxs-envelope text-primary me-2"></i>
{item.email}
</span>
) : (
<span className="text-truncate text-italic">
NA
</span>
)}
</td>
<td className="text-start d-none d-sm-table-cell">
<span className="text-truncate">
<i className="bx bxs-phone-call text-primary me-2"></i>
{item.phoneNumber}
</span>
</td>
<td className=" d-none d-sm-table-cell text-start">
<span className="text-truncate">
<i className="bx bxs-wrench text-success me-2"></i>
{item.jobRole || "Not Assign Yet"}
</span>
</td>
<td className=" d-none d-md-table-cell">
{moment(item.joiningDate)?.format("DD-MMM-YYYY")}
</td>
<td>
{/* Assuming 'isActive' property exists to determine status */}
{item.isActive ? (
<span
className="badge bg-label-success"
text-capitalized=""
>
Active
</span>
) : (
<span
className="badge bg-label-danger"
text-capitalized=""
>
Inactive
</span>
)}
</td>
{Manage_Employee && (
<td className="text-end">
<div className="dropdown">
<button
className="btn btn-icon dropdown-toggle hide-arrow"
data-bs-toggle="dropdown"
>
<i className="bx bx-dots-vertical-rounded bx-md"></i>
</button>
<div className="dropdown-menu dropdown-menu-end">
<button
onClick={() =>
navigate(
`/employee/${item.id}?for=attendance`
)
navigate(`/employee/${item.id}`)
}
className="text-heading text-truncate cursor-pointer"
className="dropdown-item py-1"
>
<span className="fw-normal">
{item.firstName} {item.middleName}{" "}
{item.lastName}
</span>
</a>
<i className="bx bx-detail bx-sm"></i> View
</button>
<button
className="dropdown-item py-1"
onClick={() => {
handleEmployeeModel(item.id);
}}
>
<i className="bx bx-edit bx-sm"></i> Edit
</button>
{!item.isSystem && (
<>
<button
className="dropdown-item py-1"
onClick={() =>
handleOpenDelete(item.id)
}
>
<i className="bx bx-task-x bx-sm"></i>{" "}
Suspend
</button>
<button
className="dropdown-item py-1"
type="button"
data-bs-toggle="modal"
data-bs-target="#managerole-modal"
onClick={() =>
handleConfigData(item.id)
}
>
<i className="bx bx-cog bx-sm"></i>{" "}
Manage Role
</button>
</>
)}
</div>
</div>
</td>
<td className="text-start d-none d-sm-table-cell">
{item.email ? (
<span className="text-truncate">
<i className="bx bxs-envelope text-primary me-2"></i>
{item.email}
</span>
) : (
<span className="text-truncate text-italic">
NA
</span>
)}
</td>
<td className="text-start d-none d-sm-table-cell">
<span className="text-truncate">
<i className="bx bxs-phone-call text-primary me-2"></i>
{item.phoneNumber}
</span>
</td>
<td className=" d-none d-sm-table-cell text-start">
<span className="text-truncate">
<i className="bx bxs-wrench text-success me-2"></i>
{item.jobRole || "Not Assign Yet"}
</span>
</td>
<td className=" d-none d-md-table-cell">
{moment(item.joiningDate)?.format("DD-MMM-YYYY")}
</td>
<td>
{showInactive ? (
<span
className="badge bg-label-danger"
text-capitalized=""
>
Inactive
</span>
) : (
<span
className="badge bg-label-success"
text-capitalized=""
>
Active
</span>
)}
</td>
{Manage_Employee && (
<td className="text-end">
<div className="dropdown">
<button
className="btn btn-icon dropdown-toggle hide-arrow"
data-bs-toggle="dropdown"
>
<i className="bx bx-dots-vertical-rounded bx-md"></i>
</button>
<div className="dropdown-menu dropdown-menu-end">
<button
onClick={() =>
navigate(`/employee/${item.id}`)
}
className="dropdown-item py-1"
>
<i className="bx bx-detail bx-sm"></i> View
</button>
<button
className="dropdown-item py-1"
onClick={() => {
handleEmployeeModel(item.id);
}}
>
<i className="bx bx-edit bx-sm"></i> Edit
</button>
{!item.isSystem && (
<>
<button
className="dropdown-item py-1"
onClick={() =>
handleOpenDelete(item.id)
}
>
<i className="bx bx-task-x bx-sm"></i>{" "}
Suspend
</button>
<button
className="dropdown-item py-1"
type="button"
data-bs-toggle="modal"
data-bs-target="#managerole-modal"
onClick={() =>
handleConfigData(item.id)
}
>
<i className="bx bx-cog bx-sm"></i>{" "}
Manage Role
</button>
</>
)}
</div>
</div>
</td>
)}
</tr>
))}
)}
</tr>
))}
</tbody>
</table>
@ -713,7 +721,6 @@ const handleAllEmployeesToggle = (e) => {
</ul>
</nav>
)}
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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