From 2109a5f1f1949b301e024cd696e7f4c2bbe25078 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Wed, 26 Nov 2025 10:12:58 +0530 Subject: [PATCH 01/18] =?UTF-8?q?=E2=80=9CCreate=20Job=E2=80=9D=20Popup=20?= =?UTF-8?q?Should=20Close=20Automatically=20After=20Job=20Creation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ServiceProject/ServiceProjectJob/ManageJob.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ServiceProject/ServiceProjectJob/ManageJob.jsx b/src/components/ServiceProject/ServiceProjectJob/ManageJob.jsx index cc2066e0..94ced5c3 100644 --- a/src/components/ServiceProject/ServiceProjectJob/ManageJob.jsx +++ b/src/components/ServiceProject/ServiceProjectJob/ManageJob.jsx @@ -140,6 +140,7 @@ const ManageJob = ({ Job }) => { formData.projectId = projectId; CreateJob(formData); + setManageJob({ isOpen: false, jobId: null }); } }; From c975e543316ff36b9d896120e292ba0ca98cbee9 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Wed, 26 Nov 2025 10:33:04 +0530 Subject: [PATCH 02/18] Logs Tab Action Redirects Back to Page 1 Automatically --- src/components/Activities/AttendcesLogs.jsx | 28 +++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/Activities/AttendcesLogs.jsx b/src/components/Activities/AttendcesLogs.jsx index f1d8e008..f55a4965 100644 --- a/src/components/Activities/AttendcesLogs.jsx +++ b/src/components/Activities/AttendcesLogs.jsx @@ -20,14 +20,21 @@ import { SpinnerLoader } from "../common/Loader"; const usePagination = (data, itemsPerPage) => { const [currentPage, setCurrentPage] = useState(1); - const maxPage = Math.ceil(data.length / itemsPerPage); + // const maxPage = Math.ceil(data.length / itemsPerPage); + const maxPage = Math.max(1, Math.ceil(data.length / itemsPerPage)); const currentItems = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; return data.slice(startIndex, endIndex); }, [data, currentPage, itemsPerPage]); - const paginate = useCallback((pageNumber) => setCurrentPage(pageNumber), []); + // const paginate = useCallback((pageNumber) => setCurrentPage(pageNumber), []); + + const paginate = useCallback((pageNumber) => { + // keep page within 1..maxPage + const p = Math.max(1, Math.min(pageNumber, maxPage)); + setCurrentPage(p); + }, [maxPage]); const resetPage = useCallback(() => setCurrentPage(1), []); return { @@ -36,6 +43,7 @@ const usePagination = (data, itemsPerPage) => { currentItems, paginate, resetPage, + setCurrentPage, }; }; @@ -125,9 +133,16 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { resetPage, } = usePagination(filteredSearchData, 20); + // useEffect(() => { + // resetPage(); + // }, [filteredSearchData]); + useEffect(() => { - resetPage(); - }, [filteredSearchData]); + if (currentPage > totalPages) { + paginate(totalPages || 1); + } + // NOTE: do NOT force reset to page 1 here — keep the same page if still valid + }, [filteredSearchData, totalPages, currentPage, paginate]); const handler = useCallback( (msg) => { @@ -144,10 +159,9 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { record.id === msg.response.id ? { ...record, ...msg.response } : record ); }); - resetPage(); } }, - [selectedProject, dateRange, resetPage] + [selectedProject, dateRange] ); useEffect(() => { @@ -214,7 +228,7 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { {isLoading ? (
From 9c02a4a92510117428a679b63541141f14bc6c8f Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Wed, 26 Nov 2025 10:49:57 +0530 Subject: [PATCH 03/18] Increase the size of date picker in OrganizationInfo at tenant and Page is not Refresh at Create form. --- src/components/Tenant/OrganizationInfo.jsx | 16 ++++++++-------- src/hooks/useTenant.js | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/Tenant/OrganizationInfo.jsx b/src/components/Tenant/OrganizationInfo.jsx index 1e166423..9cfdcb7f 100644 --- a/src/components/Tenant/OrganizationInfo.jsx +++ b/src/components/Tenant/OrganizationInfo.jsx @@ -58,12 +58,12 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => { }; useEffect(() => { - const logoImage = getValues("logoImage"); - if (logoImage) { - setLogoPreview(logoImage); - setLogoName("Uploaded Logo"); - } -}, [getValues]); + const logoImage = getValues("logoImage"); + if (logoImage) { + setLogoPreview(logoImage); + setLogoName("Uploaded Logo"); + } + }, [getValues]); return ( @@ -134,7 +134,7 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => { control={control} placeholder="DD-MM-YYYY" maxDate={new Date()} - className={errors.onBoardingDate ? "is-invalid" : ""} + className={`w-100 ${errors.onBoardingDate ? "is-invalid" : ""}`} /> {errors.onBoardingDate && (
@@ -210,7 +210,7 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
- { onSuccess: (data, variables) => { const { tenantId } = variables; showToast("Tenant Plan Added SuccessFully", "success"); - queryClient.invalidateQueries({ queryKey: ["Tenant", tenantId] }); + queryClient.invalidateQueries({ queryKey: ["Tenants", tenantId] }); if (onSuccessCallback) onSuccessCallback(); }, onError: (error) => { From da13e40fd53db8fbe5839a015dd25801b7250271 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Wed, 26 Nov 2025 11:12:53 +0530 Subject: [PATCH 04/18] =?UTF-8?q?Service=20title=20and=20group=20text=20ap?= =?UTF-8?q?pear=20too=20bold=20in=20Masters=20=E2=86=92=20Services?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/master/Services/ServicesGroups.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/master/Services/ServicesGroups.jsx b/src/components/master/Services/ServicesGroups.jsx index 4cc94648..a89235ae 100644 --- a/src/components/master/Services/ServicesGroups.jsx +++ b/src/components/master/Services/ServicesGroups.jsx @@ -109,7 +109,7 @@ const ServiceGroups = ({ service }) => { /> )} -

{group.name}

+

{group.name}

From e8f6298f930af262a1370cd3f8054ca9aeabcab7 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Wed, 26 Nov 2025 12:35:21 +0530 Subject: [PATCH 05/18] =?UTF-8?q?Clicking=20on=20Contact=20Name=20in=20Not?= =?UTF-8?q?es=20Should=20Redirect=20to=20That=20Contact=E2=80=99s=20Detail?= =?UTF-8?q?=20Page=20in=20Directory=20(Web)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Directory/NoteCardDirectoryEditable.jsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/Directory/NoteCardDirectoryEditable.jsx b/src/components/Directory/NoteCardDirectoryEditable.jsx index bc6ff517..49eba918 100644 --- a/src/components/Directory/NoteCardDirectoryEditable.jsx +++ b/src/components/Directory/NoteCardDirectoryEditable.jsx @@ -9,6 +9,7 @@ import ConfirmModal from "../common/ConfirmModal"; // Make sure path is correct import "../common/TextEditor/Editor.css"; import GlobalModel from "../common/GlobalModel"; import { useActiveInActiveNote, useUpdateNote } from "../../hooks/useDirectory"; +import { useDirectoryContext } from "../../pages/Directory/DirectoryPage"; const NoteCardDirectoryEditable = ({ noteItem, @@ -22,14 +23,14 @@ const NoteCardDirectoryEditable = ({ const [isDeleting, setIsDeleting] = useState(false); const [isRestoring, setIsRestoring] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [open_contact, setOpen_contact] = useState(null); - const [isOpenModalNote, setIsOpenModalNote] = useState(false); const { mutate: UpdateNote, isPending: isUpatingNote } = useUpdateNote(() => setEditing(false) ); const { mutate: ActiveInactive, isPending: isUpdatingStatus } = useActiveInActiveNote(() => setIsDeleteModalOpen(false)); + const { setContactOpen } = useDirectoryContext(); + const handleUpdateNote = async () => { const payload = { @@ -45,12 +46,6 @@ const NoteCardDirectoryEditable = ({ ActiveInactive({ noteId: noteItem.id, noteStatus: !noteItem.isActive }); }; - const contactProfile = (contactId) => { - DirectoryRepository.GetContactProfile(contactId).then((res) => { - setOpen_contact(res?.data); - setIsOpenModalNote(true); - }); - }; const handleRestore = async () => { try { @@ -88,7 +83,9 @@ const NoteCardDirectoryEditable = ({
contactProfile(noteItem.contactId)} + onClick={() => + setContactOpen({ contact: { id: noteItem.contactId }, Open: true }) + } > {noteItem?.contactName} {" "} @@ -97,6 +94,7 @@ const NoteCardDirectoryEditable = ({
+
From 833cb98dd31ca0e407419010b9c34fe33ccedd62 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Wed, 26 Nov 2025 16:51:01 +0530 Subject: [PATCH 06/18] Change the Name of AdvancePayment Component. --- .../AdvancePayment/AdvancePaymentList.jsx | 313 +++++------------- .../AdvancePayment/AdvancePaymentList1.jsx | 100 ------ .../AdvancePaymentListDetails.jsx | 233 +++++++++++++ .../AdvancePayment/AdvancePaymentPage.jsx | 130 ++------ .../AdvancePayment/AdvancePaymentPage1.jsx | 34 -- .../AdvancePaymentPageDetails.jsx | 108 ++++++ src/router/AppRoutes.jsx | 13 +- 7 files changed, 467 insertions(+), 464 deletions(-) delete mode 100644 src/components/AdvancePayment/AdvancePaymentList1.jsx create mode 100644 src/components/AdvancePayment/AdvancePaymentListDetails.jsx delete mode 100644 src/pages/AdvancePayment/AdvancePaymentPage1.jsx create mode 100644 src/pages/AdvancePayment/AdvancePaymentPageDetails.jsx diff --git a/src/components/AdvancePayment/AdvancePaymentList.jsx b/src/components/AdvancePayment/AdvancePaymentList.jsx index c6e9d004..39cf5d46 100644 --- a/src/components/AdvancePayment/AdvancePaymentList.jsx +++ b/src/components/AdvancePayment/AdvancePaymentList.jsx @@ -1,233 +1,100 @@ +import React from 'react' +import Avatar from "../common/Avatar"; // <-- ADD THIS +import { useExpenseAllTransactionsList } from '../../hooks/useExpense'; +import { useNavigate } from 'react-router-dom'; +import { formatFigure } from '../../utils/appUtils'; -import React, { useEffect, useMemo } from "react"; -import { useExpenseAllTransactionsList, 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 = ({ searchString }) => { -const AdvancePaymentList = ({ employeeId, searchString }) => { - const { setBalance } = useAdvancePaymentContext(); - const { data, isError, isLoading, error, isFetching } = - useExpenseTransactions(employeeId, { enabled: !!employeeId }); - const records = Array.isArray(data) ? data : []; + const { data, isError, isLoading, error } = + useExpenseAllTransactionsList(searchString); - 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, - }; - }); + const rows = data || []; + const navigate = useNavigate(); - useEffect(() => { - if (!employeeId) { - setBalance(null); - return; - } + const columns = [ + { + key: "employee", + label: "Employee Name", + align: "text-start", + customRender: (r) => ( +
navigate(`/advance-payment/${r.id}`)} + style={{ cursor: "pointer" }}> + - if (rowsWithBalance.length > 0) { - setBalance(rowsWithBalance[rowsWithBalance.length - 1].balance); - } else { - setBalance(0); - } - }, [employeeId, data, setBalance]); + + {r.firstName} {r.lastName} + +
+ ), + }, + { + key: "jobRoleName", + label: "Job Role", + align: "text-start", + customRender: (r) => ( + + {r.jobRoleName} + + ), + }, + { + key: "balanceAmount", + label: "Balance (₹)", + align: "text-end", + customRender: (r) => ( + + {formatFigure(r.balanceAmount, { + // type: "currency", + currency: "INR", + })} + + ), + }, + ]; + + if (isLoading) return

Loading...

; + if (isError) return

{error.message}

; - if (!employeeId) { return ( -
-

Please select an employee

-
- ); - } +
+
+ + + + {columns.map((col) => ( + + ))} + + - 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 ( -
-
+ {col.label} +
- - - {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", - })} -
-
-
- ); -}; + + {rows.length > 0 ? ( + rows.map((row) => ( + + {columns.map((col) => ( + + {col.customRender + ? col.customRender(row) + : col.getValue(row)} + + ))} + + )) + ) : ( + + + No Employees Found + + + )} + + +
+
+ ) +} export default AdvancePaymentList; diff --git a/src/components/AdvancePayment/AdvancePaymentList1.jsx b/src/components/AdvancePayment/AdvancePaymentList1.jsx deleted file mode 100644 index e17cd1b1..00000000 --- a/src/components/AdvancePayment/AdvancePaymentList1.jsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react' -import Avatar from "../../components/common/Avatar"; // <-- ADD THIS -import { useExpenseAllTransactionsList } from '../../hooks/useExpense'; -import { useNavigate } from 'react-router-dom'; -import { formatFigure } from '../../utils/appUtils'; - -const AdvancePaymentList1 = ({ searchString }) => { - - const { data, isError, isLoading, error } = - useExpenseAllTransactionsList(searchString); - - const rows = data || []; - const navigate = useNavigate(); - - const columns = [ - { - key: "employee", - label: "Employee Name", - align: "text-start", - customRender: (r) => ( -
navigate(`/advance-payment/${r.id}`)} - style={{ cursor: "pointer" }}> - - - - {r.firstName} {r.lastName} - -
- ), - }, - { - key: "jobRoleName", - label: "Job Role", - align: "text-start", - customRender: (r) => ( - - {r.jobRoleName} - - ), - }, - { - key: "balanceAmount", - label: "Balance (₹)", - align: "text-end", - customRender: (r) => ( - - {formatFigure(r.balanceAmount, { - // type: "currency", - currency: "INR", - })} - - ), - }, - ]; - - if (isLoading) return

Loading...

; - if (isError) return

{error.message}

; - - return ( -
-
- - - - {columns.map((col) => ( - - ))} - - - - - {rows.length > 0 ? ( - rows.map((row) => ( - - {columns.map((col) => ( - - ))} - - )) - ) : ( - - - - )} - -
- {col.label} -
- {col.customRender - ? col.customRender(row) - : col.getValue(row)} -
- No Employees Found -
-
-
- ) -} - -export default AdvancePaymentList1; diff --git a/src/components/AdvancePayment/AdvancePaymentListDetails.jsx b/src/components/AdvancePayment/AdvancePaymentListDetails.jsx new file mode 100644 index 00000000..f6af63f7 --- /dev/null +++ b/src/components/AdvancePayment/AdvancePaymentListDetails.jsx @@ -0,0 +1,233 @@ + +import React, { useEffect, useMemo } from "react"; +import { useExpenseAllTransactionsList, 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/AdvancePaymentPageDetails"; // All data +import { formatFigure } from "../../utils/appUtils"; + +const AdvancePaymentListDetails = ({ employeeId, searchString }) => { + 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 AdvancePaymentListDetails; diff --git a/src/pages/AdvancePayment/AdvancePaymentPage.jsx b/src/pages/AdvancePayment/AdvancePaymentPage.jsx index f95f199a..c634dca5 100644 --- a/src/pages/AdvancePayment/AdvancePaymentPage.jsx +++ b/src/pages/AdvancePayment/AdvancePaymentPage.jsx @@ -1,108 +1,34 @@ -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"; -import { useParams } from "react-router-dom"; -import { useExpenseTransactions } from "../../hooks/useExpense"; +import React from 'react' +import Breadcrumb from '../../components/common/Breadcrumb' +import AdvancePaymentList1 from '../../components/AdvancePayment/AdvancePaymentList' +import { useForm } from 'react-hook-form'; +import EmployeeSearchInput from '../../components/common/EmployeeSearchInput'; -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 { employeeId } = useParams(); - - const { data: transactionData } = useExpenseTransactions(employeeId, { - enabled: !!employeeId - }); - - const employeeName = useMemo(() => { - if (Array.isArray(transactionData) && transactionData.length > 0) { - const emp = transactionData[0].employee; - if (emp) return `${emp.firstName} ${emp.lastName}`; - } - return ""; - }, [transactionData]); - - const [balance, setBalance] = useState(null); - const { control, reset, watch } = useForm({ - defaultValues: { - employeeId: employeeId || "", - searchString: "", - }, - }); - - const selectedEmployeeId = employeeId || watch("employeeId"); - - const searchString = watch("searchString"); - - useEffect(() => { - const selectedEmpoyee = sessionStorage.getItem("transaction-empId"); - reset({ - employeeId: selectedEmpoyee || "", + const { control, reset, watch } = useForm({ + defaultValues: { + searchString: "", + }, }); - }, [reset]); + const searchString = watch("searchString"); + + return ( +
+ +
+
+ +
+
- return ( - -
- - -
-
-
- {balance ? ( - <> - - 0 ? "text-success" : "text-danger" - } fs-5 fw-bold ms-1`} - > - {balance > 0 ? ( - - ) : ( - - )}{" "} - {formatFigure(balance, { - type: "currency", - currency: "INR", - })} - - - ) : ( - <> - )} -
-
-
-
-
- ); -}; + ) +} -export default AdvancePaymentPage; +export default AdvancePaymentPage diff --git a/src/pages/AdvancePayment/AdvancePaymentPage1.jsx b/src/pages/AdvancePayment/AdvancePaymentPage1.jsx deleted file mode 100644 index 4ba3721b..00000000 --- a/src/pages/AdvancePayment/AdvancePaymentPage1.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import Breadcrumb from '../../components/common/Breadcrumb' -import AdvancePaymentList1 from '../../components/AdvancePayment/AdvancePaymentList1' -import { useForm } from 'react-hook-form'; -import EmployeeSearchInput from '../../components/common/EmployeeSearchInput'; - -const AdvancePaymentPage1 = () => { - const { control, reset, watch } = useForm({ - defaultValues: { - searchString: "", - }, - }); - const searchString = watch("searchString"); - - return ( -
- -
-
- -
-
- -
- ) -} - -export default AdvancePaymentPage1 diff --git a/src/pages/AdvancePayment/AdvancePaymentPageDetails.jsx b/src/pages/AdvancePayment/AdvancePaymentPageDetails.jsx new file mode 100644 index 00000000..e1fa3cac --- /dev/null +++ b/src/pages/AdvancePayment/AdvancePaymentPageDetails.jsx @@ -0,0 +1,108 @@ +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/AdvancePaymentListDetails"; +import { employee } from "../../data/masters"; +import { formatFigure } from "../../utils/appUtils"; +import { useParams } from "react-router-dom"; +import { useExpenseTransactions } from "../../hooks/useExpense"; + +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 AdvancePaymentPageDetails = () => { + const { employeeId } = useParams(); + + const { data: transactionData } = useExpenseTransactions(employeeId, { + enabled: !!employeeId + }); + + const employeeName = useMemo(() => { + if (Array.isArray(transactionData) && transactionData.length > 0) { + const emp = transactionData[0].employee; + if (emp) return `${emp.firstName} ${emp.lastName}`; + } + return ""; + }, [transactionData]); + + const [balance, setBalance] = useState(null); + const { control, reset, watch } = useForm({ + defaultValues: { + employeeId: employeeId || "", + searchString: "", + }, + }); + + const selectedEmployeeId = employeeId || watch("employeeId"); + + const searchString = watch("searchString"); + + useEffect(() => { + const selectedEmpoyee = sessionStorage.getItem("transaction-empId"); + reset({ + employeeId: selectedEmpoyee || "", + }); + }, [reset]); + + return ( + +
+ + +
+
+
+ {balance ? ( + <> + + 0 ? "text-success" : "text-danger" + } fs-5 fw-bold ms-1`} + > + {balance > 0 ? ( + + ) : ( + + )}{" "} + {formatFigure(balance, { + type: "currency", + currency: "INR", + })} + + + ) : ( + <> + )} +
+
+ +
+
+
+ ); +}; + +export default AdvancePaymentPageDetails; diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index dc556a6e..704eee01 100644 --- a/src/router/AppRoutes.jsx +++ b/src/router/AppRoutes.jsx @@ -58,10 +58,10 @@ 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"; import ServiceProjectDetail from "../pages/ServiceProject/ServiceProjectDetail"; import ManageJob from "../components/ServiceProject/ServiceProjectJob/ManageJob"; -import AdvancePaymentPage1 from "../pages/AdvancePayment/AdvancePaymentPage1"; +import AdvancePaymentPageDetails from "../pages/AdvancePayment/AdvancePaymentPageDetails"; +import AdvancePaymentPage from "../pages/AdvancePayment/AdvancePaymentPage"; const router = createBrowserRouter( [ { @@ -97,7 +97,7 @@ const router = createBrowserRouter( { path: "/projects/details", element: }, { path: "/project/manage/:projectId", element: }, { path: "/service-projects/:projectId", element: }, - {path:"/service/job",element:}, + { path: "/service/job", element: }, { path: "/employees", element: }, { path: "/employee/:employeeId", element: }, @@ -117,8 +117,11 @@ const router = createBrowserRouter( { path: "/expenses", element: }, { path: "/payment-request", element: }, { path: "/recurring-payment", element: }, - { path: "/advance-payment", element: }, - { path: "/advance-payment/:employeeId", element: }, + // { path: "/advance-payment", element: }, + // { path: "/advance-payment/:employeeId", element: }, + + { path: "/advance-payment", element: }, + { path: "/advance-payment/:employeeId", element: }, { path: "/collection", element: }, { path: "/masters", element: }, { path: "/tenants", element: }, From 7b1ad80d7809b24965c0b5d79ca1fadb89df550b Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Wed, 26 Nov 2025 16:56:01 +0530 Subject: [PATCH 07/18] Increase the gap for Advance Payment rows. --- src/components/AdvancePayment/AdvancePaymentList.jsx | 2 +- src/pages/AdvancePayment/AdvancePaymentPageDetails.jsx | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/AdvancePayment/AdvancePaymentList.jsx b/src/components/AdvancePayment/AdvancePaymentList.jsx index 39cf5d46..54031c43 100644 --- a/src/components/AdvancePayment/AdvancePaymentList.jsx +++ b/src/components/AdvancePayment/AdvancePaymentList.jsx @@ -73,7 +73,7 @@ const AdvancePaymentList = ({ searchString }) => { {rows.length > 0 ? ( rows.map((row) => ( - + {columns.map((col) => ( {col.customRender diff --git a/src/pages/AdvancePayment/AdvancePaymentPageDetails.jsx b/src/pages/AdvancePayment/AdvancePaymentPageDetails.jsx index e1fa3cac..6f6d3240 100644 --- a/src/pages/AdvancePayment/AdvancePaymentPageDetails.jsx +++ b/src/pages/AdvancePayment/AdvancePaymentPageDetails.jsx @@ -79,14 +79,9 @@ const AdvancePaymentPageDetails = () => { <> 0 ? "text-success" : "text-danger" - } fs-5 fw-bold ms-1`} + className={`${balance > 0 ? "text-success" : "text-danger"} fs-5 fw-bold ms-1`} > - {balance > 0 ? ( - - ) : ( - - )}{" "} + {balance > 0 && }{" "} {formatFigure(balance, { type: "currency", currency: "INR", @@ -96,6 +91,7 @@ const AdvancePaymentPageDetails = () => { ) : ( <> )} +
From 3072354ed25028d65aaeb429533185b131cd44cb Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Thu, 27 Nov 2025 10:16:25 +0530 Subject: [PATCH 08/18] Adding Export Functionality in Advance Payment and adding spinner on it. --- .../AdvancePayment/AdvancePaymentList.jsx | 10 ++- .../AdvancePaymentListDetails.jsx | 6 +- .../handleAdvancePaymentExport.jsx | 76 +++++++++++++++++++ .../AdvancePaymentPageDetails.jsx | 71 +++++++++++++---- src/utils/tableExportUtils.jsx | 71 +++++++++-------- 5 files changed, 187 insertions(+), 47 deletions(-) create mode 100644 src/components/AdvancePayment/handleAdvancePaymentExport.jsx diff --git a/src/components/AdvancePayment/AdvancePaymentList.jsx b/src/components/AdvancePayment/AdvancePaymentList.jsx index 54031c43..8d2d131d 100644 --- a/src/components/AdvancePayment/AdvancePaymentList.jsx +++ b/src/components/AdvancePayment/AdvancePaymentList.jsx @@ -3,6 +3,7 @@ import Avatar from "../common/Avatar"; // <-- ADD THIS import { useExpenseAllTransactionsList } from '../../hooks/useExpense'; import { useNavigate } from 'react-router-dom'; import { formatFigure } from '../../utils/appUtils'; +import { SpinnerLoader } from '../common/Loader'; const AdvancePaymentList = ({ searchString }) => { @@ -53,7 +54,14 @@ const AdvancePaymentList = ({ searchString }) => { }, ]; - if (isLoading) return

Loading...

; + if (isLoading) { + return ( +
+ +
+ ); + } + if (isError) return

{error.message}

; return ( diff --git a/src/components/AdvancePayment/AdvancePaymentListDetails.jsx b/src/components/AdvancePayment/AdvancePaymentListDetails.jsx index f6af63f7..c3800beb 100644 --- a/src/components/AdvancePayment/AdvancePaymentListDetails.jsx +++ b/src/components/AdvancePayment/AdvancePaymentListDetails.jsx @@ -8,10 +8,10 @@ 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/AdvancePaymentPageDetails"; // All data +import { useAdvancePaymentContext } from "../../pages/AdvancePayment/AdvancePaymentPageDetails"; import { formatFigure } from "../../utils/appUtils"; -const AdvancePaymentListDetails = ({ employeeId, searchString }) => { +const AdvancePaymentListDetails = ({ employeeId, searchString,tableRef }) => { const { setBalance } = useAdvancePaymentContext(); const { data, isError, isLoading, error, isFetching } = useExpenseTransactions(employeeId, { enabled: !!employeeId }); @@ -142,7 +142,7 @@ const AdvancePaymentListDetails = ({ employeeId, searchString }) => { return (
- +
{columns.map((col) => ( diff --git a/src/components/AdvancePayment/handleAdvancePaymentExport.jsx b/src/components/AdvancePayment/handleAdvancePaymentExport.jsx new file mode 100644 index 00000000..13be8501 --- /dev/null +++ b/src/components/AdvancePayment/handleAdvancePaymentExport.jsx @@ -0,0 +1,76 @@ +import moment from "moment"; +import { exportToCSV, exportToExcel, exportToPDF, printTable } from "../../utils/tableExportUtils"; + +const handleAdvancePaymentExport = (type, data, tableRef) => { + if (!data || data.length === 0) return; + + let currentBalance = 0; + const exportData = data.map((item) => { + const credit = item.amount > 0 ? item.amount : 0; + const debit = item.amount < 0 ? Math.abs(item.amount) : 0; + currentBalance += credit - debit; + + return { + Date: item.createdAt ? moment(item.createdAt).format("DD-MMM-YYYY") : "", + Description: item.title || "-", // used only for CSV/Excel + Project: item.project?.name || "-", + Credit: credit || "", + Debit: debit || "", + "Finance ID": item.financeUId || "-", + Balance: currentBalance, + }; + }); + + // Final row + exportData.push({ + Date: "", + Description: "Final Balance", + Project: "", + Credit: "", + Debit: "", + "Finance ID": "", + Balance: currentBalance, + }); + + switch (type) { + case "csv": + exportToCSV(exportData, "advance-payments"); + break; + + case "excel": + exportToExcel(exportData, "advance-payments"); + break; + + case "pdf": + // Create a copy of data ONLY for PDF (without Description) + const pdfData = exportData.map((row, index) => { + // Detect final row + const isFinal = index === exportData.length - 1; + + return { + Date: isFinal ? "" : row.Date, + Project: isFinal ? "Final Balance" : row.Project, + Credit: row.Credit, + Debit: row.Debit, + "Finance ID": row["Finance ID"], + Balance: row.Balance, + }; + }); + + exportToPDF( + pdfData, + "advance-payments", + ["Date", "Project", "Credit", "Debit", "Finance ID", "Balance"] + ); + break; + + case "print": + if (tableRef?.current) printTable(tableRef.current); + break; + + default: + break; + } +}; + +export default handleAdvancePaymentExport; diff --git a/src/pages/AdvancePayment/AdvancePaymentPageDetails.jsx b/src/pages/AdvancePayment/AdvancePaymentPageDetails.jsx index 6f6d3240..6e2918dd 100644 --- a/src/pages/AdvancePayment/AdvancePaymentPageDetails.jsx +++ b/src/pages/AdvancePayment/AdvancePaymentPageDetails.jsx @@ -3,6 +3,7 @@ import React, { useContext, useEffect, useMemo, + useRef, useState, } from "react"; import Breadcrumb from "../../components/common/Breadcrumb"; @@ -15,6 +16,7 @@ import { employee } from "../../data/masters"; import { formatFigure } from "../../utils/appUtils"; import { useParams } from "react-router-dom"; import { useExpenseTransactions } from "../../hooks/useExpense"; +import handleAdvancePaymentExport from "../../components/AdvancePayment/handleAdvancePaymentExport"; export const AdvancePaymentContext = createContext(); export const useAdvancePaymentContext = () => { @@ -60,6 +62,13 @@ const AdvancePaymentPageDetails = () => { }); }, [reset]); + + const tableRef = useRef(null); + + const handleExport = (type) => { + handleAdvancePaymentExport(type, transactionData, tableRef); + }; + return (
@@ -73,28 +82,64 @@ const AdvancePaymentPageDetails = () => { />
-
-
- {balance ? ( +
+ + {/* LEFT SIDE → EXPORT DROPDOWN */} +
+
+ + +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+ + {/* RIGHT SIDE → CURRENT BALANCE */} +
+ {balance !== null ? ( <> - + 0 ? "text-success" : "text-danger"} fs-5 fw-bold ms-1`} > {balance > 0 && }{" "} - {formatFigure(balance, { - type: "currency", - currency: "INR", - })} + {formatFigure(balance, { type: "currency", currency: "INR" })} - ) : ( - <> - )} - + ) : null}
+
- + +
diff --git a/src/utils/tableExportUtils.jsx b/src/utils/tableExportUtils.jsx index 768ff5ac..e7dfe072 100644 --- a/src/utils/tableExportUtils.jsx +++ b/src/utils/tableExportUtils.jsx @@ -46,62 +46,73 @@ const sanitizeText = (text) => { return text.replace(/[^\x00-\x7F]/g, "?"); }; -export const exportToPDF = async (data, fileName = "data", columns = null, options = {}) => { +export const exportToPDF = async (data, fileName = "data", columns = null) => { if (!data || data.length === 0) return; const pdfDoc = await PDFDocument.create(); const font = await pdfDoc.embedFont(StandardFonts.Helvetica); - // Default options - const { - columnWidths = [], // array of widths per column - fontSizeHeader = 12, - fontSizeRow = 10, - rowHeight = 25, - } = options; - - const pageWidth = 1000; + const pageWidth = 900; const pageHeight = 600; - let page = pdfDoc.addPage([pageWidth, pageHeight]); const margin = 30; + const rowHeight = 20; + + let page = pdfDoc.addPage([pageWidth, pageHeight]); let y = pageHeight - margin; const headers = columns || Object.keys(data[0]); - // Draw headers - headers.forEach((header, i) => { - const x = margin + (columnWidths[i] ? columnWidths.slice(0, i).reduce((a, b) => a + b, 0) : i * 150); - page.drawText(header, { x, y, font, size: fontSizeHeader }); + const sanitize = (value) => { + if (value === null || value === undefined) return ""; + return String(value).replace(/[^\x00-\x7F]/g, ""); // remove unicode + }; + + // ---- Draw Header Row ---- + headers.forEach((header, index) => { + const x = margin + index * 120; + page.drawText(sanitize(header), { + x, + y, + size: 12, + font, + }); }); + y -= rowHeight; - // Draw rows - data.forEach(row => { - headers.forEach((header, i) => { - const x = margin + (columnWidths[i] ? columnWidths.slice(0, i).reduce((a, b) => a + b, 0) : i * 150); - const text = row[header] || ''; - page.drawText(text, { x, y, font, size: fontSizeRow }); - }); - y -= rowHeight; - + // ---- Draw Table Rows ---- + data.forEach((row) => { if (y < margin) { + // Create a new page page = pdfDoc.addPage([pageWidth, pageHeight]); y = pageHeight - margin; } + + headers.forEach((header, index) => { + const x = margin + index * 120; + const text = sanitize(row[header]); + + page.drawText(text, { + x, + y, + size: 10, + font, + }); + }); + + y -= rowHeight; }); const pdfBytes = await pdfDoc.save(); - const blob = new Blob([pdfBytes], { type: 'application/pdf' }); - const link = document.createElement('a'); + + // Download + const blob = new Blob([pdfBytes], { type: "application/pdf" }); + const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = `${fileName}.pdf`; link.click(); }; - - - - /** * Export JSON data to PDF in a card-style format * @param {Array} data - Array of objects to export From 7e4a8157bfec325c295fa08ba2402fbf89d97e52 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Thu, 27 Nov 2025 10:40:10 +0530 Subject: [PATCH 09/18] Change the UI for Employee and Directory and adding export functionality. --- src/pages/Directory/DirectoryPage.jsx | 222 ++++++++++++++++++-------- src/pages/employee/EmployeeList.jsx | 133 +++++++-------- src/utils/exportUtils.js | 27 ---- 3 files changed, 214 insertions(+), 168 deletions(-) delete mode 100644 src/utils/exportUtils.js diff --git a/src/pages/Directory/DirectoryPage.jsx b/src/pages/Directory/DirectoryPage.jsx index 43c2f823..c5f07fbd 100644 --- a/src/pages/Directory/DirectoryPage.jsx +++ b/src/pages/Directory/DirectoryPage.jsx @@ -19,7 +19,7 @@ import BucketList from "../../components/Directory/BucketList"; import { MainDirectoryPageSkeleton } from "../../components/Directory/DirectoryPageSkeleton"; import ContactProfile from "../../components/Directory/ContactProfile"; import GlobalModel from "../../components/common/GlobalModel"; -import { exportToCSV } from "../../utils/exportUtils"; +import { exportToCSV,exportToExcel,exportToPDF1,exportToPDF,printTable } from "../../utils/tableExportUtils"; import ConfirmModal from "../../components/common/ConfirmModal"; import { useSelectedProject } from "../../slices/apiDataManager"; @@ -64,11 +64,49 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) { const [ContactData, setContactData] = useState([]); const handleExport = (type) => { - if (activeTab === "notes" && type === "csv") { - exportToCSV(notesData, "notes.csv"); - } - if (activeTab === "contacts" && type === "csv") { - exportToCSV(ContactData, "contact.csv"); + let exportData = activeTab === "notes" ? notesData : ContactData; + if (!exportData?.length) return; + + switch (type) { + case "csv": + exportToCSV(exportData, activeTab === "notes" ? "Notes" : "Contacts"); + break; + case "excel": + exportToExcel(exportData, activeTab === "notes" ? "Notes" : "Contacts"); + break; + case "pdf": + if (activeTab === "notes") { + exportToPDF1(exportData, "Notes"); + } else { + // Columns for Contacts PDF + const columns = [ + "Email", + "Phone", + "Organization", + "Category", + "Tags", + ]; + + // Sanitize and trim long text to avoid PDF overflow + const sanitizedData = exportData.map((item) => ({ + Email: (item.Email || "").slice(0, 40), + Phone: (item.Phone || "").slice(0, 20), + Organization: (item.Organization || "").slice(0, 30), + Category: (item.Category || "").slice(0, 20), + Tags: (item.Tags || "").slice(0, 40), + })); + + // Export with proper spacing + exportToPDF(sanitizedData, "Contacts", columns, { + columnWidths: [200, 120, 180, 120, 200], // Adjust widths per column + fontSizeHeader: 12, + fontSizeRow: 10, + rowHeight: 25, + }); + } + break; + default: + console.warn("Unsupported export type"); } }; @@ -133,7 +171,7 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) { ]} > )} -
+
    @@ -158,13 +196,11 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
-
-
-
-
- {activeTab === "notes" && ( -
+
+
+
+ {activeTab === "notes" && ( setSearchNote(e.target.value)} /> -
- )} + )} - {activeTab === "contacts" && ( -
-
-
+ {activeTab === "contacts" && ( +
+
setsearchContact(e.target.value)} /> +
+ {" "} + + +
-
+
+ + +
    + {activeTab === "contacts" && ( +
  • +
    + + setShowActive(e.target.checked) + } + /> +
    + + {showActive + ? "Active Contacts" + : "Inactive Contacts"} + +
  • + )} +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + setGridView(true)} > - - -
  • + +
  • + setGridView(false)} > - - - -
    - setShowActive(e.target.checked)} - /> - -
    -
-
- )} -
-
-
- -
diff --git a/src/pages/employee/EmployeeList.jsx b/src/pages/employee/EmployeeList.jsx index ba7a13ba..9417f8bc 100644 --- a/src/pages/employee/EmployeeList.jsx +++ b/src/pages/employee/EmployeeList.jsx @@ -298,31 +298,7 @@ const EmployeeList = () => {
{/* Switches: All Employees + Inactive */}
- {/* All Employees Switch */} - - {/* Show Inactive Employees Switch */} - -
- setShowInactive(e.target.checked)} - /> - -
-
- - {/* Right side: Search + Export + Add Employee */} -
- {/* Search Input - ALWAYS ENABLED */} + {/* {showAllEmployees && ( */}
- {/* Export Dropdown */} - + {/* )} */} +
+ {/* Right side: Search + Add Employee + Options */} +
{/* Add Employee Button */} {Manage_Employee && ( )} + + {/* 3-Dots Dropdown (New Combined Menu) */} +
+ +
    + +
  • +
    + setShowInactive(e.target.checked)} + /> +
    + Show Inactive Employees +
  • + + +

  • + +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
{ - if (!data || data.length === 0) return; - - const headers = Object.keys(data[0]); - const csvRows = []; - - // Add headers - csvRows.push(headers.join(",")); - - // Add values - data.forEach(row => { - const values = headers.map(header => `"${row[header] ?? ""}"`); - csvRows.push(values.join(",")); - }); - - // Create CSV Blob - const csvContent = csvRows.join("\n"); - const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); - - // Create download link - const link = document.createElement("a"); - const url = URL.createObjectURL(blob); - link.setAttribute("href", url); - link.setAttribute("download", filename); - link.click(); -}; From b5fb48104c584be2b17c67ac8239ad7e3324666f Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Thu, 27 Nov 2025 17:23:13 +0530 Subject: [PATCH 10/18] Adding Export functionality in Recurring Expense. --- .../RecurringExpense/RecurringExpenseList.jsx | 227 +++++++++--------- .../handleRecurringExpenseExport.jsx | 64 +++++ .../RecurringExpense/RecurringExpensePage.jsx | 67 +++++- 3 files changed, 241 insertions(+), 117 deletions(-) create mode 100644 src/components/RecurringExpense/handleRecurringExpenseExport.jsx diff --git a/src/components/RecurringExpense/RecurringExpenseList.jsx b/src/components/RecurringExpense/RecurringExpenseList.jsx index a4c45fba..a48361fd 100644 --- a/src/components/RecurringExpense/RecurringExpenseList.jsx +++ b/src/components/RecurringExpense/RecurringExpenseList.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { EXPENSE_DRAFT, EXPENSE_REJECTEDBY, @@ -18,7 +18,7 @@ import { useRecurringExpenseList } from "../../hooks/useExpense"; import Pagination from "../common/Pagination"; import { SpinnerLoader } from "../common/Loader"; -const RecurringExpenseList = ({ search, filterStatuses }) => { +const RecurringExpenseList = ({ search, filterStatuses, tableRef, onDataFiltered }) => { const { setManageRequest, setVieRequest, setViewRecurring } = useRecurringExpenseContext(); const navigate = useNavigate(); @@ -70,9 +70,8 @@ const RecurringExpenseList = ({ search, filterStatuses }) => { align: "text-end", getValue: (e) => e?.amount - ? `${ - e?.currency?.symbol ? e.currency.symbol + " " : "" - }${e.amount.toLocaleString()}` + ? `${e?.currency?.symbol ? e.currency.symbol + " " : "" + }${e.amount.toLocaleString()}` : "N/A", }, { @@ -112,6 +111,17 @@ const RecurringExpenseList = ({ search, filterStatuses }) => { debouncedSearch ); + const filteredData = useMemo( + () => + data?.data?.filter((item) => filterStatuses.includes(item?.status?.id)) || + [], + [data?.data, filterStatuses] + ); + + useEffect(() => { + onDataFiltered(filteredData); + }, [filteredData, onDataFiltered]); + const paginate = (page) => { if (page >= 1 && page <= (data?.totalPages ?? 1)) { setCurrentPage(page); @@ -150,10 +160,6 @@ const RecurringExpenseList = ({ search, filterStatuses }) => { ); }; - const filteredData = data?.data?.filter((item) => - filterStatuses.includes(item?.status?.id) - ); - const handleDelete = (id) => { setDeletingId(id); DeleteExpense( @@ -166,7 +172,6 @@ const RecurringExpenseList = ({ search, filterStatuses }) => { } ); }; -console.log("Tanish",filteredData) return ( <> {IsDeleteModalOpen && ( @@ -181,111 +186,111 @@ console.log("Tanish",filteredData) /> )} -
+
-
- {Array.isArray(filteredData) && filteredData.length > 0 && ( -
- - - {recurringExpenseColumns.map((col) => ( - - ))} - - - - - - {filteredData?.length > 0 ? ( - filteredData?.map((recurringExpense) => ( - - {recurringExpenseColumns.map((col) => ( - - ))} - - - )) - ) : ( +
+ {Array.isArray(filteredData) && filteredData.length > 0 && ( +
- {col.label} - Action
- {col?.customRender - ? col?.customRender(recurringExpense) - : col?.getValue(recurringExpense)} - -
- - setViewRecurring({ - recurringId: recurringExpense?.id, - view: true, - }) - } - > - -
- -
    -
  • - setManageRequest({ - IsOpen: true, - RecurringId: recurringExpense?.id, - }) - } - > - - - Modify - -
  • - -
  • { - setIsDeleteModalOpen(true); - setDeletingId(recurringExpense.id); - }} - > - - - Delete - -
  • -
-
-
-
+ - + {recurringExpenseColumns.map((col) => ( + + ))} + - )} - -
+ {col.label} + Action
- )} - {!filteredData || - filteredData.length === 0 - && ( -
- {isError ? (

{error.message}

):(

No Recurring Expense Found

)} -
+ + + + {filteredData?.length > 0 ? ( + filteredData?.map((recurringExpense) => ( + + {recurringExpenseColumns.map((col) => ( + + {col?.customRender + ? col?.customRender(recurringExpense) + : col?.getValue(recurringExpense)} + + ))} + +
+ + setViewRecurring({ + recurringId: recurringExpense?.id, + view: true, + }) + } + > + +
+ +
    +
  • + setManageRequest({ + IsOpen: true, + RecurringId: recurringExpense?.id, + }) + } + > + + + Modify + +
  • + +
  • { + setIsDeleteModalOpen(true); + setDeletingId(recurringExpense.id); + }} + > + + + Delete + +
  • +
+
+
+ + + )) + ) : ( + + + + )} + + )} -
+ {!filteredData || + filteredData.length === 0 + && ( +
+ {isError ? (

{error.message}

) : (

No Recurring Expense Found

)} +
+ )} +
{/* Pagination */} diff --git a/src/components/RecurringExpense/handleRecurringExpenseExport.jsx b/src/components/RecurringExpense/handleRecurringExpenseExport.jsx new file mode 100644 index 00000000..af9537c7 --- /dev/null +++ b/src/components/RecurringExpense/handleRecurringExpenseExport.jsx @@ -0,0 +1,64 @@ +import moment from "moment"; +import { exportToCSV,exportToExcel,exportToPDF,printTable } from "../../utils/tableExportUtils"; +import { FREQUENCY_FOR_RECURRING } from "../../utils/constants"; + +const handleRecurringExpenseExport = (type, expenses, tableRef) => { + if (!expenses || expenses.length === 0) return; + + // Mapped Export Data + const exportData = expenses.map((item) => ({ + Category: item?.expenseCategory?.name ?? "-", + Title: item?.title ?? "-", + Payee: item?.payee ?? "-", + Frequency: + item?.frequency !== undefined && item?.frequency !== null + ? FREQUENCY_FOR_RECURRING[item?.frequency] ?? "-" + : "-", + Amount: item?.amount ? item.amount.toLocaleString() : "-", + Currency: item?.currency?.symbol ?? "-", + "Next Generation Date": item?.nextGenerationDate + ? moment(item.nextGenerationDate).format("DD-MMM-YYYY") + : "-", + Status: item?.status?.name ?? "-", + "Created At": item?.createdAt + ? moment(item.createdAt).format("DD-MMM-YYYY") + : "-", + })); + + // COLUMN ORDER + const columns = [ + "Category", + "Title", + "Payee", + "Frequency", + "Amount", + "Currency", + "Next Generation Date", + "Status", + "Created At", + ]; + + switch (type) { + case "csv": + exportToCSV(exportData, "recurring-expense", columns); + break; + + case "excel": + exportToExcel(exportData, "recurring-expense", columns); + break; + + case "pdf": + exportToPDF(exportData, "recurring-expense", columns); + break; + + case "print": + if (tableRef?.current) printTable(tableRef.current); + break; + + default: + console.warn("Unhandled export type:", type); + break; + } +}; + +export default handleRecurringExpenseExport; diff --git a/src/pages/RecurringExpense/RecurringExpensePage.jsx b/src/pages/RecurringExpense/RecurringExpensePage.jsx index 12c07e10..db923657 100644 --- a/src/pages/RecurringExpense/RecurringExpensePage.jsx +++ b/src/pages/RecurringExpense/RecurringExpensePage.jsx @@ -1,4 +1,4 @@ -import React, { createContext, useState, useEffect, useContext } from "react"; +import React, { createContext, useState, useEffect, useContext, useRef } from "react"; import Breadcrumb from "../../components/common/Breadcrumb"; import GlobalModel from "../../components/common/GlobalModel"; import { useFab } from "../../Context/FabContext"; @@ -7,6 +7,7 @@ import RecurringExpenseList from "../../components/RecurringExpense/RecurringExp import { PAYEE_RECURRING_EXPENSE } from "../../utils/constants"; import { SearchRecurringExpenseSchema } from "../../components/RecurringExpense/RecurringExpenseSchema"; import ViewRecurringExpense from "../../components/RecurringExpense/ViewRecurringExpense"; +import handleRecurringExpenseExport from "../../components/RecurringExpense/handleRecurringExpenseExport"; export const RecurringExpenseContext = createContext(); export const useRecurringExpenseContext = () => { @@ -23,6 +24,10 @@ const RecurringExpensePage = () => { IsOpen: null, RecurringId: null, }); + const tableRef = useRef(null); + + const [filteredData, setFilteredData] = useState([]); + const [viewRecurring, setViewRecurring] = useState({ view: false, recurringId: null, @@ -44,6 +49,12 @@ const RecurringExpensePage = () => { prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id] ); }; + + + const handleExport = (type) => { + handleRecurringExpenseExport(type, filteredData, tableRef); + }; + return (
@@ -56,8 +67,8 @@ const RecurringExpensePage = () => { /> {/* Top Bar */} -
-
+
+
{/* Left Column: Search + Filter */}
@@ -67,7 +78,7 @@ const RecurringExpensePage = () => { className="form-control form-control-sm w-auto" placeholder="Search Recurring Expense" value={search} - style={{minWidth:"200px"}} + style={{ minWidth: "200px" }} onChange={(e) => setSearch(e.target.value)} /> @@ -99,8 +110,8 @@ const RecurringExpensePage = () => {
- {/* Right Column: Add Button */} -
+ {/* Right Column: Add Button + 3-Dots Menu */} +
+ + {/* 3-Dots Dropdown */} +
+ + +
    +
  • + +
  • + +

  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • +
+
+
+
@@ -124,6 +177,8 @@ const RecurringExpensePage = () => { {ManageRequest.IsOpen && ( From a31d5d5015fe3931f620716ee3b3430659cfba01 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Thu, 27 Nov 2025 17:41:49 +0530 Subject: [PATCH 11/18] Adding Export functionality in Payment Request. --- .../PaymentRequest/PaymentRequestList.jsx | 17 +++-- .../handlePaymentRequestExport.jsx | 58 ++++++++++++++ .../PaymentRequest/PaymentRequestPage.jsx | 75 ++++++++++++++++--- 3 files changed, 133 insertions(+), 17 deletions(-) create mode 100644 src/components/PaymentRequest/handlePaymentRequestExport.jsx diff --git a/src/components/PaymentRequest/PaymentRequestList.jsx b/src/components/PaymentRequest/PaymentRequestList.jsx index 0c9edfd5..1cd1b3bb 100644 --- a/src/components/PaymentRequest/PaymentRequestList.jsx +++ b/src/components/PaymentRequest/PaymentRequestList.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { EXPENSE_DRAFT, EXPENSE_REJECTEDBY, @@ -22,7 +22,7 @@ import Error from "../common/Error"; import Pagination from "../common/Pagination"; import PaymentRequestFilterChips from "./PaymentRequestFilterChips"; -const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter, search, groupBy = "submittedBy" }) => { +const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter, search, groupBy = "submittedBy", tableRef, onDataFiltered }) => { const { setManageRequest, setVieRequest } = usePaymentRequestContext(); const navigate = useNavigate(); const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -30,6 +30,7 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter const SelfId = useSelector( (store) => store?.globalVariables?.loginUser?.employeeInfo?.id ); + const groupByField = (items, field) => { return items.reduce((acc, item) => { let key; @@ -149,6 +150,12 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter debouncedSearch ); + useEffect(() => { + if (onDataFiltered) { + onDataFiltered(data?.data ?? []); + } + }, [data, onDataFiltered]); + if (isError) { return ; } @@ -222,7 +229,7 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter paramData={deletingId} /> )} -
+
))} - Action + Action @@ -262,7 +269,7 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter {items?.map((paymentRequest) => ( - + {paymentRequestColumns.map( (col) => (col.isAlwaysVisible || groupBy !== col.key) && ( diff --git a/src/components/PaymentRequest/handlePaymentRequestExport.jsx b/src/components/PaymentRequest/handlePaymentRequestExport.jsx new file mode 100644 index 00000000..1cf0f433 --- /dev/null +++ b/src/components/PaymentRequest/handlePaymentRequestExport.jsx @@ -0,0 +1,58 @@ +import moment from "moment"; +import { exportToCSV, exportToExcel, exportToPDF, printTable } from "../../utils/tableExportUtils"; + +const handlePaymentRequestExport = (type, requests, tableRef) => { + if (!requests || requests.length === 0) return; + + // Map export data + const exportData = requests.map((item) => ({ + "Request ID": item?.paymentRequestUID ?? "-", + "Title": item?.title ?? "-", + "Payee": item?.payee ?? "-", + "Amount": item?.amount?.toLocaleString() ?? "-", + "Currency": item?.currency?.currencyCode ?? "-", + "Created At": item?.createdAt ? moment(item.createdAt).format("DD-MMM-YYYY") : "-", + "Due Date": item?.dueDate ? moment(item.dueDate).format("DD-MMM-YYYY") : "-", + "Status": item?.expenseStatus?.name ?? "-", + "Submitted By": `${item?.createdBy?.firstName ?? ""} ${item?.createdBy?.lastName ?? ""}`.trim() || "-", + "Project": item?.project?.name ?? "-", + })); + + // Define column order + const columns = [ + "Request ID", + "Title", + "Payee", + "Amount", + "Currency", + "Created At", + "Due Date", + "Status", + "Submitted By", + "Project", + ]; + + switch (type) { + case "csv": + exportToCSV(exportData, "payment-requests", columns); + break; + + case "excel": + exportToExcel(exportData, "payment-requests", columns); + break; + + case "pdf": + exportToPDF(exportData, "payment-requests", columns); + break; + + case "print": + if (tableRef?.current) printTable(tableRef.current); + break; + + default: + console.warn("Unhandled export type:", type); + break; + } +}; + +export default handlePaymentRequestExport; diff --git a/src/pages/PaymentRequest/PaymentRequestPage.jsx b/src/pages/PaymentRequest/PaymentRequestPage.jsx index 7b7d2ff1..03be604a 100644 --- a/src/pages/PaymentRequest/PaymentRequestPage.jsx +++ b/src/pages/PaymentRequest/PaymentRequestPage.jsx @@ -9,6 +9,7 @@ import { defaultPaymentRequestFilter } from "../../components/PaymentRequest/Pay import ViewPaymentRequest from "../../components/PaymentRequest/ViewPaymentRequest"; import PreviewDocument from "../../components/Expenses/PreviewDocument"; import MakeExpense from "../../components/PaymentRequest/MakeExpense"; +import handlePaymentRequestExport from "../../components/PaymentRequest/handlePaymentRequestExport"; export const PaymentRequestContext = createContext(); export const usePaymentRequestContext = () => { @@ -30,7 +31,8 @@ const PaymentRequestPage = () => { const [search, setSearch] = useState(""); const updatedRef = useRef(); const { setOffcanvasContent, setShowTrigger } = useFab(); - + const tableRef = useRef(null); + const [filteredData, setFilteredData] = useState([]); const contextValue = { setManageRequest, setVieRequest, @@ -76,6 +78,10 @@ const PaymentRequestPage = () => { }); }; + const handleExport = (type) => { + handlePaymentRequestExport(type, filteredData, tableRef); // <-- corrected + }; + return (
@@ -92,18 +98,19 @@ const PaymentRequestPage = () => {
-
- setSearch(e.target.value)} - /> +
+
+ setSearch(e.target.value)} + /> +
- -
+
+ + {/* 3-Dots Dropdown */} +
+ + +
    +
  • + +
  • + +

  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • +
+
+
+
@@ -129,6 +178,8 @@ const PaymentRequestPage = () => { filterData={filterData} removeFilterChip={handleRemoveChip} clearFilter={clearFilter} + tableRef={tableRef} + onDataFiltered={setFilteredData} /> {/* Add/Edit Modal */} From af95009c1ff50a14042a440449343f16d4c4b0d6 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Thu, 27 Nov 2025 17:49:38 +0530 Subject: [PATCH 12/18] Adding Export functionality in Expense. --- src/components/Expenses/ExpenseList.jsx | 16 +++-- .../PaymentRequest/handleExpenseExport.jsx | 62 ++++++++++++++++ src/pages/Expense/ExpensePage.jsx | 70 ++++++++++++++++--- 3 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 src/components/PaymentRequest/handleExpenseExport.jsx diff --git a/src/components/Expenses/ExpenseList.jsx b/src/components/Expenses/ExpenseList.jsx index 16e47489..c1bf72f0 100644 --- a/src/components/Expenses/ExpenseList.jsx +++ b/src/components/Expenses/ExpenseList.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useDeleteExpense, useExpenseList } from "../../hooks/useExpense"; import Avatar from "../common/Avatar"; import { useExpenseContext } from "../../pages/Expense/ExpensePage"; @@ -24,7 +24,7 @@ import ExpenseFilterChips from "./ExpenseFilterChips"; import { defaultFilter } from "./ExpenseSchema"; import { useNavigate } from "react-router-dom"; -const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { +const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRef, onDataFiltered }) => { const [deletingId, setDeletingId] = useState(null); const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const { @@ -46,6 +46,12 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { filters, debouncedSearch ); + + useEffect(() => { + if (onDataFiltered) { + onDataFiltered(data?.data ?? []); + } + }, [data, onDataFiltered]); const SelfId = useSelector( (store) => store?.globalVariables?.loginUser?.employeeInfo?.id @@ -258,7 +264,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { groupBy={groupBy} />
@@ -313,8 +319,8 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { >
{ + if (!expenses || expenses.length === 0) return; + + // Map export data + const exportData = expenses.map((item) => ({ + "Expense ID": item?.expenseUId ?? "-", + "Expense Category": item?.expenseCategory?.name ?? "-", + "Payment Mode": item?.paymentMode?.name ?? "-", + "Submitted By": `${item?.createdBy?.firstName ?? ""} ${item?.createdBy?.lastName ?? ""}`.trim() || "-", + "Submitted": item?.createdAt ? moment(item.createdAt).format("DD-MMM-YYYY") : "-", + "Amount": item?.amount?.toLocaleString() ?? "-", + "Currency": item?.currency?.currencyCode ?? "-", + "Status": item?.status?.name ?? "-", + "Project": item?.project?.name ?? "-", + })); + + // Define column order + const columns = [ + "Expense ID", + "Expense Category", + "Payment Mode", + "Submitted By", + "Submitted", + "Amount", + "Currency", + "Status", + "Project", + ]; + + switch (type) { + case "csv": + exportToCSV(exportData, "expenses", columns); + break; + + case "excel": + exportToExcel(exportData, "expenses", columns); + break; + + case "pdf": + exportToPDF(exportData, "expenses", columns); + break; + + case "print": + if (tableRef?.current) printTable(tableRef.current); + break; + + default: + console.warn("Unhandled export type:", type); + break; + } +}; + +export default handleExpenseExport; diff --git a/src/pages/Expense/ExpensePage.jsx b/src/pages/Expense/ExpensePage.jsx index e7ac3638..b2057f97 100644 --- a/src/pages/Expense/ExpensePage.jsx +++ b/src/pages/Expense/ExpensePage.jsx @@ -30,6 +30,7 @@ import { SearchSchema, } from "../../components/Expenses/ExpenseSchema"; import PreviewDocument from "../../components/Expenses/PreviewDocument"; +import handleExpenseExport from "../../components/PaymentRequest/handleExpenseExport"; // Context export const ExpenseContext = createContext(); @@ -70,6 +71,8 @@ const ExpensePage = () => { const IsViewSelf = useHasUserPermission(VIEW_SELF_EXPENSE); const { setOffcanvasContent, setShowTrigger } = useFab(); const [filterData, setFilterdata] = useState(defaultFilter); + const tableRef = useRef(null); + const [filteredData, setFilteredData] = useState([]); const removeFilterChip = (key, id) => { setFilters((prev) => { const updated = { ...prev }; @@ -114,6 +117,10 @@ const ExpensePage = () => { removeFilterChip, }; + const handleExport = (type) => { + handleExpenseExport(type, filteredData, tableRef); // <-- corrected + }; + return (
@@ -126,17 +133,19 @@ const ExpensePage = () => {
-
- setSearchText(e.target.value)} - /> +
+
+ setSearchText(e.target.value)} + /> +
-
+
{IsCreatedAble && ( )} + + {/* 3-Dots Dropdown */} +
+ + +
    +
  • + +
  • + +

  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • +
+
+
@@ -163,6 +213,8 @@ const ExpensePage = () => { filters={filters} groupBy={groupBy} searchText={searchText} + tableRef={tableRef} + onDataFiltered={setFilteredData} /> ) : ( From 9eae0b2cd10fccdd4071db8d3d613b6132332aa9 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Thu, 27 Nov 2025 18:01:07 +0530 Subject: [PATCH 13/18] Correction in App routes. --- src/router/AppRoutes.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index 4433037c..a198d0c4 100644 --- a/src/router/AppRoutes.jsx +++ b/src/router/AppRoutes.jsx @@ -62,6 +62,7 @@ import ServiceProjectDetail from "../pages/ServiceProject/ServiceProjectDetail"; import ManageJob from "../components/ServiceProject/ServiceProjectJob/ManageJob"; import AdvancePaymentPageDetails from "../pages/AdvancePayment/AdvancePaymentPageDetails"; import AdvancePaymentPage from "../pages/AdvancePayment/AdvancePaymentPage"; +import PurchasePage from "../pages/purchase/PurchasePage"; const router = createBrowserRouter( [ { @@ -119,8 +120,6 @@ const router = createBrowserRouter( { path: "/expenses", element: }, { path: "/payment-request", element: }, { path: "/recurring-payment", element: }, - // { path: "/advance-payment", element: }, - // { path: "/advance-payment/:employeeId", element: }, { path: "/advance-payment", element: }, { path: "/advance-payment/:employeeId", element: }, From 917e7f3cccf0993dc263168b00f8d6e935469ffa Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Wed, 3 Dec 2025 09:53:41 +0530 Subject: [PATCH 14/18] Adding api for Finance for Export. --- .../PaymentRequest/handleExpenseExport.jsx | 119 ++++++----- .../handlePaymentRequestExport.jsx | 114 ++++++---- .../handleRecurringExpenseExport.jsx | 201 +++++++++++++----- src/pages/Expense/ExpensePage.jsx | 3 +- .../PaymentRequest/PaymentRequestPage.jsx | 8 +- .../RecurringExpense/RecurringExpensePage.jsx | 10 +- 6 files changed, 303 insertions(+), 152 deletions(-) diff --git a/src/components/PaymentRequest/handleExpenseExport.jsx b/src/components/PaymentRequest/handleExpenseExport.jsx index e51a95bf..e8f88786 100644 --- a/src/components/PaymentRequest/handleExpenseExport.jsx +++ b/src/components/PaymentRequest/handleExpenseExport.jsx @@ -1,61 +1,84 @@ +import ExpenseRepository from "../../repositories/ExpsenseRepository"; import moment from "moment"; import { exportToCSV, exportToExcel, exportToPDF, printTable } from "../../utils/tableExportUtils"; +import showToast from "../../services/toastService"; -/** - * Handles exporting expenses data to CSV, Excel, PDF, or Print - * @param {"csv"|"excel"|"pdf"|"print"} type - * @param {Array} expenses - List of expenses - * @param {React.RefObject} tableRef - Table reference for printing - */ -const handleExpenseExport = (type, expenses, tableRef) => { - if (!expenses || expenses.length === 0) return; +const handleExpenseExport = async ( + type, + filters = {}, + searchString = "", + tableRef = null, + setLoading = null +) => { + try { + if (setLoading) setLoading(true); - // Map export data - const exportData = expenses.map((item) => ({ - "Expense ID": item?.expenseUId ?? "-", - "Expense Category": item?.expenseCategory?.name ?? "-", - "Payment Mode": item?.paymentMode?.name ?? "-", - "Submitted By": `${item?.createdBy?.firstName ?? ""} ${item?.createdBy?.lastName ?? ""}`.trim() || "-", - "Submitted": item?.createdAt ? moment(item.createdAt).format("DD-MMM-YYYY") : "-", - "Amount": item?.amount?.toLocaleString() ?? "-", - "Currency": item?.currency?.currencyCode ?? "-", - "Status": item?.status?.name ?? "-", - "Project": item?.project?.name ?? "-", - })); + const safeSearchString = typeof searchString === "string" ? searchString : ""; + let allExpenses = []; + let pageNumber = 1; + const pageSize = 1000; // fetch 1000 per API call + let hasMore = true; - // Define column order - const columns = [ - "Expense ID", - "Expense Category", - "Payment Mode", - "Submitted By", - "Submitted", - "Amount", - "Currency", - "Status", - "Project", - ]; + while (hasMore) { + const response = await ExpenseRepository.GetExpenseList( + pageSize, + pageNumber, + filters, + safeSearchString + ); - switch (type) { - case "csv": - exportToCSV(exportData, "expenses", columns); - break; + const currentPageData = response?.data?.data || []; + allExpenses = allExpenses.concat(currentPageData); - case "excel": - exportToExcel(exportData, "expenses", columns); - break; + // If returned data length is less than pageSize, we reached the last page + if (currentPageData.length < pageSize) { + hasMore = false; + } else { + pageNumber += 1; // fetch next page + } + } - case "pdf": - exportToPDF(exportData, "expenses", columns); - break; + if (!allExpenses.length) { + showToast("No expenses found!", "warning"); + return; + } - case "print": - if (tableRef?.current) printTable(tableRef.current); - break; + // Map export data + const exportData = allExpenses.map((item) => ({ + "Expense ID": item?.expenseUId ?? "-", + "Expense Category": item?.expenseCategory?.name ?? "-", + "Payment Mode": item?.paymentMode?.name ?? "-", + "Submitted By": `${item?.createdBy?.firstName ?? ""} ${item?.createdBy?.lastName ?? ""}`.trim() || "-", + "Submitted": item?.createdAt ? moment(item.createdAt).format("DD-MMM-YYYY") : "-", + "Amount": item?.amount != null + ? `${item.amount.toLocaleString()} ${item.currency?.currencyCode ?? ""}` + : "-", + "Status": item?.status?.name ?? "-", + })); - default: - console.warn("Unhandled export type:", type); - break; + + switch (type) { + case "csv": + exportToCSV(exportData, "Expenses"); + break; + case "excel": + exportToExcel(exportData, "Expenses"); + break; + case "pdf": + exportToPDF(exportData, "Expenses"); + break; + case "print": + if (tableRef?.current) printTable(tableRef.current); + break; + default: + console.warn("Unknown export type:", type); + } + + } catch (err) { + console.error(err); + showToast("Failed to export expenses", "error"); + } finally { + if (setLoading) setLoading(false); } }; diff --git a/src/components/PaymentRequest/handlePaymentRequestExport.jsx b/src/components/PaymentRequest/handlePaymentRequestExport.jsx index 1cf0f433..7a77bcff 100644 --- a/src/components/PaymentRequest/handlePaymentRequestExport.jsx +++ b/src/components/PaymentRequest/handlePaymentRequestExport.jsx @@ -1,58 +1,84 @@ import moment from "moment"; import { exportToCSV, exportToExcel, exportToPDF, printTable } from "../../utils/tableExportUtils"; +import ExpenseRepository from "../../repositories/ExpsenseRepository"; -const handlePaymentRequestExport = (type, requests, tableRef) => { - if (!requests || requests.length === 0) return; +const handlePaymentRequestExport = async ( + type, + filters = {}, + searchString = "", + tableRef = null, + setLoading = null +) => { + try { + if (setLoading) setLoading(true); - // Map export data - const exportData = requests.map((item) => ({ - "Request ID": item?.paymentRequestUID ?? "-", - "Title": item?.title ?? "-", - "Payee": item?.payee ?? "-", - "Amount": item?.amount?.toLocaleString() ?? "-", - "Currency": item?.currency?.currencyCode ?? "-", - "Created At": item?.createdAt ? moment(item.createdAt).format("DD-MMM-YYYY") : "-", - "Due Date": item?.dueDate ? moment(item.dueDate).format("DD-MMM-YYYY") : "-", - "Status": item?.expenseStatus?.name ?? "-", - "Submitted By": `${item?.createdBy?.firstName ?? ""} ${item?.createdBy?.lastName ?? ""}`.trim() || "-", - "Project": item?.project?.name ?? "-", - })); + const safeSearchString = typeof searchString === "string" ? searchString : ""; + let allPaymentRequest = []; + let pageNumber = 1; + const pageSize = 1000; + let hasMore = true; - // Define column order - const columns = [ - "Request ID", - "Title", - "Payee", - "Amount", - "Currency", - "Created At", - "Due Date", - "Status", - "Submitted By", - "Project", - ]; + while (hasMore) { + const response = await ExpenseRepository.GetPaymentRequestList( + pageSize, + pageNumber, + filters, + true, // isActive + safeSearchString + ); - switch (type) { - case "csv": - exportToCSV(exportData, "payment-requests", columns); - break; + const currentPageData = response?.data?.data || []; + allPaymentRequest = allPaymentRequest.concat(currentPageData); - case "excel": - exportToExcel(exportData, "payment-requests", columns); - break; + if (currentPageData.length < pageSize) { + hasMore = false; + } else { + pageNumber += 1; + } + } - case "pdf": - exportToPDF(exportData, "payment-requests", columns); - break; + if (!allPaymentRequest.length) { + console.warn("No payment requests found!"); + return; + } - case "print": - if (tableRef?.current) printTable(tableRef.current); - break; + const exportData = allPaymentRequest.map((item) => ({ + "Request ID": item?.paymentRequestUID ?? "-", + "Title": item?.title ?? "-", + "Payee": item?.payee ?? "-", + "Amount": item?.amount != null ? Number(item.amount).toLocaleString() : "-", + "Currency": item?.currency?.currencyCode ?? "-", + "Created At": item?.createdAt ? moment(item.createdAt).format("DD-MMM-YYYY") : "-", + "Due Date": item?.dueDate ? moment(item.dueDate).format("DD-MMM-YYYY") : "-", + "Status": item?.expenseStatus?.name ?? "-", + "Submitted By": `${item?.createdBy?.firstName ?? ""} ${item?.createdBy?.lastName ?? ""}`.trim() || "-", + "Project": item?.project?.name ?? "-", + })); - default: - console.warn("Unhandled export type:", type); - break; + switch (type) { + case "csv": + exportToCSV(exportData, "PaymentRequests"); + break; + case "excel": + exportToExcel(exportData, "PaymentRequests"); + break; + case "pdf": + exportToPDF(exportData, "PaymentRequests"); + break; + case "print": + if (tableRef?.current) printTable(tableRef.current); + break; + default: + console.warn("Unknown export type:", type); + } + + } catch (err) { + console.error("Export failed:", err); + } finally { + if (setLoading) setLoading(false); } }; + + export default handlePaymentRequestExport; diff --git a/src/components/RecurringExpense/handleRecurringExpenseExport.jsx b/src/components/RecurringExpense/handleRecurringExpenseExport.jsx index af9537c7..358b9921 100644 --- a/src/components/RecurringExpense/handleRecurringExpenseExport.jsx +++ b/src/components/RecurringExpense/handleRecurringExpenseExport.jsx @@ -1,63 +1,160 @@ import moment from "moment"; -import { exportToCSV,exportToExcel,exportToPDF,printTable } from "../../utils/tableExportUtils"; +import { exportToCSV, exportToExcel, exportToPDF, printTable } from "../../utils/tableExportUtils"; import { FREQUENCY_FOR_RECURRING } from "../../utils/constants"; +import ExpenseRepository from "../../repositories/ExpsenseRepository"; -const handleRecurringExpenseExport = (type, expenses, tableRef) => { - if (!expenses || expenses.length === 0) return; +// const handleRecurringExpenseExport = (type, expenses, tableRef) => { +// if (!expenses || expenses.length === 0) return; - // Mapped Export Data - const exportData = expenses.map((item) => ({ - Category: item?.expenseCategory?.name ?? "-", - Title: item?.title ?? "-", - Payee: item?.payee ?? "-", - Frequency: - item?.frequency !== undefined && item?.frequency !== null - ? FREQUENCY_FOR_RECURRING[item?.frequency] ?? "-" +// // Mapped Export Data +// const exportData = expenses.map((item) => ({ +// Category: item?.expenseCategory?.name ?? "-", +// Title: item?.title ?? "-", +// Payee: item?.payee ?? "-", +// Frequency: +// item?.frequency !== undefined && item?.frequency !== null +// ? FREQUENCY_FOR_RECURRING[item?.frequency] ?? "-" +// : "-", +// Amount: item?.amount ? item.amount.toLocaleString() : "-", +// Currency: item?.currency?.symbol ?? "-", +// "Next Generation Date": item?.nextGenerationDate +// ? moment(item.nextGenerationDate).format("DD-MMM-YYYY") +// : "-", +// Status: item?.status?.name ?? "-", +// "Created At": item?.createdAt +// ? moment(item.createdAt).format("DD-MMM-YYYY") +// : "-", +// })); + +// // COLUMN ORDER +// const columns = [ +// "Category", +// "Title", +// "Payee", +// "Frequency", +// "Amount", +// "Currency", +// "Next Generation Date", +// "Status", +// "Created At", +// ]; + +// switch (type) { +// case "csv": +// exportToCSV(exportData, "recurring-expense", columns); +// break; + +// case "excel": +// exportToExcel(exportData, "recurring-expense", columns); +// break; + +// case "pdf": +// exportToPDF(exportData, "recurring-expense", columns); +// break; + +// case "print": +// if (tableRef?.current) printTable(tableRef.current); +// break; + +// default: +// console.warn("Unhandled export type:", type); +// break; +// } +// }; + +const handleRecurringExpenseExport = async ( + type, + filters = {}, + searchString = "", + tableRef = null, + setLoading = null +) => { + try { + if (setLoading) setLoading(true); + + const safeSearchString = typeof searchString === "string" ? searchString : ""; + let allRecurringExpense = []; + let pageNumber = 1; + const pageSize = 1000; + let hasMore = true; + + while (hasMore) { + const response = await ExpenseRepository.GetRecurringExpenseList( + pageSize, + pageNumber, + filters, + true, // isActive + safeSearchString + ); + + const currentPageData = response?.data?.data || []; + allRecurringExpense = allRecurringExpense.concat(currentPageData); + + if (currentPageData.length < pageSize) { + hasMore = false; + } else { + pageNumber += 1; + } + } + + if (!allRecurringExpense.length) { + console.warn("No payment requests found!"); + return; + } + + const exportData = allRecurringExpense.map((item) => ({ + Category: item?.expenseCategory?.name ?? "-", + Title: item?.title ?? "-", + Payee: item?.payee ?? "-", + Frequency: + item?.frequency !== undefined && item?.frequency !== null + ? FREQUENCY_FOR_RECURRING[item?.frequency] ?? "-" + : "-", + Amount: item?.amount ? item.amount.toLocaleString() : "-", + Currency: item?.currency?.symbol ?? "-", + "Next Generation Date": item?.nextGenerationDate + ? moment(item.nextGenerationDate).format("DD-MMM-YYYY") : "-", - Amount: item?.amount ? item.amount.toLocaleString() : "-", - Currency: item?.currency?.symbol ?? "-", - "Next Generation Date": item?.nextGenerationDate - ? moment(item.nextGenerationDate).format("DD-MMM-YYYY") - : "-", - Status: item?.status?.name ?? "-", - "Created At": item?.createdAt - ? moment(item.createdAt).format("DD-MMM-YYYY") - : "-", - })); + Status: item?.status?.name ?? "-", + "Created At": item?.createdAt + ? moment(item.createdAt).format("DD-MMM-YYYY") + : "-", + })); - // COLUMN ORDER - const columns = [ - "Category", - "Title", - "Payee", - "Frequency", - "Amount", - "Currency", - "Next Generation Date", - "Status", - "Created At", - ]; + // COLUMN ORDER + const columns = [ + "Category", + "Title", + "Payee", + "Frequency", + "Amount", + "Currency", + "Next Generation Date", + "Status", + "Created At", + ]; - switch (type) { - case "csv": - exportToCSV(exportData, "recurring-expense", columns); - break; + switch (type) { + case "csv": + exportToCSV(exportData, "recurring-expense", columns); + break; + case "excel": + exportToExcel(exportData, "recurring-expense", columns); + break; + case "pdf": + exportToPDF(exportData, "recurring-expense", columns); + break; + case "print": + if (tableRef?.current) printTable(tableRef.current); + break; + default: + console.warn("Unknown export type:", type); + } - case "excel": - exportToExcel(exportData, "recurring-expense", columns); - break; - - case "pdf": - exportToPDF(exportData, "recurring-expense", columns); - break; - - case "print": - if (tableRef?.current) printTable(tableRef.current); - break; - - default: - console.warn("Unhandled export type:", type); - break; + } catch (err) { + console.error("Export failed:", err); + } finally { + if (setLoading) setLoading(false); } }; diff --git a/src/pages/Expense/ExpensePage.jsx b/src/pages/Expense/ExpensePage.jsx index a58efc41..7f895157 100644 --- a/src/pages/Expense/ExpensePage.jsx +++ b/src/pages/Expense/ExpensePage.jsx @@ -118,9 +118,10 @@ const ExpensePage = () => { }; const handleExport = (type) => { - handleExpenseExport(type, filteredData, tableRef); // <-- corrected + handleExpenseExport(type, filters, searchText, tableRef); }; + return (
diff --git a/src/pages/PaymentRequest/PaymentRequestPage.jsx b/src/pages/PaymentRequest/PaymentRequestPage.jsx index 03be604a..19e15455 100644 --- a/src/pages/PaymentRequest/PaymentRequestPage.jsx +++ b/src/pages/PaymentRequest/PaymentRequestPage.jsx @@ -26,11 +26,13 @@ const PaymentRequestPage = () => { const [filters, setFilters] = useState(defaultPaymentRequestFilter); const [filterData, setFilterdata] = useState(null); const [ViewDocument, setDocumentView] = useState({ IsOpen: false, Image: null }); + const [searchText, setSearchText] = useState(""); const [isExpenseGenerate, setIsExpenseGenerate] = useState({ IsOpen: null, RequestId: null }); const [modalSize, setModalSize] = useState("md"); const [search, setSearch] = useState(""); const updatedRef = useRef(); const { setOffcanvasContent, setShowTrigger } = useFab(); + const [exportLoading, setExportLoading] = useState(false); const tableRef = useRef(null); const [filteredData, setFilteredData] = useState([]); const contextValue = { @@ -78,9 +80,9 @@ const PaymentRequestPage = () => { }); }; - const handleExport = (type) => { - handlePaymentRequestExport(type, filteredData, tableRef); // <-- corrected - }; + const handleExport = (type) => { + handlePaymentRequestExport(type, filters, search, tableRef, setExportLoading); +}; return ( diff --git a/src/pages/RecurringExpense/RecurringExpensePage.jsx b/src/pages/RecurringExpense/RecurringExpensePage.jsx index db923657..bd713edc 100644 --- a/src/pages/RecurringExpense/RecurringExpensePage.jsx +++ b/src/pages/RecurringExpense/RecurringExpensePage.jsx @@ -5,7 +5,7 @@ import { useFab } from "../../Context/FabContext"; import ManageRecurringExpense from "../../components/RecurringExpense/ManageRecurringExpense"; import RecurringExpenseList from "../../components/RecurringExpense/RecurringExpenseList"; import { PAYEE_RECURRING_EXPENSE } from "../../utils/constants"; -import { SearchRecurringExpenseSchema } from "../../components/RecurringExpense/RecurringExpenseSchema"; +import { defaultRecurringExpense, SearchRecurringExpenseSchema } from "../../components/RecurringExpense/RecurringExpenseSchema"; import ViewRecurringExpense from "../../components/RecurringExpense/ViewRecurringExpense"; import handleRecurringExpenseExport from "../../components/RecurringExpense/handleRecurringExpenseExport"; @@ -17,7 +17,7 @@ export const useRecurringExpenseContext = () => { "useRecurringExpenseContext must be used within an ExpenseProvider" ); } - return context; + return context; }; const RecurringExpensePage = () => { const [ManageRequest, setManageRequest] = useState({ @@ -27,7 +27,9 @@ const RecurringExpensePage = () => { const tableRef = useRef(null); const [filteredData, setFilteredData] = useState([]); - + const [exportLoading, setExportLoading] = useState(false); + const [searchText, setSearchText] = useState(""); + const [filters, setFilters] = useState(defaultRecurringExpense); const [viewRecurring, setViewRecurring] = useState({ view: false, recurringId: null, @@ -52,7 +54,7 @@ const RecurringExpensePage = () => { const handleExport = (type) => { - handleRecurringExpenseExport(type, filteredData, tableRef); + handleRecurringExpenseExport(type, filters, search, tableRef, setExportLoading); }; return ( From 214a416deb6ecd1e4af804f784d47d7294c96fcc Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Wed, 3 Dec 2025 11:30:41 +0530 Subject: [PATCH 15/18] UI updation in Create and Update Service and Infra projects. --- src/components/ServiceProject/ManageServiceProject.jsx | 6 +++--- src/components/common/SelectMultiple.jsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ServiceProject/ManageServiceProject.jsx b/src/components/ServiceProject/ManageServiceProject.jsx index 1d5b0e66..83e48032 100644 --- a/src/components/ServiceProject/ManageServiceProject.jsx +++ b/src/components/ServiceProject/ManageServiceProject.jsx @@ -183,7 +183,7 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => { {errors.shortName.message} )}
-
+
@@ -200,7 +200,7 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => { {errors.statusId.message} )}
-
+
{ )}
-
+