diff --git a/src/components/Expenses/ExpenseList.jsx b/src/components/Expenses/ExpenseList.jsx index 3b58316e..a56d340b 100644 --- a/src/components/Expenses/ExpenseList.jsx +++ b/src/components/Expenses/ExpenseList.jsx @@ -12,6 +12,7 @@ import { } from "../../utils/constants"; import { formatCurrency, + formatFigure, getColorNameFromHex, useDebounce, } from "../../utils/appUtils"; @@ -166,7 +167,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { { key: "amount", label: "Amount", - getValue: (e) => <>{formatCurrency(e?.amount)}, + getValue: (e) => <>{formatFigure(e?.amount,{type:"currency",currency : e?.currency?.currencyCode ?? "INR"} )}, isAlwaysVisible: true, align: "text-end", }, @@ -288,11 +289,11 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { (col.isAlwaysVisible || groupBy !== col.key) && ( - {col.customRender +
{col.customRender ? col.customRender(expense) - : col.getValue(expense)} + : col.getValue(expense)}
) )} @@ -307,7 +308,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { }) } > - {canDetetExpense(expense) && + { canEditExpense(expense) && (
-
- )} */} + diff --git a/src/components/Expenses/Filelist.jsx b/src/components/Expenses/Filelist.jsx index 2b1ce74e..208a596b 100644 --- a/src/components/Expenses/Filelist.jsx +++ b/src/components/Expenses/Filelist.jsx @@ -1,9 +1,10 @@ import React from "react"; import { formatFileSize, getIconByFileType } from "../../utils/appUtils"; +import Tooltip from "../common/Tooltip"; const Filelist = ({ files, removeFile, expenseToEdit }) => { return ( -
+
{files .filter((file) => { if (expenseToEdit) { @@ -12,14 +13,12 @@ const Filelist = ({ files, removeFile, expenseToEdit }) => { return true; }) .map((file, idx) => ( -
+
{/* File icon and info */}
@@ -34,14 +33,17 @@ const Filelist = ({ files, removeFile, expenseToEdit }) => {
- { - e.preventDefault(); - removeFile(expenseToEdit ? file.documentId : idx); - }} - > + + { + e.preventDefault(); + debugger; + removeFile(expenseToEdit ? file.documentId : idx); + }} + > +
@@ -51,3 +53,44 @@ const Filelist = ({ files, removeFile, expenseToEdit }) => { }; export default Filelist; + +export const FilelistView = ({ files, viewFile }) => { + return ( +
+ {files?.map((file, idx) => ( +
+
+ {/* File icon and info */} +
+ + +
{ + e.preventDefault(); + viewFile({ + IsOpen: true, + Image: file.preSignedUrl, + }); + }} + > + + {file.fileName} + + + + {" "} + {file.fileSize ? formatFileSize(file.fileSize) : ""} + + +
+
+
+
+ ))} +
+ ); +}; diff --git a/src/components/Expenses/ManageExpense.jsx b/src/components/Expenses/ManageExpense.jsx index f3872614..4ee1679c 100644 --- a/src/components/Expenses/ManageExpense.jsx +++ b/src/components/Expenses/ManageExpense.jsx @@ -31,6 +31,7 @@ import Label from "../common/Label"; import EmployeeSearchInput from "../common/EmployeeSearchInput"; import Filelist from "./Filelist"; + const ManageExpense = ({ closeModal, expenseToEdit = null }) => { const { data, @@ -128,7 +129,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { reader.onload = () => resolve(reader.result.split(",")[1]); reader.onerror = (error) => reject(error); }); - const removeFile = (index) => { + const removeFile = (index) => {documentId if (expenseToEdit) { const newFiles = files.map((file, i) => { if (file.documentId !== index) return file; @@ -148,7 +149,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { if (expenseToEdit && data) { reset({ projectId: data.project.id || "", - expensesCategoryId: data.expensesType.id || "", + expenseCategoryId: data.expensesCategory?.id || "", paymentModeId: data.paymentMode.id || "", paidById: data.paidBy.id || "", transactionDate: data.transactionDate?.slice(0, 10) || "", @@ -245,7 +246,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { - {errors.reimburseTransactionId && ( - - {errors.reimburseTransactionId.message} + + + {doc.fileName} - )} -
-
- - - {errors.reimburseDate && ( - - {errors.reimburseDate.message} - - )} -
-
- - -
-
- )} -
- {((nextStatusWithPermission.length > 0 && !IsRejectedExpense) || - (IsRejectedExpense && 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} - - )} - {Array.isArray(files) && files.length > 0 && ( - -
- {files - .filter((file) => { - if (requestToEdit) { - return file.isActive; - } - return true; - }) - .map((file, idx) => ( - -
- - {file.fileName} - - - {file.fileSize ? formatFileSize(file.fileSize) : ""} - -
- { - e.preventDefault(); - removeFile(requestToEdit ? file.documentId : idx); - }} - > -
- ))} -
- )} - - {Array.isArray(errors.billAttachments) && - errors.billAttachments.map((fileError, index) => ( -
- { - (fileError?.fileSize?.message || - fileError?.contentType?.message || - fileError?.base64Data?.message, - fileError?.documentId?.message) - } -
- ))} -
-
-
- - - -
- - +
+ + + {errors.expenseCategoryId && ( + + {errors.expenseCategoryId.message} + + )} +
- ) + + {/* Title and Advance Payment */} +
+
+ + + {errors.title && ( + {errors.title.message} + )} +
+ +
+ + + {errors.isAdvancePayment && ( + + {errors.isAdvancePayment.message} + + )} +
+
+ + {/* Date and Amount */} +
+
+ + + + {errors.dueDate && ( + {errors.dueDate.message} + )} +
+ +
+ + + {errors.amount && ( + {errors.amount.message} + )} +
+
+ + {/* Payee and Currency */} +
+
+ + + setValue("payee", val, { shouldValidate: true }) + } + error={errors.payee?.message} + /> + + {errors.payee && ( + {errors.payee.message} + )} + + {/* Checkbox below input */} +
+ + +
+
+ +
+ + + {errors.currencyId && ( + {errors.currencyId.message} + )} +
+
+ + {/* Description */} +
+
+ + + {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 +export default ManagePaymentRequest; diff --git a/src/components/PaymentRequest/PaymentRequestList.jsx b/src/components/PaymentRequest/PaymentRequestList.jsx index 9e5f351d..3e8152d5 100644 --- a/src/components/PaymentRequest/PaymentRequestList.jsx +++ b/src/components/PaymentRequest/PaymentRequestList.jsx @@ -6,6 +6,7 @@ import { } from "../../utils/constants"; import { formatCurrency, + formatFigure, getColorNameFromHex, useDebounce, } from "../../utils/appUtils"; @@ -131,7 +132,7 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => { align: "text-start", getValue: (e) => ( <> - {formatCurrency(e?.amount)} {e.currency.currencyCode} + {formatFigure(e?.amount,{type:"currency",currency : e?.currency?.currencyCode})} ), @@ -253,7 +254,7 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
{" "} - + {displayField} :{" "} {" "} @@ -271,9 +272,9 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => { key={col.key} className={`d-table-cell ${col.align ?? ""}`} > - {col?.customRender +
{col?.customRender ? col?.customRender(paymentRequest) - : col?.getValue(paymentRequest)} + : col?.getValue(paymentRequest)}
) )} diff --git a/src/components/PaymentRequest/PaymentRequestSchema.js b/src/components/PaymentRequest/PaymentRequestSchema.js index fb813b63..018b092f 100644 --- a/src/components/PaymentRequest/PaymentRequestSchema.js +++ b/src/components/PaymentRequest/PaymentRequestSchema.js @@ -7,56 +7,48 @@ const ALLOWED_TYPES = [ "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(), - amount: z.coerce - .number({ - invalid_type_error: "Amount is required and must be a number", +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(), + 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), }) - .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), - }) - ).refine((data)=>{ - if(isItself){ - payee.z.string().optional(); - } - }), - }) - }; + ) + .optional(), + }); +}; export const defaultPaymentRequest = { - title:"", + title: "", description: "", payee: "", currencyId: "", @@ -64,11 +56,10 @@ export const defaultPaymentRequest = { dueDate: "", projectId: "", expenseCategoryId: "", - isAdvancePayment:boolean, + isAdvancePayment: boolean, billAttachments: [], }; - export const SearchPaymentRequestSchema = z.object({ projectIds: z.array(z.string()).optional(), statusIds: z.array(z.string()).optional(), @@ -91,7 +82,6 @@ export const defaultPaymentRequestFilter = { endDate: null, }; - export const PaymentRequestActionScheam = ( isTransaction = false, transactionDate @@ -100,17 +90,16 @@ export const PaymentRequestActionScheam = ( .object({ comment: z.string().min(1, { message: "Please leave comment" }), statusId: z.string().min(1, { message: "Please select a status" }), - paymentRequestId: z.string().nullable().optional(), + paidTransactionId: z.string().nullable().optional(), paidAt: z.string().nullable().optional(), paidById: z.string().nullable().optional(), - }) .superRefine((data, ctx) => { if (isTransaction) { - if (!data.paymentRequestId?.trim()) { + if (!data.paidTransactionId?.trim()) { ctx.addIssue({ code: z.ZodIssueCode.custom, - path: ["reimburseTransactionId"], + path: ["paidTransactionId"], message: "Reimburse Transaction ID is required", }); } @@ -121,15 +110,7 @@ export const PaymentRequestActionScheam = ( message: "Transacion Date is required", }); } - // let reimburse_Date = localToUtc(data.reimburseDate); - // if (transactionDate > reimburse_Date) { - // ctx.addIssue({ - // code: z.ZodIssueCode.custom, - // path: ["reimburseDate"], - // message: - // "Reimburse Date must be greater than or equal to Expense created Date", - // }); - // } + if (!data.paidById) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -141,11 +122,11 @@ export const PaymentRequestActionScheam = ( }); }; -export const defaultActionValues = { +export const defaultPaymentRequestActionValues = { comment: "", statusId: "", paidTransactionId: null, paidAt: null, paidById: null, -}; \ No newline at end of file +}; diff --git a/src/components/PaymentRequest/PaymentStatusLogs.jsx b/src/components/PaymentRequest/PaymentStatusLogs.jsx new file mode 100644 index 00000000..9abb3fa5 --- /dev/null +++ b/src/components/PaymentRequest/PaymentStatusLogs.jsx @@ -0,0 +1,93 @@ +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 [visibleCount, setVisibleCount] = useState(4); + + const sortedLogs = useMemo(() => { + if (!data?.updateLogs) return []; + return [...data.updateLogs].sort( + (a, b) => new Date(b.updatedAt) - new Date(a.updatedAt) + ); + }, [data?.updateLogs]); + + const logsToShow = useMemo( + () => sortedLogs.slice(0, visibleCount), + [sortedLogs, visibleCount] + ); + + const timelineData = useMemo(() => { + return logsToShow.map((log, index) => ({ + id: index + 1, + title: log.nextStatus?.name || "Status Updated", + description: log.nextStatus?.description || "", + timeAgo: log.updatedAt, + color: getColorNameFromHex(log.nextStatus?.color) || "primary", + users: log.updatedBy + ? [ + { + firstName: log.updatedBy.firstName || "", + lastName: log?.updatedBy?.lastName || "", + role: log.updatedBy.jobRoleName || "", + avatar: log.updatedBy.photo, + }, + ] + : [], + })); + }, [logsToShow]); + + 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 index 7be1b5e3..0d7d280b 100644 --- a/src/components/PaymentRequest/ViewPaymentRequest.jsx +++ b/src/components/PaymentRequest/ViewPaymentRequest.jsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useActionOnExpense, useActionOnPaymentRequest, @@ -6,6 +6,7 @@ import { } from "../../hooks/useExpense"; import { formatCurrency, + formatFigure, getColorNameFromHex, getIconByFileType, localToUtc, @@ -28,11 +29,18 @@ import { useNavigate } from "react-router-dom"; import { usePaymentRequestContext } from "../../pages/PaymentRequest/PaymentRequestPage"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { + EXPENSE_DRAFT, EXPENSE_REJECTEDBY, PROCESS_EXPENSE, REVIEW_EXPENSE, } from "../../utils/constants"; import Label from "../common/Label"; +import { + defaultPaymentRequestActionValues, + PaymentRequestActionScheam, +} from "./PaymentRequestSchema"; +import PaymentStatusLogs from "./PaymentStatusLogs"; +import { FilelistView } from "../Expenses/Filelist"; const ViewPaymentRequest = ({ requestId }) => { const { data, isLoading, isError, error, isFetching } = @@ -42,9 +50,10 @@ const ViewPaymentRequest = ({ requestId }) => { const IsReview = useHasUserPermission(REVIEW_EXPENSE); const [imageLoaded, setImageLoaded] = useState({}); - const { setDocumentView } = usePaymentRequestContext(); + const { setDocumentView, setModalSize } = usePaymentRequestContext(); const ActionSchema = - ExpenseActionScheam(IsPaymentProcess, data?.createdAt) ?? z.object({}); + PaymentRequestActionScheam(IsPaymentProcess, data?.createdAt) ?? + z.object({}); const navigate = useNavigate(); const { register, @@ -55,7 +64,7 @@ const ViewPaymentRequest = ({ requestId }) => { formState: { errors }, } = useForm({ resolver: zodResolver(ActionSchema), - defaultValues: defaultActionValues, + defaultValues: defaultPaymentRequestActionValues, }); const userPermissions = useSelector( @@ -97,7 +106,7 @@ const ViewPaymentRequest = ({ requestId }) => { const onSubmit = (formData) => { const Payload = { ...formData, - paidAt: localToUtc(formData.reimburseDate), + paidAt: localToUtc(formData.paidAt), paymentRequestId: data.id, comment: formData.comment, }; @@ -112,17 +121,24 @@ const ViewPaymentRequest = ({ requestId }) => { return (
-
+
Payment Request Details
-
-
-
+
+
-
+
{data?.paymentRequestUID} + + + {data?.expenseStatus?.name} +
-
+
-
+
-
+
{/* Row 2 */} -
+
-
+
- {formatCurrency(data?.amount, data?.currency?.currencyCode)} + {formatFigure(data?.amount,{type:"currency",currency : data?.currency?.currencyCode})}
- {/* Row 3 */} - {/*
-
- -
{data?.paymentMode?.name}
-
-
*/} {data?.gstNumber && ( -
+