Feature#791_ViewExpense : View Expense Detailas from calling expense details api #286
@ -190,7 +190,7 @@ const ExpenseList = () => {
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setViewExpense({ expenseId: expense, view: true })
|
||||
setViewExpense({ expenseId:expense.id, view: true })
|
||||
}
|
||||
>
|
||||
<i className="bx bx-show text-primary "></i>
|
||||
|
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 { useActionOnExpense } from "../../hooks/useExpense";
|
||||
import React, { useState } from "react";
|
||||
import { useActionOnExpense, useExpense } from "../../hooks/useExpense";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ActionSchema } from "./ExpenseSchema";
|
||||
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
|
||||
|
||||
const ViewExpense = ({ ExpenseId }) => {
|
||||
const { data, isLoading, isError, error } = useExpense(ExpenseId);
|
||||
const [imageLoaded, setImageLoaded] = useState({});
|
||||
const { setDocumentView } = useExpenseContext();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@ -31,6 +36,20 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
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 (
|
||||
<form className="container px-3" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="row mb-3">
|
||||
@ -40,100 +59,175 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<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>
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Transaction Date :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{ExpenseId.paidBy.firstName} {ExpenseId.paidBy.lastName}
|
||||
{formatUTCToLocalTime(data.transactionDate)}
|
||||
</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">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">Status :</label>
|
||||
<span className="badge" style={{ backgroundColor: ExpenseId.status.color }}>
|
||||
{ExpenseId.status.displayName}
|
||||
<span
|
||||
className="badge"
|
||||
style={{ backgroundColor: data.status.color }}
|
||||
>
|
||||
{data.status.displayName}
|
||||
</span>
|
||||
</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">Pre-Approved :</label>
|
||||
<div className="text-muted">{ExpenseId.preApproved ? "Yes" : "No"}</div>
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Pre-Approved :
|
||||
</label>
|
||||
<div className="text-muted">{data.preApproved ? "Yes" : "No"}</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">Project :</label>
|
||||
<div className="text-muted text-start">{ExpenseId.project.name}</div>
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Project :
|
||||
</label>
|
||||
<div className="text-muted text-start">{data.project.name}</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">Created By :</label>
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Created By :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{ExpenseId.createdBy.firstName} {ExpenseId.createdBy.lastName}
|
||||
{data.createdBy.firstName} {data.createdBy.lastName}
|
||||
</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">Created At :</label>
|
||||
<div className="text-muted">{formatUTCToLocalTime(ExpenseId.createdAt, true)}</div>
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Created At :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{formatUTCToLocalTime(data.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 className="text-muted">{data.description}</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>
|
||||
|
||||
<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">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">Comment:</label>
|
||||
<textarea
|
||||
@ -148,7 +242,7 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
<input type="hidden" {...register("selectedStatus")} />
|
||||
|
||||
<div className="d-flex flex-wrap gap-2 my-2">
|
||||
{ExpenseId.nextStatus.map((status, index) => (
|
||||
{data.nextStatus.map((status, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
|
@ -68,6 +68,21 @@ useEffect(() => {
|
||||
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 (
|
||||
<div
|
||||
@ -80,7 +95,7 @@ useEffect(() => {
|
||||
{...dataAttributesProps}
|
||||
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-header p-0">
|
||||
{/* Close button inside the modal header */}
|
||||
|
@ -6,7 +6,7 @@ import { queryClient } from "../layouts/AuthLayout";
|
||||
// -------------------Query------------------------------------------------------
|
||||
export const useExpenseList = (pageSize, pageNumber, filter) => {
|
||||
return useQuery({
|
||||
queryKey: ["expenses", pageNumber, pageSize, filter],
|
||||
queryKey: ["Expenses", pageNumber, pageSize, filter],
|
||||
queryFn: async () =>
|
||||
await ExpenseRepository.GetExpenseList(pageSize, pageNumber, filter).then(
|
||||
(res) => res.data
|
||||
@ -16,12 +16,18 @@ export const useExpenseList = (pageSize, pageNumber, filter) => {
|
||||
};
|
||||
|
||||
export const useExpense = (ExpenseId) => {
|
||||
|
||||
console.log("ExpenseId:", ExpenseId, "Enabled:", ExpenseId !== undefined && ExpenseId !== null);
|
||||
return useQuery({
|
||||
queryKey: ["Expense", ExpenseId],
|
||||
queryFn: async () => await ExpenseRepository.GetExpenseDetails(ExpenseId),
|
||||
queryFn: async () => await ExpenseRepository.GetExpenseDetails(ExpenseId).then(
|
||||
(res) => res.data
|
||||
),
|
||||
enabled: !!ExpenseId,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// ---------------------------Mutation---------------------------------------------
|
||||
|
||||
export const useCreateExpnse = (onSuccessCallBack) => {
|
||||
|
@ -5,6 +5,7 @@ import CreateExpense from "../../components/Expenses/CreateExpense";
|
||||
import ViewExpense from "../../components/Expenses/ViewExpense";
|
||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||
import GlobalModel from "../../components/common/GlobalModel";
|
||||
import PreviewDocument from "../../components/Expenses/PreviewDocument";
|
||||
|
||||
export const ExpenseContext = createContext();
|
||||
export const useExpenseContext = () => useContext(ExpenseContext);
|
||||
@ -15,9 +16,14 @@ const ExpensePage = () => {
|
||||
expenseId: null,
|
||||
view: false,
|
||||
});
|
||||
const [ViewDocument, setDocumentView] = useState({
|
||||
IsOpen: false,
|
||||
Image: null,
|
||||
});
|
||||
|
||||
const contextValue = {
|
||||
setViewExpense,
|
||||
setDocumentView,
|
||||
};
|
||||
|
||||
return (
|
||||
@ -52,7 +58,7 @@ const ExpensePage = () => {
|
||||
data-bs-custom-class="tooltip"
|
||||
title="Add New Expense"
|
||||
className={`p-1 me-2 bg-primary rounded-circle `}
|
||||
onClick={() => setNewExpense(true)}
|
||||
onClick={() => setNewExpense(true)}
|
||||
>
|
||||
<i className="bx bx-plus fs-4 text-white"></i>
|
||||
</button>
|
||||
@ -63,30 +69,42 @@ const ExpensePage = () => {
|
||||
|
||||
<ExpenseList />
|
||||
|
||||
{isNewExpense && (
|
||||
<GlobalModel
|
||||
isOpen={isNewExpense}
|
||||
size="lg"
|
||||
closeModal={() => setNewExpense(false)}
|
||||
>
|
||||
<CreateExpense closeModal={() => setNewExpense(false)} />
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
{isNewExpense && (
|
||||
<GlobalModel
|
||||
isOpen={isNewExpense}
|
||||
size="lg"
|
||||
closeModal={() => setNewExpense(false)}
|
||||
>
|
||||
<CreateExpense closeModal={() => setNewExpense(false)} />
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
{viewExpense.view && (
|
||||
<GlobalModel
|
||||
isOpen={viewExpense.view}
|
||||
size="lg"
|
||||
modalType="top"
|
||||
closeModal={() =>
|
||||
setViewExpense({
|
||||
expenseId: null,
|
||||
view: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
<ViewExpense ExpenseId={viewExpense.expenseId} />
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
{viewExpense.view && (<GlobalModel
|
||||
isOpen={viewExpense.view}
|
||||
size="lg"
|
||||
closeModal={() =>
|
||||
setViewExpense({
|
||||
expenseId: null,
|
||||
view: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
<ViewExpense ExpenseId={viewExpense.expenseId}/>
|
||||
</GlobalModel>) }
|
||||
{ViewDocument.IsOpen && (
|
||||
<GlobalModel
|
||||
size="lg"
|
||||
m
|
||||
isOpen={ViewDocument.IsOpen}
|
||||
closeModal={() => setDocumentView({ IsOpen: false, Image: null })}
|
||||
>
|
||||
<PreviewDocument imageUrl={ViewDocument.Image} />
|
||||
</GlobalModel>
|
||||
)}
|
||||
</div>
|
||||
</ExpenseContext.Provider>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user