diff --git a/src/components/Directory/ContactsFilterPanel.jsx b/src/components/Directory/ContactsFilterPanel.jsx new file mode 100644 index 00000000..1ec0e4a6 --- /dev/null +++ b/src/components/Directory/ContactsFilterPanel.jsx @@ -0,0 +1,94 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import React, { useState, useCallback, useEffect } from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import { useLocation } from "react-router-dom"; +import moment from "moment"; + +// replace these with your actual schema + defaults +// import { contactsFilterSchema, defaultContactsFilterValues } from "./ContactsSchema"; +import { contactsFilterSchema,defaultContactsFilterValues } from "./contactsFilterSchema"; +import SelectMultiple from "../../components/common/SelectMultiple"; + +const ContactsFilterPanel = ({ onApply, buckets = [], categories = [] }) => { + const [resetKey, setResetKey] = useState(0); + + const methods = useForm({ + resolver: zodResolver(contactsFilterSchema), + defaultValues: defaultContactsFilterValues, + }); + + const { handleSubmit, reset } = methods; + + const handleClosePanel = useCallback(() => { + document.querySelector(".offcanvas.show .btn-close")?.click(); + }, []); + + const onSubmit = useCallback( + (formData) => { + onApply({ + ...formData, + // Remove startDate/endDate handling since date picker is gone + }); + handleClosePanel(); + }, + [onApply, handleClosePanel] + ); + + // ✅ Auto-close when navigating + const location = useLocation(); + useEffect(() => { + handleClosePanel(); + }, [location, handleClosePanel]); + + const onClear = useCallback(() => { + reset(defaultContactsFilterValues); + setResetKey((prev) => prev + 1); + onApply(defaultContactsFilterValues); + }, [onApply, reset]); + + return ( + +
+
+ {/* Buckets */} +
+ +
+ + {/* Categories */} +
+ +
+
+ + {/* Footer */} +
+ + +
+
+
+ ); +}; + +export default ContactsFilterPanel; diff --git a/src/components/Directory/NotesFilterPanel.jsx b/src/components/Directory/NotesFilterPanel.jsx new file mode 100644 index 00000000..16a3f1eb --- /dev/null +++ b/src/components/Directory/NotesFilterPanel.jsx @@ -0,0 +1,71 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import React, { useState, useCallback } from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import { defaultNotesFilterValues,notesFilterSchema } from "./notesFilterSchema"; +import SelectMultiple from "../../components/common/SelectMultiple"; + +const NotesFilterPanel = ({ onApply, creators = [], organizations = [] }) => { + const [resetKey, setResetKey] = useState(0); + + const methods = useForm({ + resolver: zodResolver(notesFilterSchema), + defaultValues: defaultNotesFilterValues, + }); + + const { handleSubmit, reset } = methods; + + const onSubmit = useCallback( + (formData) => { + onApply(formData); + document.querySelector(".offcanvas.show .btn-close")?.click(); + }, + [onApply] + ); + + const onClear = useCallback(() => { + reset(defaultNotesFilterValues); + setResetKey((prev) => prev + 1); + onApply(defaultNotesFilterValues); + }, [onApply, reset]); + + return ( + +
+
+ ({ id: c, name: c }))} + labelKey="name" + valueKey="id" + /> +
+ +
+ ({ id: o, name: o }))} + labelKey="name" + valueKey="id" + /> +
+ +
+ + +
+
+
+ ); +}; + +export default NotesFilterPanel; diff --git a/src/components/Directory/contactsFilterSchema.js b/src/components/Directory/contactsFilterSchema.js new file mode 100644 index 00000000..1b46d829 --- /dev/null +++ b/src/components/Directory/contactsFilterSchema.js @@ -0,0 +1,15 @@ +import { z } from "zod"; + +export const contactsFilterSchema = z.object({ + startDate: z.string().optional(), + endDate: z.string().optional(), + bucketIds: z.array(z.number()).optional(), + categoryIds: z.array(z.number()).optional(), +}); + +export const defaultContactsFilterValues = { + startDate: "", + endDate: "", + bucketIds: [], + categoryIds: [], +}; diff --git a/src/components/Directory/notesFilterSchema.js b/src/components/Directory/notesFilterSchema.js new file mode 100644 index 00000000..6451d0e6 --- /dev/null +++ b/src/components/Directory/notesFilterSchema.js @@ -0,0 +1,11 @@ +import { z } from "zod"; + +export const notesFilterSchema = z.object({ + creators: z.array(z.string()).optional(), + organizations: z.array(z.string()).optional(), +}); + +export const defaultNotesFilterValues = { + creators: [], + organizations: [], +}; diff --git a/src/pages/Directory/Directory.jsx b/src/pages/Directory/Directory.jsx index 23b01267..7a8eb310 100644 --- a/src/pages/Directory/Directory.jsx +++ b/src/pages/Directory/Directory.jsx @@ -134,9 +134,14 @@ const Directory = ({ IsPage = true, prefernceContacts }) => { setSelectedContact(null); setOpen_contact(null); }; - const [selectedCategoryIds, setSelectedCategoryIds] = useState( - contactCategory.map((category) => category.id) - ); + // const [selectedCategoryIds, setSelectedCategoryIds] = useState( + // contactCategory.map((category) => category.id) + // ); + + const [selectedCategoryIds, setSelectedCategoryIds] = useState([]); + const filtered = tempSelectedBucketIds.length + tempSelectedCategoryIds.length; + + useEffect(() => { setContactList(contacts); @@ -200,13 +205,21 @@ const Directory = ({ IsPage = true, prefernceContacts }) => { setSelectedCategoryIds(tempSelectedCategoryIds); }; + // const clearFilter = () => { + // setTempSelectedBucketIds([]); + // setTempSelectedCategoryIds([]); + // setSelectedBucketIds([]); + // setSelectedCategoryIds([]); + // }; const clearFilter = () => { setTempSelectedBucketIds([]); setTempSelectedCategoryIds([]); setSelectedBucketIds([]); setSelectedCategoryIds([]); + setFilterAppliedNotes([]); // reset notes filter }; + const { currentPage, totalPages, currentItems, paginate } = usePagination( filteredContacts, ITEMS_PER_PAGE @@ -351,8 +364,11 @@ const Directory = ({ IsPage = true, prefernceContacts }) => { loading={loading} IsActive={IsActive} setOpenBucketModal={setOpenBucketModal} - contactsToExport={contacts} - notesToExport={notes} + // contactsToExport={contacts} + // notesToExport={notes} + filtered={filtered} + contactsToExport={filteredContacts} + notesToExport={filterAppliedNotes.length > 0 ? filterAppliedNotes : notes} selectedNoteNames={selectedNoteNames} setSelectedNoteNames={setSelectedNoteNames} notesForFilter={notes} @@ -395,7 +411,7 @@ const Directory = ({ IsPage = true, prefernceContacts }) => { )} {viewType === "card" && ( -
+
{!loading && currentItems.map((contact) => (
{
)} - {viewType === "notes" && ( + {/* {viewType === "notes" && (
{ filterAppliedNotes={filterAppliedNotes} />
+ )} */} + + {viewType === "notes" && ( +
+ 0 ? filterAppliedNotes : notes} + setNotesForFilter={setNotes} + searchText={searchText} + setIsOpenModalNote={setIsOpenModalNote} + filterAppliedNotes={filterAppliedNotes} + /> +
)} + {/* Pagination */} {!loading && viewType !== "notes" && @@ -489,4 +518,4 @@ const Directory = ({ IsPage = true, prefernceContacts }) => { ); }; -export default Directory; \ No newline at end of file +export default Directory; diff --git a/src/pages/Directory/DirectoryPageHeader.jsx b/src/pages/Directory/DirectoryPageHeader.jsx index f5f4fdde..b94acaca 100644 --- a/src/pages/Directory/DirectoryPageHeader.jsx +++ b/src/pages/Directory/DirectoryPageHeader.jsx @@ -1,591 +1,288 @@ -import React, { useEffect, useState } from "react"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { exportToCSV, exportToExcel, printTable, exportToPDF } from "../../utils/tableExportUtils"; +import { useFab } from "../../Context/FabContext"; +import NotesFilterPanel from "../../components/Directory/NotesFilterPanel"; +import ContactsFilterPanel from "../../components/Directory/ContactsFilterPanel"; const DirectoryPageHeader = ({ - searchText, - setSearchText, - setIsActive, - viewType, - setViewType, - filteredBuckets, - tempSelectedBucketIds, - handleTempBucketChange, - filteredCategories, - tempSelectedCategoryIds, - handleTempCategoryChange, - clearFilter, - applyFilter, - loading, - IsActive, - contactsToExport, - notesToExport, - selectedNoteNames, - setSelectedNoteNames, - notesForFilter, - setFilterAppliedNotes + searchText, + setSearchText, + setIsActive, + viewType, + setViewType, + filteredBuckets, + tempSelectedBucketIds, + filteredCategories, + tempSelectedCategoryIds, + loading, + IsActive, + contactsToExport, + notesToExport, + notesForFilter, + setFilterAppliedNotes, + setAppliedContactFilters, }) => { - 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([]); + const [filteredCount, setFilteredCount] = useState(0); + const { setOffcanvasContent, setShowTrigger } = useFab(); - useEffect(() => { - setFiltered(tempSelectedBucketIds?.length + tempSelectedCategoryIds?.length); - }, [tempSelectedBucketIds, tempSelectedCategoryIds]); + // 🟢 Count filters for badge + useEffect(() => { + setFilteredCount(tempSelectedBucketIds?.length + tempSelectedCategoryIds?.length); + }, [tempSelectedBucketIds, tempSelectedCategoryIds]); - // New state to track active filters for notes - const [notesFilterCount, setNotesFilterCount] = useState(0); + // ----------------- EXPORT HANDLER ----------------- + const handleExport = (type) => { + let dataToExport = []; - useEffect(() => { - // Calculate the number of active filters for notes - setNotesFilterCount(selectedCreators.length + selectedOrgs.length); - }, [selectedCreators, selectedOrgs]); + if (viewType === "notes") { + if (!notesToExport || notesToExport.length === 0) return; - 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]); + const decodeHtmlEntities = (html) => { + const textarea = document.createElement("textarea"); + textarea.innerHTML = html; + return textarea.value; + }; - // Separate effect to clear selection only when switching away from notes - useEffect(() => { - if (viewType !== "notes" && selectedNoteNames.length > 0) { - setSelectedNoteNames([]); - } - }, [viewType]); + const cleanNoteText = (html) => { + if (!html) return ""; + const stripped = html.replace(/<[^>]+>/g, ""); + const decoded = decodeHtmlEntities(stripped); + return decoded.replace(/\u00A0/g, " ").replace(/\s+/g, " ").trim(); + }; - useEffect(() => { - const creatorsSet = new Set(); - const orgsSet = new Set(); + const cleanName = (name) => { + if (!name) return ""; + return name.replace(/\u00A0/g, " ").replace(/\s+/g, " ").trim(); + }; - notesForFilter.forEach((note) => { - const creator = `${note.createdBy?.firstName || ""} ${note.createdBy?.lastName || ""}`.trim(); - if (creator) creatorsSet.add(creator); + 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) return; - const org = note.organizationName; - if (org) orgsSet.add(org); - }); + 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(", ") || "", + })); + } - setAllCreators([...creatorsSet].sort()); - setAllOrganizations([...orgsSet].sort()); - setFilteredOrganizations([...orgsSet].sort()); - }, [notesForFilter]) + 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}`; - const handleToggleNoteName = (name) => { - setSelectedNoteNames(prevSelectedNames => { - if (prevSelectedNames.includes(name)) { - return prevSelectedNames.filter(n => n !== name); - } else { - return [...prevSelectedNames, name]; - } - }); - }; + 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 updateFilteredOrganizations = () => { - if (selectedCreators.length === 0) { - setFilteredOrganizations(allOrganizations); - return; - } + // ----------------- FILTER PANELS ----------------- + const handleApplyContactsFilter = useCallback( + (values) => { + setAppliedContactFilters(values); + }, + [setAppliedContactFilters] + ); - 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); - }; + const handleApplyNotesFilter = useCallback( + (values) => { + setFilterAppliedNotes(values); + }, + [setFilterAppliedNotes] + ); + // dynamically switch offcanvas filter panel + const filterPanelElement = useMemo(() => { + if (viewType === "notes") { + return ; + } return ( - <> -
-
-
    -
  • - -
  • -
  • - -
  • -
-
-
-
- -
-
- - setSearchText(e.target.value)} - style={{ width: "200px", height: "30px" }} - /> - - {/* Filter by funnel icon for Notes view */} - {viewType === "notes" && ( -
- - -
- {/* Scrollable Filter Content */} -
- {allCreators.length === 0 && filteredOrganizations.length === 0 ? ( -
- No filter found -
- ) : ( -
- {/* Created By */} -
-
-

Created By

-
- {allCreators.map((name, idx) => ( -
- handleToggleCreator(name)} - style={{ width: "1rem", height: "1rem" }} - /> - -
- ))} -
- - {/* Organization */} -
-
-

Organization

-
- {filteredOrganizations.map((org, idx) => ( -
- handleToggleOrg(org)} - style={{ width: "1rem", height: "1rem" }} - /> - -
- ))} -
-
- )} -
- - - {/* Sticky Footer Buttons */} -
- - -
-
-
- )} - - - {(viewType === "card" || viewType === "list") && ( -
- - - -
- )} - - {/* Filter by funnel icon for Contacts view (retains numerical badge) */} - {viewType !== "notes" && ( -
- - -
    -

    Filter by

    - - {filteredBuckets.length === 0 && filteredCategories.length === 0 ? ( -
    - No filter found -
    - ) : ( -
    -
    -

    Buckets

    -
    - {filteredBuckets.map(({ id, name }) => ( -
    - handleTempBucketChange(id)} - style={{ width: "1rem", height: "1rem" }} - /> - -
    - ))} -
    -
    - -
    -

    Categories

    -
    - {filteredCategories.map(({ id, name }) => ( -
    - handleTempCategoryChange(id)} - style={{ width: "1rem", height: "1rem" }} - /> - -
    - ))} -
    -
    -
    - )} - -
    - - -
    -
- -
- )} -
- -
- {(viewType === "list" || viewType === "card") && ( - - )} - - - -
-
- + ); + }, [viewType, notesForFilter, handleApplyContactsFilter, handleApplyNotesFilter]); + + useEffect(() => { + setShowTrigger(true); + setOffcanvasContent( + viewType === "notes" ? "Notes Filters" : "Contacts Filters", + filterPanelElement + ); + return () => { + setShowTrigger(false); + setOffcanvasContent("", null); + }; + }, [viewType, filterPanelElement]); + + // ----------------- UI ----------------- + return ( + <> + {/* Top Tabs */} +
+
+
    +
  • + +
  • +
  • + +
  • +
+
+
+
+ + {/* Search + Filter Trigger + View Toggle */} +
+
+ setSearchText(e.target.value)} + style={{ width: "200px", height: "30px" }} + /> + + {/* View toggles */} + {viewType !== "notes" && ( +
+ + +
+ )} +
+ + {/* Export + Toggle inactive */} +
+ {(viewType === "card" || viewType === "list") && ( + + )} + + +
+
+ + ); }; export default DirectoryPageHeader; \ No newline at end of file