From b93eaf6b95c93ff1a35ad3a00217153520f88f8a Mon Sep 17 00:00:00 2001 From: pramod mahajan Date: Thu, 28 Aug 2025 20:22:17 +0530 Subject: [PATCH] initial setuo new document --- src/components/Documents/DocumentSchema.js | 63 +++++ src/components/Documents/Documents.jsx | 52 +++++ src/components/Documents/NewDocument.jsx | 260 +++++++++++++++++++++ src/components/Employee/EmpDocuments.jsx | 4 +- src/components/common/DateRangePicker.jsx | 4 +- src/hooks/masterHook/useMaster.js | 50 ++++ src/pages/Documents/DocumentPage.jsx | 14 +- src/repositories/DocumentRepository.jsx | 3 + src/repositories/MastersRepository.jsx | 8 +- src/utils/constants.jsx | 6 + 10 files changed, 449 insertions(+), 15 deletions(-) create mode 100644 src/components/Documents/DocumentSchema.js create mode 100644 src/components/Documents/Documents.jsx create mode 100644 src/components/Documents/NewDocument.jsx diff --git a/src/components/Documents/DocumentSchema.js b/src/components/Documents/DocumentSchema.js new file mode 100644 index 00000000..5842e488 --- /dev/null +++ b/src/components/Documents/DocumentSchema.js @@ -0,0 +1,63 @@ +import { z } from "zod"; + +export const AttachmentSchema = z.object({ + fileName: z.string().min(1, {message:"File name is required"}), + base64Data: z.string().min(1, {message:"File data is required"}), + contentType: z.string().min(1, {message:"MIME type is required"}), + fileSize: z + .number() + .int() + .nonnegative("fileSize must be ≥ 0") + .max(25 * 1024 * 1024, "fileSize must be ≤ 25MB"), + description: z.string().optional().default(""), + isActive: z.boolean(), +}); + +export const TagSchema = z.object({ + name: z.string().min(1, {message:"Tag name is required"}), + isActive: z.boolean(), +}); + +export const DocumentPayloadSchema = (isMandatory, regularExp) => { + let documentIdSchema = z.string(); + + if (isMandatory) { + documentIdSchema = documentIdSchema.min(1, {message:"DocumentId is required"}); + } + + if (regularExp) { + documentIdSchema = documentIdSchema.regex( + new RegExp(regularExp), + "Invalid DocumentId format" + ); + } + + return z.object({ + name: z.string().min(1, "Name is required"), + documentId: documentIdSchema, + description: z.string().min(1,{message:"Description is required"}), + entityId: z.string().min(1,{message:"Please Select Document Entity"}), + documentTypeId: z.string().min(1,{message:"Please Select Document Type"}), + documentCategoryId:z.string().min(1,{message:"Please Select Document Category"}), + attachment: AttachmentSchema, + tags: z.array(TagSchema).default([]), + }); +}; + +export const defaultDocumentValues = { + name: "", + documentId: "", + description: "", + entityId: "", + documentTypeId: "", + documentCategoryId:"", + attachment: { + fileName: "", + base64Data: "", + contentType: "", + fileSize: 0, + description: "", + isActive: true, + }, + tags: [], +}; diff --git a/src/components/Documents/Documents.jsx b/src/components/Documents/Documents.jsx new file mode 100644 index 00000000..343310d8 --- /dev/null +++ b/src/components/Documents/Documents.jsx @@ -0,0 +1,52 @@ +import React, { useState } from 'react' +import GlobalModel from '../common/GlobalModel' +import NewDocument from './NewDocument' + + +const Documents = () => { + const [isUpload,setUpload] =useState(false); + + return ( +
+ +
+
+ {/* Search */} +
+ +
+ + {/* Actions */} +
+ + Refresh + < i className={`bx bx-refresh ms-1 `}> + + + +
+
+
+ {isUpload && ( + setUpload(false)}> + + + )} + +
+ ) +} + +export default Documents \ No newline at end of file diff --git a/src/components/Documents/NewDocument.jsx b/src/components/Documents/NewDocument.jsx new file mode 100644 index 00000000..175f46f4 --- /dev/null +++ b/src/components/Documents/NewDocument.jsx @@ -0,0 +1,260 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { defaultDocumentValues, DocumentPayloadSchema } from "./DocumentSchema"; +import Label from "../common/Label"; +import { DOCUMENTS_ENTITIES } from "../../utils/constants"; +import { + useDocumentCategories, + useDocumentTypes, +} from "../../hooks/masterHook/useMaster"; + +// util fn: convert file → base64 +const toBase64 = (file) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result); + reader.onerror = (err) => reject(err); + }); + +const NewDocument = () => { + const [selectedType, setType] = useState(""); + + const DocumentUpload = DocumentPayloadSchema( + selectedType?.isMandatory ?? null, + selectedType?.regexExpression ?? null + ); + + const { + register, + handleSubmit, + watch, + setValue, + formState: { errors }, + } = useForm({ + resolver: zodResolver(DocumentUpload), + defaultValues: defaultDocumentValues, + }); + + const onSubmit = (data) => { + console.log("Form submitted:", data); + }; + + const documentTypeId = watch("documentTypeId"); + const categoryId = watch("documentCategoryId"); + const file = watch("attachment"); + + // API Data + const { DocumentCategories, isLoading } = useDocumentCategories( + DOCUMENTS_ENTITIES.EmployeeEntity + ); + + const { DocumentTypes, isLoading: isTypeLoading } = + useDocumentTypes(categoryId); + + // File Upload + const onFileChange = async (e) => { + const uploaded = e.target.files[0]; + if (!uploaded) return; + + const base64Data = await toBase64(uploaded); + + const parsedFile = { + fileName: uploaded.name, + base64Data, + contentType: uploaded.type, + fileSize: uploaded.size, + description: "", + isActive: true, + }; + + setValue("attachment", parsedFile, { + shouldDirty: true, + shouldValidate: true, + }); + }; + + const removeFile = () => { + setValue("attachment", null, { + shouldDirty: true, + shouldValidate: true, + }); + }; + + useEffect(() => { + if (documentTypeId) { + setType(DocumentTypes?.find((type) => type.id === documentTypeId)); + } + }, [documentTypeId, DocumentTypes]); + + return ( +
+

