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"}
+
+
+
+ )
+}
+
+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: [],
+};
+