Added Document Managment feature #388

Merged
pramod.mahajan merged 124 commits from Document_Manag into main 2025-09-10 14:34:35 +00:00
6 changed files with 191 additions and 80 deletions
Showing only changes of commit 2f24d4a7ff - Show all commits

View File

@ -34,13 +34,13 @@ export const TagSchema = z.object({
isActive: z.boolean().default(true), isActive: z.boolean().default(true),
}); });
export const DocumentPayloadSchema = (docConfig = {}) => { export const DocumentPayloadSchema = (docConfig = {}) => {
const { const {
isMandatory, isMandatory,
regexExpression, regexExpression,
allowedContentType, allowedContentType,
maxSizeAllowedInMB, maxSizeAllowedInMB,
isUpdateForm,
} = docConfig; } = docConfig;
let documentIdSchema = z.string(); let documentIdSchema = z.string();
@ -58,20 +58,31 @@ export const DocumentPayloadSchema = (docConfig = {}) => {
); );
} }
// Base attachment schema
let attachmentSchema = AttachmentSchema(
allowedContentType,
maxSizeAllowedInMB
).nullable();
// If not update form, require attachment
if (!isUpdateForm) {
attachmentSchema = attachmentSchema.refine((val) => val !== null, {
message: "Attachment is required",
});
}
return z.object({ return z.object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
documentId: documentIdSchema, documentId: documentIdSchema,
description: z.string().min(1, { message: "Description is required" }), description: z.string().min(1, { message: "Description is required" }),
// entityId: z.string().min(1, { message: "Please Select Document Entity" }), documentTypeId: z
documentTypeId: z.string().min(1, { message: "Please Select Document Type" }), .string()
.min(1, { message: "Please Select Document Type" }),
documentCategoryId: z documentCategoryId: z
.string() .string()
.min(1, { message: "Please Select Document Category" }), .min(1, { message: "Please Select Document Category" }),
attachment: AttachmentSchema(allowedContentType, maxSizeAllowedInMB).nullable().refine( attachment: attachmentSchema,
(val) => val !== null, tags: z.array(TagSchema).optional().default([]),
{ message: "Attachment is required" }
),
tags: z.array(TagSchema).optional().default([]),
}); });
}; };
@ -83,14 +94,15 @@ export const defaultDocumentValues = {
// entityId: "", // entityId: "",
documentTypeId: "", documentTypeId: "",
documentCategoryId: "", documentCategoryId: "",
attachment: { // attachment: {
fileName: "", // fileName: "",
base64Data: "", // base64Data: "",
contentType: "", // contentType: "",
fileSize: 0, // fileSize: 0,
description: "", // description: "",
isActive: true, // isActive: true,
}, // },
attachment:null,
tags: [], tags: [],
}; };

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { createContext, useContext, useEffect, useState } from "react";
import GlobalModel from "../common/GlobalModel"; import GlobalModel from "../common/GlobalModel";
import NewDocument from "./ManageDocument"; import NewDocument from "./ManageDocument";
import { DOCUMENTS_ENTITIES } from "../../utils/constants"; import { DOCUMENTS_ENTITIES } from "../../utils/constants";
@ -14,13 +14,28 @@ import {
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import ManageDocument from "./ManageDocument"; import ManageDocument from "./ManageDocument";
// Context
export const DocumentContext = createContext();
export const useDocumentContext = () => {
const context = useContext(DocumentContext);
if (!context) {
throw new Error("useExpenseContext must be used within an ExpenseProvider");
}
return context;
};
const Documents = ({ Document_Entity, Entity }) => { const Documents = ({ Document_Entity, Entity }) => {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
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 { employeeId } = useParams(); const { employeeId } = useParams();
const [isUpload, setUpload] = useState(false); const [ManageDoc, setManageDoc] = useState({
document: null,
isOpen: false,
});
const { setOffcanvasContent, setShowTrigger } = useFab(); const { setOffcanvasContent, setShowTrigger } = useFab();
const methods = useForm({ const methods = useForm({
@ -48,8 +63,15 @@ const Documents = ({ Document_Entity, Entity }) => {
}; };
}, []); }, []);
const contextValues = {
ManageDoc,
setManageDoc,
}
return ( return (
<div className="mt-5"> <DocumentContext.Provider value={contextValues}>
<div className="mt-5">
<div className="card d-flex p-2"> <div className="card d-flex p-2">
<div className="row align-items-center"> <div className="row align-items-center">
{/* Search */} {/* Search */}
@ -86,7 +108,10 @@ const Documents = ({ Document_Entity, Entity }) => {
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={() => setUpload(true)} onClick={() => setManageDoc({
document: null,
isOpen: true,
})}
> >
<i className="bx bx-plus fs-4 text-white"></i> <i className="bx bx-plus fs-4 text-white"></i>
</button> </button>
@ -102,16 +127,31 @@ const Documents = ({ Document_Entity, Entity }) => {
/> />
</div> </div>
{isUpload && ( {ManageDoc.isOpen && (
<GlobalModel isOpen={isUpload} closeModal={() => setUpload(false)}> <GlobalModel
isOpen={ManageDoc.isOpen}
closeModal={() =>
setManageDoc({
document: null,
isOpen: false,
})
}
>
<ManageDocument <ManageDocument
closeModal={() => setUpload(false)} closeModal={() =>
setManageDoc({
document: null,
isOpen: false,
})
}
Document_Entity={Document_Entity} Document_Entity={Document_Entity}
Entity={Entity} Entity={Entity}
/> />
</GlobalModel> </GlobalModel>
)} )}
</div> </div>
</DocumentContext.Provider>
); );
}; };

View File

@ -6,6 +6,7 @@ import { formatUTCToLocalTime } from "../../utils/dateUtils";
import Loader from "../common/Loader"; import Loader from "../common/Loader";
import { useDebounce } from "../../utils/appUtils"; import { useDebounce } from "../../utils/appUtils";
import { DocumentTableSkeleton } from "./DocumentSkeleton"; import { DocumentTableSkeleton } from "./DocumentSkeleton";
import { useDocumentContext } from "./Documents";
export const getDocuementsStatus = (status) => { export const getDocuementsStatus = (status) => {
switch (status) { switch (status) {
@ -53,6 +54,8 @@ const DocumentsList = ({
setIsRefetching(isFetching); setIsRefetching(isFetching);
}, [isFetching, setIsRefetching]); }, [isFetching, setIsRefetching]);
const {setManageDoc} = useDocumentContext()
// check no data scenarios // check no data scenarios
const noData = !isLoading && !isError && data?.length === 0; const noData = !isLoading && !isError && data?.length === 0;
const isSearchEmpty = noData && !!debouncedSearch; const isSearchEmpty = noData && !!debouncedSearch;
@ -146,7 +149,7 @@ const DocumentsList = ({
<div className="d-flex justify-content-center gap-2"> <div className="d-flex justify-content-center gap-2">
<i className="bx bx-show text-primary cursor-pointer"></i> <i className="bx bx-show text-primary cursor-pointer"></i>
<i className="bx bx-edit text-secondary cursor-pointer"></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"></i> <i className="bx bx-trash text-danger cursor-pointer"></i>
</div> </div>

View File

@ -8,8 +8,13 @@ import {
useDocumentTypes, useDocumentTypes,
} from "../../hooks/masterHook/useMaster"; } from "../../hooks/masterHook/useMaster";
import TagInput from "../common/TagInput"; import TagInput from "../common/TagInput";
import { useUploadDocument } from "../../hooks/useDocument"; import {
useDocumentDetails,
useUpdateDocument,
useUploadDocument,
} from "../../hooks/useDocument";
import showToast from "../../services/toastService"; import showToast from "../../services/toastService";
import { useDocumentContext } from "./Documents";
const toBase64 = (file) => const toBase64 = (file) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
@ -19,11 +24,14 @@ const toBase64 = (file) =>
reader.onerror = (err) => reject(err); reader.onerror = (err) => reject(err);
}); });
const ManageDocument = ({closeModal,Document_Entity,Entity}) => { const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
const { ManageDoc } = useDocumentContext();
const isUpdateForm = Boolean(ManageDoc?.document);
const [selectedType, setSelectedType] = useState(null); const [selectedType, setSelectedType] = useState(null);
const [selectedCategory, setSelectedCategory] = useState(null); const [selectedCategory, setSelectedCategory] = useState(null);
const [schema, setSchema] = useState(() => DocumentPayloadSchema({})); const [schema, setSchema] = useState(() =>
DocumentPayloadSchema({ isUpdateForm })
);
const methods = useForm({ const methods = useForm({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: defaultDocumentValues, defaultValues: defaultDocumentValues,
@ -41,19 +49,35 @@ const ManageDocument = ({closeModal,Document_Entity,Entity}) => {
showToast("Document Uploaded Successfully", "success"); showToast("Document Uploaded Successfully", "success");
closeModal(); closeModal();
}); });
const { mutate: UpdateDocument, isPending: isUpdatinDoc } = useUpdateDocument(
() => {
showToast("Document Updated Successfully", "success");
closeModal();
}
);
const onSubmit = (data) => { const onSubmit = (data) => {
const DocumentPayload = { ...data, entityId: Entity }; if (ManageDoc?.document) {
UploadDocument(DocumentPayload); const DocumentPayload = { ...DocData, ...data };
UpdateDocument({ documentId: DocData?.id, DocumentPayload });
} else {
const DocumentPayload = { ...data, entityId: Entity };
UploadDocument(DocumentPayload);
}
}; };
const {
data: DocData,
isLoading: isDocLoading,
isError: isDocError,
DocError,
} = useDocumentDetails(ManageDoc?.document);
const file = watch("attachment"); const file = watch("attachment");
const documentTypeId = watch("documentTypeId"); const documentTypeId = watch("documentTypeId");
// This hooks calling api base Entity(Employee) and Category // This hooks calling api base Entity(Employee) and Category
const { DocumentCategories, isLoading } = useDocumentCategories( const { DocumentCategories, isLoading } =
Document_Entity useDocumentCategories(Document_Entity);
);
const categoryId = watch("documentCategoryId"); const categoryId = watch("documentCategoryId");
const { DocumentTypes, isLoading: isTypeLoading } = useDocumentTypes( const { DocumentTypes, isLoading: isTypeLoading } = useDocumentTypes(
@ -63,26 +87,29 @@ const ManageDocument = ({closeModal,Document_Entity,Entity}) => {
// Update schema whenever document type changes // Update schema whenever document type changes
useEffect(() => { useEffect(() => {
if (!documentTypeId) return; if (!documentTypeId) return;
const type = DocumentTypes?.find((t) => t.id === documentTypeId);
setSelectedType(type || null);
if (type) { const type = DocumentTypes?.find(
const newSchema = DocumentPayloadSchema({ (t) => String(t.id) === String(documentTypeId)
isMandatory: type.isMandatory ?? false, );
regexExpression: type.regexExpression ?? null, if (!type) return;
allowedContentType: type.allowedContentType ?? [
"application/pdf",
"image/jpeg",
"image/png",
],
maxSizeAllowedInMB: type.maxSizeAllowedInMB ?? 25,
});
setSchema(() => newSchema); const newSchema = DocumentPayloadSchema({
isMandatory: type.isMandatory ?? false,
regexExpression: type.regexExpression ?? null,
allowedContentType: type.allowedContentType ?? [
"application/pdf",
"image/jpeg",
"image/png",
],
maxSizeAllowedInMB: type.maxSizeAllowedInMB ?? 25,
isUpdateForm,
});
reset({ ...methods.getValues() }, { keepValues: true }); setSchema(() => newSchema);
}
}, [documentTypeId, DocumentTypes, reset]); methods.reset(methods.getValues(), { keepValues: true });
methods.formState.errors; // triggers revalidation
}, [documentTypeId, DocumentTypes, isUpdateForm]);
// File Upload // File Upload
const onFileChange = async (e) => { const onFileChange = async (e) => {
@ -127,11 +154,33 @@ const ManageDocument = ({closeModal,Document_Entity,Entity}) => {
: "" : ""
) )
.join(",") || ""; .join(",") || "";
useEffect(() => {
if (DocData) {
reset({
...defaultDocumentValues,
name: DocData?.name ?? "",
documentCategoryId: DocData?.documentType?.documentCategory?.id
? String(DocData.documentType.documentCategory.id)
: "",
documentTypeId: DocData?.documentType?.id
? String(DocData.documentType.id)
: "",
documentId: DocData?.documentId ?? "",
description: DocData?.description ?? "",
attachment: DocData?.attachment ?? null,
tags: DocData?.tags ?? [],
});
}
}, [DocData, reset]);
if (isDocLoading) return <div>Loading...</div>;
if (isDocError) return <div>{DocError.message}</div>;
return ( return (
<div className="p-2"> <div className="p-2">
<p className="fw-bold fs-6">Upload New Document</p> <p className="fw-bold fs-6">Upload New Document</p>
<FormProvider key={documentTypeId} {...methods}> <FormProvider key={documentTypeId} {...methods}>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)} className="text-start">
{/* Document Name */} {/* Document Name */}
<div className="mb-2"> <div className="mb-2">
<Label htmlFor="name" required> <Label htmlFor="name" required>
@ -174,28 +223,31 @@ const ManageDocument = ({closeModal,Document_Entity,Entity}) => {
</div> </div>
{/* Type */} {/* Type */}
{categoryId && (<div className="mb-2"> {categoryId && (
<Label htmlFor="documentTypeId">Document Type</Label> <div className="mb-2">
<select <Label htmlFor="documentTypeId">Document Type</Label>
{...register("documentTypeId")} <select
className="form-select form-select-sm" {...register("documentTypeId")}
> className="form-select form-select-sm"
{isTypeLoading && ( >
<option disabled value=""> {isTypeLoading && (
Loading... <option disabled value="">
</option> Loading...
</option>
)}
{DocumentTypes?.map((type) => (
<option key={type.id} value={type.id}>
{type.name}
</option>
))}
</select>
{errors.documentTypeId && (
<div className="danger-text">
{errors.documentTypeId.message}
</div>
)} )}
{DocumentTypes?.map((type) => ( </div>
<option key={type.id} value={type.id}> )}
{type.name}
</option>
))}
</select>
{errors.documentTypeId && (
<div className="danger-text">{errors.documentTypeId.message}</div>
)}
</div>)}
{/* Document ID */} {/* Document ID */}
<div className="mb-2"> <div className="mb-2">
@ -308,7 +360,7 @@ const ManageDocument = ({closeModal,Document_Entity,Entity}) => {
type="reset" type="reset"
className="btn btn-secondary btn-sm" className="btn btn-secondary btn-sm"
disabled={isPending} disabled={isPending}
onClick={()=>closeModal()} onClick={closeModal}
> >
Cancel Cancel
</button> </button>

View File

@ -42,8 +42,13 @@ export const useDocumentFilterEntities =(entityTypeId)=>{
export const useDocumentDetails =(documentId)=>{ export const useDocumentDetails =(documentId)=>{
return useQuery({ return useQuery({
queryKey:["Document",documentId], queryKey:["Document",documentId],
queryFn:async()=> await DocumentRepository.getDocumentById(documentId) queryFn:async()=> {
const resp = await DocumentRepository.getDocumentById(documentId);
return resp.data;
},
enabled:!!documentId
}) })
} }
//----------------------- MUTATION ------------------------- //----------------------- MUTATION -------------------------
@ -68,7 +73,7 @@ export const useUploadDocument =(onSuccessCallBack)=>{
export const useUpdateDocument =(onSuccessCallBack)=>{ export const useUpdateDocument =(onSuccessCallBack)=>{
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useMutation(({ return useMutation(({
mutationFn:async(documentId,DocumentPayload)=>DocumentRepository.UpdateDocument(documentId,DocumentPayload), mutationFn:async({documentId,DocumentPayload})=>DocumentRepository.UpdateDocument(documentId,DocumentPayload),
onSuccess:(data,variables)=>{ onSuccess:(data,variables)=>{
queryClient.invalidateQueries({queryKey:["DocumentList"]}); queryClient.invalidateQueries({queryKey:["DocumentList"]});
if(onSuccessCallBack) onSuccessCallBack() if(onSuccessCallBack) onSuccessCallBack()

View File

@ -6,10 +6,9 @@ export const DocumentRepository = {
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}`)
}, },
getDocumentById:(id)=>api.get(`/api/Document/${id}`), getDocumentById:(id)=>api.get(`/api/Document/get/details/${id}`),
getFilterEntities:(entityTypeId)=>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) UpdateDocument:(documentId,data)=>api.put(`/api/Document/edit/${documentId}`,data)
} }