added expense graph

This commit is contained in:
pramod.mahajan 2025-10-02 00:47:01 +05:30
parent 3559b0a01c
commit e26c87fd3d
8 changed files with 246 additions and 229 deletions

View File

@ -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 <div>Loading...</div>;
if (isError) return <p className="text-danger">{error.message}</p>;
return (
<div className="bg-white p-4 rounded shadow d-flex flex-column">
<div className="bg-white p-4 rounded shadow d-flex flex-column position-relative">
{/* Optional subtle loading overlay */}
{isLoading && (
<div className="position-absolute w-100 h-100 d-flex align-items-center justify-content-center bg-white bg-opacity-50 z-index-1">
<span>Loading...</span>
</div>
)}
{/* Header */}
<div className="d-flex justify-content-between align-items-center mb-3">
<div className="card-title mb-0 text-start">
@ -88,27 +85,15 @@ const AttendanceOverview = () => {
<p className="card-subtitle">Role-wise present count</p>
</div>
<div className="d-flex gap-2">
<select
className="form-select form-select-sm"
value={dayRange}
onChange={(e) => setDayRange(Number(e.target.value))}
>
<select className="form-select form-select-sm" value={dayRange} onChange={(e) => setDayRange(Number(e.target.value))}>
<option value={7}>Last 7 Days</option>
<option value={15}>Last 15 Days</option>
<option value={30}>Last 30 Days</option>
</select>
<button
className={`btn btn-sm p-1 ${view === "chart" ? "btn-primary" : "btn-outline-primary"}`}
onClick={() => setView("chart")}
title="Chart View"
>
<button className={`btn btn-sm p-1 ${view === "chart" ? "btn-primary" : "btn-outline-primary"}`} onClick={() => setView("chart")} title="Chart View">
<i className="bx bx-bar-chart-alt-2"></i>
</button>
<button
className={`btn btn-sm p-1 ${view === "table" ? "btn-primary" : "btn-outline-primary"}`}
onClick={() => setView("table")}
title="Table View"
>
<button className={`btn btn-sm p-1 ${view === "table" ? "btn-primary" : "btn-outline-primary"}`} onClick={() => setView("table")} title="Table View">
<i className="bx bx-list-ul fs-5"></i>
</button>
</div>
@ -127,9 +112,7 @@ const AttendanceOverview = () => {
<tr>
<th style={{ background: "#f8f9fa", textTransform: "none" }}>Role</th>
{dates.map((date, idx) => (
<th key={idx} style={{ background: "#f8f9fa", textTransform: "none" }}>
{date}
</th>
<th key={idx} style={{ background: "#f8f9fa", textTransform: "none" }}>{date}</th>
))}
</tr>
</thead>
@ -152,4 +135,4 @@ const AttendanceOverview = () => {
);
};
export default AttendanceOverview;
export default AttendanceOverview;

View File

@ -14,34 +14,32 @@ import { useDispatch, useSelector } from "react-redux";
// import ProjectProgressChart from "./ProjectProgressChart";
// import ProjectOverview from "../Project/ProjectOverview";
import AttendanceOverview from "./AttendanceChart";
import ExpenseChartDesign2 from "./ExpenseChartDesign2";
import ExpenseChartDesign from "./ExpenseChartDesign";
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;
// Get the selected project ID from Redux store
const projectId = useSelector((store) => store.localVariables.projectId);
const isAllProjectsSelected = projectId === null;
return (
<div className="container-fluid mt-5">
<div className="row gy-4">
<div className="col-xxl-6 col-lg-6">
<ExpenseChartDesign2 />
</div>
{!isAllProjectsSelected && (
<div className="col-xxl-6 col-lg-6">
<AttendanceOverview /> {/* Removed unnecessary projectId prop */}
</div>
)}
</div>
return (
<div className="container-fluid mt-5">
<div className="row gy-4">
<div className="col-xxl-6 col-lg-6">
<ExpenseChartDesign />
</div>
);
{!isAllProjectsSelected && (
<div className="col-xxl-6 col-lg-6">
<AttendanceOverview /> {/* Removed unnecessary projectId prop */}
</div>
)}
</div>
</div>
);
};
export default Dashboard;

View File

@ -0,0 +1,142 @@
import React, { useEffect, useState } from "react";
import Chart from "react-apexcharts";
import { useDashboard_ExpenseData } 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 ExpenseChartDesign = () => {
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 } = useDashboard_ExpenseData(
projectId,
localToUtc(startDate),
localToUtc(endDate)
);
if (isError) return <div>{error.message}</div>;
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 <div>No data found</div>;
}
return (
<div className="card shadow-sm" style={{ minHeight: "500px" }}>
<div className="card-header d-flex justify-content-between align-items-center mt-3">
<div>
<h5 className="mb-1 fw-bold">Expense Breakdown</h5>
<p className="card-subtitle me-3">Detailed project expenses</p>
</div>
<div className="text-end">
<FormProvider {...methods}>
<DateRangePicker1 />
</FormProvider>
</div>
</div>
<div className="card-body position-relative">
{/* Initial loading: show full loader */}
{isLoading && (
<div className="d-flex justify-content-center align-items-center" style={{ height: "200px" }}>
<span>Loading...</span>
</div>
)}
{/* Data display */}
{!isLoading && report.length === 0 && (
<div className="text-center text-muted py-5">No data found</div>
)}
{!isLoading && report.length > 0 && (
<>
{/* Overlay spinner for refetch */}
{isFetching && (
<div className="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center bg-white bg-opacity-75">
<span>Loading...</span>
</div>
)}
<div className="d-flex justify-content-center mb-8">
<Chart
options={donutOptions}
series={report.map((item) => item.totalApprovedAmount || 0)}
type="donut"
width="320"
/>
</div>
<div className="mb-2 w-100">
<div className="row">
{report.map((item, idx) => (
<div className="col-6 d-flex justify-content-start align-items-start mb-2" key={idx}>
<div className="avatar me-2">
<span
className="avatar-initial rounded-2"
style={{
backgroundColor: donutOptions.colors[idx % donutOptions.colors.length],
}}
>
<i className="bx bx-receipt fs-4"></i>
</span>
</div>
<div className="d-flex flex-column gap-1 text-start">
<small className="fw-bold fs-6">{item.projectName}</small>
<span className="fw-bold text-muted ms-1">{item.totalApprovedAmount}</span>
</div>
</div>
))}
</div>
</div>
</>
)}
</div>
</div>
);
};
export default ExpenseChartDesign;

