Refactor_Expenses #321

Merged
pramod.mahajan merged 249 commits from Refactor_Expenses into hotfix/MasterActivity 2025-08-01 13:14:59 +00:00
Showing only changes of commit 81712a0695 - Show all commits

View File

@ -4,14 +4,14 @@ import Avatar from "../common/Avatar";
import { useExpenseContext } from "../../pages/Expense/ExpensePage"; import { useExpenseContext } from "../../pages/Expense/ExpensePage";
import { formatDate, formatUTCToLocalTime } from "../../utils/dateUtils"; import { formatDate, formatUTCToLocalTime } from "../../utils/dateUtils";
import Pagination from "../common/Pagination"; import Pagination from "../common/Pagination";
import { APPROVE_EXPENSE, ITEMS_PER_PAGE } from "../../utils/constants"; import { APPROVE_EXPENSE } from "../../utils/constants";
import { AppColorconfig, getColorNameFromHex } from "../../utils/appUtils"; import { getColorNameFromHex } from "../../utils/appUtils";
import { ExpenseTableSkeleton } from "./ExpenseSkeleton"; import { ExpenseTableSkeleton } from "./ExpenseSkeleton";
import ConfirmModal from "../common/ConfirmModal"; import ConfirmModal from "../common/ConfirmModal";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
const ExpenseList = ({ filters }) => { const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
const [deletingId, setDeletingId] = useState(null); const [deletingId, setDeletingId] = useState(null);
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const { setViewExpense, setManageExpenseModal } = useExpenseContext(); const { setViewExpense, setManageExpenseModal } = useExpenseContext();
@ -21,8 +21,12 @@ const ExpenseList = ({ filters }) => {
const pageSize = 10; const pageSize = 10;
const { mutate: DeleteExpense, isPending } = useDeleteExpense(); const { mutate: DeleteExpense, isPending } = useDeleteExpense();
const { data, isLoading, isError, isInitialLoading, error, isFetching } = const { data, isLoading, isError, isInitialLoading, error } = useExpenseList(
useExpenseList(10, currentPage, filters); pageSize,
currentPage,
filters
);
const SelfId = useSelector( const SelfId = useSelector(
(store) => store?.globalVariables?.loginUser?.employeeInfo?.id (store) => store?.globalVariables?.loginUser?.employeeInfo?.id
); );
@ -40,70 +44,65 @@ const ExpenseList = ({ filters }) => {
); );
}; };
if (isInitialLoading) return <ExpenseTableSkeleton />;
if (isError) return <div>{error}</div>;
const items = data?.data ?? [];
const totalPages = data?.totalPages ?? 1;
const hasMore = currentPage < totalPages;
const paginate = (page) => { const paginate = (page) => {
if (page >= 1 && page <= totalPages) { if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page); setCurrentPage(page);
} }
}; };
const STATUS_ORDER = [
"Draft",
"Review Pending",
"Approval Pending",
"Process Pending",
"Processed",
"Paid",
"Rejected",
];
const groupExpensesByDateAndStatus = (expenses) => { const groupByField = (items, field) => {
const grouped = {}; return items.reduce((acc, item) => {
let key;
expenses.forEach((expense) => { switch (field) {
const dateKey = expense.transactionDate.split("T")[0]; case "transactionDate":
if (!grouped[dateKey]) grouped[dateKey] = []; key = item.transactionDate?.split("T")[0];
grouped[dateKey].push(expense); break;
}); case "status":
key = item.status?.displayName || "Unknown";
const sortedDates = Object.keys(grouped).sort( break;
(a, b) => new Date(b) - new Date(a) case "paidBy":
); key = `${item.paidBy?.firstName ?? ""} ${item.paidBy?.lastName ?? ""}`.trim();
break;
return sortedDates.map((date) => ({ case "project":
date, key = item.project?.name || "Unknown Project";
expenses: grouped[date].sort((a, b) => { break;
return ( case "paymentMode":
STATUS_ORDER.indexOf(a.status.name) - key = item.paymentMode?.name || "Unknown Mode";
STATUS_ORDER.indexOf(b.status.name) break;
); case "expensesType":
}), key = item.expensesType?.name || "Unknown Type";
})); break;
default:
key = "Others";
}
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
}, {});
}; };
if (isInitialLoading) return <ExpenseTableSkeleton />;
if (isError) return <div>{error}</div>;
const grouped = groupBy ? groupByField(data?.data ?? [], groupBy) : { All: data?.data ?? [] };
const IsGroupedByDate = ["transactionDate","createdAt"].includes(groupBy)
return ( return (
<> <>
{IsDeleteModalOpen && ( {IsDeleteModalOpen && (
<div <div
className={`modal fade ${IsDeleteModalOpen ? "show" : ""}`} className={`modal fade show`}
tabIndex="-1" tabIndex="-1"
role="dialog" role="dialog"
style={{ style={{
display: IsDeleteModalOpen ? "block" : "none", display: "block",
backgroundColor: IsDeleteModalOpen backgroundColor: "rgba(0,0,0,0.5)",
? "rgba(0,0,0,0.5)"
: "transparent",
}} }}
aria-hidden="false" aria-hidden="false"
> >
<ConfirmModal <ConfirmModal
type={"delete"} type="delete"
header={"Delete Expense"} header="Delete Expense"
message={"Are you sure you want delete?"} message="Are you sure you want delete?"
onSubmit={handleDelete} onSubmit={handleDelete}
onClose={() => setIsDeleteModalOpen(false)} onClose={() => setIsDeleteModalOpen(false)}
loading={isPending} loading={isPending}
@ -111,20 +110,15 @@ const ExpenseList = ({ filters }) => {
/> />
</div> </div>
)} )}
<div className="card ">
<div className="card">
<div className="card-datatable table-responsive"> <div className="card-datatable table-responsive">
<div <div className="dataTables_wrapper no-footer px-2">
id="DataTables_Table_0_wrapper" <table className="table border-top dataTable text-nowrap">
className="dataTables_wrapper no-footer px-2"
>
<table
className="datatables-users table border-top dataTable no-footer dtr-column text-nowrap px-3"
aria-describedby="DataTables_Table_0_info"
id="horizontal-example"
>
<thead> <thead>
<tr> <tr>
<th {![ groupBy].includes("expensesType") && (
<th
className="sorting sorting_desc d-none d-sm-table-cell" className="sorting sorting_desc d-none d-sm-table-cell"
tabIndex="0" tabIndex="0"
aria-controls="DataTables_Table_0" aria-controls="DataTables_Table_0"
@ -135,7 +129,9 @@ const ExpenseList = ({ filters }) => {
> >
<div className="text-start ms-5">Expense Type</div> <div className="text-start ms-5">Expense Type</div>
</th> </th>
<th )}
{![groupBy].includes("paymentMode") && (
<th
className="sorting sorting_desc d-table-cell" className="sorting sorting_desc d-table-cell"
tabIndex="0" tabIndex="0"
aria-controls="DataTables_Table_0" aria-controls="DataTables_Table_0"
@ -146,7 +142,9 @@ const ExpenseList = ({ filters }) => {
> >
<div className="text-start ">Payment Mode</div> <div className="text-start ">Payment Mode</div>
</th> </th>
<th )}
{![groupBy].includes("paidBy") && (
<th
className="sorting sorting_desc d-table-cell" className="sorting sorting_desc d-table-cell"
tabIndex="0" tabIndex="0"
aria-controls="DataTables_Table_0" aria-controls="DataTables_Table_0"
@ -157,165 +155,117 @@ const ExpenseList = ({ filters }) => {
> >
<div className="text-start ms-5">Paid By</div> <div className="text-start ms-5">Paid By</div>
</th> </th>
)}
<th <th><div className="text-center">Submitted</div></th>
className="sorting sorting_desc d-table-cell" <th className="text-end">Amount</th>
tabIndex="0" {![groupBy].includes("status") && <th>Status</th>}
aria-controls="DataTables_Table_0" <th>Action</th>
rowSpan="1"
colSpan="1"
aria-label="Paid By: activate to sort column ascending"
aria-sort="descending"
>
<div className="text-start ms-5">Submitted</div>
</th>
<th
className="sorting d-table-cell"
tabIndex="0"
aria-controls="DataTables_Table_0"
rowSpan="1"
colSpan="1"
aria-label="Amount: activate to sort column ascending"
>
Amount
</th>
<th
className="sorting"
tabIndex="0"
aria-controls="DataTables_Table_0"
rowSpan="1"
colSpan="1"
aria-label="Status: activate to sort column ascending"
>
Status
</th>
<th
className="sorting"
tabIndex="0"
aria-controls="DataTables_Table_0"
rowSpan="1"
colSpan="1"
aria-label="Status: activate to sort column ascending"
>
Action
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{!isInitialLoading && {Object.entries(grouped).map(([group, expenses]) => (
groupExpensesByDateAndStatus(items).map( <React.Fragment key={group}>
({ date, expenses }) => ( <tr className="tr-group text-dark">
<> <td colSpan={7} className="text-start">
<tr key={`date-${date}`} className="tr-group text-dark"> <strong>{IsGroupedByDate ? formatUTCToLocalTime(group) : group}</strong>
<td colSpan={7} className="text-start"> </td>
<strong>{formatUTCToLocalTime(date)}</strong> </tr>
</td> {expenses.map((expense) => (
</tr>{" "} <tr key={expense.id}>
{expenses.map((expense) => ( {![groupBy].includes("expensesType") && (
<tr key={expense.id}> <td className="text-start d-table-cell ">{expense.expensesType?.name || "N/A"}</td>
<td className="text-start d-table-cell ms-5"> )}
{expense.expensesType?.name || "N/A"} {![groupBy].includes("paymentMode") && (
</td> <td className="text-start d-table-cell ">{expense.paymentMode?.name || "N/A"}</td>
<td className="text-start d-table-cell ms-5"> )}
{expense.paymentMode?.name || "N/A"} {![ groupBy].includes("paidBy") && (
</td> <td className="text-start d-table-cell ">
<td className="text-start d-table-cell ms-5"> <div className="d-flex align-items-center">
<div className="d-flex align-items-center"> <Avatar
<Avatar size="xs"
size="xs" classAvatar="m-0"
classAvatar="m-0" firstName={expense.paidBy?.firstName}
firstName={expense.paidBy?.firstName} lastName={expense.paidBy?.lastName}
lastName={expense.paidBy?.lastName} />
/> <span>
<span> {`${expense.paidBy?.firstName ?? ""} ${
{`${expense.paidBy?.firstName ?? ""} ${ expense.paidBy?.lastName ?? ""
expense.paidBy?.lastName ?? "" }`.trim() || "N/A"}
}`.trim() || "N/A"}
</span>
</div>
</td>
<td className="d-table-cell text-center">
{formatUTCToLocalTime(expense?.createdAt)}
</td>
<td className="d-table-cell text-end">
<i className="bx bx-rupee b-xs"></i>
{expense?.amount}
</td>
<td>
<span
className={`badge bg-label-${
getColorNameFromHex(expense?.status?.color) ||
"secondary"
}`}
>
{expense.status?.displayName || "Unknown"}
</span> </span>
</td> </div>
<td> </td>
<div className="d-flex justify-content-center align-items-center gap-2"> )}
<span className="cursor-pointer"> <td className="d-table-cell text-center">{formatUTCToLocalTime(expense?.createdAt)}</td>
<i <td className="d-table-cell text-end">
className="bx bx-show text-primary" <i className="bx bx-rupee b-xs"></i>
onClick={() => {expense?.amount}
setViewExpense({ </td>
expenseId: expense.id, {![ groupBy].includes("status") && (
view: true, <td>
}) <span
} className={`badge bg-label-${
></i> getColorNameFromHex(expense?.status?.color) ||
</span> "secondary"
}`}
{(expense.status.name === "Draft" || >
expense.status.name === "Rejected") && {expense.status?.name || "Unknown"}
expense.createdBy.id === SelfId && ( </span>
<span className="cursor-pointer"> </td>
<i )}
className="bx bx-edit bx-sm text-secondary" <td>
onClick={() => <div className="d-flex justify-content-center gap-2">
setManageExpenseModal({ <i
IsOpen: true, className="bx bx-show text-primary cursor-pointer"
expenseId: expense.id, onClick={() =>
}) setViewExpense({ expenseId: expense.id, view: true })
} }
></i> ></i>
</span> {(expense.status.name === "Draft" ||
)} expense.status.name === "Rejected") &&
expense.createdBy.id === SelfId && (
{expense.status.name === "Draft" && <i
expense?.createdBy?.id === SelfId && ( className="bx bx-edit text-secondary cursor-pointer"
<span className="cursor-pointer"> onClick={() =>
<i setManageExpenseModal({
className="bx bx-trash bx-sm text-danger" IsOpen: true,
onClick={() => { expenseId: expense.id,
setIsDeleteModalOpen(true); })
setDeletingId(expense.id); }
}} ></i>
></i> )}
</span> {expense.status.name === "Draft" &&
)} expense?.createdBy?.id === SelfId && (
</div> <i
</td> className="bx bx-trash text-danger cursor-pointer"
</tr> onClick={() => {
))} setIsDeleteModalOpen(true);
</> setDeletingId(expense.id);
) }}
)} ></i>
)}
{!isInitialLoading && items.length === 0 && ( </div>
<tr rowSpan={8} style={{ height: "200px" }}> </td>
<td colSpan={8}>No Expnese Found</td> </tr>
))}
</React.Fragment>
))}
{data?.data?.length === 0 && (
<tr>
<td colSpan={8} className="text-center py-4">
No Expense Found
</td>
</tr> </tr>
)} )}
</tbody> </tbody>
</table> </table>
{data?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
)}
</div> </div>
{!isInitialLoading && items.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={paginate}
/>
)}
</div> </div>
</div> </div>
</> </>