In Directory changing filter logic and adding popup.

This commit is contained in:
Kartik sharma 2025-06-27 15:47:36 +05:30
parent d646711637
commit 9396caea57
4 changed files with 279 additions and 92 deletions

View File

@ -7,6 +7,8 @@ import showToast from "../../services/toastService";
import { cacheData, getCachedData } from "../../slices/apiDataManager"; import { cacheData, getCachedData } from "../../slices/apiDataManager";
import ConfirmModal from "../common/ConfirmModal"; // Make sure path is correct import ConfirmModal from "../common/ConfirmModal"; // Make sure path is correct
import "../common/TextEditor/Editor.css"; import "../common/TextEditor/Editor.css";
import ProfileContactDirectory from "./ProfileContactDirectory";
import GlobalModel from "../common/GlobalModel";
const NoteCardDirectoryEditable = ({ const NoteCardDirectoryEditable = ({
noteItem, noteItem,
@ -20,6 +22,8 @@ const NoteCardDirectoryEditable = ({
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const [isRestoring, setIsRestoring] = useState(false); const [isRestoring, setIsRestoring] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [open_contact, setOpen_contact] = useState(null);
const [isOpenModalNote, setIsOpenModalNote] = useState(false);
const handleUpdateNote = async () => { const handleUpdateNote = async () => {
try { try {
@ -69,6 +73,13 @@ const NoteCardDirectoryEditable = ({
} }
}; };
const contactProfile = (contactId) => {
DirectoryRepository.GetContactProfile(contactId).then((res) => {
setOpen_contact(res?.data);
setIsOpenModalNote(true);
});
};
const handleRestore = async () => { const handleRestore = async () => {
try { try {
setIsRestoring(true); setIsRestoring(true);
@ -84,6 +95,25 @@ const NoteCardDirectoryEditable = ({
return ( 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 <div
className="card p-1 shadow-sm border-1 mb-4 rounded" className="card p-1 shadow-sm border-1 mb-4 rounded"
style={{ style={{
@ -95,23 +125,39 @@ const NoteCardDirectoryEditable = ({
{/* Header */} {/* Header */}
<div className="d-flex justify-content-between align-items-center mb-1"> <div className="d-flex justify-content-between align-items-center mb-1">
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<Avatar <Avatar
size="xs" size="xs"
firstName={noteItem?.createdBy?.firstName} firstName={noteItem?.createdBy?.firstName}
lastName={noteItem?.createdBy?.lastName} lastName={noteItem?.createdBy?.lastName}
className="m-0" className="m-0"
/> />
<div className="d-flex flex-column ms-0"> <div>
<span className="fw-semibold small"> <div className="d-flex ms-0 align-middle cursor-pointer" onClick={() =>contactProfile(noteItem.contactId)}>
{noteItem?.createdBy?.firstName} {noteItem?.createdBy?.lastName} <span>
</span> <span className="fw-bold "> {noteItem?.contactName} </span> <span className="text-muted font-weight-normal">
<span className="text-muted" style={{ fontSize: "10px" }}> ( {noteItem?.organizationName})
{moment </span>
.utc(noteItem?.createdAt) </span>
.add(5, "hours")
.add(30, "minutes") </div>
.format("MMMM DD, YYYY [at] hh:mm A")} <div className="d-flex ms-0 align-middle">
</span>
</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>
</div> </div>

View File

@ -2,20 +2,39 @@ import React, { useEffect, useState, useMemo } from "react";
import { DirectoryRepository } from "../../repositories/DirectoryRepository"; import { DirectoryRepository } from "../../repositories/DirectoryRepository";
import NoteCardDirectoryEditable from "./NoteCardDirectoryEditable"; import NoteCardDirectoryEditable from "./NoteCardDirectoryEditable";
const NotesCardViewDirectory = ({ notes, setNotes, searchText, selectedNoteNames }) => { // Changed to array const NotesCardViewDirectory = ({ notes, setNotesForFilter, searchText, filterAppliedNotes }) => {
const [allNotes, setAllNotes] = useState([]); const [allNotes, setAllNotes] = useState([]);
const [filteredNotes, setFilteredNotes] = useState([]); const [filteredNotes, setFilteredNotes] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
const [selectedCreators, setSelectedCreators] = useState([]);
const [selectedOrgs, setSelectedOrgs] = useState([]);
const pageSize = 20; const pageSize = 20;
useEffect(() => {
fetchNotes();
}, []);
const fetchNotes = async () => { const fetchNotes = async () => {
setLoading(true); setLoading(true);
try { try {
const response = await DirectoryRepository.GetNotes(1000, 1); const response = await DirectoryRepository.GetNotes(1000, 1);
const fetchedNotes = response.data?.data || []; const fetchedNotes = response.data?.data || [];
setAllNotes(fetchedNotes); 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) { } catch (error) {
console.error("Failed to fetch notes:", error); console.error("Failed to fetch notes:", error);
} finally { } finally {
@ -23,52 +42,51 @@ const NotesCardViewDirectory = ({ notes, setNotes, searchText, selectedNoteNames
} }
}; };
useEffect(() => {
fetchNotes();
}, []);
useEffect(() => {
const applyCombinedFilter = () => {
const lowerSearch = searchText?.toLowerCase() || ""; const lowerSearch = searchText?.toLowerCase() || "";
const filtered = allNotes.filter((noteItem) => { 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 plainNote = noteItem?.note?.replace(/<[^>]+>/g, "").toLowerCase();
const fullName = `${noteItem?.createdBy?.firstName || ""} ${noteItem?.createdBy?.lastName || ""}`.trim();
const lowerFullName = fullName.toLowerCase();
const createdDate = new Date(noteItem?.createdAt).toLocaleDateString("en-IN").toLowerCase();
// Collect all string values in the note object to search through
const stringValues = []; const stringValues = [];
const extractStrings = (obj) => { const extractStrings = (obj) => {
for (const key in obj) { for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const value = obj[key]; const value = obj[key];
if (typeof value === "string") { if (typeof value === "string") {
stringValues.push(value.toLowerCase()); stringValues.push(value.toLowerCase());
} else if (typeof value === "object" && value !== null) { } else if (typeof value === "object" && value !== null) {
extractStrings(value); // Recursively extract from nested objects extractStrings(value);
} }
} }
}; };
extractStrings(noteItem); extractStrings(noteItem);
// Add manually stripped note, full name, date, etc. stringValues.push(plainNote, creator.toLowerCase());
stringValues.push(plainNote, lowerFullName, createdDate);
const matchesSearch = stringValues.some((val) => val.includes(lowerSearch)); const matchesSearch = stringValues.some((val) => val.includes(lowerSearch));
const matchesNameFilter = return matchesCreator && matchesOrg && matchesSearch;
selectedNoteNames.length === 0 || selectedNoteNames.includes(fullName);
return matchesSearch && matchesNameFilter;
}); });
setFilteredNotes(filtered); setFilteredNotes(filtered);
setNotes(filtered);
setCurrentPage(1); setCurrentPage(1);
setTotalPages(Math.ceil(filtered.length / pageSize)); setTotalPages(Math.ceil(filtered.length / pageSize));
}, [searchText, allNotes, selectedNoteNames]); };
useEffect(() => {
applyCombinedFilter();
}, [searchText, allNotes]);
useEffect(() => {
setFilteredNotes(filterAppliedNotes);
}, [filterAppliedNotes])
const currentItems = useMemo(() => { const currentItems = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize; const startIndex = (currentPage - 1) * pageSize;
@ -81,19 +99,19 @@ const NotesCardViewDirectory = ({ notes, setNotes, searchText, selectedNoteNames
} }
}; };
if (loading) { if (loading) return <p className="mt-10 text-center">Loading notes...</p>;
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>;
if (!filteredNotes.length) {
return <p className="mt-10 text-center">No matching notes found</p>;
}
return ( return (
<div <div className="w-100 h-100 ">
className="w-100 h-100" {/* Filter Dropdown */}
> <div className="dropdown mb-3 ms-2">
<div className="d-flex flex-column text-start mt-4" style={{ gap: "0rem", minHeight: "100%" }}> </div>
{/* Notes List */}
<div className="d-flex flex-column text-start" style={{ gap: "0rem", minHeight: "100%" }}>
{currentItems.map((noteItem) => ( {currentItems.map((noteItem) => (
<NoteCardDirectoryEditable <NoteCardDirectoryEditable
key={noteItem.id} key={noteItem.id}
@ -104,13 +122,12 @@ const NotesCardViewDirectory = ({ notes, setNotes, searchText, selectedNoteNames
prevNotes.map((n) => (n.id === updatedNote.id ? updatedNote : n)) prevNotes.map((n) => (n.id === updatedNote.id ? updatedNote : n))
); );
}} }}
onNoteDelete={() => { onNoteDelete={() => fetchNotes()}
fetchNotes();
}}
/> />
))} ))}
</div> </div>
{/* Pagination */}
{totalPages > 1 && ( {totalPages > 1 && (
<div className="d-flex justify-content-end mt-3 me-3"> <div className="d-flex justify-content-end mt-3 me-3">
<div className="d-flex align-items-center gap-2"> <div className="d-flex align-items-center gap-2">
@ -128,7 +145,8 @@ const NotesCardViewDirectory = ({ notes, setNotes, searchText, selectedNoteNames
return ( return (
<button <button
key={page} key={page}
className={`btn btn-sm rounded-circle border ${page === currentPage ? "btn-primary text-white" : "btn-outline-primary"}`} className={`btn btn-sm rounded-circle border ${page === currentPage ? "btn-primary text-white" : "btn-outline-primary"
}`}
style={{ width: "32px", height: "32px", padding: 0 }} style={{ width: "32px", height: "32px", padding: 0 }}
onClick={() => handlePageClick(page)} onClick={() => handlePageClick(page)}
> >
@ -152,4 +170,4 @@ const NotesCardViewDirectory = ({ notes, setNotes, searchText, selectedNoteNames
); );
}; };
export default NotesCardViewDirectory; export default NotesCardViewDirectory;

View File

@ -38,6 +38,8 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
const [IsDeleting, setDeleting] = useState(false); const [IsDeleting, setDeleting] = useState(false);
const [openBucketModal, setOpenBucketModal] = useState(false); const [openBucketModal, setOpenBucketModal] = useState(false);
const [notes, setNotes] = useState([]); const [notes, setNotes] = useState([]);
const [filterAppliedNotes, setFilterAppliedNotes] = useState([]);
// const [selectedOrgs, setSelectedOrgs] = useState([]);
// Changed to an array for multiple selections // Changed to an array for multiple selections
const [selectedNoteNames, setSelectedNoteNames] = useState([]); const [selectedNoteNames, setSelectedNoteNames] = useState([]);
@ -353,6 +355,8 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
notesToExport={notes} notesToExport={notes}
selectedNoteNames={selectedNoteNames} selectedNoteNames={selectedNoteNames}
setSelectedNoteNames={setSelectedNoteNames} setSelectedNoteNames={setSelectedNoteNames}
notesForFilter={notes}
setFilterAppliedNotes={setFilterAppliedNotes}
/> />
</div> </div>
</div> </div>
@ -421,9 +425,10 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
<div className="mt-0"> <div className="mt-0">
<NotesCardViewDirectory <NotesCardViewDirectory
notes={notes} notes={notes}
setNotes={setNotes} setNotesForFilter={setNotes}
searchText={searchText} searchText={searchText}
selectedNoteNames={selectedNoteNames} setIsOpenModalNote={setIsOpenModalNote}
filterAppliedNotes={filterAppliedNotes}
/> />
</div> </div>
)} )}

View File

@ -19,12 +19,19 @@ const DirectoryPageHeader = ({
IsActive, IsActive,
contactsToExport, contactsToExport,
notesToExport, notesToExport,
selectedNoteNames, // Changed to array selectedNoteNames,
setSelectedNoteNames, // Changed to array setSelectedNoteNames,
notesForFilter,
setFilterAppliedNotes
}) => { }) => {
const [filtered, setFiltered] = useState(0); const [filtered, setFiltered] = useState(0);
const [filteredNotes, setFilteredNotes] = useState([]);
const [noteCreators, setNoteCreators] = useState([]); const [noteCreators, setNoteCreators] = useState([]);
const [allCreators, setAllCreators] = useState([]);
const [allOrganizations, setAllOrganizations] = useState([]);
const [filteredOrganizations, setFilteredOrganizations] = useState([]);
const [selectedCreators, setSelectedCreators] = useState([]); // Corrected to setSelectedCreators
const [selectedOrgs, setSelectedOrgs] = useState([]);
useEffect(() => { useEffect(() => {
setFiltered(tempSelectedBucketIds?.length + tempSelectedCategoryIds?.length); setFiltered(tempSelectedBucketIds?.length + tempSelectedCategoryIds?.length);
@ -55,6 +62,23 @@ const DirectoryPageHeader = ({
} }
}, [viewType]); }, [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) => { const handleToggleNoteName = (name) => {
setSelectedNoteNames(prevSelectedNames => { setSelectedNoteNames(prevSelectedNames => {
@ -66,6 +90,45 @@ const DirectoryPageHeader = ({
}); });
}; };
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);
};
useEffect(() => {
updateFilteredOrganizations();
}, [selectedCreators]);
const handleExport = (type) => { const handleExport = (type) => {
let dataToExport = []; let dataToExport = [];
@ -149,6 +212,41 @@ const DirectoryPageHeader = ({
} }
}; };
const applyCombinedFilter = () => {
const lowerSearch = searchText?.toLowerCase() || "";
const filtered = notesForFilter.filter((noteItem) => {
const creator = `${noteItem.createdBy?.firstName || ""} ${noteItem.createdBy?.lastName || ""}`.trim();
const org = noteItem.organizationName;
const matchesCreator = selectedCreators.length === 0 || selectedCreators.includes(creator);
const matchesOrg = selectedOrgs.length === 0 || selectedOrgs.includes(org);
const plainNote = noteItem?.note?.replace(/<[^>]+>/g, "").toLowerCase();
const stringValues = [];
const extractStrings = (obj) => {
for (const key in obj) {
const value = obj[key];
if (typeof value === "string") {
stringValues.push(value.toLowerCase());
} else if (typeof value === "object" && value !== null) {
extractStrings(value);
}
}
};
extractStrings(noteItem);
stringValues.push(plainNote, creator.toLowerCase());
const matchesSearch = stringValues.some((val) => val.includes(lowerSearch));
return matchesCreator && matchesOrg && matchesSearch;
});
setFilteredNotes(filtered);
setFilterAppliedNotes(filtered);
};
return ( return (
<> <>
<div className="row mx-0 px-0 align-items-center mt-0"> <div className="row mx-0 px-0 align-items-center mt-0">
@ -189,52 +287,72 @@ const DirectoryPageHeader = ({
style={{ width: "200px" }} style={{ width: "200px" }}
/> />
{/* Name Filter Dropdown - now with checkboxes */} {/* Moved the "Filter by" dropdown to be triggered by the funnel icon */}
{viewType === "notes" && noteCreators.length > 0 && ( {viewType === "notes" && (
<div className="dropdown" style={{ width: "fit-content" }}> <div className="dropdown" style={{ width: "500px" }}>
<button <a
className="btn btn-sm btn-outline-secondary dropdown-toggle" // Added btn-sm for small size className="dropdown-toggle hide-arrow cursor-pointer d-flex align-items-center position-relative"
type="button"
id="notesNameFilterDropdown"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-expanded="false" aria-expanded="false"
> >
{selectedNoteNames.length > 0 <i className={`fa-solid fa-filter ms-1 fs-5 ${selectedCreators.length > 0 || selectedOrgs.length > 0 ? "text-primary" : "text-muted"}`}></i>
? `Names (${selectedNoteNames.length})` {(selectedCreators.length > 0 || selectedOrgs.length > 0) && (
: "Filter by Name"} <span className="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning" style={{ fontSize: "0.4rem" }}>
</button> {selectedCreators.length + selectedOrgs.length}
<ul className="dropdown-menu p-2" aria-labelledby="notesNameFilterDropdown"> </span>
{/* Option to clear all selections */} )}
<li> </a>
<a
className={`dropdown-item ${selectedNoteNames.length === 0 ? "active" : ""}`} <div className="dropdown-menu p-3" style={{ width: "300px", maxHeight: "400px", overflowY: "auto" }}>
href="#" <p className="text-muted mb-1">Created By</p>
onClick={(e) => { {allCreators.map((name, idx) => (
e.preventDefault(); <div className="form-check mb-1" key={`creator-${idx}`}>
setSelectedNoteNames([]); // Clear all <input
className="form-check-input"
type="checkbox"
id={`creator-${idx}`}
checked={selectedCreators.includes(name)}
onChange={() => handleToggleCreator(name)}
/>
<label className="form-check-label" htmlFor={`creator-${idx}`}>
{name}
</label>
</div>
))}
<p className="text-muted mt-3 mb-1">Organization</p>
{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" htmlFor={`org-${idx}`}>
{org}
</label>
</div>
))}
<div className="d-flex justify-content-between mt-2">
<button
className="btn btn-sm btn-outline-danger"
onClick={() => {
setSelectedCreators([]);
setSelectedOrgs([]);
setFilteredOrganizations(allOrganizations);
applyCombinedFilter();
}} }}
> >
Clear Clear
</a> </button>
</li> <button className="btn btn-sm btn-primary" onClick={applyCombinedFilter}>
<li><hr className="dropdown-divider" /></li> Apply Filter
{noteCreators.map((name, index) => ( </button>
<li key={index}> </div>
<div className="form-check dropdown-item p-0"> </div>
<input
className="form-check-input ms-2"
type="checkbox"
id={`name-filter-${index}`}
checked={selectedNoteNames.includes(name)}
onChange={() => handleToggleNoteName(name)}
/>
<label className="form-check-label ms-2" htmlFor={`name-filter-${index}`}>
{name}
</label>
</div>
</li>
))}
</ul>
</div> </div>
)} )}