Merge branch 'Recurring_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into Recurring_Expense

This commit is contained in:
pramod.mahajan 2025-11-05 23:13:07 +05:30
commit 84ed1984d7
9 changed files with 760 additions and 190 deletions

View File

@ -7,25 +7,26 @@ 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 { useCreateRecurringExpense, usePayee, useRecurringExpenseDetail, useUpdateRecurringExpense } from '../../hooks/useExpense';
import InputSuggestions from '../common/InputSuggestion';
import MultiEmployeeSearchInput from '../common/MultiEmployeeSearchInput';
function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
const data = {}
const { projectNames, loading: projectLoading, error, isError: isProjectError,
} = useProjectName();
const { data: currencyData, isLoading: currencyLoading, isError: currencyError } = useCurrencies();
const {
ExpenseCategories,
loading: ExpenseLoading,
error: ExpenseError,
} = useExpenseCategory();
data,
isLoading,
isError,
error: requestError,
} = useRecurringExpenseDetail(requestToEdit);
//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 schema = PaymentRecurringExpense();
const { register, control, watch, handleSubmit, setValue, reset, formState: { errors }, } = useForm({
resolver: zodResolver(schema),
defaultValues: defaultRecurringExpense,
@ -69,7 +70,6 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
// console.log("Veer",data)
const onSubmit = (fromdata) => {
let payload = {
...fromdata,
// strikeDate: localToUtc(fromdata.strikeDate),
@ -144,7 +144,6 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
</small>
)}
</div>
</div>
{/* Title and Is Variable */}
@ -158,6 +157,7 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
id="title"
className="form-control form-control-sm"
{...register("title")}
placeholder="Enter title"
/>
{errors.title && (
<small className="danger-text">
@ -166,7 +166,7 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
)}
</div>
<div className="col-md-6">
{/* <div className="col-md-6">
<Label htmlFor="isVariable" className="form-label" required>
Is Variable
</Label>
@ -183,10 +183,53 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
{errors.isVariable && (
<small className="danger-text">{errors.isVariable.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">
@ -199,7 +242,6 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
minDate={new Date()}
className='w-100'
/>
{errors.strikeDate && (
<small className="danger-text">
{errors.strikeDate.message}
@ -219,6 +261,7 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
step="0.01"
inputMode="decimal"
{...register("amount", { valueAsNumber: true })}
placeholder="Enter amount"
/>
{errors.amount && (
<small className="danger-text">{errors.amount.message}</small>
@ -232,22 +275,17 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
<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")}
<InputSuggestions
organizationList={Payees}
value={watch("payee") || ""}
onChange={(val) =>
setValue("payee", val, { shouldValidate: true })
}
error={errors.payee?.message}
placeholder="Select or enter 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
@ -273,26 +311,28 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
<small className="danger-text">{errors.currencyId.message}</small>
)}
</div>
</div>
{/* Notify To and Status Id */}
{/* Frequency 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 htmlFor="frequency" className="form-label" required>
Frequency
</Label>
<input
type="text"
id="notifyTo"
className="form-control form-control-sm"
{...register("notifyTo")}
/>
{errors.notifyTo && (
<small className="danger-text">
{errors.notifyTo.message}
</small>
<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>
@ -323,7 +363,6 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
<small className="danger-text">{errors.statusId.message}</small>
)}
</div>
</div>
{/* Payment Buffer Days and Number of Iteration */}
@ -340,6 +379,7 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
min="0"
step="1"
{...register("paymentBufferDays", { valueAsNumber: true })}
placeholder="Enter payment buffer days"
/>
{errors.paymentBufferDays && (
<small className="danger-text">{errors.paymentBufferDays.message}</small>
@ -356,6 +396,7 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
min="1"
step="1"
{...register("numberOfIteration", { valueAsNumber: true })}
placeholder="Enter number of iterations"
/>
{errors.numberOfIteration && (
<small className="danger-text">{errors.numberOfIteration.message}</small>
@ -363,6 +404,43 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
</div>
</div>
{/* Notify */}
{/* <div className="row my-2 text-start">
<div className="col-md-6">
<Label htmlFor="notifyTo" className="form-label" required>
Notify Employees
</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> */}
<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">
@ -383,28 +461,6 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
</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"
@ -420,8 +476,6 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
Submit
</button>
</div>
</form>
</div>
)

View 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;

View File

@ -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: "",
@ -129,30 +87,26 @@ export const defaultRecurringExpense = {
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(),
});

View File

@ -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,

View File

@ -0,0 +1,181 @@
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;

View File

@ -407,4 +407,53 @@ 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()=>{
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
})
}

View File

@ -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 && (

View File

@ -31,7 +31,8 @@ const ExpenseRepository = {
//#region Recurring Expense
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

View File

@ -159,10 +159,10 @@ export const PROJECT_STATUS = [
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",
}
@ -177,4 +177,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",
},
];