Merge branch 'Recurring_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into Recurring_Expense
This commit is contained in:
commit
84ed1984d7
@ -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>
|
||||
)
|
||||
|
||||
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: "",
|
||||
@ -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(),
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
181
src/components/common/MultiEmployeeSearchInput.jsx
Normal file
181
src/components/common/MultiEmployeeSearchInput.jsx
Normal 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;
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user