From 25f4f1e7a77d69dfc611ac69367df1e18afe8955 Mon Sep 17 00:00:00 2001 From: "pramod.mahajan" Date: Thu, 2 Oct 2025 13:10:14 +0530 Subject: [PATCH] added expense analysis card inisde dashboard --- ...ndanceChart.jsx => AttendanceOverview.jsx} | 63 +++----- src/components/Dashboard/Dashboard.jsx | 15 +- src/components/Dashboard/ExpenseAnalysis.jsx | 142 ++++++++++++++++++ src/components/Expenses/ManageExpense.jsx | 4 +- src/components/Layout/Header.jsx | 10 +- src/components/common/DateRangePicker.jsx | 16 +- src/hooks/useDashboard_Data.jsx | 19 ++- src/pages/project/ProjectDetails.jsx | 2 +- src/repositories/GlobalRepository.jsx | 27 +++- src/utils/appUtils.js | 24 ++- 10 files changed, 258 insertions(+), 64 deletions(-) rename src/components/Dashboard/{AttendanceChart.jsx => AttendanceOverview.jsx} (73%) create mode 100644 src/components/Dashboard/ExpenseAnalysis.jsx diff --git a/src/components/Dashboard/AttendanceChart.jsx b/src/components/Dashboard/AttendanceOverview.jsx similarity index 73% rename from src/components/Dashboard/AttendanceChart.jsx rename to src/components/Dashboard/AttendanceOverview.jsx index 3b58b897..05ca367c 100644 --- a/src/components/Dashboard/AttendanceChart.jsx +++ b/src/components/Dashboard/AttendanceOverview.jsx @@ -17,28 +17,30 @@ const formatDate = (dateStr) => { const AttendanceOverview = () => { const [dayRange, setDayRange] = useState(7); const [view, setView] = useState("chart"); - const selectedProject = useSelectedProject() + const selectedProject = useSelectedProject(); + const { data: attendanceOverviewData, isLoading, isError, error } = useAttendanceOverviewData( selectedProject, dayRange ); + // Use empty array while loading + const attendanceData = attendanceOverviewData || []; + const { tableData, roles, dates } = useMemo(() => { - if (!attendanceOverviewData || attendanceOverviewData.length === 0) { + if (!attendanceData || attendanceData.length === 0) { return { tableData: [], roles: [], dates: [] }; } const map = new Map(); - attendanceOverviewData.forEach((entry) => { + attendanceData.forEach((entry) => { const date = formatDate(entry.date); if (!map.has(date)) map.set(date, {}); map.get(date)[entry.role.trim()] = entry.present; }); - const uniqueRoles = [ - ...new Set(attendanceOverviewData.map((e) => e.role.trim())), - ]; + const uniqueRoles = [...new Set(attendanceData.map((e) => e.role.trim()))]; const sortedDates = [...map.keys()]; const tableData = sortedDates.map((date) => { @@ -50,7 +52,7 @@ const AttendanceOverview = () => { }); return { tableData, roles: uniqueRoles, dates: sortedDates }; - }, [attendanceOverviewData,isLoading,selectedProject,dayRange]); + }, [attendanceData]); const chartSeries = roles.map((role) => ({ name: role, @@ -58,29 +60,24 @@ const AttendanceOverview = () => { })); const chartOptions = { - chart: { - type: "bar", - stacked: true, - height: 400, - toolbar: { show: false }, - }, + chart: { type: "bar", stacked: true, height: 400, toolbar: { show: false } }, plotOptions: { bar: { borderRadius: 2, columnWidth: "60%" } }, xaxis: { categories: tableData.map((row) => row.date) }, - yaxis: { - show: true, - axisBorder: { show: true, color: "#78909C" }, - axisTicks: { show: true, color: "#78909C", width: 6 }, - }, + yaxis: { show: true, axisBorder: { show: true, color: "#78909C" }, axisTicks: { show: true, color: "#78909C", width: 6 } }, legend: { position: "bottom" }, fill: { opacity: 1 }, colors: roles.map((_, i) => flatColors[i % flatColors.length]), }; - if (isLoading) return
Loading...
; - if (isError) return

{error.message}

; - return ( -
+
+ {/* Optional subtle loading overlay */} + {isLoading && ( +
+ Loading... +
+ )} + {/* Header */}
@@ -88,27 +85,15 @@ const AttendanceOverview = () => {

Role-wise present count

- setDayRange(Number(e.target.value))}> - -
@@ -127,9 +112,7 @@ const AttendanceOverview = () => { Role {dates.map((date, idx) => ( - - {date} - + {date} ))} diff --git a/src/components/Dashboard/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx index 171b8cae..9435d3bc 100644 --- a/src/components/Dashboard/Dashboard.jsx +++ b/src/components/Dashboard/Dashboard.jsx @@ -13,26 +13,33 @@ import { useDispatch, useSelector } from "react-redux"; // import ProjectCompletionChart from "./ProjectCompletionChart"; // import ProjectProgressChart from "./ProjectProgressChart"; // import ProjectOverview from "../Project/ProjectOverview"; -import AttendanceOverview from "./AttendanceChart"; +import AttendanceOverview from "./AttendanceOverview"; import { useSelectedProject } from "../../slices/apiDataManager"; import { useProjectName } from "../../hooks/useProjects"; +import ExpenseAnalysis from "./ExpenseAnalysis"; const Dashboard = () => { - // const { projectsCardData } = useDashboardProjectsCardData(); // const { teamsCardData } = useDashboardTeamsCardData(); // const { tasksCardData } = useDashboardTasksCardData(); // Get the selected project ID from Redux store + const projectId = useSelector((store) => store.localVariables.projectId); + const isAllProjectsSelected = projectId === null; return (
- {/* {shouldShowAttendance && ( */} +
+ +
+ + {!isAllProjectsSelected && (
- +
+ )}
); diff --git a/src/components/Dashboard/ExpenseAnalysis.jsx b/src/components/Dashboard/ExpenseAnalysis.jsx new file mode 100644 index 00000000..3cdda4ae --- /dev/null +++ b/src/components/Dashboard/ExpenseAnalysis.jsx @@ -0,0 +1,142 @@ +import React, { useEffect, useState } 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 { localToUtc } from "../../utils/appUtils"; + +const ExpenseAnalysis = () => { + const projectId = useSelectedProject(); + + const methods = useForm({ + defaultValues: { + startDate: "", + endDate: "", + }, + }); + + const { watch } = methods; + + const [startDate, endDate] = watch(["startDate", "endDate"]); + console.log(startDate,endDate) + const { data, isLoading, isError, error, isFetching } = useExpenseAnalysis( + projectId, + localToUtc(startDate), + localToUtc(endDate) +); + + if (isError) return
{error.message}
; + + const report = data?.report || []; + + const labels = report.map((item) => item.projectName); + const series = report.map((item) => item.totalApprovedAmount || 0); + const total = data?.totalAmount || 0; + + 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}`, + }, + }, + }, + }, + }, + }; + + + if (data?.report === 0) { + return
No data found
; + } + + return ( +
+
+
+
Expense Breakdown
+

Detailed project expenses

+
+ +
+ + + +
+
+ +
+ {/* Initial loading: show full loader */} + {isLoading && ( +
+ Loading... +
+ )} + + {/* Data display */} + {!isLoading && report.length === 0 && ( +
No data found
+ )} + + {!isLoading && report.length > 0 && ( + <> + {/* Overlay spinner for refetch */} + {isFetching && ( +
+ Loading... +
+ )} + +
+ item.totalApprovedAmount || 0)} + type="donut" + width="320" + /> +
+ +
+
+ {report.map((item, idx) => ( +
+
+ + + +
+
+ {item.projectName} + {item.totalApprovedAmount} +
+
+ ))} +
+
+ + )} +
+
+ + ); +}; + +export default ExpenseAnalysis; diff --git a/src/components/Expenses/ManageExpense.jsx b/src/components/Expenses/ManageExpense.jsx index fa19d2c5..860a61c9 100644 --- a/src/components/Expenses/ManageExpense.jsx +++ b/src/components/Expenses/ManageExpense.jsx @@ -189,7 +189,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { const editPayload = { ...payload, id: data.id }; ExpenseUpdate({ id: data.id, payload: editPayload }); } else { - CreateExpense(payload); + console.log(fromdata) + console.log(payload) + // CreateExpense(payload); } }; const ExpenseTypeId = watch("expensesTypeId"); diff --git a/src/components/Layout/Header.jsx b/src/components/Layout/Header.jsx index ec55b48c..870672e3 100644 --- a/src/components/Layout/Header.jsx +++ b/src/components/Layout/Header.jsx @@ -67,7 +67,7 @@ const Header = () => { if (projectLoading) return "Loading..."; if (!projectNames?.length) return "No Projects Assigned"; if (projectNames.length === 1) return projectNames[0].name; - + if (selectedProject === null) return "All Projects"; const selectedObj = projectNames.find((p) => p.id === selectedProject); return selectedObj ? selectedObj.name @@ -199,6 +199,14 @@ const Header = () => { className="dropdown-menu" style={{ overflow: "auto", maxHeight: "300px" }} > + +
  • + +
  • + {[...projectsForDropdown] .sort((a, b) => a?.name?.localeCompare(b.name)) .map((project) => ( diff --git a/src/components/common/DateRangePicker.jsx b/src/components/common/DateRangePicker.jsx index 72aad155..1fd5d2f6 100644 --- a/src/components/common/DateRangePicker.jsx +++ b/src/components/common/DateRangePicker.jsx @@ -85,6 +85,7 @@ export const DateRangePicker1 = ({ resetSignal, defaultRange = true, maxDate = null, + sm,md, ...rest }) => { const inputRef = useRef(null); @@ -168,11 +169,12 @@ export const DateRangePicker1 = ({ const formattedValue = start && end ? `${start} To ${end}` : ""; return ( -
    +
    { inputRef.current = el; @@ -181,12 +183,10 @@ export const DateRangePicker1 = ({ readOnly={!allowText} autoComplete="off" /> - inputRef.current?._flatpickr?.open()} - > - - + >
    ); }; diff --git a/src/hooks/useDashboard_Data.jsx b/src/hooks/useDashboard_Data.jsx index 8800d48a..7c022b46 100644 --- a/src/hooks/useDashboard_Data.jsx +++ b/src/hooks/useDashboard_Data.jsx @@ -253,4 +253,21 @@ export const useDashboardProjectsCardData = () => { return resp.data; } }) -} \ No newline at end of file +} + +export const useExpenseAnalysis = (projectId, startDate, endDate) => { + const hasBothDates = !!startDate && !!endDate; + const noDatesSelected = !startDate && !endDate; + + const shouldFetch = + noDatesSelected || + hasBothDates; + return useQuery({ + queryKey: ["expenseAnalysis", projectId, startDate, endDate], + queryFn: async () => { + const resp = await GlobalRepository.getExpenseData(projectId, startDate, endDate); + return resp.data; + }, + enabled:shouldFetch + }); +}; \ No newline at end of file diff --git a/src/pages/project/ProjectDetails.jsx b/src/pages/project/ProjectDetails.jsx index e741e0f3..6f4e1130 100644 --- a/src/pages/project/ProjectDetails.jsx +++ b/src/pages/project/ProjectDetails.jsx @@ -15,7 +15,7 @@ import { useProjectDetails, useProjectName } from "../../hooks/useProjects"; import { ComingSoonPage } from "../Misc/ComingSoonPage"; import eventBus from "../../services/eventBus"; import ProjectProgressChart from "../../components/Dashboard/ProjectProgressChart"; -import AttendanceOverview from "../../components/Dashboard/AttendanceChart"; +import AttendanceOverview from "../../components/Dashboard/AttendanceOverview"; import { setProjectId } from "../../slices/localVariablesSlice"; import ProjectDocuments from "../../components/Project/ProjectDocuments"; import ProjectSetting from "../../components/Project/ProjectSetting"; diff --git a/src/repositories/GlobalRepository.jsx b/src/repositories/GlobalRepository.jsx index d3367fa6..9fc8b074 100644 --- a/src/repositories/GlobalRepository.jsx +++ b/src/repositories/GlobalRepository.jsx @@ -41,7 +41,32 @@ const GlobalRepository = { return api.get(url); }, - getAttendanceOverview:(projectId,days)=>api.get(`/api/dashboard/attendance-overview/${projectId}?days=${days}`) + getAttendanceOverview:(projectId,days)=>api.get(`/api/dashboard/attendance-overview/${projectId}?days=${days}`), + + + getExpenseData: (projectId, startDate, endDate) => { + let url = `api/Dashboard/expense/type` + + const queryParams = []; + + if (projectId) { + queryParams.push(`projectId=${projectId}`); + } + + if (startDate) { + queryParams.push(`startDate=${startDate}`); + } + if (endDate) { + queryParams.push(`endDate=${endDate}`); + } + + if (queryParams.length > 0) { + url += `?${queryParams.join("&")}`; + } + + return api.get(url ); + }, + }; diff --git a/src/utils/appUtils.js b/src/utils/appUtils.js index 0766514a..0533dead 100644 --- a/src/utils/appUtils.js +++ b/src/utils/appUtils.js @@ -69,14 +69,24 @@ export const normalizeAllowedContentTypes = (allowedContentType) => { return allowedContentType.split(","); return []; }; -export function localToUtc(localDateString) { - if (!localDateString || typeof localDateString !== "string") return null; +export function localToUtc(dateString) { + if (!dateString || typeof dateString !== "string") return null; - - const [year, month, day] = localDateString.trim().split("-"); - if (!year || !month || !day) return null; + const parts = dateString.trim().split("-"); + if (parts.length !== 3) return null; + + let day, month, year; + + if (parts[0].length === 4) { + // Format: yyyy-mm-dd + [year, month, day] = parts; + } else { + // Format: dd-mm-yyyy + [day, month, year] = parts; + } + + if (!day || !month || !year) return null; const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day), 0, 0, 0)); - return isNaN(date.getTime()) ? null : date.toISOString(); -} \ No newline at end of file +}