Merge branch 'Recurring_Expense' into upgrade_Expense
This commit is contained in:
commit
4ae0b403a6
@ -12,7 +12,6 @@ import {
|
||||
} from "../../utils/constants";
|
||||
import {
|
||||
formatCurrency,
|
||||
formatFigure,
|
||||
getColorNameFromHex,
|
||||
useDebounce,
|
||||
} from "../../utils/appUtils";
|
||||
@ -167,7 +166,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
{
|
||||
key: "amount",
|
||||
label: "Amount",
|
||||
getValue: (e) => <>{formatFigure(e?.amount,{type:"currency",currency : e?.currency?.currencyCode ?? "INR"} )}</>,
|
||||
getValue: (e) => <>{formatCurrency(e?.amount)}</>,
|
||||
isAlwaysVisible: true,
|
||||
align: "text-end",
|
||||
},
|
||||
@ -296,7 +295,8 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
${col.key === "submitted" ? "justify-content-center":""}
|
||||
`}>{col.customRender
|
||||
? col.customRender(expense)
|
||||
: col.getValue(expense)}</div>
|
||||
: col.getValue(expense)}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
)}
|
||||
@ -311,7 +311,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
})
|
||||
}
|
||||
></i>
|
||||
{
|
||||
{canDetetExpense(expense) &&
|
||||
canEditExpense(expense) && (
|
||||
<div className="dropdown z-2">
|
||||
<button
|
||||
|
||||
@ -14,7 +14,7 @@ export const ExpenseSchema = (expenseTypes) => {
|
||||
return z
|
||||
.object({
|
||||
projectId: z.string().min(1, { message: "Project is required" }),
|
||||
expenseCategoryId: z
|
||||
expensesCategoryId: z
|
||||
.string()
|
||||
.min(1, { message: "Expense type is required" }),
|
||||
paymentModeId: z.string().min(1, { message: "Payment mode is required" }),
|
||||
|
||||
@ -12,7 +12,7 @@ const ExpenseStatusLogs = ({ data }) => {
|
||||
return [...data.expenseLogs].sort(
|
||||
(a, b) => new Date(b.updateAt) - new Date(a.updateAt)
|
||||
);
|
||||
}, [data?.expenseLogs]);
|
||||
}, [data?.updateLogs]);
|
||||
|
||||
const logsToShow = useMemo(
|
||||
() => sortedLogs.slice(0, visibleCount),
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import React from "react";
|
||||
import { formatFileSize, getIconByFileType } from "../../utils/appUtils";
|
||||
import Tooltip from "../common/Tooltip";
|
||||
|
||||
const Filelist = ({ files, removeFile, expenseToEdit }) => {
|
||||
return (
|
||||
<div className="d-flex flex-wrap gap-2 mt-2">
|
||||
<div className="d-block">
|
||||
{files
|
||||
.filter((file) => {
|
||||
if (expenseToEdit) {
|
||||
@ -53,44 +52,3 @@ const Filelist = ({ files, removeFile, expenseToEdit }) => {
|
||||
};
|
||||
|
||||
export default Filelist;
|
||||
|
||||
export const FilelistView = ({ files, viewFile }) => {
|
||||
return (
|
||||
<div className="d-flex flex-wrap gap-2 mt-2">
|
||||
{files?.map((file, idx) => (
|
||||
<div className="col-12 col-sm-6 col-md-4 bg-white " key={idx}>
|
||||
<div className="row align-items-center">
|
||||
{/* File icon and info */}
|
||||
<div className="col-10 d-flex align-items-center gap-2">
|
||||
<i
|
||||
className={`bx ${getIconByFileType(file?.fileName)} fs-3`}
|
||||
style={{ minWidth: "30px" }}
|
||||
></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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -154,7 +154,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
if (expenseToEdit && data) {
|
||||
reset({
|
||||
projectId: data.project.id || "",
|
||||
expenseCategoryId: data.expensesCategory?.id || "",
|
||||
expensesCategoryId: data.expensesType.id || "",
|
||||
paymentModeId: data.paymentMode.id || "",
|
||||
paidById: data.paidBy.id || "",
|
||||
transactionDate: data.transactionDate?.slice(0, 10) || "",
|
||||
@ -254,7 +254,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
id="expensesCategoryId"
|
||||
{...register("expenseCategoryId")}
|
||||
{...register("expensesCategoryId")}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select Type
|
||||
|
||||
@ -44,8 +44,7 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
const IsReview = useHasUserPermission(REVIEW_EXPENSE);
|
||||
const [imageLoaded, setImageLoaded] = useState({});
|
||||
const { setDocumentView } = useExpenseContext();
|
||||
const ActionSchema =
|
||||
ExpenseActionScheam(IsPaymentProcess, data?.createdAt) ?? z.object({});
|
||||
const ActionSchema = ExpenseActionScheam(IsPaymentProcess,data?.createdAt) ?? z.object({});
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
@ -98,7 +97,7 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
const onSubmit = (formData) => {
|
||||
const Payload = {
|
||||
...formData,
|
||||
reimburseDate: localToUtc(formData.reimburseDate),
|
||||
reimburseDate:localToUtc(formData.reimburseDate),
|
||||
expenseId: ExpenseId,
|
||||
comment: formData.comment,
|
||||
};
|
||||
|
||||
@ -49,7 +49,7 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
|
||||
error: ExpenseError,
|
||||
} = useExpenseCategory();
|
||||
|
||||
const { profile } = useProfile();
|
||||
|
||||
const {
|
||||
data: Payees,
|
||||
isLoading: isPayeeLoaing,
|
||||
|
||||
@ -6,7 +6,6 @@ import {
|
||||
} from "../../utils/constants";
|
||||
import {
|
||||
formatCurrency,
|
||||
formatFigure,
|
||||
getColorNameFromHex,
|
||||
useDebounce,
|
||||
} from "../../utils/appUtils";
|
||||
@ -132,7 +131,7 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
|
||||
align: "text-start",
|
||||
getValue: (e) => (
|
||||
<>
|
||||
{formatFigure(e?.amount,{type:"currency",currency : e?.currency?.currencyCode})}
|
||||
{formatCurrency(e?.amount)} {e.currency.currencyCode}
|
||||
</>
|
||||
),
|
||||
|
||||
@ -254,7 +253,7 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
|
||||
<td colSpan={8} className="text-start">
|
||||
<div className="d-flex align-items-center">
|
||||
{" "}
|
||||
<small className="fs-6 py-1 ms-2">
|
||||
<small className="fs-6 py-1">
|
||||
{displayField} :{" "}
|
||||
</small>{" "}
|
||||
<small className="fs-6 ms-3">
|
||||
@ -272,9 +271,9 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
|
||||
key={col.key}
|
||||
className={`d-table-cell ${col.align ?? ""}`}
|
||||
>
|
||||
<div className="ms-2"> {col?.customRender
|
||||
{col?.customRender
|
||||
? col?.customRender(paymentRequest)
|
||||
: col?.getValue(paymentRequest)}</div>
|
||||
: col?.getValue(paymentRequest)}
|
||||
</td>
|
||||
)
|
||||
)}
|
||||
|
||||
@ -27,28 +27,34 @@ export const PaymentRequestSchema = (expenseTypes, isItself) => {
|
||||
.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),
|
||||
})
|
||||
)
|
||||
.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),
|
||||
})
|
||||
).refine((data)=>{
|
||||
if(isItself){
|
||||
payee.z.string().optional();
|
||||
}
|
||||
}),
|
||||
})
|
||||
};
|
||||
|
||||
export const defaultPaymentRequest = {
|
||||
title: "",
|
||||
title:"",
|
||||
description: "",
|
||||
payee: "",
|
||||
currencyId: "",
|
||||
@ -60,6 +66,7 @@ export const defaultPaymentRequest = {
|
||||
billAttachments: [],
|
||||
};
|
||||
|
||||
|
||||
export const SearchPaymentRequestSchema = z.object({
|
||||
projectIds: z.array(z.string()).optional(),
|
||||
statusIds: z.array(z.string()).optional(),
|
||||
@ -82,6 +89,7 @@ export const defaultPaymentRequestFilter = {
|
||||
endDate: null,
|
||||
};
|
||||
|
||||
|
||||
export const PaymentRequestActionScheam = (
|
||||
isTransaction = false,
|
||||
transactionDate
|
||||
@ -90,7 +98,7 @@ export const PaymentRequestActionScheam = (
|
||||
.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(),
|
||||
paymentRequestId: z.string().nullable().optional(),
|
||||
paidAt: z.string().nullable().optional(),
|
||||
paidById: z.string().nullable().optional(),
|
||||
tdsPercentage: z.string().nullable().optional(),
|
||||
@ -99,10 +107,10 @@ export const PaymentRequestActionScheam = (
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (isTransaction) {
|
||||
if (!data.paidTransactionId?.trim()) {
|
||||
if (!data.paymentRequestId?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["paidTransactionId"],
|
||||
path: ["reimburseTransactionId"],
|
||||
message: "Reimburse Transaction ID is required",
|
||||
});
|
||||
}
|
||||
@ -113,7 +121,6 @@ export const PaymentRequestActionScheam = (
|
||||
message: "Transacion Date is required",
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.paidById) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
@ -139,7 +146,7 @@ export const PaymentRequestActionScheam = (
|
||||
});
|
||||
};
|
||||
|
||||
export const defaultPaymentRequestActionValues = {
|
||||
export const defaultActionValues = {
|
||||
comment: "",
|
||||
statusId: "",
|
||||
paidTransactionId: null,
|
||||
|
||||
@ -1,93 +0,0 @@
|
||||
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;
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
useActionOnExpense,
|
||||
useActionOnPaymentRequest,
|
||||
@ -29,18 +29,11 @@ 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 } =
|
||||
@ -53,8 +46,7 @@ const ViewPaymentRequest = ({ requestId }) => {
|
||||
const { setDocumentView, setModalSize, setVieRequest, setIsExpenseGenerate } =
|
||||
usePaymentRequestContext();
|
||||
const ActionSchema =
|
||||
PaymentRequestActionScheam(IsPaymentProcess, data?.createdAt) ??
|
||||
z.object({});
|
||||
ExpenseActionScheam(IsPaymentProcess, data?.createdAt) ?? z.object({});
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
@ -65,7 +57,7 @@ const ViewPaymentRequest = ({ requestId }) => {
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(ActionSchema),
|
||||
defaultValues: defaultPaymentRequestActionValues,
|
||||
defaultValues: defaultActionValues,
|
||||
});
|
||||
|
||||
const userPermissions = useSelector(
|
||||
@ -107,7 +99,7 @@ const ViewPaymentRequest = ({ requestId }) => {
|
||||
const onSubmit = (formData) => {
|
||||
const Payload = {
|
||||
...formData,
|
||||
paidAt: localToUtc(formData.paidAt),
|
||||
paidAt: localToUtc(formData.reimburseDate),
|
||||
paymentRequestId: data.id,
|
||||
comment: formData.comment,
|
||||
};
|
||||
@ -130,22 +122,15 @@ const ViewPaymentRequest = ({ requestId }) => {
|
||||
>
|
||||
<div className="col-12 mb-2 text-center ">
|
||||
<h5 className="fw-semibold m-0">Payment Request Details</h5>
|
||||
<hr />
|
||||
</div>
|
||||
<div className="row text-start">
|
||||
<div className=" col-sm-12 col-md-7 border-none border-md-end">
|
||||
<div className="row mb-1">
|
||||
<div className="col-12 col-sm-6 col-md-8">
|
||||
<div className="row">
|
||||
<div className="col-12 d-flex justify-content-between text-start fw-semibold mb-2">
|
||||
<div className="col-12 text-start fw-semibold mb-2">
|
||||
{data?.paymentRequestUID}
|
||||
|
||||
<span
|
||||
className={`badge bg-label-${
|
||||
getColorNameFromHex(data?.expenseStatus?.color) || "secondary"
|
||||
}`}
|
||||
>
|
||||
{data?.expenseStatus?.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-12 mb-3">
|
||||
<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"
|
||||
@ -157,7 +142,7 @@ const ViewPaymentRequest = ({ requestId }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-12 mb-3">
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
@ -170,7 +155,7 @@ const ViewPaymentRequest = ({ requestId }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-12 mb-3">
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
@ -183,7 +168,7 @@ const ViewPaymentRequest = ({ requestId }) => {
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="col-md-12 mb-3">
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
@ -194,7 +179,7 @@ const ViewPaymentRequest = ({ requestId }) => {
|
||||
<div className="text-muted">{data?.payee}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-1 mb-3">
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
@ -211,8 +196,20 @@ const ViewPaymentRequest = ({ requestId }) => {
|
||||
</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"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Payment Mode :
|
||||
</label>
|
||||
<div className="text-muted">{data?.paymentMode?.name}</div>
|
||||
</div>
|
||||
</div> */}
|
||||
{data?.gstNumber && (
|
||||
<div className="col-md-12 mb-3">
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
@ -226,8 +223,25 @@ const ViewPaymentRequest = ({ requestId }) => {
|
||||
)}
|
||||
|
||||
{/* Row 4 */}
|
||||
|
||||
<div className="col-md-12 mb-3">
|
||||
<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"
|
||||
@ -241,7 +255,18 @@ const ViewPaymentRequest = ({ requestId }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-12 mb-3">
|
||||
<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"
|
||||
@ -257,7 +282,7 @@ const ViewPaymentRequest = ({ requestId }) => {
|
||||
|
||||
{/* Row 6 */}
|
||||
{data?.createdBy && (
|
||||
<div className="col-md-12 text-start">
|
||||
<div className="col-md-6 text-start">
|
||||
<div className="d-flex align-items-center">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold"
|
||||
@ -282,7 +307,7 @@ const ViewPaymentRequest = ({ requestId }) => {
|
||||
</div>
|
||||
)}
|
||||
{data?.paidBy && (
|
||||
<div className="col-md-12 text-start">
|
||||
<div className="col-md-6 text-start">
|
||||
<div className="d-flex align-items-center">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold"
|
||||
@ -311,8 +336,8 @@ const ViewPaymentRequest = ({ requestId }) => {
|
||||
<label className="fw-semibold form-label">Description : </label>
|
||||
<div className="text-muted">{data?.description}</div>
|
||||
</div>
|
||||
<div className="col-12 text-start mb-2">
|
||||
<label className="form-label me-2 mb-1 fw-semibold">
|
||||
<div className="col-6 text-start">
|
||||
<label className="form-label me-2 mb-2 fw-semibold">
|
||||
Attachment :
|
||||
</label>
|
||||
|
||||
@ -512,7 +537,7 @@ const ViewPaymentRequest = ({ requestId }) => {
|
||||
<i className="bx bx-time-five me-2 "></i>{" "}
|
||||
<p className="fw-medium">TimeLine</p>
|
||||
</div>
|
||||
<PaymentStatusLogs data={data} />
|
||||
<PaymentStat data={data} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -1,430 +1,490 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Label from '../common/Label';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useExpenseCategory } from '../../hooks/masterHook/useMaster';
|
||||
import DatePicker from '../common/DatePicker';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { defaultRecurringExpense, PaymentRecurringExpense } from './RecurringExpenseSchema';
|
||||
import { INR_CURRENCY_CODE } from '../../utils/constants';
|
||||
import { useCurrencies, useProjectName } from '../../hooks/useProjects';
|
||||
import { useCreateRecurringExpense } from '../../hooks/useExpense';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Label from "../common/Label";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import {
|
||||
useExpenseCategory,
|
||||
useRecurringStatus,
|
||||
} from "../../hooks/masterHook/useMaster";
|
||||
import DatePicker from "../common/DatePicker";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
defaultRecurringExpense,
|
||||
PaymentRecurringExpense,
|
||||
} from "./RecurringExpenseSchema";
|
||||
import {
|
||||
FREQUENCY_FOR_RECURRING,
|
||||
INR_CURRENCY_CODE,
|
||||
} from "../../utils/constants";
|
||||
import { useCurrencies, useProjectName } from "../../hooks/useProjects";
|
||||
import {
|
||||
useCreateRecurringExpense,
|
||||
usePayee,
|
||||
useRecurringExpenseDetail,
|
||||
useUpdateRecurringExpense,
|
||||
} from "../../hooks/useExpense";
|
||||
import InputSuggestions from "../common/InputSuggestion";
|
||||
import MultiEmployeeSearchInput from "../common/MultiEmployeeSearchInput";
|
||||
|
||||
function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
error: requestError,
|
||||
} = useRecurringExpenseDetail(requestToEdit);
|
||||
|
||||
const data = {}
|
||||
const { projectNames, loading: projectLoading, error, isError: isProjectError,
|
||||
} = useProjectName();
|
||||
//APIs
|
||||
const {
|
||||
projectNames,
|
||||
loading: projectLoading,
|
||||
error,
|
||||
isError: isProjectError,
|
||||
} = useProjectName();
|
||||
const {
|
||||
data: currencyData,
|
||||
isLoading: currencyLoading,
|
||||
isError: currencyError,
|
||||
} = useCurrencies();
|
||||
const {
|
||||
data: statusData,
|
||||
isLoading: statusLoading,
|
||||
isError: statusError,
|
||||
} = useRecurringStatus();
|
||||
const {
|
||||
data: Payees,
|
||||
isLoading: isPayeeLoaing,
|
||||
isError: isPayeeError,
|
||||
error: payeeError,
|
||||
} = usePayee();
|
||||
const {
|
||||
ExpenseCategories,
|
||||
loading: ExpenseLoading,
|
||||
error: ExpenseError,
|
||||
} = useExpenseCategory();
|
||||
|
||||
const { data: currencyData, isLoading: currencyLoading, isError: currencyError } = useCurrencies();
|
||||
const schema = PaymentRecurringExpense();
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
watch,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: defaultRecurringExpense,
|
||||
});
|
||||
const handleClose = () => {
|
||||
reset();
|
||||
closeModal();
|
||||
};
|
||||
|
||||
|
||||
const {
|
||||
ExpenseCategories,
|
||||
loading: ExpenseLoading,
|
||||
error: ExpenseError,
|
||||
} = useExpenseCategory();
|
||||
|
||||
const schema = PaymentRecurringExpense();
|
||||
|
||||
const { register, control, watch, handleSubmit, setValue, reset, formState: { errors }, } = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: defaultRecurringExpense,
|
||||
const { mutate: CreateRecurringExpense, isPending: createPending } =
|
||||
useCreateRecurringExpense(() => {
|
||||
handleClose();
|
||||
});
|
||||
const handleClose = () => {
|
||||
reset();
|
||||
closeModal();
|
||||
// const { mutate: PaymentRequestUpdate, isPending } = useUpdatePaymentRequest(() =>
|
||||
// handleClose()
|
||||
// );
|
||||
|
||||
useEffect(() => {
|
||||
if (requestToEdit && data) {
|
||||
reset({
|
||||
title: data.title || "",
|
||||
description: data.description || "",
|
||||
payee: data.payee || "",
|
||||
notifyTo: data.notifyTo || "",
|
||||
currencyId: data.currency.id || "",
|
||||
amount: data.amount || "",
|
||||
strikeDate: data.strikeDate?.slice(0, 10) || "",
|
||||
projectId: data.project.id || "",
|
||||
paymentBufferDays: data.paymentBufferDays || "",
|
||||
numberOfIteration: data.numberOfIteration || "",
|
||||
expenseCategoryId: data.expenseCategory.id || "",
|
||||
statusId: data.statusId || "",
|
||||
frequency: data.frequency || "",
|
||||
isVariable: data.isVariable || false,
|
||||
});
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
// console.log("Veer",data)
|
||||
|
||||
const onSubmit = (fromdata) => {
|
||||
let payload = {
|
||||
...fromdata,
|
||||
// strikeDate: localToUtc(fromdata.strikeDate),
|
||||
strikeDate: fromdata.strikeDate
|
||||
? new Date(fromdata.strikeDate).toISOString()
|
||||
: null,
|
||||
};
|
||||
if (requestToEdit) {
|
||||
const editPayload = { ...payload, id: data.id };
|
||||
PaymentRequestUpdate({ id: data.id, payload: editPayload });
|
||||
} else {
|
||||
CreateRecurringExpense(payload);
|
||||
}
|
||||
console.log("Kartik", payload);
|
||||
};
|
||||
|
||||
const { mutate: CreateRecurringExpense, isPending: createPending } = useCreateRecurringExpense(
|
||||
() => {
|
||||
handleClose();
|
||||
}
|
||||
);
|
||||
// const { mutate: PaymentRequestUpdate, isPending } = useUpdatePaymentRequest(() =>
|
||||
// handleClose()
|
||||
// );
|
||||
return (
|
||||
<div className="container p-3">
|
||||
<h5 className="m-0">
|
||||
{requestToEdit
|
||||
? "Update Expense Recurring "
|
||||
: "Create Expense Recurring"}
|
||||
</h5>
|
||||
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
|
||||
{/* Project and Category */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label className="form-label" required>
|
||||
Select Project
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register("projectId")}
|
||||
>
|
||||
<option value="">Select Project</option>
|
||||
{projectLoading ? (
|
||||
<option>Loading...</option>
|
||||
) : (
|
||||
projectNames?.map((project) => (
|
||||
<option key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{errors.projectId && (
|
||||
<small className="danger-text">{errors.projectId.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
useEffect(() => {
|
||||
if (requestToEdit && data) {
|
||||
reset({
|
||||
title: data.title || "",
|
||||
description: data.description || "",
|
||||
payee: data.payee || "",
|
||||
notifyTo: data.notifyTo || "",
|
||||
currencyId: data.currency.id || "",
|
||||
amount: data.amount || "",
|
||||
strikeDate: data.strikeDate?.slice(0, 10) || "",
|
||||
projectId: data.project.id || "",
|
||||
paymentBufferDays: data.paymentBufferDays || "",
|
||||
numberOfIteration: data.numberOfIteration || "",
|
||||
expenseCategoryId: data.expenseCategory.id || "",
|
||||
statusId: data.statusId || "",
|
||||
frequency: data.frequency || "",
|
||||
isVariable: data.isVariable || false,
|
||||
|
||||
});
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
// console.log("Veer",data)
|
||||
|
||||
const onSubmit = (fromdata) => {
|
||||
|
||||
let payload = {
|
||||
...fromdata,
|
||||
// strikeDate: localToUtc(fromdata.strikeDate),
|
||||
strikeDate: fromdata.strikeDate ? new Date(fromdata.strikeDate).toISOString() : null,
|
||||
};
|
||||
if (requestToEdit) {
|
||||
const editPayload = { ...payload, id: data.id};
|
||||
PaymentRequestUpdate({ id: data.id, payload: editPayload });
|
||||
} else {
|
||||
CreateRecurringExpense(payload);
|
||||
}
|
||||
console.log("Kartik", payload)
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container p-3">
|
||||
<h5 className="m-0">
|
||||
{requestToEdit ? "Update Expense Recurring " : "Create Expense Recurring"}
|
||||
</h5>
|
||||
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
|
||||
{/* Project and Category */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label className="form-label" required>
|
||||
Select Project
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register("projectId")}
|
||||
>
|
||||
<option value="">Select Project</option>
|
||||
{projectLoading ? (
|
||||
<option>Loading...</option>
|
||||
) : (
|
||||
projectNames?.map((project) => (
|
||||
<option key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{errors.projectId && (
|
||||
<small className="danger-text">{errors.projectId.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="expenseCategoryId" className="form-label" required>
|
||||
Expense Category
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
id="expenseCategoryId"
|
||||
{...register("expenseCategoryId")}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select Category
|
||||
</option>
|
||||
{ExpenseLoading ? (
|
||||
<option disabled>Loading...</option>
|
||||
) : (
|
||||
ExpenseCategories?.map((expense) => (
|
||||
<option key={expense.id} value={expense.id}>
|
||||
{expense.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{errors.expenseCategoryId && (
|
||||
<small className="danger-text">
|
||||
{errors.expenseCategoryId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Title and Is Variable */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="title" className="form-label" required>
|
||||
Title
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
className="form-control form-control-sm"
|
||||
{...register("title")}
|
||||
/>
|
||||
{errors.title && (
|
||||
<small className="danger-text">
|
||||
{errors.title.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="isVariable" className="form-label" required>
|
||||
Is Variable
|
||||
</Label>
|
||||
<select
|
||||
id="isVariable"
|
||||
className="form-select form-select-sm"
|
||||
{...register("isVariable", {
|
||||
setValueAs: (v) => v === "true" ? true : v === "false" ? false : false,
|
||||
})}
|
||||
>
|
||||
<option value="false">False</option>
|
||||
<option value="true">True</option>
|
||||
</select>
|
||||
{errors.isVariable && (
|
||||
<small className="danger-text">{errors.isVariable.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Date and Amount */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="strikeDate" className="form-label" required>
|
||||
Strike Date
|
||||
</Label>
|
||||
<DatePicker
|
||||
name="strikeDate"
|
||||
control={control}
|
||||
minDate={new Date()}
|
||||
className='w-100'
|
||||
/>
|
||||
|
||||
{errors.strikeDate && (
|
||||
<small className="danger-text">
|
||||
{errors.strikeDate.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="amount" className="form-label" required>
|
||||
Amount
|
||||
</Label>
|
||||
<input
|
||||
type="number"
|
||||
id="amount"
|
||||
className="form-control form-control-sm"
|
||||
min="1"
|
||||
step="0.01"
|
||||
inputMode="decimal"
|
||||
{...register("amount", { valueAsNumber: true })}
|
||||
/>
|
||||
{errors.amount && (
|
||||
<small className="danger-text">{errors.amount.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payee and Currency */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="payee" className="form-label" required>
|
||||
Payee (Supplier Name/Transporter Name/Other)
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
id="payee"
|
||||
className="form-control form-control-sm"
|
||||
{...register("payee")}
|
||||
|
||||
/>
|
||||
{errors.payee && (
|
||||
<small className="danger-text">
|
||||
{errors.payee.message}
|
||||
</small>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="currencyId" className="form-label" required>
|
||||
Currency
|
||||
</Label>
|
||||
<select
|
||||
id="currencyId"
|
||||
className="form-select form-select-sm"
|
||||
{...register("currencyId")}
|
||||
>
|
||||
<option value="">Select Currency</option>
|
||||
|
||||
{currencyLoading && <option>Loading...</option>}
|
||||
|
||||
{!currencyLoading &&
|
||||
!currencyError &&
|
||||
currencyData?.map((currency) => (
|
||||
<option key={currency.id} value={currency.id}>
|
||||
{`${currency.currencyName} (${currency.symbol})`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.currencyId && (
|
||||
<small className="danger-text">{errors.currencyId.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Notify To and Status Id */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="notifyTo" className="form-label" required>
|
||||
Notify (E-mail)
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
id="notifyTo"
|
||||
className="form-control form-control-sm"
|
||||
{...register("notifyTo")}
|
||||
|
||||
/>
|
||||
{errors.notifyTo && (
|
||||
<small className="danger-text">
|
||||
{errors.notifyTo.message}
|
||||
</small>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="statusId" className="form-label" required>
|
||||
Status
|
||||
</Label>
|
||||
<select
|
||||
id="statusId"
|
||||
className="form-select form-select-sm"
|
||||
{...register("statusId")}
|
||||
>
|
||||
<option value="">Select Currency</option>
|
||||
|
||||
{currencyLoading && <option>Loading...</option>}
|
||||
|
||||
{!currencyLoading &&
|
||||
!currencyError &&
|
||||
currencyData?.map((currency) => (
|
||||
<option key={currency.id} value={currency.id}>
|
||||
{`${currency.currencyName} (${currency.symbol})`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.statusId && (
|
||||
<small className="danger-text">{errors.statusId.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Payment Buffer Days and Number of Iteration */}
|
||||
<div className="row my-2 text-start">
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="paymentBufferDays" className="form-label" required>
|
||||
Payment Buffer Days
|
||||
</Label>
|
||||
<input
|
||||
type="number"
|
||||
id="paymentBufferDays"
|
||||
className="form-control form-control-sm"
|
||||
min="0"
|
||||
step="1"
|
||||
{...register("paymentBufferDays", { valueAsNumber: true })}
|
||||
/>
|
||||
{errors.paymentBufferDays && (
|
||||
<small className="danger-text">{errors.paymentBufferDays.message}</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="numberOfIteration" className="form-label" required>
|
||||
Number of Iteration
|
||||
</Label>
|
||||
<input
|
||||
type="number"
|
||||
id="numberOfIteration"
|
||||
className="form-control form-control-sm"
|
||||
min="1"
|
||||
step="1"
|
||||
{...register("numberOfIteration", { valueAsNumber: true })}
|
||||
/>
|
||||
{errors.numberOfIteration && (
|
||||
<small className="danger-text">{errors.numberOfIteration.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-12">
|
||||
<Label htmlFor="description" className="form-label" required>
|
||||
Description
|
||||
</Label>
|
||||
<textarea
|
||||
id="description"
|
||||
className="form-control form-control-sm"
|
||||
{...register("description")}
|
||||
rows="2"
|
||||
></textarea>
|
||||
{errors.description && (
|
||||
<small className="danger-text">
|
||||
{errors.description.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="d-flex justify-content-end gap-3">
|
||||
<button
|
||||
type="reset"
|
||||
// disabled={createPending}
|
||||
onClick={handleClose}
|
||||
className="btn btn-label-secondary btn-sm mt-3"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-sm mt-3"
|
||||
// disabled={createPending}
|
||||
>
|
||||
{createPending
|
||||
? "Please Wait..."
|
||||
: requestToEdit
|
||||
? "Update"
|
||||
: "Submit"}
|
||||
</button>
|
||||
</div> */}
|
||||
|
||||
<div className="d-flex justify-content-end gap-3">
|
||||
<button
|
||||
type="reset"
|
||||
onClick={handleClose}
|
||||
className="btn btn-label-secondary btn-sm mt-3"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-sm mt-3"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="expenseCategoryId" className="form-label" required>
|
||||
Expense Category
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
id="expenseCategoryId"
|
||||
{...register("expenseCategoryId")}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select Category
|
||||
</option>
|
||||
{ExpenseLoading ? (
|
||||
<option disabled>Loading...</option>
|
||||
) : (
|
||||
ExpenseCategories?.map((expense) => (
|
||||
<option key={expense.id} value={expense.id}>
|
||||
{expense.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{errors.expenseCategoryId && (
|
||||
<small className="danger-text">
|
||||
{errors.expenseCategoryId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
{/* Title and Is Variable */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="title" className="form-label" required>
|
||||
Title
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
className="form-control form-control-sm"
|
||||
{...register("title")}
|
||||
placeholder="Enter title"
|
||||
/>
|
||||
{errors.title && (
|
||||
<small className="danger-text">{errors.title.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-md-6 mt-2">
|
||||
<Label htmlFor="isVariable" className="form-label" required>
|
||||
Payment Type
|
||||
</Label>
|
||||
|
||||
<Controller
|
||||
name="isVariable"
|
||||
control={control}
|
||||
defaultValue={defaultRecurringExpense.isVariable ?? false}
|
||||
render={({ field }) => (
|
||||
<div className="d-flex align-items-center gap-3">
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="radio"
|
||||
id="isVariableTrue"
|
||||
className="form-check-input"
|
||||
checked={field.value === true}
|
||||
onChange={() => field.onChange(true)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="isVariableTrue"
|
||||
className="form-check-label"
|
||||
>
|
||||
Is Variable
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="radio"
|
||||
id="isVariableFalse"
|
||||
className="form-check-input"
|
||||
checked={field.value === false}
|
||||
onChange={() => field.onChange(false)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="isVariableFalse"
|
||||
className="form-check-label"
|
||||
>
|
||||
Fixed
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{errors.isVariable && (
|
||||
<small className="danger-text">{errors.isVariable.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date and Amount */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="strikeDate" className="form-label" required>
|
||||
Strike Date
|
||||
</Label>
|
||||
<DatePicker
|
||||
name="strikeDate"
|
||||
control={control}
|
||||
minDate={new Date()}
|
||||
className="w-100"
|
||||
/>
|
||||
{errors.strikeDate && (
|
||||
<small className="danger-text">{errors.strikeDate.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="amount" className="form-label" required>
|
||||
Amount
|
||||
</Label>
|
||||
<input
|
||||
type="number"
|
||||
id="amount"
|
||||
className="form-control form-control-sm"
|
||||
min="1"
|
||||
step="0.01"
|
||||
inputMode="decimal"
|
||||
{...register("amount", { valueAsNumber: true })}
|
||||
placeholder="Enter amount"
|
||||
/>
|
||||
{errors.amount && (
|
||||
<small className="danger-text">{errors.amount.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payee and Currency */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="payee" className="form-label" required>
|
||||
Payee (Supplier Name/Transporter Name/Other)
|
||||
</Label>
|
||||
<InputSuggestions
|
||||
organizationList={Payees}
|
||||
value={watch("payee") || ""}
|
||||
onChange={(val) =>
|
||||
setValue("payee", val, { shouldValidate: true })
|
||||
}
|
||||
error={errors.payee?.message}
|
||||
placeholder="Select or enter payee"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="currencyId" className="form-label" required>
|
||||
Currency
|
||||
</Label>
|
||||
<select
|
||||
id="currencyId"
|
||||
className="form-select form-select-sm"
|
||||
{...register("currencyId")}
|
||||
>
|
||||
<option value="">Select Currency</option>
|
||||
|
||||
{currencyLoading && <option>Loading...</option>}
|
||||
|
||||
{!currencyLoading &&
|
||||
!currencyError &&
|
||||
currencyData?.map((currency) => (
|
||||
<option key={currency.id} value={currency.id}>
|
||||
{`${currency.currencyName} (${currency.symbol})`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.currencyId && (
|
||||
<small className="danger-text">{errors.currencyId.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Frequency To and Status Id */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="frequency" className="form-label" required>
|
||||
Frequency
|
||||
</Label>
|
||||
<select
|
||||
id="frequency"
|
||||
className="form-select form-select-sm"
|
||||
{...register("frequency", { valueAsNumber: true })}
|
||||
>
|
||||
<option value="">Select Frequency</option>
|
||||
{Object.entries(FREQUENCY_FOR_RECURRING).map(([key, label]) => (
|
||||
<option key={key} value={key}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.frequency && (
|
||||
<small className="danger-text">{errors.frequency.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="statusId" className="form-label" required>
|
||||
Status
|
||||
</Label>
|
||||
<select
|
||||
id="statusId"
|
||||
className="form-select form-select-sm"
|
||||
{...register("statusId")}
|
||||
>
|
||||
<option value="">Select Status</option>
|
||||
|
||||
{statusLoading && <option>Loading...</option>}
|
||||
|
||||
{!currencyLoading &&
|
||||
!currencyError &&
|
||||
statusData?.map((status) => (
|
||||
<option key={status.id} value={status.id}>
|
||||
{`${status.name} `}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.statusId && (
|
||||
<small className="danger-text">{errors.statusId.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Buffer Days and Number of Iteration */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="paymentBufferDays" className="form-label" required>
|
||||
Payment Buffer Days
|
||||
</Label>
|
||||
<input
|
||||
type="number"
|
||||
id="paymentBufferDays"
|
||||
className="form-control form-control-sm"
|
||||
min="0"
|
||||
step="1"
|
||||
{...register("paymentBufferDays", { valueAsNumber: true })}
|
||||
placeholder="Enter payment buffer days"
|
||||
/>
|
||||
{errors.paymentBufferDays && (
|
||||
<small className="danger-text">
|
||||
{errors.paymentBufferDays.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="numberOfIteration" className="form-label" required>
|
||||
Number of Iteration
|
||||
</Label>
|
||||
<input
|
||||
type="number"
|
||||
id="numberOfIteration"
|
||||
className="form-control form-control-sm"
|
||||
min="1"
|
||||
step="1"
|
||||
{...register("numberOfIteration", { valueAsNumber: true })}
|
||||
placeholder="Enter number of iterations"
|
||||
/>
|
||||
{errors.numberOfIteration && (
|
||||
<small className="danger-text">
|
||||
{errors.numberOfIteration.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="notifyTo" className="form-label" required>
|
||||
Notify Employees
|
||||
</Label>
|
||||
|
||||
<MultiEmployeeSearchInput
|
||||
control={control}
|
||||
name="notifyTo"
|
||||
projectId={watch("projectId")}
|
||||
placeholder="Select Employees"
|
||||
forAll={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-12">
|
||||
<Label htmlFor="description" className="form-label" required>
|
||||
Description
|
||||
</Label>
|
||||
<textarea
|
||||
id="description"
|
||||
className="form-control form-control-sm"
|
||||
{...register("description")}
|
||||
rows="2"
|
||||
></textarea>
|
||||
{errors.description && (
|
||||
<small className="danger-text">
|
||||
{errors.description.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-end gap-3">
|
||||
<button
|
||||
type="reset"
|
||||
onClick={handleClose}
|
||||
className="btn btn-label-secondary btn-sm mt-3"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary btn-sm mt-3">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ManageRecurringExpense
|
||||
export default ManageRecurringExpense;
|
||||
|
||||
283
src/components/RecurringExpense/RecurringExpenseList.jsx
Normal file
283
src/components/RecurringExpense/RecurringExpenseList.jsx
Normal file
@ -0,0 +1,283 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
EXPENSE_DRAFT,
|
||||
EXPENSE_REJECTEDBY,
|
||||
FREQUENCY_FOR_RECURRING,
|
||||
ITEMS_PER_PAGE,
|
||||
} from "../../utils/constants";
|
||||
import {
|
||||
formatCurrency,
|
||||
useDebounce,
|
||||
} from "../../utils/appUtils";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
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 { useRecurringExpenseContext } from "../../pages/RecurringExpense/RecurringExpensePage";
|
||||
import { useRecurringExpenseList } from "../../hooks/useExpense";
|
||||
|
||||
const RecurringExpenseList = ({ search, filterStatuses }) => {
|
||||
const { setManageRequest, setVieRequest } = useRecurringExpenseContext();
|
||||
const navigate = useNavigate();
|
||||
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [deletingId, setDeletingId] = useState(null);
|
||||
|
||||
const SelfId = useSelector(
|
||||
(store) => store?.globalVariables?.loginUser?.employeeInfo?.id
|
||||
);
|
||||
|
||||
const recurringExpenseColumns = [
|
||||
{
|
||||
key: "expenseCategory",
|
||||
label: "Category",
|
||||
align: "text-start",
|
||||
getValue: (e) => e?.expenseCategory?.name || "N/A",
|
||||
},
|
||||
{
|
||||
key: "title",
|
||||
label: "Title",
|
||||
align: "text-start",
|
||||
getValue: (e) => e?.title || "N/A",
|
||||
},
|
||||
{
|
||||
key: "payee",
|
||||
label: "Payee",
|
||||
align: "text-start",
|
||||
getValue: (e) => e?.payee || "N/A",
|
||||
},
|
||||
{
|
||||
key: "frequency",
|
||||
label: "Frequency",
|
||||
align: "text-start",
|
||||
getValue: (e) =>
|
||||
e?.frequency !== undefined && e?.frequency !== null
|
||||
? FREQUENCY_FOR_RECURRING[e.frequency] || "N/A"
|
||||
: "N/A",
|
||||
},
|
||||
{
|
||||
key: "amount",
|
||||
label: "Amount",
|
||||
align: "text-end",
|
||||
getValue: (e) =>
|
||||
e?.amount
|
||||
? `${e?.currency?.symbol || ""}${e.amount.toLocaleString()}`
|
||||
: "N/A",
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
label: "Next Generation Date",
|
||||
align: "text-center",
|
||||
getValue: (e) =>
|
||||
e?.createdAt ? formatUTCToLocalTime(e.createdAt) : "N/A",
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "Status",
|
||||
align: "text-start",
|
||||
getValue: (e) => e?.status?.name || "N/A",
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const debouncedSearch = useDebounce(search, 500);
|
||||
|
||||
const { data, isLoading, isError, error, isRefetching, refetch } =
|
||||
useRecurringExpenseList(
|
||||
ITEMS_PER_PAGE,
|
||||
currentPage,
|
||||
{},
|
||||
true,
|
||||
debouncedSearch
|
||||
);
|
||||
|
||||
const recurringExpenseData = data?.data || [];
|
||||
const totalPages = data?.totalPages || 1;
|
||||
|
||||
if (isError) {
|
||||
return <Error error={error} isFeteching={isRefetching} refetch={refetch} />;
|
||||
}
|
||||
|
||||
const header = [
|
||||
"Category",
|
||||
"Title",
|
||||
"Amount",
|
||||
"Payee",
|
||||
"Frequency",
|
||||
"Next Generation",
|
||||
"Status",
|
||||
"Action",
|
||||
];
|
||||
|
||||
if (isLoading) return <ExpenseTableSkeleton headers={header} />;
|
||||
|
||||
const canEditExpense = (recurringExpense) => {
|
||||
// return (
|
||||
// (recurringExpense?.expenseStatus?.id === EXPENSE_DRAFT ||
|
||||
// EXPENSE_REJECTEDBY.includes(recurringExpense?.expenseStatus.id)) &&
|
||||
// recurringExpense?.createdBy?.id === SelfId
|
||||
// );
|
||||
};
|
||||
|
||||
const canDeleteExpense = (request) => {
|
||||
return (
|
||||
request?.expenseStatus?.id === EXPENSE_DRAFT &&
|
||||
request?.createdBy?.id === SelfId
|
||||
);
|
||||
};
|
||||
|
||||
const filteredData = recurringExpenseData.filter((item) =>
|
||||
filterStatuses.includes(item?.status?.id)
|
||||
);
|
||||
|
||||
const handleDelete = (id) => {
|
||||
setDeletingId(id);
|
||||
DeleteExpense(
|
||||
{ id },
|
||||
{
|
||||
onSettled: () => {
|
||||
setDeletingId(null);
|
||||
setIsDeleteModalOpen(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{IsDeleteModalOpen && (
|
||||
<ConfirmModal
|
||||
isOpen={IsDeleteModalOpen}
|
||||
type="delete"
|
||||
header="Delete Recurring Expense"
|
||||
message="Are you sure you want to delete?"
|
||||
onSubmit={handleDelete}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
paramData={deletingId}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="card page-min-h table-responsive px-sm-4">
|
||||
<div className="card-datatable" id="payment-request-table">
|
||||
<table className="table border-top dataTable text-nowrap align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
{recurringExpenseColumns.map((col) => (
|
||||
<th key={col.key} className={`sorting ${col.align}`}>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
<th className="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((recurringExpense) => (
|
||||
<tr key={recurringExpense.id} className="align-middle" style={{ height: "50px" }}>
|
||||
{recurringExpenseColumns.map((col) => (
|
||||
<td
|
||||
key={col.key}
|
||||
className={`d-table-cell ${col.align ?? ""} py-3`}
|
||||
>
|
||||
{col?.customRender
|
||||
? col?.customRender(recurringExpense)
|
||||
: col?.getValue(recurringExpense)}
|
||||
</td>
|
||||
))}
|
||||
<td className="sticky-action-column bg-white">
|
||||
<div className="d-flex justify-content-center gap-0">
|
||||
<i
|
||||
className="bx bx-show text-primary cursor-pointer"
|
||||
// onClick={() =>
|
||||
// setVieRequest({
|
||||
// requestId: recurringExpense.id,
|
||||
// view: true,
|
||||
// })
|
||||
// }
|
||||
></i>
|
||||
{/* Uncomment for edit/delete actions */}
|
||||
|
||||
<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"
|
||||
>
|
||||
<i className="bx bx-dots-vertical-rounded text-muted p-0"></i>
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-end w-auto">
|
||||
<li
|
||||
onClick={() =>
|
||||
setManageRequest({
|
||||
IsOpen: true,
|
||||
RecurringId: recurringExpense.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||
<i className="bx bx-edit text-primary bx-xs me-2"></i>
|
||||
Modify
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setDeletingId(recurringExpense.id);
|
||||
}}
|
||||
>
|
||||
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||
<i className="bx bx-trash text-danger bx-xs me-2"></i>
|
||||
Delete
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={recurringExpenseColumns.length + 1} className="text-center border-0 py-8">
|
||||
<p>No Recurring Expense Found</p>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="d-flex justify-content-end py-3 pe-3">
|
||||
<nav>
|
||||
<ul className="pagination mb-0">
|
||||
{[...Array(totalPages)].map((_, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={`page-item ${currentPage === index + 1 ? "active" : ""}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() => setCurrentPage(index + 1)}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecurringExpenseList;
|
||||
@ -1,65 +1,16 @@
|
||||
import { boolean, z } from "zod";
|
||||
import { INR_CURRENCY_CODE } from "../../utils/constants";
|
||||
|
||||
// export const PaymentRecurringExpense = (expenseTypes) => {
|
||||
// return z
|
||||
// .object({
|
||||
// title: z.string().min(1, { message: "Project is required" }),
|
||||
|
||||
// description: z.string().min(1, { message: "Description is required" }),
|
||||
|
||||
// payee: z.string().min(1, { message: "Supplier name is required" }),
|
||||
|
||||
// notifyTo: z.string().min(1, { message: "Notification is required" }),
|
||||
|
||||
// currencyId: z
|
||||
// .string()
|
||||
// .min(1, { message: "Currency is required" }),
|
||||
|
||||
// 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",
|
||||
// }),
|
||||
|
||||
// strikeDate: z.string().min(1, { message: "Date is required" }),
|
||||
|
||||
// projectId: z.string().min(1, { message: "Project is required" }),
|
||||
|
||||
// paymentBufferDays: z.string().min(1, { message: "Buffer days is required" }),
|
||||
|
||||
// numberOfIteration: z.string().min(1, { message: "Iteration is required" }),
|
||||
|
||||
// expenseCategoryId: z
|
||||
// .string()
|
||||
// .min(1, { message: "Expense Category is required" }),
|
||||
|
||||
// statusId: z.string().min(1, { message: "Please select a status" }),
|
||||
|
||||
// frequency: z.string().min(1, { message: "Frequency is required" }),
|
||||
|
||||
// isVariable: z.boolean().optional(),
|
||||
|
||||
// })
|
||||
// };
|
||||
|
||||
|
||||
export const PaymentRecurringExpense = (expenseTypes) => {
|
||||
return z.object({
|
||||
title: z.string().min(1, { message: "Project is required" }),
|
||||
|
||||
description: z.string().min(1, { message: "Description is required" }),
|
||||
|
||||
payee: z.string().min(1, { message: "Supplier name is required" }),
|
||||
|
||||
notifyTo: z.string().min(1, { message: "Notification is required" }),
|
||||
|
||||
title: z.string().min(1, { message: "Title is required" }).transform((val) => val.trim()),
|
||||
description: z.string().min(1, { message: "Description is required" }).transform((val) => val.trim()),
|
||||
payee: z.string().min(1, { message: "Payee name is required" }).transform((val) => val.trim()),
|
||||
notifyTo: z.string().min(1, { message: "Notification e-mail is required" }).transform((val) => val.trim()),
|
||||
currencyId: z
|
||||
.string()
|
||||
.min(1, { message: "Currency is required" }),
|
||||
.min(1, { message: "Currency is required" })
|
||||
.transform((val) => val.trim()),
|
||||
|
||||
amount: z
|
||||
.number({
|
||||
@ -76,11 +27,13 @@ export const PaymentRecurringExpense = (expenseTypes) => {
|
||||
.min(1, { message: "Date is required" })
|
||||
.refine((val) => !isNaN(Date.parse(val)), {
|
||||
message: "Invalid date format",
|
||||
}),
|
||||
})
|
||||
.transform((val) => val.trim()),
|
||||
|
||||
projectId: z
|
||||
.string()
|
||||
.min(1, { message: "Project is required" }),
|
||||
.min(1, { message: "Project is required" })
|
||||
.transform((val) => val.trim()),
|
||||
|
||||
paymentBufferDays: z
|
||||
.number({
|
||||
@ -98,23 +51,28 @@ export const PaymentRecurringExpense = (expenseTypes) => {
|
||||
|
||||
expenseCategoryId: z
|
||||
.string()
|
||||
.min(1, { message: "Expense Category is required" }),
|
||||
.min(1, { message: "Expense Category is required" })
|
||||
.transform((val) => val.trim()),
|
||||
|
||||
statusId: z
|
||||
.string()
|
||||
.min(1, { message: "Please select a status" }),
|
||||
.min(1, { message: "Please select a status" })
|
||||
.transform((val) => val.trim()),
|
||||
|
||||
frequency: z
|
||||
.number({
|
||||
required_error: "Frequency is required",
|
||||
invalid_type_error: "Frequency must be a number",
|
||||
})
|
||||
.min(1, { message: "Frequency must be greater than 0" }),
|
||||
.refine((val) => [0, 1, 2, 3, 4, 5].includes(val), {
|
||||
message: "Invalid frequency selected",
|
||||
}),
|
||||
|
||||
isVariable: z.boolean().optional(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const defaultRecurringExpense = {
|
||||
title: "",
|
||||
description: "",
|
||||
@ -122,37 +80,33 @@ export const defaultRecurringExpense = {
|
||||
notifyTo: "",
|
||||
currencyId: "",
|
||||
amount: 0,
|
||||
strikeDate: "", // or null if your DatePicker accepts null
|
||||
strikeDate: "",
|
||||
projectId: "",
|
||||
paymentBufferDays: 0,
|
||||
numberOfIteration: 1,
|
||||
expenseCategoryId: "",
|
||||
statusId: "",
|
||||
frequency: 1,
|
||||
isVariable: false,
|
||||
isVariable: true,
|
||||
};
|
||||
|
||||
|
||||
// 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 SearchRecurringExpenseSchema = z.object({
|
||||
title: z.array(z.string()).optional(),
|
||||
description: z.array(z.string()).optional(),
|
||||
payee: z.array(z.string()).optional(),
|
||||
notifyTo: z.array(z.string()).optional(),
|
||||
currencyId: z.array(z.string()).optional(),
|
||||
amount: z.array(z.string()).optional(),
|
||||
strikeDate: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
paymentBufferDays: z.string().optional(),
|
||||
numberOfIteration: z.string().optional(),
|
||||
expenseCategoryId: z.string().optional(),
|
||||
statusId: z.string().optional(),
|
||||
frequency: z.string().optional(),
|
||||
isVariable: z.string().optional(),
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@ -3,9 +3,6 @@ import { useEmployeesName } from "../../hooks/useEmployees";
|
||||
import { useDebounce } from "../../utils/appUtils";
|
||||
import { useController } from "react-hook-form";
|
||||
import Avatar from "./Avatar";
|
||||
|
||||
|
||||
|
||||
const EmployeeSearchInput = ({
|
||||
control,
|
||||
name,
|
||||
|
||||
182
src/components/common/MultiEmployeeSearchInput.jsx
Normal file
182
src/components/common/MultiEmployeeSearchInput.jsx
Normal file
@ -0,0 +1,182 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useEmployeesName } from "../../hooks/useEmployees";
|
||||
import { useDebounce } from "../../utils/appUtils";
|
||||
import { useController } from "react-hook-form";
|
||||
import Avatar from "../common/Avatar";
|
||||
|
||||
const MultiEmployeeSearchInput = ({
|
||||
control,
|
||||
name,
|
||||
projectId,
|
||||
placeholder,
|
||||
forAll,
|
||||
}) => {
|
||||
const {
|
||||
field: { onChange, value, ref },
|
||||
fieldState: { error },
|
||||
} = useController({ name, control });
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [selectedEmployees, setSelectedEmployees] = useState([]);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const debouncedSearch = useDebounce(search, 500);
|
||||
|
||||
const { data: employees, isLoading } = useEmployeesName(
|
||||
projectId,
|
||||
debouncedSearch,
|
||||
forAll
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (value && employees?.data) {
|
||||
// Ensure value is a string (sometimes it may come as array/object)
|
||||
const stringValue =
|
||||
typeof value === "string"
|
||||
? value
|
||||
: Array.isArray(value)
|
||||
? value.join(",")
|
||||
: "";
|
||||
|
||||
const emails = stringValue.split(",").filter(Boolean);
|
||||
const foundEmps = employees.data.filter((emp) =>
|
||||
emails.includes(emp.email)
|
||||
);
|
||||
|
||||
setSelectedEmployees(foundEmps);
|
||||
|
||||
if (forAll && foundEmps.length > 0) {
|
||||
setSearch(""); // clear search field
|
||||
}
|
||||
}
|
||||
}, [value, employees?.data, forAll]);
|
||||
|
||||
const handleSelect = (employee) => {
|
||||
if (!selectedEmployees.find((emp) => emp.email === employee.email)) {
|
||||
const newSelected = [...selectedEmployees, employee];
|
||||
setSelectedEmployees(newSelected);
|
||||
// Store emails instead of IDs
|
||||
onChange(
|
||||
newSelected
|
||||
.map((e) => e.email)
|
||||
.filter(Boolean)
|
||||
.join(",")
|
||||
);
|
||||
setSearch("");
|
||||
setShowDropdown(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (email) => {
|
||||
const newSelected = selectedEmployees.filter((e) => e.email !== email);
|
||||
setSelectedEmployees(newSelected);
|
||||
onChange(
|
||||
newSelected
|
||||
.map((e) => e.email)
|
||||
.filter(Boolean)
|
||||
.join(",")
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setShowDropdown(false);
|
||||
}
|
||||
};
|
||||
const handleEsc = (event) => {
|
||||
if (event.key === "Escape") setShowDropdown(false);
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
document.addEventListener("keydown", handleEsc);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
document.removeEventListener("keydown", handleEsc);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="position-relative" ref={dropdownRef}>
|
||||
<div className="d-flex flex-wrap gap-1 mb-1">
|
||||
{selectedEmployees.map((emp) => (
|
||||
<div
|
||||
key={emp.email}
|
||||
className="badge bg-label-secondary d-flex align-items-center py-0 px-1"
|
||||
style={{ fontSize: "0.75rem" }}
|
||||
>
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0 me-1"
|
||||
firstName={emp.firstName}
|
||||
lastName={emp.lastName}
|
||||
/>
|
||||
{emp.firstName} {emp.lastName}
|
||||
<span
|
||||
className="ms-1"
|
||||
style={{ cursor: "pointer", fontSize: "0.75rem" }}
|
||||
onClick={() => handleRemove(emp.email)}
|
||||
>
|
||||
×
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="search"
|
||||
ref={ref}
|
||||
className="form-control form-control-sm"
|
||||
placeholder={placeholder}
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setShowDropdown(true);
|
||||
}}
|
||||
onFocus={() => setShowDropdown(true)}
|
||||
/>
|
||||
|
||||
{showDropdown && (employees?.data?.length > 0 || isLoading) && (
|
||||
<ul
|
||||
className="list-group position-absolute bg-white w-100 shadow z-3 rounded-1 px-0"
|
||||
style={{ maxHeight: 200, overflowY: "auto", zIndex: 1050 }}
|
||||
>
|
||||
{isLoading ? (
|
||||
<li className="list-group-item py-1 px-2 text-muted">
|
||||
Searching...
|
||||
</li>
|
||||
) : (
|
||||
employees?.data
|
||||
?.filter(
|
||||
(emp) => !selectedEmployees.find((e) => e.email === emp.email)
|
||||
)
|
||||
.map((emp) => (
|
||||
<li
|
||||
key={emp.email}
|
||||
className="list-group-item list-group-item-action py-1 px-2"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSelect(emp)}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0 me-2"
|
||||
firstName={emp.firstName}
|
||||
lastName={emp.lastName}
|
||||
/>
|
||||
<span className="text-muted">{`${emp.firstName} ${emp.lastName}`}</span>
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{error && <small className="text-danger">{error.message}</small>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiEmployeeSearchInput;
|
||||
@ -1,8 +1,5 @@
|
||||
import React from "react";
|
||||
import Avatar from "./Avatar";
|
||||
import Tooltip from "./Tooltip";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import moment from "moment";
|
||||
|
||||
const Timeline = ({ items = [], transparent = true }) => {
|
||||
return (
|
||||
@ -11,99 +8,82 @@ const Timeline = ({ items = [], transparent = true }) => {
|
||||
transparent ? "timeline-transparent text-start" : ""
|
||||
}`}
|
||||
>
|
||||
{items &&
|
||||
items?.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className={`timeline-item ${
|
||||
transparent ? "timeline-item-transparent" : ""
|
||||
{items.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className={`timeline-item ${
|
||||
transparent ? "timeline-item-transparent" : ""
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`timeline-point timeline-point-${
|
||||
item.color || "primary"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`timeline-point timeline-point-${
|
||||
item.color || "primary"
|
||||
}`}
|
||||
></span>
|
||||
></span>
|
||||
|
||||
<div className="timeline-event">
|
||||
<div className="timeline-header mb-3 d-flex justify-content-between">
|
||||
<h6 className="mb-0 text-body">{item.title}</h6>
|
||||
<small className="text-body-secondary">
|
||||
<Tooltip text={formatUTCToLocalTime(item.timeAgo, true)}>
|
||||
{moment.utc(item.timeAgo).local().fromNow()}
|
||||
</Tooltip>
|
||||
</small>
|
||||
<div className="timeline-event">
|
||||
<div className="timeline-header mb-3 d-flex justify-content-between">
|
||||
<h6 className="mb-0 text-body">{item.title}</h6>
|
||||
<small className="text-body-secondary">{item.timeAgo}</small>
|
||||
</div>
|
||||
|
||||
{item.description && <p className="mb-2">{item.description}</p>}
|
||||
|
||||
{item.attachments && item.attachments.length > 0 && (
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
{item.attachments.map((att, i) => (
|
||||
<div
|
||||
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"
|
||||
/>
|
||||
)}
|
||||
<span className="h6 mb-0">{att.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.description && <p className="mb-2">{item.description}</p>}
|
||||
|
||||
{item.attachments && item.attachments.length > 0 && (
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
{item.attachments.map((att, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="badge bg-lighter rounded d-flex align-items-center gap-2 p-2"
|
||||
>
|
||||
{att.icon && (
|
||||
{item.users && item.users.length > 0 && (
|
||||
<div className="d-flex flex-wrap align-items-center mb-2">
|
||||
<ul className="list-unstyled users-list d-flex align-items-center avatar-group m-0">
|
||||
{item.users.map((user, i) => (
|
||||
<li key={i} className="avatar me-1" title={user.name}>
|
||||
{user.avatar ? (
|
||||
<img
|
||||
src={att.icon}
|
||||
alt="file"
|
||||
width="15"
|
||||
className="me-2"
|
||||
src={user.avatar}
|
||||
alt={user.name}
|
||||
className="rounded-circle"
|
||||
width="32"
|
||||
height="32"
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
firstName={user.firstName}
|
||||
lastName={user.lastName}
|
||||
/>
|
||||
)}
|
||||
<span className="h6 mb-0">{att.name}</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
{item.users && item.users.length > 0 && (
|
||||
<div className="d-flex flex-wrap align-items-center mb-2">
|
||||
<ul className="list-unstyled users-list d-flex align-items-center avatar-group m-0">
|
||||
{item.users.map((user, i) => (
|
||||
<li key={i} className="m-0" title={user.name}>
|
||||
{user.avatar ? (
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={user.name}
|
||||
className="rounded-circle"
|
||||
width="32"
|
||||
height="32"
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
size="xs"
|
||||
firstName={user.firstName}
|
||||
lastName={user.lastName}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{item.users?.length === 1 && (
|
||||
<div className="m-0">
|
||||
<p className="mb-0 text-xs fw-medium">{`${item.users[0].firstName} ${item.users[0].lastName}`}</p>
|
||||
<small>{item.users[0].role}</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
{!items ||
|
||||
(items.length == 0 && (
|
||||
<li
|
||||
className={`timeline-item text-center ${
|
||||
transparent ? "timeline-item-transparent" : ""
|
||||
}`}
|
||||
>
|
||||
Not action yet.
|
||||
</li>
|
||||
))}
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
@ -10,6 +10,20 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import showToast from "../../services/toastService";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const useRecurringStatus = ()=>{
|
||||
return useQuery({
|
||||
queryKey:["RecurringStatus"],
|
||||
queryFn:async()=>{
|
||||
const resp = await MasterRespository.getRecurringStatus();
|
||||
return resp.data
|
||||
}
|
||||
})
|
||||
}
|
||||
export const usePaymentAjustmentHead = (isActive) => {
|
||||
return useQuery({
|
||||
queryKey: ["paymentType", isActive],
|
||||
|
||||
@ -5,8 +5,7 @@ import { queryClient } from "../layouts/AuthLayout";
|
||||
import { useSelector } from "react-redux";
|
||||
import moment from "moment";
|
||||
|
||||
|
||||
|
||||
// -------------------Query------------------------------------------------------
|
||||
export const usePayee =()=>{
|
||||
return useQuery({
|
||||
queryKey:["payee"],
|
||||
@ -16,10 +15,6 @@ export const usePayee =()=>{
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// -------------------Query------------------------------------------------------
|
||||
|
||||
const cleanFilter = (filter) => {
|
||||
const cleaned = { ...filter };
|
||||
|
||||
@ -467,3 +462,52 @@ export const useCreateRecurringExpense = (onSuccessCallBack) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateRecurringExpense = (onSuccessCallBack) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, payload }) => {
|
||||
const response = await ExpenseRepository.UpdateRecurringExpense(id, payload);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (updatedExpense, variables) => {
|
||||
queryClient.removeQueries({ queryKey: ["recurringExpense", variables.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ["recurringExpenseList"] });
|
||||
showToast("Recurring Expense updated Successfully", "success");
|
||||
|
||||
if (onSuccessCallBack) onSuccessCallBack();
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast("Something went wrong.Please try again later.", "error");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useRecurringExpenseList = (
|
||||
pageSize,
|
||||
pageNumber,
|
||||
filter,
|
||||
isActive,
|
||||
searchString = "",
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: ["recurringExpenseList",pageSize,pageNumber,filter,isActive,searchString],
|
||||
queryFn: async()=>{
|
||||
debugger
|
||||
const resp = await ExpenseRepository.GetRecurringExpenseList(pageSize,pageNumber,filter,isActive,searchString);
|
||||
return resp.data;
|
||||
},
|
||||
keepPreviousData: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const useRecurringExpenseDetail =(RequestId)=>{
|
||||
return useQuery({
|
||||
queryKey:['recurringExpense',RequestId],
|
||||
queryFn:async()=>{
|
||||
RequestId
|
||||
const resp = await ExpenseRepository.GetRecurringExpense(RequestId);
|
||||
return resp.data;
|
||||
},
|
||||
enabled:!!RequestId
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import React, { createContext, useContext, useState, useEffect, useRef } from "react";
|
||||
import { useForm, useFormContext } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useSelector } from "react-redux";
|
||||
@ -25,10 +19,7 @@ import {
|
||||
VIEW_SELF_EXPENSE,
|
||||
} from "../../utils/constants";
|
||||
|
||||
import {
|
||||
defaultFilter,
|
||||
SearchSchema,
|
||||
} from "../../components/Expenses/ExpenseSchema";
|
||||
import { defaultFilter, SearchSchema } from "../../components/Expenses/ExpenseSchema";
|
||||
import PreviewDocument from "../../components/Expenses/PreviewDocument";
|
||||
|
||||
// Context
|
||||
@ -111,7 +102,7 @@ const ExpensePage = () => {
|
||||
setManageExpenseModal,
|
||||
setDocumentView,
|
||||
filterData,
|
||||
removeFilterChip,
|
||||
removeFilterChip
|
||||
};
|
||||
|
||||
return (
|
||||
@ -137,6 +128,7 @@ const ExpensePage = () => {
|
||||
</div>
|
||||
|
||||
<div className="col-6 text-end mt-2 mt-sm-0">
|
||||
|
||||
{IsCreatedAble && (
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
@ -159,6 +151,8 @@ const ExpensePage = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<ExpenseList
|
||||
filters={filters}
|
||||
groupBy={groupBy}
|
||||
@ -196,7 +190,7 @@ const ExpensePage = () => {
|
||||
{viewExpense.view && (
|
||||
<GlobalModel
|
||||
isOpen
|
||||
size="xl"
|
||||
size="lg"
|
||||
modalType="top"
|
||||
closeModal={() => setViewExpense({ expenseId: null, view: false })}
|
||||
>
|
||||
|
||||
@ -15,7 +15,7 @@ export const PaymentRequestContext = createContext();
|
||||
export const usePaymentRequestContext = () => {
|
||||
const context = useContext(PaymentRequestContext);
|
||||
if (!context) {
|
||||
throw new Error("usePaymentRequestContext must be used within an RequestPaymentProvider");
|
||||
throw new Error("usePaymentRequestContext must be used within an ExpenseProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
@ -4,7 +4,9 @@ import GlobalModel from "../../components/common/GlobalModel";
|
||||
import { useFab } from "../../Context/FabContext";
|
||||
// import { defaultPaymentRequestFilter,SearchPaymentRequestSchema } from "../../components/PaymentRequest/PaymentRequestSchema";
|
||||
import ManageRecurringExpense from "../../components/RecurringExpense/ManageRecurringExpense";
|
||||
import RecurringExpenseList from "../../components/RecurringExpense/RecurringRexpenseList";
|
||||
import RecurringExpenseList from "../../components/RecurringExpense/RecurringExpenseList";
|
||||
import { PAYEE_RECURRING_EXPENSE } from "../../utils/constants";
|
||||
import { SearchRecurringExpenseSchema } from "../../components/RecurringExpense/RecurringExpenseSchema";
|
||||
|
||||
export const RecurringExpenseContext = createContext();
|
||||
export const useRecurringExpenseContext = () => {
|
||||
@ -20,8 +22,10 @@ const RecurringExpensePage = () => {
|
||||
RequestId: null,
|
||||
});
|
||||
const [ViewRequest, setVieRequest] = useState({ view: false, requestId: null })
|
||||
const { setOffcanvasContent, setShowTrigger } = useFab();
|
||||
// const [filters, setFilters] = useState(defaultPaymentRequestFilter);
|
||||
|
||||
const [selectedStatuses, setSelectedStatuses] = useState(
|
||||
PAYEE_RECURRING_EXPENSE.map((s) => s.id)
|
||||
);
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
@ -30,19 +34,13 @@ const RecurringExpensePage = () => {
|
||||
setVieRequest
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
setShowTrigger(true);
|
||||
setOffcanvasContent(
|
||||
"Payment Request Filters",
|
||||
// <PaymentRequestFilterPanel onApply={setFilters} />
|
||||
const handleStatusChange = (id) => {
|
||||
setSelectedStatuses((prev) =>
|
||||
prev.includes(id)
|
||||
? prev.filter((s) => s !== id)
|
||||
: [...prev, id]
|
||||
);
|
||||
|
||||
return () => {
|
||||
setShowTrigger(false);
|
||||
setOffcanvasContent("", null);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
return (
|
||||
<RecurringExpenseContext.Provider value={contextValue}>
|
||||
@ -57,19 +55,47 @@ const RecurringExpensePage = () => {
|
||||
|
||||
{/* Top Bar */}
|
||||
<div className="card my-3 px-sm-4 px-0">
|
||||
<div className="card-body py-2 px-3">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-6">
|
||||
<div className="card-body py-2 px-1">
|
||||
<div className="d-flex flex-wrap align-items-center justify-content-between">
|
||||
{/* Left side: Search + Filter */}
|
||||
<div className="d-flex align-items-center flex-wrap">
|
||||
<input
|
||||
type="search"
|
||||
className="form-control form-control-sm w-auto"
|
||||
placeholder="Search Recurring Expense.."
|
||||
placeholder="Search Recurring Expense..."
|
||||
value={search}
|
||||
// onChange={(e) => setSearch(e.target.value)}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="dropdown">
|
||||
<a
|
||||
className="dropdown-toggle hide-arrow cursor-pointer p-1"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
title="Filter"
|
||||
>
|
||||
<i className="bx bx-slider-alt ms-1"></i>
|
||||
</a>
|
||||
<ul className="dropdown-menu p-2 text-capitalize">
|
||||
{PAYEE_RECURRING_EXPENSE.map(({ id, label }) => (
|
||||
<li key={id}>
|
||||
<div className="form-check">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
checked={selectedStatuses.includes(id)}
|
||||
onChange={() => handleStatusChange(id)}
|
||||
/>
|
||||
<label className="form-check-label">{label}</label>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-6 text-end mt-2 mt-sm-0">
|
||||
{/* Right side: Add Button */}
|
||||
<div className="mt-2 mt-sm-0">
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
type="button"
|
||||
@ -82,18 +108,15 @@ const RecurringExpensePage = () => {
|
||||
>
|
||||
<i className="bx bx-plus-circle me-2"></i>
|
||||
<span className="d-none d-md-inline-block">
|
||||
Add Payment Request
|
||||
Add Recurring Expense
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <PaymentRequestList
|
||||
search={search}
|
||||
filters={filters}
|
||||
/> */}
|
||||
<RecurringExpenseList/>
|
||||
|
||||
<RecurringExpenseList filterStatuses={selectedStatuses} search={search} />
|
||||
|
||||
{/* Add/Edit Modal */}
|
||||
{ManageRequest.IsOpen && (
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { api } from "../utils/axiosClient";
|
||||
|
||||
const ExpenseRepository = {
|
||||
GetPayee: () => api.get("/api/Expense/payment-request/payee"),
|
||||
//#region Expense
|
||||
GetExpenseList: (pageSize, pageNumber, filter, searchString) => {
|
||||
const payloadJsonString = JSON.stringify(filter);
|
||||
@ -40,20 +39,33 @@ const ExpenseRepository = {
|
||||
GetPaymentRequestFilter: () => api.get("/api/Expense/payment-request/filter"),
|
||||
ActionOnPaymentRequest: (data) =>
|
||||
api.post("/api/Expense/payment-request/action", data),
|
||||
DeletePaymentRequest:()=>api.get("delete here come"),
|
||||
CreatePaymentRequestExpense:(data)=>api.post('/api/Expense/payment-request/expense/create',data),
|
||||
DeletePaymentRequest: () => api.get("delete here come"),
|
||||
CreatePaymentRequestExpense: (data) =>
|
||||
api.post("/api/Expense/payment-request/expense/create", data),
|
||||
GetPayee:()=>api.get('/api/Expense/payment-request/payee'),
|
||||
//#endregion
|
||||
|
||||
//#region Recurring Expense
|
||||
|
||||
CreateRecurringExpense: (data) => api.post("/api/Expense/recurring-payment/create", data),
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Advance Payment
|
||||
GetTranctionList: (employeeId)=>api.get(`/api/Expense/get/transactions/${employeeId}`),
|
||||
GetRecurringExpenseList:(pageSize, pageNumber, filter, searchString) => {
|
||||
const payloadJsonString = JSON.stringify(filter);
|
||||
return api.get(
|
||||
`/api/expense/get/recurring-payment/list?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}`
|
||||
);
|
||||
},
|
||||
CreateRecurringExpense: (data) =>
|
||||
api.post("/api/Expense/recurring-payment/create", data),
|
||||
UpdateRecurringExpense: (id, data) =>
|
||||
api.put(`/api/Expense/recurring-payment/edit/${id}`, data),
|
||||
GetRecurringExpense: (id) =>
|
||||
api.get(`/api/Expense/get/recurring-payment/details/${id}`),
|
||||
//#endregion
|
||||
|
||||
//#region Advance Payment
|
||||
GetTranctionList: (employeeId) =>
|
||||
api.get(`/api/Expense/get/transactions/${employeeId}`),
|
||||
//#endregion
|
||||
|
||||
|
||||
};
|
||||
|
||||
export default ExpenseRepository;
|
||||
|
||||
@ -141,4 +141,7 @@ export const MasterRespository = {
|
||||
api.post(`/api/Master/payment-adjustment-head`, data),
|
||||
updatePaymentAjustmentHead: (id, data) =>
|
||||
api.put(`/api/Master/payment-adjustment-head/edit/${id}`, data),
|
||||
|
||||
|
||||
getRecurringStatus:()=>api.get(`/api/Master/recurring-status/list`)
|
||||
};
|
||||
|
||||
@ -108,7 +108,7 @@ export function localToUtc(dateString) {
|
||||
export const formatCurrency = (amount, currency = "INR", locale = "en-US") => {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: "currency",
|
||||
notation: "compact", // standard or compact
|
||||
notation: "compact",
|
||||
compactDisplay: "short",
|
||||
currency: currency,
|
||||
minimumFractionDigits: 0,
|
||||
|
||||
@ -160,10 +160,10 @@ export const PROJECT_STATUS = [
|
||||
export const DEFAULT_CURRENCY = "78e96e4a-7ce0-4164-ae3a-c833ad45ec2c";
|
||||
|
||||
export const EXPENSE_STATUS = {
|
||||
daft:"297e0d8f-f668-41b5-bfea-e03b354251c8",
|
||||
review_pending:"6537018f-f4e9-4cb3-a210-6c3b2da999d7",
|
||||
payment_pending:"f18c5cfd-7815-4341-8da2-2c2d65778e27",
|
||||
approve_pending:"4068007f-c92f-4f37-a907-bc15fe57d4d8",
|
||||
daft: "297e0d8f-f668-41b5-bfea-e03b354251c8",
|
||||
review_pending: "6537018f-f4e9-4cb3-a210-6c3b2da999d7",
|
||||
payment_pending: "f18c5cfd-7815-4341-8da2-2c2d65778e27",
|
||||
approve_pending: "4068007f-c92f-4f37-a907-bc15fe57d4d8",
|
||||
|
||||
}
|
||||
|
||||
@ -178,4 +178,32 @@ export const ALLOW_PROJECTSTATUS_ID = [
|
||||
|
||||
export const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
export const FREQUENCY_FOR_RECURRING = {
|
||||
0: "Monthly",
|
||||
1: "Quarterly",
|
||||
2: "Half-Yearly",
|
||||
3: "Yearly",
|
||||
4: "Daily",
|
||||
5: "Weekly"
|
||||
};
|
||||
|
||||
export const PAYEE_RECURRING_EXPENSE = [
|
||||
{
|
||||
id: "da462422-13b2-45cc-a175-910a225f6fc8",
|
||||
label: "Active",
|
||||
},
|
||||
{
|
||||
id: "306856fb-5655-42eb-bf8b-808bb5e84725",
|
||||
label: "Completed",
|
||||
},
|
||||
{
|
||||
id: "3ec864d2-8bf5-42fb-ba70-5090301dd816",
|
||||
label: "De-Activited",
|
||||
},
|
||||
{
|
||||
id: "8bfc9346-e092-4a80-acbf-515ae1ef6868",
|
||||
label: "Paused",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user