initial setuo new document

This commit is contained in:
pramod mahajan 2025-08-28 20:22:17 +05:30
parent d42790628c
commit b93eaf6b95
10 changed files with 449 additions and 15 deletions

View File

@ -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: [],
};

View File

@ -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 (
<div className=''>
<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"
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">
Refresh
< i className={`bx bx-refresh ms-1 `}></i>
</span>
<button
type="button"
title="Add New Document"
className="p-1 bg-primary rounded-circle cursor-pointer"
onClick={()=>setUpload(true)}
>
<i className="bx bx-plus fs-4 text-white"></i>
</button>
</div>
</div>
</div>
{isUpload && (
<GlobalModel isOpen={isUpload} closeModal={()=>setUpload(false)}>
<NewDocument/>
</GlobalModel>
)}
</div>
)
}
export default Documents

View File

@ -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 (
<div className="p-2">
<p className="fw-bold fs-6">Upload New Document</p>
<form onSubmit={handleSubmit(onSubmit)}>
{/* Document Name */}
<div className="mb-2">
<Label htmlFor="name" required>
Document Name
</Label>
<input
type="text"
className="form-control form-control-sm"
{...register("name")}
/>
{errors.name && (
<div className="danger-text">{errors.name.message}</div>
)}
</div>
{/* Category */}
<div className="mb-2">
<Label htmlFor="documentCategoryId">Document Category</Label>
<select
{...register("documentCategoryId")}
className="form-select form-select-sm"
>
{isLoading && (
<option disabled value="">
Loading...
</option>
)}
{DocumentCategories?.map((type) => (
<option key={type.id} value={type.id}>
{type.name}
</option>
))}
</select>
{errors.documentCategoryId && (
<div className="danger-text">
{errors.documentCategoryId.message}
</div>
)}
</div>
{/* Type */}
<div className="mb-2">
<Label htmlFor="documentTypeId">Document Type</Label>
<select
{...register("documentTypeId")}
className="form-select form-select-sm"
>
{isTypeLoading && (
<option disabled value="">
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>
)}
</div>
{/* Document ID */}
<div className="mb-2">
<Label
htmlFor="documentId"
required={selectedType?.isMandatory ?? false}
>
Document ID
</Label>
<input
type="text"
className="form-control form-control-sm"
{...register("documentId")}
/>
{errors.documentId && (
<div className="danger-text">{errors.documentId.message}</div>
)}
</div>
{/* Upload */}
<div className="row my-2">
<div className="col-md-12">
<label className="form-label">Upload Document</label>
<div
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
style={{ cursor: "pointer" }}
onClick={() => document.getElementById("attachment").click()}
>
<i className="bx bx-cloud-upload d-block bx-lg"></i>
<span className="text-muted d-block">
Click to select or click here to browse
</span>
<small className="text-muted">(PDF, JPG, PNG, max 25MB)</small>
<input
type="file"
id="attachment"
accept=".pdf,.jpg,.jpeg,.png"
style={{ display: "none" }}
onChange={(e) => {
onFileChange(e);
e.target.value = ""; // reset input
}}
/>
</div>
{errors.attachment && (
<small className="danger-text">
{errors.attachment.fileName?.message ||
errors.attachment.base64Data?.message ||
errors.attachment.contentType?.message ||
errors.attachment.fileSize?.message}
</small>
)}
{file && (
<div className="d-flex justify-content-between text-start p-1 mt-2">
<div>
<span className="mb-0 text-secondary small d-block">
{file.fileName}
</span>
<span className="text-body-secondary small d-block">
{(file.fileSize / 1024).toFixed(1)} KB
</span>
</div>
<i
className="bx bx-trash bx-sm cursor-pointer text-danger"
onClick={removeFile}
></i>
</div>
)}
</div>
</div>
{/* Description */}
<div className="mb-2">
<Label htmlFor="description">Description</Label>
<textarea
rows="2"
className="form-control"
{...register("description")}
></textarea>
{errors.description && (
<div className="danger-text">{errors.description.message}</div>
)}
</div>
{/* Buttons */}
<div className="d-flex justify-content-center gap-3">
<button type="submit" className="btn btn-primary btn-sm">
Submit
</button>
<button type="reset" className="btn btn-secondary btn-sm">
Cancel
</button>
</div>
</form>
</div>
);
};
export default NewDocument;

View File

@ -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 (
<>
<ComingSoonPage/>
<Documents/>
</>
);
};

View File

@ -57,8 +57,8 @@ const DateRangePicker = ({
/>
<i
className="bx bx-calendar calendar-icon cursor-pointer position-relative top-50 translate-middle-y "
style={{ right: "22px", bottom: "-8px" }}
className="bx bx-calendar calendar-icon cursor-pointer position-absolute top-12 translate-middle-y "
></i>
</div>
);

View File

@ -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) => {

View File

@ -3,13 +3,8 @@ import Breadcrumb from '../../components/common/Breadcrumb'
const DocumentPage = () => {
return (
<div className='container-fluid'>
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Document", link: null },
]}
/>
<div className=''>
<div className="card d-flex p-2">
<div className="row align-items-center">
{/* Search */}
@ -18,7 +13,7 @@ const DocumentPage = () => {
type="search"
className="form-control form-control-sm"
placeholder="Search Tenant"
placeholder="Search Document"
/>
</div>
@ -31,7 +26,7 @@ const DocumentPage = () => {
<button
type="button"
title="Add New Tenant"
title="Add New Document"
className="p-1 bg-primary rounded-circle cursor-pointer"
>
<i className="bx bx-plus fs-4 text-white"></i>
@ -39,7 +34,6 @@ const DocumentPage = () => {
</div>
</div>
</div>
</div>
)
}

View File

@ -1,2 +1,5 @@
import { api } from "../utils/axiosClient";
export const DocumentRepository = {
uploadDocument:(data)=> api.post(`/api/Document/upload`,data)
}

View File

@ -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}` : ""}`),
};

View File

@ -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 = {
}