View File

@ -1,119 +0,0 @@
import React, { useState } from "react";
import Chart from "react-apexcharts";
import DateRangePicker from "../common/DateRangePicker";
import { useDashboard_ExpenseData } from "../../hooks/useDashboard_Data";
import { useSelectedProject } from "../../slices/apiDataManager";
const ExpenseChartDesign2 = () => {
const projectId = useSelectedProject()
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
const { data, isLoading, isError, error } = useDashboard_ExpenseData(
projectId,
dateRange.startDate,
dateRange.endDate
);
if (isLoading) return <div>Loading....</div>
if (isError) return <div>{error.message}</div>;
const report = data?.report || [];
// Map the API data to chart labels and series
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 <div>
No data found
</div>
}
return (
<div className="card shadow-sm" style={{ minHeight: "500px" }}>
<div className="card-header d-flex justify-content-between align-items-center mt-3">
<div>
<h5 className="mb-1 fw-bold">Expense Breakdown</h5>
<p className="card-subtitle me-3">Detailed project expenses</p>
</div>
<div className="text-end">
<DateRangePicker
DateDifference="7"
onRangeChange={setDateRange}
endDateMode="today"
/>
</div>
</div>
<div className="card-body">
{report.length === 0 ? (
<div className="text-center text-muted py-5">
No data found
</div>
) : (
<>
<div className="d-flex justify-content-center mb-8">
<Chart
options={donutOptions}
series={report.map((item) => item.totalApprovedAmount || 0)}
type="donut"
width="320"
/>
</div>
<div className="mb-2 w-100">
<div className="row">
{report.map((item, idx) => (
<div
className="col-6 d-flex justify-content-start align-items-start mb-2"
key={idx}
>
<div className="avatar me-2">
<span
className="avatar-initial rounded-2"
style={{ backgroundColor: donutOptions.colors[idx % donutOptions.colors.length] }}
>
<i className="bx bx-receipt fs-4"></i>
</span>
</div>
<div className="d-flex flex-column gap-1 text-start">
<small className="fw-bold fs-6">{item.projectName}</small>
<span className="fw-bold text-muted ms-1">{item.totalApprovedAmount}</span>
</div>
</div>
))}
</div>
</div>
</>
)}
</div>
</div>
);
};
export default ExpenseChartDesign2;

View File

