Merge pull request 'Feature#791_ViewExpense : Partially implemented Expense Details modal with full view' (#276) from Feature#791_ViewExpense into Feature_Expense

Reviewed-on: #276
merged
This commit is contained in:
pramod.mahajan 2025-07-22 08:42:00 +00:00
commit dbdb7a1299
6 changed files with 320 additions and 68 deletions

View File

@ -3,6 +3,8 @@ import { useExpenseList } from "../../hooks/useExpense";
import Avatar from "../common/Avatar";
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
import { formatDate, formatUTCToLocalTime } from "../../utils/dateUtils";
import Pagination from "../common/Pagination";
import { ITEMS_PER_PAGE } from "../../utils/constants";
const ExpenseList = () => {
const { setViewExpense } = useExpenseContext();
@ -18,10 +20,9 @@ const ExpenseList = () => {
endDate: null,
};
const { data, isLoading, isError } = useExpenseList(5, currentPage, filter);
const { data, isLoading, isError } = useExpenseList(ITEMS_PER_PAGE, currentPage, filter);
if (isLoading) return <div>Loading...</div>;
const items = data ?? [];
const items = data.data ?? [];
const totalPages = data?.totalPages ?? 1;
const hasMore = currentPage < totalPages;
@ -167,7 +168,7 @@ const ExpenseList = () => {
</span>
</div>
</td>
<td className="d-none d-md-table-cell">{expense.amount}</td>
<td className="d-none d-md-table-cell"><i className='bx bx-rupee b-xs'></i>{expense.amount}</td>
<td>
<span
style={{
@ -186,7 +187,7 @@ const ExpenseList = () => {
<span
className="cursor-pointer"
onClick={() =>
setViewExpense({ expenseId: expense.id, view: true })
setViewExpense({ expenseId: expense, view: true })
}
>
<i className="bx bx-show "></i>
@ -197,52 +198,15 @@ const ExpenseList = () => {
</tbody>
</table>
</div>
{!isLoading && items.length > 0 && totalPages > 1 && (
<nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1 mx-1">
<li
className={`page-item ${currentPage === 1 ? "disabled" : ""}`}
>
<button
className="page-link btn-xs"
onClick={() => paginate(currentPage - 1)}
>
&laquo;
</button>
</li>
{[...Array(totalPages)].map((_, index) => (
<li
key={index}
className={`page-item ${
currentPage === index + 1 ? "active" : ""
}`}
>
<button
className="page-link"
onClick={() => paginate(index + 1)}
>
{index + 1}
</button>
</li>
))}
<li
className={`page-item ${
currentPage === totalPages ? "disabled" : ""
}`}
>
<button
className="page-link"
onClick={() => paginate(currentPage + 1)}
>
&raquo;
</button>
</li>
</ul>
</nav>
)}
{!isLoading && items.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={paginate}
/>
)}
</div>
</div>
);
};

View File

@ -1,14 +1,176 @@
import React from 'react'
import { useExpense } from '../../hooks/useExpense'
import React from "react";
import { useActionOnExpense } from "../../hooks/useExpense";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { ActionSchema } from "./ExpenseSchema";
const ViewExpense = ({ ExpenseId }) => {
const {
register,
handleSubmit,
setValue,
formState: { errors },
} = useForm({
resolver: zodResolver(ActionSchema),
defaultValues: {
comment: "",
selectedStatus: "",
},
});
const { mutate: MakeAction } = useActionOnExpense();
const onSubmit = (formData) => {
const Payload = {
expenseId: ExpenseId?.id,
statusId: formData.selectedStatus,
comment: formData.comment,
};
MakeAction(Payload);
};
const ViewExpense = ({ExpenseId}) => {
console.log(ExpenseId)
const {} = useExpense(ExpenseId)
return (
<div className='container'>
</div>
)
}
<form className="container px-3" onSubmit={handleSubmit(onSubmit)}>
<div className="row mb-3">
<div className="col-12 mb-3">
<h5 className="fw-semibold">Expense Details</h5>
<hr />
</div>
export default ViewExpense
{/* Expense Info Rows */}
<div className="col-12 col-md-4 mb-3">
<div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Transaction Date :</label>
<div className="text-muted">{formatUTCToLocalTime(ExpenseId.transactionDate)}</div>
</div>
</div>
<div className="col-12 col-md-4 mb-3">
<div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Expense Type :</label>
<div className="text-muted">{ExpenseId.expensesType.name}</div>
</div>
</div>
<div className="col-12 col-md-4 mb-3">
<div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Supplier :</label>
<div className="text-muted">{ExpenseId.supplerName}</div>
</div>
</div>
<div className="col-12 col-md-4 mb-3">
<div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Amount :</label>
<div className="text-muted"> {ExpenseId.amount}</div>
</div>
</div>
<div className="col-12 col-md-4 mb-3">
<div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Payment Mode :</label>
<div className="text-muted">{ExpenseId.paymentMode.name}</div>
</div>
</div>
<div className="col-12 col-md-4 mb-3">
<div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Paid By :</label>
<div className="text-muted">
{ExpenseId.paidBy.firstName} {ExpenseId.paidBy.lastName}
</div>
</div>
</div>
<div className="col-12 col-md-4 mb-3">
<div className="d-flex align-items-center">
<label className="form-label me-2 mb-0 fw-semibold">Status :</label>
<span className="badge" style={{ backgroundColor: ExpenseId.status.color }}>
{ExpenseId.status.displayName}
</span>
</div>
</div>
<div className="col-12 col-md-4 mb-3">
<div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Pre-Approved :</label>
<div className="text-muted">{ExpenseId.preApproved ? "Yes" : "No"}</div>
</div>
</div>
<div className="col-12 col-md-4 mb-3">
<div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Project :</label>
<div className="text-muted text-start">{ExpenseId.project.name}</div>
</div>
</div>
<div className="col-12 col-md-4 mb-3">
<div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Created By :</label>
<div className="text-muted">
{ExpenseId.createdBy.firstName} {ExpenseId.createdBy.lastName}
</div>
</div>
</div>
<div className="col-12 col-md-4 mb-3">
<div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Created At :</label>
<div className="text-muted">{formatUTCToLocalTime(ExpenseId.createdAt, true)}</div>
</div>
</div>
</div>
<div className="text-start">
<label className="form-label me-2 mb-0 fw-semibold">Description:</label>
<div className="text-muted">
Local travel reimbursement for delivery of materials to client site via City Taxi Service
</div>
</div>
<hr className="divider my-1" />
{Array.isArray(ExpenseId.nextStatus) && ExpenseId.nextStatus.length > 0 && (
<div className="col-12 mb-3 text-start">
<label className="form-label me-2 mb-0 fw-semibold">Comment:</label>
<textarea
className="form-control form-control-sm"
{...register("comment")}
rows="2"
/>
{errors.comment && (
<small className="danger-text">{errors.comment.message}</small>
)}
<input type="hidden" {...register("selectedStatus")} />
<div className="d-flex flex-wrap gap-2 my-2">
{ExpenseId.nextStatus.map((status, index) => (
<button
key={index}
type="button"
onClick={() => {
setValue("selectedStatus", status.id);
handleSubmit(onSubmit)();
}}
className="badge cursor-pointer border-0"
style={{
backgroundColor: status.color || "#6c757d",
color: "#fff",
fontSize: "0.85rem",
}}
>
{status.displayName || status.name}
</button>
))}
</div>
</div>
)}
</form>
);
};
export default ViewExpense;

View File

@ -0,0 +1,84 @@
import React from "react";
const getPaginationRange = (currentPage, totalPages, delta = 1) => {
const range = [];
const rangeWithDots = [];
let l;
for (let i = 1; i <= totalPages; i++) {
if (
i === 1 ||
i === totalPages ||
(i >= currentPage - delta && i <= currentPage + delta)
) {
range.push(i);
}
}
for (let i of range) {
if (l) {
if (i - l === 2) {
rangeWithDots.push(l + 1);
} else if (i - l !== 1) {
rangeWithDots.push("...");
}
}
rangeWithDots.push(i);
l = i;
}
return rangeWithDots;
};
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
if (totalPages <= 1) return null;
const paginationRange = getPaginationRange(currentPage, totalPages);
return (
<nav aria-label="Page navigation">
<ul className="pagination pagination-sm justify-content-end py-1 mx-1">
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
<button
className="page-link btn-xs"
onClick={() => onPageChange(currentPage - 1)}
>
&laquo;
</button>
</li>
{paginationRange.map((page, index) => (
<li
key={index}
className={`page-item ${
page === currentPage ? "active" : ""
} ${page === "..." ? "disabled" : ""}`}
>
{page === "..." ? (
<span className="page-link"></span>
) : (
<button className="page-link" onClick={() => onPageChange(page)}>
{page}
</button>
)}
</li>
))}
<li
className={`page-item ${
currentPage === totalPages ? "disabled" : ""
}`}
>
<button
className="page-link"
onClick={() => onPageChange(currentPage + 1)}
>
&raquo;
</button>
</li>
</ul>
</nav>
);
};
export default Pagination;

View File

@ -10,10 +10,12 @@ const ExpenseRepository = {
return api.get(`/api/expense/list?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}`);
},
GetExpenseDetails:(id)=>api.get(`/api/Expanse/details/${id}`),
GetExpenseDetails:(id)=>api.get(`/api/Expense/details/${id}`),
CreateExpense:(data)=>api.post("/api/Expense/create",data),
UpdateExpense:(id)=>api.put(`/api/Expanse/edit/${id}`),
DeleteExpense:(id)=>api.delete(`/api/Expanse/edit/${id}`)
UpdateExpense:(id)=>api.put(`/api/Expense/edit/${id}`),
DeleteExpense:(id)=>api.delete(`/api/Expense/edit/${id}`),
ActionOnExpense:(data)=>api.post('/api/expense/action',data)
}

View File

@ -2,4 +2,40 @@ export const formatFileSize=(bytes)=> {
if (bytes < 1024) return bytes + " B";
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
else return (bytes / (1024 * 1024)).toFixed(2) + " MB";
}
}
export const getExpenseIcon = (type) => {
switch (type.toLowerCase()) {
case 'vendor/supplier payments':
return 'bx-briefcase'; // Business-related
case 'transport':
return 'bx-car'; // Vehicle or logistics
case 'compliance & safety':
return 'bx-shield-quarter'; // Security/safety
case 'mobilization':
return 'bx-building-house'; // Setup / site infra
case 'procurement':
return 'bx-package'; // Box/package/supplies
case 'maintenance & utilities':
return 'bx-wrench'; // Repair/maintenance
case 'travelling':
return 'bx-plane'; // Personnel delivery
case 'employee welfare':
return 'bx-user-heart'; // Welfare / people
default:
return 'bx-folder'; // Fallback icon
}
};
export const getPaymentModeIcon = (mode) => {
switch (mode.toLowerCase()) {
case 'cash':
return 'bx-money'; // Cash/coins
case 'upi':
return 'bx-mobile-alt'; // Mobile payment
case 'cheque':
return 'bx-receipt'; // Paper receipt
case 'netbanking':
return 'bx-globe'; // Online/internet
default:
return 'bx-credit-card'; // Generic fallback
}
};

View File

@ -67,9 +67,13 @@ export const formatNumber = (num) => {
if (num == null || isNaN(num)) return "NA";
return Number.isInteger(num) ? num : num.toFixed(2);
};
export const formatUTCToLocalTime = (datetime) =>{
return moment.utc(datetime).local().format("DD MMMM YYYY hh:mm A");
}
export const formatUTCToLocalTime = (datetime, timeRequired = false) => {
return timeRequired
? moment.utc(datetime).local().format("DD MMMM YYYY hh:mm A")
: moment.utc(datetime).local().format("DD MMMM YYYY");
};
export const getCompletionPercentage = (completedWork, plannedWork)=> {
if (!plannedWork || plannedWork === 0) return 0;