diff --git a/src/components/PaymentRequest/ManagePaymentRequest.jsx b/src/components/PaymentRequest/ManagePaymentRequest.jsx new file mode 100644 index 00000000..1a05d673 --- /dev/null +++ b/src/components/PaymentRequest/ManagePaymentRequest.jsx @@ -0,0 +1,324 @@ +import React from 'react' +import { useProjectName } from '../../hooks/useProjects'; +import Label from '../common/Label'; +import { useForm } from 'react-hook-form'; +import { useExpenseType } from '../../hooks/masterHook/useMaster'; +import DatePicker from '../common/DatePicker'; + +function ManagePaymentRequest({ expenseToEdit = null}) { + + const { projectNames, loading: projectLoading, error, isError: isProjectError, + } = useProjectName(); + const { register, control, watch, formState: { errors }, } = useForm(); + + const { + ExpenseTypes, + loading: ExpenseLoading, + error: ExpenseError, + } = useExpenseType(); + + const files = watch("billAttachments"); + const onFileChange = async (e) => { + const newFiles = Array.from(e.target.files); + if (newFiles.length === 0) return; + + const existingFiles = watch("billAttachments") || []; + + 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("billAttachments", 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) => { + if (expenseToEdit) { + const newFiles = files.map((file, i) => { + if (file.documentId !== index) return file; + return { + ...file, + isActive: false, + }; + }); + setValue("billAttachments", newFiles, { shouldValidate: true }); + } else { + const newFiles = files.filter((_, i) => i !== index); + setValue("billAttachments", newFiles, { shouldValidate: true }); + } + }; + + return ( +
+
+ {expenseToEdit ? "Update Expense " : "Create New Expense"} +
+
+
+
+ + + {errors.projectId && ( + {errors.projectId.message} + )} +
+ +
+ + + {errors.expensesTypeId && ( + + {errors.expensesTypeId.message} + + )} +
+ +
+ +
+
+ + + + {errors.transactionDate && ( + + {errors.transactionDate.message} + + )} +
+ +
+ + + {errors.amount && ( + {errors.amount.message} + )} +
+
+ +
+
+ + + {errors.supplerName && ( + + {errors.supplerName.message} + + )} +
+ +
+ + + {errors.location && ( + {errors.location.message} + )} +
+
+ +
+
+ + + {errors.description && ( + + {errors.description.message} + + )} +
+
+ +
+
+ + +
document.getElementById("billAttachments").click()} + > + + + Click to select or click here to browse + + (PDF, JPG, PNG, max 5MB) + + { + onFileChange(e); + e.target.value = ""; + }} + /> +
+ {errors.billAttachments && ( + + {errors.billAttachments.message} + + )} + {Array.isArray(files) && files.length > 0 && ( + +
+ {files + .filter((file) => { + if (expenseToEdit) { + return file.isActive; + } + return true; + }) + .map((file, idx) => ( + +
+ + {file.fileName} + + + {file.fileSize ? formatFileSize(file.fileSize) : ""} + +
+ { + e.preventDefault(); + removeFile(expenseToEdit ? file.documentId : idx); + }} + > +
+ ))} +
+ )} + + {Array.isArray(errors.billAttachments) && + errors.billAttachments.map((fileError, index) => ( +
+ { + (fileError?.fileSize?.message || + fileError?.contentType?.message || + fileError?.base64Data?.message, + fileError?.documentId?.message) + } +
+ ))} +
+
+
+
+ ) +} + +export default ManagePaymentRequest diff --git a/src/pages/PaymentRequest/PaymentRequestPage.jsx b/src/pages/PaymentRequest/PaymentRequestPage.jsx index c3b1f979..9baea037 100644 --- a/src/pages/PaymentRequest/PaymentRequestPage.jsx +++ b/src/pages/PaymentRequest/PaymentRequestPage.jsx @@ -1,10 +1,73 @@ -import React from "react"; +import React, { useState } from "react"; import Breadcrumb from "../../components/common/Breadcrumb"; - +import GlobalModel from "../../components/common/GlobalModel"; +import ManagePaymentRequest from "../../components/PaymentRequest/ManagePaymentRequest"; const PaymentRequestPage = () => { + const [ManagePaymentRequestModal, setManagePaymentRequestModal] = useState({ + IsOpen: null, + expenseId: null, + }); + return (
- + {/* Breadcrumb */} + + + {/* Top Bar */} +
+
+
+
+ +
+ +
+ +
+
+
+
+ + {ManagePaymentRequestModal.IsOpen && ( + + setManagePaymentRequestModal({ IsOpen: null, expenseId: null }) + } + > + + setManagePaymentRequestModal({ IsOpen: null, expenseId: null }) + } + /> + + )} + + {/* Expense List Placeholder */} +
+
+

No Expense Data found

+
+
diff --git a/src/pages/PaymentRequest/PaymentRequestSchema.js b/src/pages/PaymentRequest/PaymentRequestSchema.js new file mode 100644 index 00000000..5d8b04cc --- /dev/null +++ b/src/pages/PaymentRequest/PaymentRequestSchema.js @@ -0,0 +1,78 @@ +import { z } from "zod"; + +export const PaymentRequestSchema = (expenseTypes) => { + return z + .object({ + projectId: z.string().min(1, { message: "Project is required" }), + expensesTypeId: z + .string() + .min(1, { message: "Expense type is required" }), + transactionDate: z.string().min(1, { message: "Date is required" }), + description: z.string().min(1, { message: "Description is required" }), + supplerName: z.string().min(1, { message: "Supplier name is required" }), + amount: z.coerce + .number({ + invalid_type_error: "Amount is required and must be a number", + }) + .min(1, "Amount must be Enter") + .refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), { + message: "Amount must have at most 2 decimal places", + }), + billAttachments: z + .array( + z.object({ + fileName: z.string().min(1, { message: "Filename is required" }), + base64Data: z.string().nullable(), + contentType: z + .string() + .refine((val) => ALLOWED_TYPES.includes(val), { + message: "Only PDF, PNG, JPG, or JPEG files are allowed", + }), + documentId: z.string().optional(), + fileSize: z.number().max(MAX_FILE_SIZE, { + message: "File size must be less than or equal to 5MB", + }), + description: z.string().optional(), + isActive: z.boolean().default(true), + }) + ) + .nonempty({ message: "At least one file attachment is required" }), + }) + .refine( + (data) => { + return ( + !data.projectId || (data.paidById && data.paidById.trim() !== "") + ); + }, + { + message: "Please select who paid (employee)", + path: ["paidById"], + } + ) + .superRefine((data, ctx) => { + const expenseType = expenseTypes.find( + (et) => et.id === data.expensesTypeId + ); + if ( + expenseType?.noOfPersonsRequired && + (!data.noOfPersons || data.noOfPersons < 1) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "No. of Persons is required and must be at least 1", + path: ["noOfPersons"], + }); + } + }); +}; + +export const defaultPaymentRequest = { + projectId: "", + expensesTypeId: "", + transactionDate: "", + description: "", + supplerName: "", + amount: "", + billAttachments: [], +}; +