Adding Chips in Document, Directory and Expense Page.

This commit is contained in:
Kartik Sharma 2025-10-15 12:46:47 +05:30
parent 0b02531909
commit 57d65a5fe7
14 changed files with 925 additions and 280 deletions

View 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;

View File

@ -0,0 +1,79 @@
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

@ -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 (
<div className="row my-2">
<div className="col-12">
<div className="d-flex flex-wrap align-items-start gap-1">
{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 DocumentFilterChips;

View File

@ -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 <div>Loading...</div>;
if (isError)
return <div>Error: {error?.message || "Something went wrong!"}</div>;
@ -198,18 +225,18 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
<div className="d-flex justify-content-end py-3 gap-2">
<button
type="button"
className="btn btn-label-secondary btn-xs"
className="btn btn-label-secondary btn-sm"
onClick={onClear}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-xs">
<button type="submit" className="btn btn-primary btn-sm">
Apply
</button>
</div>
</form>
</FormProvider>
);
};
});
export default DocumentFilterPanel;

View File

@ -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",
<DocumentFilterPanel entityTypeId={DocumentEntity} onApply={setFilter} />
<DocumentFilterPanel entityTypeId={DocumentEntity} onApply={setFilter} setFilterdata={setFilterdata} ref={updatedRef} />
);
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 (
<DocumentContext.Provider value={contextValues}>
<div className="mt-5">
<div className="card page-min-h d-flex p-2">
<div className="mt-2">
<div className="card page-min-h d-flex p-5">
<DocumentFilterChips filters={filters} filterData={filterData} removeFilterChip={removeFilterChip} />
<div className="row align-items-center">
{/* Search */}
<div className="d-flex col-8 col-md-8 col-lg-4 mb-md-0 align-items-center">
<div className="d-flex flex-row gap-2 col-12 col-md-8 col-lg-4 mb-md-0 align-items-center mb-2">
<div className="d-flex">
{" "}
<input
@ -144,7 +169,7 @@ const Documents = ({ Document_Entity, Entity }) => {
<span className="switch-off"></span>
</span>
<span className="switch-label">
{isActive ? "Active Document" : "In-Active Document"}
{isActive ? "Active" : "In-Active"}
</span>
</label>
</div>
@ -231,4 +256,4 @@ const Documents = ({ Document_Entity, Entity }) => {
);
};
export default Documents;
export default Documents;

View File

@ -82,9 +82,9 @@ const DocumentsList = ({
if (isLoading || isFetching) return <DocumentTableSkeleton />;
if (isError)
return <div>Error: {error?.message || "Something went wrong"}</div>;
if (isInitialEmpty) return <div>No documents found yet.</div>;
if (isSearchEmpty) return <div>No results found for "{debouncedSearch}"</div>;
if (isFilterEmpty) return <div>No documents match your filter.</div>;
if (isInitialEmpty) return <div className="py-12 my-12">No documents found yet.</div>;
if (isSearchEmpty) return <div className="py-12 my-12">No results found for "{debouncedSearch}"</div>;
if (isFilterEmpty) return <div className="py-12 my-12">No documents match your filter.</div>;
const handleDelete = () => {
ActiveInActive(
@ -138,16 +138,14 @@ const DocumentsList = ({
lastName={e.uploadedBy?.lastName}
/>
<span className="text-truncate ms-1">
{`${e.uploadedBy?.firstName ?? ""} ${
e.uploadedBy?.lastName ?? ""
}`.trim() || "N/A"}
{`${e.uploadedBy?.firstName ?? ""} ${e.uploadedBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
),
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 = ({
}
></i>
{(isSelf || canModifyDocument) && (
{(isSelf || canModifyDocument) && (
<i
className="bx bx-edit text-secondary cursor-pointer"
onClick={() =>
@ -226,7 +224,7 @@ const DocumentsList = ({
></i>
)}
{(isSelf || canDeleteDocument) && (
{(isSelf || canDeleteDocument) && (
<i
className="bx bx-trash text-danger cursor-pointer"
onClick={() => {

View File

@ -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 (
<div className="row">
<div className="col-12">
<div className="d-flex flex-wrap align-items-start gap-1 text-start">
{filterChips.map((chip) => (
<div
key={chip.key}
className="d-flex align-items-center flex-wrap px-2 py-1 "
style={{ fontSize: "0.9rem", maxWidth: "100%" }}
>
{/* Chip Label */}
<span className="fw-semibold me-2">{chip.label}:</span>
{/* Chip Items */}
<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 ExpenseFilterChips;

View File

@ -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 <ExpenseFilterSkeleton />;
if (isError && isFetched)
return <div>Something went wrong Here- {error.message} </div>;
return (
<>
<FormProvider {...methods}>
@ -150,6 +215,13 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
labelKey={(item) => item.name}
valueKey="id"
/>
<SelectMultiple
name="ExpenseTypeIds"
label="Category :"
options={data.expensesType}
labelKey={(item) => item.name}
valueKey="id"
/>
<div className="mb-3">
<label className="form-label">Status :</label>
@ -221,6 +293,6 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
</FormProvider>
</>
);
};
});
export default ExpenseFilterPanel;
export default ExpenseFilterPanel;

View File

@ -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) => (
<div className="d-flex align-items-center cursor-pointer"
onClick={() => navigate(`/employee/${e.createdBy?.id}`)}>
<Avatar
size="xs"
classAvatar="m-0"
@ -139,11 +166,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
{
key: "amount",
label: "Amount",
getValue: (e) => (
<>
<i className="bx bx-rupee b-xs"></i> {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 <ExpenseTableSkeleton />;
if (isError) return <div>{error.message}</div>;
if (isInitialLoading && !data) return <ExpenseTableSkeleton />;
if (isError) return <div>{error?.message}</div>;
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 }) => {
/>
)}
<div className="card px-0 px-sm-4">
<div className="card page-min-h px-sm-4">
{/* Filter Chips */}
<ExpenseFilterChips
filters={filters}
filterData={filterData}
removeFilterChip={removeFilterChip}
groupBy={groupBy}
/>
<div
className="card-datatable table-responsive "
id="horizontal-example"
@ -226,18 +259,24 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
</thead>
<tbody>
{Object.keys(grouped).length > 0 ? (
Object.entries(grouped).map(([group, expenses]) => (
<React.Fragment key={group}>
Object.values(grouped).map(({ key, displayField, items }) => (
<React.Fragment key={key}>
<tr className="tr-group text-dark">
<td colSpan={8} className="text-start">
<strong>
{IsGroupedByDate
? formatUTCToLocalTime(group)
: group}
</strong>
<div className="d-flex align-items-center">
{" "}
<small className="fs-6 py-1">
{displayField} :{" "}
</small>{" "}
<small className="fs-6 ms-3">
{IsGroupedByDate
? formatUTCToLocalTime(key)
: key}
</small>
</div>
</td>
</tr>
{expenses.map((expense) => (
{items?.map((expense) => (
<tr key={expense.id}>
{expenseColumns.map(
(col) =>
@ -263,27 +302,61 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
})
}
></i>
{canEditExpense(expense) && (
<i
className="bx bx-edit text-secondary cursor-pointer"
onClick={() =>
setManageExpenseModal({
IsOpen: true,
expenseId: expense.id,
})
}
></i>
)}
{canDetetExpense(expense) &&
canEditExpense(expense) && (
<div className="dropdown z-2">
<button
type="button"
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i
className="bx bx-dots-vertical-rounded text-muted p-0"
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip-dark"
title="More Action"
></i>
</button>
<ul className="dropdown-menu dropdown-menu-end w-auto">
{canDetetExpense(expense) && (
<li
onClick={() =>
setManageExpenseModal({
IsOpen: true,
expenseId: expense.id,
})
}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-edit text-primary bx-xs me-2"></i>
<span className="align-left ">
Modify
</span>
</a>
</li>
)}
{canDetetExpense(expense) && (
<i
className="bx bx-trash text-danger cursor-pointer"
onClick={() => {
setIsDeleteModalOpen(true);
setDeletingId(expense.id);
}}
></i>
)}
{canDetetExpense(expense) && (
<li
onClick={() => {
setIsDeleteModalOpen(true);
setDeletingId(expense.id);
}}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-trash text-danger bx-xs me-2"></i>
<span className="align-left">
Delete
</span>
</a>
</li>
)}
</ul>
</div>
)}
</div>
</td>
</tr>
@ -292,13 +365,17 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
))
) : (
<tr>
<td colSpan={8} className="text-center py-4">
No Expense Found
<td colSpan={8} className="text-center border-0 ">
<div className="py-8">
<p>No Expense Found</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{data?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
@ -306,8 +383,6 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
onPageChange={paginate}
/>
)}
</div>
</div>
</div>
</>
);

View File

@ -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 <ExpenseFilterSkeleton />;
if (isError && isFetched)
return <div>Something went wrong Here- {error.message} </div>;
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>
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-sm"
onClick={handleClose}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-sm">
Apply
</button>
</div>
</form>
</FormProvider>
);
};
<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;
export default ContactFilterPanel;

View File

@ -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",
<ContactFilterPanel onApply={setFilter} clearFilter={clearFilter} />
<ContactFilterPanel
ref={updatedRef}
onApply={setFilter}
clearFilter={clearFilter}
setFilterdata={setFilterdata}
/>
);
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 <div>{error.message}</div>;
// if (isLoading) return gridView ? <CardViewContactSkeleton /> : <ListViewContactSkeleton />;
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 ? (
<>
{isLoading && <Loader />}
{data?.data?.length === 0 && (
<div className="py-4 text-center">
{searchText
? `No contact found for "${searchText}"`
: "No contacts found"}
</div>
)}
{data?.data?.map((contact) => (
<div key={contact.id} className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4">
<div
key={contact.id}
className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4"
>
<CardViewContact IsActive={showActive} contact={contact} />
</div>
))}
@ -95,6 +146,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
<div className="col-12">
<ListViewContact
data={data?.data}
isLoading={isLoading}
Pagination={
<Pagination
currentPage={currentPage}
@ -109,4 +161,4 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
);
};
export default ContactsPage;
export default ContactsPage;

View File

@ -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 <ExpenseFilterSkeleton />;
if (isError && isFetched)
return <div>Something went wrong Here- {error.message} </div>;
//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 (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
<div className="row g-2">
<SelectMultiple
name="createdByIds"
label="Created By :"
options={data.createdBy}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="organizations"
label="Organization:"
options={data.organizations}
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-sm"
onClick={handleClose}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-sm">
Apply
</button>
</div>
{isLoading || isFetching ? (
<ExpenseFilterSkeleton />
) : isError && isFetched ? (
<div>Something went wrong here: {error?.message}</div>
) : (
<>
<div className="row g-2">
<SelectMultiple
name="createdByIds"
label="Created By :"
options={data?.createdBy || []}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="organizations"
label="Organization:"
options={data?.organizations || []}
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-sm"
onClick={handleClose}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-sm">
Apply
</button>
</div>
</>
)}
</form>
</FormProvider>
);
};
});
export default NoteFilterPanel;
export default NoteFilterPanel;

View File

@ -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",
<NoteFilterPanel onApply={setFilter} clearFilter={clearFilter} />
<NoteFilterPanel
ref={updatedRef} //Call here
onApply={setFilter}
clearFilter={clearFilter}
setFilterdata={setFilterdata}
/>
);
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 (
<div className="d-flex flex-column text-start mt-3">
<NoteFilterChips
filters={filters}
filterData={filterData}
removeFilterChip={handleRemoveChip}
/>
{data?.data?.length > 0 ? (
<>
{data.data.map((noteItem) => (
@ -96,7 +124,6 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
</div>
</>
) : (
// Card for "No notes available"
<div
className="card text-center d-flex align-items-center justify-content-center"
style={{ height: "200px" }}
@ -104,9 +131,9 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
<p className="text-muted mb-0">
{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."}
</p>
</div>
)}
@ -114,4 +141,4 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
);
};
export default NotesPage;
export default NotesPage;

View File

@ -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",
<ExpenseFilterPanel
onApply={setFilter}
ref={filterPanelRef}
onApply={setFilters}
handleGroupBy={setGroupBy}
clearFilter={clearFilter}
setFilterdata={setFilterdata}
/>
);
}
@ -101,6 +101,8 @@ const ExpensePage = () => {
setViewExpense,
setManageExpenseModal,
setDocumentView,
filterData,
removeFilterChip
};
return (
@ -115,20 +117,18 @@ const ExpensePage = () => {
<div className="card my-3 px-sm-4 px-0">
<div className="card-body py-2 px-3">
<div className="row align-items-center">
<div className="col-6 ">
<div className="d-flex align-items-center">
<input
type="search"
className="form-control form-control-sm w-auto"
placeholder="Search Expense"
aria-describedby="search-label"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
</div>
<div className="col-6">
<input
type="search"
className="form-control form-control-sm w-auto"
placeholder="Search Expense"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
</div>
<div className="col-6 text-end mt-2 mt-sm-0">
<div className="col-6 text-end mt-2 mt-sm-0">
{IsCreatedAble && (
<button
className="btn btn-sm btn-primary"
@ -151,6 +151,8 @@ const ExpensePage = () => {
</div>
</div>
<ExpenseList
filters={filters}
groupBy={groupBy}
@ -161,7 +163,7 @@ const ExpensePage = () => {
<div className="card text-center py-1">
<i className="fa-solid fa-triangle-exclamation fs-5" />
<p>
Access Denied: You don't have permission to perform this action !
Access Denied: You don't have permission to perform this action!
</p>
</div>
)}