Refactor_Expenses #321
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState,useMemo } from "react";
|
||||||
import { FormProvider, useForm, Controller } from "react-hook-form";
|
import { FormProvider, useForm, Controller } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { defaultFilter, SearchSchema } from "./ExpenseSchema";
|
import { defaultFilter, SearchSchema } from "./ExpenseSchema";
|
||||||
@ -11,153 +11,186 @@ import { useExpenseStatus } from "../../hooks/masterHook/useMaster";
|
|||||||
import { useEmployeesAllOrByProjectId } from "../../hooks/useEmployees";
|
import { useEmployeesAllOrByProjectId } from "../../hooks/useEmployees";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { useExpenseFilter } from "../../hooks/useExpense";
|
||||||
|
import { ExpenseFilterSkeleton } from "./ExpenseSkeleton";
|
||||||
|
|
||||||
const ExpenseFilterPanel = ({ onApply }) => {
|
|
||||||
const selectedProjectId = useSelector(
|
|
||||||
(store) => store.localVariables.projectId
|
|
||||||
);
|
|
||||||
|
|
||||||
const { projectNames, loading: projectLoading } = useProjectName();
|
|
||||||
const { ExpenseStatus = [] } = useExpenseStatus();
|
const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||||
const { employees, loading: empLoading } = useEmployeesAllOrByProjectId(
|
const selectedProjectId = useSelector((store) => store.localVariables.projectId);
|
||||||
true,
|
const { data, isLoading } = useExpenseFilter();
|
||||||
selectedProjectId,
|
|
||||||
true
|
const groupByList = useMemo(() => [
|
||||||
);
|
{ id: "transactionDate", name: "Transaction Date" },
|
||||||
|
{ id: "status", name: "Status" },
|
||||||
|
{ id: "paidBy", name: "Paid By" },
|
||||||
|
{ id: "project", name: "Project" },
|
||||||
|
{ id: "paymentMode", name: "Payment Mode" },
|
||||||
|
{ id: "expensesType", name: "Expense Type" },
|
||||||
|
{id: "createdAt",name:"Submitted"}
|
||||||
|
], []);
|
||||||
|
|
||||||
|
const [selectedGroup, setSelectedGroup] = useState(groupByList[0]);
|
||||||
|
const [resetKey, setResetKey] = useState(0);
|
||||||
|
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
resolver: zodResolver(SearchSchema),
|
resolver: zodResolver(SearchSchema),
|
||||||
defaultValues: defaultFilter,
|
defaultValues: defaultFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { control, handleSubmit, setValue, reset } = methods;
|
const { control, register, handleSubmit, reset, watch } = methods;
|
||||||
|
const isTransactionDate = watch("isTransactionDate");
|
||||||
const isValidDate = (date) => date instanceof Date && !isNaN(date);
|
|
||||||
const setDateRange = ({ startDate, endDate }) => {
|
|
||||||
const parsedStart = new Date(startDate);
|
|
||||||
const parsedEnd = new Date(endDate);
|
|
||||||
setValue(
|
|
||||||
"startDate",
|
|
||||||
isValidDate(parsedStart) ? parsedStart.toISOString().split("T")[0] : null
|
|
||||||
);
|
|
||||||
setValue(
|
|
||||||
"endDate",
|
|
||||||
isValidDate(parsedEnd) ? parsedEnd.toISOString().split("T")[0] : null
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closePanel = () => {
|
const closePanel = () => {
|
||||||
document.querySelector(".offcanvas.show .btn-close")?.click();
|
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = (data) => {
|
const handleGroupChange = (e) => {
|
||||||
|
const group = groupByList.find((g) => g.id === e.target.value);
|
||||||
|
if (group) setSelectedGroup(group);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (formData) => {
|
||||||
onApply({
|
onApply({
|
||||||
...data,
|
...formData,
|
||||||
startDate: moment.utc(data.startDate, "DD-MM-YYYY").toISOString(),
|
startDate: moment.utc(formData.startDate, "DD-MM-YYYY").toISOString(),
|
||||||
endDate: moment.utc(data.endDate, "DD-MM-YYYY").toISOString(),
|
endDate: moment.utc(formData.endDate, "DD-MM-YYYY").toISOString(),
|
||||||
});
|
});
|
||||||
|
handleGroupBy(selectedGroup.id);
|
||||||
closePanel();
|
closePanel();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClear = () => {
|
const onClear = () => {
|
||||||
reset(defaultFilter);
|
reset(defaultFilter);
|
||||||
|
setResetKey((prev) => prev + 1);
|
||||||
|
setSelectedGroup(groupByList[0]);
|
||||||
onApply(defaultFilter);
|
onApply(defaultFilter);
|
||||||
|
handleGroupBy(groupByList[0].id);
|
||||||
closePanel();
|
closePanel();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <ExpenseFilterSkeleton />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
<div className="mb-2 text-start px-2">
|
||||||
<div className="mb-3 w-100">
|
<label htmlFor="groupBySelect" className="form-label">Group By :</label>
|
||||||
<label className="form-label">Created Date</label>
|
<select
|
||||||
{/* <DateRangePicker
|
id="groupBySelect"
|
||||||
onRangeChange={setDateRange}
|
className="form-select form-select-sm"
|
||||||
endDateMode="today"
|
value={selectedGroup?.id || ""}
|
||||||
DateDifference="6"
|
onChange={handleGroupChange}
|
||||||
dateFormat="DD-MM-YYYY"
|
>
|
||||||
/> */}
|
{groupByList.map((group) => (
|
||||||
|
<option key={group.id} value={group.id}>
|
||||||
|
{group.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DateRangePicker1
|
<FormProvider {...methods}>
|
||||||
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
||||||
startField="startDate"
|
<div className="mb-3 w-100">
|
||||||
endField="endDate"
|
<div className="d-flex align-items-center mb-2">
|
||||||
/>
|
<label className="form-label me-2">Choose Date:</label>
|
||||||
</div>
|
<div className="form-check form-switch m-0">
|
||||||
|
<input
|
||||||
<div className="row g-2">
|
className="form-check-input"
|
||||||
<SelectMultiple
|
type="checkbox"
|
||||||
name="projectIds"
|
id="switchOption1"
|
||||||
label="Projects : "
|
{...register("isTransactionDate")}
|
||||||
options={projectNames}
|
|
||||||
labelKey="name"
|
|
||||||
valueKey="id"
|
|
||||||
IsLoading={projectLoading}
|
|
||||||
/>
|
|
||||||
<SelectMultiple
|
|
||||||
name="createdByIds"
|
|
||||||
label="Submitted By : "
|
|
||||||
options={employees}
|
|
||||||
labelKey={(item) => `${item.firstName} ${item.lastName}`}
|
|
||||||
valueKey="id"
|
|
||||||
IsLoading={empLoading}
|
|
||||||
/>
|
|
||||||
<SelectMultiple
|
|
||||||
name="paidById"
|
|
||||||
label="Paid By : "
|
|
||||||
options={employees}
|
|
||||||
labelKey={(item) => `${item.firstName} ${item.lastName}`}
|
|
||||||
valueKey="id"
|
|
||||||
IsLoading={empLoading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label className="form-label ">Status : </label>
|
|
||||||
<div className="d-flex flex-wrap">
|
|
||||||
{ExpenseStatus.map((status) => (
|
|
||||||
<Controller
|
|
||||||
key={status.id}
|
|
||||||
control={control}
|
|
||||||
name="statusIds"
|
|
||||||
render={({ field: { value = [], onChange } }) => (
|
|
||||||
<div className="d-flex align-items-center me-3 mb-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
value={status.id}
|
|
||||||
checked={value.includes(status.id)}
|
|
||||||
onChange={(e) => {
|
|
||||||
const checked = e.target.checked;
|
|
||||||
onChange(
|
|
||||||
checked
|
|
||||||
? [...value, status.id]
|
|
||||||
: value.filter((v) => v !== status.id)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<label className="ms-2 mb-0">{status.displayName}</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
</div>
|
||||||
|
<label className="form-label mb-0 ms-2">
|
||||||
|
{isTransactionDate ? "Submitted": "Transaction" }
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DateRangePicker1
|
||||||
|
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
||||||
|
startField="startDate"
|
||||||
|
endField="endDate"
|
||||||
|
resetSignal={resetKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row g-2">
|
||||||
|
<SelectMultiple
|
||||||
|
name="projectIds"
|
||||||
|
label="Projects :"
|
||||||
|
options={data.projects}
|
||||||
|
labelKey="name"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
<SelectMultiple
|
||||||
|
name="createdByIds"
|
||||||
|
label="Submitted By :"
|
||||||
|
options={data.createdBy}
|
||||||
|
labelKey={(item) => item.name}
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
<SelectMultiple
|
||||||
|
name="paidById"
|
||||||
|
label="Paid By :"
|
||||||
|
options={data.paidBy}
|
||||||
|
labelKey={(item) => item.name}
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="form-label">Status :</label>
|
||||||
|
<div className="row flex-wrap">
|
||||||
|
{data?.status
|
||||||
|
?.slice()
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map((status) => (
|
||||||
|
<div className="col-6" key={status.id}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="statusIds"
|
||||||
|
render={({ field: { value = [], onChange } }) => (
|
||||||
|
<div className="d-flex align-items-center me-3 mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
value={status.id}
|
||||||
|
checked={value.includes(status.id)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const checked = e.target.checked;
|
||||||
|
onChange(
|
||||||
|
checked
|
||||||
|
? [...value, status.id]
|
||||||
|
: value.filter((v) => v !== status.id)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label className="ms-2 mb-0">{status.name}</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary btn-xs"
|
className="btn btn-secondary btn-xs"
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" className="btn btn-primary btn-xs">
|
<button type="submit" className="btn btn-primary btn-xs">
|
||||||
Apply
|
Apply
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ExpenseFilterPanel;
|
export default ExpenseFilterPanel;
|
@ -5,13 +5,13 @@ import { useExpenseContext } from "../../pages/Expense/ExpensePage";
|
|||||||
import { formatDate, formatUTCToLocalTime } from "../../utils/dateUtils";
|
import { formatDate, formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||||
import Pagination from "../common/Pagination";
|
import Pagination from "../common/Pagination";
|
||||||
import { APPROVE_EXPENSE } from "../../utils/constants";
|
import { APPROVE_EXPENSE } from "../../utils/constants";
|
||||||
import { getColorNameFromHex } from "../../utils/appUtils";
|
import { getColorNameFromHex, useDebounce } from "../../utils/appUtils";
|
||||||
import { ExpenseTableSkeleton } from "./ExpenseSkeleton";
|
import { ExpenseTableSkeleton } from "./ExpenseSkeleton";
|
||||||
import ConfirmModal from "../common/ConfirmModal";
|
import ConfirmModal from "../common/ConfirmModal";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
|
const ExpenseList = ({ filters, groupBy = "transactionDate",searchText }) => {
|
||||||
const [deletingId, setDeletingId] = useState(null);
|
const [deletingId, setDeletingId] = useState(null);
|
||||||
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const { setViewExpense, setManageExpenseModal } = useExpenseContext();
|
const { setViewExpense, setManageExpenseModal } = useExpenseContext();
|
||||||
@ -19,12 +19,14 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
|
|||||||
const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE);
|
const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
|
const debouncedSearch = useDebounce(searchText, 500);
|
||||||
|
|
||||||
const { mutate: DeleteExpense, isPending } = useDeleteExpense();
|
const { mutate: DeleteExpense, isPending } = useDeleteExpense();
|
||||||
const { data, isLoading, isError, isInitialLoading, error } = useExpenseList(
|
const { data, isLoading, isError, isInitialLoading, error } = useExpenseList(
|
||||||
pageSize,
|
pageSize,
|
||||||
currentPage,
|
currentPage,
|
||||||
filters
|
filters,
|
||||||
|
debouncedSearch
|
||||||
);
|
);
|
||||||
|
|
||||||
const SelfId = useSelector(
|
const SelfId = useSelector(
|
||||||
@ -74,6 +76,9 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
|
|||||||
case "expensesType":
|
case "expensesType":
|
||||||
key = item.expensesType?.name || "Unknown Type";
|
key = item.expensesType?.name || "Unknown Type";
|
||||||
break;
|
break;
|
||||||
|
case "createdAt":
|
||||||
|
key = item.createdAt?.split("T")[0] || "Unknown Type";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
key = "Others";
|
key = "Others";
|
||||||
}
|
}
|
||||||
|
@ -8,69 +8,80 @@ const ALLOWED_TYPES = [
|
|||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
export const ExpenseSchema = (expenseTypes) => {
|
export const ExpenseSchema = (expenseTypes) => {
|
||||||
return z
|
return z
|
||||||
.object({
|
.object({
|
||||||
projectId: z.string().min(1, { message: "Project is required" }),
|
projectId: z.string().min(1, { message: "Project is required" }),
|
||||||
expensesTypeId: z.string().min(1, { message: "Expense type is required" }),
|
expensesTypeId: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Expense type is required" }),
|
||||||
paymentModeId: z.string().min(1, { message: "Payment mode is required" }),
|
paymentModeId: z.string().min(1, { message: "Payment mode is required" }),
|
||||||
paidById: z.string().min(1, { message: "Employee name is required" }),
|
paidById: z.string().min(1, { message: "Employee name is required" }),
|
||||||
transactionDate: z
|
transactionDate: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: "Date is required" })
|
.min(1, { message: "Date is required" })
|
||||||
.refine((val) => {
|
.refine(
|
||||||
const selected = new Date(val);
|
(val) => {
|
||||||
const today = new Date();
|
const selected = new Date(val);
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
// Set both to midnight to avoid time-related issues
|
// 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);
|
||||||
|
|
||||||
return selected <= today;
|
return selected <= today;
|
||||||
}, { message: "Future dates are not allowed" }),
|
},
|
||||||
|
{ message: "Future dates are not allowed" }
|
||||||
|
),
|
||||||
transactionId: z.string().optional(),
|
transactionId: z.string().optional(),
|
||||||
description: z.string().min(1, { message: "Description is required" }),
|
description: z.string().min(1, { message: "Description is required" }),
|
||||||
location: z.string().min(1, { message: "Location is required" }),
|
location: z.string().min(1, { message: "Location is required" }),
|
||||||
supplerName: z.string().min(1, { message: "Supplier name is required" }),
|
supplerName: z.string().min(1, { message: "Supplier name is required" }),
|
||||||
amount: z
|
amount: z.coerce
|
||||||
.coerce
|
.number({
|
||||||
.number({ invalid_type_error: "Amount is required and must be a number" })
|
invalid_type_error: "Amount is required and must be a number",
|
||||||
|
})
|
||||||
.min(1, "Amount must be Enter")
|
.min(1, "Amount must be Enter")
|
||||||
.refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), {
|
.refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), {
|
||||||
message: "Amount must have at most 2 decimal places",
|
message: "Amount must have at most 2 decimal places",
|
||||||
}),
|
}),
|
||||||
noOfPersons: z.coerce
|
noOfPersons: z.coerce.number().optional(),
|
||||||
.number()
|
|
||||||
.optional(),
|
|
||||||
billAttachments: z
|
billAttachments: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
fileName: z.string().min(1, { message: "Filename is required" }),
|
fileName: z.string().min(1, { message: "Filename is required" }),
|
||||||
base64Data: z.string().nullable(),
|
base64Data: z.string().nullable(),
|
||||||
contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), {
|
contentType: z
|
||||||
message: "Only PDF, PNG, JPG, or JPEG files are allowed",
|
.string()
|
||||||
}),
|
.refine((val) => ALLOWED_TYPES.includes(val), {
|
||||||
documentId:z.string().optional(),
|
message: "Only PDF, PNG, JPG, or JPEG files are allowed",
|
||||||
|
}),
|
||||||
|
documentId: z.string().optional(),
|
||||||
fileSize: z.number().max(MAX_FILE_SIZE, {
|
fileSize: z.number().max(MAX_FILE_SIZE, {
|
||||||
message: "File size must be less than or equal to 5MB",
|
message: "File size must be less than or equal to 5MB",
|
||||||
}),
|
}),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
isActive:z.boolean().default(true)
|
isActive: z.boolean().default(true),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.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(
|
||||||
(data) => {
|
(data) => {
|
||||||
return !data.projectId || (data.paidById && data.paidById.trim() !== "");
|
return (
|
||||||
|
!data.projectId || (data.paidById && data.paidById.trim() !== "")
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Please select who paid (employee)",
|
message: "Please select who paid (employee)",
|
||||||
path: ["paidById"],
|
path: ["paidById"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
const expenseType = expenseTypes.find((et) => et.id === data.expensesTypeId);
|
const expenseType = expenseTypes.find((et) => et.id === data.expensesTypeId);
|
||||||
if (expenseType?.noOfPersonsRequired && (!data.noOfPersons || data.noOfPersons < 1)) {
|
if (expenseType?.noOfPersonsRequired && (!data.noOfPersons || data.noOfPersons < 1)) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
@ -79,45 +90,70 @@ export const ExpenseSchema = (expenseTypes) => {
|
|||||||
path: ["noOfPersons"],
|
path: ["noOfPersons"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isEndProcess) {
|
||||||
|
if (!data.reimburseTransactionId || data.reimburseTransactionId.trim() === "") {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Reimburse Transaction ID is required",
|
||||||
|
path: ["reimburseTransactionId"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!data.reimburseDate) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Reimburse Date is required",
|
||||||
|
path: ["reimburseDate"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!data.reimburseById) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Reimburse By is required",
|
||||||
|
path: ["reimburseById"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExpense = {
|
export const defaultExpense = {
|
||||||
projectId: "",
|
projectId: "",
|
||||||
expensesTypeId: "",
|
expensesTypeId: "",
|
||||||
paymentModeId: "",
|
paymentModeId: "",
|
||||||
paidById: "",
|
paidById: "",
|
||||||
transactionDate: "",
|
transactionDate: "",
|
||||||
transactionId: "",
|
transactionId: "",
|
||||||
description: "",
|
description: "",
|
||||||
location: "",
|
location: "",
|
||||||
supplerName: "",
|
supplerName: "",
|
||||||
amount: "",
|
amount: "",
|
||||||
noOfPersons: "",
|
noOfPersons: "",
|
||||||
billAttachments: [],
|
billAttachments: [],
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ActionSchema = z.object({
|
export const ActionSchema = 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" }),
|
selectedStatus: z.string().min(1, { message: "Please select a status" }),
|
||||||
})
|
});
|
||||||
|
|
||||||
|
export const SearchSchema = z.object({
|
||||||
export const SearchSchema = z.object({
|
projectIds: z.array(z.string()).optional(),
|
||||||
projectIds: z.array(z.string()).optional(),
|
|
||||||
statusIds: z.array(z.string()).optional(),
|
statusIds: z.array(z.string()).optional(),
|
||||||
createdByIds: z.array(z.string()).optional(),
|
createdByIds: z.array(z.string()).optional(),
|
||||||
paidById: z.array(z.string()).optional(),
|
paidById: z.array(z.string()).optional(),
|
||||||
startDate: z.string().optional(),
|
startDate: z.string().optional(),
|
||||||
endDate: z.string().optional(),
|
endDate: z.string().optional(),
|
||||||
|
isTransactionDate: z.boolean().default(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const defaultFilter = {
|
export const defaultFilter = {
|
||||||
projectIds:[],
|
projectIds: [],
|
||||||
statusIds:[],
|
statusIds: [],
|
||||||
createdByIds:[],
|
createdByIds: [],
|
||||||
paidById:[],
|
paidById: [],
|
||||||
startDate:null,
|
isTransactionDate: true,
|
||||||
endDate:null
|
startDate: null,
|
||||||
}
|
endDate: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
@ -222,3 +222,62 @@ export const ExpenseTableSkeleton = ({ groups = 3, rowsPerGroup = 3 }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const ExpenseFilterSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="p-3 text-start">
|
||||||
|
{/* Created Date Label and Skeleton */}
|
||||||
|
<div className="mb-3 w-100">
|
||||||
|
<SkeletonLine height={14} width="120px" className="mb-1" />
|
||||||
|
<SkeletonLine height={36} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row g-2">
|
||||||
|
{/* Project Select */}
|
||||||
|
<div className="col-12 col-md-4 mb-3">
|
||||||
|
<SkeletonLine height={14} width="80px" className="mb-1" />
|
||||||
|
<SkeletonLine height={36} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submitted By Select */}
|
||||||
|
<div className="col-12 col-md-4 mb-3">
|
||||||
|
<SkeletonLine height={14} width="100px" className="mb-1" />
|
||||||
|
<SkeletonLine height={36} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Paid By Select */}
|
||||||
|
<div className="col-12 col-md-4 mb-3">
|
||||||
|
<SkeletonLine height={14} width="70px" className="mb-1" />
|
||||||
|
<SkeletonLine height={36} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Checkboxes */}
|
||||||
|
<div className="col-12 mb-3">
|
||||||
|
<SkeletonLine height={14} width="80px" className="mb-2" />
|
||||||
|
<div className="d-flex flex-wrap">
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<div className="d-flex align-items-center me-3 mb-2" key={i}>
|
||||||
|
<div
|
||||||
|
className="form-check-input bg-secondary me-2"
|
||||||
|
style={{
|
||||||
|
height: "16px",
|
||||||
|
width: "16px",
|
||||||
|
borderRadius: "3px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SkeletonLine height={14} width="60px" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
<div className="d-flex justify-content-end py-3 gap-2">
|
||||||
|
<SkeletonLine height={30} width="80px" className="rounded" />
|
||||||
|
<SkeletonLine height={30} width="80px" className="rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -16,6 +16,7 @@ import { 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";
|
||||||
|
|
||||||
const ViewExpense = ({ ExpenseId }) => {
|
const ViewExpense = ({ ExpenseId }) => {
|
||||||
const { data, isLoading, isError, error } = useExpense(ExpenseId);
|
const { data, isLoading, isError, error } = useExpense(ExpenseId);
|
||||||
@ -126,26 +127,32 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-4 mb-3 h-100">
|
<div className="row align-items-center mb-3">
|
||||||
<div className="d-flex ">
|
<div className="col-6">
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">
|
<div className="d-flex align-items-center">
|
||||||
Paid By :
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
</label>
|
Paid By:
|
||||||
<div className="text-muted">
|
</label>
|
||||||
{data.paidBy.firstName} {data.paidBy.lastName}
|
<div className="text-muted">
|
||||||
|
{data.paidBy.firstName} {data.paidBy.lastName}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 col-md-4 mb-3 h-100 text-start">
|
<div className="col-6">
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">Status :</label>
|
<div className="d-flex align-items-center">
|
||||||
<span
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
className={`badge bg-label-${
|
Status:
|
||||||
getColorNameFromHex(data?.status?.color) || "secondary"
|
</label>
|
||||||
}`}
|
<span
|
||||||
>
|
className={`badge bg-label-${
|
||||||
{data?.status?.displayName}
|
getColorNameFromHex(data?.status?.color) || "secondary"
|
||||||
</span>
|
}`}
|
||||||
|
>
|
||||||
|
{data?.status?.displayName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-4 mb-3 h-100">
|
<div className="col-12 col-md-4 mb-3 h-100">
|
||||||
@ -157,7 +164,7 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-auto mb-3 h-100">
|
<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">
|
||||||
Project :
|
Project :
|
||||||
@ -166,27 +173,88 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-auto mb-3 h-100">
|
<div className="row align-items-center mb-3">
|
||||||
<div className="d-flex">
|
<div className="col-12 col-md-auto">
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">
|
<div className="d-flex align-items-center">
|
||||||
Created By :
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
</label>
|
Created At:
|
||||||
<div className="text-muted">
|
</label>
|
||||||
{data?.createdBy?.firstName} {data?.createdBy?.lastName}
|
<div className="text-muted">
|
||||||
|
{formatUTCToLocalTime(data?.createdAt, true)}
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-auto mb-3 h-100">
|
{data.reviewedBy && (
|
||||||
<div className="d-flex">
|
<div className="col-12 col-md-auto mb-3 h-100">
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">
|
<div className="d-flex align-items-center">
|
||||||
Created At :
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
</label>
|
Reviewed By :
|
||||||
<div className="text-muted">
|
</label>
|
||||||
{formatUTCToLocalTime(data?.createdAt, true)}
|
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<Avatar
|
||||||
|
size="xs"
|
||||||
|
classAvatar="m-0"
|
||||||
|
firstName={data.reviewedBy?.firstName}
|
||||||
|
lastName={data.reviewedBy?.lastName}
|
||||||
|
/>
|
||||||
|
<span className="text-muted">
|
||||||
|
{`${data.reviewedBy?.firstName ?? ""} ${
|
||||||
|
data.reviewedBy?.lastName ?? ""
|
||||||
|
}`.trim() || "N/A"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<Avatar
|
||||||
|
size="xs"
|
||||||
|
classAvatar="m-0"
|
||||||
|
firstName={data.approvedBy?.firstName}
|
||||||
|
lastName={data.approvedBy?.lastName}
|
||||||
|
/>
|
||||||
|
<span className="text-muted">
|
||||||
|
{`${data.approvedBy?.firstName ?? ""} ${
|
||||||
|
data.approvedBy?.lastName ?? ""
|
||||||
|
}`.trim() || "N/A"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-start">
|
<div className="text-start">
|
||||||
@ -208,7 +276,7 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
if (type.includes("zip") || type.includes("rar"))
|
if (type.includes("zip") || type.includes("rar"))
|
||||||
return "bxs-file-archive";
|
return "bxs-file-archive";
|
||||||
|
|
||||||
return "bx bx-file"; // Default
|
return "bx bx-file";
|
||||||
};
|
};
|
||||||
|
|
||||||
const isImage = doc.contentType?.includes("image");
|
const isImage = doc.contentType?.includes("image");
|
||||||
@ -258,15 +326,20 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
|
|
||||||
{Array.isArray(data?.nextStatus) && data.nextStatus.length > 0 && (
|
{Array.isArray(data?.nextStatus) && data.nextStatus.length > 0 && (
|
||||||
<div className="col-12 mb-3 text-start">
|
<div className="col-12 mb-3 text-start">
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">Comment:</label>
|
{nextStatusWithPermission.length > 0 && (
|
||||||
|
<>
|
||||||
<textarea
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
className="form-control form-control-sm"
|
Comment:
|
||||||
{...register("comment")}
|
</label>
|
||||||
rows="2"
|
<textarea
|
||||||
/>
|
className="form-control form-control-sm"
|
||||||
{errors.comment && (
|
{...register("comment")}
|
||||||
<small className="danger-text">{errors.comment.message}</small>
|
rows="2"
|
||||||
|
/>
|
||||||
|
{errors.comment && (
|
||||||
|
<small className="danger-text">{errors.comment.message}</small>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<input type="hidden" {...register("selectedStatus")} />
|
<input type="hidden" {...register("selectedStatus")} />
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { useController, useFormContext } from "react-hook-form";
|
import { useController, useFormContext, useWatch } from "react-hook-form";
|
||||||
const DateRangePicker = ({
|
const DateRangePicker = ({
|
||||||
md,
|
md,
|
||||||
sm,
|
sm,
|
||||||
@ -67,13 +67,17 @@ const DateRangePicker = ({
|
|||||||
|
|
||||||
export default DateRangePicker;
|
export default DateRangePicker;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const DateRangePicker1 = ({
|
export const DateRangePicker1 = ({
|
||||||
startField = "startDate",
|
startField = "startDate",
|
||||||
endField = "endDate",
|
endField = "endDate",
|
||||||
label,
|
|
||||||
placeholder = "Select date range",
|
placeholder = "Select date range",
|
||||||
className = "",
|
className = "",
|
||||||
allowText = false,
|
allowText = false,
|
||||||
|
resetSignal, // <- NEW prop
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
@ -83,46 +87,72 @@ export const DateRangePicker1 = ({
|
|||||||
field: { ref },
|
field: { ref },
|
||||||
} = useController({ name: startField, control });
|
} = useController({ name: startField, control });
|
||||||
|
|
||||||
|
const applyDefaultDates = () => {
|
||||||
|
const today = new Date();
|
||||||
|
const past = new Date();
|
||||||
|
past.setDate(today.getDate() - 6);
|
||||||
|
|
||||||
|
const format = (d) => flatpickr.formatDate(d, "d-m-Y");
|
||||||
|
const formattedStart = format(past);
|
||||||
|
const formattedEnd = format(today);
|
||||||
|
|
||||||
|
setValue(startField, formattedStart, { shouldValidate: true });
|
||||||
|
setValue(endField, formattedEnd, { shouldValidate: true });
|
||||||
|
|
||||||
|
if (inputRef.current?._flatpickr) {
|
||||||
|
inputRef.current._flatpickr.setDate([past, today]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!inputRef.current || inputRef.current._flatpickr) return;
|
if (!inputRef.current || inputRef.current._flatpickr) return;
|
||||||
|
|
||||||
const defaultStart = getValues(startField);
|
|
||||||
const defaultEnd = getValues(endField);
|
|
||||||
|
|
||||||
const instance = flatpickr(inputRef.current, {
|
const instance = flatpickr(inputRef.current, {
|
||||||
mode: "range",
|
mode: "range",
|
||||||
dateFormat: "d-m-Y",
|
dateFormat: "d-m-Y",
|
||||||
allowInput: allowText,
|
allowInput: allowText,
|
||||||
defaultDate:
|
onChange: (selectedDates) => {
|
||||||
defaultStart && defaultEnd
|
|
||||||
? [
|
|
||||||
flatpickr.parseDate(defaultStart, "d-m-Y"),
|
|
||||||
flatpickr.parseDate(defaultEnd, "d-m-Y"),
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
onChange: (selectedDates, dateStr, fp) => {
|
|
||||||
if (selectedDates.length === 2) {
|
if (selectedDates.length === 2) {
|
||||||
const [start, end] = selectedDates;
|
const [start, end] = selectedDates;
|
||||||
const format = (d) => flatpickr.formatDate(d, "d-m-Y");
|
const format = (d) => flatpickr.formatDate(d, "d-m-Y");
|
||||||
setValue(startField, format(start));
|
setValue(startField, format(start), { shouldValidate: true });
|
||||||
setValue(endField, format(end));
|
setValue(endField, format(end), { shouldValidate: true });
|
||||||
} else {
|
} else {
|
||||||
setValue(startField, "");
|
setValue(startField, "", { shouldValidate: true });
|
||||||
setValue(endField, "");
|
setValue(endField, "", { shouldValidate: true });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
...rest,
|
...rest,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Apply default if empty
|
||||||
|
const currentStart = getValues(startField);
|
||||||
|
const currentEnd = getValues(endField);
|
||||||
|
if (!currentStart && !currentEnd) {
|
||||||
|
applyDefaultDates();
|
||||||
|
} else if (currentStart && currentEnd) {
|
||||||
|
instance.setDate([
|
||||||
|
flatpickr.parseDate(currentStart, "d-m-Y"),
|
||||||
|
flatpickr.parseDate(currentEnd, "d-m-Y"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return () => instance.destroy();
|
return () => instance.destroy();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Reapply default range on resetSignal change
|
||||||
|
useEffect(() => {
|
||||||
|
if (resetSignal !== undefined) {
|
||||||
|
applyDefaultDates();
|
||||||
|
}
|
||||||
|
}, [resetSignal]);
|
||||||
|
|
||||||
const start = getValues(startField);
|
const start = getValues(startField);
|
||||||
const end = getValues(endField);
|
const end = getValues(endField);
|
||||||
const formattedValue = start && end ? `${start} To ${end}` : "";
|
const formattedValue = start && end ? `${start} To ${end}` : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={` position-relative ${className}`}>
|
<div className={`position-relative ${className}`}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
|
@ -5,17 +5,41 @@ import { queryClient } from "../layouts/AuthLayout";
|
|||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
// -------------------Query------------------------------------------------------
|
// -------------------Query------------------------------------------------------
|
||||||
export const useExpenseList = (pageSize, pageNumber, filter) => {
|
const cleanFilter = (filter) => {
|
||||||
|
const cleaned = { ...filter };
|
||||||
|
|
||||||
|
["projectIds", "statusIds", "createdByIds", "paidById"].forEach((key) => {
|
||||||
|
if (Array.isArray(cleaned[key]) && cleaned[key].length === 0) {
|
||||||
|
delete cleaned[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cleaned.startDate) delete cleaned.startDate;
|
||||||
|
if (!cleaned.endDate) delete cleaned.endDate;
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useExpenseList = (pageSize, pageNumber, filter, searchString = '') => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["Expenses", pageNumber, pageSize, filter],
|
queryKey: ["Expenses", pageNumber, pageSize, filter, searchString],
|
||||||
queryFn: async () =>
|
queryFn: async () => {
|
||||||
await ExpenseRepository.GetExpenseList(pageSize, pageNumber, filter).then(
|
const cleanedFilter = cleanFilter(filter);
|
||||||
(res) => res.data
|
const response = await ExpenseRepository.GetExpenseList(
|
||||||
),
|
pageSize,
|
||||||
|
pageNumber,
|
||||||
|
cleanedFilter,
|
||||||
|
searchString
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const useExpense = (ExpenseId) => {
|
export const useExpense = (ExpenseId) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["Expense", ExpenseId],
|
queryKey: ["Expense", ExpenseId],
|
||||||
@ -27,6 +51,17 @@ export const useExpense = (ExpenseId) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useExpenseFilter = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["ExpenseFilter"],
|
||||||
|
queryFn: async () =>
|
||||||
|
await ExpenseRepository.GetExpenseFilter().then(
|
||||||
|
(res) => res.data
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------Mutation---------------------------------------------
|
// ---------------------------Mutation---------------------------------------------
|
||||||
|
|
||||||
export const useCreateExpnse = (onSuccessCallBack) => {
|
export const useCreateExpnse = (onSuccessCallBack) => {
|
||||||
|
@ -53,7 +53,9 @@ export const useExpenseContext = () => {
|
|||||||
const ExpensePage = () => {
|
const ExpensePage = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [filters, setFilter] = useState();
|
const [filters, setFilter] = useState();
|
||||||
|
const [groupBy, setGropBy] = useState("transactionDate");
|
||||||
const IsCreatedAble = useHasUserPermission(CREATE_EXEPENSE);
|
const IsCreatedAble = useHasUserPermission(CREATE_EXEPENSE);
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
const selectedProjectId = useSelector(
|
const selectedProjectId = useSelector(
|
||||||
(store) => store.localVariables.projectId
|
(store) => store.localVariables.projectId
|
||||||
);
|
);
|
||||||
@ -82,13 +84,8 @@ const ExpensePage = () => {
|
|||||||
defaultValues: defaultFilter,
|
defaultValues: defaultFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const { setOffcanvasContent, setShowTrigger } = useFab();
|
const { setOffcanvasContent, setShowTrigger } = useFab();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const clearFilter = () => {
|
const clearFilter = () => {
|
||||||
setFilter({
|
setFilter({
|
||||||
projectIds: [],
|
projectIds: [],
|
||||||
@ -106,7 +103,12 @@ const ExpensePage = () => {
|
|||||||
|
|
||||||
setOffcanvasContent(
|
setOffcanvasContent(
|
||||||
"Expense Filters",
|
"Expense Filters",
|
||||||
<ExpenseFilterPanel onApply={(data) => setFilter(data)} />
|
<ExpenseFilterPanel
|
||||||
|
onApply={(data) => {
|
||||||
|
setFilter(data);
|
||||||
|
}}
|
||||||
|
handleGroupBy={(groupId) => setGropBy(groupId)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
return () => {
|
return () => {
|
||||||
setOffcanvasContent("", null);
|
setOffcanvasContent("", null);
|
||||||
@ -125,20 +127,32 @@ const ExpensePage = () => {
|
|||||||
/>
|
/>
|
||||||
{IsViewAll || IsViewSelf ? (
|
{IsViewAll || IsViewSelf ? (
|
||||||
<>
|
<>
|
||||||
<div className="card my-1 text-start px-0">
|
<div className="card my-1 px-0">
|
||||||
<div className="card-body py-1 px-1">
|
<div className="card-body py-2 px-3">
|
||||||
<div className="row">
|
<div className="row align-items-center">
|
||||||
<div className="col-5 col-sm-4 d-flex aligin-items-center"></div>
|
<div className="col-12 col-sm-6 col-md-4">
|
||||||
<div className="col-7 col-sm-8 text-end gap-2">
|
<div className="input-group input-group-sm">
|
||||||
|
<span className="input-group-text" id="search-label">
|
||||||
|
Search
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Search Expense"
|
||||||
|
aria-label="Search"
|
||||||
|
aria-describedby="search-label"
|
||||||
|
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">
|
||||||
{IsCreatedAble && (
|
{IsCreatedAble && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="tooltip"
|
|
||||||
data-bs-offset="0,8"
|
|
||||||
data-bs-placement="top"
|
|
||||||
data-bs-custom-class="tooltip"
|
|
||||||
title="Add New Expense"
|
title="Add New Expense"
|
||||||
className={`p-1 me-2 bg-primary rounded-circle `}
|
className="p-1 me-2 bg-primary rounded-circle"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setManageExpenseModal({
|
setManageExpenseModal({
|
||||||
IsOpen: true,
|
IsOpen: true,
|
||||||
@ -153,7 +167,8 @@ const ExpensePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ExpenseList filters={filters} />
|
|
||||||
|
<ExpenseList filters={filters} groupBy={groupBy} searchText={searchText}/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="card text-center py-1">
|
<div className="card text-center py-1">
|
||||||
|
@ -2,12 +2,12 @@ import { api } from "../utils/axiosClient";
|
|||||||
|
|
||||||
|
|
||||||
const ExpenseRepository = {
|
const ExpenseRepository = {
|
||||||
GetExpenseList: ( pageSize, pageNumber, filter ) => {
|
GetExpenseList: ( pageSize, pageNumber, filter,searchString ) => {
|
||||||
const payloadJsonString = JSON.stringify(filter);
|
const payloadJsonString = JSON.stringify(filter);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return api.get(`/api/expense/list?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}`);
|
return api.get(`/api/expense/list?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
GetExpenseDetails:(id)=>api.get(`/api/Expense/details/${id}`),
|
GetExpenseDetails:(id)=>api.get(`/api/Expense/details/${id}`),
|
||||||
@ -16,7 +16,8 @@ const ExpenseRepository = {
|
|||||||
DeleteExpense:(id)=>api.delete(`/api/Expense/delete/${id}`),
|
DeleteExpense:(id)=>api.delete(`/api/Expense/delete/${id}`),
|
||||||
|
|
||||||
ActionOnExpense:(data)=>api.post('/api/expense/action',data),
|
ActionOnExpense:(data)=>api.post('/api/expense/action',data),
|
||||||
|
|
||||||
|
GetExpenseFilter:()=>api.get('/api/Expense/filter')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const formatFileSize=(bytes)=> {
|
export const formatFileSize=(bytes)=> {
|
||||||
if (bytes < 1024) return bytes + " B";
|
if (bytes < 1024) return bytes + " B";
|
||||||
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
|
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
|
||||||
@ -34,3 +36,14 @@ export const getColorNameFromHex = (hex) => {
|
|||||||
|
|
||||||
return null; //
|
return null; //
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useDebounce = (value, delay = 500) => {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user