diff --git a/src/components/PaymentRequest/ActionPaymentRequest.jsx b/src/components/PaymentRequest/ActionPaymentRequest.jsx new file mode 100644 index 00000000..4951da18 --- /dev/null +++ b/src/components/PaymentRequest/ActionPaymentRequest.jsx @@ -0,0 +1,249 @@ +import React, { useMemo, useState } from "react"; +import DatePicker from "../common/DatePicker"; +import EmployeeSearchInput from "../common/EmployeeSearchInput"; +import Label from "../common/Label"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + defaultPRActionValues, + PaymentRequestActionScheam, +} from "./PaymentRequestSchema"; +import { + useActionOnPaymentRequest, + usePaymentRequestDetail, +} from "../../hooks/useExpense"; +import { + CREATE_EXEPENSE, + EXPENSE_CREATE, + EXPENSE_PROCESSED, + EXPENSE_REJECTEDBY, + PROCESS_EXPENSE, + REVIEW_EXPENSE, +} from "../../utils/constants"; +import { useHasUserPermission } from "../../hooks/useHasUserPermission"; +import { useNavigate } from "react-router-dom"; +import { useForm } from "react-hook-form"; +import { useSelector } from "react-redux"; +import { usePaymentRequestContext } from "../../pages/PaymentRequest/PaymentRequestPage"; +import { localToUtc } from "../../utils/appUtils"; + +const ActionPaymentRequest = ({ requestId }) => { + const { setIsExpenseGenerate, setVieRequest } = usePaymentRequestContext(); + const { data, isLoading, isError, error, isFetching } = + usePaymentRequestDetail(requestId); + const [IsPaymentProcess, setIsPaymentProcess] = useState(false); + const [clickedStatusId, setClickedStatusId] = useState(null); + + const IsReview = useHasUserPermission(REVIEW_EXPENSE); + const [imageLoaded, setImageLoaded] = useState({}); + + const ActionSchema = + PaymentRequestActionScheam(IsPaymentProcess, data?.createdAt) ?? + z.object({}); + const navigate = useNavigate(); + const { + register, + handleSubmit, + setValue, + reset, + control, + formState: { errors }, + } = useForm({ + resolver: zodResolver(ActionSchema), + defaultValues: defaultPRActionValues, + }); + + const userPermissions = useSelector( + (state) => state?.globalVariables?.loginUser?.featurePermissions || [] + ); + const CurrentUser = useSelector( + (state) => state?.globalVariables?.loginUser?.employeeInfo + ); + + const nextStatusWithPermission = useMemo(() => { + if (!Array.isArray(data?.nextStatus)) return []; + + return data.nextStatus.filter((status) => { + const permissionIds = Array.isArray(status?.permissionIds) + ? status.permissionIds + : []; + + if (permissionIds?.length === 0) return true; + if (permissionIds.includes(PROCESS_EXPENSE)) { + setIsPaymentProcess(true); + } + return permissionIds.some((id) => userPermissions.includes(id)); + }); + }, [data, userPermissions]); + + const isRejectedRequest = useMemo(() => { + return EXPENSE_REJECTEDBY.includes(data?.status?.id); + }, [data]); + const isProccesed = useMemo(() => { + return data?.expenseStatus?.id === EXPENSE_PROCESSED; + }, [data]); + + const isCreatedBy = useMemo(() => { + return data?.createdBy?.id === CurrentUser?.id; + }, [data, CurrentUser]); + + const { mutate: MakeAction, isPending } = useActionOnPaymentRequest(() => { + setClickedStatusId(null); + reset(); + }); + + const onSubmit = (formData) => { + const Payload = { + ...formData, + paidAt: localToUtc(formData.paidAt), + paymentRequestId: data.id, + comment: formData.comment, + }; + MakeAction(Payload); + }; + + if (isLoading) return
Lading..
; + if (isError) return ; + const handleImageLoad = (id) => { + setImageLoaded((prev) => ({ ...prev, [id]: true })); + }; + const handleExpense = () => { + setIsExpenseGenerate({ IsOpen: true, requestId: requestId }); + setVieRequest({ IsOpen: false, requestId: null }); + }; + + return ( +
+ {IsPaymentProcess && + !isProccesed && + nextStatusWithPermission?.length > 0 && ( +
+
+ + + {errors.paidTransactionId && ( + + {errors.paidTransactionId.message} + + )} +
+
+ + + {errors.paidAt && ( + {errors.paidAt.message} + )} +
+
+ + +
+
+ + + {errors.tdsPercentage && ( + + {errors.tdsPercentage.message} + + )} +
+
+ + + {errors.baseAmount && ( + + {errors.baseAmount.message} + + )} +
+
+ + + {errors.taxAmount && ( + + {errors.taxAmount.message} + + )} +
+
+ )} +
+ {((nextStatusWithPermission?.length > 0 && + !isRejectedRequest && + !isProccesed) || + (isRejectedRequest && isCreatedBy)) && ( + <> + + + {errors.description && ( + + {errors.description.message} + + )} +
+ + + {/* Upload Document */} +
+
+ + +
document.getElementById("billAttachments").click()} + > + + + Click to select or click here to browse + + (PDF, JPG, PNG, max 5MB) + + { + onFileChange(e); + e.target.value = ""; + }} + /> +
+ {errors.billAttachments && ( + + {errors.billAttachments.message} + + )} + {files?.length > 0 && ( + + )} + + {Array.isArray(errors.billAttachments) && + errors.billAttachments.map((fileError, index) => ( +
+ { + (fileError?.fileSize?.message || + fileError?.contentType?.message || + fileError?.base64Data?.message, + fileError?.documentId?.message) + } +
+ ))} +
+
+
+ + +
+
+ + ); +} + +export default ManagePaymentRequest; diff --git a/src/components/PaymentRequest/PaymentRequestFilterPanel.jsx b/src/components/PaymentRequest/PaymentRequestFilterPanel.jsx new file mode 100644 index 00000000..65d788e9 --- /dev/null +++ b/src/components/PaymentRequest/PaymentRequestFilterPanel.jsx @@ -0,0 +1,202 @@ +import React, { useEffect, useState, useMemo } from "react"; +import { FormProvider, useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { defaultPaymentRequestFilter,SearchPaymentRequestSchema } from "./PaymentRequestSchema"; + +import DateRangePicker, { DateRangePicker1 } from "../common/DateRangePicker"; +import SelectMultiple from "../common/SelectMultiple"; +import { useProjectName } from "../../hooks/useProjects"; +import { useExpenseStatus } from "../../hooks/masterHook/useMaster"; +import { useEmployeesAllOrByProjectId } from "../../hooks/useEmployees"; +import { useSelector } from "react-redux"; +import moment from "moment"; +import { usePaymentRequestFilter } from "../../hooks/useExpense"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; + +const PaymentRequestFilterPanel = ({ onApply, handleGroupBy }) => { + const { status } = useParams(); + const navigate = useNavigate(); + const selectedProjectId = useSelector( + (store) => store.localVariables.projectId + ); + const { data, isLoading, isError, error, isFetching, isFetched } = + usePaymentRequestFilter(); + + const groupByList = useMemo(() => { + return [ + { id: "projects", name: "Project" }, + { id: "status", name: "Status" }, + { id: "createdBy", name: "Submitted By" }, + { id: "currency", name: "Currency" }, + { id: "expensesCategory", name: "Expense Category" }, + { id: "payees", name: "Payee" }, + { id: "date", name: "Due Date" }, + + ].sort((a, b) => a.name.localeCompare(b.name)); + }, []); + + const [selectedGroup, setSelectedGroup] = useState(groupByList[6]); + const [resetKey, setResetKey] = useState(0); + + + const methods = useForm({ + resolver: zodResolver(SearchPaymentRequestSchema), + defaultValues: defaultPaymentRequestFilter, + }); + + const { control, handleSubmit, reset, setValue, watch } = methods; + const isTransactionDate = watch("isTransactionDate"); + + const closePanel = () => { + document.querySelector(".offcanvas.show .btn-close")?.click(); + }; + + + + const handleGroupChange = (e) => { + const group = groupByList.find((g) => g.id === e.target.value); + if (group) setSelectedGroup(group); + }; + + + const onSubmit = (formData) => { + onApply({ + ...formData, + startDate: moment.utc(formData.startDate, "DD-MM-YYYY").toISOString(), + endDate: moment.utc(formData.endDate, "DD-MM-YYYY").toISOString(), + }); + handleGroupBy(selectedGroup.id); + // closePanel(); + }; + + const onClear = () => { + reset(defaultPaymentRequestFilter); + setResetKey((prev) => prev + 1); + onApply(defaultPaymentRequestFilter); + if (status) { + navigate("/expenses", { replace: true }); + } + }; + + const location = useLocation(); + useEffect(() => { + closePanel(); + }, [location]); + + const [appliedStatusId, setAppliedStatusId] = useState(null); + + if (isError && isFetched) + return
Something went wrong Here- {error.message}
; + + return ( + <> + +
+
+
+ +
+ +
+ +
+ + item.name} + valueKey="id" + /> + item.name} + valueKey="id" + /> + item.name} + valueKey="id" + /> + item.name} + valueKey="id" + /> + +
+ +
+ {data?.status + ?.slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .map((status) => ( +
+ ( +
+ { + const checked = e.target.checked; + onChange( + checked + ? [...value, status.id] + : value.filter((v) => v !== status.id) + ); + }} + /> + +
+ )} + /> +
+ ))} +
+
+
+ +
+ + +
+
+
+ + ); +}; + +export default PaymentRequestFilterPanel; \ No newline at end of file diff --git a/src/components/PaymentRequest/PaymentRequestList.jsx b/src/components/PaymentRequest/PaymentRequestList.jsx new file mode 100644 index 00000000..20e9c4e1 --- /dev/null +++ b/src/components/PaymentRequest/PaymentRequestList.jsx @@ -0,0 +1,380 @@ +import React, { useState } from "react"; +import { + EXPENSE_DRAFT, + EXPENSE_REJECTEDBY, + ITEMS_PER_PAGE, +} from "../../utils/constants"; +import { + formatCurrency, + formatFigure, + getColorNameFromHex, + useDebounce, +} from "../../utils/appUtils"; +import { usePaymentRequestList } from "../../hooks/useExpense"; +import { formatDate, formatUTCToLocalTime } from "../../utils/dateUtils"; +import Avatar from "../../components/common/Avatar"; +import { usePaymentRequestContext } from "../../pages/PaymentRequest/PaymentRequestPage"; +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"; +import Pagination from "../common/Pagination"; + +const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => { + const { setManageRequest, setVieRequest } = usePaymentRequestContext(); + const navigate = useNavigate(); + const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [deletingId, setDeletingId] = useState(null); + const SelfId = useSelector( + (store) => store?.globalVariables?.loginUser?.employeeInfo?.id + ); + const groupByField = (items, field) => { + return items.reduce((acc, item) => { + let key; + let displayField; + + switch (field) { + case "transactionDate": + key = item?.transactionDate?.split("T")[0]; + displayField = "Transaction Date"; + break; + case "status": + key = item?.status?.displayName || "Unknown"; + displayField = "Status"; + break; + case "submittedBy": + key = `${item?.createdBy?.firstName ?? ""} ${ + item.createdBy?.lastName ?? "" + }`.trim(); + displayField = "Submitted By"; + break; + case "project": + key = item?.project?.name || "Unknown Project"; + displayField = "Project"; + break; + case "paymentMode": + key = item?.paymentMode?.name || "Unknown Mode"; + displayField = "Payment Mode"; + break; + case "expensesType": + key = item?.expensesType?.name || "Unknown Type"; + displayField = "Expense Category"; + break; + case "createdAt": + key = item?.createdAt?.split("T")[0] || "Unknown Date"; + displayField = "Created Date"; + break; + default: + key = "Others"; + displayField = "Others"; + } + + const groupKey = `${field}_${key}`; // unique key for object property + if (!acc[groupKey]) { + acc[groupKey] = { key, displayField, items: [] }; + } + + acc[groupKey].items.push(item); + return acc; + }, {}); + }; + + const paymentRequestColumns = [ + { + key: "paymentRequestUID", + label: "Request ID", + align: "text-start mx-2", + getValue: (e) => e.paymentRequestUID || "N/A", + }, + { + key: "title", + label: "Request Title", + align: "text-start", + getValue: (e) => e.title || "N/A", + }, + // { key: "payee", label: "Payee", align: "text-start" }, + { + key: "SubmittedBy", + label: "Submitted By", + align: "text-start", + getValue: (e) => + `${e.createdBy?.firstName ?? ""} ${ + e.createdBy?.lastName ?? "" + }`.trim() || "N/A", + customRender: (e) => ( +
navigate(`/employee/${e.createdBy?.id}`)} + > + + + {`${e.createdBy?.firstName ?? ""} ${ + e.createdBy?.lastName ?? "" + }`.trim() || "N/A"} + +
+ ), + }, + { + key: "createdAt", + label: "Submitted On", + align: "text-start", + getValue: (e) => formatUTCToLocalTime(e?.createdAt), + }, + { + key: "amount", + label: "Amount", + align: "text-end", + getValue: (e) => + formatFigure(e?.amount, { + type: "currency", + currency: e?.currency?.currencyCode, + }), + }, + { + key: "expenseStatus", + label: "Status", + align: "text-center", + getValue: (e) => ( + + {e?.expenseStatus?.name || "Unknown"} + + ), + }, + ]; + + const [currentPage, setCurrentPage] = useState(1); + const debouncedSearch = useDebounce(search, 500); + + const { data, isLoading, isError, error, isRefetching, refetch } = + usePaymentRequestList( + ITEMS_PER_PAGE, + currentPage, + filters, + true, + debouncedSearch + ); + + if (isError) { + return ; + } + const header = [ + "Request ID", + "Request Title", + "Submitted By", + "Submitted On", + "Amount", + "Status", + "Action", + ]; + if (isLoading) return ; + + const grouped = groupBy + ? Object.fromEntries( + Object.entries(groupByField(data?.data ?? [], groupBy)).sort( + ([keyA], [keyB]) => keyA.localeCompare(keyB) + ) + ) + : { All: data?.data ?? [] }; + + const IsGroupedByDate = [ + { key: "transactionDate", displayField: "Transaction Date" }, + { key: "createdAt", displayField: "created Date" }, + ]?.includes(groupBy); + + const paginate = (page) => { + if (page >= 1 && page <= (data?.totalPages ?? 1)) { + setCurrentPage(page); + } + }; + const canEditExpense = (paymentRequest) => { + return ( + (paymentRequest?.expenseStatus?.id === EXPENSE_DRAFT || + EXPENSE_REJECTEDBY.includes(paymentRequest?.expenseStatus.id)) && + paymentRequest?.createdBy?.id === SelfId + ); + }; + const canDetetExpense = (request) => { + return ( + request?.expenseStatus?.id === EXPENSE_DRAFT && + request?.createdBy?.id === SelfId + ); + }; + + const handleDelete = (id) => { + setDeletingId(id); + DeleteExpense( + { id }, + { + onSettled: () => { + setDeletingId(null); + setIsDeleteModalOpen(false); + }, + } + ); + }; + + return ( + <> + {IsDeleteModalOpen && ( + setIsDeleteModalOpen(false)} + // loading={isPending} + paramData={deletingId} + /> + )} +
+
+ + + + {paymentRequestColumns.map((col) => ( + + ))} + + + + + + {Object.keys(grouped).length > 0 ? ( + Object.values(grouped).map(({ key, displayField, items }) => ( + + + + + {items?.map((paymentRequest) => ( + + {paymentRequestColumns.map( + (col) => + (col.isAlwaysVisible || groupBy !== col.key) && ( + + ) + )} + + + ))} + + )) + ) : ( + + + + )} + +
+ {col.label} + Action
+
+ {" "} + + {displayField} :{" "} + {" "} + + {IsGroupedByDate ? formatUTCToLocalTime(key) : key} + +
+
+ {col?.customRender + ? col?.customRender(paymentRequest) + : col?.getValue(paymentRequest)} + +
+ + setVieRequest({ + requestId: paymentRequest.id, + view: true, + }) + } + > + {canEditExpense(paymentRequest) && ( +
+ +
    +
  • + setManageRequest({ + IsOpen: true, + RequestId: paymentRequest.id, + }) + } + > + + + + Modify + + +
  • + + {canDetetExpense(paymentRequest) && ( +
  • { + setIsDeleteModalOpen(true); + setDeletingId(paymentRequest.id); + }} + > + + + + Delete + + +
  • + )} +
+
+ )} +
+
+
+

