Refactor_Expenses #321

Merged
pramod.mahajan merged 249 commits from Refactor_Expenses into hotfix/MasterActivity 2025-08-01 13:14:59 +00:00
6 changed files with 291 additions and 177 deletions
Showing only changes of commit b864ed0529 - Show all commits

View File

@ -1,16 +1,16 @@
// components/Expense/ExpenseFilterPanel.jsx
import React, { useEffect } 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";
const ExpenseFilterPanel = ({ onApply }) => { const ExpenseFilterPanel = ({ onApply }) => {
const selectedProjectId = useSelector( const selectedProjectId = useSelector(
@ -51,7 +51,11 @@ const ExpenseFilterPanel = ({ onApply }) => {
}; };
const onSubmit = (data) => { const onSubmit = (data) => {
onApply(data); onApply({
...data,
startDate: moment.utc(data.startDate, "DD-MM-YYYY").toISOString(),
endDate: moment.utc(data.endDate, "DD-MM-YYYY").toISOString(),
});
closePanel(); closePanel();
}; };
@ -66,11 +70,17 @@ const ExpenseFilterPanel = ({ onApply }) => {
<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> <label className="form-label">Created Date</label>
<DateRangePicker {/* <DateRangePicker
onRangeChange={setDateRange} onRangeChange={setDateRange}
endDateMode="today" endDateMode="today"
DateDifference="6" DateDifference="6"
dateFormat="DD-MM-YYYY" dateFormat="DD-MM-YYYY"
/> */}
<DateRangePicker1
placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="startDate"
endField="endDate"
/> />
</div> </div>

View File

@ -11,8 +11,6 @@ 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" }) => {
const [deletingId, setDeletingId] = useState(null); const [deletingId, setDeletingId] = useState(null);
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@ -20,7 +18,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
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 { mutate: DeleteExpense, isPending } = useDeleteExpense(); const { mutate: DeleteExpense, isPending } = useDeleteExpense();
const { data, isLoading, isError, isInitialLoading, error } = useExpenseList( const { data, isLoading, isError, isInitialLoading, error } = useExpenseList(
@ -63,7 +61,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";
@ -88,20 +88,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 +112,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 +134,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 +169,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 +186,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,67 +206,80 @@ 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 ? (
<React.Fragment key={group}> Object.entries(grouped).map(([group, expenses]) => (
<tr className="tr-group text-dark"> <React.Fragment key={group}>
<td colSpan={8} className="text-start"> <tr className="tr-group text-dark">
<strong>{IsGroupedByDate ? formatUTCToLocalTime(group) : group}</strong> <td colSpan={8} className="text-start">
</td> <strong>
</tr> {IsGroupedByDate
{expenses.map((expense) => ( ? formatUTCToLocalTime(group)
<tr key={expense.id}> : group}
{expenseColumns.map( </strong>
(col) =>
(col.isAlwaysVisible || groupBy !== col.key) && (
<td key={col.key} className={`d-table-cell ${col.align ?? ""}`}>
{col.customRender
? col.customRender(expense)
: col.getValue(expense)}
</td>
)
)}
<td>
<div className="d-flex justify-content-center gap-2">
<i
className="bx bx-show text-primary cursor-pointer"
onClick={() =>
setViewExpense({ expenseId: expense.id, view: true })
}
></i>
{(expense.status.name === "Draft" ||
expense.status.name === "Rejected") &&
expense.createdBy.id === SelfId && (
<i
className="bx bx-edit text-secondary cursor-pointer"
onClick={() =>
setManageExpenseModal({
IsOpen: true,
expenseId: expense.id
})
}
></i>
)}
{expense.status.name === "Draft" &&
expense?.createdBy?.id === SelfId && (
<i
className="bx bx-trash text-danger cursor-pointer"
onClick={() => {
setIsDeleteModalOpen(true);
setDeletingId(expense.id);
}}
></i>
)}
</div>
</td> </td>
</tr> </tr>
))} {expenses.map((expense) => (
</React.Fragment> <tr key={expense.id}>
))} {expenseColumns.map(
{data?.data?.length === 0 && ( (col) =>
(col.isAlwaysVisible || groupBy !== col.key) && (
<td
key={col.key}
className={`d-table-cell ${col.align ?? ""}`}
>
{col.customRender
? col.customRender(expense)
: col.getValue(expense)}
</td>
)
)}
<td className="sticky-action-column bg-white">
<div className="d-flex justify-content-center gap-2">
<i
className="bx bx-show text-primary cursor-pointer"
onClick={() =>
setViewExpense({
expenseId: expense.id,
view: true,
})
}
></i>
{(expense.status.name === "Draft" ||
expense.status.name === "Rejected") &&
expense.createdBy.id === SelfId && (
<i
className="bx bx-edit text-secondary cursor-pointer"
onClick={() =>
setManageExpenseModal({
IsOpen: true,
expenseId: expense.id,
})
}
></i>
)}
{expense.status.name === "Draft" &&
expense.createdBy.id === SelfId && (
<i
className="bx bx-trash text-danger cursor-pointer"
onClick={() => {
setIsDeleteModalOpen(true);
setDeletingId(expense.id);
}}
></i>
)}
</div>
</td>
</tr>
))}
</React.Fragment>
))
) : (
<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 +303,3 @@ const ExpenseList = ({ filters, groupBy = "transactionDate" }) => {
}; };
export default ExpenseList; export default ExpenseList;

View File

@ -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}

View File

@ -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"> <input
{/* <label htmlFor="flatpickr-single" className="form-label"> type="text"
Select Date className="form-control form-control-sm "
</label> */} placeholder={placeholder}
<input defaultValue={
type="text" value ? flatpickr.formatDate(flatpickr.parseDate(value, "Y-m-d"), "d-m-Y") : ""
id="flatpickr-single" }
className="form-control" ref={(el) => {
placeholder="YYYY-MM-DD" inputRef.current = el;
ref={inputRef} ref(el);
/> }}
</div> readOnly={!allowText}
autoComplete="off"
/>
<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>
); );
}; };

