Compare commits

..

No commits in common. "765b4356a6919a4058f9626be28d75bc57a915d2" and "006ca21e1db31796208094ad18ce1db074aeeed6" have entirely different histories.

6 changed files with 130 additions and 394 deletions

View File

@ -1,56 +0,0 @@
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 (
<div className="d-flex flex-wrap align-items-center gap-2">
{filterChips.map((chipGroup) => (
<div key={chipGroup.key} className="d-flex align-items-center flex-wrap">
<span className="fw-semibold me-2">{chipGroup.label}:</span>
{chipGroup.items.map((item) => (
<span
key={item.id}
className="d-flex align-items-center bg-light rounded px-2 py-1 me-1"
>
<span>{item.name}</span>
<button
type="button"
className="btn-close btn-close-white btn-sm ms-2"
style={{
filter: "invert(1) grayscale(1)",
opacity: 0.7,
fontSize: "0.6rem",
}}
onClick={() => removeFilterChip(chipGroup.key, item.id)}
/>
</span>
))}
</div>
))}
</div>
);
};
export default ContactFilterChips;

View File

@ -1,79 +0,0 @@
import React, { useMemo } from "react";
import moment from "moment";
const NoteFilterChips = ({ filters, filterData, removeFilterChip }) => {
// Normalize data (in case its 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 (
<div className="row my-2">
<div className="col-12">
<div className="d-flex flex-wrap align-items-start gap-2">
{filterChips.map((chip) => (
<div
key={chip.key}
className="d-flex align-items-center flex-wrap px-2 py-1"
style={{ fontSize: "0.9rem" }}
>
<span className="fw-semibold me-2">{chip.label}:</span>
<div className="d-flex flex-wrap align-items-center gap-1">
{chip.items.map((item) => (
<span
key={item.id}
className="d-flex align-items-center bg-light rounded px-2 py-1 text-xs"
>
<span>{item.name}</span>
<button
type="button"
className="btn-close btn-close-white btn-sm ms-2"
style={{
filter: "invert(1) grayscale(1)",
opacity: 0.7,
fontSize: "0.6rem",
}}
onClick={() => removeFilterChip(chip.key, item.id)}
/>
</span>
))}
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default NoteFilterChips;

View File

@ -1,10 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import React, { import React from "react";
useEffect,
useImperativeHandle,
forwardRef,
useMemo,
} from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { import {
contactsFilter, contactsFilter,
@ -13,101 +8,70 @@ import {
import { useContactFilter } from "../../hooks/useDirectory"; import { useContactFilter } from "../../hooks/useDirectory";
import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton"; import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton";
import SelectMultiple from "../../components/common/SelectMultiple"; import SelectMultiple from "../../components/common/SelectMultiple";
import { useParams } from "react-router-dom";
const ContactFilterPanel = forwardRef( const ContactFilterPanel = ({ onApply, clearFilter }) => {
({ onApply, clearFilter, setFilterdata }, ref) => { const { data, isError, isLoading, error, isFetched, isFetching } =
const { data, isError, isLoading, error, isFetched, isFetching } = useContactFilter();
useContactFilter();
const { status } = useParams();
const dynamicdefaultContactFilter = useMemo(() => { const methods = useForm({
return { resolver: zodResolver(contactsFilter),
...defaultContactFilter, defaultValues: defaultContactFilter,
bucketIds: defaultContactFilter.bucketIds || [], });
categoryIds: defaultContactFilter.categoryIds || [],
};
}, [status]);
const methods = useForm({ const closePanel = () => {
resolver: zodResolver(contactsFilter), document.querySelector(".offcanvas.show .btn-close")?.click();
defaultValues: dynamicdefaultContactFilter, };
});
const { handleSubmit, reset, setValue, getValues } = methods; const { register, handleSubmit, reset, watch } = methods;
useImperativeHandle(ref, () => ({ const onSubmit = (formData) => {
resetFieldValue: (name, value) => { onApply(formData);
if (value !== undefined) { closePanel();
setValue(name, value); };
} else {
reset({ ...getValues(), [name]: defaultContactFilter[name] });
}
},
getValues, // optional: allows parent to read current form values
}));
useEffect(() => { const handleClose = () => {
if (data && setFilterdata) { reset(defaultContactFilter);
setFilterdata(data); onApply(defaultContactFilter);
} closePanel();
}, [data, setFilterdata]); };
const closePanel = () => { if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
document.querySelector(".offcanvas.show .btn-close")?.click(); if (isError && isFetched)
}; return <div>Something went wrong Here- {error.message} </div>;
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
<div className="row g-2">
<SelectMultiple
name="bucketIds"
label="Buckets :"
options={data.buckets}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="categoryIds"
label="Contact Category :"
options={data.contactCategories}
labelKey={(item) => item.name}
valueKey="id"
/>
</div>
<div className="d-flex justify-content-end py-3 gap-2">
<button
type="button"
className="btn btn-label-secondary btn-xs"
onClick={handleClose}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-xs">
Apply
</button>
</div>
</form>
</FormProvider>
);
};
const onSubmit = (formData) => { export default ContactFilterPanel;
onApply(formData);
closePanel();
};
const handleClose = () => {
reset(defaultContactFilter);
onApply(defaultContactFilter);
closePanel();
};
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
if (isError && isFetched)
return <div>Something went wrong {error?.message}</div>;
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
<div className="row g-2">
<SelectMultiple
name="bucketIds"
label="Buckets:"
options={data?.buckets || []}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="categoryIds"
label="Contact Category:"
options={data?.contactCategories || []}
labelKey={(item) => item.name}
valueKey="id"
/>
</div>
<div className="d-flex justify-content-end py-3 gap-2">
<button
type="button"
className="btn btn-label-secondary btn-xs"
onClick={handleClose}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-xs">
Apply
</button>
</div>
</form>
</FormProvider>
);
}
);
export default ContactFilterPanel;

View File

@ -1,19 +1,21 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { useFab } from "../../Context/FabContext"; import { useFab } from "../../Context/FabContext";
import { useContactList } from "../../hooks/useDirectory"; import { useContactList } from "../../hooks/useDirectory";
import { useDirectoryContext } from "./DirectoryPage"; import { useDirectoryContext } from "./DirectoryPage";
import CardViewContact from "../../components/Directory/CardViewContact"; import CardViewContact from "../../components/Directory/CardViewContact";
import { ITEMS_PER_PAGE } from "../../utils/constants"; import { ITEMS_PER_PAGE } from "../../utils/constants";
import ContactFilterPanel from "./ContactFilterPanel"; import ContactFilterPanel from "./ContactFilterPanel";
import ContactFilterChips from "../../components/Directory/ContactFilterChips";
import { defaultContactFilter } from "../../components/Directory/DirectorySchema"; import { defaultContactFilter } from "../../components/Directory/DirectorySchema";
import { useDebounce } from "../../utils/appUtils"; import { useDebounce } from "../../utils/appUtils";
import Pagination from "../../components/common/Pagination"; import Pagination from "../../components/common/Pagination";
import ListViewContact from "../../components/Directory/ListViewContact"; import ListViewContact from "../../components/Directory/ListViewContact";
import {
CardViewContactSkeleton,
ListViewContactSkeleton,
} from "../../components/Directory/DirectoryPageSkeleton";
import Loader from "../../components/common/Loader"; import Loader from "../../components/common/Loader";
// Utility for CSV export // Utility function to format contacts for CSV export
const formatExportData = (contacts) => { const formatExportData = (contacts) => {
return contacts.map((contact) => ({ return contacts.map((contact) => ({
Email: contact.contactEmails?.map((e) => e.emailAddress).join(", ") || "", Email: contact.contactEmails?.map((e) => e.emailAddress).join(", ") || "",
@ -32,10 +34,8 @@ const formatExportData = (contacts) => {
const ContactsPage = ({ projectId, searchText, onExport }) => { const ContactsPage = ({ projectId, searchText, onExport }) => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilter] = useState(defaultContactFilter); const [filters, setFilter] = useState(defaultContactFilter);
const [filterData, setFilterdata] = useState(null);
const debouncedSearch = useDebounce(searchText, 500); const debouncedSearch = useDebounce(searchText, 500);
const { showActive, gridView } = useDirectoryContext(); const { showActive, gridView } = useDirectoryContext();
const updatedRef = useRef();
const { data, isError, isLoading, error } = useContactList( const { data, isError, isLoading, error } = useContactList(
showActive, showActive,
projectId, projectId,
@ -46,19 +46,13 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
); );
const { setOffcanvasContent, setShowTrigger } = useFab(); const { setOffcanvasContent, setShowTrigger } = useFab();
// clear filters
const clearFilter = () => setFilter(defaultContactFilter); const clearFilter = () => setFilter(defaultContactFilter);
useEffect(() => { useEffect(() => {
setShowTrigger(true); setShowTrigger(true);
setOffcanvasContent( setOffcanvasContent(
"Contacts Filters", "Contacts Filters",
<ContactFilterPanel <ContactFilterPanel onApply={setFilter} clearFilter={clearFilter} />
ref={updatedRef}
onApply={setFilter}
clearFilter={clearFilter}
setFilterdata={setFilterdata}
/>
); );
return () => { return () => {
@ -67,7 +61,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
}; };
}, []); }, []);
// export data // 🔹 Format contacts for export
useEffect(() => { useEffect(() => {
if (data?.data && onExport) { if (data?.data && onExport) {
onExport(formatExportData(data.data)); onExport(formatExportData(data.data));
@ -80,49 +74,23 @@ 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 <div>{error.message}</div>; if (isError) return <div>{error.message}</div>;
// if (isLoading) return gridView ? <CardViewContactSkeleton /> : <ListViewContactSkeleton />;
return ( return (
<div className="row mt-4"> <div className="row mt-5">
{/* Chips Section */}
<div className="col-12 mb-2">
<ContactFilterChips
filters={filters}
filterData={filterData}
removeFilterChip={handleRemoveChip}
clearFilter={clearFilter}
/>
</div>
{/* Grid / List View */}
{gridView ? ( {gridView ? (
<> <>
{isLoading && <Loader />} {isLoading && (
<div>
{data?.data?.length === 0 && ( <Loader />
<div className="py-4 text-center">
{searchText
? `No contact found for "${searchText}"`
: "No contacts found"}
</div> </div>
)} )}
{data?.data?.length === 0 && (<div className="py-12 ">
{searchText ? `No contact found for "${searchText}"`:"No contacts found" }
</div>)}
{data?.data?.map((contact) => ( {data?.data?.map((contact) => (
<div <div
key={contact.id} key={contact.id}
@ -161,4 +129,4 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
); );
}; };
export default ContactsPage; export default ContactsPage;

View File

@ -1,38 +1,29 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect, useImperativeHandle, forwardRef, useMemo } from "react"; import React from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { import {
defaultNotesFilter, defaultNotesFilter,
notesFilter, notesFilter,
} from "../../components/Directory/DirectorySchema"; } from "../../components/Directory/DirectorySchema";
import { useNoteFilter } from "../../hooks/useDirectory"; import { useContactFilter, useNoteFilter } from "../../hooks/useDirectory";
import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton"; import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton";
import SelectMultiple from "../../components/common/SelectMultiple"; import SelectMultiple from "../../components/common/SelectMultiple";
const NoteFilterPanel = forwardRef(({ onApply, clearFilter, setFilterdata }, ref) => { const NoteFilterPanel = ({ onApply, clearFilter }) => {
const { data, isError, isLoading, error, isFetched, isFetching } = useNoteFilter(); 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({ const methods = useForm({
resolver: zodResolver(notesFilter), resolver: zodResolver(notesFilter),
defaultValues: dynamicdefaultNotesFilter, defaultValues: defaultNotesFilter,
}); });
const { handleSubmit, reset, setValue, getValues } = methods;
const closePanel = () => { const closePanel = () => {
document.querySelector(".offcanvas.show .btn-close")?.click(); document.querySelector(".offcanvas.show .btn-close")?.click();
}; };
const { register, handleSubmit, reset, watch } = methods;
const onSubmit = (formData) => { const onSubmit = (formData) => {
onApply(formData); onApply(formData);
closePanel(); closePanel();
@ -40,72 +31,47 @@ const NoteFilterPanel = forwardRef(({ onApply, clearFilter, setFilterdata }, ref
const handleClose = () => { const handleClose = () => {
reset(defaultNotesFilter); reset(defaultNotesFilter);
onApply(defaultNotesFilter); onApply(defaultNotesFilter);
closePanel(); 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 <ExpenseFilterSkeleton />;
if (isError && isFetched)
return <div>Something went wrong Here- {error.message} </div>;
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start"> <form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
{isLoading || isFetching ? ( <div className="row g-2">
<ExpenseFilterSkeleton /> <SelectMultiple
) : isError && isFetched ? ( name="createdByIds"
<div>Something went wrong here: {error?.message}</div> label="Created By :"
) : ( options={data.createdBy}
<> labelKey="name"
<div className="row g-2"> valueKey="id"
<SelectMultiple />
name="createdByIds" <SelectMultiple
label="Created By :" name="organizations"
options={data?.createdBy || []} label="Organization:"
labelKey="name" options={data.organizations}
valueKey="id" labelKey={(item) => item.name}
/> valueKey="id"
<SelectMultiple />
name="organizations" </div>
label="Organization:" <div className="d-flex justify-content-end py-3 gap-2">
options={data?.organizations || []} <button
labelKey={(item) => item.name} type="button"
valueKey="id" className="btn btn-label-secondary btn-sm"
/> onClick={handleClose}
</div> >
Clear
<div className="d-flex justify-content-end py-3 gap-2"> </button>
<button <button type="submit" className="btn btn-primary btn-sm">
type="button" Apply
className="btn btn-label-secondary btn-sm" </button>
onClick={handleClose} </div>
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-sm">
Apply
</button>
</div>
</>
)}
</form> </form>
</FormProvider> </FormProvider>
); );
}); };
export default NoteFilterPanel; export default NoteFilterPanel;

View File

@ -1,4 +1,5 @@
import React, { useEffect, useRef, useState } from "react"; // NotesPage.jsx
import React, { useEffect, useState } from "react";
import { useFab } from "../../Context/FabContext"; import { useFab } from "../../Context/FabContext";
import { useNotes } from "../../hooks/useDirectory"; import { useNotes } from "../../hooks/useDirectory";
import NoteFilterPanel from "./NoteFilterPanel"; import NoteFilterPanel from "./NoteFilterPanel";
@ -8,14 +9,11 @@ import { useDebounce } from "../../utils/appUtils";
import NoteCardDirectoryEditable from "../../components/Directory/NoteCardDirectoryEditable"; import NoteCardDirectoryEditable from "../../components/Directory/NoteCardDirectoryEditable";
import Pagination from "../../components/common/Pagination"; import Pagination from "../../components/common/Pagination";
import { NoteCardSkeleton } from "../../components/Directory/DirectoryPageSkeleton"; import { NoteCardSkeleton } from "../../components/Directory/DirectoryPageSkeleton";
import NoteFilterChips from "../../components/Directory/NoteFilterChips";
const NotesPage = ({ projectId, searchText, onExport }) => { const NotesPage = ({ projectId, searchText, onExport }) => {
const [filters, setFilter] = useState(defaultNotesFilter); const [filters, setFilter] = useState(defaultNotesFilter);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const debouncedSearch = useDebounce(searchText, 500); const debouncedSearch = useDebounce(searchText, 500);
const [filterData, setFilterdata] = useState(null);
const updatedRef = useRef();
const { data, isLoading, isError, error } = useNotes( const { data, isLoading, isError, error } = useNotes(
projectId, projectId,
@ -35,12 +33,7 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
setShowTrigger(true); setShowTrigger(true);
setOffcanvasContent( setOffcanvasContent(
"Notes Filters", "Notes Filters",
<NoteFilterPanel <NoteFilterPanel onApply={setFilter} clearFilter={clearFilter} />
ref={updatedRef} //Call here
onApply={setFilter}
clearFilter={clearFilter}
setFilterdata={setFilterdata}
/>
); );
return () => { return () => {
@ -49,27 +42,11 @@ 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 // Format data for export
const formatExportData = (notes) => { const formatExportData = (notes) => {
return notes.map((n) => ({ return notes.map((n) => ({
ContactName: n.contactName || "", ContactName: n.contactName || "",
Note: n.note ? n.note.replace(/<[^>]+>/g, "") : "", Note: n.note ? n.note.replace(/<[^>]+>/g, "") : "", // strip HTML tags
Organization: n.organizationName || "", Organization: n.organizationName || "",
CreatedBy: n.createdBy CreatedBy: n.createdBy
? `${n.createdBy.firstName || ""} ${n.createdBy.lastName || ""}`.trim() ? `${n.createdBy.firstName || ""} ${n.createdBy.lastName || ""}`.trim()
@ -82,6 +59,7 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
})); }));
}; };
// Pass formatted notes to parent for export
useEffect(() => { useEffect(() => {
if (data?.data && onExport) { if (data?.data && onExport) {
onExport(formatExportData(data.data)); onExport(formatExportData(data.data));
@ -99,12 +77,6 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
return ( return (
<div className="d-flex flex-column text-start mt-3"> <div className="d-flex flex-column text-start mt-3">
<NoteFilterChips
filters={filters}
filterData={filterData}
removeFilterChip={handleRemoveChip}
/>
{data?.data?.length > 0 ? ( {data?.data?.length > 0 ? (
<> <>
{data.data.map((noteItem) => ( {data.data.map((noteItem) => (
@ -124,6 +96,7 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
</div> </div>
</> </>
) : ( ) : (
// Card for "No notes available"
<div <div
className="card text-center d-flex align-items-center justify-content-center" className="card text-center d-flex align-items-center justify-content-center"
style={{ height: "200px" }} style={{ height: "200px" }}
@ -131,9 +104,9 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
<p className="text-muted mb-0"> <p className="text-muted mb-0">
{debouncedSearch {debouncedSearch
? `No notes found matching "${searchText}"` ? `No notes found matching "${searchText}"`
: Object.keys(filters).some((k) => filters[k]?.length) : Object.keys(filters).some((k) => filters[k] && filters[k].length)
? "No notes found for the applied filters." ? "No notes found for the applied filters."
: "No notes available."} : "No notes available."}
</p> </p>
</div> </div>
)} )}
@ -141,4 +114,4 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
); );
}; };
export default NotesPage; export default NotesPage;