diff --git a/src/components/Expenses/Filelist.jsx b/src/components/Expenses/Filelist.jsx index b5a89452..c91c044d 100644 --- a/src/components/Expenses/Filelist.jsx +++ b/src/components/Expenses/Filelist.jsx @@ -3,6 +3,7 @@ import { formatFileSize, getIconByFileType } from "../../utils/appUtils"; import Tooltip from "../common/Tooltip"; const Filelist = ({ files, removeFile, expenseToEdit, sm = 6, md = 4 }) => { + debugger return (
{files diff --git a/src/components/purchase/ManagePurchase.jsx b/src/components/purchase/ManagePurchase.jsx index c846fd99..a011a3c7 100644 --- a/src/components/purchase/ManagePurchase.jsx +++ b/src/components/purchase/ManagePurchase.jsx @@ -16,7 +16,7 @@ import { } from "../../hooks/usePurchase"; const ManagePurchase = ({ onClose, purchaseId }) => { const { data } = usePurchase(purchaseId); - const [activeTab, setActiveTab] = useState(0); + const [activeTab, setActiveTab] = useState(2); const [completedTabs, setCompletedTabs] = useState([]); const stepsConfig = [ @@ -36,7 +36,7 @@ const ManagePurchase = ({ onClose, purchaseId }) => { name: "Payment Details", icon: "bx bx-credit-card bx-md", subtitle: "Amount, tax & due date", - component: , + component: , }, ]; @@ -60,6 +60,18 @@ const ManagePurchase = ({ onClose, purchaseId }) => { projectId: data.project.id, organizationId: data.organization.id, supplierId: data.supplier.id, + attachments: data.attachments + ? data?.attachments?.map((doc) => ({ + fileName: doc.fileName, + base64Data: null, + contentType: doc.contentType, + documentId: doc.id, + fileSize: 0, + description: "", + preSignedUrl: doc.preSignedUrl, + isActive: doc.isActive ?? true, + })) + : [] }); setCompletedTabs([0, 1, 2]); } diff --git a/src/components/purchase/PurchasePaymentDetails.jsx b/src/components/purchase/PurchasePaymentDetails.jsx index f5bb9570..a65b0cd3 100644 --- a/src/components/purchase/PurchasePaymentDetails.jsx +++ b/src/components/purchase/PurchasePaymentDetails.jsx @@ -3,8 +3,8 @@ import Label from "../common/Label"; import { useAppFormContext } from "../../hooks/appHooks/useAppForm"; import DatePicker from "../common/DatePicker"; import { useInvoiceAttachmentTypes } from "../../hooks/masterHook/useMaster"; - -const PurchasePaymentDetails = () => { +import Filelist from "../Expenses/Filelist"; +const PurchasePaymentDetails = ({purchaseId=null}) => { const { data, isLoading, error, isError } = useInvoiceAttachmentTypes(); const { register, @@ -26,6 +26,68 @@ const PurchasePaymentDetails = () => { } }, [baseAmount, taxAmount, setValue]); + const files = watch("attachments"); + const onFileChange = async (e) => { + const newFiles = Array.from(e.target.files); + if (newFiles.length === 0) return; + + const existingFiles = watch("attachments") || []; + + const parsedFiles = await Promise.all( + newFiles.map(async (file) => { + const base64Data = await toBase64(file); + return { + fileName: file.name, + base64Data, + contentType: file.type, + fileSize: file.size, + description: "", + isActive: true, + }; + }) + ); + + const combinedFiles = [ + ...existingFiles, + ...parsedFiles.filter( + (newFile) => + !existingFiles.some( + (f) => + f.fileName === newFile.fileName && f.fileSize === newFile.fileSize + ) + ), + ]; + + setValue("attachments", combinedFiles, { + shouldDirty: true, + shouldValidate: true, + }); + }; + + const toBase64 = (file) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result.split(",")[1]); + reader.onerror = (error) => reject(error); + }); + const removeFile = (index) => { + debugger + if (purchaseId) { + const newFiles = files.map((file, i) => { + if (file.documentId !== index) return file; + return { + ...file, + isActive: false, + }; + }); + setValue("attachments", newFiles, { shouldValidate: true }); + } else { + const newFiles = files.filter((_, i) => i !== index); + setValue("attachments", newFiles, { shouldValidate: true }); + } + }; + return (
@@ -56,7 +118,7 @@ const PurchasePaymentDetails = () => { id="taxAmount" type="number" className="form-control form-control-xs" - {...register("taxAmount",{ valueAsNumber: true })} + {...register("taxAmount", { valueAsNumber: true })} /> {errors?.taxAmount && ( @@ -75,7 +137,7 @@ const PurchasePaymentDetails = () => { id="totalAmount" type="number" className="form-control form-control-xs" - {...register("totalAmount",{ valueAsNumber: true })} + {...register("totalAmount", { valueAsNumber: true })} readOnly /> @@ -93,7 +155,7 @@ const PurchasePaymentDetails = () => { id="transportCharges" type="number" className="form-control form-control-xs" - {...register("transportCharges",{ valueAsNumber: true })} + {...register("transportCharges", { valueAsNumber: true })} /> {errors?.transportCharges && ( @@ -138,6 +200,63 @@ const PurchasePaymentDetails = () => {
)}
+ +
+
+ + +
document.getElementById("attachments").click()} + > + + + Click to select or click here to browse + + (PDF, JPG, PNG, max 5MB) + + { + onFileChange(e); + e.target.value = ""; + }} + /> +
+ {errors.attachments && ( + + {errors.attachments.message} + + )} + {files.length > 0 && ( + + )} + + {Array.isArray(errors.attachments) && + errors.attachments.map((fileError, index) => ( +
+ { + (fileError?.fileSize?.message || + fileError?.contentType?.message || + fileError?.base64Data?.message, + fileError?.documentId?.message) + } +
+ ))} +
+
); }; diff --git a/src/components/purchase/PurchaseSchema.jsx b/src/components/purchase/PurchaseSchema.jsx index 2656fa58..cce47575 100644 --- a/src/components/purchase/PurchaseSchema.jsx +++ b/src/components/purchase/PurchaseSchema.jsx @@ -1,39 +1,63 @@ import { z } from "zod"; import { normalizeAllowedContentTypes } from "../../utils/appUtils"; -export const AttachmentSchema = (allowedContentType, maxSizeAllowedInMB) => { - const allowedTypes = normalizeAllowedContentTypes(allowedContentType); +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB +const ALLOWED_TYPES = [ + "application/pdf", + "image/png", + "image/jpg", + "image/jpeg", +]; - return z.object({ - fileName: z.string().min(1, { message: "File name is required" }), - base64Data: z.string().min(1, { message: "File data is required" }), - invoiceAttachmentTypeId: z - .string() - .min(1, { message: "File data is required" }), +export const AttachmentSchema = z.object({ + documentId: z.string().optional(), + invoiceAttachmentTypeId: z.string().min(1, { message: "Attachment type is required" }), + fileName: z.string().min(1, { message: "Filename is required" }), + base64Data: z.string().min(1, { message: "File data is required" }), + contentType: z + .string() + .refine((val) => ALLOWED_TYPES.includes(val), { + message: "Only PDF, PNG, JPG, or JPEG files are allowed", + }), + fileSize: z.number().max(MAX_FILE_SIZE, { + message: "File size must be less than or equal to 5MB", + }), + description: z.string().optional().default(""), + isActive: z.boolean().default(true), +}); +// export const AttachmentSchema = (allowedContentType, maxSizeAllowedInMB) => { +// const allowedTypes = normalizeAllowedContentTypes(allowedContentType); - contentType: z - .string() - .min(1, { message: "MIME type is required" }) - .refine( - (val) => (allowedTypes.length ? allowedTypes.includes(val) : true), - { - message: `File type must be one of: ${allowedTypes.join(", ")}`, - } - ), +// return z.object({ +// fileName: z.string().min(1, { message: "File name is required" }), +// base64Data: z.string().min(1, { message: "File data is required" }), +// invoiceAttachmentTypeId: z +// .string() +// .min(1, { message: "File data is required" }), - fileSize: z - .number() - .int() - .nonnegative("fileSize must be ≥ 0") - .max( - (maxSizeAllowedInMB ?? 25) * 1024 * 1024, - `fileSize must be ≤ ${maxSizeAllowedInMB ?? 25}MB` - ), +// contentType: z +// .string() +// .min(1, { message: "MIME type is required" }) +// .refine( +// (val) => (allowedTypes.length ? allowedTypes.includes(val) : true), +// { +// message: `File type must be one of: ${allowedTypes.join(", ")}`, +// } +// ), - description: z.string().optional().default(""), - isActive: z.boolean(), - }); -}; +// fileSize: z +// .number() +// .int() +// .nonnegative("fileSize must be ≥ 0") +// .max( +// (maxSizeAllowedInMB ?? 25) * 1024 * 1024, +// `fileSize must be ≤ ${maxSizeAllowedInMB ?? 25}MB` +// ), + +// description: z.string().optional().default(""), +// isActive: z.boolean(), +// }); +// }; export const PurchaseSchema = z.object({ title: z.string().min(1, { message: "Title is required" }), @@ -65,8 +89,10 @@ export const PurchaseSchema = z.object({ paymentDueDate: z.coerce.date().nullable(), transportCharges: z.number().nullable(), description: z.string().min(1, { message: "Description is required" }), + attachments: z + .array(AttachmentSchema) + .nonempty({ message: "At least one file attachment is required" }), - // attachments: z.array(AttachmentSchema([], 25)).optional(), }); // deliveryChallanNo: z @@ -104,6 +130,7 @@ export const defaultPurchaseValue = { paymentDueDate: null, transportCharges: null, description: "", + attachments: [], // attachments: [], }; @@ -180,7 +207,7 @@ export const DeliveryChallanSchema = z.object({ invoiceAttachmentTypeId: z.string().nullable(), deliveryChallanDate: z.string().min(1, { message: "Deliver date required" }), description: z.string().min(1, { message: "Description required" }), - attachment: z.any().refine( + attachment: z.any().refine( (val) => val && typeof val === "object" && !!val.base64Data, { message: "Please upload document",