Feature#791_ViewExpense : Partially implemented Expense Details modal with full view #276
@ -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)}
|
||||
>
|
||||
«
|
||||
</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)}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
{!isLoading && items.length > 0 && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={paginate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
|
84
src/components/common/Pagination.jsx
Normal file
84
src/components/common/Pagination.jsx
Normal 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)}
|
||||
>
|
||||
«
|
||||
</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)}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user