Added Document Managment feature #388

Merged
pramod.mahajan merged 124 commits from Document_Manag into main 2025-09-10 14:34:35 +00:00
9 changed files with 333 additions and 45 deletions
Showing only changes of commit 518928e439 - Show all commits

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 ( 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, (val) => val !== null,
{ message: "Attachment is required" } { message: "Attachment is required" }
), ),
tags: z.array(TagSchema).optional().default([]), tags: z.array(TagSchema).optional().default([]),
}); });
}; };
@ -95,3 +93,28 @@ export const defaultDocumentValues = {
}, },
tags: [], 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 }) => { export const DocumentTableSkeleton = ({ rows = 5 }) => {
return ( return (
<div className="card px-2">
<table className="card-body table border-top dataTable no-footer dtr-column text-nowrap"> <table className="card-body table border-top dataTable no-footer dtr-column text-nowrap">
<thead> <thead>
<tr> <tr>
@ -65,6 +65,6 @@ export const DocumentTableSkeleton = ({ rows = 5 }) => {
))} ))}
</tbody> </tbody>
</table> </table>
</div>
); );
}; };

View File

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

View File

@ -24,14 +24,23 @@ export const getDocuementsStatus = (status) => {
); );
} }
}; };
const DocumentsList = ({ Document_Entity, Entity,searchText ,setIsRefetching, const DocumentsList = ({
setRefetchFn,}) => { Document_Entity,
Entity,
filters,
searchText,
setIsRefetching,
setRefetchFn,
}) => {
const debouncedSearch = useDebounce(searchText, 500); const debouncedSearch = useDebounce(searchText, 500);
const { data, isError, isLoading, error,refetch,isFetching } = useDocumentListByEntityId( const { data, isError, isLoading, error, refetch, isFetching } =
useDocumentListByEntityId(
Document_Entity, Document_Entity,
Entity, Entity,
ITEMS_PER_PAGE, ITEMS_PER_PAGE,
1,{},debouncedSearch 1,
filters,
debouncedSearch
); );
// Pass the refetch function to parent when component mounts // Pass the refetch function to parent when component mounts
@ -44,8 +53,18 @@ const DocumentsList = ({ Document_Entity, Entity,searchText ,setIsRefetching,
setIsRefetching(isFetching); setIsRefetching(isFetching);
}, [isFetching, setIsRefetching]); }, [isFetching, setIsRefetching]);
if (isLoading) return <DocumentTableSkeleton/>; // check no data scenarios
if (isError) return <p>Error: {error?.message || "Something went wrong"}</p>; 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 = [ const DocumentColumns = [
{ {

View File

@ -19,7 +19,7 @@ const toBase64 = (file) =>
reader.onerror = (err) => reject(err); reader.onerror = (err) => reject(err);
}); });
const NewDocument = ({closeModal,Document_Entity,Entity}) => { const ManageDocument = ({closeModal,Document_Entity,Entity}) => {
const [selectedType, setSelectedType] = useState(null); const [selectedType, setSelectedType] = useState(null);
const [selectedCategory, setSelectedCategory] = 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(); closePanel();
}; };
// Close popup when navigating to another component // Close popup when navigating to another component
const location = useLocation(); const location = useLocation();
useEffect(() => { useEffect(() => {
closePanel(); closePanel();

View File

@ -10,7 +10,12 @@ const cleanFilter = (filter) => {
if (Array.isArray(cleaned[key]) && cleaned[key].length === 0) { if (Array.isArray(cleaned[key]) && cleaned[key].length === 0) {
delete cleaned[key]; delete cleaned[key];
} }
});
["startDate", "endDate", "isVerified"].forEach((key) => {
if (cleaned[key] === null) {
delete cleaned[key];
}
}); });
return cleaned; return cleaned;
@ -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 ------------------------- //----------------------- MUTATION -------------------------
export const useUploadDocument =(onSuccessCallBack)=>{ export const useUploadDocument =(onSuccessCallBack)=>{
@ -46,3 +65,20 @@ 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}`), 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)
} }