merged recurring into upgrade expense
This commit is contained in:
parent
4ae0b403a6
commit
1f784f330d
@ -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({
|
||||||
|
|||||||
@ -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) => ({
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
93
src/components/PaymentRequest/PaymentStatusLogs.jsx
Normal file
93
src/components/PaymentRequest/PaymentStatusLogs.jsx
Normal 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;
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>}
|
||||||
|
|||||||
@ -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 })}
|
||||||
>
|
>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user