Refactor_Expenses #321
@ -3,6 +3,8 @@ import { useExpenseList } from "../../hooks/useExpense";
|
|||||||
import Avatar from "../common/Avatar";
|
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 { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||||
|
|
||||||
const ExpenseList = () => {
|
const ExpenseList = () => {
|
||||||
const { setViewExpense } = useExpenseContext();
|
const { setViewExpense } = useExpenseContext();
|
||||||
@ -18,10 +20,9 @@ const ExpenseList = () => {
|
|||||||
endDate: null,
|
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>;
|
if (isLoading) return <div>Loading...</div>;
|
||||||
const items = data ?? [];
|
const items = data.data ?? [];
|
||||||
const totalPages = data?.totalPages ?? 1;
|
const totalPages = data?.totalPages ?? 1;
|
||||||
const hasMore = currentPage < totalPages;
|
const hasMore = currentPage < totalPages;
|
||||||
|
|
||||||
@ -167,7 +168,7 @@ const ExpenseList = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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>
|
<td>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
@ -186,7 +187,7 @@ const ExpenseList = () => {
|
|||||||
<span
|
<span
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setViewExpense({ expenseId: expense.id, view: true })
|
setViewExpense({ expenseId: expense, view: true })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-show "></i>
|
<i className="bx bx-show "></i>
|
||||||
@ -197,52 +198,15 @@ const ExpenseList = () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{!isLoading && items.length > 0 && totalPages > 1 && (
|
{!isLoading && items.length > 0 && (
|
||||||
<nav aria-label="Page ">
|
<Pagination
|
||||||
<ul className="pagination pagination-sm justify-content-end py-1 mx-1">
|
currentPage={currentPage}
|
||||||
<li
|
totalPages={totalPages}
|
||||||
className={`page-item ${currentPage === 1 ? "disabled" : ""}`}
|
onPageChange={paginate}
|
||||||
>
|
/>
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,14 +1,176 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import { useExpense } from '../../hooks/useExpense'
|
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 ViewExpense = ({ ExpenseId }) => {
|
||||||
console.log(ExpenseId)
|
const {
|
||||||
const {} = useExpense(ExpenseId)
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='container'>
|
<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>
|
</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}`);
|
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),
|
CreateExpense:(data)=>api.post("/api/Expense/create",data),
|
||||||
UpdateExpense:(id)=>api.put(`/api/Expanse/edit/${id}`),
|
UpdateExpense:(id)=>api.put(`/api/Expense/edit/${id}`),
|
||||||
DeleteExpense:(id)=>api.delete(`/api/Expanse/edit/${id}`)
|
DeleteExpense:(id)=>api.delete(`/api/Expense/edit/${id}`),
|
||||||
|
|
||||||
|
ActionOnExpense:(data)=>api.post('/api/expense/action',data)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,3 +3,39 @@ export const formatFileSize=(bytes)=> {
|
|||||||
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
|
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
|
||||||
else return (bytes / (1024 * 1024)).toFixed(2) + " MB";
|
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";
|
if (num == null || isNaN(num)) return "NA";
|
||||||
return Number.isInteger(num) ? num : num.toFixed(2);
|
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)=> {
|
export const getCompletionPercentage = (completedWork, plannedWork)=> {
|
||||||
if (!plannedWork || plannedWork === 0) return 0;
|
if (!plannedWork || plannedWork === 0) return 0;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user