Compare commits
2 Commits
6fbc0411db
...
e31bb7c487
| Author | SHA1 | Date | |
|---|---|---|---|
| e31bb7c487 | |||
| b864ed0529 |
@ -1,111 +1,152 @@
|
|||||||
// components/Expense/ExpenseFilterPanel.jsx
|
import React, { useEffect, useState,useMemo } from "react";
|
||||||
import React, { useEffect } 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";
|
||||||
|
|
||||||
import DateRangePicker from "../common/DateRangePicker";
|
import DateRangePicker, { DateRangePicker1 } from "../common/DateRangePicker";
|
||||||
import SelectMultiple from "../common/SelectMultiple";
|
import SelectMultiple from "../common/SelectMultiple";
|
||||||
|
|
||||||
import { useProjectName } from "../../hooks/useProjects";
|
import { useProjectName } from "../../hooks/useProjects";
|
||||||
import { useExpenseStatus } from "../../hooks/masterHook/useMaster";
|
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 { 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) => {
|
||||||
onApply(data);
|
const group = groupByList.find((g) => g.id === e.target.value);
|
||||||
|
if (group) setSelectedGroup(group);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (formData) => {
|
||||||
|
onApply({
|
||||||
|
...formData,
|
||||||
|
startDate: moment.utc(formData.startDate, "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 (
|
||||||
|
<>
|
||||||
|
<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">
|
||||||
<div className="mb-3 w-100">
|
<div className="mb-3 w-100">
|
||||||
<label className="form-label">Created Date</label>
|
<div className="d-flex align-items-center mb-2">
|
||||||
<DateRangePicker
|
<label className="form-label me-2">Choose Date:</label>
|
||||||
onRangeChange={setDateRange}
|
<div className="form-check form-switch m-0">
|
||||||
endDateMode="today"
|
<input
|
||||||
DateDifference="6"
|
className="form-check-input"
|
||||||
dateFormat="DD-MM-YYYY"
|
type="checkbox"
|
||||||
|
id="switchOption1"
|
||||||
|
{...register("isTransactionDate")}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
|
||||||
<div className="row g-2">
|
<div className="row g-2">
|
||||||
<SelectMultiple
|
<SelectMultiple
|
||||||
name="projectIds"
|
name="projectIds"
|
||||||
label="Projects : "
|
label="Projects :"
|
||||||
options={projectNames}
|
options={data.projects}
|
||||||
labelKey="name"
|
labelKey="name"
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
IsLoading={projectLoading}
|
|
||||||
/>
|
/>
|
||||||
<SelectMultiple
|
<SelectMultiple
|
||||||
name="createdByIds"
|
name="createdByIds"
|
||||||
label="Submitted By : "
|
label="Submitted By :"
|
||||||
options={employees}
|
options={data.createdBy}
|
||||||
labelKey={(item) => `${item.firstName} ${item.lastName}`}
|
labelKey={(item) => item.name}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
IsLoading={empLoading}
|
|
||||||
/>
|
/>
|
||||||
<SelectMultiple
|
<SelectMultiple
|
||||||
name="paidById"
|
name="paidById"
|
||||||
label="Paid By : "
|
label="Paid By :"
|
||||||
options={employees}
|
options={data.paidBy}
|
||||||
labelKey={(item) => `${item.firstName} ${item.lastName}`}
|
labelKey={(item) => item.name}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
IsLoading={empLoading}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="form-label ">Status : </label>
|
<label className="form-label">Status :</label>
|
||||||
<div className="d-flex flex-wrap">
|
<div className="row flex-wrap">
|
||||||
{ExpenseStatus.map((status) => (
|
{data?.status
|
||||||
|
?.slice()
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map((status) => (
|
||||||
|
<div className="col-6" key={status.id}>
|
||||||
<Controller
|
<Controller
|
||||||
key={status.id}
|
|
||||||
control={control}
|
control={control}
|
||||||
name="statusIds"
|
name="statusIds"
|
||||||
render={({ field: { value = [], onChange } }) => (
|
render={({ field: { value = [], onChange } }) => (
|
||||||
@ -124,10 +165,11 @@ const ExpenseFilterPanel = ({ onApply }) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<label className="ms-2 mb-0">{status.displayName}</label>
|
<label className="ms-2 mb-0">{status.name}</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -147,6 +189,7 @@ const ExpenseFilterPanel = ({ onApply }) => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -5,28 +5,28 @@ 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",searchText }) => {
|
||||||
|
|
||||||
const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
|
|
||||||
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();
|
||||||
const IsExpenseEditable = useHasUserPermission();
|
const IsExpenseEditable = useHasUserPermission();
|
||||||
const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE);
|
const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const pageSize = 10;
|
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(
|
||||||
@ -63,7 +63,9 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
|
|||||||
key = item.status?.displayName || "Unknown";
|
key = item.status?.displayName || "Unknown";
|
||||||
break;
|
break;
|
||||||
case "paidBy":
|
case "paidBy":
|
||||||
key = `${item.paidBy?.firstName ?? ""} ${item.paidBy?.lastName ?? ""}`.trim();
|
key = `${item.paidBy?.firstName ?? ""} ${
|
||||||
|
item.paidBy?.lastName ?? ""
|
||||||
|
}`.trim();
|
||||||
break;
|
break;
|
||||||
case "project":
|
case "project":
|
||||||
key = item.project?.name || "Unknown Project";
|
key = item.project?.name || "Unknown Project";
|
||||||
@ -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";
|
||||||
}
|
}
|
||||||
@ -88,20 +93,21 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
|
|||||||
key: "expensesType",
|
key: "expensesType",
|
||||||
label: "Expense Type",
|
label: "Expense Type",
|
||||||
getValue: (e) => e.expensesType?.name || "N/A",
|
getValue: (e) => e.expensesType?.name || "N/A",
|
||||||
align:"text-start",
|
align: "text-start",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "paymentMode",
|
key: "paymentMode",
|
||||||
label: "Payment Mode",
|
label: "Payment Mode",
|
||||||
getValue: (e) => e.paymentMode?.name || "N/A",
|
getValue: (e) => e.paymentMode?.name || "N/A",
|
||||||
align:"text-start"
|
align: "text-start",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "paidBy",
|
key: "paidBy",
|
||||||
label: "Paid By",
|
label: "Paid By",
|
||||||
align:"text-start",
|
align: "text-start",
|
||||||
getValue: (e) =>
|
getValue: (e) =>
|
||||||
`${e.paidBy?.firstName ?? ""} ${e.paidBy?.lastName ?? ""}`.trim() || "N/A",
|
`${e.paidBy?.firstName ?? ""} ${e.paidBy?.lastName ?? ""}`.trim() ||
|
||||||
|
"N/A",
|
||||||
customRender: (e) => (
|
customRender: (e) => (
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
@ -111,16 +117,18 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
|
|||||||
lastName={e.paidBy?.lastName}
|
lastName={e.paidBy?.lastName}
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{`${e.paidBy?.firstName ?? ""} ${e.paidBy?.lastName ?? ""}`.trim() || "N/A"}
|
{`${e.paidBy?.firstName ?? ""} ${
|
||||||
|
e.paidBy?.lastName ?? ""
|
||||||
|
}`.trim() || "N/A"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "submitted",
|
key: "submitted",
|
||||||
label: "Submitted",
|
label: "Submitted",
|
||||||
getValue: (e) => formatUTCToLocalTime(e?.createdAt),
|
getValue: (e) => formatUTCToLocalTime(e?.createdAt),
|
||||||
isAlwaysVisible: true
|
isAlwaysVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "amount",
|
key: "amount",
|
||||||
@ -131,24 +139,30 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
|
|||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
isAlwaysVisible: true,
|
isAlwaysVisible: true,
|
||||||
align: "text-end"
|
align: "text-end",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "status",
|
key: "status",
|
||||||
label: "Status",
|
label: "Status",
|
||||||
align:"text-center",
|
align: "text-center",
|
||||||
getValue: (e) => (
|
getValue: (e) => (
|
||||||
<span className={`badge bg-label-${getColorNameFromHex(e?.status?.color) || "secondary"}`}>
|
<span
|
||||||
|
className={`badge bg-label-${
|
||||||
|
getColorNameFromHex(e?.status?.color) || "secondary"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{e.status?.name || "Unknown"}
|
{e.status?.name || "Unknown"}
|
||||||
</span>
|
</span>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (isInitialLoading) return <ExpenseTableSkeleton />;
|
if (isInitialLoading) return <ExpenseTableSkeleton />;
|
||||||
if (isError) return <div>{error}</div>;
|
if (isError) return <div>{error}</div>;
|
||||||
|
|
||||||
const grouped = groupBy ? groupByField(data?.data ?? [], groupBy) : { All: data?.data ?? [] };
|
const grouped = groupBy
|
||||||
|
? groupByField(data?.data ?? [], groupBy)
|
||||||
|
: { All: data?.data ?? [] };
|
||||||
const IsGroupedByDate = ["transactionDate", "createdAt"].includes(groupBy);
|
const IsGroupedByDate = ["transactionDate", "createdAt"].includes(groupBy);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -160,7 +174,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
style={{
|
style={{
|
||||||
display: "block",
|
display: "block",
|
||||||
backgroundColor: "rgba(0,0,0,0.5)"
|
backgroundColor: "rgba(0,0,0,0.5)",
|
||||||
}}
|
}}
|
||||||
aria-hidden="false"
|
aria-hidden="false"
|
||||||
>
|
>
|
||||||
@ -177,8 +191,11 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-datatable table-responsive">
|
<div
|
||||||
<div className="dataTables_wrapper no-footer px-2">
|
className="card-datatable table-responsive "
|
||||||
|
id="horizontal-example"
|
||||||
|
>
|
||||||
|
<div className="dataTables_wrapper no-footer px-2 ">
|
||||||
<table className="table border-top dataTable text-nowrap">
|
<table className="table border-top dataTable text-nowrap">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -194,15 +211,22 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
|
|||||||
</th>
|
</th>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<th>Action</th>
|
<th className="sticky-action-column bg-white text-center">
|
||||||
|
Action
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{Object.entries(grouped).map(([group, expenses]) => (
|
{Object.keys(grouped).length > 0 ? (
|
||||||
|
Object.entries(grouped).map(([group, expenses]) => (
|
||||||
<React.Fragment key={group}>
|
<React.Fragment key={group}>
|
||||||
<tr className="tr-group text-dark">
|
<tr className="tr-group text-dark">
|
||||||
<td colSpan={8} className="text-start">
|
<td colSpan={8} className="text-start">
|
||||||
<strong>{IsGroupedByDate ? formatUTCToLocalTime(group) : group}</strong>
|
<strong>
|
||||||
|
{IsGroupedByDate
|
||||||
|
? formatUTCToLocalTime(group)
|
||||||
|
: group}
|
||||||
|
</strong>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{expenses.map((expense) => (
|
{expenses.map((expense) => (
|
||||||
@ -210,19 +234,25 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
|
|||||||
{expenseColumns.map(
|
{expenseColumns.map(
|
||||||
(col) =>
|
(col) =>
|
||||||
(col.isAlwaysVisible || groupBy !== col.key) && (
|
(col.isAlwaysVisible || groupBy !== col.key) && (
|
||||||
<td key={col.key} className={`d-table-cell ${col.align ?? ""}`}>
|
<td
|
||||||
|
key={col.key}
|
||||||
|
className={`d-table-cell ${col.align ?? ""}`}
|
||||||
|
>
|
||||||
{col.customRender
|
{col.customRender
|
||||||
? col.customRender(expense)
|
? col.customRender(expense)
|
||||||
: col.getValue(expense)}
|
: col.getValue(expense)}
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<td>
|
<td className="sticky-action-column bg-white">
|
||||||
<div className="d-flex justify-content-center gap-2">
|
<div className="d-flex justify-content-center gap-2">
|
||||||
<i
|
<i
|
||||||
className="bx bx-show text-primary cursor-pointer"
|
className="bx bx-show text-primary cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setViewExpense({ expenseId: expense.id, view: true })
|
setViewExpense({
|
||||||
|
expenseId: expense.id,
|
||||||
|
view: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
></i>
|
></i>
|
||||||
{(expense.status.name === "Draft" ||
|
{(expense.status.name === "Draft" ||
|
||||||
@ -233,13 +263,13 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
setManageExpenseModal({
|
setManageExpenseModal({
|
||||||
IsOpen: true,
|
IsOpen: true,
|
||||||
expenseId: expense.id
|
expenseId: expense.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
></i>
|
></i>
|
||||||
)}
|
)}
|
||||||
{expense.status.name === "Draft" &&
|
{expense.status.name === "Draft" &&
|
||||||
expense?.createdBy?.id === SelfId && (
|
expense.createdBy.id === SelfId && (
|
||||||
<i
|
<i
|
||||||
className="bx bx-trash text-danger cursor-pointer"
|
className="bx bx-trash text-danger cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -253,8 +283,8 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
|
|||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))
|
||||||
{data?.data?.length === 0 && (
|
) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={8} className="text-center py-4">
|
<td colSpan={8} className="text-center py-4">
|
||||||
No Expense Found
|
No Expense Found
|
||||||
@ -278,4 +308,3 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ExpenseList;
|
export default ExpenseList;
|
||||||
|
|
||||||
|
|||||||
@ -8,18 +8,20 @@ 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(
|
||||||
|
(val) => {
|
||||||
const selected = new Date(val);
|
const selected = new Date(val);
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
|
||||||
@ -28,42 +30,51 @@ export const ExpenseSchema = (expenseTypes) => {
|
|||||||
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
|
||||||
|
.string()
|
||||||
|
.refine((val) => ALLOWED_TYPES.includes(val), {
|
||||||
message: "Only PDF, PNG, JPG, or JPEG files are allowed",
|
message: "Only PDF, PNG, JPG, or JPEG files are allowed",
|
||||||
}),
|
}),
|
||||||
documentId:z.string().optional(),
|
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)",
|
||||||
@ -79,6 +90,30 @@ 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"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -95,29 +130,30 @@ export const defaultExpense = {
|
|||||||
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import {
|
|||||||
} from "../../hooks/useExpense";
|
} from "../../hooks/useExpense";
|
||||||
import ExpenseSkeleton from "./ExpenseSkeleton";
|
import ExpenseSkeleton from "./ExpenseSkeleton";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import DatePicker from "../common/DatePicker";
|
||||||
|
|
||||||
const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||||
const {
|
const {
|
||||||
@ -44,6 +45,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
watch,
|
watch,
|
||||||
setValue,
|
setValue,
|
||||||
reset,
|
reset,
|
||||||
|
control,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
@ -171,7 +173,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
const onSubmit = (fromdata) => {
|
const onSubmit = (fromdata) => {
|
||||||
let payload = {...fromdata,transactionDate: moment.utc(fromdata.transactionDate, 'YYYY-MM-DD').toISOString()}
|
let payload = {...fromdata,transactionDate: moment.utc(fromdata.transactionDate, 'DD-MM-YYYY').toISOString()}
|
||||||
if (expenseToEdit) {
|
if (expenseToEdit) {
|
||||||
const editPayload = { ...payload, id: data.id };
|
const editPayload = { ...payload, id: data.id };
|
||||||
ExpenseUpdate({ id: data.id, payload: editPayload });
|
ExpenseUpdate({ id: data.id, payload: editPayload });
|
||||||
@ -321,13 +323,18 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
<label for="transactionDate" className="form-label ">
|
<label for="transactionDate" className="form-label ">
|
||||||
Transaction Date
|
Transaction Date
|
||||||
</label>
|
</label>
|
||||||
<input
|
{/* <input
|
||||||
type="date"
|
type="date"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
placeholder="YYYY-MM-DD"
|
placeholder="YYYY-MM-DD"
|
||||||
id="flatpickr-date"
|
id="flatpickr-date"
|
||||||
{...register("transactionDate")}
|
{...register("transactionDate")}
|
||||||
|
/> */}
|
||||||
|
<DatePicker
|
||||||
|
name="transactionDate"
|
||||||
|
control={control}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{errors.transactionDate && (
|
{errors.transactionDate && (
|
||||||
<small className="danger-text">
|
<small className="danger-text">
|
||||||
{errors.transactionDate.message}
|
{errors.transactionDate.message}
|
||||||
|
|||||||
@ -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,10 +127,11 @@ 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">
|
||||||
|
<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">
|
||||||
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}
|
||||||
@ -137,8 +139,11 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
</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">
|
||||||
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
|
Status:
|
||||||
|
</label>
|
||||||
<span
|
<span
|
||||||
className={`badge bg-label-${
|
className={`badge bg-label-${
|
||||||
getColorNameFromHex(data?.status?.color) || "secondary"
|
getColorNameFromHex(data?.status?.color) || "secondary"
|
||||||
@ -147,6 +152,8 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
{data?.status?.displayName}
|
{data?.status?.displayName}
|
||||||
</span>
|
</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">
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
@ -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">
|
||||||
|
<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">
|
||||||
Created By :
|
Created At:
|
||||||
</label>
|
|
||||||
<div className="text-muted">
|
|
||||||
{data?.createdBy?.firstName} {data?.createdBy?.lastName}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 col-md-auto mb-3 h-100">
|
|
||||||
<div className="d-flex">
|
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">
|
|
||||||
Created At :
|
|
||||||
</label>
|
</label>
|
||||||
<div className="text-muted">
|
<div className="text-muted">
|
||||||
{formatUTCToLocalTime(data?.createdAt, true)}
|
{formatUTCToLocalTime(data?.createdAt, true)}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
{data.reviewedBy && (
|
||||||
|
<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">
|
||||||
|
Reviewed By :
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
{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,8 +326,11 @@ 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 && (
|
||||||
|
<>
|
||||||
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
|
Comment:
|
||||||
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
{...register("comment")}
|
{...register("comment")}
|
||||||
@ -268,6 +339,8 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
{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")} />
|
<input type="hidden" {...register("selectedStatus")} />
|
||||||
|
|
||||||
|
|||||||
@ -1,39 +1,69 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useController } from "react-hook-form";
|
||||||
|
|
||||||
const DatePicker = ({ onDateChange }) => {
|
|
||||||
|
const DatePicker = ({
|
||||||
|
name,
|
||||||
|
control,
|
||||||
|
placeholder = "DD-MM-YYYY",
|
||||||
|
className = "",
|
||||||
|
allowText = false,
|
||||||
|
maxDate=new Date(),
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
const fp = flatpickr(inputRef.current, {
|
field: { onChange, value, ref }
|
||||||
dateFormat: "Y-m-d",
|
} = useController({
|
||||||
defaultDate: new Date(),
|
name,
|
||||||
onChange: (selectedDates, dateStr) => {
|
control
|
||||||
if (onDateChange) {
|
|
||||||
onDateChange(dateStr); // Pass selected date to parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
useEffect(() => {
|
||||||
// Cleanup flatpickr instance
|
if (inputRef.current) {
|
||||||
fp.destroy();
|
flatpickr(inputRef.current, {
|
||||||
};
|
dateFormat: "d-m-Y",
|
||||||
}, [onDateChange]);
|
allowInput: allowText,
|
||||||
|
defaultDate: value
|
||||||
|
? flatpickr.parseDate(value, "Y-m-d")
|
||||||
|
: null,
|
||||||
|
maxDate:maxDate,
|
||||||
|
onChange: function (selectedDates, dateStr) {
|
||||||
|
onChange(dateStr);
|
||||||
|
},
|
||||||
|
...rest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [inputRef]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mt-3">
|
<div className={` position-relative ${className}`}>
|
||||||
<div className="mb-3">
|
|
||||||
{/* <label htmlFor="flatpickr-single" className="form-label">
|
|
||||||
Select Date
|
|
||||||
</label> */}
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="flatpickr-single"
|
className="form-control form-control-sm "
|
||||||
className="form-control"
|
placeholder={placeholder}
|
||||||
placeholder="YYYY-MM-DD"
|
defaultValue={
|
||||||
ref={inputRef}
|
value ? flatpickr.formatDate(flatpickr.parseDate(value, "Y-m-d"), "d-m-Y") : ""
|
||||||
|
}
|
||||||
|
ref={(el) => {
|
||||||
|
inputRef.current = el;
|
||||||
|
ref(el);
|
||||||
|
}}
|
||||||
|
readOnly={!allowText}
|
||||||
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
<span
|
||||||
|
className="position-absolute top-50 end-0 pe-1 translate-middle-y cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
if (inputRef.current && inputRef.current._flatpickr) {
|
||||||
|
inputRef.current._flatpickr.open();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="bx bx-calendar bx-sm fs-5 text-muted"></i>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { useController, useFormContext, useWatch } from "react-hook-form";
|
||||||
const DateRangePicker = ({
|
const DateRangePicker = ({
|
||||||
md,
|
md,
|
||||||
sm,
|
sm,
|
||||||
@ -48,7 +48,7 @@ const DateRangePicker = ({
|
|||||||
}, [onRangeChange, DateDifference, endDateMode]);
|
}, [onRangeChange, DateDifference, endDateMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`col-${sm} col-sm-${md} px-1 position-relative`}>
|
<div className={`col-${sm} col-sm-${md} px-1 position-relative`}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-sm ps-2 pe-5 me-4"
|
className="form-control form-control-sm ps-2 pe-5 me-4"
|
||||||
@ -59,11 +59,118 @@ const DateRangePicker = ({
|
|||||||
|
|
||||||
<i
|
<i
|
||||||
className="bx bx-calendar calendar-icon cursor-pointer position-absolute top-50 translate-middle-y "
|
className="bx bx-calendar calendar-icon cursor-pointer position-absolute top-50 translate-middle-y "
|
||||||
style={{right:"12px"}}
|
style={{ right: "12px" }}
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DateRangePicker;
|
export default DateRangePicker;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const DateRangePicker1 = ({
|
||||||
|
startField = "startDate",
|
||||||
|
endField = "endDate",
|
||||||
|
placeholder = "Select date range",
|
||||||
|
className = "",
|
||||||
|
allowText = false,
|
||||||
|
resetSignal, // <- NEW prop
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
const { control, setValue, getValues } = useFormContext();
|
||||||
|
|
||||||
|
const {
|
||||||
|
field: { ref },
|
||||||
|
} = 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(() => {
|
||||||
|
if (!inputRef.current || inputRef.current._flatpickr) return;
|
||||||
|
|
||||||
|
const instance = flatpickr(inputRef.current, {
|
||||||
|
mode: "range",
|
||||||
|
dateFormat: "d-m-Y",
|
||||||
|
allowInput: allowText,
|
||||||
|
onChange: (selectedDates) => {
|
||||||
|
if (selectedDates.length === 2) {
|
||||||
|
const [start, end] = selectedDates;
|
||||||
|
const format = (d) => flatpickr.formatDate(d, "d-m-Y");
|
||||||
|
setValue(startField, format(start), { shouldValidate: true });
|
||||||
|
setValue(endField, format(end), { shouldValidate: true });
|
||||||
|
} else {
|
||||||
|
setValue(startField, "", { shouldValidate: true });
|
||||||
|
setValue(endField, "", { shouldValidate: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...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();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Reapply default range on resetSignal change
|
||||||
|
useEffect(() => {
|
||||||
|
if (resetSignal !== undefined) {
|
||||||
|
applyDefaultDates();
|
||||||
|
}
|
||||||
|
}, [resetSignal]);
|
||||||
|
|
||||||
|
const start = getValues(startField);
|
||||||
|
const end = getValues(endField);
|
||||||
|
const formattedValue = start && end ? `${start} To ${end}` : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`position-relative ${className}`}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
placeholder={placeholder}
|
||||||
|
defaultValue={formattedValue}
|
||||||
|
ref={(el) => {
|
||||||
|
inputRef.current = el;
|
||||||
|
ref(el);
|
||||||
|
}}
|
||||||
|
readOnly={!allowText}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="position-absolute top-50 end-0 pe-1 translate-middle-y cursor-pointer"
|
||||||
|
onClick={() => inputRef.current?._flatpickr?.open()}
|
||||||
|
>
|
||||||
|
<i className="bx bx-calendar bx-sm fs-5 text-muted"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@ -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
|
||||||
);
|
);
|
||||||
@ -81,48 +83,9 @@ const ExpensePage = () => {
|
|||||||
resolver: zodResolver(SearchSchema),
|
resolver: zodResolver(SearchSchema),
|
||||||
defaultValues: defaultFilter,
|
defaultValues: defaultFilter,
|
||||||
});
|
});
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
control,
|
|
||||||
getValues,
|
|
||||||
trigger,
|
|
||||||
setValue,
|
|
||||||
watch,
|
|
||||||
reset,
|
|
||||||
formState: { errors },
|
|
||||||
} = methods;
|
|
||||||
|
|
||||||
const { projectNames, loading: projectLoading } = useProjectName();
|
|
||||||
const { ExpenseStatus, loading: statusLoading, error } = useExpenseStatus();
|
|
||||||
const { employees, loading: empLoading } = useEmployeesAllOrByProjectId(
|
|
||||||
true,
|
|
||||||
selectedProjectId,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
const { setOffcanvasContent, setShowTrigger } = useFab();
|
const { setOffcanvasContent, setShowTrigger } = useFab();
|
||||||
|
|
||||||
const onSubmit = (data) => {
|
|
||||||
setFilter(data);
|
|
||||||
};
|
|
||||||
const isValidDate = (date) => {
|
|
||||||
return 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 clearFilter = () => {
|
const clearFilter = () => {
|
||||||
setFilter({
|
setFilter({
|
||||||
projectIds: [],
|
projectIds: [],
|
||||||
@ -140,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);
|
||||||
@ -159,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,
|
||||||
@ -187,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}`),
|
||||||
@ -17,6 +17,7 @@ const ExpenseRepository = {
|
|||||||
|
|
||||||
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