added expense analysis card inisde dashboard

This commit is contained in:
pramod.mahajan 2025-10-02 13:10:14 +05:30
parent 0c1889e1c1
commit 25f4f1e7a7
10 changed files with 258 additions and 64 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>

View File

@ -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 (
<div className="container-fluid mt-5">
<div className="row gy-4">
{/* {shouldShowAttendance && ( */}
<div className="col-xxl-6 col-lg-6">
<ExpenseAnalysis />
</div>
{!isAllProjectsSelected && (
<div className="col-xxl-6 col-lg-6">
<AttendanceOverview />
<AttendanceOverview />
</div>
)}
</div>
</div>
);

View File

@ -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 <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 ExpenseAnalysis;

View File

@ -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");

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

@ -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 (
<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,12 +183,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

@ -253,4 +253,21 @@ export const useDashboardProjectsCardData = () => {
return resp.data;
}
})
}
}
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
});
};

View File

@ -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";

View File

@ -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 );
},
};

View File

@ -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();
}
}