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 ? (
diff --git a/src/components/AdvancePayment/AdvancePaymentList.jsx b/src/components/AdvancePayment/AdvancePaymentList.jsx index c6e9d004..8d2d131d 100644 --- a/src/components/AdvancePayment/AdvancePaymentList.jsx +++ b/src/components/AdvancePayment/AdvancePaymentList.jsx @@ -1,233 +1,108 @@ +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 { SpinnerLoader } from '../common/Loader'; -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" }}> + + + + {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 ( +
+ +
+ ); } - if (rowsWithBalance.length > 0) { - setBalance(rowsWithBalance[rowsWithBalance.length - 1].balance); - } else { - setBalance(0); - } - }, [employeeId, data, setBalance]); + 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..c3800beb --- /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"; +import { formatFigure } from "../../utils/appUtils"; + +const AdvancePaymentListDetails = ({ employeeId, searchString,tableRef }) => { + 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/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/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 = ({
+
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 }) => { >
{ +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/handleExpenseExport.jsx b/src/components/PaymentRequest/handleExpenseExport.jsx new file mode 100644 index 00000000..0e64302b --- /dev/null +++ b/src/components/PaymentRequest/handleExpenseExport.jsx @@ -0,0 +1,85 @@ +import ExpenseRepository from "../../repositories/ExpsenseRepository"; +import moment from "moment"; +import { exportToCSV, exportToExcel, exportToPDF, printTable } from "../../utils/tableExportUtils"; +import showToast from "../../services/toastService"; + +const HandleExpenseExport = async ( + type, + filters = {}, + searchString = "", + tableRef = null, + setLoading = null +) => { + try { + if (setLoading) setLoading(true); + + const safeSearchString = typeof searchString === "string" ? searchString : ""; + let allExpenses = []; + let pageNumber = 1; + const pageSize = 1000; // fetch 1000 per API call + let hasMore = true; + + while (hasMore) { + const response = await ExpenseRepository.GetExpenseList( + pageSize, + pageNumber, + filters, + safeSearchString + ); + + const currentPageData = response?.data?.data || []; + allExpenses = allExpenses.concat(currentPageData); + + // 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 + } + } + + if (!allExpenses.length) { + showToast("No expenses found!", "warning"); + return; + } + + // 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 ?? "-", + })); + + + 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); + } +}; + +export default HandleExpenseExport; diff --git a/src/components/PaymentRequest/handlePaymentRequestExport.jsx b/src/components/PaymentRequest/handlePaymentRequestExport.jsx new file mode 100644 index 00000000..80edb268 --- /dev/null +++ b/src/components/PaymentRequest/handlePaymentRequestExport.jsx @@ -0,0 +1,84 @@ +import moment from "moment"; +import { exportToCSV, exportToExcel, exportToPDF, printTable } from "../../utils/tableExportUtils"; +import ExpenseRepository from "../../repositories/ExpsenseRepository"; + +const HandlePaymentRequestExport = async ( + type, + filters = {}, + searchString = "", + tableRef = null, + setLoading = null +) => { + try { + if (setLoading) setLoading(true); + + const safeSearchString = typeof searchString === "string" ? searchString : ""; + let allPaymentRequest = []; + let pageNumber = 1; + const pageSize = 1000; + let hasMore = true; + + while (hasMore) { + const response = await ExpenseRepository.GetPaymentRequestList( + pageSize, + pageNumber, + filters, + true, // isActive + safeSearchString + ); + + const currentPageData = response?.data?.data || []; + allPaymentRequest = allPaymentRequest.concat(currentPageData); + + if (currentPageData.length < pageSize) { + hasMore = false; + } else { + pageNumber += 1; + } + } + + if (!allPaymentRequest.length) { + console.warn("No payment requests found!"); + return; + } + + 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 ?? "-", + })); + + 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/Project/Infrastructure/BuildingModel.jsx b/src/components/Project/Infrastructure/BuildingModel.jsx index 9edbd07a..f5818905 100644 --- a/src/components/Project/Infrastructure/BuildingModel.jsx +++ b/src/components/Project/Infrastructure/BuildingModel.jsx @@ -100,7 +100,7 @@ const BuildingModel = ({ project, onClose, editingBuilding = null }) => { return (
-
Manage Buildings
+
Manage Buildings
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/Directory/NotesPage.jsx b/src/pages/Directory/NotesPage.jsx index 45abe8d3..0860132d 100644 --- a/src/pages/Directory/NotesPage.jsx +++ b/src/pages/Directory/NotesPage.jsx @@ -125,8 +125,7 @@ const NotesPage = ({ projectId, searchText, onExport }) => { ) : (

{debouncedSearch diff --git a/src/pages/Expense/ExpensePage.jsx b/src/pages/Expense/ExpensePage.jsx index d7b2b594..fcabf458 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,11 @@ const ExpensePage = () => { removeFilterChip, }; + const handleExport = (type) => { + HandleExpenseExport(type, filters, searchText, tableRef); + }; + + return (

@@ -126,17 +134,19 @@ const ExpensePage = () => {
-
- setSearchText(e.target.value)} - /> +
+
+ setSearchText(e.target.value)} + /> +
-
+
{IsCreatedAble && ( )} + + {/* 3-Dots Dropdown */} +
+ + +
    +
  • + +
  • + +

  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • +
+
+
@@ -163,6 +214,8 @@ const ExpensePage = () => { filters={filters} groupBy={groupBy} searchText={searchText} + tableRef={tableRef} + onDataFiltered={setFilteredData} /> ) : ( diff --git a/src/pages/PaymentRequest/PaymentRequestPage.jsx b/src/pages/PaymentRequest/PaymentRequestPage.jsx index 7b7d2ff1..7c904cf5 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 = () => { @@ -25,12 +26,15 @@ 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 = { setManageRequest, setVieRequest, @@ -76,6 +80,10 @@ const PaymentRequestPage = () => { }); }; + const handleExport = (type) => { + HandlePaymentRequestExport(type, filters, search, tableRef, setExportLoading); +}; + return (
@@ -92,18 +100,19 @@ const PaymentRequestPage = () => {
-
- setSearch(e.target.value)} - /> +
+
+ setSearch(e.target.value)} + /> +
- -
+
+ + {/* 3-Dots Dropdown */} +
+ + +
    +
  • + +
  • + +

  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • +
+
+
+
@@ -129,6 +180,8 @@ const PaymentRequestPage = () => { filterData={filterData} removeFilterChip={handleRemoveChip} clearFilter={clearFilter} + tableRef={tableRef} + onDataFiltered={setFilteredData} /> {/* Add/Edit Modal */} diff --git a/src/pages/RecurringExpense/RecurringExpensePage.jsx b/src/pages/RecurringExpense/RecurringExpensePage.jsx index 12c07e10..1097ebd3 100644 --- a/src/pages/RecurringExpense/RecurringExpensePage.jsx +++ b/src/pages/RecurringExpense/RecurringExpensePage.jsx @@ -1,12 +1,13 @@ -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"; 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"; export const RecurringExpenseContext = createContext(); export const useRecurringExpenseContext = () => { @@ -16,13 +17,19 @@ export const useRecurringExpenseContext = () => { "useRecurringExpenseContext must be used within an ExpenseProvider" ); } - return context; + return context; }; const RecurringExpensePage = () => { const [ManageRequest, setManageRequest] = useState({ IsOpen: null, RecurringId: null, }); + 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, @@ -44,6 +51,12 @@ const RecurringExpensePage = () => { prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id] ); }; + + + const handleExport = (type) => { + HandleRecurringExpenseExport(type, filters, search, tableRef, setExportLoading); + }; + return (
@@ -56,8 +69,8 @@ const RecurringExpensePage = () => { /> {/* Top Bar */} -
-
+
+
{/* Left Column: Search + Filter */}
@@ -67,7 +80,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 +112,8 @@ const RecurringExpensePage = () => {
- {/* Right Column: Add Button */} -
+ {/* Right Column: Add Button + 3-Dots Menu */} +
+ + {/* 3-Dots Dropdown */} +
+ + +
    +
  • + +
  • + +

  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • +
+
+
+
@@ -124,6 +179,8 @@ const RecurringExpensePage = () => { {ManageRequest.IsOpen && ( 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 +
  • + + +

  • + +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
}, { 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: }, @@ -120,8 +120,9 @@ 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: "/collection", element: }, // Purchases and Inventory diff --git a/src/utils/exportUtils.js b/src/utils/exportUtils.js deleted file mode 100644 index 40f7d423..00000000 --- a/src/utils/exportUtils.js +++ /dev/null @@ -1,27 +0,0 @@ -// utils/exportUtils.js -export const exportToCSV = (data, filename = "export.csv") => { - 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(); -}; 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