No Request Found

+
+
+
+ + {/* Pagination */} + +
+ + ); +}; + +export default PaymentRequestList; diff --git a/src/components/PaymentRequest/PaymentRequestSchema.js b/src/components/PaymentRequest/PaymentRequestSchema.js new file mode 100644 index 00000000..8ef8b1eb --- /dev/null +++ b/src/components/PaymentRequest/PaymentRequestSchema.js @@ -0,0 +1,180 @@ +import { boolean, z } from "zod"; +import { INR_CURRENCY_CODE } from "../../utils/constants"; +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB +const ALLOWED_TYPES = [ + "application/pdf", + "image/png", + "image/jpg", + "image/jpeg", +]; +export const PaymentRequestSchema = (expenseTypes, isItself) => { + return z.object({ + title: z.string().min(1, { message: "Project is required" }), + projectId: z.string().min(1, { message: "Project is required" }), + expenseCategoryId: z + .string() + .min(1, { message: "Expense Category is required" }), + currencyId: z.string().min(1, { message: "Currency is required" }), + dueDate: z.string().min(1, { message: "Date is required" }), + description: z.string().min(1, { message: "Description is required" }), + payee: z.string().min(1, { message: "Supplier name is required" }), + isAdvancePayment: z.boolean().optional().default(false), + amount: z.coerce + .number({ + invalid_type_error: "Amount is required and must be a number", + }) + .min(1, "Amount must be Enter") + .refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), { + message: "Amount must have at most 2 decimal places", + }), + + billAttachments: z.array( + z.object({ + fileName: z.string().min(1, { message: "Filename is required" }), + base64Data: z.string().nullable(), + contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), { + message: "Only PDF, PNG, JPG, or JPEG files are allowed", + }), + documentId: z.string().optional(), + fileSize: z.number().max(MAX_FILE_SIZE, { + message: "File size must be less than or equal to 5MB", + }), + description: z.string().optional(), + isActive: z.boolean().default(true), + }) + ), + }); +}; + +export const defaultPaymentRequest = { + title: "", + description: "", + payee: "", + currencyId: "", + amount: "", + dueDate: "", + projectId: "", + expenseCategoryId: "", + isAdvancePayment: false, + billAttachments: [], +}; + +export const SearchPaymentRequestSchema = z.object({ + projectIds: z.array(z.string()).optional(), + statusIds: z.array(z.string()).optional(), + createdByIds: z.array(z.string()).optional(), + currencyIds: z.array(z.string()).optional(), + expenseCategoryIds: z.array(z.string()).optional(), + payees: z.array(z.string()).optional(), + startDate: z.string().optional(), + endDate: z.string().optional(), +}); + +export const defaultPaymentRequestFilter = { + projectIds: [], + statusIds: [], + createdByIds: [], + currencyIds: [], + expenseCategoryIds: [], + payees: [], + startDate: null, + endDate: null, +}; + +export const PaymentRequestActionScheam = ( + isTransaction = false, + transactionDate +) => { + return z + .object({ + comment: z.string().min(1, { message: "Please leave comment" }), + statusId: z.string().min(1, { message: "Please select a status" }), + paidTransactionId: z.string().nullable().optional(), + paidAt: z.string().nullable().optional(), + paidById: z.string().nullable().optional(), + tdsPercentage: z.string().nullable().optional(), + baseAmount: z.string().nullable().optional(), + taxAmount: z.string().nullable().optional(), + }) + .superRefine((data, ctx) => { + if (isTransaction) { + if (!data.paidTransactionId?.trim()) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["paidTransactionId"], + message: "Transaction ID is required", + }); + } + if (!data.paidAt) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["paidAt"], + message: "Transacion Date is required", + }); + } + if (!data.paidById) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["paidById"], + message: "Paid By is required", + }); + } + if (!data.baseAmount) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["baseAmount"], + message: "Base Amount i required", + }); + } + if (!data.taxAmount) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["taxAmount"], + message: "Tax is required", + }); + } + } + }); +}; + +export const defaultPRActionValues = { + comment: "", + statusId: "", + paidTransactionId: null, + paidAt: null, + paidById: null, + tdsPercentage: "0", + baseAmount: null, + taxAmount: "0", +}; + +export const RequestedExpenseSchema = z.object({ + paymentModeId: z.string().min(1, { message: "Payment mode is required" }), + location: z.string().min(1, { message: "Location is required" }), + gstNumber: z.string().optional(), + billAttachments: z + .array( + z.object({ + fileName: z.string().min(1, { message: "Filename is required" }), + base64Data: z.string().nullable(), + contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), { + message: "Only PDF, PNG, JPG, or JPEG files are allowed", + }), + documentId: z.string().optional(), + fileSize: z.number().max(MAX_FILE_SIZE, { + message: "File size must be less than or equal to 5MB", + }), + description: z.string().optional(), + isActive: z.boolean().default(true), + }) + ) + .nonempty({ message: "At least one file attachment is required" }), +}); + +export const DefaultRequestedExpense = { + paymentModeId: "", + location: "", + gstNumber: "", + // amount:"", + billAttachments: [], +}; diff --git a/src/components/PaymentRequest/PaymentStatusLogs.jsx b/src/components/PaymentRequest/PaymentStatusLogs.jsx new file mode 100644 index 00000000..4dfaa42e --- /dev/null +++ b/src/components/PaymentRequest/PaymentStatusLogs.jsx @@ -0,0 +1,89 @@ +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 PaymentStatusLogs = ({ data }) => { + + const sortedLogs = useMemo(() => { + if (!data?.updateLogs) return []; + return [...data.updateLogs].sort( + (a, b) => new Date(b.updatedAt) - new Date(a.updatedAt) + ); + }, [data?.updateLogs]); + + + + const timelineData = useMemo(() => { + return sortedLogs.map((log, index) => ({ + id: log.id, + title: log.nextStatus?.name || "Status Updated", + description: log.nextStatus?.description || "", + timeAgo: log.updatedAt, + color: getColorNameFromHex(log.nextStatus?.color) || "primary", + userComment:log.comment, + users: log.updatedBy + ? [ + { + firstName: log.updatedBy.firstName || "", + lastName: log?.updatedBy?.lastName || "", + role: log.updatedBy.jobRoleName || "", + avatar: log.updatedBy.photo, + }, + ] + : [], + })); + }, [sortedLogs]); + + const handleShowMore = () => { + setVisibleCount((prev) => prev + 4); + }; + return ( +
+ {/*
+ {logsToShow.map((log) => ( +
+ + +
+
+
+ {`${log.updatedBy.firstName} ${log.updatedBy.lastName}`} + + {log.action} + + + {formatUTCToLocalTime(log.updateAt, true)} + +
+
+ {log.comment} +
+
+
+
+ ))} +
+ + {sortedLogs.length > visibleCount && ( +
+ +
+ )} */} + + +
+ ); +}; + +export default PaymentStatusLogs; diff --git a/src/components/PaymentRequest/ViewPaymentRequest.jsx b/src/components/PaymentRequest/ViewPaymentRequest.jsx new file mode 100644 index 00000000..aa7b46b2 --- /dev/null +++ b/src/components/PaymentRequest/ViewPaymentRequest.jsx @@ -0,0 +1,495 @@ +import { useMemo, useState } from "react"; +import { + useActionOnExpense, + useActionOnPaymentRequest, + usePaymentRequestDetail, +} from "../../hooks/useExpense"; +import { + formatCurrency, + formatFigure, + getColorNameFromHex, + getIconByFileType, + localToUtc, +} from "../../utils/appUtils"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import Avatar from "../common/Avatar"; +import DatePicker from "../common/DatePicker"; +import EmployeeSearchInput from "../common/EmployeeSearchInput"; +import Error from "../common/Error"; +import { + defaultActionValues, + ExpenseActionScheam, +} from "../Expenses/ExpenseSchema"; +import { ExpenseDetailsSkeleton } from "../Expenses/ExpenseSkeleton"; +import ExpenseStatusLogs from "../Expenses/ExpenseStatusLogs"; +import { useSelector } from "react-redux"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { useNavigate } from "react-router-dom"; +import { usePaymentRequestContext } from "../../pages/PaymentRequest/PaymentRequestPage"; +import { useHasUserPermission } from "../../hooks/useHasUserPermission"; +import { + EXPENSE_PROCESSED, + EXPENSE_REJECTEDBY, + PROCESS_EXPENSE, + REVIEW_EXPENSE, +} from "../../utils/constants"; +import Label from "../common/Label"; +import { FilelistView } from "../Expenses/Filelist"; +import PaymentStatusLogs from "./PaymentStatusLogs"; +import { + defaultPRActionValues, + PaymentRequestActionScheam, +} from "./PaymentRequestSchema"; +import ActionPaymentRequest from "./ActionPaymentRequest"; + +const ViewPaymentRequest = ({ requestId }) => { + const [IsPaymentProcess, setIsPaymentProcess] = useState(false); + const [clickedStatusId, setClickedStatusId] = useState(null); + const [imageLoaded, setImageLoaded] = useState({}); + const IsReview = useHasUserPermission(REVIEW_EXPENSE); + const navigate = useNavigate(); + + const { data, isLoading, isError, error, isFetching } = + usePaymentRequestDetail(requestId); + + const { setDocumentView, setModalSize, setVieRequest, setIsExpenseGenerate } = + usePaymentRequestContext(); + const ActionSchema = + PaymentRequestActionScheam(IsPaymentProcess, data?.createdAt) ?? + z.object({}); + + const { + register, + handleSubmit, + setValue, + reset, + control, + formState: { errors }, + } = useForm({ + resolver: zodResolver(ActionSchema), + defaultValues: defaultPRActionValues, + }); + + const userPermissions = useSelector( + (state) => state?.globalVariables?.loginUser?.featurePermissions || [] + ); + const CurrentUser = useSelector( + (state) => state?.globalVariables?.loginUser?.employeeInfo + ); + + const nextStatusWithPermission = useMemo(() => { + if (!Array.isArray(data?.nextStatus)) return []; + + return data.nextStatus.filter((status) => { + const permissionIds = Array.isArray(status?.permissionIds) + ? status.permissionIds + : []; + + if (permissionIds?.length === 0) return true; + if (permissionIds.includes(PROCESS_EXPENSE)) { + setIsPaymentProcess(true); + } + return permissionIds.some((id) => userPermissions.includes(id)); + }); + }, [data, userPermissions]); + + const isRejectedRequest = useMemo(() => { + return EXPENSE_REJECTEDBY.includes(data?.status?.id); + }, [data]); + + const isCreatedBy = useMemo(() => { + return data?.createdBy?.id === CurrentUser?.id; + }, [data, CurrentUser]); + + const { mutate: MakeAction, isPending } = useActionOnPaymentRequest(() => { + setClickedStatusId(null); + reset(); + }); + + const onSubmit = (formData) => { + const Payload = { + ...formData, + paidAt: localToUtc(formData.paidAt), + paymentRequestId: data.id, + comment: formData.comment, + }; + MakeAction(Payload); + }; + + if (isLoading) return ; + if (isError) return ; + const handleImageLoad = (id) => { + setImageLoaded((prev) => ({ ...prev, [id]: true })); + }; + + return ( +
+
+
Payment Request Details
+
+
+
+
+
+
+ {data?.paymentRequestUID} + + {data?.expenseStatus?.name} + +
+
+
+ +
{data?.project?.name || "—"}
+
+
+ +
+
+ +
+ {formatUTCToLocalTime(data?.dueDate)} +
+
+
+ +
+
+ +
{data?.expenseCategory?.name}
+
+
+ + {/* Row 2 */} +
+
+ +
{data?.payee}
+
+
+
+
+ +
+ {formatFigure(data?.amount, { + type: "currency", + currency: data?.currency?.currencyCode, + })} +
+
+
+ + {data?.gstNumber && ( +
+
+ +
{data?.gstNumber}
+
+
+ )} + +
+
+ +
+ {formatUTCToLocalTime(data?.createdAt, true)} +
+
+
+ + {/* Row 6 */} + {data?.createdBy && ( +
+
+ +
+ + + {`${data?.createdBy?.firstName ?? ""} ${ + data?.createdBy?.lastName ?? "" + }`.trim() || "N/A"} + +
+
+
+ )} + {data?.paidBy && ( +
+
+ +
+ + + {`${data?.paidBy?.firstName ?? ""} ${ + data?.paidBy?.lastName ?? "" + }`.trim() || "N/A"} + +
+
+
+ )} + +
+ +
{data?.description}
+
+
+ + +
+ {data?.attachments?.length > 0 ? ( + + ) : ( +

No Attachment

+ )} +
+
+ + {data?.paidTransactionId && ( +
+
+ + {data?.paidTransactionId} +
+
+ + {formatUTCToLocalTime(data?.paidAt)} +
+ + {data?.paidBy && ( + <> +
+ + + + {`${data?.paidBy?.firstName} ${data?.paidBy?.lastName}`.trim()} + +
+ + )} +
+ )} + + {/* {Array.isArray(data?.nextStatus) && (data?.nextStatus?.length > 0) && ( + <> + {IsPaymentProcess && nextStatusWithPermission?.length > 0 && ( +
+
+ + + {errors.paidTransactionId && ( + + {errors.paidTransactionId.message} + + )} +
+
+ + + {errors.paidAt && ( + + {errors.paidAt.message} + + )} +
+
+ + +
+
+ + + {errors.tdsPercentage && ( + + {errors.tdsPercentage.message} + + )} +
+
+ + + {errors.baseAmount && ( + + {errors.baseAmount.message} + + )} +
+
+ + + {errors.taxAmount && ( + + {errors.taxAmount.message} + + )} +
+
+ )} +
+ {((nextStatusWithPermission?.length > 0 && + !isRejectedRequest) || + (isRejectedRequest && isCreatedBy)) && ( + <> + +