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

View File

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

View File

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

View File

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

View File

@ -118,74 +118,75 @@ const ViewPaymentRequest = ({ requestId }) => {
</div>
<div className="row mb-1">
<div className="col-12 col-sm-6 col-md-8">
<div className="col-12 text-start fw-semibold my-2">
{data?.paymentRequestUID}
</div>
{/* 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 className="row">
<div className="col-12 text-start fw-semibold mb-2">
{data?.paymentRequestUID}
</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" }}
>
Due Date :
</label>
<div className="text-muted">
{formatUTCToLocalTime(data?.dueDate)}
<div className="col-md-6 mb-3">
<div className="d-block d-md-flex align-items-center">
<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="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="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 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" }}
>
Due Date :
</label>
<div className="text-muted">
{formatUTCToLocalTime(data?.dueDate)}
</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>
</div>
{/* Row 3 */}
{/* <div className="col-md-6 mb-3">
{/* Row 2 */}
<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">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
@ -195,302 +196,305 @@ const ViewPaymentRequest = ({ requestId }) => {
</label>
<div className="text-muted">{data?.paymentMode?.name}</div>
</div>
</div> */}
{data?.gstNumber && (
</div> */}
{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="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
GST Number :
Status :
</label>
<div className="text-muted">{data?.gstNumber}</div>
<span
className={`badge bg-label-${
getColorNameFromHex(data?.expenseStatus?.color) ||
"secondary"
}`}
>
{data?.expenseStatus?.name}
</span>
</div>
</div>
)}
{/* 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">
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold"
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Created By :
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">
<Avatar
size="xs"
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}
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>
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
Created By :
</label>
<div className="d-flex align-items-center">
<Avatar
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}
lastName={data?.paidBy?.lastName}
/>
<span className="text-muted">
{`${data?.paidBy?.firstName} ${data?.paidBy?.lastName}`.trim()}
{`${data?.paidBy?.firstName ?? ""} ${
data?.paidBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</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)) && (
</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 && (
<>
<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>
)}
<div className="col-md-6 d-flex align-items-center">
<label className="form-label me-2 mb-0 fw-semibold">
Paid By :
</label>
<Avatar
size="xs"
classAvatar="m-0 me-1"
firstName={data?.paidBy?.firstName}
lastName={data?.paidBy?.lastName}
/>
<span className="text-muted">
{`${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>
</>
)}
)}
{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 className="col-12 col-sm-6 col-md-4">
<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 (
<div className="container 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>
)
}
<div className="container-fluid d-flex flex-column align-items-center justify-content-center text-center py-5">
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 Avatar from "./Avatar";
const Timeline = ({ items = [], transparent = true }) => {
return (
<ul className={`timeline ${transparent ? "timeline-transparent text-start" : ""}`}>
<ul
className={`timeline ${
transparent ? "timeline-transparent text-start" : ""
}`}
>
{items.map((item) => (
<li
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-header mb-3 d-flex justify-content-between">
@ -26,7 +36,14 @@ const Timeline = ({ items = [], transparent = true }) => {
key={i}
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>
</div>
))}
@ -47,17 +64,18 @@ const Timeline = ({ items = [], transparent = true }) => {
height="32"
/>
) : (
<span className="avatar-initial rounded-circle pull-up bg-light">
{user.name}
</span>
<Avatar
firstName={user.firstName}
lastName={user.lastName}
/>
)}
</li>
))}
</ul>
{item.users[0]?.role && (
<div className="ms-2">
<p className="mb-0 small fw-medium">{item.users[0].name}</p>
{item.users?.length === 1 && (
<div className="m-0">
<p className="mb-0 small fw-medium">{`${item.users[0].firstName} ${item.users[0].lastName}`}</p>
<small>{item.users[0].role}</small>
</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_REJECTEDBY = [
"d1ee5eec-24b6-4364-8673-a8f859c60729",
"965eda62-7907-4963-b4a1-657fb0b2724b",
"d1ee5eec-24b6-4364-8673-a8f859c60729",
];