Adding Export functionality in Recurring Expense.

This commit is contained in:
Kartik Sharma 2025-11-27 17:23:13 +05:30
parent 359a787bdb
commit b5fb48104c
3 changed files with 241 additions and 117 deletions

View File

@ -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)
/>
)}
<div className="card page-min-h table-responsive px-sm-4">
<div className="card page-min-h table-responsive px-sm-4" ref={tableRef}>
<div className="card-datatable" id="payment-request-table">
<div className="mx-2">
{Array.isArray(filteredData) && filteredData.length > 0 && (
<table className="table border-top dataTable text-nowrap align-middle">
<thead>
<tr>
{recurringExpenseColumns.map((col) => (
<th key={col.key} className={`sorting ${col.align}`}>
{col.label}
</th>
))}
<th className="text-center">Action</th>
</tr>
</thead>
<tbody>
{filteredData?.length > 0 ? (
filteredData?.map((recurringExpense) => (
<tr
key={recurringExpense.id}
className="align-middle"
style={{ height: "40px" }}
>
{recurringExpenseColumns.map((col) => (
<td
key={col.key}
className={`d-table-cell ${col.align ?? ""} py-3`}
>
{col?.customRender
? col?.customRender(recurringExpense)
: col?.getValue(recurringExpense)}
</td>
))}
<td className="sticky-action-column bg-white">
<div className="d-flex flex-row gap-2 gap-0">
<i
className="bx bx-show text-primary cursor-pointer"
onClick={() =>
setViewRecurring({
recurringId: recurringExpense?.id,
view: true,
})
}
></i>
<div className="dropdown z-2">
<button
type="button"
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
data-bs-toggle="dropdown"
>
<i className="bx bx-dots-vertical-rounded text-muted p-0"></i>
</button>
<ul className="dropdown-menu dropdown-menu-end w-auto">
<li
onClick={() =>
setManageRequest({
IsOpen: true,
RecurringId: recurringExpense?.id,
})
}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-edit text-primary bx-xs me-2"></i>
Modify
</a>
</li>
<li
onClick={() => {
setIsDeleteModalOpen(true);
setDeletingId(recurringExpense.id);
}}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-trash text-danger bx-xs me-2"></i>
Delete
</a>
</li>
</ul>
</div>
</div>
</td>
</tr>
))
) : (
<div className="mx-2">
{Array.isArray(filteredData) && filteredData.length > 0 && (
<table className="table border-top dataTable text-nowrap align-middle">
<thead>
<tr>
<td
colSpan={recurringExpenseColumns?.length + 1}
className="text-center border-0 py-8"
></td>
{recurringExpenseColumns.map((col) => (
<th key={col.key} className={`sorting ${col.align}`}>
{col.label}
</th>
))}
<th className="text-end">Action</th>
</tr>
)}
</tbody>
</table>
)}
{!filteredData ||
filteredData.length === 0
&& (
<div className="d-flex justify-content-center align-items-center h-64">
{isError ? (<p>{error.message}</p>):(<p>No Recurring Expense Found</p>)}
</div>
</thead>
<tbody>
{filteredData?.length > 0 ? (
filteredData?.map((recurringExpense) => (
<tr
key={recurringExpense.id}
className="align-middle"
style={{ height: "40px" }}
>
{recurringExpenseColumns.map((col) => (
<td
key={col.key}
className={`d-table-cell ${col.align ?? ""} py-3`}
>
{col?.customRender
? col?.customRender(recurringExpense)
: col?.getValue(recurringExpense)}
</td>
))}
<td className="sticky-action-column bg-white text-end">
<div className="d-flex justify-content-end flex-row gap-2 gap-0">
<i
className="bx bx-show text-primary cursor-pointer"
onClick={() =>
setViewRecurring({
recurringId: recurringExpense?.id,
view: true,
})
}
></i>
<div className="dropdown z-2">
<button
type="button"
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
data-bs-toggle="dropdown"
>
<i className="bx bx-dots-vertical-rounded text-muted p-0"></i>
</button>
<ul className="dropdown-menu dropdown-menu-end w-auto">
<li
onClick={() =>
setManageRequest({
IsOpen: true,
RecurringId: recurringExpense?.id,
})
}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-edit text-primary bx-xs me-2"></i>
Modify
</a>
</li>
<li
onClick={() => {
setIsDeleteModalOpen(true);
setDeletingId(recurringExpense.id);
}}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-trash text-danger bx-xs me-2"></i>
Delete
</a>
</li>
</ul>
</div>
</div>
</td>
</tr>
))
) : (
<tr>
<td
colSpan={recurringExpenseColumns?.length + 1}
className="text-center border-0 py-8"
></td>
</tr>
)}
</tbody>
</table>
)}
</div>
{!filteredData ||
filteredData.length === 0
&& (
<div className="d-flex justify-content-center align-items-center h-64">
{isError ? (<p>{error.message}</p>) : (<p>No Recurring Expense Found</p>)}
</div>
)}
</div>
</div>
{/* Pagination */}

View File

@ -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;

View File

@ -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 (
<RecurringExpenseContext.Provider value={contextValue}>
<div className="container-fluid">
@ -56,8 +67,8 @@ const RecurringExpensePage = () => {
/>
{/* Top Bar */}
<div className="card my-3 px-sm-4 px-0">
<div className="card-body py-2 px-1 mx-2">
<div className="card my-3 px-sm-0 px-0">
<div className="card-body py-2 px-5">
<div className="row align-items-center mb-0">
{/* Left Column: Search + Filter */}
<div className="col-md-8 col-sm-12 mb-2 mb-md-0">
@ -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 = () => {
</div>
</div>
{/* Right Column: Add Button */}
<div className="col-md-4 col-sm-12 text-md-end text-end">
{/* Right Column: Add Button + 3-Dots Menu */}
<div className="col-md-4 col-sm-12 text-md-end text-end d-flex justify-content-end align-items-center gap-0">
<button
className="btn btn-sm btn-primary"
type="button"
@ -116,7 +127,49 @@ const RecurringExpensePage = () => {
Add Recurring Expense
</span>
</button>
{/* 3-Dots Dropdown */}
<div className="dropdown">
<button
className="btn btn-icon"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i className="bx bx-dots-vertical-rounded bx-md"></i>
</button>
<ul className="dropdown-menu dropdown-menu-end shadow-sm" style={{ minWidth: "220px" }}>
<li>
<button className="dropdown-item" onClick={() => handleExport("print")}>
<i className="bx bx-printer me-2"></i> Print
</button>
</li>
<li><hr className="dropdown-divider" /></li>
<li>
<button className="dropdown-item" onClick={() => handleExport("csv")}>
<i className="bx bx-file me-2"></i> CSV
</button>
</li>
<li>
<button className="dropdown-item" onClick={() => handleExport("excel")}>
<i className="bx bxs-file-export me-2"></i> Excel
</button>
</li>
<li>
<button className="dropdown-item" onClick={() => handleExport("pdf")}>
<i className="bx bxs-file-pdf me-2"></i> PDF
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
@ -124,6 +177,8 @@ const RecurringExpensePage = () => {
<RecurringExpenseList
filterStatuses={selectedStatuses}
search={search}
tableRef={tableRef}
onDataFiltered={setFilteredData}
/>
{ManageRequest.IsOpen && (