merged recurring into upgrade expense

This commit is contained in:
pramod.mahajan 2025-11-06 01:00:11 +05:30
parent 4ae0b403a6
commit 1f784f330d
11 changed files with 208 additions and 74 deletions

View File

@ -169,7 +169,7 @@ export const defaultActionValues = {
reimburseById: null, reimburseById: null,
tdsPercentage: 0, tdsPercentage: 0,
baseAmount:null, baseAmount:null,
taxAmount: 0, taxAmount: null,
}; };
export const SearchSchema = z.object({ export const SearchSchema = z.object({

View File

@ -18,7 +18,7 @@ const ExpenseStatusLogs = ({ data }) => {
() => sortedLogs.slice(0, visibleCount), () => sortedLogs.slice(0, visibleCount),
[sortedLogs, visibleCount] [sortedLogs, visibleCount]
); );
console.log(logsToShow)
const timelineData = useMemo(() => { const timelineData = useMemo(() => {
return logsToShow.map((log, index) => ({ return logsToShow.map((log, index) => ({

View File

@ -1,54 +1,96 @@
import React from "react"; import React from "react";
import { formatFileSize, getIconByFileType } from "../../utils/appUtils"; import { formatFileSize, getIconByFileType } from "../../utils/appUtils";
import Tooltip from "../common/Tooltip";
const Filelist = ({ files, removeFile, expenseToEdit }) => { const Filelist = ({ files, removeFile, expenseToEdit }) => {
return ( return (
<div className="d-block"> <div className="d-flex flex-wrap gap-2 my-1">
{files {files
.filter((file) => { .filter((file) => {
if (expenseToEdit) { if (expenseToEdit) {
return file.isActive; return file.isActive;
} }
return true; return true;
}) })
.map((file, idx) => ( .map((file, idx) => (
<div className="col-12 col-sm-6 col-md-4 mb-2" key={idx}> <div className="col-12 col-sm-6 col-md-4 mb-2" key={idx}>
<div className="d-flex align-items-center justify-content-between bg-white border rounded p-1 "> <div className="d-flex align-items-center justify-content-between bg-white border rounded p-1">
{/* File icon and info */} {/* File icon and info */}
<div className="d-flex align-items-center flex-grow-1 gap-2 overflow-hidden"> <div className="d-flex align-items-center flex-grow-1 gap-2 overflow-hidden">
<i <i
className={`bx ${getIconByFileType( className={`bx ${getIconByFileType(
file?.contentType file?.contentType
)} fs-3 text-primary`} )} fs-3 text-primary`}
style={{ minWidth: "30px" }} style={{ minWidth: "30px" }}
></i> ></i>
<div className="d-flex flex-column text-truncate"> <div className="d-flex flex-column text-truncate">
<span className="fw-semibold small text-truncate"> <span className="fw-semibold small text-truncate">
{file.fileName} {file.fileName}
</span> </span>
<span className="text-body-secondary small"> <span className="text-body-secondary small">
{file.fileSize ? formatFileSize(file.fileSize) : ""} {file.fileSize ? formatFileSize(file.fileSize) : ""}
</span> </span>
</div>
</div>
{/* Delete icon */}
<Tooltip text="Remove file">
<i
className="bx bx-sm bx-trash text-danger fs-4 cursor-pointer ms-2"
role="button"
onClick={(e) => {
e.preventDefault();
removeFile(expenseToEdit ? file.documentId : idx);
}}
></i>
</Tooltip>
</div> </div>
</div> </div>
))}
</div> {/* Delete icon */}
<Tooltip text="Remove file">
<i
className="bx bx-sm bx-trash text-danger fs-4 cursor-pointer ms-2"
role="button"
onClick={(e) => {
e.preventDefault();
removeFile(expenseToEdit ? file.documentId : idx);
}}
></i>
</Tooltip>
</div>
</div>
))}
</div>
); );
}; };
export default Filelist; export default Filelist;
export const FilelistView = ({ files, viewFile }) => {
console.log( files)
return (
<div className="d-flex flex-wrap gap-2 mt-2">
{files?.map((file, idx) => (
<div className=" bg-white " key={idx}>
<div className="row align-items-center">
{/* File icon and info */}
<div className="col-12 d-flex align-items-center gap-2">
<i
className={`bx ${getIconByFileType(file?.fileName)} fs-3`}
></i>
<div
className="d-flex flex-column text-truncate"
onClick={(e) => {
e.preventDefault();
viewFile({
IsOpen: true,
Image: file.preSignedUrl,
});
}}
>
<span className="fw-medium small text-truncate">
{file.fileName}
</span>
<span className="text-body-secondary small">
<Tooltip text={"Click on file"}>
{" "}
{file.fileSize ? formatFileSize(file.fileSize) : ""}
</Tooltip>
</span>
</div>
</div>
</div>
</div>
))}
</div>
);
};

View File

@ -134,7 +134,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
reader.onerror = (error) => reject(error); reader.onerror = (error) => reject(error);
}); });
const removeFile = (index) => { const removeFile = (index) => {
documentId;
if (expenseToEdit) { if (expenseToEdit) {
const newFiles = files.map((file, i) => { const newFiles = files.map((file, i) => {
if (file.documentId !== index) return file; if (file.documentId !== index) return file;

View File

@ -109,6 +109,7 @@ const ViewExpense = ({ ExpenseId }) => {
const handleImageLoad = (id) => { const handleImageLoad = (id) => {
setImageLoaded((prev) => ({ ...prev, [id]: true })); setImageLoaded((prev) => ({ ...prev, [id]: true }));
}; };
console.log(errors)
return ( return (
<form className="container px-3" onSubmit={handleSubmit(onSubmit)}> <form className="container px-3" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 mb-1"> <div className="col-12 mb-1">

View File

@ -392,10 +392,7 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
error={errors.payee?.message} error={errors.payee?.message}
/> />
{errors.payee && (
<small className="danger-text">{errors.payee.message}</small>
)}
{/* Checkbox below input */} {/* Checkbox below input */}
<div className="form-check mt-2"> <div className="form-check mt-2">
<input <input

View File

@ -29,27 +29,24 @@ export const PaymentRequestSchema = (expenseTypes, isItself) => {
}), }),
billAttachments: z billAttachments: z
.array( .array(
z.object({ z.object({
fileName: z.string().min(1, { message: "Filename is required" }), fileName: z.string().min(1, { message: "Filename is required" }),
base64Data: z.string().nullable(), base64Data: z.string().nullable(),
contentType: z contentType: z
.string() .string()
.refine((val) => ALLOWED_TYPES.includes(val), { .refine((val) => ALLOWED_TYPES.includes(val), {
message: "Only PDF, PNG, JPG, or JPEG files are allowed", message: "Only PDF, PNG, JPG, or JPEG files are allowed",
}), }),
documentId: z.string().optional(), documentId: z.string().optional(),
fileSize: z.number().max(MAX_FILE_SIZE, { fileSize: z.number().max(MAX_FILE_SIZE, {
message: "File size must be less than or equal to 5MB", message: "File size must be less than or equal to 5MB",
}), }),
description: z.string().optional(), description: z.string().optional(),
isActive: z.boolean().default(true), isActive: z.boolean().default(true),
}) })
).refine((data)=>{ )
if(isItself){ ,
payee.z.string().optional();
}
}),
}) })
}; };

View File

@ -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 (
<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
size="xs"
firstName={log.updatedBy.firstName}
lastName={log.updatedBy.lastName}
/>
<div className="flex-grow-1">
<div className="text-start">
<div className="flex">
<span>{`${log.updatedBy.firstName} ${log.updatedBy.lastName}`}</span>
<small className="text-secondary text-tiny ms-2">
<em>{log.action}</em>
</small>
<span className="text-tiny text-secondary d-block">
{formatUTCToLocalTime(log.updateAt, true)}
</span>
</div>
<div className="d-flex align-items-center text-muted small mt-1">
<span>{log.comment}</span>
</div>
</div>
</div>
</div>
))}
</div>
{sortedLogs.length > visibleCount && (
<div className="text-center my-1">
<button
className="btn btn-xs btn-outline-primary"
onClick={handleShowMore}
>
Show More
</button>
</div>
)} */}
<Timeline items={timelineData} />
</div>
);
};
export default PaymentStatusLogs;

View File

@ -34,6 +34,8 @@ import {
REVIEW_EXPENSE, REVIEW_EXPENSE,
} from "../../utils/constants"; } from "../../utils/constants";
import Label from "../common/Label"; import Label from "../common/Label";
import { FilelistView } from "../Expenses/Filelist";
import PaymentStatusLogs from "./PaymentStatusLogs";
const ViewPaymentRequest = ({ requestId }) => { const ViewPaymentRequest = ({ requestId }) => {
const { data, isLoading, isError, error, isFetching } = const { data, isLoading, isError, error, isFetching } =
@ -125,7 +127,7 @@ const ViewPaymentRequest = ({ requestId }) => {
<hr /> <hr />
</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-7">
<div className="row"> <div className="row">
<div className="col-12 text-start fw-semibold mb-2"> <div className="col-12 text-start fw-semibold mb-2">
{data?.paymentRequestUID} {data?.paymentRequestUID}
@ -537,7 +539,7 @@ const ViewPaymentRequest = ({ requestId }) => {
<i className="bx bx-time-five me-2 "></i>{" "} <i className="bx bx-time-five me-2 "></i>{" "}
<p className="fw-medium">TimeLine</p> <p className="fw-medium">TimeLine</p>
</div> </div>
<PaymentStat data={data} /> <PaymentStatusLogs data={data} />
</div> </div>
</div> </div>
</form> </form>

View File

@ -1,5 +1,8 @@
import React from "react"; import React from "react";
import Avatar from "./Avatar"; import Avatar from "./Avatar";
import Tooltip from "./Tooltip";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import moment from "moment";
const Timeline = ({ items = [], transparent = true }) => { const Timeline = ({ items = [], transparent = true }) => {
return ( return (
@ -24,7 +27,7 @@ const Timeline = ({ items = [], transparent = true }) => {
<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">
<h6 className="mb-0 text-body">{item.title}</h6> <h6 className="mb-0 text-body">{item.title}</h6>
<small className="text-body-secondary">{item.timeAgo}</small> <small className="text-body-secondary"><Tooltip text={formatUTCToLocalTime(item.timeAgo,true)}>{moment(item.timeAgo).fromNow()}</Tooltip></small>
</div> </div>
{item.description && <p className="mb-2">{item.description}</p>} {item.description && <p className="mb-2">{item.description}</p>}

View File

@ -190,7 +190,7 @@ const ExpensePage = () => {
{viewExpense.view && ( {viewExpense.view && (
<GlobalModel <GlobalModel
isOpen isOpen
size="lg" size="xl"
modalType="top" modalType="top"
closeModal={() => setViewExpense({ expenseId: null, view: false })} closeModal={() => setViewExpense({ expenseId: null, view: false })}
> >