Compare commits

..

No commits in common. "6fbc0411db16ef3551c6fef0f7e7472594d4e3fd" and "d6ead0fd4cb04af6da0a26e78d75adcab045262d" have entirely different histories.

3 changed files with 246 additions and 216 deletions

View File

@ -4,16 +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 } from "../../utils/constants"; import { APPROVE_EXPENSE, ITEMS_PER_PAGE } from "../../utils/constants";
import { getColorNameFromHex } from "../../utils/appUtils"; import { AppColorconfig, 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();
@ -23,12 +21,8 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
const pageSize = 10; const pageSize = 10;
const { mutate: DeleteExpense, isPending } = useDeleteExpense(); const { mutate: DeleteExpense, isPending } = useDeleteExpense();
const { data, isLoading, isError, isInitialLoading, error } = useExpenseList( const { data, isLoading, isError, isInitialLoading, error, isFetching } =
pageSize, useExpenseList(10, currentPage, filters);
currentPage,
filters
);
const SelfId = useSelector( const SelfId = useSelector(
(store) => store?.globalVariables?.loginUser?.employeeInfo?.id (store) => store?.globalVariables?.loginUser?.employeeInfo?.id
); );
@ -46,128 +40,70 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
); );
}; };
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 <= (data?.totalPages ?? 1)) { if (page >= 1 && page <= totalPages) {
setCurrentPage(page); setCurrentPage(page);
} }
}; };
const STATUS_ORDER = [
const groupByField = (items, field) => { "Draft",
return items.reduce((acc, item) => { "Review Pending",
let key; "Approval Pending",
switch (field) { "Process Pending",
case "transactionDate": "Processed",
key = item.transactionDate?.split("T")[0]; "Paid",
break; "Rejected",
case "status":
key = item.status?.displayName || "Unknown";
break;
case "paidBy":
key = `${item.paidBy?.firstName ?? ""} ${item.paidBy?.lastName ?? ""}`.trim();
break;
case "project":
key = item.project?.name || "Unknown Project";
break;
case "paymentMode":
key = item.paymentMode?.name || "Unknown Mode";
break;
case "expensesType":
key = item.expensesType?.name || "Unknown Type";
break;
default:
key = "Others";
}
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
}, {});
};
const expenseColumns = [
{
key: "expensesType",
label: "Expense Type",
getValue: (e) => e.expensesType?.name || "N/A",
align:"text-start",
},
{
key: "paymentMode",
label: "Payment Mode",
getValue: (e) => e.paymentMode?.name || "N/A",
align:"text-start"
},
{
key: "paidBy",
label: "Paid By",
align:"text-start",
getValue: (e) =>
`${e.paidBy?.firstName ?? ""} ${e.paidBy?.lastName ?? ""}`.trim() || "N/A",
customRender: (e) => (
<div className="d-flex align-items-center">
<Avatar
size="xs"
classAvatar="m-0"
firstName={e.paidBy?.firstName}
lastName={e.paidBy?.lastName}
/>
<span>
{`${e.paidBy?.firstName ?? ""} ${e.paidBy?.lastName ?? ""}`.trim() || "N/A"}
</span>
</div>
)
},
{
key: "submitted",
label: "Submitted",
getValue: (e) => formatUTCToLocalTime(e?.createdAt),
isAlwaysVisible: true
},
{
key: "amount",
label: "Amount",
getValue: (e) => (
<>
<i className="bx bx-rupee b-xs"></i> {e?.amount}
</>
),
isAlwaysVisible: true,
align: "text-end"
},
{
key: "status",
label: "Status",
align:"text-center",
getValue: (e) => (
<span className={`badge bg-label-${getColorNameFromHex(e?.status?.color) || "secondary"}`}>
{e.status?.name || "Unknown"}
</span>
)
}
]; ];
if (isInitialLoading) return <ExpenseTableSkeleton />; const groupExpensesByDateAndStatus = (expenses) => {
if (isError) return <div>{error}</div>; const grouped = {};
const grouped = groupBy ? groupByField(data?.data ?? [], groupBy) : { All: data?.data ?? [] }; expenses.forEach((expense) => {
const IsGroupedByDate = ["transactionDate", "createdAt"].includes(groupBy); const dateKey = expense.transactionDate.split("T")[0];
if (!grouped[dateKey]) grouped[dateKey] = [];
grouped[dateKey].push(expense);
});
const sortedDates = Object.keys(grouped).sort(
(a, b) => new Date(b) - new Date(a)
);
return sortedDates.map((date) => ({
date,
expenses: grouped[date].sort((a, b) => {
return (
STATUS_ORDER.indexOf(a.status.name) -
STATUS_ORDER.indexOf(b.status.name)
);
}),
}));
};
return ( return (
<> <>
{IsDeleteModalOpen && ( {IsDeleteModalOpen && (
<div <div
className={`modal fade show`} className={`modal fade ${IsDeleteModalOpen ? "show" : ""}`}
tabIndex="-1" tabIndex="-1"
role="dialog" role="dialog"
style={{ style={{
display: "block", display: IsDeleteModalOpen ? "block" : "none",
backgroundColor: "rgba(0,0,0,0.5)" backgroundColor: IsDeleteModalOpen
? "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}
@ -175,102 +111,211 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
/> />
</div> </div>
)} )}
<div className="card ">
<div className="card">
<div className="card-datatable table-responsive"> <div className="card-datatable table-responsive">
<div className="dataTables_wrapper no-footer px-2"> <div
<table className="table border-top dataTable text-nowrap"> id="DataTables_Table_0_wrapper"
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>
{expenseColumns.map( <th
(col) => className="sorting sorting_desc d-none d-sm-table-cell"
(col.isAlwaysVisible || groupBy !== col.key) && ( tabIndex="0"
<th aria-controls="DataTables_Table_0"
key={col.key} rowSpan="1"
className={`sorting d-table-cell`} colSpan="1"
aria-sort="descending" aria-label="Expense Type: activate to sort column ascending"
> aria-sort="descending"
<div className={`${col.align}`}>{col.label}</div> >
</th> <div className="text-start ms-5">Expense Type</div>
) </th>
)} <th
<th>Action</th> className="sorting sorting_desc d-table-cell"
tabIndex="0"
aria-controls="DataTables_Table_0"
rowSpan="1"
colSpan="1"
aria-label="Payment Mode: activate to sort column ascending"
aria-sort="descending"
>
<div className="text-start ">Payment Mode</div>
</th>
<th
className="sorting sorting_desc d-table-cell"
tabIndex="0"
aria-controls="DataTables_Table_0"
rowSpan="1"
colSpan="1"
aria-label="Paid By: activate to sort column ascending"
aria-sort="descending"
>
<div className="text-start ms-5">Paid By</div>
</th>
<th
className="sorting sorting_desc d-table-cell"
tabIndex="0"
aria-controls="DataTables_Table_0"
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>
{Object.entries(grouped).map(([group, expenses]) => ( {!isInitialLoading &&
<React.Fragment key={group}> groupExpensesByDateAndStatus(items).map(
<tr className="tr-group text-dark"> ({ date, expenses }) => (
<td colSpan={8} className="text-start"> <>
<strong>{IsGroupedByDate ? formatUTCToLocalTime(group) : group}</strong> <tr key={`date-${date}`} className="tr-group text-dark">
</td> <td colSpan={7} className="text-start">
</tr> <strong>{formatUTCToLocalTime(date)}</strong>
{expenses.map((expense) => ( </td>
<tr key={expense.id}> </tr>{" "}
{expenseColumns.map( {expenses.map((expense) => (
(col) => <tr key={expense.id}>
(col.isAlwaysVisible || groupBy !== col.key) && ( <td className="text-start d-table-cell ms-5">
<td key={col.key} className={`d-table-cell ${col.align ?? ""}`}> {expense.expensesType?.name || "N/A"}
{col.customRender </td>
? col.customRender(expense) <td className="text-start d-table-cell ms-5">
: col.getValue(expense)} {expense.paymentMode?.name || "N/A"}
</td> </td>
) <td className="text-start d-table-cell ms-5">
)} <div className="d-flex align-items-center">
<td> <Avatar
<div className="d-flex justify-content-center gap-2"> size="xs"
<i classAvatar="m-0"
className="bx bx-show text-primary cursor-pointer" firstName={expense.paidBy?.firstName}
onClick={() => lastName={expense.paidBy?.lastName}
setViewExpense({ expenseId: expense.id, view: true }) />
} <span>
></i> {`${expense.paidBy?.firstName ?? ""} ${
{(expense.status.name === "Draft" || expense.paidBy?.lastName ?? ""
expense.status.name === "Rejected") && }`.trim() || "N/A"}
expense.createdBy.id === SelfId && ( </span>
<i </div>
className="bx bx-edit text-secondary cursor-pointer" </td>
onClick={() => <td className="d-table-cell text-center">
setManageExpenseModal({ {formatUTCToLocalTime(expense?.createdAt)}
IsOpen: true, </td>
expenseId: expense.id <td className="d-table-cell text-end">
}) <i className="bx bx-rupee b-xs"></i>
} {expense?.amount}
></i> </td>
)} <td>
{expense.status.name === "Draft" && <span
expense?.createdBy?.id === SelfId && ( className={`badge bg-label-${
<i getColorNameFromHex(expense?.status?.color) ||
className="bx bx-trash text-danger cursor-pointer" "secondary"
onClick={() => { }`}
setIsDeleteModalOpen(true); >
setDeletingId(expense.id); {expense.status?.displayName || "Unknown"}
}} </span>
></i> </td>
)} <td>
</div> <div className="d-flex justify-content-center align-items-center gap-2">
</td> <span className="cursor-pointer">
</tr> <i
))} className="bx bx-show text-primary"
</React.Fragment> onClick={() =>
))} setViewExpense({
{data?.data?.length === 0 && ( expenseId: expense.id,
<tr> view: true,
<td colSpan={8} className="text-center py-4"> })
No Expense Found }
</td> ></i>
</span>
{(expense.status.name === "Draft" ||
expense.status.name === "Rejected") &&
expense.createdBy.id === SelfId && (
<span className="cursor-pointer">
<i
className="bx bx-edit bx-sm text-secondary"
onClick={() =>
setManageExpenseModal({
IsOpen: true,
expenseId: expense.id,
})
}
></i>
</span>
)}
{expense.status.name === "Draft" &&
expense?.createdBy?.id === SelfId && (
<span className="cursor-pointer">
<i
className="bx bx-trash bx-sm text-danger"
onClick={() => {
setIsDeleteModalOpen(true);
setDeletingId(expense.id);
}}
></i>
</span>
)}
</div>
</td>
</tr>
))}
</>
)
)}
{!isInitialLoading && items.length === 0 && (
<tr rowSpan={8} style={{ height: "200px" }}>
<td colSpan={8}>No Expnese 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>
</> </>
@ -278,4 +323,3 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
}; };
export default ExpenseList; export default ExpenseList;

View File

@ -16,19 +16,7 @@ export const ExpenseSchema = (expenseTypes) => {
expensesTypeId: z.string().min(1, { message: "Expense type is required" }), expensesTypeId: z.string().min(1, { message: "Expense type is required" }),
paymentModeId: z.string().min(1, { message: "Payment mode is required" }), paymentModeId: z.string().min(1, { message: "Payment mode is required" }),
paidById: z.string().min(1, { message: "Employee name is required" }), paidById: z.string().min(1, { message: "Employee name is required" }),
transactionDate: z transactionDate: z.string().min(1, { message: "Date is required" }),
.string()
.min(1, { message: "Date is required" })
.refine((val) => {
const selected = new Date(val);
const today = new Date();
// Set both to midnight to avoid time-related issues
selected.setHours(0, 0, 0, 0);
today.setHours(0, 0, 0, 0);
return selected <= today;
}, { message: "Future dates are not allowed" }),
transactionId: z.string().optional(), transactionId: z.string().optional(),
description: z.string().min(1, { message: "Description is required" }), description: z.string().min(1, { message: "Description is required" }),
location: z.string().min(1, { message: "Location is required" }), location: z.string().min(1, { message: "Location is required" }),

View File

@ -22,7 +22,6 @@ import {
useUpdateExpense, useUpdateExpense,
} from "../../hooks/useExpense"; } from "../../hooks/useExpense";
import ExpenseSkeleton from "./ExpenseSkeleton"; import ExpenseSkeleton from "./ExpenseSkeleton";
import moment from "moment";
const ManageExpense = ({ closeModal, expenseToEdit = null }) => { const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
const { const {
@ -170,8 +169,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
handleClose(); handleClose();
} }
); );
const onSubmit = (fromdata) => { const onSubmit = (payload) => {
let payload = {...fromdata,transactionDate: moment.utc(fromdata.transactionDate, 'YYYY-MM-DD').toISOString()}
if (expenseToEdit) { if (expenseToEdit) {
const editPayload = { ...payload, id: data.id }; const editPayload = { ...payload, id: data.id };
ExpenseUpdate({ id: data.id, payload: editPayload }); ExpenseUpdate({ id: data.id, payload: editPayload });