Merge pull request 'Issues_Expense_2W Changes of Expense, Payment Request and Recurring Expense.' (#505) from Issues_Expense_2W into upgrade_Expense

Reviewed-on: #505
Merged
This commit is contained in:
pramod.mahajan 2025-11-07 11:29:53 +00:00
commit 4cbac98986
5 changed files with 259 additions and 255 deletions

View File

@ -35,7 +35,7 @@ const ExpenseByProject = () => {
const getSelectedTypeName = () => { const getSelectedTypeName = () => {
if (!selectedType) return "All Types"; if (!selectedType) return "All Types";
const found = ExpenseTypes.find((t) => t.id === selectedType); const found = ExpenseCategories.find((t) => t.id === selectedType);
return found ? found.name : "All Types"; return found ? found.name : "All Types";
}; };
@ -69,8 +69,8 @@ const ExpenseByProject = () => {
const ExpenseCategoryType = [ const ExpenseCategoryType = [
{id:1,category:"Category",label:"Category"}, { id: 1, category: "Category", label: "Category" },
{id:2,category:"Project",label:"Project"} { id: 2, category: "Project", label: "Project" }
] ]
return ( return (
@ -91,19 +91,19 @@ const ExpenseByProject = () => {
> >
{viewMode} {viewMode}
</button> </button>
<ul className="dropdown-menu dropdown-menu-end "> <ul className="dropdown-menu dropdown-menu-end ">
{ExpenseCategoryType.map((cat)=>( {ExpenseCategoryType.map((cat) => (
<li> <li>
<button <button
className="dropdown-item" className="dropdown-item"
onClick={() => { onClick={() => {
setViewMode(cat.category); setViewMode(cat.category);
setSelectedType(""); setSelectedType("");
}} }}
> >
{cat.label} {cat.label}
</button> </button>
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
@ -115,8 +115,8 @@ const ExpenseByProject = () => {
<button <button
key={item} key={item}
className={`border-0 px-2 py-1 text-sm rounded ${range === item className={`border-0 px-2 py-1 text-sm rounded ${range === item
? "text-white bg-primary" ? "text-white bg-primary"
: "text-body bg-transparent" : "text-body bg-transparent"
}`} }`}
style={{ cursor: "pointer", transition: "all 0.2s ease" }} style={{ cursor: "pointer", transition: "all 0.2s ease" }}
onClick={() => setRange(item)} onClick={() => setRange(item)}

View File

@ -13,230 +13,236 @@ import { useParams } from "react-router-dom";
const DocumentFilterPanel = forwardRef( const DocumentFilterPanel = forwardRef(
({ entityTypeId, onApply, setFilterdata }, ref) => { ({ entityTypeId, onApply, setFilterdata }, ref) => {
const [resetKey, setResetKey] = useState(0); const [resetKey, setResetKey] = useState(0);
const { status } = useParams(); const { status } = useParams();
const { data, isError, isLoading, error } = const { data, isError, isLoading, error } =
useDocumentFilterEntities(entityTypeId); useDocumentFilterEntities(entityTypeId);
//changes useEffect(() => {
return () => {
closePanel();
};
}, []);
const dynamicDocumentFilterDefaultValues = useMemo(() => { //changes
return {
...DocumentFilterDefaultValues, const dynamicDocumentFilterDefaultValues = useMemo(() => {
uploadedByIds: DocumentFilterDefaultValues.uploadedByIds || [], return {
documentCategoryIds: DocumentFilterDefaultValues.documentCategoryIds || [], ...DocumentFilterDefaultValues,
documentTypeIds: DocumentFilterDefaultValues.documentTypeIds || [], uploadedByIds: DocumentFilterDefaultValues.uploadedByIds || [],
documentTagIds: DocumentFilterDefaultValues.documentTagIds || [], documentCategoryIds: DocumentFilterDefaultValues.documentCategoryIds || [],
startDate: DocumentFilterDefaultValues.startDate, documentTypeIds: DocumentFilterDefaultValues.documentTypeIds || [],
endDate: DocumentFilterDefaultValues.endDate, documentTagIds: DocumentFilterDefaultValues.documentTagIds || [],
startDate: DocumentFilterDefaultValues.startDate,
endDate: DocumentFilterDefaultValues.endDate,
};
}, [status]);
const methods = useForm({
resolver: zodResolver(DocumentFilterSchema),
defaultValues: dynamicDocumentFilterDefaultValues,
});
const { handleSubmit, reset, setValue, watch } = methods;
// Watch values from form
const isUploadedAt = watch("isUploadedAt");
const isVerified = watch("isVerified");
// Close the offcanvas (bootstrap specific)
const closePanel = () => {
document.querySelector(".offcanvas.show .btn-close")?.click();
}; };
}, [status]); useImperativeHandle(ref, () => ({
resetFieldValue: (name, value) => {
if (value !== undefined) {
setValue(name, value);
} else {
reset({ ...methods.getValues(), [name]: DocumentFilterDefaultValues[name] });
}
},
getValues: methods.getValues, // optional, to read current filter state
}));
const methods = useForm({ //changes
resolver: zodResolver(DocumentFilterSchema), useEffect(() => {
defaultValues: dynamicDocumentFilterDefaultValues, if (data && setFilterdata) {
}); setFilterdata(data);
const { handleSubmit, reset, setValue, watch } = methods;
// Watch values from form
const isUploadedAt = watch("isUploadedAt");
const isVerified = watch("isVerified");
// Close the offcanvas (bootstrap specific)
const closePanel = () => {
document.querySelector(".offcanvas.show .btn-close")?.click();
};
useImperativeHandle(ref, () => ({
resetFieldValue: (name, value) => {
if (value !== undefined) {
setValue(name, value);
} else {
reset({ ...methods.getValues(), [name]: DocumentFilterDefaultValues[name] });
} }
}, }, [data, setFilterdata]);
getValues: methods.getValues, // optional, to read current filter state
}));
//changes const onSubmit = (values) => {
useEffect(() => { onApply({
if (data && setFilterdata) { ...values,
setFilterdata(data); startDate: values.startDate
} ? moment.utc(values.startDate, "DD-MM-YYYY").toISOString()
}, [data, setFilterdata]); : null,
endDate: values.endDate
? moment.utc(values.endDate, "DD-MM-YYYY").toISOString()
: null,
});
// closePanel();
};
const onSubmit = (values) => { const onClear = () => {
onApply({ reset(DocumentFilterDefaultValues);
...values, setResetKey((prev) => prev + 1);
startDate: values.startDate onApply(DocumentFilterDefaultValues);
? moment.utc(values.startDate, "DD-MM-YYYY").toISOString() // closePanel();
: null, };
endDate: values.endDate
? moment.utc(values.endDate, "DD-MM-YYYY").toISOString()
: null,
});
// closePanel();
};
const onClear = () => { if (isLoading) return <div>Loading...</div>;
reset(DocumentFilterDefaultValues); if (isError)
setResetKey((prev) => prev + 1); return <div>Error: {error?.message || "Something went wrong!"}</div>;
onApply(DocumentFilterDefaultValues);
// closePanel();
};
if (isLoading) return <div>Loading...</div>; const {
if (isError) uploadedBy = [],
return <div>Error: {error?.message || "Something went wrong!"}</div>; documentCategory = [],
documentType = [],
const { documentTag = [],
uploadedBy = [], } = data?.data || {};
documentCategory = [],
documentType = [],
documentTag = [],
} = data?.data || {};
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
{/* Date Range Section */} {/* Date Range Section */}
<div className="mb-2"> <div className="mb-2">
<div className="text-start d-flex align-items-center my-1"> <div className="text-start d-flex align-items-center my-1">
<label className="form-label me-2 my-0">Choose Date:</label> <label className="form-label me-2 my-0">Choose Date:</label>
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none"> <div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
<button <button
type="button" type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${isUploadedAt ? "active btn-secondary text-white" : "" className={`btn px-2 py-1 rounded-0 text-tiny ${isUploadedAt ? "active btn-secondary text-white" : ""
}`} }`}
onClick={() => setValue("isUploadedAt", true)} onClick={() => setValue("isUploadedAt", true)}
> >
Uploaded On Uploaded On
</button> </button>
<button <button
type="button" type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${!isUploadedAt ? "active btn-secondary text-white" : "" className={`btn px-2 py-1 rounded-0 text-tiny ${!isUploadedAt ? "active btn-secondary text-white" : ""
}`} }`}
onClick={() => setValue("isUploadedAt", false)} onClick={() => setValue("isUploadedAt", false)}
> >
Updated On Updated On
</button> </button>
</div>
</div>
<DateRangePicker1
placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="startDate"
endField="endDate"
defaultRange={false}
resetSignal={resetKey}
maxDate={new Date()}
/>
</div>
{/* Dropdown Filters */}
<div className="row g-2 text-start">
<SelectMultiple
name="uploadedByIds"
label="Uploaded By:"
options={uploadedBy}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="documentCategoryIds"
label="Document Category:"
options={documentCategory}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="documentTypeIds"
label="Document Type:"
options={documentType}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="documentTagIds"
label="Tags:"
options={documentTag}
labelKey="name"
valueKey="id"
/>
</div>
{/* Status Filter */}
<div className="text-start my-2">
<label className="form-label d-block mb-2">Choose Status:</label>
<div className="d-flex gap-4">
<label className="switch switch-sm">
<input
type="radio"
className="switch-input"
name="isVerified"
checked={isVerified === null}
onChange={() => setValue("isVerified", null)}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className="switch-label">All</span>
</label>
<label className="switch switch-sm">
<input
type="radio"
className="switch-input"
name="isVerified"
checked={isVerified === true}
onChange={() => setValue("isVerified", true)}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className="switch-label">Verified</span>
</label>
<label className="switch switch-sm">
<input
type="radio"
className="switch-input"
name="isVerified"
checked={isVerified === false}
onChange={() => setValue("isVerified", false)}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className="switch-label">Rejected</span>
</label>
</div> </div>
</div> </div>
<DateRangePicker1 {/* Footer Buttons */}
placeholder="DD-MM-YYYY To DD-MM-YYYY" <div className="d-flex justify-content-end py-3 gap-2">
startField="startDate" <button
endField="endDate" type="button"
defaultRange={false} className="btn btn-label-secondary btn-sm"
resetSignal={resetKey} onClick={onClear}
maxDate={new Date()} >
/> Clear
</div> </button>
<button type="submit" className="btn btn-primary btn-sm">
{/* Dropdown Filters */} Apply
<div className="row g-2 text-start"> </button>
<SelectMultiple
name="uploadedByIds"
label="Uploaded By:"
options={uploadedBy}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="documentCategoryIds"
label="Document Category:"
options={documentCategory}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="documentTypeIds"
label="Document Type:"
options={documentType}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="documentTagIds"
label="Tags:"
options={documentTag}
labelKey="name"
valueKey="id"
/>
</div>
{/* Status Filter */}
<div className="text-start my-2">
<label className="form-label d-block mb-2">Choose Status:</label>
<div className="d-flex gap-4">
<label className="switch switch-sm">
<input
type="radio"
className="switch-input"
name="isVerified"
checked={isVerified === null}
onChange={() => setValue("isVerified", null)}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className="switch-label">All</span>
</label>
<label className="switch switch-sm">
<input
type="radio"
className="switch-input"
name="isVerified"
checked={isVerified === true}
onChange={() => setValue("isVerified", true)}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className="switch-label">Verified</span>
</label>
<label className="switch switch-sm">
<input
type="radio"
className="switch-input"
name="isVerified"
checked={isVerified === false}
onChange={() => setValue("isVerified", false)}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className="switch-label">Rejected</span>
</label>
</div> </div>
</div> </form>
</FormProvider>
{/* Footer Buttons */} );
<div className="d-flex justify-content-end py-3 gap-2"> });
<button
type="button"
className="btn btn-label-secondary btn-sm"
onClick={onClear}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-sm">
Apply
</button>
</div>
</form>
</FormProvider>
);
});
export default DocumentFilterPanel; export default DocumentFilterPanel;

View File

@ -519,7 +519,7 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
? "Please Wait..." ? "Please Wait..."
: requestToEdit : requestToEdit
? "Update" ? "Update"
: "Save as Draft"} : "Submit"}
</button> </button>
</div> </div>
</form> </form>

View File

@ -43,9 +43,8 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
displayField = "Status"; displayField = "Status";
break; break;
case "submittedBy": case "submittedBy":
key = `${item?.createdBy?.firstName ?? ""} ${ key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? ""
item.createdBy?.lastName ?? "" }`.trim();
}`.trim();
displayField = "Submitted By"; displayField = "Submitted By";
break; break;
case "project": case "project":
@ -98,9 +97,8 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
label: "Submitted By", label: "Submitted By",
align: "text-start", align: "text-start",
getValue: (e) => getValue: (e) =>
`${e.createdBy?.firstName ?? ""} ${ `${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
e.createdBy?.lastName ?? "" }`.trim() || "N/A",
}`.trim() || "N/A",
customRender: (e) => ( customRender: (e) => (
<div <div
className="d-flex align-items-center cursor-pointer" className="d-flex align-items-center cursor-pointer"
@ -113,9 +111,8 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
lastName={e.createdBy?.lastName} lastName={e.createdBy?.lastName}
/> />
<span className="text-truncate"> <span className="text-truncate">
{`${e.createdBy?.firstName ?? ""} ${ {`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
e.createdBy?.lastName ?? "" }`.trim() || "N/A"}
}`.trim() || "N/A"}
</span> </span>
</div> </div>
), ),
@ -128,15 +125,12 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
}, },
{ {
key: "amount", key: "amount",
label: " Amount", label: "Amount",
align: "text-start", align: "text-end",
getValue: (e) => ( getValue: (e) =>
<> e?.amount
{formatCurrency(e?.amount)}&nbsp;{e.currency.currencyCode} ? `${e?.currency?.symbol ? e.currency.symbol + " " : ""}${e.amount.toLocaleString()}`
</> : "N/A",
),
align: "text-end px-3",
}, },
{ {
key: "expenseStatus", key: "expenseStatus",
@ -144,9 +138,8 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
align: "text-center", align: "text-center",
getValue: (e) => ( getValue: (e) => (
<span <span
className={`badge bg-label-${ className={`badge bg-label-${getColorNameFromHex(e?.expenseStatus?.color) || "secondary"
getColorNameFromHex(e?.expenseStatus?.color) || "secondary" }`}
}`}
> >
{e?.expenseStatus?.name || "Unknown"} {e?.expenseStatus?.name || "Unknown"}
</span> </span>
@ -181,8 +174,13 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
if (isLoading) return <ExpenseTableSkeleton headers={header} />; if (isLoading) return <ExpenseTableSkeleton headers={header} />;
const grouped = groupBy const grouped = groupBy
? groupByField(data?.data ?? [], groupBy) ? Object.fromEntries(
Object.entries(groupByField(data?.data ?? [], groupBy)).sort(([keyA], [keyB]) =>
keyA.localeCompare(keyB)
)
)
: { All: data?.data ?? [] }; : { All: data?.data ?? [] };
const IsGroupedByDate = [ const IsGroupedByDate = [
{ key: "transactionDate", displayField: "Transaction Date" }, { key: "transactionDate", displayField: "Transaction Date" },
{ key: "createdAt", displayField: "created Date" }, { key: "createdAt", displayField: "created Date" },

View File

@ -68,7 +68,7 @@ const RecurringExpenseList = ({ search, filterStatuses }) => {
align: "text-end", align: "text-end",
getValue: (e) => getValue: (e) =>
e?.amount e?.amount
? `${e?.currency?.symbol || ""}${e.amount.toLocaleString()}` ? `${e?.currency?.symbol ? e.currency.symbol + " " : ""}${e.amount.toLocaleString()}`
: "N/A", : "N/A",
}, },
{ {
@ -109,7 +109,7 @@ const RecurringExpenseList = ({ search, filterStatuses }) => {
); );
const paginate = (page) => { const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) { if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page); setCurrentPage(page);
} }
@ -277,11 +277,11 @@ const RecurringExpenseList = ({ search, filterStatuses }) => {
</div> </div>
{/* Pagination */} {/* Pagination */}
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}
totalPages={data?.totalPages} totalPages={data?.totalPages}
onPageChange={paginate} onPageChange={paginate}
/> />
</div> </div>
</> </>
); );