Merge branch 'upgrade_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into Recurring_Expense

This commit is contained in:
Kartik Sharma 2025-11-04 15:43:10 +05:30
commit 76fe342c1e
8 changed files with 534 additions and 439 deletions

View File

@ -13,7 +13,7 @@ const ExpenseByProject = () => {
const [viewMode, setViewMode] = useState("Category"); const [viewMode, setViewMode] = useState("Category");
const [chartData, setChartData] = useState({ categories: [], data: [] }); const [chartData, setChartData] = useState({ categories: [], data: [] });
const { ExpenseTypes, loading: typeLoading } = useExpenseCategory(); const { ExpenseCategories, loading: typeLoading } = useExpenseCategory();
const { data: expenseApiData, isLoading } = useExpenseDataByProject( const { data: expenseApiData, isLoading } = useExpenseDataByProject(
projectId, projectId,
@ -133,7 +133,7 @@ const ExpenseByProject = () => {
style={{ maxWidth: "200px" }} style={{ maxWidth: "200px" }}
> >
<option value="">All Types</option> <option value="">All Types</option>
{ExpenseTypes.map((type) => ( {ExpenseCategories?.map((type) => (
<option key={type.id} value={type.id}> <option key={type.id} value={type.id}>
{type.name} {type.name}
</option> </option>

View File

@ -2,9 +2,10 @@ import { useState, useMemo } from "react";
import Avatar from "../common/Avatar"; import Avatar from "../common/Avatar";
import { formatUTCToLocalTime } from "../../utils/dateUtils"; import { formatUTCToLocalTime } from "../../utils/dateUtils";
import Timeline from "../common/TimeLine"; import Timeline from "../common/TimeLine";
import moment from "moment";
import { getColorNameFromHex } from "../../utils/appUtils";
const ExpenseStatusLogs = ({ data }) => { const ExpenseStatusLogs = ({ data }) => {
const [visibleCount, setVisibleCount] = useState(4); const [visibleCount, setVisibleCount] = useState(4);
const sortedLogs = useMemo(() => { const sortedLogs = useMemo(() => {
if (!data?.updateLogs) return []; if (!data?.updateLogs) return [];
@ -18,32 +19,33 @@ const ExpenseStatusLogs = ({ data }) => {
[sortedLogs, visibleCount] [sortedLogs, visibleCount]
); );
const timelineData = useMemo(() => { const timelineData = useMemo(() => {
return logsToShow.map((log, index) => ({ return logsToShow.map((log, index) => ({
id: index + 1, id: index + 1,
title: log.nextStatus?.name || "Status Updated", title: log.nextStatus?.name || "Status Updated",
description: log.nextStatus?.description || "", description: log.nextStatus?.description || "",
timeAgo: formatTimeAgo(log.updatedAt), timeAgo: moment.utc(log?.updatedAt).local().fromNow(),
color: log.nextStatus?.color || "primary", color: getColorNameFromHex(log.nextStatus?.color) || "primary",
users: log.updatedBy users: log.updatedBy
? [ ? [
{ {
name: `${log.updatedBy.firstName || ""} ${` log?.updatedBy?.lastName` || ""}`.trim(), firstName: log.updatedBy.firstName || "",
lastName: log?.updatedBy?.lastName || "",
role: log.updatedBy.jobRoleName || "", role: log.updatedBy.jobRoleName || "",
avatar: log.updatedBy.photo || "assets/img/avatars/default.png", avatar: log.updatedBy.photo,
}, },
] ]
: [], : [],
})); }));
}, [logsToShow]) }, [logsToShow]);
const handleShowMore = () => { const handleShowMore = () => {
setVisibleCount((prev) => prev + 4); setVisibleCount((prev) => prev + 4);
}; };
return ( return (
<> <div className="page-min-h overflow-auto">
<div className="row g-2"> {/* <div className="row g-2">
{logsToShow.map((log) => ( {logsToShow.map((log) => (
<div key={log.id} className="col-12 d-flex align-items-start mb-1"> <div key={log.id} className="col-12 d-flex align-items-start mb-1">
<Avatar <Avatar
@ -81,13 +83,11 @@ const timelineData = useMemo(() => {
Show More Show More
</button> </button>
</div> </div>
)} )} */}
<Timeline items={timelineData} /> <Timeline items={timelineData} />
</> </div>
); );
}; };
export default ExpenseStatusLogs; export default ExpenseStatusLogs;

View File

@ -30,7 +30,7 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
const { profile } = useProfile(); const { profile } = useProfile();
const schema = PaymentRequestSchema(ExpenseTypes); const schema = PaymentRequestSchema(ExpenseCategories);
const { register, control, watch, handleSubmit, setValue, reset, formState: { errors }, } = useForm({ const { register, control, watch, handleSubmit, setValue, reset, formState: { errors }, } = useForm({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: defaultPaymentRequest, defaultValues: defaultPaymentRequest,
@ -492,7 +492,7 @@ setValue('payee',profile?.employeeInfo.id)
<button <button
type="reset" type="reset"
disabled={createPending} disabled={createPending || isPending}
onClick={handleClose} onClick={handleClose}
className="btn btn-label-secondary btn-sm mt-3" className="btn btn-label-secondary btn-sm mt-3"
> >
@ -501,9 +501,9 @@ setValue('payee',profile?.employeeInfo.id)
<button <button
type="submit" type="submit"
className="btn btn-primary btn-sm mt-3" className="btn btn-primary btn-sm mt-3"
disabled={createPending} disabled={createPending || isPending}
> >
{createPending {createPending || isPending
? "Please Wait..." ? "Please Wait..."
: requestToEdit : requestToEdit
? "Update" ? "Update"

View File

@ -1,5 +1,9 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { EXPENSE_DRAFT, EXPENSE_REJECTEDBY, ITEMS_PER_PAGE } from "../../utils/constants"; import {
EXPENSE_DRAFT,
EXPENSE_REJECTEDBY,
ITEMS_PER_PAGE,
} from "../../utils/constants";
import { import {
formatCurrency, formatCurrency,
getColorNameFromHex, getColorNameFromHex,
@ -13,6 +17,7 @@ import { ExpenseTableSkeleton } from "../Expenses/ExpenseSkeleton";
import ConfirmModal from "../common/ConfirmModal"; import ConfirmModal from "../common/ConfirmModal";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import Error from "../common/Error";
const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => { const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
const { setManageRequest, setVieRequest } = usePaymentRequestContext(); const { setManageRequest, setVieRequest } = usePaymentRequestContext();
@ -37,8 +42,9 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
displayField = "Status"; displayField = "Status";
break; break;
case "submittedBy": case "submittedBy":
key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? "" key = `${item?.createdBy?.firstName ?? ""} ${
}`.trim(); item.createdBy?.lastName ?? ""
}`.trim();
displayField = "Submitted By"; displayField = "Submitted By";
break; break;
case "project": case "project":
@ -91,8 +97,9 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
label: "Submitted By", label: "Submitted By",
align: "text-start", align: "text-start",
getValue: (e) => getValue: (e) =>
`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? "" `${e.createdBy?.firstName ?? ""} ${
}`.trim() || "N/A", e.createdBy?.lastName ?? ""
}`.trim() || "N/A",
customRender: (e) => ( customRender: (e) => (
<div <div
className="d-flex align-items-center cursor-pointer" className="d-flex align-items-center cursor-pointer"
@ -105,8 +112,9 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
lastName={e.createdBy?.lastName} lastName={e.createdBy?.lastName}
/> />
<span className="text-truncate"> <span className="text-truncate">
{`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? "" {`${e.createdBy?.firstName ?? ""} ${
}`.trim() || "N/A"} e.createdBy?.lastName ?? ""
}`.trim() || "N/A"}
</span> </span>
</div> </div>
), ),
@ -135,8 +143,9 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
align: "text-center", align: "text-center",
getValue: (e) => ( getValue: (e) => (
<span <span
className={`badge bg-label-${getColorNameFromHex(e?.expenseStatus?.color) || "secondary" className={`badge bg-label-${
}`} getColorNameFromHex(e?.expenseStatus?.color) || "secondary"
}`}
> >
{e?.expenseStatus?.name || "Unknown"} {e?.expenseStatus?.name || "Unknown"}
</span> </span>
@ -147,24 +156,20 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const debouncedSearch = useDebounce(search, 500); const debouncedSearch = useDebounce(search, 500);
const { data, isLoading, isError, error, isFetching } = usePaymentRequestList( const { data, isLoading, isError, error, isRefetching, refetch } =
ITEMS_PER_PAGE, usePaymentRequestList(
currentPage, ITEMS_PER_PAGE,
filters, currentPage,
true, filters,
debouncedSearch true,
); debouncedSearch
);
const paymentRequestData = data?.data || []; const paymentRequestData = data?.data || [];
const totalPages = data?.data?.totalPages || 1; const totalPages = data?.data?.totalPages || 1;
if (isError) { if (isError) {
return ( return <Error error={error} isFeteching={isRefetching} refetch={refetch} />;
<div className="text-center text-danger py-5">
<p>Failed to load payment requests.</p>
<small>{error?.message || "Something went wrong."}</small>
</div>
);
} }
const header = [ const header = [
"Request ID", "Request ID",
@ -188,13 +193,14 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
const canEditExpense = (paymentRequest) => { const canEditExpense = (paymentRequest) => {
return ( return (
(paymentRequest?.expenseStatus?.id === EXPENSE_DRAFT || (paymentRequest?.expenseStatus?.id === EXPENSE_DRAFT ||
EXPENSE_REJECTEDBY.includes(paymentRequest?.expenseStatus?.id)) && EXPENSE_REJECTEDBY.includes(paymentRequest?.expenseStatus.id)) &&
paymentRequest?.createdBy?.id === SelfId paymentRequest?.createdBy?.id === SelfId
); );
}; };
const canDetetExpense = (request) => { const canDetetExpense = (request) => {
return ( return (
request?.expenseStatus?.id === EXPENSE_DRAFT && request?.createdBy?.id === SelfId request?.expenseStatus?.id === EXPENSE_DRAFT &&
request?.createdBy?.id === SelfId
); );
}; };
@ -282,39 +288,41 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
}) })
} }
></i> ></i>
{canDetetExpense(paymentRequest) && {canEditExpense(paymentRequest) && (
canEditExpense(paymentRequest) && ( <div className="dropdown z-2">
<div className="dropdown z-2"> <button
<button type="button"
type="button" className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0" data-bs-toggle="dropdown"
data-bs-toggle="dropdown" aria-expanded="false"
aria-expanded="false" >
<i
className="bx bx-dots-vertical-rounded text-muted p-0"
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip-dark"
title="More Action"
></i>
</button>
<ul className="dropdown-menu dropdown-menu-end w-auto">
<li
onClick={() =>
setManageRequest({
IsOpen: true,
RequestId: paymentRequest.id,
})
}
> >
<i <a className="dropdown-item px-2 cursor-pointer py-1">
className="bx bx-dots-vertical-rounded text-muted p-0" <i className="bx bx-edit text-primary bx-xs me-2"></i>
data-bs-toggle="tooltip" <span className="align-left ">
data-bs-offset="0,8" Modify
data-bs-placement="top" </span>
data-bs-custom-class="tooltip-dark" </a>
title="More Action" </li>
></i>
</button>
<ul className="dropdown-menu dropdown-menu-end w-auto">
<li
onClick={() =>
setManageRequest({
IsOpen: true,
RequestId: paymentRequest.id,
})
}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-edit text-primary bx-xs me-2"></i>
<span className="align-left ">Modify</span>
</a>
</li>
{canDetetExpense(paymentRequest) && (
<li <li
onClick={() => { onClick={() => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
@ -323,11 +331,15 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
> >
<a className="dropdown-item px-2 cursor-pointer py-1"> <a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-trash text-danger bx-xs me-2"></i> <i className="bx bx-trash text-danger bx-xs me-2"></i>
<span className="align-left">Delete</span> <span className="align-left">
Delete
</span>
</a> </a>
</li> </li>
</ul> )}
</div>)} </ul>
</div>
)}
</div> </div>
</td> </td>
</tr> </tr>
@ -355,8 +367,9 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
{[...Array(totalPages)].map((_, index) => ( {[...Array(totalPages)].map((_, index) => (
<li <li
key={index} key={index}
className={`page-item ${currentPage === index + 1 ? "active" : "" className={`page-item ${
}`} currentPage === index + 1 ? "active" : ""
}`}
> >
<button <button
className="page-link" className="page-link"

View File

@ -118,74 +118,75 @@ const ViewPaymentRequest = ({ requestId }) => {
</div> </div>
<div className="row mb-1"> <div className="row mb-1">
<div className="col-12 col-sm-6 col-md-8"> <div className="col-12 col-sm-6 col-md-8">
<div className="col-12 text-start fw-semibold my-2"> <div className="row">
{data?.paymentRequestUID} <div className="col-12 text-start fw-semibold mb-2">
</div> {data?.paymentRequestUID}
{/* Row 1 */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Project Name :
</label>
<div className="text-muted">{data.project.name}</div>
</div> </div>
</div> <div className="col-md-6 mb-3">
<div className="col-md-6 mb-3"> <div className="d-block d-md-flex align-items-center">
<div className="d-flex"> <label
<label className="form-label me-2 mb-0 fw-semibold text-start"
className="form-label me-2 mb-0 fw-semibold text-start" style={{ minWidth: "130px" }}
style={{ minWidth: "130px" }} >
> Project Name:
Due Date : </label>
</label> <div className="text-muted">{data?.project?.name || "—"}</div>
<div className="text-muted">
{formatUTCToLocalTime(data?.dueDate)}
</div> </div>
</div> </div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Expense Category :
</label>
<div className="text-muted">{data?.expenseCategory?.name}</div>
</div>
</div>
{/* Row 2 */} <div className="col-md-6 mb-3">
<div className="col-md-6 mb-3"> <div className="d-flex">
<div className="d-flex"> <label
<label className="form-label me-2 mb-0 fw-semibold text-start"
className="form-label me-2 mb-0 fw-semibold text-start" style={{ minWidth: "130px" }}
style={{ minWidth: "130px" }} >
> Due Date :
Supplier : </label>
</label> <div className="text-muted">
<div className="text-muted">{data?.payee}</div> {formatUTCToLocalTime(data?.dueDate)}
</div> </div>
</div> </div>
<div className="col-md-6 mb-3"> </div>
<div className="d-flex"> <div className="col-md-6 mb-3">
<label <div className="d-flex">
className="form-label me-2 mb-0 fw-semibold text-start" <label
style={{ minWidth: "130px" }} className="form-label me-2 mb-0 fw-semibold text-start"
> style={{ minWidth: "130px" }}
Amount : >
</label> Expense Category :
<div className="text-muted"> </label>
{formatCurrency(data?.amount, data?.currency?.currencyCode)} <div className="text-muted">{data?.expenseCategory?.name}</div>
</div> </div>
</div> </div>
</div>
{/* Row 3 */} {/* Row 2 */}
{/* <div className="col-md-6 mb-3"> <div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Supplier :
</label>
<div className="text-muted">{data?.payee}</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Amount :
</label>
<div className="text-muted">
{formatCurrency(data?.amount, data?.currency?.currencyCode)}
</div>
</div>
</div>
{/* Row 3 */}
{/* <div className="col-md-6 mb-3">
<div className="d-flex"> <div className="d-flex">
<label <label
className="form-label me-2 mb-0 fw-semibold text-start" className="form-label me-2 mb-0 fw-semibold text-start"
@ -195,302 +196,305 @@ const ViewPaymentRequest = ({ requestId }) => {
</label> </label>
<div className="text-muted">{data?.paymentMode?.name}</div> <div className="text-muted">{data?.paymentMode?.name}</div>
</div> </div>
</div> */} </div> */}
{data?.gstNumber && ( {data?.gstNumber && (
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
GST Number :
</label>
<div className="text-muted">{data?.gstNumber}</div>
</div>
</div>
)}
{/* Row 4 */}
<div className="col-md-6 mb-3"> <div className="col-md-6 mb-3">
<div className="d-flex"> <div className="d-flex">
<label <label
className="form-label me-2 mb-0 fw-semibold text-start" className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }} style={{ minWidth: "130px" }}
> >
GST Number : Status :
</label> </label>
<div className="text-muted">{data?.gstNumber}</div> <span
className={`badge bg-label-${
getColorNameFromHex(data?.expenseStatus?.color) ||
"secondary"
}`}
>
{data?.expenseStatus?.name}
</span>
</div> </div>
</div> </div>
)} <div className="col-md-6 mb-3">
<div className="d-flex">
{/* Row 4 */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Status :
</label>
<span
className={`badge bg-label-${
getColorNameFromHex(data?.expenseStatus?.color) || "secondary"
}`}
>
{data?.expenseStatus?.name}
</span>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Pre-Approved :
</label>
<div className="text-muted">
{data?.preApproved ? "Yes" : "No"}
</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Project :
</label>
<div className="text-muted">{data?.project?.name}</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Created At :
</label>
<div className="text-muted">
{formatUTCToLocalTime(data?.createdAt, true)}
</div>
</div>
</div>
{/* Row 6 */}
{data?.createdBy && (
<div className="col-md-6 text-start">
<div className="d-flex align-items-center">
<label <label
className="form-label me-2 mb-0 fw-semibold" className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }} style={{ minWidth: "130px" }}
> >
Created By : Pre-Approved :
</label> </label>
<div className="text-muted">
{data?.preApproved ? "Yes" : "No"}
</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Project :
</label>
<div className="text-muted">{data?.project?.name}</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Created At :
</label>
<div className="text-muted">
{formatUTCToLocalTime(data?.createdAt, true)}
</div>
</div>
</div>
{/* Row 6 */}
{data?.createdBy && (
<div className="col-md-6 text-start">
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<Avatar <label
size="xs" className="form-label me-2 mb-0 fw-semibold"
classAvatar="m-0" style={{ minWidth: "130px" }}
firstName={data?.createdBy?.firstName} >
lastName={data?.createdBy?.lastName} Created By :
/> </label>
<span className="text-muted"> <div className="d-flex align-items-center">
{`${data?.createdBy?.firstName ?? ""} ${
data?.createdBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
</div>
</div>
)}
{data?.paidBy && (
<div className="col-md-6 text-start">
<div className="d-flex align-items-center">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
Paid By :
</label>
<div className="d-flex align-items-center ">
<Avatar
size="xs"
classAvatar="m-0"
firstName={data?.paidBy?.firstName}
lastName={data?.paidBy?.lastName}
/>
<span className="text-muted">
{`${data?.paidBy?.firstName ?? ""} ${
data?.paidBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
</div>
</div>
)}
<div className="text-start my-1">
<label className="fw-semibold form-label">Description : </label>
<div className="text-muted">{data?.description}</div>
</div>
<div className="col-6 text-start">
<label className="form-label me-2 mb-2 fw-semibold">
Attachment :
</label>
<div className="d-flex flex-wrap gap-2">
{data?.documents?.length > 0 ? (
data?.documents?.map((doc) => {
const isImage = doc?.contentType?.includes("image");
return (
<div
key={doc.documentId}
className="border rounded hover-scale p-2 d-flex flex-column align-items-center"
style={{
width: "80px",
cursor: isImage ? "pointer" : "default",
}}
onClick={() => {
if (isImage) {
setDocumentView({
IsOpen: true,
Image: doc.preSignedUrl,
});
}
}}
>
<i
className={`bx ${getIconByFileType(doc.contentType)}`}
style={{ fontSize: "30px" }}
></i>
<small
className="text-center text-tiny text-truncate w-100"
title={doc.fileName}
>
{doc.fileName}
</small>
</div>
);
})
) : (
<p className="m-0">No Attachment</p>
)}
</div>
</div>
{data?.paidTransactionId && (
<div className="row text-start mt-2">
<div className="col-md-6 mb-sm-0 mb-2">
<label className="form-label me-2 mb-0 fw-semibold">
Transaction ID :
</label>
{data?.paidTransactionId}
</div>
<div className="col-md-6 ">
<label className="form-label me-2 mb-0 fw-semibold">
Transaction Date :
</label>
{formatUTCToLocalTime(data?.paidAt)}
</div>
{data?.paidBy && (
<>
<div className="col-md-6 d-flex align-items-center">
<label className="form-label me-2 mb-0 fw-semibold">
Paid By :
</label>
<Avatar <Avatar
size="xs" size="xs"
classAvatar="m-0 me-1" classAvatar="m-0"
firstName={data?.createdBy?.firstName}
lastName={data?.createdBy?.lastName}
/>
<span className="text-muted">
{`${data?.createdBy?.firstName ?? ""} ${
data?.createdBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
</div>
</div>
)}
{data?.paidBy && (
<div className="col-md-6 text-start">
<div className="d-flex align-items-center">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
Paid By :
</label>
<div className="d-flex align-items-center ">
<Avatar
size="xs"
classAvatar="m-0"
firstName={data?.paidBy?.firstName} firstName={data?.paidBy?.firstName}
lastName={data?.paidBy?.lastName} lastName={data?.paidBy?.lastName}
/> />
<span className="text-muted"> <span className="text-muted">
{`${data?.paidBy?.firstName} ${data?.paidBy?.lastName}`.trim()} {`${data?.paidBy?.firstName ?? ""} ${
data?.paidBy?.lastName ?? ""
}`.trim() || "N/A"}
</span> </span>
</div> </div>
</>
)}
</div>
)}
{Array.isArray(data?.nextStatus) && data?.nextStatus.length > 0 && (
<>
{IsPaymentProcess && nextStatusWithPermission?.length > 0 && (
<div className="row">
<div className="col-12 col-md-6 text-start">
<label className="form-label">Transaction Id </label>
<input
type="text"
className="form-control form-control-sm"
{...register("paidTransactionId")}
/>
{errors.paidTransactionId && (
<small className="danger-text">
{errors.paidTransactionId.message}
</small>
)}
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label">Transaction Date </label>
<DatePicker
name="paidAt"
control={control}
minDate={data?.createdAt}
maxDate={new Date()}
/>
{errors.paidAt && (
<small className="danger-text">
{errors.paidAt.message}
</small>
)}
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label">Paid By </label>
<EmployeeSearchInput
control={control}
name="paidById"
projectId={null}
/>
</div>
</div> </div>
)} </div>
<div className="col-12 mb-3 text-start"> )}
{((nextStatusWithPermission.length > 0 && !isRejectedRequest) ||
(isRejectedRequest && isCreatedBy)) && ( <div className="text-start my-1">
<label className="fw-semibold form-label">Description : </label>
<div className="text-muted">{data?.description}</div>
</div>
<div className="col-6 text-start">
<label className="form-label me-2 mb-2 fw-semibold">
Attachment :
</label>
<div className="d-flex flex-wrap gap-2">
{data?.documents?.length > 0 ? (
data?.documents?.map((doc) => {
const isImage = doc?.contentType?.includes("image");
return (
<div
key={doc.documentId}
className="border rounded hover-scale p-2 d-flex flex-column align-items-center"
style={{
width: "80px",
cursor: isImage ? "pointer" : "default",
}}
onClick={() => {
if (isImage) {
setDocumentView({
IsOpen: true,
Image: doc.preSignedUrl,
});
}
}}
>
<i
className={`bx ${getIconByFileType(doc.contentType)}`}
style={{ fontSize: "30px" }}
></i>
<small
className="text-center text-tiny text-truncate w-100"
title={doc.fileName}
>
{doc.fileName}
</small>
</div>
);
})
) : (
<p className="m-0">No Attachment</p>
)}
</div>
</div>
{data?.paidTransactionId && (
<div className="row text-start mt-2">
<div className="col-md-6 mb-sm-0 mb-2">
<label className="form-label me-2 mb-0 fw-semibold">
Transaction ID :
</label>
{data?.paidTransactionId}
</div>
<div className="col-md-6 ">
<label className="form-label me-2 mb-0 fw-semibold">
Transaction Date :
</label>
{formatUTCToLocalTime(data?.paidAt)}
</div>
{data?.paidBy && (
<> <>
<Label className="form-label me-2 mb-0" required> <div className="col-md-6 d-flex align-items-center">
Comment <label className="form-label me-2 mb-0 fw-semibold">
</Label> Paid By :
<textarea </label>
className="form-control form-control-sm" <Avatar
{...register("comment")} size="xs"
rows="2" classAvatar="m-0 me-1"
/> firstName={data?.paidBy?.firstName}
{errors.comment && ( lastName={data?.paidBy?.lastName}
<small className="danger-text"> />
{errors.comment.message} <span className="text-muted">
</small> {`${data?.paidBy?.firstName} ${data?.paidBy?.lastName}`.trim()}
)} </span>
</div>
</> </>
)} )}
{nextStatusWithPermission?.length > 0 &&
(!isRejectedRequest || isCreatedBy) && (
<div className="text-end flex-wrap gap-2 my-2 mt-3">
{nextStatusWithPermission?.map((status, index) => (
<button
key={status.id || index}
type="button"
onClick={() => {
setClickedStatusId(status.id);
setValue("statusId", status.id);
handleSubmit(onSubmit)();
}}
disabled={isPending || isFetching}
className="btn btn-primary btn-sm cursor-pointer mx-2 border-0"
>
{isPending && clickedStatusId === status.id
? "Please Wait..."
: status.displayName || status.name}
</button>
))}
</div>
)}
</div> </div>
</> )}
)}
{Array.isArray(data?.nextStatus) && data?.nextStatus.length > 0 && (
<>
{IsPaymentProcess && nextStatusWithPermission?.length > 0 && (
<div className="row">
<div className="col-12 col-md-6 text-start">
<label className="form-label">Transaction Id </label>
<input
type="text"
className="form-control form-control-sm"
{...register("paidTransactionId")}
/>
{errors.paidTransactionId && (
<small className="danger-text">
{errors.paidTransactionId.message}
</small>
)}
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label">Transaction Date </label>
<DatePicker
name="paidAt"
control={control}
minDate={data?.createdAt}
maxDate={new Date()}
/>
{errors.paidAt && (
<small className="danger-text">
{errors.paidAt.message}
</small>
)}
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label">Paid By </label>
<EmployeeSearchInput
control={control}
name="paidById"
projectId={null}
/>
</div>
</div>
)}
<div className="col-12 mb-3 text-start">
{((nextStatusWithPermission.length > 0 &&
!isRejectedRequest) ||
(isRejectedRequest && isCreatedBy)) && (
<>
<Label className="form-label me-2 mb-0" required>
Comment
</Label>
<textarea
className="form-control form-control-sm"
{...register("comment")}
rows="2"
/>
{errors.comment && (
<small className="danger-text">
{errors.comment.message}
</small>
)}
</>
)}
{nextStatusWithPermission?.length > 0 &&
(!isRejectedRequest || isCreatedBy) && (
<div className="text-end flex-wrap gap-2 my-2 mt-3">
{nextStatusWithPermission?.map((status, index) => (
<button
key={status.id || index}
type="button"
onClick={() => {
setClickedStatusId(status.id);
setValue("statusId", status.id);
handleSubmit(onSubmit)();
}}
disabled={isPending || isFetching}
className="btn btn-primary btn-sm cursor-pointer mx-2 border-0"
>
{isPending && clickedStatusId === status.id
? "Please Wait..."
: status.displayName || status.name}
</button>
))}
</div>
)}
</div>
</>
)}
</div>
</div> </div>
<div className="col-12 col-sm-6 col-md-4"> <div className="col-12 col-sm-6 col-md-4">
<ExpenseStatusLogs data={data} /> <ExpenseStatusLogs data={data} />

View File

@ -1,20 +1,80 @@
import React from 'react' import React from "react";
const Error = ({ error = {}, isFeteching=false, refetch, close }) => {
const statusCode = error.statusCode || error?.response?.status || "Error";
const message =
error.message ||
error?.response?.data?.message ||
"Something went wrong. Please try again later.";
const getErrorInfo = (code) => {
if (code >= 500) {
return {
icon: "bx bx-server text-danger",
title: "Internal Server Error",
subtitle: "Our servers are currently experiencing issues.",
};
}
if (code === 404) {
return {
icon: "bx bx-error text-warning",
title: "Page Not Found",
subtitle: "The requested resource could not be found.",
};
}
if (code === 403) {
return {
icon: "bx bx-block text-danger",
title: "Access Denied",
subtitle: "You do not have permission to access this resource.",
};
}
if (code === 401) {
return {
icon: "bx bx-lock text-danger",
title: "Unauthorized",
subtitle: "Please log in to continue.",
};
}
if (code === 0 || code === "ERR_NETWORK") {
return {
icon: "bx bx-wifi-off text-secondary",
title: "Network Error",
subtitle: "Please check your internet connection.",
};
}
return {
icon: "bx bx-error-circle text-danger",
title: "Unexpected Error",
subtitle: "An unknown error occurred.",
};
};
const { icon, title, subtitle } = getErrorInfo(statusCode);
const Error = ({error,close}) => {
console.log(error)
return ( return (
<div className="container text-center py-5"> <div className="container-fluid d-flex flex-column align-items-center justify-content-center text-center py-5">
<h1 className="display-4 fw-bold text-danger">{error.statusCode || error?.response?.status
}</h1>
<h2 className="mb-3">Internal Server Error</h2>
<p className="lead">
{error.message}
</p>
<a href="/" className="btn btn-primary btn-sm mt-3" onClick={()=>close()}>
Go to Home
</a>
</div>
)
}
export default Error <h4 className="mb-2">{title}</h4>
<p className="text-muted mb-2">{subtitle}</p>
{message && (
<p className="lead small text-body-secondary mb-4">{message}</p>
)}
<div class="d-grid gap-2 col-4 mx-auto">
<button
class="btn btn-sm btn-outline-secondary btn-lg"
type="button"
onClick={refetch}
disabled={isFeteching}
>
<i className={`bx bx-refresh ${isFeteching ? "bx-spin":""} me-1`}></i> Retry
</button>
</div>
</div>
);
};
export default Error;

View File

@ -1,15 +1,25 @@
import React from "react"; import React from "react";
import Avatar from "./Avatar";
const Timeline = ({ items = [], transparent = true }) => { const Timeline = ({ items = [], transparent = true }) => {
return ( return (
<ul className={`timeline ${transparent ? "timeline-transparent text-start" : ""}`}> <ul
className={`timeline ${
transparent ? "timeline-transparent text-start" : ""
}`}
>
{items.map((item) => ( {items.map((item) => (
<li <li
key={item.id} key={item.id}
className={`timeline-item ${transparent ? "timeline-item-transparent" : ""}`} className={`timeline-item ${
transparent ? "timeline-item-transparent" : ""
}`}
> >
<span className={`timeline-point timeline-point-${item.color || "primary"}`}></span> <span
className={`timeline-point timeline-point-${
item.color || "primary"
}`}
></span>
<div className="timeline-event"> <div className="timeline-event">
<div className="timeline-header mb-3 d-flex justify-content-between"> <div className="timeline-header mb-3 d-flex justify-content-between">
@ -26,7 +36,14 @@ const Timeline = ({ items = [], transparent = true }) => {
key={i} key={i}
className="badge bg-lighter rounded d-flex align-items-center gap-2 p-2" className="badge bg-lighter rounded d-flex align-items-center gap-2 p-2"
> >
{att.icon && <img src={att.icon} alt="file" width="15" className="me-2" />} {att.icon && (
<img
src={att.icon}
alt="file"
width="15"
className="me-2"
/>
)}
<span className="h6 mb-0">{att.name}</span> <span className="h6 mb-0">{att.name}</span>
</div> </div>
))} ))}
@ -47,17 +64,18 @@ const Timeline = ({ items = [], transparent = true }) => {
height="32" height="32"
/> />
) : ( ) : (
<span className="avatar-initial rounded-circle pull-up bg-light"> <Avatar
{user.name} firstName={user.firstName}
</span> lastName={user.lastName}
/>
)} )}
</li> </li>
))} ))}
</ul> </ul>
{item.users[0]?.role && ( {item.users?.length === 1 && (
<div className="ms-2"> <div className="m-0">
<p className="mb-0 small fw-medium">{item.users[0].name}</p> <p className="mb-0 small fw-medium">{`${item.users[0].firstName} ${item.users[0].lastName}`}</p>
<small>{item.users[0].role}</small> <small>{item.users[0].role}</small>
</div> </div>
)} )}

View File

@ -67,8 +67,8 @@ export const PROCESS_EXPENSE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11";
export const EXPENSE_MANAGE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"; export const EXPENSE_MANAGE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11";
export const EXPENSE_REJECTEDBY = [ export const EXPENSE_REJECTEDBY = [
"d1ee5eec-24b6-4364-8673-a8f859c60729",
"965eda62-7907-4963-b4a1-657fb0b2724b", "965eda62-7907-4963-b4a1-657fb0b2724b",
"d1ee5eec-24b6-4364-8673-a8f859c60729",
]; ];