From df41f4c82dabee330534fde7dd247117b3de4fd7 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Sat, 11 Oct 2025 10:30:15 +0530 Subject: [PATCH] Adding Chips in Directory Contact and Notes Page. --- .../Directory/ContactFilterChips.jsx | 56 +++++++ src/components/Directory/NoteFilterChips.jsx | 79 +++++++++ src/pages/Directory/ContactFilterPanel.jsx | 156 +++++++++++------- src/pages/Directory/ContactsPage.jsx | 68 ++++++-- src/pages/Directory/NoteFilterPanel.jsx | 118 ++++++++----- src/pages/Directory/NotesPage.jsx | 47 ++++-- 6 files changed, 394 insertions(+), 130 deletions(-) create mode 100644 src/components/Directory/ContactFilterChips.jsx create mode 100644 src/components/Directory/NoteFilterChips.jsx diff --git a/src/components/Directory/ContactFilterChips.jsx b/src/components/Directory/ContactFilterChips.jsx new file mode 100644 index 00000000..d321afa3 --- /dev/null +++ b/src/components/Directory/ContactFilterChips.jsx @@ -0,0 +1,56 @@ +import React, { useMemo } from "react"; + +const ContactFilterChips = ({ filters, filterData, removeFilterChip, clearFilter }) => { + const data = filterData?.data || filterData || {}; + + const filterChips = useMemo(() => { + const chips = []; + + const addGroup = (ids, list, label, key) => { + if (!ids?.length) return; + const items = ids.map((id) => ({ + id, + name: list?.find((i) => i.id === id)?.name || id, + })); + chips.push({ key, label, items }); + }; + + addGroup(filters.bucketIds, data.buckets, "Buckets", "bucketIds"); + addGroup(filters.categoryIds, data.contactCategories, "Category", "categoryIds"); + + return chips; + }, [filters, filterData]); + + if (!filterChips.length) return null; + + return ( +
+ {filterChips.map((chipGroup) => ( +
+ {chipGroup.label}: + {chipGroup.items.map((item) => ( + + {item.name} +
+ ))} + +
+ ); +}; + +export default ContactFilterChips; \ No newline at end of file diff --git a/src/components/Directory/NoteFilterChips.jsx b/src/components/Directory/NoteFilterChips.jsx new file mode 100644 index 00000000..5569a460 --- /dev/null +++ b/src/components/Directory/NoteFilterChips.jsx @@ -0,0 +1,79 @@ +import React, { useMemo } from "react"; +import moment from "moment"; + +const NoteFilterChips = ({ filters, filterData, removeFilterChip }) => { + // Normalize data (in case it’s wrapped in .data) + const data = filterData?.data || filterData || {}; + + const filterChips = useMemo(() => { + const chips = []; + + const buildGroup = (ids, list, label, key) => { + if (!ids?.length) return; + const items = ids.map((id) => ({ + id, + name: list?.find((item) => item.id === id)?.name || id, + })); + chips.push({ key, label, items }); + }; + + // Build chips dynamically + buildGroup(filters.createdByIds, data.createdBy, "Created By", "createdByIds"); + buildGroup(filters.organizations, data.organizations, "Organization", "organizations"); + + // Example: Add date range if you ever add in future + if (filters.startDate || filters.endDate) { + const start = filters.startDate ? moment(filters.startDate).format("DD-MM-YYYY") : ""; + const end = filters.endDate ? moment(filters.endDate).format("DD-MM-YYYY") : ""; + chips.push({ + key: "dateRange", + label: "Date Range", + items: [{ id: "dateRange", name: `${start} - ${end}` }], + }); + } + + return chips; + }, [filters, filterData]); + + if (!filterChips.length) return null; + + return ( +
+
+
+ {filterChips.map((chip) => ( +
+ {chip.label}: +
+ {chip.items.map((item) => ( + + {item.name} +
+
+ ))} +
+
+
+ ); +}; + +export default NoteFilterChips; \ No newline at end of file diff --git a/src/pages/Directory/ContactFilterPanel.jsx b/src/pages/Directory/ContactFilterPanel.jsx index 25f833db..e77c8469 100644 --- a/src/pages/Directory/ContactFilterPanel.jsx +++ b/src/pages/Directory/ContactFilterPanel.jsx @@ -1,5 +1,10 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import React from "react"; +import React, { + useEffect, + useImperativeHandle, + forwardRef, + useMemo, +} from "react"; import { FormProvider, useForm } from "react-hook-form"; import { contactsFilter, @@ -8,70 +13,101 @@ import { import { useContactFilter } from "../../hooks/useDirectory"; import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton"; import SelectMultiple from "../../components/common/SelectMultiple"; +import { useParams } from "react-router-dom"; -const ContactFilterPanel = ({ onApply, clearFilter }) => { - const { data, isError, isLoading, error, isFetched, isFetching } = - useContactFilter(); +const ContactFilterPanel = forwardRef( + ({ onApply, clearFilter, setFilterdata }, ref) => { + const { data, isError, isLoading, error, isFetched, isFetching } = + useContactFilter(); + const { status } = useParams(); - const methods = useForm({ - resolver: zodResolver(contactsFilter), - defaultValues: defaultContactFilter, - }); + const dynamicdefaultContactFilter = useMemo(() => { + return { + ...defaultContactFilter, + bucketIds: defaultContactFilter.bucketIds || [], + categoryIds: defaultContactFilter.categoryIds || [], + }; + }, [status]); - const closePanel = () => { - document.querySelector(".offcanvas.show .btn-close")?.click(); - }; + const methods = useForm({ + resolver: zodResolver(contactsFilter), + defaultValues: dynamicdefaultContactFilter, + }); - const { register, handleSubmit, reset, watch } = methods; + const { handleSubmit, reset, setValue, getValues } = methods; - const onSubmit = (formData) => { - onApply(formData); - closePanel(); - }; + useImperativeHandle(ref, () => ({ + resetFieldValue: (name, value) => { + if (value !== undefined) { + setValue(name, value); + } else { + reset({ ...getValues(), [name]: defaultContactFilter[name] }); + } + }, + getValues, // optional: allows parent to read current form values + })); - const handleClose = () => { - reset(defaultContactFilter); - onApply(defaultContactFilter); - closePanel(); - }; + useEffect(() => { + if (data && setFilterdata) { + setFilterdata(data); + } + }, [data, setFilterdata]); - if (isLoading || isFetching) return ; - if (isError && isFetched) - return
Something went wrong Here- {error.message}
; - return ( - -
-
- - item.name} - valueKey="id" - /> -
-
- - -
-
-
- ); -}; + const closePanel = () => { + document.querySelector(".offcanvas.show .btn-close")?.click(); + }; -export default ContactFilterPanel; + const onSubmit = (formData) => { + onApply(formData); + closePanel(); + }; + + const handleClose = () => { + reset(defaultContactFilter); + onApply(defaultContactFilter); + closePanel(); + }; + + if (isLoading || isFetching) return ; + if (isError && isFetched) + return
Something went wrong — {error?.message}
; + + return ( + +
+
+ + item.name} + valueKey="id" + /> +
+ +
+ + +
+
+
+ ); + } +); + +export default ContactFilterPanel; \ No newline at end of file diff --git a/src/pages/Directory/ContactsPage.jsx b/src/pages/Directory/ContactsPage.jsx index 56cf0de0..195ebb96 100644 --- a/src/pages/Directory/ContactsPage.jsx +++ b/src/pages/Directory/ContactsPage.jsx @@ -1,21 +1,19 @@ -import React, { useEffect, useState } from "react"; + +import React, { useEffect, useRef, useState } from "react"; import { useFab } from "../../Context/FabContext"; import { useContactList } from "../../hooks/useDirectory"; import { useDirectoryContext } from "./DirectoryPage"; import CardViewContact from "../../components/Directory/CardViewContact"; import { ITEMS_PER_PAGE } from "../../utils/constants"; import ContactFilterPanel from "./ContactFilterPanel"; +import ContactFilterChips from "../../components/Directory/ContactFilterChips"; import { defaultContactFilter } from "../../components/Directory/DirectorySchema"; import { useDebounce } from "../../utils/appUtils"; import Pagination from "../../components/common/Pagination"; import ListViewContact from "../../components/Directory/ListViewContact"; -import { - CardViewContactSkeleton, - ListViewContactSkeleton, -} from "../../components/Directory/DirectoryPageSkeleton"; import Loader from "../../components/common/Loader"; -// Utility function to format contacts for CSV export +// Utility for CSV export const formatExportData = (contacts) => { return contacts.map((contact) => ({ Email: contact.contactEmails?.map((e) => e.emailAddress).join(", ") || "", @@ -34,8 +32,10 @@ const formatExportData = (contacts) => { const ContactsPage = ({ projectId, searchText, onExport }) => { const [currentPage, setCurrentPage] = useState(1); const [filters, setFilter] = useState(defaultContactFilter); + const [filterData, setFilterdata] = useState(null); const debouncedSearch = useDebounce(searchText, 500); const { showActive, gridView } = useDirectoryContext(); + const updatedRef = useRef(); const { data, isError, isLoading, error } = useContactList( showActive, projectId, @@ -46,13 +46,19 @@ const ContactsPage = ({ projectId, searchText, onExport }) => { ); const { setOffcanvasContent, setShowTrigger } = useFab(); + // clear filters const clearFilter = () => setFilter(defaultContactFilter); useEffect(() => { setShowTrigger(true); setOffcanvasContent( "Contacts Filters", - + ); return () => { @@ -61,7 +67,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => { }; }, []); - // 🔹 Format contacts for export + // export data useEffect(() => { if (data?.data && onExport) { onExport(formatExportData(data.data)); @@ -74,23 +80,49 @@ const ContactsPage = ({ projectId, searchText, onExport }) => { } }; + const handleRemoveChip = (key, id) => { + setFilter((prev) => { + const updated = { ...prev }; + + if (Array.isArray(updated[key])) { + updated[key] = updated[key].filter((v) => v !== id); + updatedRef.current?.resetFieldValue(key, updated[key]); + } else { + updated[key] = null; + updatedRef.current?.resetFieldValue(key, null); + } + + return updated; + }); + }; + if (isError) return
{error.message}
; - // if (isLoading) return gridView ? : ; return ( -
+
+ {/* Chips Section */} +
+ +
+ + {/* Grid / List View */} {gridView ? ( <> - {isLoading && ( -
- + {isLoading && } + + {data?.data?.length === 0 && ( +
+ {searchText + ? `No contact found for "${searchText}"` + : "No contacts found"}
)} - - {data?.data?.length === 0 && (
- {searchText ? `No contact found for "${searchText}"`:"No contacts found" } -
)} {data?.data?.map((contact) => (
{ ); }; -export default ContactsPage; +export default ContactsPage; \ No newline at end of file diff --git a/src/pages/Directory/NoteFilterPanel.jsx b/src/pages/Directory/NoteFilterPanel.jsx index 87abd52d..b3ac36d1 100644 --- a/src/pages/Directory/NoteFilterPanel.jsx +++ b/src/pages/Directory/NoteFilterPanel.jsx @@ -1,29 +1,38 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import React from "react"; +import React, { useEffect, useImperativeHandle, forwardRef, useMemo } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { defaultNotesFilter, notesFilter, } from "../../components/Directory/DirectorySchema"; -import { useContactFilter, useNoteFilter } from "../../hooks/useDirectory"; +import { useNoteFilter } from "../../hooks/useDirectory"; import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton"; import SelectMultiple from "../../components/common/SelectMultiple"; -const NoteFilterPanel = ({ onApply, clearFilter }) => { - const { data, isError, isLoading, error, isFetched, isFetching } = - useNoteFilter(); +const NoteFilterPanel = forwardRef(({ onApply, clearFilter, setFilterdata }, ref) => { + const { data, isError, isLoading, error, isFetched, isFetching } = useNoteFilter(); + + + //Add this for Filter chip remover + const dynamicdefaultNotesFilter = useMemo(() => { + return { + ...defaultNotesFilter, + bucketIds: defaultNotesFilter.bucketIds || [], + categoryIds: defaultNotesFilter.categoryIds || [], + }; + }, [status]); const methods = useForm({ resolver: zodResolver(notesFilter), - defaultValues: defaultNotesFilter, + defaultValues: dynamicdefaultNotesFilter, }); + const { handleSubmit, reset, setValue, getValues } = methods; + const closePanel = () => { document.querySelector(".offcanvas.show .btn-close")?.click(); }; - const { register, handleSubmit, reset, watch } = methods; - const onSubmit = (formData) => { onApply(formData); closePanel(); @@ -31,47 +40,72 @@ const NoteFilterPanel = ({ onApply, clearFilter }) => { const handleClose = () => { reset(defaultNotesFilter); - onApply(defaultNotesFilter); + onApply(defaultNotesFilter); closePanel(); }; + +//Add this for Filter chip remover + useImperativeHandle(ref, () => ({ + resetFieldValue: (name, value) => { + if (value !== undefined) { + setValue(name, value); + } else { + reset({ ...getValues(), [name]: defaultNotesFilter[name] }); + } + }, + getValues, + })); + + + useEffect(() => { + if (data && setFilterdata) { + setFilterdata(data); + } + }, [data, setFilterdata]); - if (isLoading || isFetching) return ; - if (isError && isFetched) - return
Something went wrong Here- {error.message}
; return (
-
- - item.name} - valueKey="id" - /> -
-
- - -
+ {isLoading || isFetching ? ( + + ) : isError && isFetched ? ( +
Something went wrong here: {error?.message}
+ ) : ( + <> +
+ + item.name} + valueKey="id" + /> +
+ +
+ + +
+ + )}
); -}; +}); -export default NoteFilterPanel; +export default NoteFilterPanel; \ No newline at end of file diff --git a/src/pages/Directory/NotesPage.jsx b/src/pages/Directory/NotesPage.jsx index 3f0d4348..45abe8d3 100644 --- a/src/pages/Directory/NotesPage.jsx +++ b/src/pages/Directory/NotesPage.jsx @@ -1,5 +1,4 @@ -// NotesPage.jsx -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useFab } from "../../Context/FabContext"; import { useNotes } from "../../hooks/useDirectory"; import NoteFilterPanel from "./NoteFilterPanel"; @@ -9,11 +8,14 @@ import { useDebounce } from "../../utils/appUtils"; import NoteCardDirectoryEditable from "../../components/Directory/NoteCardDirectoryEditable"; import Pagination from "../../components/common/Pagination"; import { NoteCardSkeleton } from "../../components/Directory/DirectoryPageSkeleton"; +import NoteFilterChips from "../../components/Directory/NoteFilterChips"; const NotesPage = ({ projectId, searchText, onExport }) => { const [filters, setFilter] = useState(defaultNotesFilter); const [currentPage, setCurrentPage] = useState(1); const debouncedSearch = useDebounce(searchText, 500); + const [filterData, setFilterdata] = useState(null); + const updatedRef = useRef(); const { data, isLoading, isError, error } = useNotes( projectId, @@ -33,7 +35,12 @@ const NotesPage = ({ projectId, searchText, onExport }) => { setShowTrigger(true); setOffcanvasContent( "Notes Filters", - + ); return () => { @@ -42,11 +49,27 @@ const NotesPage = ({ projectId, searchText, onExport }) => { }; }, []); + const handleRemoveChip = (key, id) => { + setFilter((prev) => { + const updated = { ...prev }; + + if (Array.isArray(updated[key])) { + updated[key] = updated[key].filter((v) => v !== id); + updatedRef.current?.resetFieldValue(key, updated[key]); //IMP + } else { + updated[key] = null; + updatedRef.current?.resetFieldValue(key, null); + } + + return updated; + }); + }; + // Format data for export const formatExportData = (notes) => { return notes.map((n) => ({ ContactName: n.contactName || "", - Note: n.note ? n.note.replace(/<[^>]+>/g, "") : "", // strip HTML tags + Note: n.note ? n.note.replace(/<[^>]+>/g, "") : "", Organization: n.organizationName || "", CreatedBy: n.createdBy ? `${n.createdBy.firstName || ""} ${n.createdBy.lastName || ""}`.trim() @@ -59,7 +82,6 @@ const NotesPage = ({ projectId, searchText, onExport }) => { })); }; - // Pass formatted notes to parent for export useEffect(() => { if (data?.data && onExport) { onExport(formatExportData(data.data)); @@ -77,6 +99,12 @@ const NotesPage = ({ projectId, searchText, onExport }) => { return (
+ + {data?.data?.length > 0 ? ( <> {data.data.map((noteItem) => ( @@ -96,7 +124,6 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
) : ( - // Card for "No notes available"
{

{debouncedSearch ? `No notes found matching "${searchText}"` - : Object.keys(filters).some((k) => filters[k] && filters[k].length) - ? "No notes found for the applied filters." - : "No notes available."} + : Object.keys(filters).some((k) => filters[k]?.length) + ? "No notes found for the applied filters." + : "No notes available."}

)} @@ -114,4 +141,4 @@ const NotesPage = ({ projectId, searchText, onExport }) => { ); }; -export default NotesPage; \ No newline at end of file +export default NotesPage;