added expenses Reimburse
This commit is contained in:
parent
b30940c2aa
commit
35f221038d
@ -73,21 +73,6 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2 text-start px-2">
|
||||
<label htmlFor="groupBySelect" className="form-label">Group By :</label>
|
||||
<select
|
||||
id="groupBySelect"
|
||||
className="form-select form-select-sm"
|
||||
value={selectedGroup?.id || ""}
|
||||
onChange={handleGroupChange}
|
||||
>
|
||||
{groupByList.map((group) => (
|
||||
<option key={group.id} value={group.id}>
|
||||
{group.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
||||
@ -174,6 +159,21 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2 text-start ">
|
||||
<label htmlFor="groupBySelect" className="form-label">Group By :</label>
|
||||
<select
|
||||
id="groupBySelect"
|
||||
className="form-select form-select-sm"
|
||||
value={selectedGroup?.id || ""}
|
||||
onChange={handleGroupChange}
|
||||
>
|
||||
{groupByList.map((group) => (
|
||||
<option key={group.id} value={group.id}>
|
||||
{group.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-end py-3 gap-2">
|
||||
<button
|
||||
@ -189,6 +189,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -25,7 +25,6 @@ export const ExpenseSchema = (expenseTypes) => {
|
||||
const selected = new Date(val);
|
||||
const today = new Date();
|
||||
|
||||
// Set both to midnight to avoid time-related issues
|
||||
selected.setHours(0, 0, 0, 0);
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
@ -65,9 +64,7 @@ export const ExpenseSchema = (expenseTypes) => {
|
||||
})
|
||||
)
|
||||
.nonempty({ message: "At least one file attachment is required" }),
|
||||
reimburseTransactionId: z.string().optional(),
|
||||
reimburseDate: z.string().optional(),
|
||||
reimburseById: z.string().optional(),
|
||||
|
||||
|
||||
})
|
||||
.refine(
|
||||
@ -132,10 +129,53 @@ export const defaultExpense = {
|
||||
billAttachments: [],
|
||||
};
|
||||
|
||||
export const ActionSchema = z.object({
|
||||
comment: z.string().min(1, { message: "Please leave comment" }),
|
||||
selectedStatus: z.string().min(1, { message: "Please select a status" }),
|
||||
});
|
||||
|
||||
export const ExpenseActionScheam = (isReimbursement = false) => {
|
||||
return z
|
||||
.object({
|
||||
comment: z.string().min(1, { message: "Please leave comment" }),
|
||||
statusId: z.string().min(1, { message: "Please select a status" }),
|
||||
reimburseTransactionId: z.string().nullable().optional(),
|
||||
reimburseDate: z.string().nullable().optional(),
|
||||
reimburseById: z.string().nullable().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (isReimbursement) {
|
||||
if (!data.reimburseTransactionId?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["reimburseTransactionId"],
|
||||
message: "Reimburse Transaction ID is required",
|
||||
});
|
||||
}
|
||||
if (!data.reimburseDate) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["reimburseDate"],
|
||||
message: "Reimburse Date is required",
|
||||
});
|
||||
}
|
||||
if (!data.reimburseById) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["reimburseById"],
|
||||
message: "Reimburse By is required",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const defaultActionValues = {
|
||||
comment: "",
|
||||
statusId: "",
|
||||
|
||||
reimburseTransactionId: null,
|
||||
reimburseDate: null,
|
||||
reimburseById: null,
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const SearchSchema = z.object({
|
||||
projectIds: z.array(z.string()).optional(),
|
||||
|
@ -206,7 +206,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="row my-2">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="projectId" className="form-label">
|
||||
<label className="form-label">
|
||||
Select Project
|
||||
</label>
|
||||
<select
|
||||
@ -230,7 +230,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<label for="expensesTypeId" className="form-label ">
|
||||
<label htmlFor="expensesTypeId" className="form-label ">
|
||||
Expense Type
|
||||
</label>
|
||||
<select
|
||||
@ -261,7 +261,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
|
||||
<div className="row my-2">
|
||||
<div className="col-md-6">
|
||||
<label for="paymentModeId" className="form-label ">
|
||||
<label htmlFor="paymentModeId" className="form-label ">
|
||||
Payment Mode
|
||||
</label>
|
||||
<select
|
||||
@ -290,7 +290,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<label for="paidById" className="form-label ">
|
||||
<label htmlFor="paidById" className="form-label ">
|
||||
Paid By
|
||||
</label>
|
||||
<select
|
||||
@ -320,7 +320,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
|
||||
<div className="row my-2">
|
||||
<div className="col-md-6">
|
||||
<label for="transactionDate" className="form-label ">
|
||||
<label htmlFor="transactionDate" className="form-label ">
|
||||
Transaction Date
|
||||
</label>
|
||||
{/* <input
|
||||
@ -343,7 +343,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<label for="amount" className="form-label ">
|
||||
<label htmlFor="amount" className="form-label ">
|
||||
Amount
|
||||
</label>
|
||||
<input
|
||||
@ -361,9 +361,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-2">
|
||||
<div className="row my-2">
|
||||
<div className="col-md-6">
|
||||
<label for="supplerName" className="form-label ">
|
||||
<label htmlFor="supplerName" className="form-label ">
|
||||
Supplier Name
|
||||
</label>
|
||||
<input
|
||||
@ -380,7 +380,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<label for="location" className="form-label ">
|
||||
<label htmlFor="location" className="form-label ">
|
||||
Location
|
||||
</label>
|
||||
<input
|
||||
@ -396,7 +396,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
<div className="row my-2">
|
||||
<div className="col-md-6">
|
||||
<label for="statusId" className="form-label ">
|
||||
<label htmlFor="statusId" className="form-label ">
|
||||
TransactionId
|
||||
</label>
|
||||
<input
|
||||
@ -415,7 +415,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
|
||||
{ExpenseType?.noOfPersonsRequired && (
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="noOfPersons" >
|
||||
<label >
|
||||
No. of Persons
|
||||
</label>
|
||||
<input
|
||||
@ -436,7 +436,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
|
||||
<div className="row my-2">
|
||||
<div className="col-md-12" >
|
||||
<label for="description">Description</label>
|
||||
<label htmlFor="description">Description</label>
|
||||
<textarea
|
||||
id="description"
|
||||
className="form-control form-control-sm"
|
||||
|
@ -7,35 +7,43 @@ import {
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ActionSchema } from "./ExpenseSchema";
|
||||
import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
|
||||
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
|
||||
import { getColorNameFromHex } from "../../utils/appUtils";
|
||||
import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { REVIEW_EXPENSE } from "../../utils/constants";
|
||||
import { PROCESS_EXPENSE, REVIEW_EXPENSE } from "../../utils/constants";
|
||||
import { useProfile } from "../../hooks/useProfile";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Avatar from "../common/Avatar";
|
||||
import Error from "../common/Error";
|
||||
import DatePicker from "../common/DatePicker";
|
||||
import { useEmployeeRoles, useEmployeesName } from "../../hooks/useEmployees";
|
||||
import EmployeeSearchInput from "../common/EmployeeSearchInput";
|
||||
import { z } from "zod";
|
||||
import moment from "moment";
|
||||
|
||||
const ViewExpense = ({ ExpenseId }) => {
|
||||
const { data, isLoading, isError, error } = useExpense(ExpenseId);
|
||||
const [IsPaymentProcess, setIsPaymentProcess] = useState(false);
|
||||
const [clickedStatusId, setClickedStatusId] = useState(null);
|
||||
|
||||
const IsReview = useHasUserPermission(REVIEW_EXPENSE);
|
||||
const [imageLoaded, setImageLoaded] = useState({});
|
||||
const { setDocumentView } = useExpenseContext();
|
||||
const ActionSchema = ExpenseActionScheam(IsPaymentProcess) ?? z.object({});
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
reset,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(ActionSchema),
|
||||
defaultValues: {
|
||||
comment: "",
|
||||
selectedStatus: "",
|
||||
},
|
||||
defaultValues: defaultActionValues,
|
||||
});
|
||||
|
||||
const userPermissions = useSelector(
|
||||
@ -51,171 +59,208 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
: [];
|
||||
|
||||
if (permissionIds.length === 0) return true;
|
||||
|
||||
if (permissionIds.includes(PROCESS_EXPENSE)) {
|
||||
setIsPaymentProcess(true);
|
||||
}
|
||||
return permissionIds.some((id) => userPermissions.includes(id));
|
||||
});
|
||||
}, [data, userPermissions]);
|
||||
|
||||
const { mutate: MakeAction } = useActionOnExpense(() => reset());
|
||||
const { mutate: MakeAction,isPending } = useActionOnExpense(() => {
|
||||
setClickedStatusId(null);
|
||||
reset()});
|
||||
|
||||
const onSubmit = (formData) => {
|
||||
const Payload = {
|
||||
...formData,
|
||||
reimburseDate:moment.utc(formData.reimburseDate, "DD-MM-YYYY").toISOString(),
|
||||
expenseId: ExpenseId,
|
||||
statusId: formData.selectedStatus,
|
||||
comment: formData.comment,
|
||||
};
|
||||
|
||||
MakeAction(Payload);
|
||||
};
|
||||
|
||||
if (isLoading) return <ExpenseDetailsSkeleton />;
|
||||
if (isError) return <Error error={error} />;
|
||||
const handleImageLoad = (id) => {
|
||||
setImageLoaded((prev) => ({ ...prev, [id]: true }));
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="container px-3" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="row mb-3">
|
||||
<div className="row mb-3">
|
||||
<div className="col-12 mb-3">
|
||||
<h5 className="fw-semibold">Expense Details</h5>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
{/* Each info block below now has h-100 to stretch */}
|
||||
<div className="col-12 col-md-6 mb-3 h-100">
|
||||
{/* Row 1 */}
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Transaction Date :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{formatUTCToLocalTime(data.transactionDate)}
|
||||
{formatUTCToLocalTime(data?.transactionDate)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-6 mb-3 h-100">
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Expense Type :
|
||||
</label>
|
||||
<div className="text-muted">{data.expensesType.name}</div>
|
||||
<div className="text-muted">{data?.expensesType?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-4 mb-3 h-100">
|
||||
{/* Row 2 */}
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Supplier :
|
||||
</label>
|
||||
<div className="text-muted">{data.supplerName}</div>
|
||||
<div className="text-muted">{data?.supplerName}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-4 mb-3 h-100">
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">Amount :</label>
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Amount :
|
||||
</label>
|
||||
<div className="text-muted">₹ {data.amount}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-4 mb-3 h-100">
|
||||
{/* Row 3 */}
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
<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 className="text-muted">{data?.paymentMode?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row align-items-center mb-3">
|
||||
<div className="col-6">
|
||||
<div className="d-flex align-items-center">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Paid By:
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{data.paidBy.firstName} {data.paidBy.lastName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-6">
|
||||
<div className="d-flex align-items-center">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Status:
|
||||
</label>
|
||||
<span
|
||||
className={`badge bg-label-${
|
||||
getColorNameFromHex(data?.status?.color) || "secondary"
|
||||
}`}
|
||||
>
|
||||
{data?.status?.displayName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-4 mb-3 h-100">
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Paid By :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{data?.paidBy?.firstName} {data?.paidBy?.lastName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 4 */}
|
||||
<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?.status?.color) || "secondary"
|
||||
}`}
|
||||
>
|
||||
{data?.status?.displayName}
|
||||
</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"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Pre-Approved :
|
||||
</label>
|
||||
<div className="text-muted">{data.preApproved ? "Yes" : "No"}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-6 mb-3 h-100">
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Project :
|
||||
</label>
|
||||
<div className="text-muted text-start">{data?.project?.name}</div>
|
||||
<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"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Created At :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{formatUTCToLocalTime(data?.createdAt, true)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row align-items-center mb-3">
|
||||
<div className="col-12 col-md-auto">
|
||||
{/* Row 6 */}
|
||||
{data.createdBy && (
|
||||
<div className="col-md-6 mb-3 text-start">
|
||||
<div className="d-flex align-items-center">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Created At:
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Created By :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{formatUTCToLocalTime(data?.createdAt, true)}
|
||||
<div className="d-flex align-items-center gap-1">
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0"
|
||||
firstName={data.createdBy?.firstName}
|
||||
lastName={data.createdBy?.lastName}
|
||||
/>
|
||||
<span className="text-muted">
|
||||
{`${data.createdBy?.firstName ?? ""} ${
|
||||
data.createdBy?.lastName ?? ""
|
||||
}`.trim() || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.createdBy && (
|
||||
<div className="col-12 col-md">
|
||||
<div className="d-flex align-items-center">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Created By:
|
||||
</label>
|
||||
<div className="d-flex align-items-center gap-1">
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0"
|
||||
firstName={data.createdBy?.firstName}
|
||||
lastName={data.createdBy?.lastName}
|
||||
/>
|
||||
<span className="text-muted">
|
||||
{`${data.createdBy?.firstName ?? ""} ${
|
||||
data.createdBy?.lastName ?? ""
|
||||
}`.trim() || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.reviewedBy && (
|
||||
<div className="col-12 col-md-auto mb-3 h-100">
|
||||
<div className="col-md-6 mb-3 text-start">
|
||||
<div className="d-flex align-items-center">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Reviewed By :
|
||||
</label>
|
||||
|
||||
<div className="d-flex align-items-center">
|
||||
<Avatar
|
||||
size="xs"
|
||||
@ -232,13 +277,16 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data.approvedBy && (
|
||||
<div className="col-12 col-md-auto mb-3 h-100">
|
||||
<div className="d-flex align-items-center">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Approved By :
|
||||
</label>
|
||||
|
||||
{data.approvedBy && (
|
||||
<div className="col-md-6 mb-3 text-start">
|
||||
<div className="d-flex align-items-center">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Approved By :{" "}
|
||||
</label>
|
||||
<div className="d-flex align-items-center">
|
||||
<Avatar
|
||||
size="xs"
|
||||
@ -255,14 +303,72 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data.processedBy && (
|
||||
<div className="col-md-6 mb-3 text-start">
|
||||
<div className="d-flex align-items-center">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Processed By :{" "}
|
||||
</label>
|
||||
<div className="d-flex align-items-center">
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0"
|
||||
firstName={data.processedBy?.firstName}
|
||||
lastName={data.processedBy?.lastName}
|
||||
/>
|
||||
<span className="text-muted">
|
||||
{`${data.processedBy?.firstName ?? ""} ${
|
||||
data.processedBy?.lastName ?? ""
|
||||
}`.trim() || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{data.expensesReimburse && (<div className="row text-start">
|
||||
<div className="col-md-6 mb-3">
|
||||
<strong>Transaction ID :</strong> {data.expensesReimburse.reimburseTransactionId || "N/A"}
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<strong>Reimburse Date :</strong>{" "}
|
||||
{ moment(data.expensesReimburse.reimburseDate).format("DD-MM-YYYY") }
|
||||
</div>
|
||||
|
||||
{data.expensesReimburse && (
|
||||
<>
|
||||
<div className="col-md-6 mb-3 d-flex align-items-center">
|
||||
<strong className="me-2">Reimburse By :</strong>
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0 me-1"
|
||||
firstName={data?.expensesReimburse?.reimburseBy?.firstName}
|
||||
lastName={data?.expensesReimburse?.reimburseBy?.lastName}
|
||||
/>
|
||||
<span className="text-muted">
|
||||
{`${data?.expensesReimburse?.reimburseBy?.firstName} ${data?.expensesReimburse?.reimburseBy?.lastName}`.trim()}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="col-md-6 mb-3">
|
||||
<strong>Note :</strong> {data.expensesReimburse.reimburseNote}
|
||||
</div>
|
||||
|
||||
</div>)}
|
||||
|
||||
|
||||
|
||||
<div className="text-start">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">Description:</label>
|
||||
<label className="form-label me-2 mb-0 fw-semibold">Description :</label>
|
||||
<div className="text-muted">{data?.description}</div>
|
||||
</div>
|
||||
<div className="col-12 my-2 text-start">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">Attachment:</label>
|
||||
<label className="form-label me-2 mb-0 fw-semibold">Attachment :</label>
|
||||
|
||||
{data?.documents?.map((doc) => {
|
||||
const getIconByType = (type) => {
|
||||
@ -284,7 +390,7 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
return (
|
||||
<div
|
||||
className="list-group-item list-group-item-action d-flex align-items-center"
|
||||
key={doc.id}
|
||||
key={doc.documentId}
|
||||
>
|
||||
<div
|
||||
className="rounded me-1 d-flex align-items-center justify-content-center cursor-pointer"
|
||||
@ -325,43 +431,79 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
<hr className="divider my-1" />
|
||||
|
||||
{Array.isArray(data?.nextStatus) && data.nextStatus.length > 0 && (
|
||||
<div className="col-12 mb-3 text-start">
|
||||
{nextStatusWithPermission.length > 0 && (
|
||||
<>
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Comment:
|
||||
</label>
|
||||
<textarea
|
||||
className="form-control form-control-sm"
|
||||
{...register("comment")}
|
||||
rows="2"
|
||||
/>
|
||||
{errors.comment && (
|
||||
<small className="danger-text">{errors.comment.message}</small>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<input type="hidden" {...register("selectedStatus")} />
|
||||
|
||||
{nextStatusWithPermission?.length > 0 && (
|
||||
<div className="text-center flex-wrap gap-2 my-2">
|
||||
{nextStatusWithPermission?.map((status, index) => (
|
||||
<button
|
||||
key={status.id || index}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setValue("selectedStatus", status.id);
|
||||
handleSubmit(onSubmit)();
|
||||
}}
|
||||
className="btn btn-primary btn-sm cursor-pointer mx-2 border-0"
|
||||
>
|
||||
{status.displayName || status.name}
|
||||
</button>
|
||||
))}
|
||||
<>
|
||||
{IsPaymentProcess && (
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6 text-start">
|
||||
<label className="form-label">Transaction Id </label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("reimburseTransactionId")}
|
||||
/>
|
||||
{errors.reimburseTransactionId && (
|
||||
<small className="danger-text">
|
||||
{errors.reimburseTransactionId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 text-start">
|
||||
<label className="form-label">Transaction Date </label>
|
||||
<DatePicker name="reimburseDate" control={control} />
|
||||
{errors.reimburseDate && (
|
||||
<small className="danger-text">
|
||||
{errors.reimburseDate.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 text-start">
|
||||
<label className="form-label">Employeee </label>
|
||||
<EmployeeSearchInput
|
||||
control={control}
|
||||
name="reimburseById"
|
||||
projectId={data?.project?.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 mb-3 text-start">
|
||||
{nextStatusWithPermission.length > 0 && (
|
||||
<>
|
||||
<label className="form-label me-2 mb-0 ">Comment:</label>
|
||||
<textarea
|
||||
className="form-control form-control-sm"
|
||||
{...register("comment")}
|
||||
rows="2"
|
||||
/>
|
||||
{errors.comment && (
|
||||
<small className="danger-text">
|
||||
{errors.comment.message}
|
||||
</small>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{nextStatusWithPermission?.length > 0 && (
|
||||
<div className="text-center flex-wrap gap-2 my-2">
|
||||
{nextStatusWithPermission?.map((status, index) => (
|
||||
<button
|
||||
key={status.id || index}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setClickedStatusId(status.id);
|
||||
setValue("statusId", status.id);
|
||||
handleSubmit(onSubmit)();
|
||||
}}
|
||||
disabled={isPending}
|
||||
className="btn btn-primary btn-sm cursor-pointer mx-2 border-0"
|
||||
>
|
||||
{(isPending && clickedStatusId === status.id) ? "Please Wait..." : (status.displayName || status.name)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
|
85
src/components/common/EmployeeSearchInput.jsx
Normal file
85
src/components/common/EmployeeSearchInput.jsx
Normal file
@ -0,0 +1,85 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEmployeesName } from "../../hooks/useEmployees";
|
||||
import { useDebounce } from "../../utils/appUtils";
|
||||
import { useController } from "react-hook-form";
|
||||
|
||||
|
||||
|
||||
|
||||
const EmployeeSearchInput = ({ control, name, projectId }) => {
|
||||
const {
|
||||
field: { onChange, value, ref },
|
||||
fieldState: { error },
|
||||
} = useController({ name, control });
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const debouncedSearch = useDebounce(search, 500);
|
||||
|
||||
const {
|
||||
data: employees,
|
||||
isLoading,
|
||||
} = useEmployeesName(projectId, debouncedSearch);
|
||||
|
||||
useEffect(() => {
|
||||
if (value && !search) {
|
||||
const found = employees?.data?.find((emp) => emp.id === value);
|
||||
if (found) setSearch(found.firstName + " " + found.lastName);
|
||||
}
|
||||
}, [value, employees]);
|
||||
|
||||
const handleSelect = (employee) => {
|
||||
onChange(employee.id);
|
||||
setSearch(employee.firstName + " " + employee.lastName);
|
||||
setShowDropdown(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="position-relative">
|
||||
<input
|
||||
type="text"
|
||||
ref={ref}
|
||||
className={`form-control form-control-sm`}
|
||||
placeholder="Search employee..."
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setShowDropdown(true);
|
||||
onChange(""); // Clear previous selection
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (search) setShowDropdown(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
{showDropdown && (employees?.data?.length > 0 || isLoading) && (
|
||||
<ul
|
||||
className="list-group position-absolute bg-white w-100 shadow z-3 rounded-none"
|
||||
style={{ maxHeight: 200, overflowY: "auto" }}
|
||||
>
|
||||
{isLoading ? (
|
||||
<li className="list-group-item">
|
||||
<a>Searching...</a>
|
||||
|
||||
</li>
|
||||
) : (
|
||||
employees?.data?.map((emp) => (
|
||||
<li
|
||||
key={emp.id}
|
||||
className="list-group-item list-group-item-action"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSelect(emp)}
|
||||
>
|
||||
{emp.firstName} {emp.lastName}
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{error && <small className="danger-text">{error.message}</small>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeeSearchInput;
|
20
src/components/common/Error.jsx
Normal file
20
src/components/common/Error.jsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
|
||||
const Error = ({error,close}) => {
|
||||
console.log(error)
|
||||
return (
|
||||
<div className="container text-center py-5">
|
||||
<h1 className="display-4 fw-bold text-danger">{error.statusCode || error?.response?.status
|
||||
}</h1>
|
||||
<h2 className="mb-3">Internal Server Error</h2>
|
||||
<p className="lead">
|
||||
{error.message}
|
||||
</p>
|
||||
<a href="/" className="btn btn-primary btn-sm mt-3" onClick={()=>close()}>
|
||||
Go to Home
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Error
|
@ -3,27 +3,22 @@ import { cacheData, getCachedData } from "../slices/apiDataManager";
|
||||
import { RolesRepository } from "../repositories/MastersRepository";
|
||||
import EmployeeRepository from "../repositories/EmployeeRepository";
|
||||
import ProjectRepository from "../repositories/ProjectRepository";
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import showToast from "../services/toastService";
|
||||
import {useSelector} from "react-redux";
|
||||
import {store} from "../store/store";
|
||||
import {queryClient} from "../layouts/AuthLayout";
|
||||
|
||||
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
import { store } from "../store/store";
|
||||
import { queryClient } from "../layouts/AuthLayout";
|
||||
|
||||
// Query ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
export const useAllEmployees = ( showInactive ) =>
|
||||
{
|
||||
export const useAllEmployees = (showInactive) => {
|
||||
const {
|
||||
data = [],
|
||||
isLoading,
|
||||
error,
|
||||
refetch, // optional if you want recall functionality
|
||||
} = useQuery({
|
||||
queryKey: ['allEmployee', showInactive],
|
||||
queryKey: ["allEmployee", showInactive],
|
||||
queryFn: async () => {
|
||||
const res = await EmployeeRepository.getAllEmployeeList(showInactive);
|
||||
return res.data;
|
||||
@ -34,14 +29,12 @@ export const useAllEmployees = ( showInactive ) =>
|
||||
employeesList: data,
|
||||
loading: isLoading,
|
||||
error,
|
||||
recallEmployeeData: refetch,
|
||||
recallEmployeeData: refetch,
|
||||
};
|
||||
};
|
||||
|
||||
// ManageBucket.jsx
|
||||
export const useEmployees = ( selectedProject ) =>
|
||||
{
|
||||
|
||||
export const useEmployees = (selectedProject) => {
|
||||
const {
|
||||
data = [],
|
||||
isLoading,
|
||||
@ -50,7 +43,9 @@ export const useEmployees = ( selectedProject ) =>
|
||||
} = useQuery({
|
||||
queryKey: ["employeeListByProject", selectedProject],
|
||||
queryFn: async () => {
|
||||
const res = await EmployeeRepository.getEmployeeListByproject(selectedProject);
|
||||
const res = await EmployeeRepository.getEmployeeListByproject(
|
||||
selectedProject
|
||||
);
|
||||
return res.data || res;
|
||||
},
|
||||
enabled: !!selectedProject,
|
||||
@ -72,12 +67,12 @@ export const useEmployeeRoles = (employeeId) => {
|
||||
isLoading: loading,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['employeeRoles', employeeId],
|
||||
queryKey: ["employeeRoles", employeeId],
|
||||
queryFn: async () => {
|
||||
const res = await RolesRepository.getEmployeeRoles(employeeId);
|
||||
return res.data;
|
||||
},
|
||||
enabled: !!employeeId,
|
||||
enabled: !!employeeId,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -95,12 +90,12 @@ export const useEmployeesByProject = (projectId) => {
|
||||
error,
|
||||
refetch: recallProjectEmplloyee,
|
||||
} = useQuery({
|
||||
queryKey: ['projectEmployees', projectId],
|
||||
queryKey: ["projectEmployees", projectId],
|
||||
queryFn: async () => {
|
||||
const res = await ProjectRepository.getEmployeesByProject(projectId);
|
||||
return res.data;
|
||||
},
|
||||
enabled: !!projectId,
|
||||
enabled: !!projectId,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -112,16 +107,17 @@ export const useEmployeesByProject = (projectId) => {
|
||||
};
|
||||
|
||||
// EmployeeList.jsx
|
||||
export const useEmployeesAllOrByProjectId = (showAllEmployees ,projectId,
|
||||
showInactive) => {
|
||||
|
||||
|
||||
const queryKey = showAllEmployees
|
||||
? ['allEmployees', showInactive]
|
||||
: ['projectEmployees', projectId, showInactive];
|
||||
export const useEmployeesAllOrByProjectId = (
|
||||
showAllEmployees,
|
||||
projectId,
|
||||
showInactive
|
||||
) => {
|
||||
const queryKey = showAllEmployees
|
||||
? ["allEmployees", showInactive]
|
||||
: ["projectEmployees", projectId, showInactive];
|
||||
|
||||
const queryFn = async () => {
|
||||
if (showAllEmployees) {
|
||||
if (showAllEmployees) {
|
||||
const res = await EmployeeRepository.getAllEmployeeList(showInactive);
|
||||
return res.data;
|
||||
} else {
|
||||
@ -139,7 +135,8 @@ export const useEmployeesAllOrByProjectId = (showAllEmployees ,projectId,
|
||||
} = useQuery({
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled:typeof showInactive === "boolean" && (showAllEmployees || !!projectId),
|
||||
enabled:
|
||||
typeof showInactive === "boolean" && (showAllEmployees || !!projectId),
|
||||
});
|
||||
|
||||
return {
|
||||
@ -151,69 +148,85 @@ export const useEmployeesAllOrByProjectId = (showAllEmployees ,projectId,
|
||||
};
|
||||
|
||||
// ManageEmployee.jsx
|
||||
export const useEmployeeProfile = ( employeeId ) =>
|
||||
{
|
||||
export const useEmployeeProfile = (employeeId) => {
|
||||
const isEnabled = !!employeeId;
|
||||
const {
|
||||
data = null,
|
||||
isLoading: loading,
|
||||
error,
|
||||
refetch
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ['employeeProfile', employeeId],
|
||||
queryKey: ["employeeProfile", employeeId],
|
||||
queryFn: async () => {
|
||||
if (!employeeId) return null;
|
||||
const res = await EmployeeRepository.getEmployeeProfile(employeeId);
|
||||
return res.data;
|
||||
},
|
||||
enabled: isEnabled,
|
||||
enabled: isEnabled,
|
||||
});
|
||||
|
||||
return {
|
||||
employee: data,
|
||||
loading,
|
||||
error,
|
||||
refetch
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export const useEmployeesName = (projectId, search) => {
|
||||
return useQuery({
|
||||
queryKey: ["employees", projectId, search],
|
||||
queryFn: async() => await EmployeeRepository.getEmployeeName(projectId, search),
|
||||
|
||||
staleTime: 5 * 60 * 1000, // Optional: cache for 5 minutes
|
||||
});
|
||||
};
|
||||
|
||||
// Mutation------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
export const useUpdateEmployee = () =>
|
||||
{
|
||||
const selectedProject = useSelector((store)=>store.localVariables.projectId)
|
||||
const queryClient = useQueryClient();
|
||||
export const useUpdateEmployee = () => {
|
||||
const selectedProject = useSelector(
|
||||
(store) => store.localVariables.projectId
|
||||
);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (employeeData) => EmployeeRepository.manageEmployee(employeeData),
|
||||
mutationFn: (employeeData) =>
|
||||
EmployeeRepository.manageEmployee(employeeData),
|
||||
onSuccess: (_, variables) => {
|
||||
const id = variables.id || variables.employeeId;
|
||||
const isAllEmployee = variables.IsAllEmployee;
|
||||
|
||||
|
||||
// Cache invalidation
|
||||
queryClient.invalidateQueries( {queryKey:[ 'allEmployees'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["allEmployees"] });
|
||||
// queryClient.invalidateQueries(['employeeProfile', id]);
|
||||
queryClient.invalidateQueries( {queryKey: [ 'projectEmployees' ]} );
|
||||
queryClient.removeQueries( {queryKey: [ "empListByProjectAllocated" ]} );
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["projectEmployees"] });
|
||||
queryClient.removeQueries({ queryKey: ["empListByProjectAllocated"] });
|
||||
|
||||
// queryClient.invalidateQueries( {queryKey:[ 'employeeListByProject']} );
|
||||
showToast( `Employee ${ id ? 'updated' : 'created' } successfully`, 'success' );
|
||||
showToast(
|
||||
`Employee ${id ? "updated" : "created"} successfully`,
|
||||
"success"
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
const msg = error?.response?.data?.message || error.message || 'Something went wrong';
|
||||
showToast(msg, 'error');
|
||||
const msg =
|
||||
error?.response?.data?.message ||
|
||||
error.message ||
|
||||
"Something went wrong";
|
||||
showToast(msg, "error");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const useSuspendEmployee = ({ setIsDeleteModalOpen, setemployeeLodaing }) => {
|
||||
export const useSuspendEmployee = ({
|
||||
setIsDeleteModalOpen,
|
||||
setemployeeLodaing,
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
const selectedProject = useSelector((store)=>store.localVariables.projectId)
|
||||
const selectedProject = useSelector(
|
||||
(store) => store.localVariables.projectId
|
||||
);
|
||||
return useMutation({
|
||||
mutationFn: (id) => {
|
||||
setemployeeLodaing(true);
|
||||
@ -221,12 +234,12 @@ export const useSuspendEmployee = ({ setIsDeleteModalOpen, setemployeeLodaing })
|
||||
},
|
||||
|
||||
onSuccess: () => {
|
||||
|
||||
|
||||
// queryClient.invalidateQueries( ['allEmployee',false]);
|
||||
queryClient.invalidateQueries( {queryKey: [ 'projectEmployees' ]} );
|
||||
queryClient.invalidateQueries( {queryKey:[ 'employeeListByProject' ,selectedProject]} );
|
||||
showToast("Employee deleted successfully.", "success");
|
||||
queryClient.invalidateQueries({ queryKey: ["projectEmployees"] });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["employeeListByProject", selectedProject],
|
||||
});
|
||||
showToast("Employee deleted successfully.", "success");
|
||||
setIsDeleteModalOpen(false);
|
||||
},
|
||||
|
||||
@ -247,8 +260,11 @@ export const useSuspendEmployee = ({ setIsDeleteModalOpen, setemployeeLodaing })
|
||||
|
||||
// Manage Role
|
||||
|
||||
|
||||
export const useUpdateEmployeeRoles = ({ onClose, resetForm, onSuccessCallback } = {}) => {
|
||||
export const useUpdateEmployeeRoles = ({
|
||||
onClose,
|
||||
resetForm,
|
||||
onSuccessCallback,
|
||||
} = {}) => {
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationFn: (updates) => RolesRepository.createEmployeeRoles(updates),
|
||||
@ -259,12 +275,14 @@ export const useUpdateEmployeeRoles = ({ onClose, resetForm, onSuccessCallback }
|
||||
onClose?.();
|
||||
onSuccessCallback?.();
|
||||
|
||||
queryClient.invalidateQueries( {queryKey: [ "employeeRoles" ]} );
|
||||
queryClient.invalidateQueries( {queryKey: [ "profile" ]} );
|
||||
queryClient.invalidateQueries({ queryKey: ["employeeRoles"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
||||
},
|
||||
onError: (err) => {
|
||||
const message =
|
||||
err?.response?.data?.message || err?.message || "Error occurred while updating roles";
|
||||
err?.response?.data?.message ||
|
||||
err?.message ||
|
||||
"Error occurred while updating roles";
|
||||
showToast(message, "error");
|
||||
},
|
||||
});
|
||||
|
@ -20,10 +20,14 @@ const cleanFilter = (filter) => {
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
|
||||
export const useExpenseList = (pageSize, pageNumber, filter, searchString = '') => {
|
||||
export const useExpenseList = (
|
||||
pageSize,
|
||||
pageNumber,
|
||||
filter,
|
||||
searchString = ""
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: ["Expenses", pageNumber, pageSize, filter, searchString],
|
||||
queryKey: ["Expenses", pageNumber, pageSize, filter, searchString],
|
||||
queryFn: async () => {
|
||||
const cleanedFilter = cleanFilter(filter);
|
||||
const response = await ExpenseRepository.GetExpenseList(
|
||||
@ -38,8 +42,6 @@ export const useExpenseList = (pageSize, pageNumber, filter, searchString = '')
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const useExpense = (ExpenseId) => {
|
||||
return useQuery({
|
||||
queryKey: ["Expense", ExpenseId],
|
||||
@ -55,13 +57,10 @@ export const useExpenseFilter = () => {
|
||||
return useQuery({
|
||||
queryKey: ["ExpenseFilter"],
|
||||
queryFn: async () =>
|
||||
await ExpenseRepository.GetExpenseFilter().then(
|
||||
(res) => res.data
|
||||
),
|
||||
await ExpenseRepository.GetExpenseFilter().then((res) => res.data),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// ---------------------------Mutation---------------------------------------------
|
||||
|
||||
export const useCreateExpnse = (onSuccessCallBack) => {
|
||||
@ -143,9 +142,9 @@ export const useUpdateExpense = (onSuccessCallBack) => {
|
||||
|
||||
if (onSuccessCallBack) onSuccessCallBack();
|
||||
},
|
||||
onError:(error)=>{
|
||||
showToast( "Something went wrong.Please try again later.", "error");
|
||||
}
|
||||
onError: (error) => {
|
||||
showToast("Something went wrong.Please try again later.", "error");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -181,22 +180,24 @@ export const useActionOnExpense = (onSuccessCallBack) => {
|
||||
};
|
||||
}
|
||||
);
|
||||
queryClient.setQueriesData(
|
||||
{ queryKey: ["Expense", updatedExpense.id] },
|
||||
(oldData) => {
|
||||
return {
|
||||
...oldData,
|
||||
nextStatus: updatedExpense.nextStatus,
|
||||
status: updatedExpense.status,
|
||||
};
|
||||
}
|
||||
);
|
||||
// queryClient.setQueriesData(
|
||||
// { queryKey: ["Expense", updatedExpense.id] },
|
||||
// (oldData) => {
|
||||
// return {
|
||||
// ...oldData,
|
||||
// nextStatus: updatedExpense.nextStatus,
|
||||
// status: updatedExpense.status,
|
||||
// };
|
||||
// }
|
||||
// );
|
||||
queryClient.invalidateQueries({queryKey:["Expense",updatedExpense.id]})
|
||||
|
||||
if (onSuccessCallBack) onSuccessCallBack();
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(
|
||||
error.message || "Something went wrong.Please try again later.",
|
||||
error.response.data.message ||
|
||||
"Something went wrong.Please try again later.",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
@ -213,7 +214,8 @@ export const useDeleteExpense = () => {
|
||||
},
|
||||
onSuccess: (data, variables) => {
|
||||
queryClient.setQueryData(["Expenses"], (oldData) => {
|
||||
if (!oldData || !oldData.data) return queryClient.invalidateQueries({queryKey:["Expenses"]});
|
||||
if (!oldData || !oldData.data)
|
||||
return queryClient.invalidateQueries({ queryKey: ["Expenses"] });
|
||||
|
||||
const updatedList = oldData.data.filter(
|
||||
(expense) => expense.id !== variables.id
|
||||
@ -228,18 +230,18 @@ export const useDeleteExpense = () => {
|
||||
showToast(data.message || "Expense deleted successfully", "success");
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(error.message || error.response.message || "Something went wrong.Please try again later.", "error");
|
||||
showToast(
|
||||
error.message ||
|
||||
error.response.message ||
|
||||
"Something went wrong.Please try again later.",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
export const useHasAnyPermission = (permissionIdsInput) => {
|
||||
const permissions = useSelector(
|
||||
(state) => state?.profile?.permissions || []
|
||||
);
|
||||
const permissions = useSelector((state) => state?.profile?.permissions || []);
|
||||
|
||||
const permissionIds = Array.isArray(permissionIdsInput)
|
||||
? permissionIdsInput
|
||||
@ -249,4 +251,4 @@ export const useHasAnyPermission = (permissionIdsInput) => {
|
||||
if (permissionIds.length === 0) return true;
|
||||
|
||||
return permissionIds.some((id) => permissions.includes(id));
|
||||
};
|
||||
};
|
||||
|
@ -1,46 +1,33 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import {
|
||||
useForm,
|
||||
useFieldArray,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
Controller,
|
||||
} from "react-hook-form";
|
||||
import React, { createContext, useContext, useState, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
// Components
|
||||
import ExpenseList from "../../components/Expenses/ExpenseList";
|
||||
import ViewExpense from "../../components/Expenses/ViewExpense";
|
||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||
import GlobalModel from "../../components/common/GlobalModel";
|
||||
import PreviewDocument from "../../components/Expenses/PreviewDocument";
|
||||
import ManageExpense from "../../components/Expenses/ManageExpense";
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
import { useExpenseStatus } from "../../hooks/masterHook/useMaster";
|
||||
import {
|
||||
useEmployees,
|
||||
useEmployeesAllOrByProjectId,
|
||||
} from "../../hooks/useEmployees";
|
||||
import { useSelector } from "react-redux";
|
||||
import DateRangePicker from "../../components/common/DateRangePicker";
|
||||
import SelectMultiple from "../../components/common/SelectMultiple";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
defaultFilter,
|
||||
SearchSchema,
|
||||
} from "../../components/Expenses/ExpenseSchema";
|
||||
import ExpenseFilterPanel from "../../components/Expenses/ExpenseFilterPanel";
|
||||
|
||||
// Context & Hooks
|
||||
import { useFab } from "../../Context/FabContext";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import {
|
||||
CREATE_EXEPENSE,
|
||||
VIEW_ALL_EXPNESE,
|
||||
VIEW_SELF_EXPENSE,
|
||||
} from "../../utils/constants";
|
||||
import { useFab } from "../../Context/FabContext";
|
||||
import ExpenseFilterPanel from "../../components/Expenses/ExpenseFilterPanel";
|
||||
|
||||
// Schema & Defaults
|
||||
import {
|
||||
defaultFilter,
|
||||
SearchSchema,
|
||||
} from "../../components/Expenses/ExpenseSchema";
|
||||
|
||||
// Context
|
||||
export const ExpenseContext = createContext();
|
||||
export const useExpenseContext = () => {
|
||||
const context = useContext(ExpenseContext);
|
||||
@ -51,114 +38,101 @@ export const useExpenseContext = () => {
|
||||
};
|
||||
|
||||
const ExpensePage = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [filters, setFilter] = useState();
|
||||
const [groupBy, setGropBy] = useState("transactionDate");
|
||||
const IsCreatedAble = useHasUserPermission(CREATE_EXEPENSE);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const selectedProjectId = useSelector(
|
||||
(store) => store.localVariables.projectId
|
||||
);
|
||||
|
||||
const [filters, setFilter] = useState();
|
||||
const [groupBy, setGroupBy] = useState("transactionDate");
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const [ManageExpenseModal, setManageExpenseModal] = useState({
|
||||
IsOpen: null,
|
||||
expenseId: null,
|
||||
});
|
||||
|
||||
const [viewExpense, setViewExpense] = useState({
|
||||
expenseId: null,
|
||||
view: false,
|
||||
});
|
||||
|
||||
const [ViewDocument, setDocumentView] = useState({
|
||||
IsOpen: false,
|
||||
Image: null,
|
||||
});
|
||||
|
||||
const IsCreatedAble = useHasUserPermission(CREATE_EXEPENSE);
|
||||
const IsViewAll = useHasUserPermission(VIEW_ALL_EXPNESE);
|
||||
const IsViewSelf = useHasUserPermission(VIEW_SELF_EXPENSE);
|
||||
const contextValue = {
|
||||
setViewExpense,
|
||||
setManageExpenseModal,
|
||||
setDocumentView,
|
||||
};
|
||||
|
||||
const { setOffcanvasContent, setShowTrigger } = useFab();
|
||||
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(SearchSchema),
|
||||
defaultValues: defaultFilter,
|
||||
});
|
||||
|
||||
const { setOffcanvasContent, setShowTrigger } = useFab();
|
||||
const { reset } = methods;
|
||||
|
||||
const clearFilter = () => {
|
||||
setFilter({
|
||||
projectIds: [],
|
||||
statusIds: [],
|
||||
createdByIds: [],
|
||||
paidById: [],
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
});
|
||||
setFilter(defaultFilter);
|
||||
reset();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setShowTrigger(true);
|
||||
|
||||
setOffcanvasContent(
|
||||
"Expense Filters",
|
||||
<ExpenseFilterPanel
|
||||
onApply={(data) => {
|
||||
setFilter(data);
|
||||
}}
|
||||
handleGroupBy={(groupId) => setGropBy(groupId)}
|
||||
onApply={setFilter}
|
||||
handleGroupBy={setGroupBy}
|
||||
clearFilter={clearFilter}
|
||||
/>
|
||||
);
|
||||
|
||||
return () => {
|
||||
setOffcanvasContent("", null);
|
||||
setShowTrigger(false);
|
||||
setOffcanvasContent("", null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const contextValue = {
|
||||
setViewExpense,
|
||||
setManageExpenseModal,
|
||||
setDocumentView,
|
||||
};
|
||||
|
||||
return (
|
||||
<ExpenseContext.Provider value={contextValue}>
|
||||
<div className="container-fluid">
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: "Home", link: "/" },
|
||||
{ label: "Expense", link: null },
|
||||
]}
|
||||
/>
|
||||
{IsViewAll || IsViewSelf ? (
|
||||
<Breadcrumb data={[{ label: "Home", link: "/" }, { label: "Expense" }]} />
|
||||
|
||||
{(IsViewAll || IsViewSelf) ? (
|
||||
<>
|
||||
<div className="card my-1 px-0">
|
||||
<div className="card my-1">
|
||||
<div className="card-body py-2 px-3">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-12 col-sm-6 col-md-4">
|
||||
<div className="input-group input-group-sm">
|
||||
<span className="input-group-text" id="search-label">
|
||||
Search
|
||||
</span>
|
||||
<div className="col-sm-6 col-md-4">
|
||||
<div className="d-flex align-items-center">
|
||||
<span className="form-label me-2" id="search-label">Search</span>
|
||||
<input
|
||||
type="search"
|
||||
className="form-control"
|
||||
className="form-control form-control-sm w-auto"
|
||||
placeholder="Search Expense"
|
||||
aria-label="Search"
|
||||
aria-describedby="search-label"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-sm-6 col-md-8 text-end mt-2 mt-sm-0">
|
||||
<div className="col-sm-6 col-md-8 text-end mt-2 mt-sm-0">
|
||||
{IsCreatedAble && (
|
||||
<button
|
||||
type="button"
|
||||
title="Add New Expense"
|
||||
className="p-1 me-2 bg-primary rounded-circle"
|
||||
onClick={() =>
|
||||
setManageExpenseModal({
|
||||
IsOpen: true,
|
||||
expenseId: null,
|
||||
})
|
||||
}
|
||||
title="Add New Expense"
|
||||
onClick={() => setManageExpenseModal({ IsOpen: true, expenseId: null })}
|
||||
>
|
||||
<i className="bx bx-plus fs-4 text-white"></i>
|
||||
</button>
|
||||
@ -168,49 +142,36 @@ const ExpensePage = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ExpenseList filters={filters} groupBy={groupBy} searchText={searchText}/>
|
||||
<ExpenseList filters={filters} groupBy={groupBy} searchText={searchText} />
|
||||
</>
|
||||
) : (
|
||||
<div className="card text-center py-1">
|
||||
<i className="fa-solid fa-triangle-exclamation fs-5"></i>
|
||||
<p>
|
||||
Access Denied: You don't have permission to perform this action. !
|
||||
</p>
|
||||
<i className="fa-solid fa-triangle-exclamation fs-5" />
|
||||
<p>Access Denied: You don't have permission to perform this action!</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modals */}
|
||||
{ManageExpenseModal.IsOpen && (
|
||||
<GlobalModel
|
||||
isOpen={ManageExpenseModal.IsOpen}
|
||||
isOpen
|
||||
size="lg"
|
||||
closeModal={() =>
|
||||
setManageExpenseModal({
|
||||
IsOpen: null,
|
||||
expenseId: null,
|
||||
})
|
||||
}
|
||||
closeModal={() => setManageExpenseModal({ IsOpen: null, expenseId: null })}
|
||||
>
|
||||
<ManageExpense
|
||||
key={ManageExpenseModal.expenseId ?? "new"}
|
||||
expenseToEdit={ManageExpenseModal.expenseId}
|
||||
closeModal={() =>
|
||||
setManageExpenseModal({ IsOpen: null, expenseId: null })
|
||||
}
|
||||
closeModal={() => setManageExpenseModal({ IsOpen: null, expenseId: null })}
|
||||
/>
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
{viewExpense.view && (
|
||||
<GlobalModel
|
||||
isOpen={viewExpense.view}
|
||||
isOpen
|
||||
size="lg"
|
||||
modalType="top"
|
||||
closeModal={() =>
|
||||
setViewExpense({
|
||||
expenseId: null,
|
||||
view: false,
|
||||
})
|
||||
}
|
||||
closeModal={() => setViewExpense({ expenseId: null, view: false })}
|
||||
>
|
||||
<ViewExpense ExpenseId={viewExpense.expenseId} />
|
||||
</GlobalModel>
|
||||
@ -218,9 +179,9 @@ const ExpensePage = () => {
|
||||
|
||||
{ViewDocument.IsOpen && (
|
||||
<GlobalModel
|
||||
isOpen
|
||||
size="lg"
|
||||
key={ViewDocument.IsOpen ?? "new"}
|
||||
isOpen={ViewDocument.IsOpen}
|
||||
key={ViewDocument.Image ?? "doc"}
|
||||
closeModal={() => setDocumentView({ IsOpen: false, Image: null })}
|
||||
>
|
||||
<PreviewDocument imageUrl={ViewDocument.Image} />
|
||||
|
@ -1,17 +1,22 @@
|
||||
import { api } from "../utils/axiosClient";
|
||||
|
||||
const EmployeeRepository = {
|
||||
getAllEmployeeList:(showInactive)=>api.get(`api/employee/list?showInactive=${showInactive}`),
|
||||
getAllEmployeeList: (showInactive) =>
|
||||
api.get(`api/employee/list?showInactive=${showInactive}`),
|
||||
getEmployeeListByproject: (projectid) =>
|
||||
api.get(`/api/employee/list/${projectid}`),
|
||||
searchEmployees: (query) =>
|
||||
api.get(`/api/employee/search/${query}`),
|
||||
manageEmployee: (data) =>
|
||||
api.post("/api/employee/manage", data),
|
||||
searchEmployees: (query) => api.get(`/api/employee/search/${query}`),
|
||||
manageEmployee: (data) => api.post("/api/employee/manage", data),
|
||||
updateEmployee: (id, data) => api.put(`/users/${id}`, data),
|
||||
// deleteEmployee: ( id ) => api.delete( `/users/${ id }` ),
|
||||
getEmployeeProfile:(id)=>api.get(`/api/employee/profile/get/${id}`),
|
||||
deleteEmployee:(id)=>api.delete(`/api/employee/${id}`)
|
||||
getEmployeeProfile: (id) => api.get(`/api/employee/profile/get/${id}`),
|
||||
deleteEmployee: (id) => api.delete(`/api/employee/${id}`),
|
||||
getEmployeeName: (projectId, search) =>
|
||||
api.get(
|
||||
`/api/Employee/basic${projectId ? `?projectId=${projectId}` : ""}${
|
||||
search ? `${projectId ? "&" : "?"}searchString=${search}` : ""
|
||||
}`
|
||||
),
|
||||
};
|
||||
|
||||
export default EmployeeRepository;
|
||||
|
@ -44,7 +44,7 @@ axiosClient.interceptors.response.use(
|
||||
const originalRequest = error.config;
|
||||
|
||||
// Skip retry for public requests or already retried ones
|
||||
if (!originalRequest || originalRequest._retry || originalRequest.authRequired === false) {
|
||||
if (!originalRequest && originalRequest._retry || originalRequest.authRequired === false) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user