adedd fillter sidepanel and handle filter object on api level

This commit is contained in:
pramod mahajan 2025-08-30 17:31:36 +05:30
parent cec16ded3e
commit 518928e439
9 changed files with 333 additions and 45 deletions

View File

@ -1,9 +1,183 @@
import React from 'react'
import React, { useState } from "react";
import { useDocumentFilterEntities } from "../../hooks/useDocument";
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
DocumentFilterDefaultValues,
DocumentFilterSchema,
} from "./DocumentSchema";
import { DateRangePicker1 } from "../common/DateRangePicker";
import SelectMultiple from "../common/SelectMultiple";
import moment from "moment";
const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
const [resetKey, setResetKey] = useState(0);
const { data, isError, isLoading, error } =
useDocumentFilterEntities(entityTypeId);
const methods = useForm({
resolver: zodResolver(DocumentFilterSchema),
defaultValues: DocumentFilterDefaultValues,
});
const { handleSubmit, reset, setValue, watch } = methods;
// Watch values from form
const isUploadedAt = watch("isUploadedAt");
const isVerified = watch("isVerified");
// Close the offcanvas (bootstrap specific)
const closePanel = () => {
document.querySelector(".offcanvas.show .btn-close")?.click();
};
const onSubmit = (values) => {
onApply({
...values,
startDate: values.startDate
? moment.utc(values.startDate, "DD-MM-YYYY").toISOString()
: null,
endDate: values.endDate
? moment.utc(values.endDate, "DD-MM-YYYY").toISOString()
: null,
});
closePanel();
};
const onClear = () => {
reset(DocumentFilterDefaultValues);
setResetKey((prev) => prev + 1);
onApply(DocumentFilterDefaultValues);
closePanel();
};
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error?.message || "Something went wrong!"}</div>;
const { uploadedBy = [], documentCategory = [], documentType = [], documentTag = [] } =
data?.data || {};
const DocumentFilterPanel = () => {
return (
<h1>filter</h1>
)
}
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
{/* Date Range Section */}
<div className="mb-2">
<div className="text-start d-flex align-items-center my-1">
<label className="form-label me-2 my-0">Choose Date:</label>
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
isUploadedAt ? "active btn-secondary text-white" : ""
}`}
onClick={() => setValue("isUploadedAt", true)}
>
Uploaded On
</button>
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
!isUploadedAt ? "active btn-secondary text-white" : ""
}`}
onClick={() => setValue("isUploadedAt", false)}
>
Updated On
</button>
</div>
</div>
export default DocumentFilterPanel
<DateRangePicker1
placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="startDate"
endField="endDate"
defaultRange={false}
resetSignal={resetKey}
/>
</div>
{/* Dropdown Filters */}
<div className="row g-2 text-start">
<SelectMultiple
name="uploadedByIds"
label="Uploaded By:"
options={uploadedBy}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="documentCategoryIds"
label="Document Category:"
options={documentCategory}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="documentTypeIds"
label="Document Type:"
options={documentType}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="documentTagIds"
label="Tags:"
options={documentTag}
labelKey="name"
valueKey="id"
/>
</div>
{/* Status Filter */}
<div className="text-start d-flex align-items-center my-2">
<label className="form-label me-2 my-0">Choose Status:</label>
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
isVerified === null ? "active btn-primary text-white" : "text-primary"
}`}
onClick={() => setValue("isVerified", null)}
>
All
</button>
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
isVerified === true ? "active btn-success text-white" : "text-success"
}`}
onClick={() => setValue("isVerified", true)}
>
Verified
</button>
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
isVerified === false ? "active btn-danger text-white" : "text-danger"
}`}
onClick={() => setValue("isVerified", false)}
>
Rejected
</button>
</div>
</div>
{/* Footer Buttons */}
<div className="d-flex justify-content-end py-3 gap-2">
<button
type="button"
className="btn btn-secondary btn-xs"
onClick={onClear}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-xs">
Apply
</button>
</div>
</form>
</FormProvider>
);
};
export default DocumentFilterPanel;

View File

@ -71,9 +71,7 @@ export const DocumentPayloadSchema = (docConfig = {}) => {
(val) => val !== null,
{ message: "Attachment is required" }
),
tags: z.array(TagSchema).optional().default([]),
});
};
@ -95,3 +93,28 @@ export const defaultDocumentValues = {
},
tags: [],
};
//--------------------Filter-------------------------
export const DocumentFilterSchema = z.object({
uploadedByIds: z.array(z.string()).default([]),
documentCategoryIds: z.array(z.string()).default([]),
documentTypeIds: z.array(z.string()).default([]),
documentTagIds: z.array(z.string()).default([]),
isUploadedAt: z.boolean().default(true),
isVerified: z.boolean().nullable().optional(),
startDate: z.string().nullable().optional(),
endDate: z.string().nullable().optional(),
});
export const DocumentFilterDefaultValues = {
uploadedByIds: [],
documentCategoryIds: [],
documentTypeIds: [],
documentTagIds: [],
isUploadedAt: true,
isVerified: null,
startDate: null,
endDate: null,
};

View File

@ -19,7 +19,7 @@ const SkeletonCell = ({
export const DocumentTableSkeleton = ({ rows = 5 }) => {
return (
<div className="card px-2">
<table className="card-body table border-top dataTable no-footer dtr-column text-nowrap">
<thead>
<tr>
@ -65,6 +65,6 @@ export const DocumentTableSkeleton = ({ rows = 5 }) => {
))}
</tbody>
</table>
</div>
);
};

View File

@ -1,29 +1,53 @@
import React, { useEffect, useState } from "react";
import GlobalModel from "../common/GlobalModel";
import NewDocument from "./NewDocument";
import NewDocument from "./ManageDocument";
import { DOCUMENTS_ENTITIES } from "../../utils/constants";
import { useParams } from "react-router-dom";
import DocumentsList from "./DocumentsList";
import DocumentFilterPanel from "./DocumentFilterPanel";
import { useFab } from "../../Context/FabContext";
import { useForm } from "react-hook-form";
import {
DocumentFilterDefaultValues,
DocumentFilterSchema,
} from "./DocumentSchema";
import { zodResolver } from "@hookform/resolvers/zod";
import ManageDocument from "./ManageDocument";
const Documents = ({ Document_Entity, Entity }) => {
const [searchText, setSearchText] = useState("");
const [filters, setFilter] = useState();
const [isRefetching, setIsRefetching] = useState(false);
const [refetchFn, setRefetchFn] = useState(null);
const { employeeId } = useParams();
const [isUpload, setUpload] = useState(false);
const { setOffcanvasContent, setShowTrigger } = useFab();
const methods = useForm({
resolver: zodResolver(DocumentFilterSchema),
defaultValues: DocumentFilterDefaultValues,
});
const { reset } = methods;
const clearFilter = () => {
setFilter(DocumentFilterDefaultValues);
reset();
};
useEffect(() => {
setShowTrigger(true);
setOffcanvasContent("Document Filters", <DocumentFilterPanel />);
setOffcanvasContent(
"Document Filters",
<DocumentFilterPanel entityTypeId={Document_Entity} onApply={setFilter} />
);
return () => {
setShowTrigger(false);
setOffcanvasContent("", null);
};
}, []);
return (
<div className="mt-5">
<div className="card d-flex p-2">
@ -41,12 +65,21 @@ const Documents = ({ Document_Entity, Entity }) => {
{/* Actions */}
<div className="col-6 col-md-6 col-lg-9 text-end">
<span className="text-tiny text-muted p-1 border-0 bg-none lead mx-3 cursor-pointer" disabled={isRefetching}
onClick={() => refetchFn && refetchFn()}>
<span
className="text-tiny text-muted p-1 border-0 bg-none lead mx-3 cursor-pointer"
disabled={isRefetching}
onClick={() => {
setSearchText("");
setFilter(DocumentFilterDefaultValues);
refetchFn && refetchFn();
}}
>
Refresh
<i className={`bx bx-refresh ms-1 ${
isRefetching ? "bx-spin" : ""
}`}></i>
<i
className={`bx bx-refresh ms-1 ${
isRefetching ? "bx-spin" : ""
}`}
></i>
</span>
<button
@ -62,6 +95,7 @@ const Documents = ({ Document_Entity, Entity }) => {
<DocumentsList
Document_Entity={Document_Entity}
Entity={Entity}
filters={filters}
searchText={searchText}
setIsRefetching={setIsRefetching}
setRefetchFn={setRefetchFn}
@ -70,7 +104,7 @@ const Documents = ({ Document_Entity, Entity }) => {
{isUpload && (
<GlobalModel isOpen={isUpload} closeModal={() => setUpload(false)}>
<NewDocument
<ManageDocument
closeModal={() => setUpload(false)}
Document_Entity={Document_Entity}
Entity={Entity}

View File

@ -24,28 +24,47 @@ export const getDocuementsStatus = (status) => {
);
}
};
const DocumentsList = ({ Document_Entity, Entity,searchText ,setIsRefetching,
setRefetchFn,}) => {
const debouncedSearch = useDebounce(searchText, 500);
const { data, isError, isLoading, error,refetch,isFetching } = useDocumentListByEntityId(
Document_Entity,
Entity,
ITEMS_PER_PAGE,
1,{},debouncedSearch
);
// Pass the refetch function to parent when component mounts
useEffect(() => {
setRefetchFn(() => refetch);
}, [setRefetchFn, refetch]);
// Sync fetching status with parent
useEffect(() => {
setIsRefetching(isFetching);
}, [isFetching, setIsRefetching]);
const DocumentsList = ({
Document_Entity,
Entity,
filters,
searchText,
setIsRefetching,
setRefetchFn,
}) => {
const debouncedSearch = useDebounce(searchText, 500);
const { data, isError, isLoading, error, refetch, isFetching } =
useDocumentListByEntityId(
Document_Entity,
Entity,
ITEMS_PER_PAGE,
1,
filters,
debouncedSearch
);
if (isLoading) return <DocumentTableSkeleton/>;
if (isError) return <p>Error: {error?.message || "Something went wrong"}</p>;
// Pass the refetch function to parent when component mounts
useEffect(() => {
setRefetchFn(() => refetch);
}, [setRefetchFn, refetch]);
// Sync fetching status with parent
useEffect(() => {
setIsRefetching(isFetching);
}, [isFetching, setIsRefetching]);
// check no data scenarios
const noData = !isLoading && !isError && data?.length === 0;
const isSearchEmpty = noData && !!debouncedSearch;
const isFilterEmpty = noData && false;
const isInitialEmpty = noData && !debouncedSearch;
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>;
const DocumentColumns = [
{

View File

@ -19,7 +19,7 @@ const toBase64 = (file) =>
reader.onerror = (err) => reject(err);
});
const NewDocument = ({closeModal,Document_Entity,Entity}) => {
const ManageDocument = ({closeModal,Document_Entity,Entity}) => {
const [selectedType, setSelectedType] = useState(null);
const [selectedCategory, setSelectedCategory] = useState(null);
@ -319,4 +319,4 @@ const NewDocument = ({closeModal,Document_Entity,Entity}) => {
);
};
export default NewDocument;
export default ManageDocument;

View File

@ -71,7 +71,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
closePanel();
};
// Close popup when navigating to another component
// Close popup when navigating to another component
const location = useLocation();
useEffect(() => {
closePanel();

View File

@ -10,9 +10,14 @@ const cleanFilter = (filter) => {
if (Array.isArray(cleaned[key]) && cleaned[key].length === 0) {
delete cleaned[key];
}
});
["startDate", "endDate", "isVerified"].forEach((key) => {
if (cleaned[key] === null) {
delete cleaned[key];
}
});
return cleaned;
};
export const useDocumentListByEntityId=(entityTypeId,entityId,pageSize, pageNumber, filter,searchString="")=>{
@ -27,6 +32,20 @@ export const useDocumentListByEntityId=(entityTypeId,entityId,pageSize, pageNumb
})
}
export const useDocumentFilterEntities =(entityTypeId)=>{
return useQuery({
queryKey:["DFilter",entityTypeId],
queryFn:async()=> await DocumentRepository.getFilterEntities(entityTypeId)
})
}
export const useDocumentDetails =(documentId)=>{
return useQuery({
queryKey:["Document",documentId],
queryFn:async()=> await DocumentRepository.getDocumentById(documentId)
})
}
//----------------------- MUTATION -------------------------
export const useUploadDocument =(onSuccessCallBack)=>{
@ -45,4 +64,21 @@ export const useUploadDocument =(onSuccessCallBack)=>{
);
},
}))
}
export const useUpdateDocument =(onSuccessCallBack)=>{
const queryClient = useQueryClient()
return useMutation(({
mutationFn:async(documentId,DocumentPayload)=>DocumentRepository.UpdateDocument(documentId,DocumentPayload),
onSuccess:(data,variables)=>{
queryClient.invalidateQueries({queryKey:["DocumentList"]});
if(onSuccessCallBack) onSuccessCallBack()
},
onError: (error) => {
console.log(error)
showToast(
error.response.data.message || "Something went wrong please try again !",
"error"
);
},
}))
}

View File

@ -8,6 +8,8 @@ export const DocumentRepository = {
},
getDocumentById:(id)=>api.get(`/api/Document/${id}`),
getFilterEntities:(entityId)=>api.get(`/api/Document/get/filter${entityTypeId}`),
getFilterEntities:(entityTypeId)=>api.get(`/api/Document/get/filter/${entityTypeId}`),
UpdateDocument:(documentId,data)=>api.get(`/api/Expense/edit/${documentId}`,data)
}