Added Document Managment feature #388

Merged
pramod.mahajan merged 124 commits from Document_Manag into main 2025-09-10 14:34:35 +00:00
4 changed files with 202 additions and 142 deletions
Showing only changes of commit fd14328562 - Show all commits

View File

@ -21,12 +21,13 @@ export const DocumentContext = createContext();
export const useDocumentContext = () => { export const useDocumentContext = () => {
const context = useContext(DocumentContext); const context = useContext(DocumentContext);
if (!context) { if (!context) {
throw new Error("useDocumentContext must be used within an DocumentProvider"); throw new Error(
"useDocumentContext must be used within an DocumentProvider"
);
} }
return context; return context;
}; };
export const getDocuementsStatus = (status) => { export const getDocuementsStatus = (status) => {
switch (status) { switch (status) {
case true: case true:
@ -46,22 +47,22 @@ export const getDocuementsStatus = (status) => {
}; };
const Documents = ({ Document_Entity, Entity }) => { const Documents = ({ Document_Entity, Entity }) => {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [isActive, setIsActive] = useState(true);
const [filters, setFilter] = 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 [DocumentEntity,setDocumentEntity] = useState(Document_Entity) const [DocumentEntity, setDocumentEntity] = useState(Document_Entity);
const { employeeId } = useParams(); const { employeeId } = useParams();
const [OpenDocument,setOpenDocument] = useState(false) const [OpenDocument, setOpenDocument] = useState(false);
const [ManageDoc, setManageDoc] = useState({ const [ManageDoc, setManageDoc] = useState({
document: null, document: null,
isOpen: false, isOpen: false,
}); });
const [viewDoc, setViewDoc] = useState({ const [viewDoc, setViewDoc] = useState({
document: null, document: null,
isOpen: false, isOpen: false,
}); });
const { setOffcanvasContent, setShowTrigger } = useFab(); const { setOffcanvasContent, setShowTrigger } = useFab();
const methods = useForm({ const methods = useForm({
@ -95,34 +96,33 @@ const Documents = ({ Document_Entity, Entity }) => {
viewDoc, viewDoc,
setViewDoc, setViewDoc,
setOpenDocument, setOpenDocument,
OpenDocument OpenDocument,
} };
useEffect(()=>{ useEffect(() => {
if(Document_Entity){ if (Document_Entity) {
setDocumentEntity(Document_Entity) setDocumentEntity(Document_Entity);
} }
},[Document_Entity]) }, [Document_Entity]);
return ( return (
<DocumentContext.Provider value={contextValues}> <DocumentContext.Provider value={contextValues}>
<div className="mt-5">
<div className="card d-flex p-2">
<div className="row align-items-center">
{/* Search */}
<div className="col-6 col-md-6 col-lg-3 mb-md-0">
<input
type="search"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="form-control form-control-sm"
placeholder="Search Document"
/>
</div>
<div className="mt-5"> {/* Actions */}
<div className="card d-flex p-2"> <div className="col-6 col-md-6 col-lg-9 text-end">
<div className="row align-items-center"> {/* <span
{/* Search */}
<div className="col-6 col-md-6 col-lg-3 mb-md-0">
<input
type="search"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="form-control form-control-sm"
placeholder="Search Document"
/>
</div>
{/* 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" className="text-tiny text-muted p-1 border-0 bg-none lead mx-3 cursor-pointer"
disabled={isRefetching} disabled={isRefetching}
onClick={() => { onClick={() => {
@ -138,71 +138,96 @@ const Documents = ({ Document_Entity, Entity }) => {
}`} }`}
></i> ></i>
</span> */} </span> */}
<label className="switch switch-sm">
<input
type="checkbox"
className="switch-input"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className="switch-label">
{isActive ? "Active" : "In-Active"}
</span>
</label>
<button <button
type="button" type="button"
title="Add New Document" title="Add New Document"
className="p-1 bg-primary rounded-circle cursor-pointer" className="p-1 bg-primary rounded-circle cursor-pointer"
onClick={() => setManageDoc({ onClick={() =>
document: null, setManageDoc({
isOpen: true, document: null,
})} isOpen: true,
> })
<i className="bx bx-plus fs-4 text-white"></i> }
</button> >
<i className="bx bx-plus fs-4 text-white"></i>
</button>
</div>
</div> </div>
<DocumentsList
Document_Entity={DocumentEntity}
Entity={Entity}
filters={filters}
searchText={searchText}
setIsRefetching={setIsRefetching}
setRefetchFn={setRefetchFn}
isActive={isActive}
/>
</div> </div>
<DocumentsList
Document_Entity={DocumentEntity}
Entity={Entity}
filters={filters}
searchText={searchText}
setIsRefetching={setIsRefetching}
setRefetchFn={setRefetchFn}
/>
</div>
{ManageDoc.isOpen && ( {ManageDoc.isOpen && (
<GlobalModel <GlobalModel
isOpen={ManageDoc.isOpen} isOpen={ManageDoc.isOpen}
closeModal={() =>
setManageDoc({
document: null,
isOpen: false,
})
}
>
<ManageDocument
closeModal={() => closeModal={() =>
setManageDoc({ setManageDoc({
document: null, document: null,
isOpen: false, isOpen: false,
}) })
} }
Document_Entity={DocumentEntity} >
Entity={Entity} <ManageDocument
/> closeModal={() =>
</GlobalModel> setManageDoc({
)} document: null,
isOpen: false,
})
}
Document_Entity={DocumentEntity}
Entity={Entity}
/>
</GlobalModel>
)}
{viewDoc.isOpen && ( {viewDoc.isOpen && (
<GlobalModel size="lg" isOpen={viewDoc.isOpen} closeModal={()=>setViewDoc({ <GlobalModel
document:null, size="lg"
isOpen:false isOpen={viewDoc.isOpen}
})}> closeModal={() =>
<ViewDocument /> setViewDoc({
</GlobalModel> document: null,
)} isOpen: false,
})
}
>
<ViewDocument />
</GlobalModel>
)}
{OpenDocument && ( {OpenDocument && (
<GlobalModel isOpen={OpenDocument} closeModal={()=>setOpenDocument(false)}> <GlobalModel
<DocumentViewerModal/> isOpen={OpenDocument}
</GlobalModel> closeModal={() => setOpenDocument(false)}
)} >
<DocumentViewerModal />
</div> </GlobalModel>
)}
</div>
</DocumentContext.Provider> </DocumentContext.Provider>
); );
}; };

View File

@ -6,12 +6,12 @@ import {
import { ITEMS_PER_PAGE } from "../../utils/constants"; import { ITEMS_PER_PAGE } from "../../utils/constants";
import Avatar from "../common/Avatar"; import Avatar from "../common/Avatar";
import { formatUTCToLocalTime } from "../../utils/dateUtils"; import { formatUTCToLocalTime } from "../../utils/dateUtils";
import Loader from "../common/Loader";
import { useDebounce } from "../../utils/appUtils"; import { useDebounce } from "../../utils/appUtils";
import { DocumentTableSkeleton } from "./DocumentSkeleton"; import { DocumentTableSkeleton } from "./DocumentSkeleton";
import { getDocuementsStatus, useDocumentContext } from "./Documents"; import { getDocuementsStatus, useDocumentContext } from "./Documents";
import Pagination from "../common/Pagination"; import Pagination from "../common/Pagination";
import ConfirmModal from "../common/ConfirmModal"; import ConfirmModal from "../common/ConfirmModal";
import { isPending } from "@reduxjs/toolkit";
const DocumentsList = ({ const DocumentsList = ({
Document_Entity, Document_Entity,
@ -20,11 +20,14 @@ const DocumentsList = ({
searchText, searchText,
setIsRefetching, setIsRefetching,
setRefetchFn, setRefetchFn,
isActive,
}) => { }) => {
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deletingId, setDeletingId] = useState(null); const [deletingId, setDeletingId] = useState(null);
const [restoringIds, setRestoringIds] = useState([]);
const debouncedSearch = useDebounce(searchText, 500); const debouncedSearch = useDebounce(searchText, 500);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const { data, isError, isLoading, error, refetch, isFetching } = const { data, isError, isLoading, error, refetch, isFetching } =
useDocumentListByEntityId( useDocumentListByEntityId(
Document_Entity, Document_Entity,
@ -32,21 +35,21 @@ const DocumentsList = ({
ITEMS_PER_PAGE, ITEMS_PER_PAGE,
currentPage, currentPage,
filters, filters,
debouncedSearch debouncedSearch,
isActive
); );
// Pass the refetch function to parent when component mounts
useEffect(() => { useEffect(() => {
setRefetchFn(() => refetch); setRefetchFn(() => refetch);
}, [setRefetchFn, refetch]); }, [setRefetchFn, refetch]);
// Sync fetching status with parent
useEffect(() => { useEffect(() => {
setIsRefetching(isFetching); setIsRefetching(isFetching);
}, [isFetching, setIsRefetching]); }, [isFetching, setIsRefetching]);
const { setManageDoc, setViewDoc } = useDocumentContext(); const { setManageDoc, setViewDoc } = useDocumentContext();
const { mutate: ActiveInActive, isPending } = useActiveInActiveDocument(); const { mutate: ActiveInActive, isPending } = useActiveInActiveDocument();
const paginate = (page) => { const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) { if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page); setCurrentPage(page);
@ -66,9 +69,8 @@ const DocumentsList = ({
if (isFilterEmpty) return <div>No documents match your filter.</div>; if (isFilterEmpty) return <div>No documents match your filter.</div>;
const handleDelete = () => { const handleDelete = () => {
debugger;
ActiveInActive( ActiveInActive(
{ documentId: deletingId, isActive: false }, { documentId: deletingId, isActive: !isActive },
{ {
onSettled: () => { onSettled: () => {
setDeletingId(null); setDeletingId(null);
@ -77,6 +79,21 @@ const DocumentsList = ({
} }
); );
}; };
const handleRestore = (docId) => {
setRestoringIds((prev) => [...prev, docId]);
ActiveInActive(
{ documentId: docId, isActive: true },
{
onSettled: () => {
setRestoringIds((prev) => prev.filter((id) => id !== docId));
refetch();
},
}
);
};
const DocumentColumns = [ const DocumentColumns = [
{ {
key: "name", key: "name",
@ -94,10 +111,6 @@ const DocumentsList = ({
key: "uploadedBy", key: "uploadedBy",
label: "Uploaded By", label: "Uploaded By",
align: "text-start", align: "text-start",
getValue: (e) =>
`${e.uploadedBy?.firstName ?? ""} ${
e.uploadedBy?.lastName ?? ""
}`.trim() || "N/A",
customRender: (e) => ( customRender: (e) => (
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<Avatar <Avatar
@ -106,27 +119,31 @@ const DocumentsList = ({
firstName={e.uploadedBy?.firstName} firstName={e.uploadedBy?.firstName}
lastName={e.uploadedBy?.lastName} lastName={e.uploadedBy?.lastName}
/> />
<span className="text-truncate "> <span className="text-truncate ms-1">
{`${e.uploadedBy?.firstName ?? ""} ${ {`${e.uploadedBy?.firstName ?? ""} ${
e.uploadedBy?.lastName ?? "" e.uploadedBy?.lastName ?? ""
}`.trim() || "N/A"} }`.trim() || "N/A"}
</span> </span>
</div> </div>
), ),
getValue: (e) =>
`${e.uploadedBy?.firstName ?? ""} ${
e.uploadedBy?.lastName ?? ""
}`.trim() || "N/A",
}, },
{ {
key: "uploadedAt", key: "uploadedAt",
label: "Uploaded on", label: "Uploaded on",
getValue: (e) => formatUTCToLocalTime(e?.uploadedAt), getValue: (e) => formatUTCToLocalTime(e.uploadedAt),
isAlwaysVisible: true,
align: "text-center", align: "text-center",
isAlwaysVisible: true,
}, },
{ {
key: "Status", key: "Status",
label: "status", label: "Status",
getValue: (e) => getDocuementsStatus(e.isVerified), getValue: (e) => getDocuementsStatus(e.isVerified),
isAlwaysVisible: true,
align: "text-center", align: "text-center",
isAlwaysVisible: true,
}, },
]; ];
@ -134,13 +151,10 @@ const DocumentsList = ({
<> <>
{IsDeleteModalOpen && ( {IsDeleteModalOpen && (
<div <div
className={`modal fade show`} className="modal fade show"
tabIndex="-1" tabIndex="-1"
role="dialog" role="dialog"
style={{ style={{ display: "block", backgroundColor: "rgba(0,0,0,0.5)" }}
display: "block",
backgroundColor: "rgba(0,0,0,0.5)",
}}
aria-hidden="false" aria-hidden="false"
> >
<ConfirmModal <ConfirmModal
@ -149,7 +163,7 @@ const DocumentsList = ({
message="Are you sure you want delete?" message="Are you sure you want delete?"
onSubmit={handleDelete} onSubmit={handleDelete}
onClose={() => setIsDeleteModalOpen(false)} onClose={() => setIsDeleteModalOpen(false)}
loading={isPending} loading={!!isPending}
paramData={deletingId} paramData={deletingId}
/> />
</div> </div>
@ -170,48 +184,66 @@ const DocumentsList = ({
</tr> </tr>
</thead> </thead>
<tbody className="text-start"> <tbody className="text-start">
{data?.data?.map((doc) => ( {data?.data?.map((doc) => {
<tr key={doc.id}> const isRestoring = restoringIds.includes(doc.id);
{DocumentColumns.map((col) => (
<td key={col.key} className={`sorting ${col.align}`}> return (
{col.customRender <tr key={doc.id}>
? col.customRender(doc) {DocumentColumns.map((col) => (
: col.getValue(doc)} <td key={col.key} className={`sorting ${col.align}`}>
{col.customRender
? col.customRender(doc)
: col.getValue(doc)}
</td>
))}
<td className="text-center">
{doc.isActive ? (
<div className="d-flex justify-content-center gap-2">
<i
className="bx bx-show text-primary cursor-pointer"
onClick={() =>
setViewDoc({ document: doc.id, isOpen: true })
}
></i>
<i
className="bx bx-edit text-secondary cursor-pointer"
onClick={() =>
setManageDoc({ document: doc.id, isOpen: true })
}
></i>
<i
className="bx bx-trash text-danger cursor-pointer"
onClick={() => {
setIsDeleteModalOpen(true);
setDeletingId(doc.id);
}}
></i>
</div>
) : isRestoring ? (
<div
className="spinner-border spinner-border-sm text-primary"
role="status"
>
<span className="visually-hidden">Loading...</span>
</div>
) : (
<i
className="bx bx-recycle me-1 text-primary cursor-pointer"
onClick={() => handleRestore(doc.id)}
></i>
)}
</td> </td>
))} </tr>
<td className="text-center"> );
<div className="d-flex justify-content-center gap-2"> })}
<i
className="bx bx-show text-primary cursor-pointer"
onClick={() =>
setViewDoc({ document: doc?.id, isOpen: true })
}
></i>
<i
className="bx bx-edit text-secondary cursor-pointer"
onClick={() =>
setManageDoc({ document: doc?.id, isOpen: true })
}
></i>
<i
className="bx bx-trash text-danger cursor-pointer"
onClick={() => {
setIsDeleteModalOpen(true);
setDeletingId(doc?.id);
}}
></i>
</div>
</td>
</tr>
))}
</tbody> </tbody>
</table> </table>
{data?.data?.length > 0 && ( {data?.data?.length > 0 && (
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}
totalPages={data?.totalPages} totalPages={data.totalPages}
onPageChange={paginate} onPageChange={paginate}
/> />
)} )}

View File

@ -31,7 +31,8 @@ export const useDocumentListByEntityId = (
pageSize, pageSize,
pageNumber, pageNumber,
filter, filter,
searchString = "" searchString = "",
isActive
) => { ) => {
return useQuery({ return useQuery({
queryKey: [ queryKey: [
@ -42,6 +43,7 @@ export const useDocumentListByEntityId = (
pageNumber, pageNumber,
filter, filter,
searchString, searchString,
isActive
], ],
queryFn: async () => { queryFn: async () => {
const cleanedFilter = cleanFilter(filter); const cleanedFilter = cleanFilter(filter);
@ -51,7 +53,8 @@ export const useDocumentListByEntityId = (
pageSize, pageSize,
pageNumber, pageNumber,
cleanedFilter, cleanedFilter,
searchString searchString,
isActive
); );
return resp.data return resp.data

View File

@ -2,9 +2,9 @@ import { api } from "../utils/axiosClient";
export const DocumentRepository = { export const DocumentRepository = {
uploadDocument:(data)=> api.post(`/api/Document/upload`,data), uploadDocument:(data)=> api.post(`/api/Document/upload`,data),
getDocumentList:(entityTypeId,entityId,pageSize, pageNumber, filter,searchString)=>{ getDocumentList:(entityTypeId,entityId,pageSize, pageNumber, filter,searchString,isActive)=>{
const payloadJsonString = JSON.stringify(filter); const payloadJsonString = JSON.stringify(filter);
return api.get(`/api/Document/list/${entityTypeId}/entity/${entityId}/?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}`) return api.get(`/api/Document/list/${entityTypeId}/entity/${entityId}/?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}&isActive=${isActive}`)
}, },
getDocumentById:(id)=>api.get(`/api/Document/get/details/${id}`), getDocumentById:(id)=>api.get(`/api/Document/get/details/${id}`),