In Directory changing filter logic and adding popup.

This commit is contained in:
Kartik sharma 2025-06-27 15:47:36 +05:30
parent abc8bd8629
commit deb7100899
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 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,
@ -20,6 +22,8 @@ const NoteCardDirectoryEditable = ({
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 {
@ -69,6 +73,13 @@ const NoteCardDirectoryEditable = ({
}
};
const contactProfile = (contactId) => {
DirectoryRepository.GetContactProfile(contactId).then((res) => {
setOpen_contact(res?.data);
setIsOpenModalNote(true);
});
};
const handleRestore = async () => {
try {
setIsRestoring(true);
@ -84,6 +95,25 @@ const NoteCardDirectoryEditable = ({
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 p-1 shadow-sm border-1 mb-4 rounded"
style={{
@ -95,23 +125,39 @@ const NoteCardDirectoryEditable = ({
{/* Header */}
<div className="d-flex justify-content-between align-items-center mb-1">
<div className="d-flex align-items-center">
<Avatar
size="xs"
firstName={noteItem?.createdBy?.firstName}
lastName={noteItem?.createdBy?.lastName}
className="m-0"
/>
<div className="d-flex flex-column ms-0">
<span className="fw-semibold small">
{noteItem?.createdBy?.firstName} {noteItem?.createdBy?.lastName}
</span>
<span className="text-muted" style={{ fontSize: "10px" }}>
{moment
.utc(noteItem?.createdAt)
.add(5, "hours")
.add(30, "minutes")
.format("MMMM DD, YYYY [at] hh:mm A")}
</span>
<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>

View File

@ -2,20 +2,39 @@ import React, { useEffect, useState, useMemo } from "react";
import { DirectoryRepository } from "../../repositories/DirectoryRepository";
import NoteCardDirectoryEditable from "./NoteCardDirectoryEditable";
const NotesCardViewDirectory = ({ notes, setNotes, searchText, selectedNoteNames }) => { // Changed to array
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 {
@ -23,52 +42,51 @@ const NotesCardViewDirectory = ({ notes, setNotes, searchText, selectedNoteNames
}
};
useEffect(() => {
fetchNotes();
}, []);
useEffect(() => {
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 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 extractStrings = (obj) => {
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const value = obj[key];
if (typeof value === "string") {
stringValues.push(value.toLowerCase());
} else if (typeof value === "object" && value !== null) {
extractStrings(value); // Recursively extract from nested objects
extractStrings(value);
}
}
};
extractStrings(noteItem);
// Add manually stripped note, full name, date, etc.
stringValues.push(plainNote, lowerFullName, createdDate);
stringValues.push(plainNote, creator.toLowerCase());
const matchesSearch = stringValues.some((val) => val.includes(lowerSearch));
const matchesNameFilter =
selectedNoteNames.length === 0 || selectedNoteNames.includes(fullName);
return matchesSearch && matchesNameFilter;
return matchesCreator && matchesOrg && matchesSearch;
});
setFilteredNotes(filtered);
setNotes(filtered);
setCurrentPage(1);
setTotalPages(Math.ceil(filtered.length / pageSize));
}, [searchText, allNotes, selectedNoteNames]);
};
useEffect(() => {
applyCombinedFilter();
}, [searchText, allNotes]);
useEffect(() => {
setFilteredNotes(filterAppliedNotes);
}, [filterAppliedNotes])
const currentItems = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
@ -81,19 +99,19 @@ const NotesCardViewDirectory = ({ notes, setNotes, searchText, selectedNoteNames
}
};
if (loading) {
return <p className="mt-10 text-center">Loading notes...</p>;
}
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>;
if (!filteredNotes.length) {
return <p className="mt-10 text-center">No matching notes found</p>;
}
return (
<div
className="w-100 h-100"
>
<div className="d-flex flex-column text-start mt-4" style={{ gap: "0rem", minHeight: "100%" }}>
<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}
@ -104,13 +122,12 @@ const NotesCardViewDirectory = ({ notes, setNotes, searchText, selectedNoteNames
prevNotes.map((n) => (n.id === updatedNote.id ? updatedNote : n))
);
}}
onNoteDelete={() => {
fetchNotes();
}}
onNoteDelete={() => fetchNotes()}
/>
))}
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="d-flex justify-content-end mt-3 me-3">
<div className="d-flex align-items-center gap-2">
@ -128,7 +145,8 @@ const NotesCardViewDirectory = ({ notes, setNotes, searchText, selectedNoteNames
return (
<button
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 }}
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 [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([]);
@ -353,6 +355,8 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
notesToExport={notes}
selectedNoteNames={selectedNoteNames}
setSelectedNoteNames={setSelectedNoteNames}
notesForFilter={notes}
setFilterAppliedNotes={setFilterAppliedNotes}
/>
</div>
</div>
@ -421,9 +425,10 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
<div className="mt-0">
<NotesCardViewDirectory
notes={notes}
setNotes={setNotes}
setNotesForFilter={setNotes}
searchText={searchText}
selectedNoteNames={selectedNoteNames}
setIsOpenModalNote={setIsOpenModalNote}
filterAppliedNotes={filterAppliedNotes}
/>
</div>
)}

View File

@ -19,12 +19,19 @@ const DirectoryPageHeader = ({
IsActive,
contactsToExport,
notesToExport,
selectedNoteNames, // Changed to array
setSelectedNoteNames, // Changed to array
selectedNoteNames,
setSelectedNoteNames,
notesForFilter,
setFilterAppliedNotes
}) => {
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([]); // Corrected to setSelectedCreators
const [selectedOrgs, setSelectedOrgs] = useState([]);
useEffect(() => {
setFiltered(tempSelectedBucketIds?.length + tempSelectedCategoryIds?.length);
@ -55,6 +62,23 @@ const DirectoryPageHeader = ({
}
}, [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 => {
@ -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) => {
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 (
<>
<div className="row mx-0 px-0 align-items-center mt-0">
@ -189,52 +287,72 @@ const DirectoryPageHeader = ({
style={{ width: "200px" }}
/>
{/* Name Filter Dropdown - now with checkboxes */}
{viewType === "notes" && noteCreators.length > 0 && (
<div className="dropdown" style={{ width: "fit-content" }}>
<button
className="btn btn-sm btn-outline-secondary dropdown-toggle" // Added btn-sm for small size
type="button"
id="notesNameFilterDropdown"
{/* Moved the "Filter by" dropdown to be triggered by the funnel icon */}
{viewType === "notes" && (
<div className="dropdown" style={{ width: "500px" }}>
<a
className="dropdown-toggle hide-arrow cursor-pointer d-flex align-items-center position-relative"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{selectedNoteNames.length > 0
? `Names (${selectedNoteNames.length})`
: "Filter by Name"}
</button>
<ul className="dropdown-menu p-2" aria-labelledby="notesNameFilterDropdown">
{/* Option to clear all selections */}
<li>
<a
className={`dropdown-item ${selectedNoteNames.length === 0 ? "active" : ""}`}
href="#"
onClick={(e) => {
e.preventDefault();
setSelectedNoteNames([]); // Clear all
<i className={`fa-solid fa-filter ms-1 fs-5 ${selectedCreators.length > 0 || selectedOrgs.length > 0 ? "text-primary" : "text-muted"}`}></i>
{(selectedCreators.length > 0 || selectedOrgs.length > 0) && (
<span className="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning" style={{ fontSize: "0.4rem" }}>
{selectedCreators.length + selectedOrgs.length}
</span>
)}
</a>
<div className="dropdown-menu p-3" style={{ width: "300px", maxHeight: "400px", overflowY: "auto" }}>
<p className="text-muted mb-1">Created By</p>
{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" 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
</a>
</li>
<li><hr className="dropdown-divider" /></li>
{noteCreators.map((name, index) => (
<li key={index}>
<div className="form-check dropdown-item p-0">
<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>
</button>
<button className="btn btn-sm btn-primary" onClick={applyCombinedFilter}>
Apply Filter
</button>
</div>
</div>
</div>
)}