133 lines
5.1 KiB
JavaScript
133 lines
5.1 KiB
JavaScript
import React, { useState, useMemo } from "react";
|
|
import { useSelector } from "react-redux";
|
|
import ReactApexChart from "react-apexcharts";
|
|
import { useAttendanceOverviewData } from "../../hooks/useDashboard_Data";
|
|
import flatColors from "../Charts/flatColor";
|
|
import ChartSkeleton from "../Charts/Skelton";
|
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
|
import { formatDate_DayMonth } from "../../utils/dateUtils";
|
|
|
|
|
|
|
|
const AttendanceOverview = () => {
|
|
const [dayRange, setDayRange] = useState(7);
|
|
const [view, setView] = useState("chart");
|
|
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 (!attendanceData || attendanceData.length === 0) {
|
|
return { tableData: [], roles: [], dates: [] };
|
|
}
|
|
|
|
const map = new Map();
|
|
|
|
attendanceData.forEach((entry) => {
|
|
const date = formatDate_DayMonth(entry.date);
|
|
if (!map.has(date)) map.set(date, {});
|
|
map.get(date)[entry.role.trim()] = entry.present;
|
|
});
|
|
|
|
const uniqueRoles = [...new Set(attendanceData.map((e) => e.role.trim()))];
|
|
const sortedDates = [...map.keys()];
|
|
|
|
const tableData = sortedDates.map((date) => {
|
|
const row = { date };
|
|
uniqueRoles.forEach((role) => {
|
|
row[role] = map.get(date)?.[role] ?? 0;
|
|
});
|
|
return row;
|
|
});
|
|
|
|
return { tableData, roles: uniqueRoles, dates: sortedDates };
|
|
}, [attendanceData]);
|
|
|
|
const chartSeries = roles.map((role) => ({
|
|
name: role,
|
|
data: tableData.map((row) => row[role]),
|
|
}));
|
|
|
|
const chartOptions = {
|
|
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 } },
|
|
legend: { position: "bottom" },
|
|
fill: { opacity: 1 },
|
|
colors: roles.map((_, i) => flatColors[i % flatColors.length]),
|
|
};
|
|
|
|
return (
|
|
<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">
|
|
<h5 className="mb-1 fw-bold">Attendance Overview</h5>
|
|
<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))}>
|
|
<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">
|
|
<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">
|
|
<i className="bx bx-list-ul fs-5"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-grow-1 d-flex align-items-center justify-content-center">
|
|
{view === "chart" ? (
|
|
<div className="w-100">
|
|
<ReactApexChart options={chartOptions} series={chartSeries} type="bar" height={400} />
|
|
</div>
|
|
) : (
|
|
<div className="table-responsive w-100" style={{ maxHeight: "350px", overflowY: "auto" }}>
|
|
<table className="table table-bordered table-sm text-start align-middle mb-0">
|
|
<thead className="table-light" style={{ position: "sticky", top: 0, zIndex: 1 }}>
|
|
<tr>
|
|
<th style={{ background: "#f8f9fa", textTransform: "none" }}>Role</th>
|
|
{dates.map((date, idx) => (
|
|
<th key={idx} style={{ background: "#f8f9fa", textTransform: "none" }}>{date}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{roles.map((role) => (
|
|
<tr key={role}>
|
|
<td>{role}</td>
|
|
{tableData.map((row, idx) => {
|
|
const value = row[role];
|
|
return <td key={idx} style={value > 0 ? { backgroundColor: "#d5d5d5" } : {}}>{value}</td>;
|
|
})}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AttendanceOverview; |