@ -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" }}
>
<li>
<button
className="dropdown-item"
onClick={() => handleProjectChange(null)}
>All Project</button>
</li>
{[...projectsForDropdown]
.sort((a, b) => a?.name?.localeCompare(b.name))
.map((project) => (

View File

@ -14,7 +14,6 @@ const DateRangePicker = ({
if (endDateMode === "yesterday") {
endDate.setDate(endDate.getDate() - 1);
}
endDate.setHours(0, 0, 0, 0);
const startDate = new Date(endDate);
@ -30,9 +29,14 @@ const DateRangePicker = ({
static: true,
clickOpens: true,
maxDate: endDate,
onChange: (selectedDates, dateStr) => {
const [startDateString, endDateString] = dateStr.split(" to ");
onRangeChange?.({ startDate: startDateString, endDate: endDateString });
onChange: (selectedDates) => {
if (selectedDates.length === 2) {
const [start, end] = selectedDates;
onRangeChange?.({
startDate: start.toLocaleDateString("en-CA"),
endDate: end.toLocaleDateString("en-CA"),
});
}
},
});
@ -47,9 +51,7 @@ const DateRangePicker = ({
}, [onRangeChange, DateDifference, endDateMode]);
const handleIconClick = () => {
if (inputRef.current) {
inputRef.current._flatpickr.open(); // directly opens flatpickr
}
inputRef.current?._flatpickr?.open();
};
return (
@ -61,9 +63,9 @@ const DateRangePicker = ({
id="flatpickr-range"
ref={inputRef}
/>
<i
className="bx bx-calendar calendar-icon cursor-pointer position-relative top-50 translate-middle-y " onClick={handleIconClick}
className="bx bx-calendar calendar-icon cursor-pointer position-relative top-50 translate-middle-y"
onClick={handleIconClick}
style={{ right: "22px", bottom: "-8px" }}
></i>
</div>
@ -72,10 +74,6 @@ const DateRangePicker = ({
export default DateRangePicker;
export const DateRangePicker1 = ({
startField = "startDate",
endField = "endDate",
@ -85,6 +83,8 @@ export const DateRangePicker1 = ({
resetSignal,
defaultRange = true,
maxDate = null,
sm,
md,
...rest
}) => {
const inputRef = useRef(null);
@ -118,7 +118,7 @@ export const DateRangePicker1 = ({
mode: "range",
dateFormat: "d-m-Y",
allowInput: allowText,
maxDate ,
maxDate,
onChange: (selectedDates) => {
if (selectedDates.length === 2) {
const [start, end] = selectedDates;
@ -148,31 +148,31 @@ export const DateRangePicker1 = ({
}, []);
useEffect(() => {
if (resetSignal !== undefined) {
if (defaultRange) {
applyDefaultDates();
} else {
setValue(startField, "", { shouldValidate: true });
setValue(endField, "", { shouldValidate: true });
if (resetSignal !== undefined) {
if (defaultRange) {
applyDefaultDates();
} else {
setValue(startField, "", { shouldValidate: true });
setValue(endField, "", { shouldValidate: true });
if (inputRef.current?._flatpickr) {
inputRef.current._flatpickr.clear();
if (inputRef.current?._flatpickr) {
inputRef.current._flatpickr.clear();
}
}
}
}
}, [resetSignal, defaultRange, setValue, startField, endField]);
}, [resetSignal, defaultRange, setValue, startField, endField]);
const start = getValues(startField);
const end = getValues(endField);
const formattedValue = start && end ? `${start} To ${end}` : "";
return (
<div className={`position-relative ${className}`}>
<div className={`col-${sm} col-sm-${md} px-1 position-relative`}>
<input
type="text"
className="form-control form-control-sm"
placeholder={placeholder}
className="form-control form-control-sm ps-2 pe-5 me-4 cursor-pointer"
placeholder="From to End"
id="flatpickr-range"
defaultValue={formattedValue}
ref={(el) => {
inputRef.current = el;
@ -181,13 +181,10 @@ export const DateRangePicker1 = ({
readOnly={!allowText}
autoComplete="off"
/>
<span
className="position-absolute top-50 end-0 pe-1 translate-middle-y cursor-pointer"
<i
className="bx bx-calendar calendar-icon cursor-pointer position-absolute top-50 end-0 translate-middle-y me-2"
onClick={() => inputRef.current?._flatpickr?.open()}
>
<i className="bx bx-calendar bx-sm fs-5 text-muted"></i>
</span>
></i>
</div>
);
};

View File

@ -212,12 +212,19 @@ export const useDashboard_AttendanceData = (date, projectId) => {
}
export const useDashboard_ExpenseData = (projectId, startDate, endDate) => {
const hasBothDates = !!startDate && !!endDate;
const noDatesSelected = !startDate && !endDate;
const shouldFetch =
noDatesSelected ||
hasBothDates;
return useQuery({
queryKey: ["dashboardExpenses", projectId, startDate, endDate],
queryFn: async () => {
const resp = await GlobalRepository.getExpenseData(projectId, startDate, endDate);
return resp.data; // this will return the "data" object from API response
return resp.data;
},
enabled:shouldFetch
});
};
@ -242,16 +249,17 @@ export const useDashboardTasksCardData = (projectId) => {
}
})
}
// export const useAttendanceOverviewData = (projectId, days) => {
// return useQuery({
// queryKey:["dashboardAttendanceOverView",projectId],
// queryFn:async()=> {
export const useAttendanceOverviewData = (projectId, days) => {
return useQuery({
queryKey: ["dashboardAttendanceOverView", projectId, days],
queryFn: async () => {
const resp = await GlobalRepository.getAttendanceOverview(projectId, days);
return resp.data;
},
enabled: !!projectId,
});
};
// const resp = await GlobalRepository.getAttendanceOverview(projectId, days);
// return resp.data;
// }
// })
// }
export const useDashboardProjectsCardData = () => {
return useQuery({

View File

@ -72,11 +72,11 @@ export const normalizeAllowedContentTypes = (allowedContentType) => {
export function localToUtc(localDateString) {
if (!localDateString || typeof localDateString !== "string") return null;
const [year, month, day] = localDateString.trim().split("-");
if (!year || !month || !day) return null;
const [day, month, year] = localDateString.trim().split("-");
if (!day || !month || !year) return null;
// Create date in UTC
const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day), 0, 0, 0));
return isNaN(date.getTime()) ? null : date.toISOString();
}
}