429 lines
13 KiB
JavaScript
429 lines
13 KiB
JavaScript
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import React, { useEffect, useState } from "react";
|
|
import { useForm, FormProvider } from "react-hook-form";
|
|
import { defaultDocumentValues, DocumentPayloadSchema } from "./DocumentSchema";
|
|
import Label from "../common/Label";
|
|
import {
|
|
useDocumentCategories,
|
|
useDocumentTypes,
|
|
} from "../../hooks/masterHook/useMaster";
|
|
import TagInput from "../common/TagInput";
|
|
import {
|
|
useDocumentDetails,
|
|
useDocumentTags,
|
|
useUpdateDocument,
|
|
useUploadDocument,
|
|
} from "../../hooks/useDocument";
|
|
import showToast from "../../services/toastService";
|
|
import { useDocumentContext } from "./Documents";
|
|
import { isPending } from "@reduxjs/toolkit";
|
|
|
|
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 MergedTagsWithExistenStatus = (formTags = [], originalTags = []) => {
|
|
const tagMap = new Map();
|
|
|
|
const safeFormTags = Array.isArray(formTags) ? formTags : [];
|
|
const safeOriginalTags = Array.isArray(originalTags) ? originalTags : [];
|
|
|
|
safeOriginalTags.forEach(tag => {
|
|
if (tag?.name) {
|
|
tagMap.set(tag.name, { ...tag, isActive: tag.isActive ?? true });
|
|
}
|
|
});
|
|
|
|
safeFormTags.forEach(tag => {
|
|
if (tag?.name) {
|
|
tagMap.set(tag.name, { ...tag, isActive: true });
|
|
}
|
|
});
|
|
|
|
safeOriginalTags.forEach(tag => {
|
|
if (tag?.name && !safeFormTags.some(t => t.name === tag.name)) {
|
|
tagMap.set(tag.name, { ...tag, isActive: false });
|
|
}
|
|
});
|
|
|
|
return Array.from(tagMap.values());
|
|
};
|
|
|
|
|
|
const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
|
|
const { ManageDoc } = useDocumentContext();
|
|
const isUpdateForm = Boolean(ManageDoc?.document);
|
|
const [selectedType, setSelectedType] = useState(null);
|
|
const [selectedCategory, setSelectedCategory] = useState(null);
|
|
const [schema, setSchema] = useState(() =>
|
|
DocumentPayloadSchema({ isUpdateForm })
|
|
);
|
|
const methods = useForm({
|
|
resolver: zodResolver(schema),
|
|
defaultValues: defaultDocumentValues,
|
|
});
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
watch,
|
|
setValue,
|
|
reset,
|
|
formState: { errors },
|
|
} = methods;
|
|
const { mutate: UploadDocument, isPending: isUploading } = useUploadDocument(
|
|
() => {
|
|
showToast("Document Uploaded Successfully", "success");
|
|
closeModal();
|
|
}
|
|
);
|
|
const { mutate: UpdateDocument, isPending: isUpdating } = useUpdateDocument(
|
|
() => {
|
|
showToast("Document Updated Successfully", "success");
|
|
closeModal();
|
|
}
|
|
);
|
|
|
|
|
|
const onSubmit = (data) => {
|
|
const normalizeAttachment = (attachment) => {
|
|
if (!attachment) return null;
|
|
return {
|
|
...attachment,
|
|
fileSize: Math.ceil(attachment.fileSize / 1024),
|
|
};
|
|
};
|
|
|
|
const payload = {
|
|
...data,
|
|
attachment: normalizeAttachment(data.attachment),
|
|
};
|
|
|
|
if (ManageDoc?.document) {
|
|
const DocumentPayload = {
|
|
...payload,
|
|
id: DocData.id,
|
|
tags: MergedTagsWithExistenStatus(data?.tags, DocData?.tags),
|
|
};
|
|
UpdateDocument({ documentId: DocData?.id, DocumentPayload });
|
|
} else {
|
|
const DocumentPayload = { ...payload, entityId: Entity };
|
|
UploadDocument(DocumentPayload);
|
|
}
|
|
};
|
|
|
|
const {
|
|
data: DocData,
|
|
isLoading: isDocLoading,
|
|
isError: isDocError,
|
|
DocError,
|
|
} = useDocumentDetails(ManageDoc?.document);
|
|
const file = watch("attachment");
|
|
|
|
const documentTypeId = watch("documentTypeId");
|
|
|
|
// This hooks calling api base Entity(Employee) and Category
|
|
const { DocumentCategories, isLoading } =
|
|
useDocumentCategories(Document_Entity);
|
|
|
|
const categoryId = watch("documentCategoryId");
|
|
const { DocumentTypes, isLoading: isTypeLoading } = useDocumentTypes(
|
|
categoryId || null
|
|
);
|
|
const {data:DocumentTags} = useDocumentTags()
|
|
|
|
// Update schema whenever document type changes
|
|
useEffect(() => {
|
|
if (!documentTypeId) return;
|
|
|
|
const type = DocumentTypes?.find(
|
|
(t) => String(t.id) === String(documentTypeId)
|
|
);
|
|
if (!type) return;
|
|
setSelectedType(type)
|
|
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,
|
|
});
|
|
|
|
setSchema(() => newSchema);
|
|
|
|
methods.reset(methods.getValues(), { keepValues: true });
|
|
methods.formState.errors; // triggers revalidation
|
|
}, [documentTypeId, DocumentTypes, isUpdateForm]);
|
|
|
|
// 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,
|
|
});
|
|
};
|
|
|
|
// build dynamic file accept string
|
|
const fileAccept =
|
|
selectedType?.allowedContentType
|
|
?.split(",")
|
|
.map((t) =>
|
|
t === "application/pdf"
|
|
? ".pdf"
|
|
: t === "image/jpeg"
|
|
? ".jpg,.jpeg"
|
|
: t === "image/png"
|
|
? ".png"
|
|
: ""
|
|
)
|
|
.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>;
|
|
|
|
const isPending = isUploading || isUpdating;
|
|
return (
|
|
<div className="p-2">
|
|
<p className="fw-bold fs-6">Upload New Document</p>
|
|
<FormProvider key={documentTypeId} {...methods}>
|
|
<form onSubmit={handleSubmit(onSubmit)} className="text-start">
|
|
{/* 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>
|
|
)}
|
|
{!isLoading && <option value="">Select Category</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 */}
|
|
{categoryId && (
|
|
<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 htmlFor="attachment" required>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">
|
|
({selectedType?.allowedContentType || "PDF/JPG/PNG"}, max{" "}
|
|
{selectedType?.maxSizeAllowedInMB ?? 25}MB)
|
|
</small>
|
|
|
|
<input
|
|
type="file"
|
|
id="attachment"
|
|
accept={selectedType?.allowedContentType}
|
|
style={{ display: "none" }}
|
|
onChange={(e) => {
|
|
onFileChange(e);
|
|
e.target.value = ""; // reset input
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{errors.attachment && (
|
|
<small className="danger-text">
|
|
{errors.attachment.message
|
|
? errors.attachment.message
|
|
: errors.attachment.fileName?.message ||
|
|
errors.attachment.base64Data?.message ||
|
|
errors.attachment.contentType?.message ||
|
|
errors.attachment.fileSize?.message}
|
|
</small>
|
|
)}
|
|
|
|
{file?.base64Data && (
|
|
<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>
|
|
<div className="mb-2">
|
|
<TagInput name="tags" label="Tags" placeholder="Tags.." options={DocumentTags} />
|
|
{errors.tags && (
|
|
<small className="danger-text">{errors.tags.message}</small>
|
|
)}
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<div className="mb-2">
|
|
<Label htmlFor="description" required>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-end gap-3 mt-4">
|
|
<button
|
|
type="reset"
|
|
className="btn btn-label-secondary btn-sm"
|
|
disabled={isPending}
|
|
onClick={closeModal}
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="btn btn-primary btn-sm"
|
|
disabled={isPending}
|
|
>
|
|
{isPending ? "Please Wait..." : " Submit"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</FormProvider>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ManageDocument;
|