Merge branch 'Purchase_Invoice_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Weidget_Dashboard_Services

This commit is contained in:
Kartik Sharma 2025-12-09 17:34:44 +05:30
commit b20dd2d0d4
10 changed files with 1258 additions and 725 deletions

File diff suppressed because it is too large Load Diff

View File

@ -42,7 +42,12 @@ const HorizontalBarChart = ({
categories.length === seriesData.length;
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
const combined = seriesData.map((value, index) => ({

View File

@ -12,14 +12,17 @@ const ProjectCompletionChart = () => {
isError,
error,
} = useProjectCompletionStatus();
const projectNames = projects?.map((p) => p.name) || [];
const projectProgress =
projects?.map((p) => {
const completed = p.completedWork || 0;
const planned = p.plannedWork || 1;
const percent = planned ? (completed / planned) * 100 : 0;
return Math.min(Math.round(percent), 100);
}) || [];
const filteredProjects = projects?.filter((p) => p.completedWork > 0) || [];
const projectNames = filteredProjects.map((p) => p.name);
const projectProgress = filteredProjects.map((p) => {
const completed = p.completedWork || 0;
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 (
<div className="card h-100">

View File

@ -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 { zodResolver } from "@hookform/resolvers/zod";
import { defaultFilter, SearchSchema } from "./ExpenseSchema";
@ -15,282 +21,291 @@ import { useExpenseFilter } from "../../hooks/useExpense";
import { ExpenseFilterSkeleton } from "./ExpenseSkeleton";
import { useLocation, useNavigate, useParams } from "react-router-dom";
const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }, ref) => {
const { status } = useParams();
const navigate = useNavigate();
const selectedProjectId = useSelector(
(store) => store.localVariables.projectId
);
const { data, isLoading, isError, error, isFetching, isFetched } =
useExpenseFilter();
const ExpenseFilterPanel = forwardRef(
({ onApply, handleGroupBy, setFilterdata }, ref) => {
const { status } = useParams();
const navigate = useNavigate();
const selectedProjectId = useSelector(
(store) => store.localVariables.projectId
);
const { data, isLoading, isError, error, isFetching, isFetched } =
useExpenseFilter();
const groupByList = useMemo(() => {
return [
{ id: "transactionDate", name: "Transaction Date" },
{ id: "status", name: "Status" },
{ id: "submittedBy", name: "Submitted By" },
{ id: "project", name: "Project" },
{ id: "paymentMode", name: "Payment Mode" },
{ id: "expenseCategory", name: "Expense Category" },
{ id: "createdAt", name: "Submitted Date" },
].sort((a, b) => a.name.localeCompare(b.name));
}, []);
const groupByList = useMemo(() => {
return [
{ id: "none", name: "None" },
{ id: "transactionDate", name: "Transaction Date" },
{ id: "status", name: "Status" },
{ id: "submittedBy", name: "Submitted By" },
{ id: "project", name: "Project" },
{ id: "paymentMode", name: "Payment Mode" },
{ id: "expenseCategory", name: "Expense Category" },
{ id: "createdAt", name: "Submitted Date" },
].sort((a, b) => a.name.localeCompare(b.name));
}, []);
const [selectedGroup, setSelectedGroup] = useState(groupByList[6]);
const [resetKey, setResetKey] = useState(0);
const [selectedGroup, setSelectedGroup] = useState(groupByList[0]);
const [resetKey, setResetKey] = useState(0);
const dynamicDefaultFilter = useMemo(() => {
return {
...defaultFilter,
statusIds: status ? [status] : defaultFilter.statusIds || [],
projectIds: defaultFilter.projectIds || [],
createdByIds: defaultFilter.createdByIds || [],
paidById: defaultFilter.paidById || [],
expenseCategoryIds: defaultFilter.expenseCategoryIds || [],
isTransactionDate: defaultFilter.isTransactionDate ?? true,
startDate: defaultFilter.startDate,
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,
const dynamicDefaultFilter = useMemo(() => {
return {
...defaultFilter,
statusIds: status ? [status] : defaultFilter.statusIds || [],
projectIds: defaultFilter.projectIds || [],
createdByIds: defaultFilter.createdByIds || [],
paidById: defaultFilter.paidById || [],
expenseCategoryIds: defaultFilter.expenseCategoryIds || [],
isTransactionDate: defaultFilter.isTransactionDate ?? true,
startDate: defaultFilter.startDate,
endDate: defaultFilter.endDate,
};
}, [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);
setAppliedStatusId(status);
}
}, [
status,
data,
dynamicDefaultFilter,
onApply,
handleGroupBy,
selectedGroup.id,
appliedStatusId,
selectedProjectId,
]);
// closePanel();
};
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
if (isError && isFetched)
return <div>Something went wrong Here- {error.message} </div>;
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);
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" : ""
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,
};
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)}
>
Transaction Date
</button>
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${!isTransactionDate ? "active btn-primary text-white" : ""
onClick={() => setValue("isTransactionDate", true)}
>
Transaction Date
</button>
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
!isTransactionDate ? "active btn-primary text-white" : ""
}`}
onClick={() => setValue("isTransactionDate", false)}
>
Submitted Date
</button>
onClick={() => setValue("isTransactionDate", false)}
>
Submitted Date
</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>
<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 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 || "none"}
onChange={handleGroupChange}
>
{groupByList.map((group) => (
<option key={group.id} value={group.id}>
{group.name}
</option>
))}
</select>
</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">
<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>
</>
);
});
<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 ExpenseFilterPanel;
export default ExpenseFilterPanel;

View File

@ -23,8 +23,15 @@ import { useSelector } from "react-redux";
import ExpenseFilterChips from "./ExpenseFilterChips";
import { defaultFilter } from "./ExpenseSchema";
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 [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const {
@ -46,7 +53,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
filters,
debouncedSearch
);
useEffect(() => {
if (onDataFiltered) {
onDataFiltered(data?.data ?? []);
@ -76,55 +83,65 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
}
};
const groupByField = (items, field) => {
return items.reduce((acc, item) => {
let key;
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 groupByField = (items, field) => {
if (!field || field === "none") {
return {
All: {
key: "All",
displayField: "All",
items: items || []
}
};
}
const groupKey = `${field}_${key}`; // unique key for object property
if (!acc[groupKey]) {
acc[groupKey] = { key, displayField, items: [] };
}
return items.reduce((acc, item) => {
let key;
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 = [
{
@ -150,8 +167,9 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
label: "Submitted By",
align: "text-start",
getValue: (e) =>
`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
}`.trim() || "N/A",
`${e.createdBy?.firstName ?? ""} ${
e.createdBy?.lastName ?? ""
}`.trim() || "N/A",
customRender: (e) => (
<div
className="d-flex align-items-center cursor-pointer"
@ -164,8 +182,9 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
lastName={e.createdBy?.lastName}
/>
<span className="text-truncate">
{`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
}`.trim() || "N/A"}
{`${e.createdBy?.firstName ?? ""} ${
e.createdBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
),
@ -197,8 +216,9 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
align: "text-center",
getValue: (e) => (
<span
className={`badge bg-label-${getColorNameFromHex(e?.status?.color) || "secondary"
}`}
className={`badge bg-label-${
getColorNameFromHex(e?.status?.color) || "secondary"
}`}
>
{e.status?.name || "Unknown"}
</span>
@ -218,12 +238,18 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
return <ExpenseTableSkeleton headers={headers} />;
if (isError) return <div>{error?.message}</div>;
const grouped = groupBy
? groupByField(data?.data ?? [], groupBy)
: { All: data?.data ?? [] };
const isNoGrouping = !groupBy || groupBy === "none";
const grouped = isNoGrouping
? { All: { key: "All", displayField: "All", items: data?.data ?? [] } }
: groupByField(data?.data ?? [], groupBy);
const IsGroupedByDate = [
{key:"none",displayField:"None"},
{ key: "transactionDate", displayField: "Transaction Date" },
{ key: "createdAt", displayField: "created Date" },
{ key: "createdAt", displayField: "created Date", },
]?.includes(groupBy);
const canEditExpense = (expense) => {
@ -264,7 +290,8 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
groupBy={groupBy}
/>
<div
className="card-datatable table-responsive" ref={tableRef}
className="card-datatable table-responsive"
ref={tableRef}
id="horizontal-example"
>
<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.values(grouped).map(({ key, displayField, items }) => (
<React.Fragment key={key}>
<tr className="tr-group text-dark">
<td colSpan={8} className="text-start">
<div className="d-flex align-items-center px-2">
{" "}
<small className="fs-6 py-1">
{displayField} :{" "}
</small>{" "}
<small className="fs-6 ms-3">
{IsGroupedByDate
? formatUTCToLocalTime(key)
: key}
</small>
</div>
</td>
</tr>
{!isNoGrouping && (
<tr className="tr-group text-dark">
<td colSpan={8} className="text-start">
<div className="d-flex align-items-center px-2">
{" "}
<small className="fs-6 py-1">
{displayField} :{" "}
</small>{" "}
<small className="fs-6 ms-3">
{IsGroupedByDate
? formatUTCToLocalTime(key)
: key}
</small>
</div>
</td>
</tr>
)}
{items?.map((expense) => (
<tr key={expense.id}>
{expenseColumns.map(
@ -314,22 +343,26 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
(col.isAlwaysVisible || groupBy !== col.key) && (
<td
key={col.key}
className={`d-table-cell ml-2 ${col.align ?? ""
} `}
className={`d-table-cell ml-2 ${
col.align ?? ""
} `}
>
<div
className={`d-flex px-2 ${col.key === "status"
? "justify-content-center"
: ""
}
${col.key === "amount"
? "justify-content-end"
: ""
}
${col.key === "submitted"
className={`d-flex px-2 ${
col.key === "status"
? "justify-content-center"
: ""
}
}
${
col.key === "amount"
? "justify-content-end"
: ""
}
${
col.key === "submitted"
? "justify-content-center"
: ""
}
`}
>
{col.customRender

View File

@ -1,5 +1,6 @@
import { error } from "pdf-lib";
import { useState, useRef, useEffect } from "react";
import { iframeDocuments } from "../../utils/constants";
const PreviewDocument = ({ files = [] }) => {
const images = Array.isArray(files) ? files : [files];
@ -19,7 +20,9 @@ const PreviewDocument = ({ files = [] }) => {
const currentFile = images[index];
const fileUrl = currentFile?.preSignedUrl;
const isPDF = fileUrl?.toLowerCase().endsWith(".pdf");
const isDocumentType = iframeDocuments.includes(
currentFile?.contentType.toLowerCase()
);
useEffect(() => {
setRotation(0);
@ -28,8 +31,10 @@ const PreviewDocument = ({ files = [] }) => {
setLoading(true);
}, [index]);
const zoomIn = () => !isPDF && setScale((prev) => Math.min(prev + 0.2, MAX_ZOOM));
const zoomOut = () => !isPDF && setScale((prev) => Math.max(prev - 0.2, MIN_ZOOM));
const zoomIn = () =>
!isDocumentType && setScale((prev) => Math.min(prev + 0.2, MAX_ZOOM));
const zoomOut = () =>
!isDocumentType && setScale((prev) => Math.max(prev - 0.2, MIN_ZOOM));
const resetAll = () => {
setRotation(0);
@ -46,7 +51,7 @@ const PreviewDocument = ({ files = [] }) => {
};
const handleMouseDown = (e) => {
if (isPDF) return;
if (isDocumentType) return;
setDragging(true);
startPos.current = {
x: e.clientX - position.x,
@ -55,7 +60,7 @@ const PreviewDocument = ({ files = [] }) => {
};
const handleMouseMove = (e) => {
if (!dragging || isPDF) return;
if (!dragging || isDocumentType) return;
setPosition({
x: e.clientX - startPos.current.x,
@ -65,114 +70,107 @@ const PreviewDocument = ({ files = [] }) => {
const handleMouseUp = () => setDragging(false);
const handleDoubleClick = () => !isPDF && resetAll();
const handleDoubleClick = () => !isDocumentType && resetAll();
return (
<>
{/* Controls */}
<div className="d-flex justify-content-start align-items-center mb-2">
<div className="d-flex gap-3">
{!isPDF && (
<>
<i
className="bx bx-rotate-right cursor-pointer fs-4"
onClick={() => setRotation((prev) => prev + 90)}
title="Rotate"
/>
<i
className="bx bx-zoom-in cursor-pointer fs-4"
onClick={zoomIn}
title="Zoom In"
/>
<i
className="bx bx-zoom-out cursor-pointer fs-4"
onClick={zoomOut}
title="Zoom Out"
/>
<i
className="bx bx-reset cursor-pointer fs-4"
onClick={resetAll}
title="Reset"
/>
</>
)}
</div>
</div>
<>
{/* Controls */}
<div className="d-flex justify-content-start align-items-center mb-2">
<div className="d-flex gap-3">
{!isDocumentType && (
<>
<i
className="bx bx-rotate-right cursor-pointer fs-4"
onClick={() => setRotation((prev) => prev + 90)}
title="Rotate"
/>
<i
className="bx bx-zoom-in cursor-pointer fs-4"
onClick={zoomIn}
title="Zoom In"
/>
<i
className="bx bx-zoom-out cursor-pointer fs-4"
onClick={zoomOut}
title="Zoom Out"
/>
<i
className="bx bx-reset cursor-pointer fs-4"
onClick={resetAll}
title="Reset"
/>
</>
)}
</div>
</div>
<div
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onDoubleClick={handleDoubleClick}
className="position-relative d-flex justify-content-center align-items-center bg-light-secondary overflow-hidden"
<div
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onDoubleClick={handleDoubleClick}
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={{
minHeight: "70vh",
userSelect: "none",
borderRadius: "10px",
width: "100%",
height: "70vh",
border: "none",
}}
>
{loading && <div className="text-secondary">Loading...</div>}
onLoad={() => setLoading(false)}
/>
) : (
<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 */}
{isPDF ? (
<iframe
src={"./Expenses.pdf"}
title="PDF Preview"
style={{
width: "100%",
height: "70vh",
border: "none",
}}
onLoad={() => setLoading(false)}
onError={(error)=>{
console.log(error)
}}
/>
) : (
/* IMAGE VIEW */
<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>
<div className="d-flex justify-content-between mt-2">
<div className="text-muted 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>
</>
<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>
</>
);
};

View File

@ -1,7 +1,6 @@
import React from 'react'
const Error = ({error,close}) => {
console.log(error)
return (
<div className="container text-center py-5">
<h1 className="display-4 fw-bold text-danger">{error.statusCode || error?.response?.status

View File

@ -124,7 +124,6 @@ export const useUploadDocument = (onSuccessCallBack) => {
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
console.log(error);
showToast(
error.response.data.message ||
"Something went wrong please try again !",
@ -145,7 +144,6 @@ export const useUpdateDocument = (onSuccessCallBack) => {
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
console.log(error);
showToast(
error.response.data.message ||
"Something went wrong please try again !",

View File

@ -50,7 +50,7 @@ const ExpensePage = () => {
);
const [filters, setFilters] = useState(defaultFilter);
const [groupBy, setGroupBy] = useState("transactionDate");
const [groupBy, setGroupBy] = useState("none");
const [searchText, setSearchText] = useState("");
const filterPanelRef = useRef();
const [ManageExpenseModal, setManageExpenseModal] = useState({

View File

@ -7,6 +7,19 @@ export const DURATION_TIME = 10; // minutes
export const ITEMS_PER_PAGE = 20;
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 VIEW_MASTER = "5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d";