Creating Form for Recurring Expense.

This commit is contained in:
Kartik Sharma 2025-11-04 15:42:00 +05:30
parent bede21aff2
commit 44a7749cd3
6 changed files with 723 additions and 0 deletions

View File

@ -0,0 +1,431 @@
import React, { useEffect, useState } from 'react'
import Label from '../common/Label';
import { useForm } from 'react-hook-form';
import { useExpenseCategory } from '../../hooks/masterHook/useMaster';
import DatePicker from '../common/DatePicker';
// import { useCreatePaymentRequest, usePaymentRequestDetail, useUpdatePaymentRequest } from '../../hooks/useExpense';
import { zodResolver } from '@hookform/resolvers/zod';
import { defaultRecurringExpense, PaymentRecurringExpense } from './RecurringExpenseSchema';
import { INR_CURRENCY_CODE } from '../../utils/constants';
import { useCurrencies, useProjectName } from '../../hooks/useProjects';
function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
const data = {}
const { projectNames, loading: projectLoading, error, isError: isProjectError,
} = useProjectName();
const { data: currencyData, isLoading: currencyLoading, isError: currencyError } = useCurrencies();
const {
ExpenseCategories,
loading: ExpenseLoading,
error: ExpenseError,
} = useExpenseCategory();
const schema = PaymentRecurringExpense();
const { register, control, watch, handleSubmit, setValue, reset, formState: { errors }, } = useForm({
resolver: zodResolver(schema),
defaultValues: defaultRecurringExpense,
});
const handleClose = () => {
reset();
closeModal();
};
// const { mutate: CreatePaymentRequest, isPending: createPending } = useCreatePaymentRequest(
// () => {
// handleClose();
// }
// );
// const { mutate: PaymentRequestUpdate, isPending } = useUpdatePaymentRequest(() =>
// handleClose()
// );
useEffect(() => {
if (requestToEdit && data) {
reset({
title: data.title || "",
description: data.description || "",
payee: data.payee || "",
notifyTo: data.notifyTo || "",
currencyId: data.currency.id || "",
amount: data.amount || "",
strikeDate: data.strikeDate?.slice(0, 10) || "",
projectId: data.project.id || "",
paymentBufferDays: data.paymentBufferDays || "",
numberOfIteration: data.numberOfIteration || "",
expenseCategoryId: data.expenseCategory.id || "",
statusId: data.statusId || "",
frequency: data.frequency || "",
isVariable: data.isVariable || false,
});
}
}, [data, reset]);
// console.log("Veer",data)
const onSubmit = (fromdata) => {
let payload = {
...fromdata,
// strikeDate: localToUtc(fromdata.strikeDate),
strikeDate: fromdata.strikeDate ? new Date(fromdata.strikeDate).toISOString() : null,
};
// if (requestToEdit) {
// const editPayload = { ...payload, id: data.id};
// PaymentRequestUpdate({ id: data.id, payload: editPayload });
// } else {
// CreatePaymentRequest(payload);
// }
console.log("Kartik", payload)
};
return (
<div className="container p-3">
<h5 className="m-0">
{requestToEdit ? "Update Payment Request " : "Create Payment Request"}
</h5>
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
{/* Project and Category */}
<div className="row my-2 text-start">
<div className="col-md-6">
<Label className="form-label" required>
Select Project
</Label>
<select
className="form-select form-select-sm"
{...register("projectId")}
>
<option value="">Select Project</option>
{projectLoading ? (
<option>Loading...</option>
) : (
projectNames?.map((project) => (
<option key={project.id} value={project.id}>
{project.name}
</option>
))
)}
</select>
{errors.projectId && (
<small className="danger-text">{errors.projectId.message}</small>
)}
</div>
<div className="col-md-6">
<Label htmlFor="expenseCategoryId" className="form-label" required>
Expense Category
</Label>
<select
className="form-select form-select-sm"
id="expenseCategoryId"
{...register("expenseCategoryId")}
>
<option value="" disabled>
Select Category
</option>
{ExpenseLoading ? (
<option disabled>Loading...</option>
) : (
ExpenseCategories?.map((expense) => (
<option key={expense.id} value={expense.id}>
{expense.name}
</option>
))
)}
</select>
{errors.expenseCategoryId && (
<small className="danger-text">
{errors.expenseCategoryId.message}
</small>
)}
</div>
</div>
{/* Title and Is Variable */}
<div className="row my-2 text-start">
<div className="col-md-6">
<Label htmlFor="title" className="form-label" required>
Title
</Label>
<input
type="text"
id="title"
className="form-control form-control-sm"
{...register("title")}
/>
{errors.title && (
<small className="danger-text">
{errors.title.message}
</small>
)}
</div>
<div className="col-md-6">
<Label htmlFor="isVariable" className="form-label" required>
Is Variable
</Label>
<select
id="isVariable"
className="form-select form-select-sm"
{...register("isVariable", {
setValueAs: (v) => v === "true" ? true : v === "false" ? false : false,
})}
>
<option value="false">False</option>
<option value="true">True</option>
</select>
{errors.isVariable && (
<small className="danger-text">{errors.isVariable.message}</small>
)}
</div>
</div>
{/* Date and Amount */}
<div className="row my-2 text-start">
<div className="col-md-6">
<Label htmlFor="strikeDate" className="form-label" required>
Strike Date
</Label>
<DatePicker
name="strikeDate"
control={control}
minDate={new Date()}
className='w-100'
/>
{errors.strikeDate && (
<small className="danger-text">
{errors.strikeDate.message}
</small>
)}
</div>
<div className="col-md-6">
<Label htmlFor="amount" className="form-label" required>
Amount
</Label>
<input
type="number"
id="amount"
className="form-control form-control-sm"
min="1"
step="0.01"
inputMode="decimal"
{...register("amount", { valueAsNumber: true })}
/>
{errors.amount && (
<small className="danger-text">{errors.amount.message}</small>
)}
</div>
</div>
{/* Payee and Currency */}
<div className="row my-2 text-start">
<div className="col-md-6">
<Label htmlFor="payee" className="form-label" required>
Payee (Supplier Name/Transporter Name/Other)
</Label>
<input
type="text"
id="payee"
className="form-control form-control-sm"
{...register("payee")}
/>
{errors.payee && (
<small className="danger-text">
{errors.payee.message}
</small>
)}
</div>
<div className="col-md-6">
<Label htmlFor="currencyId" className="form-label" required>
Currency
</Label>
<select
id="currencyId"
className="form-select form-select-sm"
{...register("currencyId")}
>
<option value="">Select Currency</option>
{currencyLoading && <option>Loading...</option>}
{!currencyLoading &&
!currencyError &&
currencyData?.map((currency) => (
<option key={currency.id} value={currency.id}>
{`${currency.currencyName} (${currency.symbol})`}
</option>
))}
</select>
{errors.currencyId && (
<small className="danger-text">{errors.currencyId.message}</small>
)}
</div>
</div>
{/* Notify To and Status Id */}
<div className="row my-2 text-start">
<div className="col-md-6">
<Label htmlFor="notifyTo" className="form-label" required>
Notify (E-mail)
</Label>
<input
type="text"
id="notifyTo"
className="form-control form-control-sm"
{...register("notifyTo")}
/>
{errors.notifyTo && (
<small className="danger-text">
{errors.notifyTo.message}
</small>
)}
</div>
<div className="col-md-6">
<Label htmlFor="statusId" className="form-label" required>
Status
</Label>
<select
id="statusId"
className="form-select form-select-sm"
{...register("statusId")}
>
<option value="">Select Currency</option>
{currencyLoading && <option>Loading...</option>}
{!currencyLoading &&
!currencyError &&
currencyData?.map((currency) => (
<option key={currency.id} value={currency.id}>
{`${currency.currencyName} (${currency.symbol})`}
</option>
))}
</select>
{errors.statusId && (
<small className="danger-text">{errors.statusId.message}</small>
)}
</div>
</div>
{/* Payment Buffer Days and Number of Iteration */}
<div className="row my-2 text-start">
<div className="col-md-6">
<Label htmlFor="paymentBufferDays" className="form-label" required>
Payment Buffer Days
</Label>
<input
type="number"
id="paymentBufferDays"
className="form-control form-control-sm"
min="0"
step="1"
{...register("paymentBufferDays", { valueAsNumber: true })}
/>
{errors.paymentBufferDays && (
<small className="danger-text">{errors.paymentBufferDays.message}</small>
)}
</div>
<div className="col-md-6">
<Label htmlFor="numberOfIteration" className="form-label" required>
Number of Iteration
</Label>
<input
type="number"
id="numberOfIteration"
className="form-control form-control-sm"
min="1"
step="1"
{...register("numberOfIteration", { valueAsNumber: true })}
/>
{errors.numberOfIteration && (
<small className="danger-text">{errors.numberOfIteration.message}</small>
)}
</div>
</div>
{/* Description */}
<div className="row my-2 text-start">
<div className="col-md-12">
<Label htmlFor="description" className="form-label" required>
Description
</Label>
<textarea
id="description"
className="form-control form-control-sm"
{...register("description")}
rows="2"
></textarea>
{errors.description && (
<small className="danger-text">
{errors.description.message}
</small>
)}
</div>
</div>
{/* <div className="d-flex justify-content-end gap-3">
<button
type="reset"
// disabled={createPending}
onClick={handleClose}
className="btn btn-label-secondary btn-sm mt-3"
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary btn-sm mt-3"
// disabled={createPending}
>
{createPending
? "Please Wait..."
: requestToEdit
? "Update"
: "Submit"}
</button>
</div> */}
<div className="d-flex justify-content-end gap-3">
<button
type="reset"
onClick={handleClose}
className="btn btn-label-secondary btn-sm mt-3"
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary btn-sm mt-3"
>
Submit
</button>
</div>
</form>
</div>
)
}
export default ManageRecurringExpense