View File

@ -1,5 +1,5 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { useController, useFormContext } from "react-hook-form";
const DateRangePicker = ({ const DateRangePicker = ({
md, md,
sm, sm,
@ -48,22 +48,99 @@ 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"
placeholder="From to End" placeholder="From to End"
id="flatpickr-range" id="flatpickr-range"
ref={inputRef} ref={inputRef}
/> />
<i
className="bx bx-calendar calendar-icon cursor-pointer position-absolute top-50 translate-middle-y "
style={{right:"12px"}}
></i>
</div>
<i
className="bx bx-calendar calendar-icon cursor-pointer position-absolute top-50 translate-middle-y "
style={{ right: "12px" }}
></i>
</div>
); );
}; };
export default DateRangePicker; export default DateRangePicker;
export const DateRangePicker1 = ({
startField = "startDate",
endField = "endDate",
label,
placeholder = "Select date range",
className = "",
allowText = false,
...rest
}) => {
const inputRef = useRef(null);
const { control, setValue, getValues } = useFormContext();
const {
field: { ref },
} = useController({ name: startField, control });
useEffect(() => {
if (!inputRef.current || inputRef.current._flatpickr) return;
const defaultStart = getValues(startField);
const defaultEnd = getValues(endField);
const instance = flatpickr(inputRef.current, {
mode: "range",
dateFormat: "d-m-Y",
allowInput: allowText,
defaultDate:
defaultStart && defaultEnd
? [
flatpickr.parseDate(defaultStart, "d-m-Y"),
flatpickr.parseDate(defaultEnd, "d-m-Y"),
]
: null,
onChange: (selectedDates, dateStr, fp) => {
if (selectedDates.length === 2) {
const [start, end] = selectedDates;
const format = (d) => flatpickr.formatDate(d, "d-m-Y");
setValue(startField, format(start));
setValue(endField, format(end));
} else {
setValue(startField, "");
setValue(endField, "");
}
},
...rest,
});
return () => instance.destroy();
}, []);
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>
);
};

View File

@ -81,47 +81,13 @@ 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({