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/components/Documents/DocumentFilterChips.jsx b/src/components/Documents/DocumentFilterChips.jsx new file mode 100644 index 00000000..e6b56d7c --- /dev/null +++ b/src/components/Documents/DocumentFilterChips.jsx @@ -0,0 +1,94 @@ +import React, { useMemo } from "react"; +import moment from "moment"; + +const DocumentFilterChips = ({ filters, filterData, removeFilterChip }) => { + // Normalize structure: handle both "filterData.data" and plain "filterData" + 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 using normalized data + buildGroup(filters.uploadedByIds, data.uploadedBy || [], "Uploaded By", "uploadedByIds"); + buildGroup(filters.documentCategoryIds, data.documentCategory || [], "Category", "documentCategoryIds"); + buildGroup(filters.documentTypeIds, data.documentType || [], "Type", "documentTypeIds"); + buildGroup(filters.documentTagIds, data.documentTag || [], "Tags", "documentTagIds"); + + if (filters.statusIds?.length) { + const items = filters.statusIds.map((status) => ({ + id: status, + name: + status === true + ? "Verified" + : status === false + ? "Rejected" + : "Pending", + })); + chips.push({ key: "statusIds", label: "Status", items }); + } + + 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 DocumentFilterChips; diff --git a/src/components/Documents/DocumentFilterPanel.jsx b/src/components/Documents/DocumentFilterPanel.jsx index 581277e8..b266c986 100644 --- a/src/components/Documents/DocumentFilterPanel.jsx +++ b/src/components/Documents/DocumentFilterPanel.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useMemo, useImperativeHandle, forwardRef } from "react"; import { useDocumentFilterEntities } from "../../hooks/useDocument"; import { FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -9,18 +9,34 @@ import { import { DateRangePicker1 } from "../common/DateRangePicker"; import SelectMultiple from "../common/SelectMultiple"; import moment from "moment"; -import { useLocation } from "react-router-dom"; +import { useParams } from "react-router-dom"; -const DocumentFilterPanel = ({ entityTypeId, onApply }) => { +const DocumentFilterPanel = forwardRef( + ({ entityTypeId, onApply, setFilterdata }, ref) => { const [resetKey, setResetKey] = useState(0); - const location = useLocation(); + const { status } = useParams(); const { data, isError, isLoading, error } = useDocumentFilterEntities(entityTypeId); + //changes + + const dynamicDocumentFilterDefaultValues = useMemo(() => { + return { + ...DocumentFilterDefaultValues, + uploadedByIds: DocumentFilterDefaultValues.uploadedByIds || [], + documentCategoryIds: DocumentFilterDefaultValues.documentCategoryIds || [], + documentTypeIds: DocumentFilterDefaultValues.documentTypeIds || [], + documentTagIds: DocumentFilterDefaultValues.documentTagIds || [], + startDate: DocumentFilterDefaultValues.startDate, + endDate: DocumentFilterDefaultValues.endDate, + }; + + }, [status]); + const methods = useForm({ resolver: zodResolver(DocumentFilterSchema), - defaultValues: DocumentFilterDefaultValues, + defaultValues: dynamicDocumentFilterDefaultValues, }); const { handleSubmit, reset, setValue, watch } = methods; @@ -34,6 +50,24 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => { document.querySelector(".offcanvas.show .btn-close")?.click(); }; + useImperativeHandle(ref, () => ({ + resetFieldValue: (name, value) => { + if (value !== undefined) { + setValue(name, value); + } else { + reset({ ...methods.getValues(), [name]: DocumentFilterDefaultValues[name] }); + } + }, + getValues: methods.getValues, // optional, to read current filter state + })); + + //changes + useEffect(() => { + if (data && setFilterdata) { + setFilterdata(data); + } + }, [data, setFilterdata]); + const onSubmit = (values) => { onApply({ ...values, @@ -54,13 +88,6 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => { closePanel(); }; - // Close popup when navigating to another component - useEffect(() => { - return () => { - closePanel(); - }; - }, []); - if (isLoading) return
Loading...
; if (isError) return
Error: {error?.message || "Something went wrong!"}
; @@ -198,18 +225,18 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
-
); -}; +}); export default DocumentFilterPanel; diff --git a/src/components/Documents/Documents.jsx b/src/components/Documents/Documents.jsx index d724c98a..930ceb8c 100644 --- a/src/components/Documents/Documents.jsx +++ b/src/components/Documents/Documents.jsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useEffect, useState } from "react"; +import React, { createContext, useContext, useEffect, useRef, useState } from "react"; import GlobalModel from "../common/GlobalModel"; import NewDocument from "./ManageDocument"; import { DOCUMENTS_ENTITIES, UPLOAD_DOCUMENT } from "../../utils/constants"; @@ -17,6 +17,7 @@ import ViewDocument from "./ViewDocument"; import DocumentViewerModal from "./DocumentViewerModal"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useProfile } from "../../hooks/useProfile"; +import DocumentFilterChips from "./DocumentFilterChips"; // Context export const DocumentContext = createContext(); @@ -51,12 +52,14 @@ const Documents = ({ Document_Entity, Entity }) => { const [isSelf, setIsSelf] = useState(false); const [searchText, setSearchText] = useState(""); const [isActive, setIsActive] = useState(true); - const [filters, setFilter] = useState(); + const [filters, setFilter] = useState(DocumentFilterDefaultValues); const [isRefetching, setIsRefetching] = useState(false); const [refetchFn, setRefetchFn] = useState(null); const [DocumentEntity, setDocumentEntity] = useState(Document_Entity); const { employeeId } = useParams(); const [OpenDocument, setOpenDocument] = useState(false); + const [filterData, setFilterdata] = useState(DocumentFilterDefaultValues); + const updatedRef = useRef(); const [ManageDoc, setManageDoc] = useState({ document: null, isOpen: false, @@ -92,7 +95,7 @@ const Documents = ({ Document_Entity, Entity }) => { setShowTrigger(true); setOffcanvasContent( "Document Filters", - + ); return () => { @@ -115,13 +118,35 @@ const Documents = ({ Document_Entity, Entity }) => { setDocumentEntity(Document_Entity); } }, [Document_Entity]); + + + const removeFilterChip = (key, id) => { + const updatedFilters = { ...filters }; + if (Array.isArray(updatedFilters[key])) { + updatedFilters[key] = updatedFilters[key].filter((v) => v !== id); + updatedRef.current?.resetFieldValue(key,updatedFilters[key]); + } + else if (key === "dateRange") { + updatedFilters.startDate = null; + updatedFilters.endDate = null; + updatedRef.current?.resetFieldValue("startDate",null); + updatedRef.current?.resetFieldValue("endDate",null); + } + else { + updatedFilters[key] = null; + } + setFilter(updatedFilters); + return updatedFilters; + }; + return ( -
-
+
+
+
{/* Search */} -
+
{" "} { - {isActive ? "Active Document" : "In-Active Document"} + {isActive ? "Active" : "In-Active"}
@@ -231,4 +256,4 @@ const Documents = ({ Document_Entity, Entity }) => { ); }; -export default Documents; +export default Documents; \ No newline at end of file diff --git a/src/components/Documents/DocumentsList.jsx b/src/components/Documents/DocumentsList.jsx index b56060ea..2121c9dd 100644 --- a/src/components/Documents/DocumentsList.jsx +++ b/src/components/Documents/DocumentsList.jsx @@ -82,9 +82,9 @@ const DocumentsList = ({ if (isLoading || isFetching) return ; if (isError) return
Error: {error?.message || "Something went wrong"}
; - if (isInitialEmpty) return
No documents found yet.
; - if (isSearchEmpty) return
No results found for "{debouncedSearch}"
; - if (isFilterEmpty) return
No documents match your filter.
; + if (isInitialEmpty) return
No documents found yet.
; + if (isSearchEmpty) return
No results found for "{debouncedSearch}"
; + if (isFilterEmpty) return
No documents match your filter.
; const handleDelete = () => { ActiveInActive( @@ -138,16 +138,14 @@ const DocumentsList = ({ lastName={e.uploadedBy?.lastName} /> - {`${e.uploadedBy?.firstName ?? ""} ${ - e.uploadedBy?.lastName ?? "" - }`.trim() || "N/A"} + {`${e.uploadedBy?.firstName ?? ""} ${e.uploadedBy?.lastName ?? "" + }`.trim() || "N/A"}
), getValue: (e) => - `${e.uploadedBy?.firstName ?? ""} ${ - e.uploadedBy?.lastName ?? "" - }`.trim() || "N/A", + `${e.uploadedBy?.firstName ?? ""} ${e.uploadedBy?.lastName ?? "" + }`.trim() || "N/A", }, { key: "uploadedAt", @@ -217,7 +215,7 @@ const DocumentsList = ({ } > - {(isSelf || canModifyDocument) && ( + {(isSelf || canModifyDocument) && ( @@ -226,7 +224,7 @@ const DocumentsList = ({ > )} - {(isSelf || canDeleteDocument) && ( + {(isSelf || canDeleteDocument) && ( { diff --git a/src/components/Expenses/ExpenseFilterChips.jsx b/src/components/Expenses/ExpenseFilterChips.jsx new file mode 100644 index 00000000..66c8a376 --- /dev/null +++ b/src/components/Expenses/ExpenseFilterChips.jsx @@ -0,0 +1,86 @@ +import React, { useMemo } from "react"; + +const ExpenseFilterChips = ({ filters, filterData, removeFilterChip }) => { + // Build chips from filters + 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 }); + }; + + buildGroup(filters.projectIds, filterData.projects, "Project", "projectIds"); + buildGroup(filters.createdByIds, filterData.createdBy, "Submitted By", "createdByIds"); + buildGroup(filters.paidById, filterData.paidBy, "Paid By", "paidById"); + buildGroup(filters.statusIds, filterData.status, "Status", "statusIds"); + buildGroup(filters.ExpenseTypeIds, filterData.expensesType, "Category", "ExpenseTypeIds"); + + if (filters.startDate || filters.endDate) { + const start = filters.startDate + ? new Date(filters.startDate).toLocaleDateString() + : ""; + const end = filters.endDate + ? new Date(filters.endDate).toLocaleDateString() + : ""; + 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.label}: + + {/* Chip Items */} +
+ {chip.items.map((item) => ( + + {item.name} +
+
+ ))} +
+
+
+ ); +}; + +export default ExpenseFilterChips; + + diff --git a/src/components/Expenses/ExpenseFilterPanel.jsx b/src/components/Expenses/ExpenseFilterPanel.jsx index fffd746c..0ec19ce6 100644 --- a/src/components/Expenses/ExpenseFilterPanel.jsx +++ b/src/components/Expenses/ExpenseFilterPanel.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useMemo } from "react"; +import React, { forwardRef, useEffect, useImperativeHandle, useState, useMemo } from "react"; import { FormProvider, useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { defaultFilter, SearchSchema } from "./ExpenseSchema"; @@ -15,7 +15,7 @@ import { useExpenseFilter } from "../../hooks/useExpense"; import { ExpenseFilterSkeleton } from "./ExpenseSkeleton"; import { useLocation, useNavigate, useParams } from "react-router-dom"; -const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { +const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }, ref) => { const { status } = useParams(); const navigate = useNavigate(); const selectedProjectId = useSelector( @@ -31,17 +31,31 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { { id: "submittedBy", name: "Submitted By" }, { id: "project", name: "Project" }, { id: "paymentMode", name: "Payment Mode" }, - { id: "expensesType", name: "Expense Type" }, + { id: "expensesType", name: "Expense Category" }, { id: "createdAt", name: "Submitted Date" }, ].sort((a, b) => a.name.localeCompare(b.name)); }, []); - const [selectedGroup, setSelectedGroup] = useState(groupByList[0]); + const [selectedGroup, setSelectedGroup] = useState(groupByList[6]); const [resetKey, setResetKey] = useState(0); + const dynamicDefaultFilter = useMemo(() => { + return { + ...defaultFilter, + statusIds: status ? [status] : defaultFilter.statusIds || [], + projectIds: selectedProjectId ? [selectedProjectId] : [], + createdByIds: defaultFilter.createdByIds || [], + paidById: defaultFilter.paidById || [], + ExpenseTypeIds: defaultFilter.ExpenseTypeIds || [], + isTransactionDate: defaultFilter.isTransactionDate ?? true, + startDate: defaultFilter.startDate, + endDate: defaultFilter.endDate, + }; + }, [status,selectedProjectId]); + const methods = useForm({ resolver: zodResolver(SearchSchema), - defaultValues: defaultFilter, + defaultValues: dynamicDefaultFilter, }); const { control, handleSubmit, reset, setValue, watch } = methods; @@ -51,11 +65,30 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { document.querySelector(".offcanvas.show .btn-close")?.click(); }; + // Change here + useEffect(() => { + if (data && setFilterdata) { + setFilterdata(data); + } + }, [data, setFilterdata]); + const handleGroupChange = (e) => { const group = groupByList.find((g) => g.id === e.target.value); if (group) setSelectedGroup(group); }; + useImperativeHandle(ref, () => ({ + resetFieldValue: (name, value) => { + // Reset specific field + if (value !== undefined) { + setValue(name, value); + } else { + reset({ ...methods.getValues(), [name]: defaultFilter[name] }); + } + }, + getValues: methods.getValues, // optional, to read current filter state + })); + const onSubmit = (formData) => { onApply({ ...formData, @@ -78,19 +111,51 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { } }; - - - - // Close popup when navigating to another component const location = useLocation(); useEffect(() => { closePanel(); }, [location]); + const [appliedStatusId, setAppliedStatusId] = useState(null); + + useEffect(() => { + if (!status) return; + + if (status !== appliedStatusId && data) { + const filterWithStatus = { + ...dynamicDefaultFilter, + projectIds: selectedProjectId ? [selectedProjectId] : [], // βœ… include project ID + startDate: dynamicDefaultFilter.startDate + ? moment.utc(dynamicDefaultFilter.startDate, "DD-MM-YYYY").toISOString() + : undefined, + endDate: dynamicDefaultFilter.endDate + ? moment.utc(dynamicDefaultFilter.endDate, "DD-MM-YYYY").toISOString() + : undefined, + }; + + onApply(filterWithStatus); + handleGroupBy(selectedGroup.id); + + setAppliedStatusId(status); + } + }, [ + status, + data, + dynamicDefaultFilter, + onApply, + handleGroupBy, + selectedGroup.id, + appliedStatusId, + selectedProjectId, // βœ… added dependency + ]); + + if (isLoading || isFetching) return ; if (isError && isFetched) return
Something went wrong Here- {error.message}
; + + return ( <> @@ -150,6 +215,13 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { labelKey={(item) => item.name} valueKey="id" /> + item.name} + valueKey="id" + />
@@ -221,6 +293,6 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { ); -}; +}); -export default ExpenseFilterPanel; +export default ExpenseFilterPanel; \ No newline at end of file diff --git a/src/components/Expenses/ExpenseList.jsx b/src/components/Expenses/ExpenseList.jsx index 8d96e121..7cccb5d3 100644 --- a/src/components/Expenses/ExpenseList.jsx +++ b/src/components/Expenses/ExpenseList.jsx @@ -10,23 +10,30 @@ import { EXPENSE_REJECTEDBY, ITEMS_PER_PAGE, } from "../../utils/constants"; -import { getColorNameFromHex, useDebounce } from "../../utils/appUtils"; +import { + formatCurrency, + getColorNameFromHex, + useDebounce, +} from "../../utils/appUtils"; import { ExpenseTableSkeleton } from "./ExpenseSkeleton"; import ConfirmModal from "../common/ConfirmModal"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useSelector } from "react-redux"; +import ExpenseFilterChips from "./ExpenseFilterChips"; +import { defaultFilter } from "./ExpenseSchema"; import { useNavigate } from "react-router-dom"; const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { const [deletingId, setDeletingId] = useState(null); const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const { setViewExpense, setManageExpenseModal } = useExpenseContext(); + const { setViewExpense, setManageExpenseModal, filterData, removeFilterChip } = useExpenseContext(); const IsExpenseEditable = useHasUserPermission(); const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE); const [currentPage, setCurrentPage] = useState(1); const debouncedSearch = useDebounce(searchText, 500); const navigate = useNavigate(); + const { mutate: DeleteExpense, isPending } = useDeleteExpense(); const { data, isLoading, isError, isInitialLoading, error } = useExpenseList( ITEMS_PER_PAGE, @@ -61,39 +68,60 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { const groupByField = (items, field) => { return items.reduce((acc, item) => { let key; + let displayField; + switch (field) { case "transactionDate": - key = item.transactionDate?.split("T")[0]; + key = item?.transactionDate?.split("T")[0]; + displayField = "Transaction Date"; break; case "status": - key = item.status?.displayName || "Unknown"; + key = item?.status?.displayName || "Unknown"; + displayField = "Status"; break; case "submittedBy": - key = `${item.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? "" + key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? "" }`.trim(); + displayField = "Submitted By"; break; case "project": - key = item.project?.name || "Unknown Project"; + key = item?.project?.name || "Unknown Project"; + displayField = "Project"; break; case "paymentMode": - key = item.paymentMode?.name || "Unknown Mode"; + key = item?.paymentMode?.name || "Unknown Mode"; + displayField = "Payment Mode"; break; case "expensesType": - key = item.expensesType?.name || "Unknown Type"; + key = item?.expensesType?.name || "Unknown Type"; + displayField = "Expense Category"; break; case "createdAt": - key = item.createdAt?.split("T")[0] || "Unknown Type"; + key = item?.createdAt?.split("T")[0] || "Unknown Date"; + displayField = "Created Date"; break; default: key = "Others"; + displayField = "Others"; } - if (!acc[key]) acc[key] = []; - acc[key].push(item); + + const groupKey = `${field}_${key}`; // unique key for object property + if (!acc[groupKey]) { + acc[groupKey] = { key, displayField, items: [] }; + } + + acc[groupKey].items.push(item); return acc; }, {}); }; const expenseColumns = [ + { + key: "expenseUId", + label: "Expense Id", + getValue: (e) => e.expenseUId || "N/A", + align: "text-start mx-2", + }, { key: "expensesType", label: "Expense Type", @@ -116,7 +144,6 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { customRender: (e) => (
navigate(`/employee/${e.createdBy?.id}`)}> - { { key: "amount", label: "Amount", - getValue: (e) => ( - <> - {e?.amount} - - ), + getValue: (e) => <>{formatCurrency(e?.amount)}, isAlwaysVisible: true, align: "text-end", }, @@ -162,27 +185,30 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { }, ]; - if (isInitialLoading) return ; - if (isError) return
{error.message}
; + if (isInitialLoading && !data) return ; + if (isError) return
{error?.message}
; const grouped = groupBy ? groupByField(data?.data ?? [], groupBy) : { All: data?.data ?? [] }; - const IsGroupedByDate = ["transactionDate", "createdAt"].includes(groupBy); + const IsGroupedByDate = [ + { key: "transactionDate", displayField: "Transaction Date" }, + { key: "createdAt", displayField: "created Date" }, + ]?.includes(groupBy); + const canEditExpense = (expense) => { return ( - (expense.status.id === EXPENSE_DRAFT || - EXPENSE_REJECTEDBY.includes(expense.status.id)) && - expense.createdBy?.id === SelfId + (expense?.status?.id === EXPENSE_DRAFT || + EXPENSE_REJECTEDBY.includes(expense?.status?.id)) && + expense?.createdBy?.id === SelfId ); }; const canDetetExpense = (expense) => { return ( - expense.status.id === EXPENSE_DRAFT && expense.createdBy.id === SelfId + expense?.status?.id === EXPENSE_DRAFT && expense?.createdBy?.id === SelfId ); }; - return ( <> {IsDeleteModalOpen && ( @@ -198,7 +224,14 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { /> )} -
+
+ {/* Filter Chips */} +
{ {Object.keys(grouped).length > 0 ? ( - Object.entries(grouped).map(([group, expenses]) => ( - + Object.values(grouped).map(({ key, displayField, items }) => ( + - - {IsGroupedByDate - ? formatUTCToLocalTime(group) - : group} - +
+ {" "} + + {displayField} :{" "} + {" "} + + {IsGroupedByDate + ? formatUTCToLocalTime(key) + : key} + +
- {expenses.map((expense) => ( + {items?.map((expense) => ( {expenseColumns.map( (col) => @@ -263,27 +302,61 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { }) } >
- {canEditExpense(expense) && ( - - setManageExpenseModal({ - IsOpen: true, - expenseId: expense.id, - }) - } - > - )} + {canDetetExpense(expense) && + canEditExpense(expense) && ( +
+ +
    + {canDetetExpense(expense) && ( +
  • + setManageExpenseModal({ + IsOpen: true, + expenseId: expense.id, + }) + } + > + + + + Modify + + +
  • + )} - {canDetetExpense(expense) && ( - { - setIsDeleteModalOpen(true); - setDeletingId(expense.id); - }} - > - )} + {canDetetExpense(expense) && ( +
  • { + setIsDeleteModalOpen(true); + setDeletingId(expense.id); + }} + > + + + + Delete + + +
  • + )} +
+
+ )}
@@ -292,13 +365,17 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { )) ) : ( - - No Expense Found + +
+

No Expense Found

+
)} +
+
{data?.data?.length > 0 && ( { onPageChange={paginate} /> )} -
-
); diff --git a/src/pages/Directory/ContactFilterPanel.jsx b/src/pages/Directory/ContactFilterPanel.jsx index 063a6268..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, { useEffect } from "react"; +import React, { + useEffect, + useImperativeHandle, + forwardRef, + useMemo, +} from "react"; import { FormProvider, useForm } from "react-hook-form"; import { contactsFilter, @@ -8,83 +13,101 @@ import { import { useContactFilter } from "../../hooks/useDirectory"; import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton"; import SelectMultiple from "../../components/common/SelectMultiple"; -import { useLocation } from "react-router-dom"; +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 location = useLocation(); + const dynamicdefaultContactFilter = useMemo(() => { + return { + ...defaultContactFilter, + bucketIds: defaultContactFilter.bucketIds || [], + categoryIds: defaultContactFilter.categoryIds || [], + }; + }, [status]); - const methods = useForm({ - resolver: zodResolver(contactsFilter), - defaultValues: defaultContactFilter, - }); + const methods = useForm({ + resolver: zodResolver(contactsFilter), + defaultValues: dynamicdefaultContactFilter, + }); - const closePanel = () => { - document.querySelector(".offcanvas.show .btn-close")?.click(); - }; + const { handleSubmit, reset, setValue, getValues } = methods; - const { register, handleSubmit, reset, watch } = methods; + 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 onSubmit = (formData) => { - onApply(formData); - closePanel(); - }; + useEffect(() => { + if (data && setFilterdata) { + setFilterdata(data); + } + }, [data, setFilterdata]); - const handleClose = () => { - reset(defaultContactFilter); - onApply(defaultContactFilter); - closePanel(); - }; + const closePanel = () => { + document.querySelector(".offcanvas.show .btn-close")?.click(); + }; - - useEffect(() => { - return () => { + const onSubmit = (formData) => { + onApply(formData); closePanel(); }; - }, []); - if (isLoading || isFetching) return ; - if (isError && isFetched) - return
Something went wrong Here- {error.message}
; + 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" + /> +
- return ( - - -
- - item.name} - valueKey="id" - /> -
-
- - -
- -
- ); -}; +
+ + +
+ +
+ ); + } +); -export default ContactFilterPanel; +export default ContactFilterPanel; \ No newline at end of file diff --git a/src/pages/Directory/ContactsPage.jsx b/src/pages/Directory/ContactsPage.jsx index 670b3ef9..195ebb96 100644 --- a/src/pages/Directory/ContactsPage.jsx +++ b/src/pages/Directory/ContactsPage.jsx @@ -1,26 +1,30 @@ -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(", ") || "", - Phone: contact.contactPhones?.map(p => p.phoneNumber).join(", ") || "", - Created: contact.createdAt ? new Date(contact.createdAt).toLocaleString() : "", + return contacts.map((contact) => ({ + Email: contact.contactEmails?.map((e) => e.emailAddress).join(", ") || "", + Phone: contact.contactPhones?.map((p) => p.phoneNumber).join(", ") || "", + Created: contact.createdAt + ? new Date(contact.createdAt).toLocaleString() + : "", Location: contact.address || "", Organization: contact.organization || "", Category: contact.contactCategory?.name || "", - Tags: contact.tags?.map(t => t.name).join(", ") || "", + Tags: contact.tags?.map((t) => t.name).join(", ") || "", Buckets: contact.bucketIds?.join(", ") || "", })); }; @@ -28,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, @@ -40,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 () => { @@ -55,7 +67,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => { }; }, []); - // πŸ”Ή Format contacts for export + // export data useEffect(() => { if (data?.data && onExport) { onExport(formatExportData(data.data)); @@ -68,15 +80,54 @@ 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 && } + + {data?.data?.length === 0 && ( +
+ {searchText + ? `No contact found for "${searchText}"` + : "No contacts found"} +
+ )} + {data?.data?.map((contact) => ( -
+
))} @@ -95,6 +146,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
{ ); }; -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 0b21fc46..6cc5bdbc 100644 --- a/src/pages/Directory/NoteFilterPanel.jsx +++ b/src/pages/Directory/NoteFilterPanel.jsx @@ -1,36 +1,38 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useEffect } 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"; -import { useLocation } from "react-router-dom"; -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(); - useEffect(() => { - return () => { - closePanel(); + + //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(); @@ -42,43 +44,70 @@ const NoteFilterPanel = ({ onApply, clearFilter }) => { closePanel(); }; - if (isLoading || isFetching) return ; - if (isError && isFetched) - return
Something went wrong Here- {error.message}
; + //Add this for Filter chip remover + useImperativeHandle(ref, () => ({ + resetFieldValue: (name, value) => { + setTimeout(() => { + if (value !== undefined) { + setValue(name, value); + } else { + reset({ ...getValues(), [name]: defaultNotesFilter[name] }); + } + }, 0); + }, + getValues, + })); + + + useEffect(() => { + if (data && setFilterdata) { + setFilterdata(data); + } + }, [data, setFilterdata]); + 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 58790562..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 }) => { }; }, []); - // πŸ”Ή Format data for export + 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; diff --git a/src/pages/Expense/ExpensePage.jsx b/src/pages/Expense/ExpensePage.jsx index 5d2ef806..f5c3ea5d 100644 --- a/src/pages/Expense/ExpensePage.jsx +++ b/src/pages/Expense/ExpensePage.jsx @@ -1,17 +1,16 @@ -import React, { createContext, useContext, useState, useEffect } from "react"; -import { useForm } from "react-hook-form"; +import React, { createContext, useContext, useState, useEffect, useRef } from "react"; +import { useForm, useFormContext } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useSelector } from "react-redux"; -import ExpenseList from "../../components/Expenses/ExpenseList"; -import ViewExpense from "../../components/Expenses/ViewExpense"; import Breadcrumb from "../../components/common/Breadcrumb"; import GlobalModel from "../../components/common/GlobalModel"; -import PreviewDocument from "../../components/Expenses/PreviewDocument"; +import ExpenseList from "../../components/Expenses/ExpenseList"; +import ViewExpense from "../../components/Expenses/ViewExpense"; import ManageExpense from "../../components/Expenses/ManageExpense"; import ExpenseFilterPanel from "../../components/Expenses/ExpenseFilterPanel"; +import ExpenseFilterChips from "../../components/Expenses/ExpenseFilterChips"; -// Context & Hooks import { useFab } from "../../Context/FabContext"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { @@ -20,11 +19,8 @@ import { VIEW_SELF_EXPENSE, } from "../../utils/constants"; -// Schema & Defaults -import { - defaultFilter, - SearchSchema, -} from "../../components/Expenses/ExpenseSchema"; +import { defaultFilter, SearchSchema } from "../../components/Expenses/ExpenseSchema"; +import PreviewDocument from "../../components/Expenses/PreviewDocument"; // Context export const ExpenseContext = createContext(); @@ -41,10 +37,10 @@ const ExpensePage = () => { (store) => store.localVariables.projectId ); - const [filters, setFilter] = useState(); + const [filters, setFilters] = useState(defaultFilter); const [groupBy, setGroupBy] = useState("transactionDate"); const [searchText, setSearchText] = useState(""); - + const filterPanelRef = useRef(); const [ManageExpenseModal, setManageExpenseModal] = useState({ IsOpen: null, expenseId: null, @@ -63,19 +59,22 @@ const ExpensePage = () => { const IsCreatedAble = useHasUserPermission(CREATE_EXEPENSE); const IsViewAll = useHasUserPermission(VIEW_ALL_EXPNESE); const IsViewSelf = useHasUserPermission(VIEW_SELF_EXPENSE); - const { setOffcanvasContent, setShowTrigger } = useFab(); - - const methods = useForm({ - resolver: zodResolver(SearchSchema), - defaultValues: defaultFilter, - }); - - const { reset } = methods; - - const clearFilter = () => { - setFilter(defaultFilter); - reset(); + const [filterData, setFilterdata] = useState(defaultFilter); + const removeFilterChip = (key, id) => { + setFilters((prev) => { + const updated = { ...prev }; + if (Array.isArray(updated[key])) { + updated[key] = updated[key].filter((v) => v !== id); + filterPanelRef.current?.resetFieldValue(key, updated[key]); + } else if (key === "dateRange") { + updated.startDate = null; + updated.endDate = null; + filterPanelRef.current?.resetFieldValue("startDate", null); + filterPanelRef.current?.resetFieldValue("endDate", null); + } + return updated; + }); }; useEffect(() => { @@ -84,9 +83,10 @@ const ExpensePage = () => { setOffcanvasContent( "Expense Filters", ); } @@ -101,6 +101,8 @@ const ExpensePage = () => { setViewExpense, setManageExpenseModal, setDocumentView, + filterData, + removeFilterChip }; return ( @@ -115,20 +117,18 @@ const ExpensePage = () => {
-
-
- setSearchText(e.target.value)} - /> -
+
+ setSearchText(e.target.value)} + />
-
+
+ {IsCreatedAble && (
+ + {

- Access Denied: You don't have permission to perform this action ! + Access Denied: You don't have permission to perform this action!

)}