added expenses Reimburse

This commit is contained in:
pramod mahajan 2025-08-01 00:42:51 +05:30
parent b30940c2aa
commit 35f221038d
11 changed files with 657 additions and 383 deletions

View File

@ -73,21 +73,6 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
return ( 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}> <FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start"> <form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
@ -174,6 +159,21 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
</div> </div>
</div> </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"> <div className="d-flex justify-content-end py-3 gap-2">
<button <button
@ -189,6 +189,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
</div> </div>
</form> </form>
</FormProvider> </FormProvider>
</> </>
); );
}; };

View File

@ -25,7 +25,6 @@ export const ExpenseSchema = (expenseTypes) => {
const selected = new Date(val); const selected = new Date(val);
const today = new Date(); const today = new Date();
// Set both to midnight to avoid time-related issues
selected.setHours(0, 0, 0, 0); selected.setHours(0, 0, 0, 0);
today.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" }), .nonempty({ message: "At least one file attachment is required" }),
reimburseTransactionId: z.string().optional(),
reimburseDate: z.string().optional(),
reimburseById: z.string().optional(),
}) })
.refine( .refine(
@ -132,10 +129,53 @@ export const defaultExpense = {
billAttachments: [], billAttachments: [],
}; };
export const ActionSchema = z.object({
export const ExpenseActionScheam = (isReimbursement = false) => {
return z
.object({
comment: z.string().min(1, { message: "Please leave comment" }), comment: z.string().min(1, { message: "Please leave comment" }),
selectedStatus: z.string().min(1, { message: "Please select a status" }), 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({ export const SearchSchema = z.object({
projectIds: z.array(z.string()).optional(), projectIds: z.array(z.string()).optional(),

View File

@ -206,7 +206,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}> <form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
<div className="row my-2"> <div className="row my-2">
<div className="col-md-6"> <div className="col-md-6">
<label htmlFor="projectId" className="form-label"> <label className="form-label">
Select Project Select Project
</label> </label>
<select <select
@ -230,7 +230,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label for="expensesTypeId" className="form-label "> <label htmlFor="expensesTypeId" className="form-label ">
Expense Type Expense Type
</label> </label>
<select <select
@ -261,7 +261,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<div className="row my-2"> <div className="row my-2">
<div className="col-md-6"> <div className="col-md-6">
<label for="paymentModeId" className="form-label "> <label htmlFor="paymentModeId" className="form-label ">
Payment Mode Payment Mode
</label> </label>
<select <select
@ -290,7 +290,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label for="paidById" className="form-label "> <label htmlFor="paidById" className="form-label ">
Paid By Paid By
</label> </label>
<select <select
@ -320,7 +320,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<div className="row my-2"> <div className="row my-2">
<div className="col-md-6"> <div className="col-md-6">
<label for="transactionDate" className="form-label "> <label htmlFor="transactionDate" className="form-label ">
Transaction Date Transaction Date
</label> </label>
{/* <input {/* <input
@ -343,7 +343,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label for="amount" className="form-label "> <label htmlFor="amount" className="form-label ">
Amount Amount
</label> </label>
<input <input
@ -361,9 +361,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</div> </div>
</div> </div>
<div class="row my-2"> <div className="row my-2">
<div className="col-md-6"> <div className="col-md-6">
<label for="supplerName" className="form-label "> <label htmlFor="supplerName" className="form-label ">
Supplier Name Supplier Name
</label> </label>
<input <input
@ -380,7 +380,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label for="location" className="form-label "> <label htmlFor="location" className="form-label ">
Location Location
</label> </label>
<input <input
@ -396,7 +396,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</div> </div>
<div className="row my-2"> <div className="row my-2">
<div className="col-md-6"> <div className="col-md-6">
<label for="statusId" className="form-label "> <label htmlFor="statusId" className="form-label ">
TransactionId TransactionId
</label> </label>
<input <input
@ -415,7 +415,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
{ExpenseType?.noOfPersonsRequired && ( {ExpenseType?.noOfPersonsRequired && (
<div className="col-md-6"> <div className="col-md-6">
<label htmlFor="noOfPersons" > <label >
No. of Persons No. of Persons
</label> </label>
<input <input
@ -436,7 +436,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<div className="row my-2"> <div className="row my-2">
<div className="col-md-12" > <div className="col-md-12" >
<label for="description">Description</label> <label htmlFor="description">Description</label>
<textarea <textarea
id="description" id="description"
className="form-control form-control-sm" className="form-control form-control-sm"

View File

@ -7,35 +7,43 @@ import {
import { formatUTCToLocalTime } from "../../utils/dateUtils"; import { formatUTCToLocalTime } from "../../utils/dateUtils";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { ActionSchema } from "./ExpenseSchema"; import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
import { useExpenseContext } from "../../pages/Expense/ExpensePage"; import { useExpenseContext } from "../../pages/Expense/ExpensePage";
import { getColorNameFromHex } from "../../utils/appUtils"; import { getColorNameFromHex } from "../../utils/appUtils";
import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton"; import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; 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 { useProfile } from "../../hooks/useProfile";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import Avatar from "../common/Avatar"; 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 ViewExpense = ({ ExpenseId }) => {
const { data, isLoading, isError, error } = useExpense(ExpenseId); const { data, isLoading, isError, error } = useExpense(ExpenseId);
const [IsPaymentProcess, setIsPaymentProcess] = useState(false);
const [clickedStatusId, setClickedStatusId] = useState(null);
const IsReview = useHasUserPermission(REVIEW_EXPENSE); const IsReview = useHasUserPermission(REVIEW_EXPENSE);
const [imageLoaded, setImageLoaded] = useState({}); const [imageLoaded, setImageLoaded] = useState({});
const { setDocumentView } = useExpenseContext(); const { setDocumentView } = useExpenseContext();
const ActionSchema = ExpenseActionScheam(IsPaymentProcess) ?? z.object({});
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
register, register,
handleSubmit, handleSubmit,
setValue, setValue,
reset, reset,
control,
formState: { errors }, formState: { errors },
} = useForm({ } = useForm({
resolver: zodResolver(ActionSchema), resolver: zodResolver(ActionSchema),
defaultValues: { defaultValues: defaultActionValues,
comment: "",
selectedStatus: "",
},
}); });
const userPermissions = useSelector( const userPermissions = useSelector(
@ -51,24 +59,29 @@ const ViewExpense = ({ ExpenseId }) => {
: []; : [];
if (permissionIds.length === 0) return true; if (permissionIds.length === 0) return true;
if (permissionIds.includes(PROCESS_EXPENSE)) {
setIsPaymentProcess(true);
}
return permissionIds.some((id) => userPermissions.includes(id)); return permissionIds.some((id) => userPermissions.includes(id));
}); });
}, [data, userPermissions]); }, [data, userPermissions]);
const { mutate: MakeAction } = useActionOnExpense(() => reset()); const { mutate: MakeAction,isPending } = useActionOnExpense(() => {
setClickedStatusId(null);
reset()});
const onSubmit = (formData) => { const onSubmit = (formData) => {
const Payload = { const Payload = {
...formData,
reimburseDate:moment.utc(formData.reimburseDate, "DD-MM-YYYY").toISOString(),
expenseId: ExpenseId, expenseId: ExpenseId,
statusId: formData.selectedStatus,
comment: formData.comment, comment: formData.comment,
}; };
MakeAction(Payload); MakeAction(Payload);
}; };
if (isLoading) return <ExpenseDetailsSkeleton />; if (isLoading) return <ExpenseDetailsSkeleton />;
if (isError) return <Error error={error} />;
const handleImageLoad = (id) => { const handleImageLoad = (id) => {
setImageLoaded((prev) => ({ ...prev, [id]: true })); setImageLoaded((prev) => ({ ...prev, [id]: true }));
}; };
@ -81,67 +94,89 @@ const ViewExpense = ({ ExpenseId }) => {
<hr /> <hr />
</div> </div>
{/* Each info block below now has h-100 to stretch */} {/* Row 1 */}
<div className="col-12 col-md-6 mb-3 h-100"> <div className="col-md-6 mb-3">
<div className="d-flex"> <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 : Transaction Date :
</label> </label>
<div className="text-muted"> <div className="text-muted">
{formatUTCToLocalTime(data.transactionDate)} {formatUTCToLocalTime(data?.transactionDate)}
</div> </div>
</div> </div>
</div> </div>
<div className="col-md-6 mb-3">
<div className="col-12 col-md-6 mb-3 h-100">
<div className="d-flex"> <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 : Expense Type :
</label> </label>
<div className="text-muted">{data.expensesType.name}</div> <div className="text-muted">{data?.expensesType?.name}</div>
</div> </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"> <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 : Supplier :
</label> </label>
<div className="text-muted">{data.supplerName}</div> <div className="text-muted">{data?.supplerName}</div>
</div> </div>
</div> </div>
<div className="col-md-6 mb-3">
<div className="col-12 col-md-4 mb-3 h-100">
<div className="d-flex"> <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 className="text-muted"> {data.amount}</div>
</div> </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"> <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 : Payment Mode :
</label> </label>
<div className="text-muted">{data.paymentMode.name}</div> <div className="text-muted">{data?.paymentMode?.name}</div>
</div> </div>
</div> </div>
<div className="col-md-6 mb-3">
<div className="row align-items-center mb-3"> <div className="d-flex">
<div className="col-6"> <label
<div className="d-flex align-items-center"> className="form-label me-2 mb-0 fw-semibold text-start"
<label className="form-label me-2 mb-0 fw-semibold"> style={{ minWidth: "130px" }}
>
Paid By : Paid By :
</label> </label>
<div className="text-muted"> <div className="text-muted">
{data.paidBy.firstName} {data.paidBy.lastName} {data?.paidBy?.firstName} {data?.paidBy?.lastName}
</div> </div>
</div> </div>
</div> </div>
<div className="col-6"> {/* Row 4 */}
<div className="d-flex align-items-center"> <div className="col-md-6 mb-3">
<label className="form-label me-2 mb-0 fw-semibold"> <div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Status : Status :
</label> </label>
<span <span
@ -153,30 +188,35 @@ const ViewExpense = ({ ExpenseId }) => {
</span> </span>
</div> </div>
</div> </div>
</div> <div className="col-md-6 mb-3">
<div className="col-12 col-md-4 mb-3 h-100">
<div className="d-flex"> <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" }}
>
Pre-Approved : Pre-Approved :
</label> </label>
<div className="text-muted">{data.preApproved ? "Yes" : "No"}</div> <div className="text-muted">{data.preApproved ? "Yes" : "No"}</div>
</div> </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"> <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 : Project :
</label> </label>
<div className="text-muted text-start">{data?.project?.name}</div> <div className="text-muted">{data?.project?.name}</div>
</div> </div>
</div> </div>
<div className="col-md-6 mb-3">
<div className="row align-items-center mb-3"> <div className="d-flex">
<div className="col-12 col-md-auto"> <label
<div className="d-flex align-items-center"> className="form-label me-2 mb-0 fw-semibold text-start"
<label className="form-label me-2 mb-0 fw-semibold"> style={{ minWidth: "130px" }}
>
Created At : Created At :
</label> </label>
<div className="text-muted"> <div className="text-muted">
@ -185,10 +225,14 @@ const ViewExpense = ({ ExpenseId }) => {
</div> </div>
</div> </div>
{/* Row 6 */}
{data.createdBy && ( {data.createdBy && (
<div className="col-12 col-md"> <div className="col-md-6 mb-3 text-start">
<div className="d-flex align-items-center"> <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" }}
>
Created By : Created By :
</label> </label>
<div className="d-flex align-items-center gap-1"> <div className="d-flex align-items-center gap-1">
@ -207,15 +251,16 @@ const ViewExpense = ({ ExpenseId }) => {
</div> </div>
</div> </div>
)} )}
</div>
{data.reviewedBy && ( {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"> <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 : Reviewed By :
</label> </label>
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<Avatar <Avatar
size="xs" size="xs"
@ -232,13 +277,16 @@ const ViewExpense = ({ ExpenseId }) => {
</div> </div>
</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"> <div className="d-flex align-items-center">
<Avatar <Avatar
size="xs" size="xs"
@ -255,7 +303,65 @@ const ViewExpense = ({ ExpenseId }) => {
</div> </div>
</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>
)}
</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"> <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>
@ -284,7 +390,7 @@ const ViewExpense = ({ ExpenseId }) => {
return ( return (
<div <div
className="list-group-item list-group-item-action d-flex align-items-center" className="list-group-item list-group-item-action d-flex align-items-center"
key={doc.id} key={doc.documentId}
> >
<div <div
className="rounded me-1 d-flex align-items-center justify-content-center cursor-pointer" className="rounded me-1 d-flex align-items-center justify-content-center cursor-pointer"
@ -325,25 +431,58 @@ const ViewExpense = ({ ExpenseId }) => {
<hr className="divider my-1" /> <hr className="divider my-1" />
{Array.isArray(data?.nextStatus) && data.nextStatus.length > 0 && ( {Array.isArray(data?.nextStatus) && data.nextStatus.length > 0 && (
<>
{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 className="col-12 mb-3 text-start"> <div className="col-12 mb-3 text-start">
{nextStatusWithPermission.length > 0 && ( {nextStatusWithPermission.length > 0 && (
<> <>
<label className="form-label me-2 mb-0 fw-semibold"> <label className="form-label me-2 mb-0 ">Comment:</label>
Comment:
</label>
<textarea <textarea
className="form-control form-control-sm" className="form-control form-control-sm"
{...register("comment")} {...register("comment")}
rows="2" rows="2"
/> />
{errors.comment && ( {errors.comment && (
<small className="danger-text">{errors.comment.message}</small> <small className="danger-text">
{errors.comment.message}
</small>
)} )}
</> </>
)} )}
<input type="hidden" {...register("selectedStatus")} />
{nextStatusWithPermission?.length > 0 && ( {nextStatusWithPermission?.length > 0 && (
<div className="text-center flex-wrap gap-2 my-2"> <div className="text-center flex-wrap gap-2 my-2">
{nextStatusWithPermission?.map((status, index) => ( {nextStatusWithPermission?.map((status, index) => (
@ -351,17 +490,20 @@ const ViewExpense = ({ ExpenseId }) => {
key={status.id || index} key={status.id || index}
type="button" type="button"
onClick={() => { onClick={() => {
setValue("selectedStatus", status.id); setClickedStatusId(status.id);
setValue("statusId", status.id);
handleSubmit(onSubmit)(); handleSubmit(onSubmit)();
}} }}
disabled={isPending}
className="btn btn-primary btn-sm cursor-pointer mx-2 border-0" className="btn btn-primary btn-sm cursor-pointer mx-2 border-0"
> >
{status.displayName || status.name} {(isPending && clickedStatusId === status.id) ? "Please Wait..." : (status.displayName || status.name)}
</button> </button>
))} ))}
</div> </div>
)} )}
</div> </div>
</>
)} )}
</form> </form>
); );

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

View 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

View File

@ -3,27 +3,22 @@ import { cacheData, getCachedData } from "../slices/apiDataManager";
import { RolesRepository } from "../repositories/MastersRepository"; import { RolesRepository } from "../repositories/MastersRepository";
import EmployeeRepository from "../repositories/EmployeeRepository"; import EmployeeRepository from "../repositories/EmployeeRepository";
import ProjectRepository from "../repositories/ProjectRepository"; 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 showToast from "../services/toastService";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { store } from "../store/store"; import { store } from "../store/store";
import { queryClient } from "../layouts/AuthLayout"; import { queryClient } from "../layouts/AuthLayout";
// Query --------------------------------------------------------------------------- // Query ---------------------------------------------------------------------------
export const useAllEmployees = (showInactive) => {
export const useAllEmployees = ( showInactive ) =>
{
const { const {
data = [], data = [],
isLoading, isLoading,
error, error,
refetch, // optional if you want recall functionality refetch, // optional if you want recall functionality
} = useQuery({ } = useQuery({
queryKey: ['allEmployee', showInactive], queryKey: ["allEmployee", showInactive],
queryFn: async () => { queryFn: async () => {
const res = await EmployeeRepository.getAllEmployeeList(showInactive); const res = await EmployeeRepository.getAllEmployeeList(showInactive);
return res.data; return res.data;
@ -39,9 +34,7 @@ export const useAllEmployees = ( showInactive ) =>
}; };
// ManageBucket.jsx // ManageBucket.jsx
export const useEmployees = ( selectedProject ) => export const useEmployees = (selectedProject) => {
{
const { const {
data = [], data = [],
isLoading, isLoading,
@ -50,7 +43,9 @@ export const useEmployees = ( selectedProject ) =>
} = useQuery({ } = useQuery({
queryKey: ["employeeListByProject", selectedProject], queryKey: ["employeeListByProject", selectedProject],
queryFn: async () => { queryFn: async () => {
const res = await EmployeeRepository.getEmployeeListByproject(selectedProject); const res = await EmployeeRepository.getEmployeeListByproject(
selectedProject
);
return res.data || res; return res.data || res;
}, },
enabled: !!selectedProject, enabled: !!selectedProject,
@ -72,7 +67,7 @@ export const useEmployeeRoles = (employeeId) => {
isLoading: loading, isLoading: loading,
error, error,
} = useQuery({ } = useQuery({
queryKey: ['employeeRoles', employeeId], queryKey: ["employeeRoles", employeeId],
queryFn: async () => { queryFn: async () => {
const res = await RolesRepository.getEmployeeRoles(employeeId); const res = await RolesRepository.getEmployeeRoles(employeeId);
return res.data; return res.data;
@ -95,7 +90,7 @@ export const useEmployeesByProject = (projectId) => {
error, error,
refetch: recallProjectEmplloyee, refetch: recallProjectEmplloyee,
} = useQuery({ } = useQuery({
queryKey: ['projectEmployees', projectId], queryKey: ["projectEmployees", projectId],
queryFn: async () => { queryFn: async () => {
const res = await ProjectRepository.getEmployeesByProject(projectId); const res = await ProjectRepository.getEmployeesByProject(projectId);
return res.data; return res.data;
@ -112,13 +107,14 @@ export const useEmployeesByProject = (projectId) => {
}; };
// EmployeeList.jsx // EmployeeList.jsx
export const useEmployeesAllOrByProjectId = (showAllEmployees ,projectId, export const useEmployeesAllOrByProjectId = (
showInactive) => { showAllEmployees,
projectId,
showInactive
) => {
const queryKey = showAllEmployees const queryKey = showAllEmployees
? ['allEmployees', showInactive] ? ["allEmployees", showInactive]
: ['projectEmployees', projectId, showInactive]; : ["projectEmployees", projectId, showInactive];
const queryFn = async () => { const queryFn = async () => {
if (showAllEmployees) { if (showAllEmployees) {
@ -139,7 +135,8 @@ export const useEmployeesAllOrByProjectId = (showAllEmployees ,projectId,
} = useQuery({ } = useQuery({
queryKey, queryKey,
queryFn, queryFn,
enabled:typeof showInactive === "boolean" && (showAllEmployees || !!projectId), enabled:
typeof showInactive === "boolean" && (showAllEmployees || !!projectId),
}); });
return { return {
@ -151,16 +148,15 @@ export const useEmployeesAllOrByProjectId = (showAllEmployees ,projectId,
}; };
// ManageEmployee.jsx // ManageEmployee.jsx
export const useEmployeeProfile = ( employeeId ) => export const useEmployeeProfile = (employeeId) => {
{
const isEnabled = !!employeeId; const isEnabled = !!employeeId;
const { const {
data = null, data = null,
isLoading: loading, isLoading: loading,
error, error,
refetch refetch,
} = useQuery({ } = useQuery({
queryKey: ['employeeProfile', employeeId], queryKey: ["employeeProfile", employeeId],
queryFn: async () => { queryFn: async () => {
if (!employeeId) return null; if (!employeeId) return null;
const res = await EmployeeRepository.getEmployeeProfile(employeeId); const res = await EmployeeRepository.getEmployeeProfile(employeeId);
@ -173,47 +169,64 @@ export const useEmployeeProfile = ( employeeId ) =>
employee: data, employee: data,
loading, loading,
error, 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------------------------------------------------------------------ // Mutation------------------------------------------------------------------
export const useUpdateEmployee = () => {
const selectedProject = useSelector(
export const useUpdateEmployee = () => (store) => store.localVariables.projectId
{ );
const selectedProject = useSelector((store)=>store.localVariables.projectId)
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (employeeData) => EmployeeRepository.manageEmployee(employeeData), mutationFn: (employeeData) =>
EmployeeRepository.manageEmployee(employeeData),
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
const id = variables.id || variables.employeeId; const id = variables.id || variables.employeeId;
const isAllEmployee = variables.IsAllEmployee; const isAllEmployee = variables.IsAllEmployee;
// Cache invalidation // Cache invalidation
queryClient.invalidateQueries( {queryKey:[ 'allEmployees'] }); queryClient.invalidateQueries({ queryKey: ["allEmployees"] });
// queryClient.invalidateQueries(['employeeProfile', id]); // queryClient.invalidateQueries(['employeeProfile', id]);
queryClient.invalidateQueries( {queryKey: [ 'projectEmployees' ]} ); queryClient.invalidateQueries({ queryKey: ["projectEmployees"] });
queryClient.removeQueries({ queryKey: ["empListByProjectAllocated"] }); queryClient.removeQueries({ queryKey: ["empListByProjectAllocated"] });
// queryClient.invalidateQueries( {queryKey:[ 'employeeListByProject']} ); // queryClient.invalidateQueries( {queryKey:[ 'employeeListByProject']} );
showToast( `Employee ${ id ? 'updated' : 'created' } successfully`, 'success' ); showToast(
`Employee ${id ? "updated" : "created"} successfully`,
"success"
);
}, },
onError: (error) => { onError: (error) => {
const msg = error?.response?.data?.message || error.message || 'Something went wrong'; const msg =
showToast(msg, 'error'); error?.response?.data?.message ||
error.message ||
"Something went wrong";
showToast(msg, "error");
}, },
}); });
}; };
export const useSuspendEmployee = ({
setIsDeleteModalOpen,
export const useSuspendEmployee = ({ setIsDeleteModalOpen, setemployeeLodaing }) => { setemployeeLodaing,
}) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const selectedProject = useSelector((store)=>store.localVariables.projectId) const selectedProject = useSelector(
(store) => store.localVariables.projectId
);
return useMutation({ return useMutation({
mutationFn: (id) => { mutationFn: (id) => {
setemployeeLodaing(true); setemployeeLodaing(true);
@ -221,11 +234,11 @@ export const useSuspendEmployee = ({ setIsDeleteModalOpen, setemployeeLodaing })
}, },
onSuccess: () => { onSuccess: () => {
// queryClient.invalidateQueries( ['allEmployee',false]); // queryClient.invalidateQueries( ['allEmployee',false]);
queryClient.invalidateQueries( {queryKey: [ 'projectEmployees' ]} ); queryClient.invalidateQueries({ queryKey: ["projectEmployees"] });
queryClient.invalidateQueries( {queryKey:[ 'employeeListByProject' ,selectedProject]} ); queryClient.invalidateQueries({
queryKey: ["employeeListByProject", selectedProject],
});
showToast("Employee deleted successfully.", "success"); showToast("Employee deleted successfully.", "success");
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false);
}, },
@ -247,8 +260,11 @@ export const useSuspendEmployee = ({ setIsDeleteModalOpen, setemployeeLodaing })
// Manage Role // Manage Role
export const useUpdateEmployeeRoles = ({
export const useUpdateEmployeeRoles = ({ onClose, resetForm, onSuccessCallback } = {}) => { onClose,
resetForm,
onSuccessCallback,
} = {}) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (updates) => RolesRepository.createEmployeeRoles(updates), mutationFn: (updates) => RolesRepository.createEmployeeRoles(updates),
@ -264,7 +280,9 @@ export const useUpdateEmployeeRoles = ({ onClose, resetForm, onSuccessCallback }
}, },
onError: (err) => { onError: (err) => {
const message = 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"); showToast(message, "error");
}, },
}); });

View File

@ -20,8 +20,12 @@ const cleanFilter = (filter) => {
return cleaned; return cleaned;
}; };
export const useExpenseList = (
export const useExpenseList = (pageSize, pageNumber, filter, searchString = '') => { pageSize,
pageNumber,
filter,
searchString = ""
) => {
return useQuery({ return useQuery({
queryKey: ["Expenses", pageNumber, pageSize, filter, searchString], queryKey: ["Expenses", pageNumber, pageSize, filter, searchString],
queryFn: async () => { queryFn: async () => {
@ -38,8 +42,6 @@ export const useExpenseList = (pageSize, pageNumber, filter, searchString = '')
}); });
}; };
export const useExpense = (ExpenseId) => { export const useExpense = (ExpenseId) => {
return useQuery({ return useQuery({
queryKey: ["Expense", ExpenseId], queryKey: ["Expense", ExpenseId],
@ -55,13 +57,10 @@ export const useExpenseFilter = () => {
return useQuery({ return useQuery({
queryKey: ["ExpenseFilter"], queryKey: ["ExpenseFilter"],
queryFn: async () => queryFn: async () =>
await ExpenseRepository.GetExpenseFilter().then( await ExpenseRepository.GetExpenseFilter().then((res) => res.data),
(res) => res.data
),
}); });
}; };
// ---------------------------Mutation--------------------------------------------- // ---------------------------Mutation---------------------------------------------
export const useCreateExpnse = (onSuccessCallBack) => { export const useCreateExpnse = (onSuccessCallBack) => {
@ -145,7 +144,7 @@ export const useUpdateExpense = (onSuccessCallBack) => {
}, },
onError: (error) => { onError: (error) => {
showToast("Something went wrong.Please try again later.", "error"); showToast("Something went wrong.Please try again later.", "error");
} },
}); });
}; };
@ -181,22 +180,24 @@ export const useActionOnExpense = (onSuccessCallBack) => {
}; };
} }
); );
queryClient.setQueriesData( // queryClient.setQueriesData(
{ queryKey: ["Expense", updatedExpense.id] }, // { queryKey: ["Expense", updatedExpense.id] },
(oldData) => { // (oldData) => {
return { // return {
...oldData, // ...oldData,
nextStatus: updatedExpense.nextStatus, // nextStatus: updatedExpense.nextStatus,
status: updatedExpense.status, // status: updatedExpense.status,
}; // };
} // }
); // );
queryClient.invalidateQueries({queryKey:["Expense",updatedExpense.id]})
if (onSuccessCallBack) onSuccessCallBack(); if (onSuccessCallBack) onSuccessCallBack();
}, },
onError: (error) => { onError: (error) => {
showToast( showToast(
error.message || "Something went wrong.Please try again later.", error.response.data.message ||
"Something went wrong.Please try again later.",
"error" "error"
); );
}, },
@ -213,7 +214,8 @@ export const useDeleteExpense = () => {
}, },
onSuccess: (data, variables) => { onSuccess: (data, variables) => {
queryClient.setQueryData(["Expenses"], (oldData) => { 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( const updatedList = oldData.data.filter(
(expense) => expense.id !== variables.id (expense) => expense.id !== variables.id
@ -228,18 +230,18 @@ export const useDeleteExpense = () => {
showToast(data.message || "Expense deleted successfully", "success"); showToast(data.message || "Expense deleted successfully", "success");
}, },
onError: (error) => { 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) => { export const useHasAnyPermission = (permissionIdsInput) => {
const permissions = useSelector( const permissions = useSelector((state) => state?.profile?.permissions || []);
(state) => state?.profile?.permissions || []
);
const permissionIds = Array.isArray(permissionIdsInput) const permissionIds = Array.isArray(permissionIdsInput)
? permissionIdsInput ? permissionIdsInput

View File

@ -1,46 +1,33 @@
import React, { import React, { createContext, useContext, useState, useEffect } from "react";
createContext, import { useForm } from "react-hook-form";
useContext, import { zodResolver } from "@hookform/resolvers/zod";
useState, import { useSelector } from "react-redux";
useRef,
useEffect, // Components
} from "react";
import {
useForm,
useFieldArray,
FormProvider,
useFormContext,
Controller,
} from "react-hook-form";
import ExpenseList from "../../components/Expenses/ExpenseList"; import ExpenseList from "../../components/Expenses/ExpenseList";
import ViewExpense from "../../components/Expenses/ViewExpense"; import ViewExpense from "../../components/Expenses/ViewExpense";
import Breadcrumb from "../../components/common/Breadcrumb"; import Breadcrumb from "../../components/common/Breadcrumb";
import GlobalModel from "../../components/common/GlobalModel"; import GlobalModel from "../../components/common/GlobalModel";
import PreviewDocument from "../../components/Expenses/PreviewDocument"; import PreviewDocument from "../../components/Expenses/PreviewDocument";
import ManageExpense from "../../components/Expenses/ManageExpense"; import ManageExpense from "../../components/Expenses/ManageExpense";
import { useProjectName } from "../../hooks/useProjects"; import ExpenseFilterPanel from "../../components/Expenses/ExpenseFilterPanel";
import { useExpenseStatus } from "../../hooks/masterHook/useMaster";
import { // Context & Hooks
useEmployees, import { useFab } from "../../Context/FabContext";
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 { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { import {
CREATE_EXEPENSE, CREATE_EXEPENSE,
VIEW_ALL_EXPNESE, VIEW_ALL_EXPNESE,
VIEW_SELF_EXPENSE, VIEW_SELF_EXPENSE,
} from "../../utils/constants"; } 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 ExpenseContext = createContext();
export const useExpenseContext = () => { export const useExpenseContext = () => {
const context = useContext(ExpenseContext); const context = useContext(ExpenseContext);
@ -51,95 +38,87 @@ export const useExpenseContext = () => {
}; };
const ExpensePage = () => { 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( const selectedProjectId = useSelector(
(store) => store.localVariables.projectId (store) => store.localVariables.projectId
); );
const [filters, setFilter] = useState();
const [groupBy, setGroupBy] = useState("transactionDate");
const [searchText, setSearchText] = useState("");
const [ManageExpenseModal, setManageExpenseModal] = useState({ const [ManageExpenseModal, setManageExpenseModal] = useState({
IsOpen: null, IsOpen: null,
expenseId: null, expenseId: null,
}); });
const [viewExpense, setViewExpense] = useState({ const [viewExpense, setViewExpense] = useState({
expenseId: null, expenseId: null,
view: false, view: false,
}); });
const [ViewDocument, setDocumentView] = useState({ const [ViewDocument, setDocumentView] = useState({
IsOpen: false, IsOpen: false,
Image: null, Image: null,
}); });
const IsCreatedAble = useHasUserPermission(CREATE_EXEPENSE);
const IsViewAll = useHasUserPermission(VIEW_ALL_EXPNESE); const IsViewAll = useHasUserPermission(VIEW_ALL_EXPNESE);
const IsViewSelf = useHasUserPermission(VIEW_SELF_EXPENSE); const IsViewSelf = useHasUserPermission(VIEW_SELF_EXPENSE);
const contextValue = {
setViewExpense, const { setOffcanvasContent, setShowTrigger } = useFab();
setManageExpenseModal,
setDocumentView,
};
const methods = useForm({ const methods = useForm({
resolver: zodResolver(SearchSchema), resolver: zodResolver(SearchSchema),
defaultValues: defaultFilter, defaultValues: defaultFilter,
}); });
const { setOffcanvasContent, setShowTrigger } = useFab(); const { reset } = methods;
const clearFilter = () => { const clearFilter = () => {
setFilter({ setFilter(defaultFilter);
projectIds: [],
statusIds: [],
createdByIds: [],
paidById: [],
startDate: null,
endDate: null,
});
reset(); reset();
}; };
useEffect(() => { useEffect(() => {
setShowTrigger(true); setShowTrigger(true);
setOffcanvasContent( setOffcanvasContent(
"Expense Filters", "Expense Filters",
<ExpenseFilterPanel <ExpenseFilterPanel
onApply={(data) => { onApply={setFilter}
setFilter(data); handleGroupBy={setGroupBy}
}} clearFilter={clearFilter}
handleGroupBy={(groupId) => setGropBy(groupId)}
/> />
); );
return () => { return () => {
setOffcanvasContent("", null);
setShowTrigger(false); setShowTrigger(false);
setOffcanvasContent("", null);
}; };
}, []); }, []);
const contextValue = {
setViewExpense,
setManageExpenseModal,
setDocumentView,
};
return ( return (
<ExpenseContext.Provider value={contextValue}> <ExpenseContext.Provider value={contextValue}>
<div className="container-fluid"> <div className="container-fluid">
<Breadcrumb <Breadcrumb data={[{ label: "Home", link: "/" }, { label: "Expense" }]} />
data={[
{ label: "Home", link: "/" }, {(IsViewAll || IsViewSelf) ? (
{ label: "Expense", link: null },
]}
/>
{IsViewAll || IsViewSelf ? (
<> <>
<div className="card my-1 px-0"> <div className="card my-1">
<div className="card-body py-2 px-3"> <div className="card-body py-2 px-3">
<div className="row align-items-center"> <div className="row align-items-center">
<div className="col-12 col-sm-6 col-md-4"> <div className="col-sm-6 col-md-4">
<div className="input-group input-group-sm"> <div className="d-flex align-items-center">
<span className="input-group-text" id="search-label"> <span className="form-label me-2" id="search-label">Search</span>
Search
</span>
<input <input
type="search" type="search"
className="form-control" className="form-control form-control-sm w-auto"
placeholder="Search Expense" placeholder="Search Expense"
aria-label="Search"
aria-describedby="search-label" aria-describedby="search-label"
value={searchText} value={searchText}
onChange={(e) => setSearchText(e.target.value)} onChange={(e) => setSearchText(e.target.value)}
@ -147,18 +126,13 @@ const ExpensePage = () => {
</div> </div>
</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 && ( {IsCreatedAble && (
<button <button
type="button" type="button"
title="Add New Expense"
className="p-1 me-2 bg-primary rounded-circle" className="p-1 me-2 bg-primary rounded-circle"
onClick={() => title="Add New Expense"
setManageExpenseModal({ onClick={() => setManageExpenseModal({ IsOpen: true, expenseId: null })}
IsOpen: true,
expenseId: null,
})
}
> >
<i className="bx bx-plus fs-4 text-white"></i> <i className="bx bx-plus fs-4 text-white"></i>
</button> </button>
@ -172,45 +146,32 @@ const ExpensePage = () => {
</> </>
) : ( ) : (
<div className="card text-center py-1"> <div className="card text-center py-1">
<i className="fa-solid fa-triangle-exclamation fs-5"></i> <i className="fa-solid fa-triangle-exclamation fs-5" />
<p> <p>Access Denied: You don't have permission to perform this action!</p>
Access Denied: You don't have permission to perform this action. !
</p>
</div> </div>
)} )}
{/* Modals */}
{ManageExpenseModal.IsOpen && ( {ManageExpenseModal.IsOpen && (
<GlobalModel <GlobalModel
isOpen={ManageExpenseModal.IsOpen} isOpen
size="lg" size="lg"
closeModal={() => closeModal={() => setManageExpenseModal({ IsOpen: null, expenseId: null })}
setManageExpenseModal({
IsOpen: null,
expenseId: null,
})
}
> >
<ManageExpense <ManageExpense
key={ManageExpenseModal.expenseId ?? "new"} key={ManageExpenseModal.expenseId ?? "new"}
expenseToEdit={ManageExpenseModal.expenseId} expenseToEdit={ManageExpenseModal.expenseId}
closeModal={() => closeModal={() => setManageExpenseModal({ IsOpen: null, expenseId: null })}
setManageExpenseModal({ IsOpen: null, expenseId: null })
}
/> />
</GlobalModel> </GlobalModel>
)} )}
{viewExpense.view && ( {viewExpense.view && (
<GlobalModel <GlobalModel
isOpen={viewExpense.view} isOpen
size="lg" size="lg"
modalType="top" modalType="top"
closeModal={() => closeModal={() => setViewExpense({ expenseId: null, view: false })}
setViewExpense({
expenseId: null,
view: false,
})
}
> >
<ViewExpense ExpenseId={viewExpense.expenseId} /> <ViewExpense ExpenseId={viewExpense.expenseId} />
</GlobalModel> </GlobalModel>
@ -218,9 +179,9 @@ const ExpensePage = () => {
{ViewDocument.IsOpen && ( {ViewDocument.IsOpen && (
<GlobalModel <GlobalModel
isOpen
size="lg" size="lg"
key={ViewDocument.IsOpen ?? "new"} key={ViewDocument.Image ?? "doc"}
isOpen={ViewDocument.IsOpen}
closeModal={() => setDocumentView({ IsOpen: false, Image: null })} closeModal={() => setDocumentView({ IsOpen: false, Image: null })}
> >
<PreviewDocument imageUrl={ViewDocument.Image} /> <PreviewDocument imageUrl={ViewDocument.Image} />

View File

@ -1,17 +1,22 @@
import { api } from "../utils/axiosClient"; import { api } from "../utils/axiosClient";
const EmployeeRepository = { const EmployeeRepository = {
getAllEmployeeList:(showInactive)=>api.get(`api/employee/list?showInactive=${showInactive}`), getAllEmployeeList: (showInactive) =>
api.get(`api/employee/list?showInactive=${showInactive}`),
getEmployeeListByproject: (projectid) => getEmployeeListByproject: (projectid) =>
api.get(`/api/employee/list/${projectid}`), api.get(`/api/employee/list/${projectid}`),
searchEmployees: (query) => searchEmployees: (query) => api.get(`/api/employee/search/${query}`),
api.get(`/api/employee/search/${query}`), manageEmployee: (data) => api.post("/api/employee/manage", data),
manageEmployee: (data) =>
api.post("/api/employee/manage", data),
updateEmployee: (id, data) => api.put(`/users/${id}`, data), updateEmployee: (id, data) => api.put(`/users/${id}`, data),
// deleteEmployee: ( id ) => api.delete( `/users/${ id }` ), // deleteEmployee: ( id ) => api.delete( `/users/${ id }` ),
getEmployeeProfile: (id) => api.get(`/api/employee/profile/get/${id}`), getEmployeeProfile: (id) => api.get(`/api/employee/profile/get/${id}`),
deleteEmployee:(id)=>api.delete(`/api/employee/${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; export default EmployeeRepository;

View File

@ -44,7 +44,7 @@ axiosClient.interceptors.response.use(
const originalRequest = error.config; const originalRequest = error.config;
// Skip retry for public requests or already retried ones // 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); return Promise.reject(error);
} }