View File

@ -0,0 +1,158 @@
import { boolean, z } from "zod";
import { INR_CURRENCY_CODE } from "../../utils/constants";
// export const PaymentRecurringExpense = (expenseTypes) => {
// return z
// .object({
// title: z.string().min(1, { message: "Project is required" }),
// description: z.string().min(1, { message: "Description is required" }),
// payee: z.string().min(1, { message: "Supplier name is required" }),
// notifyTo: z.string().min(1, { message: "Notification is required" }),
// currencyId: z
// .string()
// .min(1, { message: "Currency 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",
// }),
// strikeDate: z.string().min(1, { message: "Date is required" }),
// projectId: z.string().min(1, { message: "Project is required" }),
// paymentBufferDays: z.string().min(1, { message: "Buffer days is required" }),
// numberOfIteration: z.string().min(1, { message: "Iteration is required" }),
// expenseCategoryId: z
// .string()
// .min(1, { message: "Expense Category is required" }),
// statusId: z.string().min(1, { message: "Please select a status" }),
// frequency: z.string().min(1, { message: "Frequency is required" }),
// isVariable: z.boolean().optional(),
// })
// };
export const PaymentRecurringExpense = (expenseTypes) => {
return z.object({
title: z.string().min(1, { message: "Project is required" }),
description: z.string().min(1, { message: "Description is required" }),
payee: z.string().min(1, { message: "Supplier name is required" }),
notifyTo: z.string().min(1, { message: "Notification is required" }),
currencyId: z
.string()
.min(1, { message: "Currency is required" }),
amount: z
.number({
required_error: "Amount is required",
invalid_type_error: "Amount must be a number",
})
.min(1, { message: "Amount must be greater than 0" })
.refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), {
message: "Amount must have at most 2 decimal places",
}),
strikeDate: z
.string()
.min(1, { message: "Date is required" })
.refine((val) => !isNaN(Date.parse(val)), {
message: "Invalid date format",
}),
projectId: z
.string()
.min(1, { message: "Project is required" }),
paymentBufferDays: z
.number({
required_error: "Buffer days is required",
invalid_type_error: "Buffer days must be a number",
})
.min(0, { message: "Buffer days cannot be negative" }),
numberOfIteration: z
.number({
required_error: "Iteration is required",
invalid_type_error: "Iteration must be a number",
})
.min(1, { message: "Iteration must be at least 1" }),
expenseCategoryId: z
.string()
.min(1, { message: "Expense Category is required" }),
statusId: z
.string()
.min(1, { message: "Please select a status" }),
frequency: z
.number({
required_error: "Frequency is required",
invalid_type_error: "Frequency must be a number",
})
.min(1, { message: "Frequency must be greater than 0" }),
isVariable: z.boolean().optional(),
});
};
export const defaultRecurringExpense = {
title: "",
description: "",
payee: "",
notifyTo: "",
currencyId: "",
amount: 0,
strikeDate: "", // or null if your DatePicker accepts null
projectId: "",
paymentBufferDays: 0,
numberOfIteration: 1,
expenseCategoryId: "",
statusId: "",
frequency: 1,
isVariable: false,
};
// export const SearchPaymentRequestSchema = z.object({
// projectIds: z.array(z.string()).optional(),
// statusIds: z.array(z.string()).optional(),
// createdByIds: z.array(z.string()).optional(),
// currencyIds: z.array(z.string()).optional(),
// expenseCategoryIds: z.array(z.string()).optional(),
// payees: z.array(z.string()).optional(),
// startDate: z.string().optional(),
// endDate: z.string().optional(),
// });
// export const defaultPaymentRequestFilter = {
// projectIds: [],
// statusIds: [],
// createdByIds: [],
// currencyIds: [],
// expenseCategoryIds: [],
// payees: [],
// startDate: null,
// endDate: null,
// };

