From a7f1ba97c37e175519b7c3a5fc77c2549a4fb6c9 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Tue, 14 Oct 2025 17:14:06 +0530 Subject: [PATCH] Creating new weidgets in Dashboard in main. --- src/components/Dashboard/Dashboard.jsx | 21 ++- src/components/Dashboard/ExpenseAnalysis.jsx | 154 ++++++++++++++++ src/components/Dashboard/ExpenseByProject.jsx | 164 ++++++++++++++++++ src/components/Dashboard/ExpenseStatus.jsx | 157 +++++++++++++++++ .../Expenses/ExpenseFilterPanel.jsx | 21 ++- src/components/Expenses/ExpenseList.jsx | 26 +-- src/hooks/useDashboard_Data.jsx | 97 ++++++++--- src/repositories/GlobalRepository.jsx | 76 ++++++-- src/router/AppRoutes.jsx | 2 +- src/utils/appUtils.js | 63 ++++--- src/utils/constants.jsx | 9 + src/utils/dateUtils.jsx | 22 ++- 12 files changed, 714 insertions(+), 98 deletions(-) create mode 100644 src/components/Dashboard/ExpenseAnalysis.jsx create mode 100644 src/components/Dashboard/ExpenseByProject.jsx create mode 100644 src/components/Dashboard/ExpenseStatus.jsx diff --git a/src/components/Dashboard/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx index 311fa763..cc22006e 100644 --- a/src/components/Dashboard/Dashboard.jsx +++ b/src/components/Dashboard/Dashboard.jsx @@ -14,6 +14,9 @@ import ProjectCompletionChart from "./ProjectCompletionChart"; import ProjectProgressChart from "./ProjectProgressChart"; import ProjectOverview from "../Project/ProjectOverview"; import AttendanceOverview from "./AttendanceChart"; +import ExpenseAnalysis from "./ExpenseAnalysis"; +import ExpenseStatus from "./ExpenseStatus"; +import ExpenseByProject from "./ExpenseByProject"; const Dashboard = () => { const { projectsCardData } = useDashboardProjectsCardData(); @@ -56,11 +59,25 @@ const Dashboard = () => {
+
+
+ +
+
+ +
+
+ +
+
{!isAllProjectsSelected && ( -
- {/* ✅ Removed unnecessary projectId prop */} +
+
)} +
+ +
); diff --git a/src/components/Dashboard/ExpenseAnalysis.jsx b/src/components/Dashboard/ExpenseAnalysis.jsx new file mode 100644 index 00000000..78126d21 --- /dev/null +++ b/src/components/Dashboard/ExpenseAnalysis.jsx @@ -0,0 +1,154 @@ +import React, { useEffect, useMemo } from "react"; +import Chart from "react-apexcharts"; +import { useExpenseAnalysis } from "../../hooks/useDashboard_Data"; +import { useSelectedProject } from "../../slices/apiDataManager"; +import { DateRangePicker1 } from "../common/DateRangePicker"; +import { FormProvider, useForm } from "react-hook-form"; +import { formatCurrency, localToUtc } from "../../utils/appUtils"; + +const ExpenseAnalysis = () => { + const projectId = useSelectedProject(); + + const methods = useForm({ + defaultValues: { startDate: "", endDate: "" }, + }); + + const { watch } = methods; + const [startDate, endDate] = watch(["startDate", "endDate"]); + + const { data, isLoading, isError, error, isFetching } = useExpenseAnalysis( + projectId, + startDate ? localToUtc(startDate) : null, + endDate ? localToUtc(endDate) : null + ); + + if (isError) return
{error.message}
; + + const report = data?.report ?? []; + const { labels, series, total } = useMemo(() => { + const labels = report.map((item) => item.projectName); + const series = report.map((item) => item.totalApprovedAmount || 0); + const total = formatCurrency(data?.totalAmount || 0); + return { labels, series, total }; + }, [report, data?.totalAmount]); + + const donutOptions = { + chart: { type: "donut" }, + labels, + legend: { show: false }, + dataLabels: { enabled: true, formatter: (val) => `${val.toFixed(0)}%` }, + colors: ["#7367F0", "#28C76F", "#FF9F43", "#EA5455", "#00CFE8", "#FF78B8"], + plotOptions: { + pie: { + donut: { + size: "70%", + labels: { + show: true, + total: { + show: true, + label: "Total", + fontSize: "16px", + formatter: () => `${total}`, + }, + }, + }, + }, + }, + responsive: [ + { + breakpoint: 576, // mobile breakpoint + options: { + chart: { width: "100%" }, + }, + }, + ], + }; + + return ( + <> +
+
+
+
Expense Breakdown
+

Category Wise Expense Breakdown

+
+ +
+ + + +
+
+ + {/* Card body */} +
+ {isLoading && ( +
+ Loading... +
+ )} + + {!isLoading && report.length === 0 && ( +
No data found
+ )} + + {!isLoading && report.length > 0 && ( + <> + {isFetching && ( +
+ Loading... +
+ )} + +
+ +
+ +
+
+ {report.map((item, idx) => ( +
+
+ + + +
+
+ {item.projectName} + + {formatCurrency(item.totalApprovedAmount)} + +
+
+ ))} +
+
+ + )} +
+
+ {/* Header */} + + + ); +}; + +export default ExpenseAnalysis; diff --git a/src/components/Dashboard/ExpenseByProject.jsx b/src/components/Dashboard/ExpenseByProject.jsx new file mode 100644 index 00000000..819824ca --- /dev/null +++ b/src/components/Dashboard/ExpenseByProject.jsx @@ -0,0 +1,164 @@ +import React, { useState, useEffect } from "react"; +import Chart from "react-apexcharts"; +import { useExpenseType } from "../../hooks/masterHook/useMaster"; +import { useSelector } from "react-redux"; +import { useExpenseDataByProject } from "../../hooks/useDashboard_Data"; +import { formatCurrency } from "../../utils/appUtils"; +import { formatDate_DayMonth } from "../../utils/dateUtils"; + +const ExpenseByProject = () => { + const projectId = useSelector((store) => store.localVariables.projectId); + const [range, setRange] = useState("12M"); + const [selectedType, setSelectedType] = useState(""); + const [viewMode, setViewMode] = useState("Category"); + const [chartData, setChartData] = useState({ categories: [], data: [] }); + + const { ExpenseTypes, loading: typeLoading } = useExpenseType(); + + const { data: expenseApiData, isLoading } = useExpenseDataByProject( + projectId, + selectedType, + range === "All" ? null : parseInt(range) + ); + + useEffect(() => { + if (expenseApiData) { + const categories = expenseApiData.map((item) => + formatDate_DayMonth(item.monthName, item.year) + ); + const data = expenseApiData.map((item) => item.total); + setChartData({ categories, data }); + } else { + setChartData({ categories: [], data: [] }); + } + }, [expenseApiData]); + + const getSelectedTypeName = () => { + if (!selectedType) return "All Types"; + const found = ExpenseTypes.find((t) => t.id === selectedType); + return found ? found.name : "All Types"; + }; + + const options = { + chart: { type: "bar", toolbar: { show: false } }, + plotOptions: { + bar: { horizontal: false, columnWidth: "55%", borderRadius: 4 }, + }, + dataLabels: { enabled: true, formatter: (val) => formatCurrency(val) }, + xaxis: { + categories: chartData.categories, + labels: { style: { fontSize: "12px" }, rotate: -45 }, + }, + tooltip: { + y: { + formatter: (val) => `${formatCurrency(val)} (${getSelectedTypeName()})`, + }, + }, + + annotations: { xaxis: [{ x: 0, strokeDashArray: 0 }] }, + fill: { opacity: 1 }, + colors: ["#2196f3"], + }; + + const series = [ + { + name: getSelectedTypeName(), + data: chartData.data, + }, + ]; + + return ( +
+ {/* Header */} +
+
+
+
Monthly Expense -
+

Detailed project expenses

+
+
+ +
    +
  • + +
  • +
  • + +
  • +
+
+
+ + {/* Range Buttons + Expense Dropdown */} +
+ {["1M", "3M", "6M", "12M", "All"].map((item) => ( + + ))} + {viewMode === "Category" && ( + + )} +
+
+ + {/* Chart */} +
+ {isLoading ? ( +

Loading chart...

+ ) : !expenseApiData || expenseApiData.length === 0 ? ( +
No data found
+ ) : ( + + )} +
+ +
+ ); +}; + +export default ExpenseByProject; diff --git a/src/components/Dashboard/ExpenseStatus.jsx b/src/components/Dashboard/ExpenseStatus.jsx new file mode 100644 index 00000000..d6fefe7d --- /dev/null +++ b/src/components/Dashboard/ExpenseStatus.jsx @@ -0,0 +1,157 @@ +import React, { useEffect, useState } from "react"; +import { useExpense } from "../../hooks/useExpense"; +import { useExpenseStatus } from "../../hooks/useDashboard_Data"; +import { useSelectedProject } from "../../slices/apiDataManager"; +import { useProjectName } from "../../hooks/useProjects"; +import { countDigit, formatCurrency } from "../../utils/appUtils"; +import { EXPENSE_MANAGE, EXPENSE_STATUS } from "../../utils/constants"; +import { useNavigate } from "react-router-dom"; +import { useHasUserPermission } from "../../hooks/useHasUserPermission"; + + +const ExpenseStatus = () => { + const [projectName, setProjectName] = useState("All Project"); + const selectedProject = useSelectedProject(); + const { projectNames, loading } = useProjectName(); + const { data, isPending, error } = useExpenseStatus(selectedProject); + const navigate = useNavigate(); + const isManageExpense = useHasUserPermission(EXPENSE_MANAGE) + + useEffect(() => { + if (selectedProject && projectNames?.length) { + const project = projectNames.find((p) => p.id === selectedProject); + setProjectName(project?.name || "All Project"); + } else { + setProjectName("All Project"); + } + }, [projectNames, selectedProject]); + + const handleNavigate = (status) => { + if (selectedProject) { + navigate(`/expenses/${status}/${selectedProject}`); + } else { + navigate(`/expenses/${status}`); + } + }; + return ( + <> +
+
+
Expense - By Status
+

{projectName}

+
+
+ +
+ +
+ {[ + { + title: "Pending Payment", + count: data?.processPending?.count || 0, + amount: data?.processPending?.totalAmount || 0, + icon: "bx bx-rupee", + iconColor: "text-primary", + status: EXPENSE_STATUS.payment_pending, + }, + { + title: "Pending Approve", + count: data?.approvePending?.count || 0, + amount: data?.approvePending?.totalAmount || 0, + icon: "fa-solid fa-check", + iconColor: "text-warning", + status: EXPENSE_STATUS.approve_pending, + }, + { + title: "Pending Review", + count: data?.reviewPending?.count || 0, + amount: data?.reviewPending?.totalAmount || 0, + icon: "bx bx-search-alt-2", + iconColor: "text-secondary", + status: EXPENSE_STATUS.review_pending, + }, + { + title: "Draft", + count: data?.draft?.count || 0, + amount: data?.draft?.totalAmount || 0, + icon: "bx bx-file-blank", + iconColor: "text-info", + status: EXPENSE_STATUS.daft, + }, + ].map((item, idx) => ( +
handleNavigate(item?.status)} + > +
+
+ + + +
+
+
+ {item?.title} + {item?.amount ? ( + + {formatCurrency(item?.amount)} + + ) : ( + {formatCurrency(0)} + )} +
+
+ = 3 ? "text-xl" : "text-2xl" + } text-gray-500`} + > + {item?.count || 0} + + + + +
+
+
+
+ ))} +
+ +
+ {isManageExpense && ( +
handleNavigate(EXPENSE_STATUS.process_pending)} + > +
+ 3 ? "text-base" : "text-lg" + }`} + > + Project Spendings: + {" "} + + (All Processed Payments) + +
+
+ 3 ? "text-" : "text-3xl" + } text-md`} + > + {formatCurrency(data?.totalAmount || 0)} + + + + +
+
+ )} +
+
+ + ); +}; + +export default ExpenseStatus; diff --git a/src/components/Expenses/ExpenseFilterPanel.jsx b/src/components/Expenses/ExpenseFilterPanel.jsx index c04a0981..fffd746c 100644 --- a/src/components/Expenses/ExpenseFilterPanel.jsx +++ b/src/components/Expenses/ExpenseFilterPanel.jsx @@ -13,9 +13,11 @@ import { useSelector } from "react-redux"; import moment from "moment"; import { useExpenseFilter } from "../../hooks/useExpense"; import { ExpenseFilterSkeleton } from "./ExpenseSkeleton"; -import { useLocation } from "react-router-dom"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { + const { status } = useParams(); + const navigate = useNavigate(); const selectedProjectId = useSelector( (store) => store.localVariables.projectId ); @@ -71,8 +73,14 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { onApply(defaultFilter); handleGroupBy(groupByList[0].id); closePanel(); + if (status) { + navigate("/expenses", { replace: true }); + } }; + + + // Close popup when navigating to another component const location = useLocation(); useEffect(() => { @@ -82,6 +90,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { if (isLoading || isFetching) return ; if (isError && isFetched) return
Something went wrong Here- {error.message}
; + return ( <> @@ -92,18 +101,16 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {