Compare commits
8 Commits
a4f3a6f0ed
...
ab737a80d4
| Author | SHA1 | Date | |
|---|---|---|---|
| ab737a80d4 | |||
| f30430c254 | |||
| fa9736b620 | |||
| 5209b78df7 | |||
| 9bbd2dd014 | |||
| 280715d62f | |||
| 2ab2a1d8bd | |||
| dbdb7a1299 |
@ -254,6 +254,11 @@ const CreateExpense = ({closeModal}) => {
|
|||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
|
{errors.paidById && (
|
||||||
|
<small className="danger-text">
|
||||||
|
{errors.paidById.message}
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -20,8 +20,9 @@ const ExpenseList = () => {
|
|||||||
endDate: null,
|
endDate: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, isLoading, isError } = useExpenseList(ITEMS_PER_PAGE, currentPage, filter);
|
|
||||||
if (isLoading) return <div>Loading...</div>;
|
const { data, isLoading, isError,isInitialLoading } = useExpenseList(2, currentPage, filter);
|
||||||
|
if (isInitialLoading) return <div>Loading...</div>;
|
||||||
const items = data.data ?? [];
|
const items = data.data ?? [];
|
||||||
const totalPages = data?.totalPages ?? 1;
|
const totalPages = data?.totalPages ?? 1;
|
||||||
const hasMore = currentPage < totalPages;
|
const hasMore = currentPage < totalPages;
|
||||||
@ -37,12 +38,13 @@ const ExpenseList = () => {
|
|||||||
<div
|
<div
|
||||||
id="DataTables_Table_0_wrapper"
|
id="DataTables_Table_0_wrapper"
|
||||||
className="dataTables_wrapper no-footer"
|
className="dataTables_wrapper no-footer"
|
||||||
|
|
||||||
>
|
>
|
||||||
<table
|
<table
|
||||||
className="datatables-users table border-top dataTable no-footer dtr-column text-nowrap"
|
className="datatables-users table border-top dataTable no-footer dtr-column text-nowrap"
|
||||||
id="DataTables_Table_0"
|
|
||||||
aria-describedby="DataTables_Table_0_info"
|
aria-describedby="DataTables_Table_0_info"
|
||||||
style={{ width: "100%" }}
|
id="horizontal-example"
|
||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -131,7 +133,7 @@ const ExpenseList = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && items.length === 0 && (
|
{!isInitialLoading && items.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={7} className="text-center py-3">
|
<td colSpan={7} className="text-center py-3">
|
||||||
No expenses found.
|
No expenses found.
|
||||||
@ -139,12 +141,12 @@ const ExpenseList = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading &&
|
{!isInitialLoading &&
|
||||||
items.map((expense) => (
|
items.map((expense) => (
|
||||||
<tr key={expense.id} className="odd">
|
<tr key={expense.id} className="odd">
|
||||||
<td className="sorting_1" colSpan={2}>
|
<td className="sorting_1" colSpan={2}>
|
||||||
<div className="d-flex justify-content-start align-items-center user-name ms-6">
|
<div className="d-flex justify-content-start align-items-center user-name ms-6">
|
||||||
<span>{formatUTCToLocalTime(expense.createdAt)}</span>
|
<span>{formatUTCToLocalTime(expense.transactionDate)}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-start d-none d-sm-table-cell ms-5">
|
<td className="text-start d-none d-sm-table-cell ms-5">
|
||||||
@ -168,7 +170,7 @@ const ExpenseList = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="d-none d-md-table-cell"><i className='bx bx-rupee b-xs'></i>{expense.amount}</td>
|
<td className="d-none d-md-table-cell text-end"><i className='bx bx-rupee b-xs'></i>{expense.amount}</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
@ -183,22 +185,37 @@ const ExpenseList = () => {
|
|||||||
{expense.status?.name || "Unknown"}
|
{expense.status?.name || "Unknown"}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td >
|
||||||
<span
|
<div className="d-flex justify-content-center align-items-center gap-2">
|
||||||
|
<span
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setViewExpense({ expenseId: expense, view: true })
|
setViewExpense({ expenseId:expense.id, view: true })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-show "></i>
|
<i className="bx bx-show text-primary "></i>
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
|
||||||
|
>
|
||||||
|
<i className='bx bx-edit bx-sm text-secondary'></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
|
||||||
|
>
|
||||||
|
<i className='bx bx-trash bx-sm text-danger' ></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{!isLoading && items.length > 0 && (
|
{!isInitialLoading && items.length > 0 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
|
|||||||
@ -67,3 +67,8 @@ export const ExpenseSchema = (expenseTypes) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ActionSchema = z.object({
|
||||||
|
comment : z.string().min(1,{message:"Please leave comment"}),
|
||||||
|
selectedStatus: z.string().min(1, { message: "Please select a status" }),
|
||||||
|
})
|
||||||
28
src/components/Expenses/PreviewDocument.jsx
Normal file
28
src/components/Expenses/PreviewDocument.jsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const PreviewDocument = ({ imageUrl }) => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="d-flex justify-content-center align-items-center" style={{ minHeight: "50vh" }}>
|
||||||
|
{loading && (
|
||||||
|
<div className="text-secondary text-center mb-2">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt="Full View"
|
||||||
|
className="img-fluid"
|
||||||
|
style={{
|
||||||
|
maxHeight: "100vh",
|
||||||
|
objectFit: "contain",
|
||||||
|
display: loading ? "none" : "block",
|
||||||
|
}}
|
||||||
|
onLoad={() => setLoading(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PreviewDocument;
|
||||||
@ -1,11 +1,16 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { useActionOnExpense } from "../../hooks/useExpense";
|
import { useActionOnExpense, useExpense } from "../../hooks/useExpense";
|
||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { ActionSchema } from "./ExpenseSchema";
|
import { ActionSchema } from "./ExpenseSchema";
|
||||||
|
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
|
||||||
|
|
||||||
const ViewExpense = ({ ExpenseId }) => {
|
const ViewExpense = ({ ExpenseId }) => {
|
||||||
|
const { data, isLoading, isError, error } = useExpense(ExpenseId);
|
||||||
|
const [imageLoaded, setImageLoaded] = useState({});
|
||||||
|
const { setDocumentView } = useExpenseContext();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -31,6 +36,20 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
MakeAction(Payload);
|
MakeAction(Payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="d-flex justify-content-center align-items-center"
|
||||||
|
style={{ minHeight: "300px" }}
|
||||||
|
>
|
||||||
|
<div className="fs-5 text-muted">Loading...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const handleImageLoad = (id) => {
|
||||||
|
setImageLoaded((prev) => ({ ...prev, [id]: true }));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="container px-3" onSubmit={handleSubmit(onSubmit)}>
|
<form className="container px-3" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="row mb-3">
|
<div className="row mb-3">
|
||||||
@ -40,100 +59,175 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expense Info Rows */}
|
{/* Expense Info Rows */}
|
||||||
<div className="col-12 col-md-4 mb-3">
|
<div className="col-12 col-sm-6 col-md-4 mb-3">
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">Transaction Date :</label>
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
<div className="text-muted">{formatUTCToLocalTime(ExpenseId.transactionDate)}</div>
|
Transaction Date :
|
||||||
</div>
|
</label>
|
||||||
</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">
|
<div className="text-muted">
|
||||||
{ExpenseId.paidBy.firstName} {ExpenseId.paidBy.lastName}
|
{formatUTCToLocalTime(data.transactionDate)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-4 mb-3">
|
<div className="col-12 col-sm-6 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">{data.expensesType.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 col-sm-6 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">{data.supplerName}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 col-sm-6 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">₹ {data.amount}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 col-sm-6 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">{data.paymentMode.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 col-sm-6 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">
|
||||||
|
{data.paidBy.firstName} {data.paidBy.lastName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 col-sm-6 col-md-4 mb-3">
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">Status :</label>
|
<label className="form-label me-2 mb-0 fw-semibold">Status :</label>
|
||||||
<span className="badge" style={{ backgroundColor: ExpenseId.status.color }}>
|
<span
|
||||||
{ExpenseId.status.displayName}
|
className="badge"
|
||||||
|
style={{ backgroundColor: data.status.color }}
|
||||||
|
>
|
||||||
|
{data.status.displayName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-4 mb-3">
|
<div className="col-12 col-sm-6 col-md-4 mb-3">
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">Pre-Approved :</label>
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
<div className="text-muted">{ExpenseId.preApproved ? "Yes" : "No"}</div>
|
Pre-Approved :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">{data.preApproved ? "Yes" : "No"}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-4 mb-3">
|
<div className="col-12 col-sm-6 col-md-4 mb-3">
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">Project :</label>
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
<div className="text-muted text-start">{ExpenseId.project.name}</div>
|
Project :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted text-start">{data.project.name}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-4 mb-3">
|
<div className="col-12 col-sm-6 col-md-4 mb-3">
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">Created By :</label>
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
|
Created By :
|
||||||
|
</label>
|
||||||
<div className="text-muted">
|
<div className="text-muted">
|
||||||
{ExpenseId.createdBy.firstName} {ExpenseId.createdBy.lastName}
|
{data.createdBy.firstName} {data.createdBy.lastName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-4 mb-3">
|
<div className="col-12 col-sm-6 col-md-4 mb-3">
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">Created At :</label>
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
<div className="text-muted">{formatUTCToLocalTime(ExpenseId.createdAt, true)}</div>
|
Created At :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">
|
||||||
|
{formatUTCToLocalTime(data.createdAt, true)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-start">
|
<div className="text-start">
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">Description:</label>
|
<label className="form-label me-2 mb-0 fw-semibold">Description:</label>
|
||||||
<div className="text-muted">
|
<div className="text-muted">{data.description}</div>
|
||||||
Local travel reimbursement for delivery of materials to client site via City Taxi Service
|
</div>
|
||||||
</div>
|
<div className="col-12 my-2 text-start ">
|
||||||
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
|
Attachement :
|
||||||
|
</label>
|
||||||
|
{data.documents?.map((doc) => (
|
||||||
|
<a
|
||||||
|
className="list-group-item list-group-item-action d-flex align-items-center"
|
||||||
|
key={doc.id}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="rounded me-3 d-flex align-items-center justify-content-center"
|
||||||
|
style={{ height: "50px", width: "80px", position: "relative" }}
|
||||||
|
>
|
||||||
|
{doc.contentType === "application/pdf" ? (
|
||||||
|
<i className="bx bxs-file-pdf"></i>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{!imageLoaded[doc.id] && (
|
||||||
|
<div className="position-absolute text-secondary">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<img
|
||||||
|
src={doc.thumbPreSignedUrl}
|
||||||
|
alt={doc.fileName}
|
||||||
|
className="img-fluid rounded"
|
||||||
|
style={{
|
||||||
|
maxHeight: "100%",
|
||||||
|
maxWidth: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
|
opacity: imageLoaded[doc.id] ? 1 : 0,
|
||||||
|
transition: "opacity 0.3s ease-in-out",
|
||||||
|
}}
|
||||||
|
onLoad={() => handleImageLoad(doc.id)}
|
||||||
|
onClick={() =>
|
||||||
|
setDocumentView({ IsOpen: true, Image: doc.preSignedUrl })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-100">
|
||||||
|
<p className="mb-0">{doc.fileName}</p>
|
||||||
|
<div className="d">
|
||||||
|
<i className="bx bx-cloud-download cursor-pointer"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className="divider my-1" />
|
<hr className="divider my-1" />
|
||||||
|
|
||||||
{Array.isArray(ExpenseId.nextStatus) && ExpenseId.nextStatus.length > 0 && (
|
{Array.isArray(data.nextStatus) && data.nextStatus.length > 0 && (
|
||||||
<div className="col-12 mb-3 text-start">
|
<div className="col-12 mb-3 text-start">
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">Comment:</label>
|
<label className="form-label me-2 mb-0 fw-semibold">Comment:</label>
|
||||||
<textarea
|
<textarea
|
||||||
@ -148,7 +242,7 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
<input type="hidden" {...register("selectedStatus")} />
|
<input type="hidden" {...register("selectedStatus")} />
|
||||||
|
|
||||||
<div className="d-flex flex-wrap gap-2 my-2">
|
<div className="d-flex flex-wrap gap-2 my-2">
|
||||||
{ExpenseId.nextStatus.map((status, index) => (
|
{data.nextStatus.map((status, index) => (
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -68,6 +68,21 @@ useEffect(() => {
|
|||||||
backgroundColor: 'rgba(0,0,0,0.3)',
|
backgroundColor: 'rgba(0,0,0,0.3)',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPositionClass = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'top':
|
||||||
|
return 'mt-3';
|
||||||
|
case 'bottom':
|
||||||
|
return 'mt-auto mb-0 d-flex flex-column';
|
||||||
|
case 'left':
|
||||||
|
return 'position-absolute top-50 start-0 translate-middle-y me-auto';
|
||||||
|
case 'right':
|
||||||
|
return 'position-absolute top-50 end-0 translate-middle-y ms-auto';
|
||||||
|
case 'center':
|
||||||
|
default:
|
||||||
|
return 'modal-dialog-centered';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -80,7 +95,7 @@ useEffect(() => {
|
|||||||
{...dataAttributesProps}
|
{...dataAttributesProps}
|
||||||
style={backdropStyle}
|
style={backdropStyle}
|
||||||
>
|
>
|
||||||
<div className={`modal-dialog ${dialogClass} ${modalSizeClass } mx-sm-auto mx-1`} role={role} >
|
<div className={`modal-dialog ${modalSizeClass} ${getPositionClass(modalType)} ${dialogClass} mx-sm-auto mx-1`} role={role}>
|
||||||
<div className="modal-content">
|
<div className="modal-content">
|
||||||
<div className="modal-header p-0">
|
<div className="modal-header p-0">
|
||||||
{/* Close button inside the modal header */}
|
{/* Close button inside the modal header */}
|
||||||
|
|||||||
@ -1,68 +1,95 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import ExpenseRepository from "../repositories/ExpsenseRepository"
|
import ExpenseRepository from "../repositories/ExpsenseRepository";
|
||||||
import showToast from "../services/toastService"
|
import showToast from "../services/toastService";
|
||||||
import { queryClient } from "../layouts/AuthLayout";
|
import { queryClient } from "../layouts/AuthLayout";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// -------------------Query------------------------------------------------------
|
// -------------------Query------------------------------------------------------
|
||||||
export const useExpenseList=( pageSize, pageNumber, filter ) =>{
|
export const useExpenseList = (pageSize, pageNumber, filter) => {
|
||||||
|
return useQuery({
|
||||||
return useQuery({
|
queryKey: ["Expenses", pageNumber, pageSize, filter],
|
||||||
queryKey: ["expenses", pageNumber, pageSize, filter],
|
queryFn: async () =>
|
||||||
queryFn: async() =>
|
await ExpenseRepository.GetExpenseList(pageSize, pageNumber, filter).then(
|
||||||
await ExpenseRepository.GetExpenseList( pageSize, pageNumber, filter ).then((res) => res.data),
|
(res) => res.data
|
||||||
|
),
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useExpense =(ExpenseId)=>{
|
|
||||||
return useQuery({
|
|
||||||
queryKey:["Expense",ExpenseId],
|
|
||||||
queryFn:async()=>await ExpenseRepository.GetExpenseDetails(ExpenseId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export const useExpense = (ExpenseId) => {
|
||||||
|
|
||||||
|
console.log("ExpenseId:", ExpenseId, "Enabled:", ExpenseId !== undefined && ExpenseId !== null);
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["Expense", ExpenseId],
|
||||||
|
queryFn: async () => await ExpenseRepository.GetExpenseDetails(ExpenseId).then(
|
||||||
|
(res) => res.data
|
||||||
|
),
|
||||||
|
enabled: !!ExpenseId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------Mutation---------------------------------------------
|
// ---------------------------Mutation---------------------------------------------
|
||||||
|
|
||||||
export const useCreateExpnse =(onSuccessCallBack)=>{
|
export const useCreateExpnse = (onSuccessCallBack) => {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async(payload)=>{
|
mutationFn: async (payload) => {
|
||||||
await ExpenseRepository.CreateExpense(payload)
|
await ExpenseRepository.CreateExpense(payload);
|
||||||
},
|
|
||||||
onSuccess:(_,variables)=>{
|
|
||||||
showToast("Expense Created Successfully","success")
|
|
||||||
queryClient.invalidateQueries({queryKey:["expenses"]})
|
|
||||||
if (onSuccessCallBack) onSuccessCallBack();
|
|
||||||
},
|
|
||||||
onError:(error)=>{
|
|
||||||
showToast(error.message || "Something went wrong please try again !","success")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const demoExpense = {
|
|
||||||
projectId: "proj_123",
|
|
||||||
expensesTypeId: "1", // corresponds to Travel
|
|
||||||
paymentModeId: "pm_456",
|
|
||||||
paidById: "emp_789",
|
|
||||||
transactionDate: "2025-07-21",
|
|
||||||
transactionId: "TXN-001234",
|
|
||||||
description: "Taxi fare from airport to hotel",
|
|
||||||
location: "New York",
|
|
||||||
supplerName: "City Taxi Service",
|
|
||||||
amount: 45.50,
|
|
||||||
noOfPersons: 2,
|
|
||||||
billAttachments: [
|
|
||||||
{
|
|
||||||
fileName: "receipt.pdf",
|
|
||||||
base64Data: "JVBERi0xLjQKJcfs...", // truncated base64 example string
|
|
||||||
contentType: "application/pdf",
|
|
||||||
fileSize: 450000, // less than 5MB
|
|
||||||
description: "Taxi receipt",
|
|
||||||
},
|
},
|
||||||
],
|
onSuccess: (_, variables) => {
|
||||||
|
showToast("Expense Created Successfully", "success");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["expenses"] });
|
||||||
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(
|
||||||
|
error.message || "Something went wrong please try again !",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useActionOnExpense = (onSuccessCallBack) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (payload) => {
|
||||||
|
const response = await ExpenseRepository.ActionOnExpense(payload);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: (updatedExpense, variables) => {
|
||||||
|
showToast("Expense updated successfully", "success");
|
||||||
|
|
||||||
|
queryClient.setQueriesData(
|
||||||
|
{
|
||||||
|
queryKey: ["expenses"],
|
||||||
|
exact: false,
|
||||||
|
},
|
||||||
|
(oldData) => {
|
||||||
|
if (!oldData) return oldData;
|
||||||
|
return {
|
||||||
|
...oldData,
|
||||||
|
data: oldData.data.map((item) =>
|
||||||
|
item.id === updatedExpense.id
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
nextStatus: updatedExpense.nextStatus,
|
||||||
|
status: updatedExpense.status,
|
||||||
|
}
|
||||||
|
: item
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(
|
||||||
|
error.message || "Something went wrong, please try again!",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import CreateExpense from "../../components/Expenses/CreateExpense";
|
|||||||
import ViewExpense from "../../components/Expenses/ViewExpense";
|
import ViewExpense from "../../components/Expenses/ViewExpense";
|
||||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||||
import GlobalModel from "../../components/common/GlobalModel";
|
import GlobalModel from "../../components/common/GlobalModel";
|
||||||
|
import PreviewDocument from "../../components/Expenses/PreviewDocument";
|
||||||
|
|
||||||
export const ExpenseContext = createContext();
|
export const ExpenseContext = createContext();
|
||||||
export const useExpenseContext = () => useContext(ExpenseContext);
|
export const useExpenseContext = () => useContext(ExpenseContext);
|
||||||
@ -15,9 +16,14 @@ const ExpensePage = () => {
|
|||||||
expenseId: null,
|
expenseId: null,
|
||||||
view: false,
|
view: false,
|
||||||
});
|
});
|
||||||
|
const [ViewDocument, setDocumentView] = useState({
|
||||||
|
IsOpen: false,
|
||||||
|
Image: null,
|
||||||
|
});
|
||||||
|
|
||||||
const contextValue = {
|
const contextValue = {
|
||||||
setViewExpense,
|
setViewExpense,
|
||||||
|
setDocumentView,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -46,11 +52,15 @@ const ExpensePage = () => {
|
|||||||
<div className="col-7 col-sm-8 text-end gap-2">
|
<div className="col-7 col-sm-8 text-end gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-sm btn-primary me-2"
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-offset="0,8"
|
||||||
|
data-bs-placement="top"
|
||||||
|
data-bs-custom-class="tooltip"
|
||||||
|
title="Add New Expense"
|
||||||
|
className={`p-1 me-2 bg-primary rounded-circle `}
|
||||||
onClick={() => setNewExpense(true)}
|
onClick={() => setNewExpense(true)}
|
||||||
>
|
>
|
||||||
<span className="icon-base bx bx-plus-circle me-1"></span>
|
<i className="bx bx-plus fs-4 text-white"></i>
|
||||||
Add New
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -59,30 +69,42 @@ const ExpensePage = () => {
|
|||||||
|
|
||||||
<ExpenseList />
|
<ExpenseList />
|
||||||
|
|
||||||
{isNewExpense && (
|
{isNewExpense && (
|
||||||
<GlobalModel
|
<GlobalModel
|
||||||
isOpen={isNewExpense}
|
isOpen={isNewExpense}
|
||||||
size="lg"
|
size="lg"
|
||||||
closeModal={() => setNewExpense(false)}
|
closeModal={() => setNewExpense(false)}
|
||||||
>
|
>
|
||||||
<CreateExpense closeModal={() => setNewExpense(false)} />
|
<CreateExpense closeModal={() => setNewExpense(false)} />
|
||||||
</GlobalModel>
|
</GlobalModel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{viewExpense.view && (
|
||||||
|
<GlobalModel
|
||||||
|
isOpen={viewExpense.view}
|
||||||
|
size="lg"
|
||||||
|
modalType="top"
|
||||||
|
closeModal={() =>
|
||||||
|
setViewExpense({
|
||||||
|
expenseId: null,
|
||||||
|
view: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ViewExpense ExpenseId={viewExpense.expenseId} />
|
||||||
|
</GlobalModel>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ViewDocument.IsOpen && (
|
||||||
{viewExpense.view && (<GlobalModel
|
<GlobalModel
|
||||||
isOpen={viewExpense.view}
|
size="lg"
|
||||||
size="lg"
|
m
|
||||||
closeModal={() =>
|
isOpen={ViewDocument.IsOpen}
|
||||||
setViewExpense({
|
closeModal={() => setDocumentView({ IsOpen: false, Image: null })}
|
||||||
expenseId: null,
|
>
|
||||||
view: false,
|
<PreviewDocument imageUrl={ViewDocument.Image} />
|
||||||
})
|
</GlobalModel>
|
||||||
}
|
)}
|
||||||
>
|
|
||||||
<ViewExpense ExpenseId={viewExpense.expenseId}/>
|
|
||||||
</GlobalModel>) }
|
|
||||||
</div>
|
</div>
|
||||||
</ExpenseContext.Provider>
|
</ExpenseContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -17,7 +17,9 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => {
|
|||||||
"checkLists",
|
"checkLists",
|
||||||
"isSystem",
|
"isSystem",
|
||||||
"isActive",
|
"isActive",
|
||||||
"noOfPersonsRequired"
|
"noOfPersonsRequired",
|
||||||
|
"color",
|
||||||
|
"displayName"
|
||||||
];
|
];
|
||||||
|
|
||||||
const safeData = Array.isArray(data) ? data : [];
|
const safeData = Array.isArray(data) ? data : [];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user