Adding Chips in Directory Contact and Notes Page.
This commit is contained in:
parent
bb2d7b8923
commit
df41f4c82d
56
src/components/Directory/ContactFilterChips.jsx
Normal file
56
src/components/Directory/ContactFilterChips.jsx
Normal file
@ -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 (
|
||||||
|
<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;
|
79
src/components/Directory/NoteFilterChips.jsx
Normal file
79
src/components/Directory/NoteFilterChips.jsx
Normal file
@ -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 (
|
||||||
|
<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;
|
@ -1,5 +1,10 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
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 { FormProvider, useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
contactsFilter,
|
contactsFilter,
|
||||||
@ -8,70 +13,101 @@ 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 = ({ onApply, clearFilter }) => {
|
const ContactFilterPanel = forwardRef(
|
||||||
const { data, isError, isLoading, error, isFetched, isFetching } =
|
({ onApply, clearFilter, setFilterdata }, ref) => {
|
||||||
useContactFilter();
|
const { data, isError, isLoading, error, isFetched, isFetching } =
|
||||||
|
useContactFilter();
|
||||||
|
const { status } = useParams();
|
||||||
|
|
||||||
const methods = useForm({
|
const dynamicdefaultContactFilter = useMemo(() => {
|
||||||
resolver: zodResolver(contactsFilter),
|
return {
|
||||||
defaultValues: defaultContactFilter,
|
...defaultContactFilter,
|
||||||
});
|
bucketIds: defaultContactFilter.bucketIds || [],
|
||||||
|
categoryIds: defaultContactFilter.categoryIds || [],
|
||||||
|
};
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
const closePanel = () => {
|
const methods = useForm({
|
||||||
document.querySelector(".offcanvas.show .btn-close")?.click();
|
resolver: zodResolver(contactsFilter),
|
||||||
};
|
defaultValues: dynamicdefaultContactFilter,
|
||||||
|
});
|
||||||
|
|
||||||
const { register, handleSubmit, reset, watch } = methods;
|
const { handleSubmit, reset, setValue, getValues } = methods;
|
||||||
|
|
||||||
const onSubmit = (formData) => {
|
useImperativeHandle(ref, () => ({
|
||||||
onApply(formData);
|
resetFieldValue: (name, value) => {
|
||||||
closePanel();
|
if (value !== undefined) {
|
||||||
};
|
setValue(name, value);
|
||||||
|
} else {
|
||||||
|
reset({ ...getValues(), [name]: defaultContactFilter[name] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getValues, // optional: allows parent to read current form values
|
||||||
|
}));
|
||||||
|
|
||||||
const handleClose = () => {
|
useEffect(() => {
|
||||||
reset(defaultContactFilter);
|
if (data && setFilterdata) {
|
||||||
onApply(defaultContactFilter);
|
setFilterdata(data);
|
||||||
closePanel();
|
}
|
||||||
};
|
}, [data, setFilterdata]);
|
||||||
|
|
||||||
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
|
const closePanel = () => {
|
||||||
if (isError && isFetched)
|
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ContactFilterPanel;
|
const onSubmit = (formData) => {
|
||||||
|
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;
|
@ -1,21 +1,19 @@
|
|||||||
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 function to format contacts for CSV export
|
// Utility 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(", ") || "",
|
||||||
@ -34,8 +32,10 @@ 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,13 +46,19 @@ 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 onApply={setFilter} clearFilter={clearFilter} />
|
<ContactFilterPanel
|
||||||
|
ref={updatedRef}
|
||||||
|
onApply={setFilter}
|
||||||
|
clearFilter={clearFilter}
|
||||||
|
setFilterdata={setFilterdata}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -61,7 +67,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 🔹 Format contacts for export
|
// export data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.data && onExport) {
|
if (data?.data && onExport) {
|
||||||
onExport(formatExportData(data.data));
|
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 <div>{error.message}</div>;
|
if (isError) return <div>{error.message}</div>;
|
||||||
// if (isLoading) return gridView ? <CardViewContactSkeleton /> : <ListViewContactSkeleton />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row mt-5">
|
<div className="row mt-4">
|
||||||
|
{/* Chips Section */}
|
||||||
|
<div className="col-12 mb-2">
|
||||||
|
<ContactFilterChips
|
||||||
|
filters={filters}
|
||||||
|
filterData={filterData}
|
||||||
|
removeFilterChip={handleRemoveChip}
|
||||||
|
clearFilter={clearFilter}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid / List View */}
|
||||||
{gridView ? (
|
{gridView ? (
|
||||||
<>
|
<>
|
||||||
{isLoading && (
|
{isLoading && <Loader />}
|
||||||
<div>
|
|
||||||
<Loader />
|
{data?.data?.length === 0 && (
|
||||||
|
<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}
|
||||||
@ -129,4 +161,4 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ContactsPage;
|
export default ContactsPage;
|
@ -1,29 +1,38 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
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 { FormProvider, useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
defaultNotesFilter,
|
defaultNotesFilter,
|
||||||
notesFilter,
|
notesFilter,
|
||||||
} from "../../components/Directory/DirectorySchema";
|
} from "../../components/Directory/DirectorySchema";
|
||||||
import { useContactFilter, useNoteFilter } from "../../hooks/useDirectory";
|
import { 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 = ({ onApply, clearFilter }) => {
|
const NoteFilterPanel = forwardRef(({ onApply, clearFilter, setFilterdata }, ref) => {
|
||||||
const { data, isError, isLoading, error, isFetched, isFetching } =
|
const { data, isError, isLoading, error, isFetched, isFetching } = useNoteFilter();
|
||||||
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: defaultNotesFilter,
|
defaultValues: dynamicdefaultNotesFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
@ -31,47 +40,72 @@ const NoteFilterPanel = ({ onApply, clearFilter }) => {
|
|||||||
|
|
||||||
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">
|
||||||
<div className="row g-2">
|
{isLoading || isFetching ? (
|
||||||
<SelectMultiple
|
<ExpenseFilterSkeleton />
|
||||||
name="createdByIds"
|
) : isError && isFetched ? (
|
||||||
label="Created By :"
|
<div>Something went wrong here: {error?.message}</div>
|
||||||
options={data.createdBy}
|
) : (
|
||||||
labelKey="name"
|
<>
|
||||||
valueKey="id"
|
<div className="row g-2">
|
||||||
/>
|
<SelectMultiple
|
||||||
<SelectMultiple
|
name="createdByIds"
|
||||||
name="organizations"
|
label="Created By :"
|
||||||
label="Organization:"
|
options={data?.createdBy || []}
|
||||||
options={data.organizations}
|
labelKey="name"
|
||||||
labelKey={(item) => item.name}
|
valueKey="id"
|
||||||
valueKey="id"
|
/>
|
||||||
/>
|
<SelectMultiple
|
||||||
</div>
|
name="organizations"
|
||||||
<div className="d-flex justify-content-end py-3 gap-2">
|
label="Organization:"
|
||||||
<button
|
options={data?.organizations || []}
|
||||||
type="button"
|
labelKey={(item) => item.name}
|
||||||
className="btn btn-label-secondary btn-sm"
|
valueKey="id"
|
||||||
onClick={handleClose}
|
/>
|
||||||
>
|
</div>
|
||||||
Clear
|
|
||||||
</button>
|
<div className="d-flex justify-content-end py-3 gap-2">
|
||||||
<button type="submit" className="btn btn-primary btn-sm">
|
<button
|
||||||
Apply
|
type="button"
|
||||||
</button>
|
className="btn btn-label-secondary btn-sm"
|
||||||
</div>
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary btn-sm">
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default NoteFilterPanel;
|
export default NoteFilterPanel;
|
@ -1,5 +1,4 @@
|
|||||||
// NotesPage.jsx
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
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";
|
||||||
@ -9,11 +8,14 @@ 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,
|
||||||
@ -33,7 +35,12 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
|
|||||||
setShowTrigger(true);
|
setShowTrigger(true);
|
||||||
setOffcanvasContent(
|
setOffcanvasContent(
|
||||||
"Notes Filters",
|
"Notes Filters",
|
||||||
<NoteFilterPanel onApply={setFilter} clearFilter={clearFilter} />
|
<NoteFilterPanel
|
||||||
|
ref={updatedRef} //Call here
|
||||||
|
onApply={setFilter}
|
||||||
|
clearFilter={clearFilter}
|
||||||
|
setFilterdata={setFilterdata}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
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
|
// 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, "") : "", // strip HTML tags
|
Note: n.note ? n.note.replace(/<[^>]+>/g, "") : "",
|
||||||
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()
|
||||||
@ -59,7 +82,6 @@ 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));
|
||||||
@ -77,6 +99,12 @@ 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) => (
|
||||||
@ -96,7 +124,6 @@ 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" }}
|
||||||
@ -104,9 +131,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] && filters[k].length)
|
: Object.keys(filters).some((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>
|
||||||
)}
|
)}
|
||||||
@ -114,4 +141,4 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NotesPage;
|
export default NotesPage;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user