From e7fcfdb1548910612f9dc6a54105b160676b6753 Mon Sep 17 00:00:00 2001 From: "pramod.mahajan" Date: Sat, 8 Nov 2025 16:07:09 +0530 Subject: [PATCH] added Advance Payment --- .../AdvancePayment/AdvancePaymentList.jsx | 234 ++++++++++++++++++ .../PaymentRequest/PaymentRequestSchema.js | 9 +- .../RecurringExpense/ViewRecurringExpense.jsx | 103 ++++---- .../AdvancePayment/AdvancePaymentPage.jsx | 96 +++++++ src/router/AppRoutes.jsx | 2 + src/utils/constants.jsx | 2 +- 6 files changed, 398 insertions(+), 48 deletions(-) create mode 100644 src/components/AdvancePayment/AdvancePaymentList.jsx create mode 100644 src/pages/AdvancePayment/AdvancePaymentPage.jsx diff --git a/src/components/AdvancePayment/AdvancePaymentList.jsx b/src/components/AdvancePayment/AdvancePaymentList.jsx new file mode 100644 index 00000000..e9113b48 --- /dev/null +++ b/src/components/AdvancePayment/AdvancePaymentList.jsx @@ -0,0 +1,234 @@ + +import React, { useEffect, useMemo } from "react"; +import { useExpenseTransactions } from "../../hooks/useExpense"; +import Error from "../common/Error"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import Loader, { SpinnerLoader } from "../common/Loader"; +import { useForm, useFormContext } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { employee } from "../../data/masters"; +import { useAdvancePaymentContext } from "../../pages/AdvancePayment/AdvancePaymentPage"; +import { formatFigure } from "../../utils/appUtils"; + +const AdvancePaymentList = ({ employeeId }) => { + const { setBalance } = useAdvancePaymentContext(); + const { data, isError, isLoading, error, isFetching } = + useExpenseTransactions(employeeId, { enabled: !!employeeId }); + + const records = Array.isArray(data) ? data : []; + + let currentBalance = 0; + const rowsWithBalance = records.map((r) => { + const isCredit = r.amount > 0; + const credit = isCredit ? r.amount : 0; + const debit = !isCredit ? Math.abs(r.amount) : 0; + currentBalance += credit - debit; + return { + id: r.id, + description: r.title || "-", + projectName: r.project?.name || "-", + createdAt: r.createdAt, + credit, + debit, + financeUId: r.financeUId, + balance: currentBalance, + }; + }); + + useEffect(() => { + if (!employeeId) { + setBalance(null); + return; + } + + if (rowsWithBalance.length > 0) { + setBalance(rowsWithBalance[rowsWithBalance.length - 1].balance); + } else { + setBalance(0); + } + }, [employeeId, data, setBalance]); + + if (!employeeId) { + return ( +
+

Please select an employee

+
+ ); + } + + if (isLoading || isFetching) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( +
+ {error?.status === 404 + ? "No advance payment transactions found." + : } +
+ ); + } + const columns = [ + { + key: "date", + label: ( + <> + Date + + ), + align: "text-start", + }, + { key: "description", label: "Description", align: "text-start" }, + + { + key: "credit", + label: ( + <> + Credit + + ), + align: "text-end", + }, + { + key: "debit", + label: ( + <> + Debit + + ), + align: "text-end", + }, + + { + key: "balance", + label: ( + <> + Balance + + ), + align: "text-end fw-bold", + }, + ]; + + // Handle empty records + if (rowsWithBalance.length === 0) { + return ( +
+ No advance payment records found. +
+ ); + } + const DecideCreditOrDebit = ({ financeUId }) => { + if (!financeUId) return null; + + const prefix = financeUId?.substring(0, 2).toUpperCase(); + + if (prefix === "PR") return +; + if (prefix === "EX") return -; + + return null; + }; + + return ( +
+ + + + {columns.map((col) => ( + + ))} + + + + {Array.isArray(data) && data.length > 0 ? ( + data.map((row) => ( + + {columns.map((col) => ( + + ))} + + )) + ) : ( + + + + )} + + + + + + + + +
+ {col.label} +
+ {col.key === "credit" ? ( + row.amount > 0 ? ( + {row.amount.toLocaleString("en-IN")} + ) : ( + "-" + ) + ) : col.key === "debit" ? ( + row.amount < 0 ? ( + + {Math.abs(row.amount).toLocaleString("en-IN")} + + ) : ( + "-" + ) + ) : col.key === "balance" ? ( +
+ {/* */} + + {formatFigure(row.currentBalance)} + +
+ ) : col.key === "date" ? ( + + {formatUTCToLocalTime(row.paidAt)} + + ) : ( +
+ + {row.project?.name || "-"} + + {row.title || "-"} +
+ )} +
+ No advance payment records found. +
+ {" "} +
+ Final Balance +
+
+
+ {currentBalance.toLocaleString("en-IN", { + style: "currency", + currency: "INR", + })} +
+
+
+ ); +}; + +export default AdvancePaymentList; diff --git a/src/components/PaymentRequest/PaymentRequestSchema.js b/src/components/PaymentRequest/PaymentRequestSchema.js index 175bf2ae..03ae49df 100644 --- a/src/components/PaymentRequest/PaymentRequestSchema.js +++ b/src/components/PaymentRequest/PaymentRequestSchema.js @@ -1,6 +1,6 @@ import { boolean, z } from "zod"; -import { INR_CURRENCY_CODE } from "../../utils/constants"; +import { CREATE_EXEPENSE, EXPENSE_DRAFT, EXPENSE_STATUS, INR_CURRENCY_CODE } from "../../utils/constants"; const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB const ALLOWED_TYPES = [ "application/pdf", @@ -228,3 +228,10 @@ export const DefaultRequestedExpense = { }; +export const STATUS_HEADING = { + [EXPENSE_STATUS.draft]: "Initiation", + [EXPENSE_STATUS.review_pending]: "Review & Validation", + [EXPENSE_STATUS.approve_pending]: "Approval", + [EXPENSE_STATUS.payment_pending]: "Processing & Disbursement", + [EXPENSE_STATUS.payment_processed]: "Payment Completed", +}; diff --git a/src/components/RecurringExpense/ViewRecurringExpense.jsx b/src/components/RecurringExpense/ViewRecurringExpense.jsx index 0c7a5327..a021d2db 100644 --- a/src/components/RecurringExpense/ViewRecurringExpense.jsx +++ b/src/components/RecurringExpense/ViewRecurringExpense.jsx @@ -1,24 +1,24 @@ -import React from 'react' -import { useRecurringExpenseDetail } from '../../hooks/useExpense'; -import { formatUTCToLocalTime } from '../../utils/dateUtils'; -import { formatFigure, getColorNameFromHex } from '../../utils/appUtils'; -import Avatar from '../common/Avatar'; -import { FREQUENCY_FOR_RECURRING } from '../../utils/constants'; -import { ExpenseDetailsSkeleton } from '../Expenses/ExpenseSkeleton'; +import React from "react"; +import { useRecurringExpenseDetail } from "../../hooks/useExpense"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import { formatFigure, getColorNameFromHex } from "../../utils/appUtils"; +import Avatar from "../common/Avatar"; +import { FREQUENCY_FOR_RECURRING } from "../../utils/constants"; +import { ExpenseDetailsSkeleton } from "../Expenses/ExpenseSkeleton"; const ViewRecurringExpense = ({ RecurringId }) => { - const { data, isLoading, isError, error, isFetching } = useRecurringExpenseDetail(RecurringId); + const { data, isLoading, isError, error, isFetching } = + useRecurringExpenseDetail(RecurringId); const statusColorMap = { - "da462422-13b2-45cc-a175-910a225f6fc8": "primary", // Active - "306856fb-5655-42eb-bf8b-808bb5e84725": "success", // Completed - "3ec864d2-8bf5-42fb-ba70-5090301dd816": "danger", // De-Activated - "8bfc9346-e092-4a80-acbf-515ae1ef6868": "warning", // Paused + "da462422-13b2-45cc-a175-910a225f6fc8": "primary", // Active + "306856fb-5655-42eb-bf8b-808bb5e84725": "success", // Completed + "3ec864d2-8bf5-42fb-ba70-5090301dd816": "danger", // De-Activated + "8bfc9346-e092-4a80-acbf-515ae1ef6868": "warning", // Paused }; if (isLoading) return ; return ( -
Recurring Payment Details
@@ -26,13 +26,14 @@ const ViewRecurringExpense = ({ RecurringId }) => {
{/*
*/}
- {/* Row 1 Recurring Id and Status */}
{data?.recurringPaymentUID} {data?.status?.name || "N/A"} @@ -48,12 +49,12 @@ const ViewRecurringExpense = ({ RecurringId }) => { > Expense Category : -
{data?.expenseCategory?.name || "N/A"}
+
+ {data?.expenseCategory?.name || "N/A"} +
- - {/* Row 3 Amount and Project */}
@@ -66,7 +67,9 @@ const ViewRecurringExpense = ({ RecurringId }) => {
{data?.amount != null - ? `${data?.currency?.symbol ?? "¥"} ${Number(data.amount).toFixed(2)} ${data?.currency?.currencyCode ?? "CN"}` + ? `${data?.currency?.symbol ?? "¥"} ${Number( + data.amount + ).toFixed(2)} ${data?.currency?.currencyCode ?? "CN"}` : "N/A"}
@@ -144,11 +147,11 @@ const ViewRecurringExpense = ({ RecurringId }) => {
{data?.notifyTo?.length > 0 ? data.notifyTo?.map((user, index) => ( - - {user.email} - {index < data?.notifyTo?.length - 1 && ", "} - - )) + + {user.email} + {index < data?.notifyTo?.length - 1 && ", "} + + )) : "N/A"}
@@ -201,7 +204,9 @@ const ViewRecurringExpense = ({ RecurringId }) => { > Payment Buffer Days : -
{data?.paymentBufferDays || "N/A"}
+
+ {data?.paymentBufferDays || "N/A"} +
@@ -231,11 +236,12 @@ const ViewRecurringExpense = ({ RecurringId }) => { > Number of Iteration : -
{data?.numberOfIteration || "N/A"}
+
+ {data?.numberOfIteration || "N/A"} +
- {/* Row 9 Created By and Updated By*/}
@@ -253,20 +259,22 @@ const ViewRecurringExpense = ({ RecurringId }) => { lastName={data?.createdBy?.lastName} /> - {`${data?.createdBy?.firstName ?? ""} ${data?.createdBy?.lastName ?? ""}`.trim() || "N/A"} + {`${data?.createdBy?.firstName ?? ""} ${ + data?.createdBy?.lastName ?? "" + }`.trim() || "N/A"}
-
-
- + {data?.updatedBy && ( +
+
+ - {data?.updatedBy ? ( <> { lastName={data.updatedBy.lastName} /> - {`${data.updatedBy.firstName ?? ""} ${data.updatedBy.lastName ?? ""}`.trim() || "N/A"} + {`${data.updatedBy.firstName ?? ""} ${ + data.updatedBy.lastName ?? "" + }`.trim() || "N/A"} - ) : ( - N/A - )} +
-
+ )} {/* Row 10 Description */} @@ -293,7 +301,10 @@ const ViewRecurringExpense = ({ RecurringId }) => { > Description : -
+
{data?.description || "N/A"}
@@ -301,7 +312,7 @@ const ViewRecurringExpense = ({ RecurringId }) => { {/*
*/}
- ) -} + ); +}; -export default ViewRecurringExpense \ No newline at end of file +export default ViewRecurringExpense; diff --git a/src/pages/AdvancePayment/AdvancePaymentPage.jsx b/src/pages/AdvancePayment/AdvancePaymentPage.jsx new file mode 100644 index 00000000..bc2125ef --- /dev/null +++ b/src/pages/AdvancePayment/AdvancePaymentPage.jsx @@ -0,0 +1,96 @@ +import React, { + createContext, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import Breadcrumb from "../../components/common/Breadcrumb"; +import { useEmployee } from "../../hooks/useEmployees"; +import EmployeeSearchInput from "../../components/common/EmployeeSearchInput"; +import { useForm } from "react-hook-form"; +import Label from "../../components/common/Label"; +import AdvancePaymentList from "../../components/AdvancePayment/AdvancePaymentList"; +import { employee } from "../../data/masters"; +import { formatFigure } from "../../utils/appUtils"; + +export const AdvancePaymentContext = createContext(); +export const useAdvancePaymentContext = () => { + const context = useContext(AdvancePaymentContext); + if (!context) { + throw new Error( + "useAdvancePaymentContext must be used within an AdvancePaymentProvider" + ); + } + return context; +}; +const AdvancePaymentPage = () => { + const [balance, setBalance] = useState(null); + const {control, reset, watch } = useForm({ + defaultValues: { + employeeId: "", + }, + }); + + const selectedEmployeeId = watch("employeeId"); + useEffect(() => { + const selectedEmpoyee = sessionStorage.getItem("transaction-empId"); + reset({ + employeeId: selectedEmpoyee || "", + }); + }, [reset]); + + + return ( + +
+ +
+
+
+
+ +
+
+
+ {balance ? ( + <> + + 0 ? "text-success" : "text-danger" + } fs-5 fw-bold ms-1`} + > + {formatFigure(balance, { + type: "currency", + currency: "INR", + })} + + + ) : ( + <> + )} +
+
+ + + +
+
+
+ ); +}; + +export default AdvancePaymentPage; diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index 5a15d60c..185ac564 100644 --- a/src/router/AppRoutes.jsx +++ b/src/router/AppRoutes.jsx @@ -58,6 +58,7 @@ import SubscriptionSummary from "../pages/Home/SubscriptionSummary"; import MakeSubscription from "../pages/Home/MakeSubscription"; import PaymentRequestPage from "../pages/PaymentRequest/PaymentRequestPage"; import RecurringExpensePage from "../pages/RecurringExpense/RecurringExpensePage"; +import AdvancePaymentPage from "../pages/AdvancePayment/AdvancePaymentPage"; const router = createBrowserRouter( [ { @@ -110,6 +111,7 @@ const router = createBrowserRouter( { path: "/expenses", element: }, { path: "/payment-request", element: }, { path: "/recurring-payment", element: }, + { path: "/advance-payment", element: }, { path: "/collection", element: }, { path: "/masters", element: }, { path: "/tenants", element: }, diff --git a/src/utils/constants.jsx b/src/utils/constants.jsx index 57f5d4ed..e27bc393 100644 --- a/src/utils/constants.jsx +++ b/src/utils/constants.jsx @@ -163,7 +163,7 @@ export const EXPENSE_STATUS = { review_pending: "6537018f-f4e9-4cb3-a210-6c3b2da999d7", payment_pending: "f18c5cfd-7815-4341-8da2-2c2d65778e27", approve_pending: "4068007f-c92f-4f37-a907-bc15fe57d4d8", - + payment_processed:"61578360-3a49-4c34-8604-7b35a3787b95", } export const UUID_REGEX =