diff --git a/src/components/Dashboard/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx index 750e68be..31f214d7 100644 --- a/src/components/Dashboard/Dashboard.jsx +++ b/src/components/Dashboard/Dashboard.jsx @@ -17,6 +17,7 @@ import AttendanceOverview from "./AttendanceOverview"; import { useSelectedProject } from "../../slices/apiDataManager"; import { useProjectName } from "../../hooks/useProjects"; import ExpenseAnalysis from "./ExpenseAnalysis"; +import ExpenseByProject from "./ExpenseByProject"; import ExpenseStatus from "./ExpenseStatus"; const Dashboard = () => { @@ -53,6 +54,9 @@ const Dashboard = () => { +
+ +
); }; diff --git a/src/components/Dashboard/ExpenseByProject.jsx b/src/components/Dashboard/ExpenseByProject.jsx new file mode 100644 index 00000000..bd7e115d --- /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

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

Loading chart...

+ ) : ( + + )} +
+
+ ); +}; + +export default ExpenseByProject; diff --git a/src/components/Layout/Header.jsx b/src/components/Layout/Header.jsx index c75851c2..92d270e7 100644 --- a/src/components/Layout/Header.jsx +++ b/src/components/Layout/Header.jsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState,useMemo } from "react"; +import { useCallback, useEffect, useState, useMemo } from "react"; import getGreetingMessage from "../../utils/greetingHandler"; import { cacheData, @@ -22,7 +22,7 @@ import { ALLOW_PROJECTSTATUS_ID, MANAGE_PROJECT, UUID_REGEX } from "../../utils/ import { useAuthModal, useLogout } from "../../hooks/useAuth"; const Header = () => { - const { profile } = useProfile(); + const { profile } = useProfile(); const { data: masterData } = useMaster(); const location = useLocation(); const dispatch = useDispatch(); @@ -57,8 +57,8 @@ const Header = () => { isDashboardPath ? projectNames : projectNames?.filter((project) => - ALLOW_PROJECTSTATUS_ID.includes(project.projectStatusId) - ), + ALLOW_PROJECTSTATUS_ID.includes(project.projectStatusId) + ), [projectNames, isDashboardPath] ); @@ -66,11 +66,11 @@ 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"; + if (selectedProject === null) return "All Projects"; const selectedObj = projectNames.find((p) => p.id === selectedProject); return selectedObj - ? selectedObj.name - : projectNames[0]?.name || "No Projects Assigned"; + ? selectedObj.name + : projectNames[0]?.name || "No Projects Assigned"; }, [projectLoading, projectNames, selectedProject]); // ===== Role Helper ===== @@ -199,13 +199,13 @@ const Header = () => { style={{ overflow: "auto", maxHeight: "300px" }} > -
  • - -
  • - +
  • + +
  • + {[...projectsForDropdown] .sort((a, b) => a?.name?.localeCompare(b.name)) .map((project) => ( diff --git a/src/hooks/useDashboard_Data.jsx b/src/hooks/useDashboard_Data.jsx index 5d11705a..4dc1b0a7 100644 --- a/src/hooks/useDashboard_Data.jsx +++ b/src/hooks/useDashboard_Data.jsx @@ -200,63 +200,63 @@ export const useDashboard_Data = ({ days, FromDate, projectId }) => { // }) // } -export const useDashboard_AttendanceData = (date,projectId)=>{ - return useQuery({ - queryKey:["dashboardAttendances",date,projectId], - queryFn:async()=> { - - const resp = await await GlobalRepository.getDashboardAttendanceData(date, projectId) - return resp.data; +export const useDashboard_AttendanceData = (date, projectId) => { + return useQuery({ + queryKey: ["dashboardAttendances", date, projectId], + queryFn: async () => { + + const resp = await await GlobalRepository.getDashboardAttendanceData(date, projectId) + return resp.data; } }) } -export const useDashboardTeamsCardData =(projectId)=>{ - return useQuery({ - queryKey:["dashboardTeams",projectId], - queryFn:async()=> { - - const resp = await GlobalRepository.getDashboardTeamsCardData(projectId) - return resp.data; +export const useDashboardTeamsCardData = (projectId) => { + return useQuery({ + queryKey: ["dashboardTeams", projectId], + queryFn: async () => { + + const resp = await GlobalRepository.getDashboardTeamsCardData(projectId) + return resp.data; } }) } export const useDashboardTasksCardData = (projectId) => { - return useQuery({ - queryKey:["dashboardTasks",projectId], - queryFn:async()=> { - - const resp = await GlobalRepository.getDashboardTasksCardData(projectId) - return resp.data; + return useQuery({ + queryKey: ["dashboardTasks", projectId], + queryFn: async () => { + + const resp = await GlobalRepository.getDashboardTasksCardData(projectId) + return resp.data; } }) } export const useAttendanceOverviewData = (projectId, days) => { - return useQuery({ - queryKey:["dashboardAttendanceOverView",projectId,days], - queryFn:async()=> { - - const resp = await GlobalRepository.getAttendanceOverview(projectId, days); - return resp.data; + return useQuery({ + queryKey: ["dashboardAttendanceOverView", projectId, days], + queryFn: async () => { + + const resp = await GlobalRepository.getAttendanceOverview(projectId, days); + return resp.data; }, - enabled:!!projectId + enabled: !!projectId }) } export const useDashboardProjectsCardData = () => { - return useQuery({ - queryKey:["dashboardProjects"], - queryFn:async()=> { - - const resp = await GlobalRepository.getDashboardProjectsCardData(); - return resp.data; + return useQuery({ + queryKey: ["dashboardProjects"], + queryFn: async () => { + + const resp = await GlobalRepository.getDashboardProjectsCardData(); + return resp.data; } }) } export const useExpenseAnalysis = (projectId, startDate, endDate) => { - const hasBothDates = !!startDate && !!endDate; + const hasBothDates = !!startDate && !!endDate; const noDatesSelected = !startDate && !endDate; const shouldFetch = @@ -266,9 +266,20 @@ export const useExpenseAnalysis = (projectId, startDate, endDate) => { queryKey: ["expenseAnalysis", projectId, startDate, endDate], queryFn: async () => { const resp = await GlobalRepository.getExpenseData(projectId, startDate, endDate); - return resp.data; + return resp.data; }, - enabled:shouldFetch + enabled: shouldFetch + }); +}; + +export const useExpenseDataByProject = (projectId, categoryId, months) => { + return useQuery({ + queryKey: ["expenseByProject", projectId, categoryId, months], + queryFn: async () => { + const resp = await GlobalRepository.getExpenseDataByProject(projectId, categoryId, months); + return resp.data; + }, + }); }; export const useExpenseStatus = (projectId)=>{ diff --git a/src/repositories/GlobalRepository.jsx b/src/repositories/GlobalRepository.jsx index 999d58b4..2d7347b1 100644 --- a/src/repositories/GlobalRepository.jsx +++ b/src/repositories/GlobalRepository.jsx @@ -3,12 +3,12 @@ import { api } from "../utils/axiosClient"; const GlobalRepository = { getDashboardProgressionData: ({ days = '', FromDate = '', projectId = '' }) => { let params; - if(projectId == null){ + if (projectId == null) { params = new URLSearchParams({ days: days.toString(), FromDate, }); - }else{ + } else { params = new URLSearchParams({ days: days.toString(), FromDate, @@ -19,35 +19,35 @@ const GlobalRepository = { return api.get(`/api/Dashboard/Progression?${params.toString()}`); }, - getDashboardAttendanceData: ( date,projectId ) => { + getDashboardAttendanceData: (date, projectId) => { - return api.get(`/api/Dashboard/project-attendance/${projectId}?date=${date}`); -}, + return api.get(`/api/Dashboard/project-attendance/${projectId}?date=${date}`); + }, getDashboardProjectsCardData: () => { return api.get(`/api/Dashboard/projects`); }, - + getDashboardTeamsCardData: (projectId) => { - const url = projectId - ? `/api/Dashboard/teams?projectId=${projectId}` - : `/api/Dashboard/teams`; - return api.get(url); -}, + const url = projectId + ? `/api/Dashboard/teams?projectId=${projectId}` + : `/api/Dashboard/teams`; + return api.get(url); + }, getDashboardTasksCardData: (projectId) => { - const url = projectId - ? `/api/Dashboard/tasks?projectId=${projectId}` - : `/api/Dashboard/tasks`; - return api.get(url); -}, + const url = projectId + ? `/api/Dashboard/tasks?projectId=${projectId}` + : `/api/Dashboard/tasks`; + 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 = []; + getExpenseData: (projectId, startDate, endDate) => { + let url = `api/Dashboard/expense/type` + + const queryParams = []; if (projectId) { queryParams.push(`projectId=${projectId}`); @@ -56,7 +56,7 @@ const GlobalRepository = { if (startDate) { queryParams.push(`startDate=${startDate}`); } - if (endDate) { + if (endDate) { queryParams.push(`endDate=${endDate}`); } @@ -64,7 +64,31 @@ const GlobalRepository = { url += `?${queryParams.join("&")}`; } - return api.get(url ); + return api.get(url); + }, + + + getExpenseDataByProject: (projectId, categoryId, months) => { + let url = `api/Dashboard/expense/monthly` + + const queryParams = []; + + if (projectId) { + queryParams.push(`projectId=${projectId}`); + } + + if (categoryId) { + queryParams.push(`categoryId=${categoryId}`); + } + if (months) { + queryParams.push(`months=${months}`); + } + + if (queryParams.length > 0) { + url += `?${queryParams.join("&")}`; + } + + return api.get(url); }, getExpenseStatus:(projectId)=>api.get(`/api/Dashboard/expense/pendings${projectId ? `?projectId=${projectId}`:""}`) diff --git a/src/utils/dateUtils.jsx b/src/utils/dateUtils.jsx index 61ccc8c3..6b5d9746 100644 --- a/src/utils/dateUtils.jsx +++ b/src/utils/dateUtils.jsx @@ -94,10 +94,21 @@ export const getCompletionPercentage = (completedWork, plannedWork)=> { return clamped.toFixed(2); } -export const formatDate_DayMonth = (dateInput)=>{ - const date = new Date(dateInput); - return format(date, "d MMM"); -} +// export const formatDate_DayMonth = (dateInput)=>{ +// const date = new Date(dateInput); +// return format(date, "d MMM"); +// } + +export const formatDate_DayMonth = (monthName, year) => { + if (!monthName || !year) return ""; + try { + const shortMonth = monthName.substring(0, 3); + return `${shortMonth} ${year}`; + } catch { + return ""; + } +}; + export const getTenantStatus =(statusId)=>{ return ActiveTenant === statusId ? " bg-label-success":"bg-label-secondary"