Upload New Document

+ +
+ {/* Document Name */} +
+ + + {errors.name && ( +
{errors.name.message}
+ )} +
+ + {/* Category */} +
+ + + {errors.documentCategoryId && ( +
+ {errors.documentCategoryId.message} +
+ )} +
+ + {/* Type */} +
+ + + {errors.documentTypeId && ( +
{errors.documentTypeId.message}
+ )} +
+ + {/* Document ID */} +
+ + + {errors.documentId && ( +
{errors.documentId.message}
+ )} +
+ + {/* Upload */} +
+
+ + +
document.getElementById("attachment").click()} + > + + + Click to select or click here to browse + + (PDF, JPG, PNG, max 25MB) + + { + onFileChange(e); + e.target.value = ""; // reset input + }} + /> +
+ + {errors.attachment && ( + + {errors.attachment.fileName?.message || + errors.attachment.base64Data?.message || + errors.attachment.contentType?.message || + errors.attachment.fileSize?.message} + + )} + + {file && ( +
+
+ + {file.fileName} + + + {(file.fileSize / 1024).toFixed(1)} KB + +
+ +
+ )} +
+
+ + {/* Description */} +
+ + + {errors.description && ( +
{errors.description.message}
+ )} +
+ + {/* Buttons */} +
+ + +
+
+
+ ); +}; + +export default NewDocument; diff --git a/src/components/Employee/EmpDocuments.jsx b/src/components/Employee/EmpDocuments.jsx index 46aa33d0..39d6e900 100644 --- a/src/components/Employee/EmpDocuments.jsx +++ b/src/components/Employee/EmpDocuments.jsx @@ -1,10 +1,12 @@ import React, { useState, useEffect } from "react"; import { ComingSoonPage } from "../../pages/Misc/ComingSoonPage"; +import DocumentPage from "../../pages/Documents/DocumentPage"; +import Documents from "../Documents/Documents"; const EmpDocuments = ({ profile, loggedInUser }) => { return ( <> - + ); }; diff --git a/src/components/common/DateRangePicker.jsx b/src/components/common/DateRangePicker.jsx index 9b7505fe..90ea9b24 100644 --- a/src/components/common/DateRangePicker.jsx +++ b/src/components/common/DateRangePicker.jsx @@ -57,8 +57,8 @@ const DateRangePicker = ({ /> ); diff --git a/src/hooks/masterHook/useMaster.js b/src/hooks/masterHook/useMaster.js index 5675aba7..9a31dcf8 100644 --- a/src/hooks/masterHook/useMaster.js +++ b/src/hooks/masterHook/useMaster.js @@ -171,6 +171,56 @@ const { return { ExpenseStatus, loading, error }; } +export const useDocumentTypes =(category)=>{ +const { + data: DocumentTypes = [], + error, + isError, + isLoading + } = useQuery({ + queryKey: ["Document Type"], + queryFn: async () => { + const res = await MasterRespository.getDocumentTypes(category) + return res.data; + }, + enabled:!!category, + onError: (error) => { + showToast( + error?.response?.data?.message || + error.message || + "Failed to fetch Expense Status", + "error" + ); + }, + }); + + return { DocumentTypes, isError, isLoading, error }; +} +export const useDocumentCategories =(EntityType)=>{ +const { + data: DocumentCategories = [], + error, + isError, + isLoading + } = useQuery({ + queryKey: ["Document Category"], + queryFn: async () => { + const res = await MasterRespository.getDocumentCategories(EntityType) + return res.data; + }, + enabled:!!EntityType, + onError: (error) => { + showToast( + error?.response?.data?.message || + error.message || + "Failed to fetch Expense Status", + "error" + ); + }, + }); + + return { DocumentCategories, isError, isLoading, error }; +} // ===Application Masters Query================================================= const fetchMasterData = async (masterType) => { diff --git a/src/pages/Documents/DocumentPage.jsx b/src/pages/Documents/DocumentPage.jsx index f519bb5e..ba71070d 100644 --- a/src/pages/Documents/DocumentPage.jsx +++ b/src/pages/Documents/DocumentPage.jsx @@ -3,13 +3,8 @@ import Breadcrumb from '../../components/common/Breadcrumb' const DocumentPage = () => { return ( -
- +
+
{/* Search */} @@ -18,7 +13,7 @@ const DocumentPage = () => { type="search" className="form-control form-control-sm" - placeholder="Search Tenant" + placeholder="Search Document" />
@@ -31,7 +26,7 @@ const DocumentPage = () => {
- ) } diff --git a/src/repositories/DocumentRepository.jsx b/src/repositories/DocumentRepository.jsx index 052b1ad6..d01d7984 100644 --- a/src/repositories/DocumentRepository.jsx +++ b/src/repositories/DocumentRepository.jsx @@ -1,2 +1,5 @@ import { api } from "../utils/axiosClient"; +export const DocumentRepository = { + uploadDocument:(data)=> api.post(`/api/Document/upload`,data) +} diff --git a/src/repositories/MastersRepository.jsx b/src/repositories/MastersRepository.jsx index d084bf86..f8a885aa 100644 --- a/src/repositories/MastersRepository.jsx +++ b/src/repositories/MastersRepository.jsx @@ -83,6 +83,10 @@ export const MasterRespository = { api.put(`/api/Master/expenses-status/edit/${id}`, data), - getDocumentCategories:()=>api.get("/api/Master/document-category/list"), - getDocumentTypes:()=>api.get("/api/Master/document-type/list") + getDocumentCategories: (entityType) => + api.get(`/api/Master/document-category/list${entityType ? `?entityTypeId=${entityType}` : ""}`), + +getDocumentTypes: (category) => + api.get(`/api/Master/document-type/list${category ? `?documentCategoryId=${category}` : ""}`), + }; diff --git a/src/utils/constants.jsx b/src/utils/constants.jsx index 555e3a0b..b5daeb44 100644 --- a/src/utils/constants.jsx +++ b/src/utils/constants.jsx @@ -79,6 +79,12 @@ export const TENANT_STATUS = [ {id:"35d7840a-164a-448b-95e6-efb2ec84a751",name:"Supspended"} ] +export const DOCUMENTS_ENTITIES = { + ProjectEntity : "c8fe7115-aa27-43bc-99f4-7b05fabe436e", + EmployeeEntity:"dbb9555a-7a0c-40f2-a9ed-f0463f1ceed7", +} + + export const CONSTANT_TEXT = { }