added expense analysis card inisde dashboard
This commit is contained in:
parent
0c1889e1c1
commit
25f4f1e7a7
@ -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>
|
@ -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 />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
142
src/components/Dashboard/ExpenseAnalysis.jsx
Normal file
142
src/components/Dashboard/ExpenseAnalysis.jsx
Normal 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;
|
@ -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");
|
||||
|
@ -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) => (
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -254,3 +254,20 @@ export const useDashboardProjectsCardData = () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
};
|
@ -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";
|
||||
|
@ -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 );
|
||||
},
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
@ -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 parts = dateString.trim().split("-");
|
||||
if (parts.length !== 3) return null;
|
||||
|
||||
const [year, month, day] = localDateString.trim().split("-");
|
||||
if (!year || !month || !day) 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();
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user