View File

@ -0,0 +1,132 @@
import React, { createContext, useState, useEffect, useContext } from "react";
import Breadcrumb from "../../components/common/Breadcrumb";
import GlobalModel from "../../components/common/GlobalModel";
import { useFab } from "../../Context/FabContext";
// import { defaultPaymentRequestFilter,SearchPaymentRequestSchema } from "../../components/PaymentRequest/PaymentRequestSchema";
import ManageRecurringExpense from "../../components/RecurringExpense/ManageRecurringExpense";
export const RecurringExpenseContext = createContext();
export const useRecurringExpenseContext = () => {
const context = useContext(RecurringExpenseContext);
if (!context) {
throw new Error("useRecurringExpenseContext must be used within an ExpenseProvider");
}
return context;
};
const RecurringExpensePage = () => {
const [ManageRequest, setManageRequest] = useState({
IsOpen: null,
RequestId: null,
});
const [ViewRequest,setVieRequest] = useState({view:false,requestId:null})
const { setOffcanvasContent, setShowTrigger } = useFab();
// const [filters, setFilters] = useState(defaultPaymentRequestFilter);
const [search, setSearch] = useState("");
const contextValue = {
setManageRequest,
setVieRequest
};
useEffect(() => {
setShowTrigger(true);
setOffcanvasContent(
"Payment Request Filters",
// <PaymentRequestFilterPanel onApply={setFilters} />
);
return () => {
setShowTrigger(false);
setOffcanvasContent("", null);
};
}, []);
return (
<RecurringExpenseContext.Provider value={contextValue}>
<div className="container-fluid">
{/* Breadcrumb */}
<Breadcrumb
data={[
{ label: "Home", link: "/" },
{ label: "Finance", link: "/Payment Request" },
{ label: "Payment Request" },
]}
/>
{/* Top Bar */}
<div className="card my-3 px-sm-4 px-0">
<div className="card-body py-2 px-3">
<div className="row align-items-center">
<div className="col-6">
<input
type="search"
className="form-control form-control-sm w-auto"
placeholder="Search Payment Req.."
value={search}
// onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className="col-6 text-end mt-2 mt-sm-0">
<button
className="btn btn-sm btn-primary"
type="button"
onClick={() =>
setManageRequest({
IsOpen: true,
expenseId: null,
})
}
>
<i className="bx bx-plus-circle me-2"></i>
<span className="d-none d-md-inline-block">
Add Payment Request
</span>
</button>
</div>
</div>
</div>
</div>
{/* <PaymentRequestList
search={search}
filters={filters}
/> */}
{/* Add/Edit Modal */}
{ManageRequest.IsOpen && (
<GlobalModel
isOpen
size="lg"
closeModal={() =>
setManageRequest({ IsOpen: null, expenseId: null })
}
>
<ManageRecurringExpense
key={ManageRequest.RequestId ?? "new"}
requestToEdit={ManageRequest.RequestId}
closeModal={() =>
setManageRequest({ IsOpen: null, RequestId: null })
}
/>
</GlobalModel>
)}
{/* {ViewRequest.view && (
<GlobalModel
isOpen
size="xl"
modalType="top"
closeModal={() => setVieRequest({ requestId: null, view: false })}
>
<ViewPaymentRequest requestId={ViewRequest?.requestId}/>
</GlobalModel>
)} */}
</div>
</RecurringExpenseContext.Provider>
);
};
export default RecurringExpensePage;

View File

@ -56,6 +56,7 @@ import { ComingSoonPage } from "../pages/Misc/ComingSoonPage";
import CollectionPage from "../pages/collections/CollectionPage";
import AdvancePaymentPage from "../pages/AdvancePayment/AdvancePaymentPage";
import PaymentRequestPage from "../pages/PaymentRequest/PaymentRequestPage";
import RecurringExpensePage from "../pages/RecurringExpense/RecurringExpensePage";
const router = createBrowserRouter(
[
{
@ -102,6 +103,7 @@ const router = createBrowserRouter(
{ path: "/expenses", element: <ExpensePage /> },
{ path: "/payment-request", element: <PaymentRequestPage /> },
{ path: "/advance-payment", element: <AdvancePaymentPage /> },
{ path: "/recurring-payment", element: <RecurringExpensePage /> },
{ path: "/masters", element: <MasterPage /> },
{ path: "/tenants", element: <TenantPage /> },
{ path: "/tenants/new-tenant", element: <CreateTenant /> },