Merge branch 'Purchase_Invoice_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Weidget_Dashboard_Services
This commit is contained in:
commit
b20dd2d0d4
File diff suppressed because it is too large
Load Diff
@ -42,7 +42,12 @@ const HorizontalBarChart = ({
|
|||||||
categories.length === seriesData.length;
|
categories.length === seriesData.length;
|
||||||
|
|
||||||
if (!hasValidData) {
|
if (!hasValidData) {
|
||||||
return <div className="text-center text-gray-500">No data to display</div>;
|
return <div
|
||||||
|
className="d-flex justify-content-center align-items-center text-muted"
|
||||||
|
style={{ height: "300px" }}
|
||||||
|
>
|
||||||
|
No data found
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
// Combine seriesData and categories, then sort in descending order
|
// Combine seriesData and categories, then sort in descending order
|
||||||
const combined = seriesData.map((value, index) => ({
|
const combined = seriesData.map((value, index) => ({
|
||||||
|
|||||||
@ -12,14 +12,17 @@ const ProjectCompletionChart = () => {
|
|||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
} = useProjectCompletionStatus();
|
} = useProjectCompletionStatus();
|
||||||
const projectNames = projects?.map((p) => p.name) || [];
|
const filteredProjects = projects?.filter((p) => p.completedWork > 0) || [];
|
||||||
const projectProgress =
|
|
||||||
projects?.map((p) => {
|
const projectNames = filteredProjects.map((p) => p.name);
|
||||||
const completed = p.completedWork || 0;
|
|
||||||
const planned = p.plannedWork || 1;
|
const projectProgress = filteredProjects.map((p) => {
|
||||||
const percent = planned ? (completed / planned) * 100 : 0;
|
const completed = p.completedWork || 0;
|
||||||
return Math.min(Math.round(percent), 100);
|
const planned = p.plannedWork || 1;
|
||||||
}) || [];
|
const percent = planned ? (completed / planned) * 100 : 0;
|
||||||
|
return Math.min(parseFloat(percent.toFixed(2)), 100); // limit to 2 decimals
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card h-100">
|
<div className="card h-100">
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
import React, { forwardRef, useEffect, useImperativeHandle, useState, useMemo } from "react";
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
import { FormProvider, useForm, Controller } from "react-hook-form";
|
import { FormProvider, useForm, Controller } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { defaultFilter, SearchSchema } from "./ExpenseSchema";
|
import { defaultFilter, SearchSchema } from "./ExpenseSchema";
|
||||||
@ -15,282 +21,291 @@ import { useExpenseFilter } from "../../hooks/useExpense";
|
|||||||
import { ExpenseFilterSkeleton } from "./ExpenseSkeleton";
|
import { ExpenseFilterSkeleton } from "./ExpenseSkeleton";
|
||||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }, ref) => {
|
const ExpenseFilterPanel = forwardRef(
|
||||||
const { status } = useParams();
|
({ onApply, handleGroupBy, setFilterdata }, ref) => {
|
||||||
const navigate = useNavigate();
|
const { status } = useParams();
|
||||||
const selectedProjectId = useSelector(
|
const navigate = useNavigate();
|
||||||
(store) => store.localVariables.projectId
|
const selectedProjectId = useSelector(
|
||||||
);
|
(store) => store.localVariables.projectId
|
||||||
const { data, isLoading, isError, error, isFetching, isFetched } =
|
);
|
||||||
useExpenseFilter();
|
const { data, isLoading, isError, error, isFetching, isFetched } =
|
||||||
|
useExpenseFilter();
|
||||||
|
|
||||||
const groupByList = useMemo(() => {
|
const groupByList = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{ id: "transactionDate", name: "Transaction Date" },
|
{ id: "none", name: "None" },
|
||||||
{ id: "status", name: "Status" },
|
{ id: "transactionDate", name: "Transaction Date" },
|
||||||
{ id: "submittedBy", name: "Submitted By" },
|
{ id: "status", name: "Status" },
|
||||||
{ id: "project", name: "Project" },
|
{ id: "submittedBy", name: "Submitted By" },
|
||||||
{ id: "paymentMode", name: "Payment Mode" },
|
{ id: "project", name: "Project" },
|
||||||
{ id: "expenseCategory", name: "Expense Category" },
|
{ id: "paymentMode", name: "Payment Mode" },
|
||||||
{ id: "createdAt", name: "Submitted Date" },
|
{ id: "expenseCategory", name: "Expense Category" },
|
||||||
].sort((a, b) => a.name.localeCompare(b.name));
|
{ id: "createdAt", name: "Submitted Date" },
|
||||||
}, []);
|
].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [selectedGroup, setSelectedGroup] = useState(groupByList[6]);
|
const [selectedGroup, setSelectedGroup] = useState(groupByList[0]);
|
||||||
const [resetKey, setResetKey] = useState(0);
|
const [resetKey, setResetKey] = useState(0);
|
||||||
|
|
||||||
const dynamicDefaultFilter = useMemo(() => {
|
const dynamicDefaultFilter = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
...defaultFilter,
|
...defaultFilter,
|
||||||
statusIds: status ? [status] : defaultFilter.statusIds || [],
|
statusIds: status ? [status] : defaultFilter.statusIds || [],
|
||||||
projectIds: defaultFilter.projectIds || [],
|
projectIds: defaultFilter.projectIds || [],
|
||||||
createdByIds: defaultFilter.createdByIds || [],
|
createdByIds: defaultFilter.createdByIds || [],
|
||||||
paidById: defaultFilter.paidById || [],
|
paidById: defaultFilter.paidById || [],
|
||||||
expenseCategoryIds: defaultFilter.expenseCategoryIds || [],
|
expenseCategoryIds: defaultFilter.expenseCategoryIds || [],
|
||||||
isTransactionDate: defaultFilter.isTransactionDate ?? true,
|
isTransactionDate: defaultFilter.isTransactionDate ?? true,
|
||||||
startDate: defaultFilter.startDate,
|
startDate: defaultFilter.startDate,
|
||||||
endDate: defaultFilter.endDate,
|
endDate: defaultFilter.endDate,
|
||||||
};
|
|
||||||
}, [status, selectedProjectId]);
|
|
||||||
|
|
||||||
const methods = useForm({
|
|
||||||
resolver: zodResolver(SearchSchema),
|
|
||||||
defaultValues: dynamicDefaultFilter,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { control, handleSubmit, reset, setValue, watch } = methods;
|
|
||||||
const isTransactionDate = watch("isTransactionDate");
|
|
||||||
|
|
||||||
const closePanel = () => {
|
|
||||||
document.querySelector(".offcanvas.show .btn-close")?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Change here
|
|
||||||
useEffect(() => {
|
|
||||||
if (data && setFilterdata) {
|
|
||||||
setFilterdata(data);
|
|
||||||
}
|
|
||||||
}, [data, setFilterdata]);
|
|
||||||
|
|
||||||
const handleGroupChange = (e) => {
|
|
||||||
const group = groupByList.find((g) => g.id === e.target.value);
|
|
||||||
if (group) setSelectedGroup(group);
|
|
||||||
};
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
resetFieldValue: (name, value) => {
|
|
||||||
// Reset specific field
|
|
||||||
if (value !== undefined) {
|
|
||||||
setValue(name, value);
|
|
||||||
} else {
|
|
||||||
reset({ ...methods.getValues(), [name]: defaultFilter[name] });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getValues: methods.getValues, // optional, to read current filter state
|
|
||||||
}));
|
|
||||||
|
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClear = () => {
|
|
||||||
reset(defaultFilter);
|
|
||||||
setResetKey((prev) => prev + 1);
|
|
||||||
setSelectedGroup(groupByList[0]);
|
|
||||||
onApply(defaultFilter);
|
|
||||||
handleGroupBy(groupByList[0].id);
|
|
||||||
// closePanel();
|
|
||||||
if (status) {
|
|
||||||
navigate("/expenses", { replace: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const location = useLocation();
|
|
||||||
useEffect(() => {
|
|
||||||
closePanel();
|
|
||||||
}, [location]);
|
|
||||||
|
|
||||||
const [appliedStatusId, setAppliedStatusId] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!status || !data) return;
|
|
||||||
|
|
||||||
if (status !== appliedStatusId) {
|
|
||||||
const filterWithStatus = {
|
|
||||||
...dynamicDefaultFilter,
|
|
||||||
projectIds: selectedProjectId ? [selectedProjectId] : dynamicDefaultFilter.projectIds || [],
|
|
||||||
startDate: dynamicDefaultFilter.startDate
|
|
||||||
? moment.utc(dynamicDefaultFilter.startDate, "DD-MM-YYYY").toISOString()
|
|
||||||
: undefined,
|
|
||||||
endDate: dynamicDefaultFilter.endDate
|
|
||||||
? moment.utc(dynamicDefaultFilter.endDate, "DD-MM-YYYY").toISOString()
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
|
}, [status, selectedProjectId]);
|
||||||
|
|
||||||
onApply(filterWithStatus);
|
const methods = useForm({
|
||||||
|
resolver: zodResolver(SearchSchema),
|
||||||
|
defaultValues: dynamicDefaultFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { control, handleSubmit, reset, setValue, watch } = methods;
|
||||||
|
const isTransactionDate = watch("isTransactionDate");
|
||||||
|
|
||||||
|
const closePanel = () => {
|
||||||
|
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Change here
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && setFilterdata) {
|
||||||
|
setFilterdata(data);
|
||||||
|
}
|
||||||
|
}, [data, setFilterdata]);
|
||||||
|
|
||||||
|
const handleGroupChange = (e) => {
|
||||||
|
const group = groupByList.find((g) => g.id === e.target.value);
|
||||||
|
if (group) setSelectedGroup(group);
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
resetFieldValue: (name, value) => {
|
||||||
|
// Reset specific field
|
||||||
|
if (value !== undefined) {
|
||||||
|
setValue(name, value);
|
||||||
|
} else {
|
||||||
|
reset({ ...methods.getValues(), [name]: defaultFilter[name] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getValues: methods.getValues, // optional, to read current filter state
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
handleGroupBy(selectedGroup.id);
|
||||||
setAppliedStatusId(status);
|
// closePanel();
|
||||||
}
|
};
|
||||||
}, [
|
|
||||||
status,
|
|
||||||
data,
|
|
||||||
dynamicDefaultFilter,
|
|
||||||
onApply,
|
|
||||||
handleGroupBy,
|
|
||||||
selectedGroup.id,
|
|
||||||
appliedStatusId,
|
|
||||||
selectedProjectId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
|
const onClear = () => {
|
||||||
if (isError && isFetched)
|
reset(defaultFilter);
|
||||||
return <div>Something went wrong Here- {error.message} </div>;
|
setResetKey((prev) => prev + 1);
|
||||||
|
setSelectedGroup(groupByList[0]);
|
||||||
|
onApply(defaultFilter);
|
||||||
|
handleGroupBy(groupByList[0].id);
|
||||||
|
// closePanel();
|
||||||
|
if (status) {
|
||||||
|
navigate("/expenses", { replace: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
useEffect(() => {
|
||||||
|
closePanel();
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
|
const [appliedStatusId, setAppliedStatusId] = useState(null);
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<>
|
if (!status || !data) return;
|
||||||
<FormProvider {...methods}>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
if (status !== appliedStatusId) {
|
||||||
<div className="mb-3 w-100">
|
const filterWithStatus = {
|
||||||
<div className="d-flex align-items-center mb-2">
|
...dynamicDefaultFilter,
|
||||||
<label className="form-label me-2">Filter By:</label>
|
projectIds: selectedProjectId
|
||||||
<div className="d-inline-flex border rounded-pill mb-1 overflow-hidden shadow-none">
|
? [selectedProjectId]
|
||||||
<button
|
: dynamicDefaultFilter.projectIds || [],
|
||||||
type="button"
|
startDate: dynamicDefaultFilter.startDate
|
||||||
className={`btn px-2 py-1 rounded-0 text-tiny ${isTransactionDate ? "active btn-primary text-white" : ""
|
? moment
|
||||||
|
.utc(dynamicDefaultFilter.startDate, "DD-MM-YYYY")
|
||||||
|
.toISOString()
|
||||||
|
: undefined,
|
||||||
|
endDate: dynamicDefaultFilter.endDate
|
||||||
|
? moment
|
||||||
|
.utc(dynamicDefaultFilter.endDate, "DD-MM-YYYY")
|
||||||
|
.toISOString()
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
onApply(filterWithStatus);
|
||||||
|
handleGroupBy(selectedGroup.id);
|
||||||
|
setAppliedStatusId(status);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
status,
|
||||||
|
data,
|
||||||
|
dynamicDefaultFilter,
|
||||||
|
onApply,
|
||||||
|
handleGroupBy,
|
||||||
|
selectedGroup.id,
|
||||||
|
appliedStatusId,
|
||||||
|
selectedProjectId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
|
||||||
|
if (isError && isFetched)
|
||||||
|
return <div>Something went wrong Here- {error.message} </div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
||||||
|
<div className="mb-3 w-100">
|
||||||
|
<div className="d-flex align-items-center mb-2">
|
||||||
|
<label className="form-label me-2">Filter By:</label>
|
||||||
|
<div className="d-inline-flex border rounded-pill mb-1 overflow-hidden shadow-none">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
||||||
|
isTransactionDate ? "active btn-primary text-white" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setValue("isTransactionDate", true)}
|
onClick={() => setValue("isTransactionDate", true)}
|
||||||
>
|
>
|
||||||
Transaction Date
|
Transaction Date
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn px-2 py-1 rounded-0 text-tiny ${!isTransactionDate ? "active btn-primary text-white" : ""
|
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
||||||
|
!isTransactionDate ? "active btn-primary text-white" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setValue("isTransactionDate", false)}
|
onClick={() => setValue("isTransactionDate", false)}
|
||||||
>
|
>
|
||||||
Submitted Date
|
Submitted Date
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DateRangePicker1
|
||||||
|
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
||||||
|
startField="startDate"
|
||||||
|
endField="endDate"
|
||||||
|
resetSignal={resetKey}
|
||||||
|
defaultRange={false}
|
||||||
|
maxDate={new Date()}
|
||||||
|
className="w-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row g-2">
|
||||||
|
<SelectMultiple
|
||||||
|
name="projectIds"
|
||||||
|
label="Projects :"
|
||||||
|
options={data.projects}
|
||||||
|
labelKey="name"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
<SelectMultiple
|
||||||
|
name="createdByIds"
|
||||||
|
label="Submitted By :"
|
||||||
|
options={data.createdBy}
|
||||||
|
labelKey={(item) => item.name}
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
<SelectMultiple
|
||||||
|
name="paidById"
|
||||||
|
label="Paid By :"
|
||||||
|
options={data.paidBy}
|
||||||
|
labelKey={(item) => item.name}
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
<SelectMultiple
|
||||||
|
name="expenseCategoryIds"
|
||||||
|
label="Category :"
|
||||||
|
options={data.expenseCategory}
|
||||||
|
labelKey={(item) => item.name}
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="form-label">Status :</label>
|
||||||
|
<div className="row flex-wrap">
|
||||||
|
{data?.status
|
||||||
|
?.slice()
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map((status) => (
|
||||||
|
<div className="col-6" key={status.id}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="statusIds"
|
||||||
|
render={({ field: { value = [], onChange } }) => (
|
||||||
|
<div className="d-flex align-items-center me-3 mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
value={status.id}
|
||||||
|
checked={value.includes(status.id)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const checked = e.target.checked;
|
||||||
|
onChange(
|
||||||
|
checked
|
||||||
|
? [...value, status.id]
|
||||||
|
: value.filter((v) => v !== status.id)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label className="ms-2 mb-0">{status.name}</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DateRangePicker1
|
<div className="mb-2 text-start ">
|
||||||
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
<label htmlFor="groupBySelect" className="form-label">
|
||||||
startField="startDate"
|
Group By :
|
||||||
endField="endDate"
|
</label>
|
||||||
resetSignal={resetKey}
|
<select
|
||||||
defaultRange={false}
|
id="groupBySelect"
|
||||||
maxDate={new Date()}
|
className="form-select form-select-sm"
|
||||||
className="w-100"
|
value={selectedGroup?.id || "none"}
|
||||||
/>
|
onChange={handleGroupChange}
|
||||||
</div>
|
>
|
||||||
|
{groupByList.map((group) => (
|
||||||
<div className="row g-2">
|
<option key={group.id} value={group.id}>
|
||||||
<SelectMultiple
|
{group.name}
|
||||||
name="projectIds"
|
</option>
|
||||||
label="Projects :"
|
))}
|
||||||
options={data.projects}
|
</select>
|
||||||
labelKey="name"
|
|
||||||
valueKey="id"
|
|
||||||
/>
|
|
||||||
<SelectMultiple
|
|
||||||
name="createdByIds"
|
|
||||||
label="Submitted By :"
|
|
||||||
options={data.createdBy}
|
|
||||||
labelKey={(item) => item.name}
|
|
||||||
valueKey="id"
|
|
||||||
/>
|
|
||||||
<SelectMultiple
|
|
||||||
name="paidById"
|
|
||||||
label="Paid By :"
|
|
||||||
options={data.paidBy}
|
|
||||||
labelKey={(item) => item.name}
|
|
||||||
valueKey="id"
|
|
||||||
/>
|
|
||||||
<SelectMultiple
|
|
||||||
name="expenseCategoryIds"
|
|
||||||
label="Category :"
|
|
||||||
options={data.expenseCategory}
|
|
||||||
labelKey={(item) => item.name}
|
|
||||||
valueKey="id"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label className="form-label">Status :</label>
|
|
||||||
<div className="row flex-wrap">
|
|
||||||
{data?.status
|
|
||||||
?.slice()
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
|
||||||
.map((status) => (
|
|
||||||
<div className="col-6" key={status.id}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="statusIds"
|
|
||||||
render={({ field: { value = [], onChange } }) => (
|
|
||||||
<div className="d-flex align-items-center me-3 mb-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
value={status.id}
|
|
||||||
checked={value.includes(status.id)}
|
|
||||||
onChange={(e) => {
|
|
||||||
const checked = e.target.checked;
|
|
||||||
onChange(
|
|
||||||
checked
|
|
||||||
? [...value, status.id]
|
|
||||||
: value.filter((v) => v !== status.id)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<label className="ms-2 mb-0">{status.name}</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="mb-2 text-start ">
|
|
||||||
<label htmlFor="groupBySelect" className="form-label">
|
|
||||||
Group By :
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="groupBySelect"
|
|
||||||
className="form-select form-select-sm"
|
|
||||||
value={selectedGroup?.id || ""}
|
|
||||||
onChange={handleGroupChange}
|
|
||||||
>
|
|
||||||
{groupByList.map((group) => (
|
|
||||||
<option key={group.id} value={group.id}>
|
|
||||||
{group.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="d-flex justify-content-end py-3 gap-2">
|
<div className="d-flex justify-content-end py-3 gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-label-secondary btn-sm"
|
className="btn btn-label-secondary btn-sm"
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" className="btn btn-primary btn-sm">
|
<button type="submit" className="btn btn-primary btn-sm">
|
||||||
Apply
|
Apply
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default ExpenseFilterPanel;
|
export default ExpenseFilterPanel;
|
||||||
@ -23,8 +23,15 @@ import { useSelector } from "react-redux";
|
|||||||
import ExpenseFilterChips from "./ExpenseFilterChips";
|
import ExpenseFilterChips from "./ExpenseFilterChips";
|
||||||
import { defaultFilter } from "./ExpenseSchema";
|
import { defaultFilter } from "./ExpenseSchema";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { displayName } from "react-quill";
|
||||||
|
|
||||||
const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRef, onDataFiltered }) => {
|
const ExpenseList = ({
|
||||||
|
filters,
|
||||||
|
groupBy,
|
||||||
|
searchText,
|
||||||
|
tableRef,
|
||||||
|
onDataFiltered,
|
||||||
|
}) => {
|
||||||
const [deletingId, setDeletingId] = useState(null);
|
const [deletingId, setDeletingId] = useState(null);
|
||||||
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const {
|
const {
|
||||||
@ -76,55 +83,65 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const groupByField = (items, field) => {
|
const groupByField = (items, field) => {
|
||||||
return items.reduce((acc, item) => {
|
if (!field || field === "none") {
|
||||||
let key;
|
return {
|
||||||
let displayField;
|
All: {
|
||||||
|
key: "All",
|
||||||
switch (field) {
|
displayField: "All",
|
||||||
case "transactionDate":
|
items: items || []
|
||||||
key = formatUTCToLocalTime(item?.transactionDate);
|
|
||||||
displayField = "Transaction Date";
|
|
||||||
break;
|
|
||||||
case "status":
|
|
||||||
key = item?.status?.displayName || "Unknown";
|
|
||||||
displayField = "Status";
|
|
||||||
break;
|
|
||||||
case "submittedBy":
|
|
||||||
key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? ""
|
|
||||||
}`.trim();
|
|
||||||
displayField = "Submitted By";
|
|
||||||
break;
|
|
||||||
case "project":
|
|
||||||
key = item?.project?.name || "Unknown Project";
|
|
||||||
displayField = "Project";
|
|
||||||
break;
|
|
||||||
case "paymentMode":
|
|
||||||
key = item?.paymentMode?.name || "Unknown Mode";
|
|
||||||
displayField = "Payment Mode";
|
|
||||||
break;
|
|
||||||
case "expenseCategory":
|
|
||||||
key = item?.expenseCategory?.name || "Unknown Type";
|
|
||||||
displayField = "Expense Category";
|
|
||||||
break;
|
|
||||||
case "createdAt":
|
|
||||||
key = item?.createdAt?.split("T")[0] || "Unknown Date";
|
|
||||||
displayField = "Created Date";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
key = "Others";
|
|
||||||
displayField = "Others";
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const groupKey = `${field}_${key}`; // unique key for object property
|
return items.reduce((acc, item) => {
|
||||||
if (!acc[groupKey]) {
|
let key;
|
||||||
acc[groupKey] = { key, displayField, items: [] };
|
let displayField;
|
||||||
}
|
|
||||||
|
switch (field) {
|
||||||
|
case "transactionDate":
|
||||||
|
key = formatUTCToLocalTime(item?.transactionDate);
|
||||||
|
displayField = "Transaction Date";
|
||||||
|
break;
|
||||||
|
case "status":
|
||||||
|
key = item?.status?.displayName || "Unknown";
|
||||||
|
displayField = "Status";
|
||||||
|
break;
|
||||||
|
case "submittedBy":
|
||||||
|
key = `${item?.createdBy?.firstName ?? ""} ${item?.createdBy?.lastName ?? ""}`.trim();
|
||||||
|
displayField = "Submitted By";
|
||||||
|
break;
|
||||||
|
case "project":
|
||||||
|
key = item?.project?.name || "Unknown Project";
|
||||||
|
displayField = "Project";
|
||||||
|
break;
|
||||||
|
case "paymentMode":
|
||||||
|
key = item?.paymentMode?.name || "Unknown Mode";
|
||||||
|
displayField = "Payment Mode";
|
||||||
|
break;
|
||||||
|
case "expenseCategory":
|
||||||
|
key = item?.expenseCategory?.name || "Unknown Type";
|
||||||
|
displayField = "Expense Category";
|
||||||
|
break;
|
||||||
|
case "createdAt":
|
||||||
|
key = item?.createdAt?.split("T")[0] || "Unknown Date";
|
||||||
|
displayField = "Created Date";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
key = "Others";
|
||||||
|
displayField = "Others";
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupKey = `${field}_${key}`;
|
||||||
|
if (!acc[groupKey]) {
|
||||||
|
acc[groupKey] = { key, displayField, items: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[groupKey].items.push(item);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
acc[groupKey].items.push(item);
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const expenseColumns = [
|
const expenseColumns = [
|
||||||
{
|
{
|
||||||
@ -150,8 +167,9 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
|||||||
label: "Submitted By",
|
label: "Submitted By",
|
||||||
align: "text-start",
|
align: "text-start",
|
||||||
getValue: (e) =>
|
getValue: (e) =>
|
||||||
`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
|
`${e.createdBy?.firstName ?? ""} ${
|
||||||
}`.trim() || "N/A",
|
e.createdBy?.lastName ?? ""
|
||||||
|
}`.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"
|
||||||
@ -164,8 +182,9 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
|||||||
lastName={e.createdBy?.lastName}
|
lastName={e.createdBy?.lastName}
|
||||||
/>
|
/>
|
||||||
<span className="text-truncate">
|
<span className="text-truncate">
|
||||||
{`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
|
{`${e.createdBy?.firstName ?? ""} ${
|
||||||
}`.trim() || "N/A"}
|
e.createdBy?.lastName ?? ""
|
||||||
|
}`.trim() || "N/A"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@ -197,8 +216,9 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
|||||||
align: "text-center",
|
align: "text-center",
|
||||||
getValue: (e) => (
|
getValue: (e) => (
|
||||||
<span
|
<span
|
||||||
className={`badge bg-label-${getColorNameFromHex(e?.status?.color) || "secondary"
|
className={`badge bg-label-${
|
||||||
}`}
|
getColorNameFromHex(e?.status?.color) || "secondary"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{e.status?.name || "Unknown"}
|
{e.status?.name || "Unknown"}
|
||||||
</span>
|
</span>
|
||||||
@ -218,12 +238,18 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
|||||||
return <ExpenseTableSkeleton headers={headers} />;
|
return <ExpenseTableSkeleton headers={headers} />;
|
||||||
if (isError) return <div>{error?.message}</div>;
|
if (isError) return <div>{error?.message}</div>;
|
||||||
|
|
||||||
const grouped = groupBy
|
const isNoGrouping = !groupBy || groupBy === "none";
|
||||||
? groupByField(data?.data ?? [], groupBy)
|
const grouped = isNoGrouping
|
||||||
: { All: data?.data ?? [] };
|
? { All: { key: "All", displayField: "All", items: data?.data ?? [] } }
|
||||||
|
: groupByField(data?.data ?? [], groupBy);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const IsGroupedByDate = [
|
const IsGroupedByDate = [
|
||||||
|
{key:"none",displayField:"None"},
|
||||||
{ key: "transactionDate", displayField: "Transaction Date" },
|
{ key: "transactionDate", displayField: "Transaction Date" },
|
||||||
{ key: "createdAt", displayField: "created Date" },
|
{ key: "createdAt", displayField: "created Date", },
|
||||||
]?.includes(groupBy);
|
]?.includes(groupBy);
|
||||||
|
|
||||||
const canEditExpense = (expense) => {
|
const canEditExpense = (expense) => {
|
||||||
@ -264,7 +290,8 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
|||||||
groupBy={groupBy}
|
groupBy={groupBy}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="card-datatable table-responsive" ref={tableRef}
|
className="card-datatable table-responsive"
|
||||||
|
ref={tableRef}
|
||||||
id="horizontal-example"
|
id="horizontal-example"
|
||||||
>
|
>
|
||||||
<div className="dataTables_wrapper no-footer px-2 ">
|
<div className="dataTables_wrapper no-footer px-2 ">
|
||||||
@ -292,21 +319,23 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
|||||||
{Object.keys(grouped).length > 0 ? (
|
{Object.keys(grouped).length > 0 ? (
|
||||||
Object.values(grouped).map(({ key, displayField, items }) => (
|
Object.values(grouped).map(({ key, displayField, items }) => (
|
||||||
<React.Fragment key={key}>
|
<React.Fragment key={key}>
|
||||||
<tr className="tr-group text-dark">
|
{!isNoGrouping && (
|
||||||
<td colSpan={8} className="text-start">
|
<tr className="tr-group text-dark">
|
||||||
<div className="d-flex align-items-center px-2">
|
<td colSpan={8} className="text-start">
|
||||||
{" "}
|
<div className="d-flex align-items-center px-2">
|
||||||
<small className="fs-6 py-1">
|
{" "}
|
||||||
{displayField} :{" "}
|
<small className="fs-6 py-1">
|
||||||
</small>{" "}
|
{displayField} :{" "}
|
||||||
<small className="fs-6 ms-3">
|
</small>{" "}
|
||||||
{IsGroupedByDate
|
<small className="fs-6 ms-3">
|
||||||
? formatUTCToLocalTime(key)
|
{IsGroupedByDate
|
||||||
: key}
|
? formatUTCToLocalTime(key)
|
||||||
</small>
|
: key}
|
||||||
</div>
|
</small>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
{items?.map((expense) => (
|
{items?.map((expense) => (
|
||||||
<tr key={expense.id}>
|
<tr key={expense.id}>
|
||||||
{expenseColumns.map(
|
{expenseColumns.map(
|
||||||
@ -314,22 +343,26 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
|||||||
(col.isAlwaysVisible || groupBy !== col.key) && (
|
(col.isAlwaysVisible || groupBy !== col.key) && (
|
||||||
<td
|
<td
|
||||||
key={col.key}
|
key={col.key}
|
||||||
className={`d-table-cell ml-2 ${col.align ?? ""
|
className={`d-table-cell ml-2 ${
|
||||||
} `}
|
col.align ?? ""
|
||||||
|
} `}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`d-flex px-2 ${col.key === "status"
|
className={`d-flex px-2 ${
|
||||||
? "justify-content-center"
|
col.key === "status"
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${col.key === "amount"
|
|
||||||
? "justify-content-end"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${col.key === "submitted"
|
|
||||||
? "justify-content-center"
|
? "justify-content-center"
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
|
${
|
||||||
|
col.key === "amount"
|
||||||
|
? "justify-content-end"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
col.key === "submitted"
|
||||||
|
? "justify-content-center"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{col.customRender
|
{col.customRender
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { error } from "pdf-lib";
|
import { error } from "pdf-lib";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import { iframeDocuments } from "../../utils/constants";
|
||||||
|
|
||||||
const PreviewDocument = ({ files = [] }) => {
|
const PreviewDocument = ({ files = [] }) => {
|
||||||
const images = Array.isArray(files) ? files : [files];
|
const images = Array.isArray(files) ? files : [files];
|
||||||
@ -19,7 +20,9 @@ const PreviewDocument = ({ files = [] }) => {
|
|||||||
const currentFile = images[index];
|
const currentFile = images[index];
|
||||||
const fileUrl = currentFile?.preSignedUrl;
|
const fileUrl = currentFile?.preSignedUrl;
|
||||||
|
|
||||||
const isPDF = fileUrl?.toLowerCase().endsWith(".pdf");
|
const isDocumentType = iframeDocuments.includes(
|
||||||
|
currentFile?.contentType.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRotation(0);
|
setRotation(0);
|
||||||
@ -28,8 +31,10 @@ const PreviewDocument = ({ files = [] }) => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
}, [index]);
|
}, [index]);
|
||||||
|
|
||||||
const zoomIn = () => !isPDF && setScale((prev) => Math.min(prev + 0.2, MAX_ZOOM));
|
const zoomIn = () =>
|
||||||
const zoomOut = () => !isPDF && setScale((prev) => Math.max(prev - 0.2, MIN_ZOOM));
|
!isDocumentType && setScale((prev) => Math.min(prev + 0.2, MAX_ZOOM));
|
||||||
|
const zoomOut = () =>
|
||||||
|
!isDocumentType && setScale((prev) => Math.max(prev - 0.2, MIN_ZOOM));
|
||||||
|
|
||||||
const resetAll = () => {
|
const resetAll = () => {
|
||||||
setRotation(0);
|
setRotation(0);
|
||||||
@ -46,7 +51,7 @@ const PreviewDocument = ({ files = [] }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseDown = (e) => {
|
const handleMouseDown = (e) => {
|
||||||
if (isPDF) return;
|
if (isDocumentType) return;
|
||||||
setDragging(true);
|
setDragging(true);
|
||||||
startPos.current = {
|
startPos.current = {
|
||||||
x: e.clientX - position.x,
|
x: e.clientX - position.x,
|
||||||
@ -55,7 +60,7 @@ const PreviewDocument = ({ files = [] }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseMove = (e) => {
|
const handleMouseMove = (e) => {
|
||||||
if (!dragging || isPDF) return;
|
if (!dragging || isDocumentType) return;
|
||||||
|
|
||||||
setPosition({
|
setPosition({
|
||||||
x: e.clientX - startPos.current.x,
|
x: e.clientX - startPos.current.x,
|
||||||
@ -65,114 +70,107 @@ const PreviewDocument = ({ files = [] }) => {
|
|||||||
|
|
||||||
const handleMouseUp = () => setDragging(false);
|
const handleMouseUp = () => setDragging(false);
|
||||||
|
|
||||||
const handleDoubleClick = () => !isPDF && resetAll();
|
const handleDoubleClick = () => !isDocumentType && resetAll();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
<div className="d-flex justify-content-start align-items-center mb-2">
|
<div className="d-flex justify-content-start align-items-center mb-2">
|
||||||
<div className="d-flex gap-3">
|
<div className="d-flex gap-3">
|
||||||
{!isPDF && (
|
{!isDocumentType && (
|
||||||
<>
|
<>
|
||||||
<i
|
<i
|
||||||
className="bx bx-rotate-right cursor-pointer fs-4"
|
className="bx bx-rotate-right cursor-pointer fs-4"
|
||||||
onClick={() => setRotation((prev) => prev + 90)}
|
onClick={() => setRotation((prev) => prev + 90)}
|
||||||
title="Rotate"
|
title="Rotate"
|
||||||
/>
|
/>
|
||||||
<i
|
<i
|
||||||
className="bx bx-zoom-in cursor-pointer fs-4"
|
className="bx bx-zoom-in cursor-pointer fs-4"
|
||||||
onClick={zoomIn}
|
onClick={zoomIn}
|
||||||
title="Zoom In"
|
title="Zoom In"
|
||||||
/>
|
/>
|
||||||
<i
|
<i
|
||||||
className="bx bx-zoom-out cursor-pointer fs-4"
|
className="bx bx-zoom-out cursor-pointer fs-4"
|
||||||
onClick={zoomOut}
|
onClick={zoomOut}
|
||||||
title="Zoom Out"
|
title="Zoom Out"
|
||||||
/>
|
/>
|
||||||
<i
|
<i
|
||||||
className="bx bx-reset cursor-pointer fs-4"
|
className="bx bx-reset cursor-pointer fs-4"
|
||||||
onClick={resetAll}
|
onClick={resetAll}
|
||||||
title="Reset"
|
title="Reset"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
onMouseLeave={handleMouseUp}
|
onMouseLeave={handleMouseUp}
|
||||||
onDoubleClick={handleDoubleClick}
|
onDoubleClick={handleDoubleClick}
|
||||||
className="position-relative d-flex justify-content-center align-items-center bg-light-secondary overflow-hidden"
|
className="position-relative d-flex justify-content-center align-items-center bg-light-secondary overflow-hidden"
|
||||||
|
style={{
|
||||||
|
minHeight: "70vh",
|
||||||
|
userSelect: "none",
|
||||||
|
borderRadius: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading && <div className="text-secondary">Loading...</div>}
|
||||||
|
|
||||||
|
{isDocumentType ? (
|
||||||
|
<iframe
|
||||||
|
src={fileUrl}
|
||||||
|
title="Document Preview"
|
||||||
style={{
|
style={{
|
||||||
minHeight: "70vh",
|
width: "100%",
|
||||||
|
height: "70vh",
|
||||||
userSelect: "none",
|
border: "none",
|
||||||
borderRadius: "10px",
|
|
||||||
}}
|
}}
|
||||||
>
|
onLoad={() => setLoading(false)}
|
||||||
{loading && <div className="text-secondary">Loading...</div>}
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={fileUrl}
|
||||||
|
alt="Preview"
|
||||||
|
draggable="false"
|
||||||
|
style={{
|
||||||
|
maxHeight: "60vh",
|
||||||
|
display: loading ? "none" : "block",
|
||||||
|
transform: `
|
||||||
|
translate(${position.x}px, ${position.y}px)
|
||||||
|
scale(${scale})
|
||||||
|
rotate(${rotation}deg)
|
||||||
|
`,
|
||||||
|
transition: dragging ? "none" : "transform 0.2s ease",
|
||||||
|
cursor: dragging ? "grabbing" : "grab",
|
||||||
|
}}
|
||||||
|
onLoad={() => setLoading(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* PDF VIEW */}
|
<div className="d-flex justify-content-between mt-2">
|
||||||
{isPDF ? (
|
<div className="text-muted small">
|
||||||
<iframe
|
Scroll = change file | Double click = reset (images only)
|
||||||
src={"./Expenses.pdf"}
|
</div>
|
||||||
title="PDF Preview"
|
<div className="d-flex align-items-center gap-2">
|
||||||
style={{
|
<i
|
||||||
width: "100%",
|
className="bx bx-chevron-left cursor-pointer fs-4"
|
||||||
height: "70vh",
|
onClick={prevImage}
|
||||||
border: "none",
|
/>
|
||||||
}}
|
<span>
|
||||||
onLoad={() => setLoading(false)}
|
{index + 1} / {images.length}
|
||||||
onError={(error)=>{
|
</span>
|
||||||
console.log(error)
|
<i
|
||||||
}}
|
className="bx bx-chevron-right cursor-pointer fs-4"
|
||||||
/>
|
onClick={nextImage}
|
||||||
) : (
|
/>
|
||||||
/* IMAGE VIEW */
|
</div>
|
||||||
<img
|
</div>
|
||||||
src={fileUrl}
|
</>
|
||||||
alt="Preview"
|
|
||||||
draggable="false"
|
|
||||||
style={{
|
|
||||||
maxHeight: "60vh",
|
|
||||||
display: loading ? "none" : "block",
|
|
||||||
transform: `
|
|
||||||
translate(${position.x}px, ${position.y}px)
|
|
||||||
scale(${scale})
|
|
||||||
rotate(${rotation}deg)
|
|
||||||
`,
|
|
||||||
transition: dragging ? "none" : "transform 0.2s ease",
|
|
||||||
cursor: dragging ? "grabbing" : "grab",
|
|
||||||
}}
|
|
||||||
onLoad={() => setLoading(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className="d-flex justify-content-between">
|
|
||||||
<div className="text-center text-muted mt-2 small">
|
|
||||||
Scroll = change file | Double click = reset (images only)
|
|
||||||
</div>
|
|
||||||
<div className="d-flex align-items-center gap-2">
|
|
||||||
<i
|
|
||||||
className="bx bx-chevron-left cursor-pointer fs-4"
|
|
||||||
onClick={prevImage}
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
{index + 1} / {images.length}
|
|
||||||
</span>
|
|
||||||
<i
|
|
||||||
className="bx bx-chevron-right cursor-pointer fs-4"
|
|
||||||
onClick={nextImage}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const Error = ({error,close}) => {
|
const Error = ({error,close}) => {
|
||||||
console.log(error)
|
|
||||||
return (
|
return (
|
||||||
<div className="container text-center py-5">
|
<div className="container text-center py-5">
|
||||||
<h1 className="display-4 fw-bold text-danger">{error.statusCode || error?.response?.status
|
<h1 className="display-4 fw-bold text-danger">{error.statusCode || error?.response?.status
|
||||||
|
|||||||
@ -124,7 +124,6 @@ export const useUploadDocument = (onSuccessCallBack) => {
|
|||||||
if (onSuccessCallBack) onSuccessCallBack();
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.log(error);
|
|
||||||
showToast(
|
showToast(
|
||||||
error.response.data.message ||
|
error.response.data.message ||
|
||||||
"Something went wrong please try again !",
|
"Something went wrong please try again !",
|
||||||
@ -145,7 +144,6 @@ export const useUpdateDocument = (onSuccessCallBack) => {
|
|||||||
if (onSuccessCallBack) onSuccessCallBack();
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.log(error);
|
|
||||||
showToast(
|
showToast(
|
||||||
error.response.data.message ||
|
error.response.data.message ||
|
||||||
"Something went wrong please try again !",
|
"Something went wrong please try again !",
|
||||||
|
|||||||
@ -50,7 +50,7 @@ const ExpensePage = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [filters, setFilters] = useState(defaultFilter);
|
const [filters, setFilters] = useState(defaultFilter);
|
||||||
const [groupBy, setGroupBy] = useState("transactionDate");
|
const [groupBy, setGroupBy] = useState("none");
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const filterPanelRef = useRef();
|
const filterPanelRef = useRef();
|
||||||
const [ManageExpenseModal, setManageExpenseModal] = useState({
|
const [ManageExpenseModal, setManageExpenseModal] = useState({
|
||||||
|
|||||||
@ -7,6 +7,19 @@ export const DURATION_TIME = 10; // minutes
|
|||||||
export const ITEMS_PER_PAGE = 20;
|
export const ITEMS_PER_PAGE = 20;
|
||||||
export const OTP_EXPIRY_SECONDS = 300; // OTP time
|
export const OTP_EXPIRY_SECONDS = 300; // OTP time
|
||||||
|
|
||||||
|
export const iframeDocuments = [
|
||||||
|
"application/pdf",
|
||||||
|
"application/msword",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"application/vnd.ms-excel",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"application/mspowerpoint",
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
"text/plain",
|
||||||
|
"application/rtf",
|
||||||
|
"text/csv",
|
||||||
|
];
|
||||||
|
|
||||||
export const MANAGE_MASTER = "588a8824-f924-4955-82d8-fc51956cf323";
|
export const MANAGE_MASTER = "588a8824-f924-4955-82d8-fc51956cf323";
|
||||||
|
|
||||||
export const VIEW_MASTER = "5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d";
|
export const VIEW_MASTER = "5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user