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;
|
||||
|
||||
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) => ({
|
||||
|
||||
@ -12,14 +12,17 @@ const ProjectCompletionChart = () => {
|
||||
isError,
|
||||
error,
|
||||
} = useProjectCompletionStatus();
|
||||
const projectNames = projects?.map((p) => p.name) || [];
|
||||
const projectProgress =
|
||||
projects?.map((p) => {
|
||||
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(Math.round(percent), 100);
|
||||
}) || [];
|
||||
return Math.min(parseFloat(percent.toFixed(2)), 100); // limit to 2 decimals
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<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 { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { defaultFilter, SearchSchema } from "./ExpenseSchema";
|
||||
@ -15,7 +21,8 @@ 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 ExpenseFilterPanel = forwardRef(
|
||||
({ onApply, handleGroupBy, setFilterdata }, ref) => {
|
||||
const { status } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const selectedProjectId = useSelector(
|
||||
@ -26,6 +33,7 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
|
||||
|
||||
const groupByList = useMemo(() => {
|
||||
return [
|
||||
{ id: "none", name: "None" },
|
||||
{ id: "transactionDate", name: "Transaction Date" },
|
||||
{ id: "status", name: "Status" },
|
||||
{ id: "submittedBy", name: "Submitted By" },
|
||||
@ -36,7 +44,7 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
|
||||
].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 dynamicDefaultFilter = useMemo(() => {
|
||||
@ -124,12 +132,18 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
|
||||
if (status !== appliedStatusId) {
|
||||
const filterWithStatus = {
|
||||
...dynamicDefaultFilter,
|
||||
projectIds: selectedProjectId ? [selectedProjectId] : dynamicDefaultFilter.projectIds || [],
|
||||
projectIds: selectedProjectId
|
||||
? [selectedProjectId]
|
||||
: dynamicDefaultFilter.projectIds || [],
|
||||
startDate: dynamicDefaultFilter.startDate
|
||||
? moment.utc(dynamicDefaultFilter.startDate, "DD-MM-YYYY").toISOString()
|
||||
? moment
|
||||
.utc(dynamicDefaultFilter.startDate, "DD-MM-YYYY")
|
||||
.toISOString()
|
||||
: undefined,
|
||||
endDate: dynamicDefaultFilter.endDate
|
||||
? moment.utc(dynamicDefaultFilter.endDate, "DD-MM-YYYY").toISOString()
|
||||
? moment
|
||||
.utc(dynamicDefaultFilter.endDate, "DD-MM-YYYY")
|
||||
.toISOString()
|
||||
: undefined,
|
||||
};
|
||||
|
||||
@ -152,8 +166,6 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
|
||||
if (isError && isFetched)
|
||||
return <div>Something went wrong Here- {error.message} </div>;
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...methods}>
|
||||
@ -164,7 +176,8 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
|
||||
<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" : ""
|
||||
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
||||
isTransactionDate ? "active btn-primary text-white" : ""
|
||||
}`}
|
||||
onClick={() => setValue("isTransactionDate", true)}
|
||||
>
|
||||
@ -172,7 +185,8 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
|
||||
</button>
|
||||
<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)}
|
||||
>
|
||||
@ -264,7 +278,7 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
|
||||
<select
|
||||
id="groupBySelect"
|
||||
className="form-select form-select-sm"
|
||||
value={selectedGroup?.id || ""}
|
||||
value={selectedGroup?.id || "none"}
|
||||
onChange={handleGroupChange}
|
||||
>
|
||||
{groupByList.map((group) => (
|
||||
@ -291,6 +305,7 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
|
||||
</FormProvider>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default ExpenseFilterPanel;
|
||||
@ -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 {
|
||||
@ -77,6 +84,16 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
||||
};
|
||||
|
||||
const groupByField = (items, field) => {
|
||||
if (!field || field === "none") {
|
||||
return {
|
||||
All: {
|
||||
key: "All",
|
||||
displayField: "All",
|
||||
items: items || []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return items.reduce((acc, item) => {
|
||||
let key;
|
||||
let displayField;
|
||||
@ -91,8 +108,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
||||
displayField = "Status";
|
||||
break;
|
||||
case "submittedBy":
|
||||
key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? ""
|
||||
}`.trim();
|
||||
key = `${item?.createdBy?.firstName ?? ""} ${item?.createdBy?.lastName ?? ""}`.trim();
|
||||
displayField = "Submitted By";
|
||||
break;
|
||||
case "project":
|
||||
@ -116,7 +132,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
||||
displayField = "Others";
|
||||
}
|
||||
|
||||
const groupKey = `${field}_${key}`; // unique key for object property
|
||||
const groupKey = `${field}_${key}`;
|
||||
if (!acc[groupKey]) {
|
||||
acc[groupKey] = { key, displayField, items: [] };
|
||||
}
|
||||
@ -126,6 +142,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
||||
}, {});
|
||||
};
|
||||
|
||||
|
||||
const expenseColumns = [
|
||||
{
|
||||
key: "expenseUId",
|
||||
@ -150,7 +167,8 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
||||
label: "Submitted By",
|
||||
align: "text-start",
|
||||
getValue: (e) =>
|
||||
`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
|
||||
`${e.createdBy?.firstName ?? ""} ${
|
||||
e.createdBy?.lastName ?? ""
|
||||
}`.trim() || "N/A",
|
||||
customRender: (e) => (
|
||||
<div
|
||||
@ -164,7 +182,8 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
||||
lastName={e.createdBy?.lastName}
|
||||
/>
|
||||
<span className="text-truncate">
|
||||
{`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
|
||||
{`${e.createdBy?.firstName ?? ""} ${
|
||||
e.createdBy?.lastName ?? ""
|
||||
}`.trim() || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
@ -197,7 +216,8 @@ 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"}
|
||||
@ -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,6 +319,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
||||
{Object.keys(grouped).length > 0 ? (
|
||||
Object.values(grouped).map(({ key, displayField, items }) => (
|
||||
<React.Fragment key={key}>
|
||||
{!isNoGrouping && (
|
||||
<tr className="tr-group text-dark">
|
||||
<td colSpan={8} className="text-start">
|
||||
<div className="d-flex align-items-center px-2">
|
||||
@ -307,6 +335,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText, tableRe
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{items?.map((expense) => (
|
||||
<tr key={expense.id}>
|
||||
{expenseColumns.map(
|
||||
@ -314,19 +343,23 @@ 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"
|
||||
className={`d-flex px-2 ${
|
||||
col.key === "status"
|
||||
? "justify-content-center"
|
||||
: ""
|
||||
}
|
||||
${col.key === "amount"
|
||||
${
|
||||
col.key === "amount"
|
||||
? "justify-content-end"
|
||||
: ""
|
||||
}
|
||||
${col.key === "submitted"
|
||||
${
|
||||
col.key === "submitted"
|
||||
? "justify-content-center"
|
||||
: ""
|
||||
}
|
||||
|
||||
@ -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,14 +70,14 @@ 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 && (
|
||||
{!isDocumentType && (
|
||||
<>
|
||||
<i
|
||||
className="bx bx-rotate-right cursor-pointer fs-4"
|
||||
@ -108,30 +113,24 @@ const PreviewDocument = ({ files = [] }) => {
|
||||
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>}
|
||||
|
||||
{/* PDF VIEW */}
|
||||
{isPDF ? (
|
||||
{isDocumentType ? (
|
||||
<iframe
|
||||
src={"./Expenses.pdf"}
|
||||
title="PDF Preview"
|
||||
src={fileUrl}
|
||||
title="Document Preview"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "70vh",
|
||||
border: "none",
|
||||
}}
|
||||
onLoad={() => setLoading(false)}
|
||||
onError={(error)=>{
|
||||
console.log(error)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
/* IMAGE VIEW */
|
||||
<img
|
||||
src={fileUrl}
|
||||
alt="Preview"
|
||||
@ -152,10 +151,8 @@ const PreviewDocument = ({ files = [] }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="d-flex justify-content-between">
|
||||
<div className="text-center text-muted mt-2 small">
|
||||
<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">
|
||||
@ -173,6 +170,7 @@ const PreviewDocument = ({ files = [] }) => {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 !",
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user