155 lines
5.3 KiB
JavaScript
155 lines
5.3 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";
|
|
|
|
const formatDate = (dateStr) => {
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleDateString("en-GB", {
|
|
day: "2-digit",
|
|
month: "long",
|
|
});
|
|
};
|
|
|
|
const AttendanceOverview = () => {
|
|
const [dayRange, setDayRange] = useState(7);
|
|
const [view, setView] = useState("chart");
|
|
const selectedProject = useSelectedProject()
|
|
const { data: attendanceOverviewData, isLoading, isError, error } = useAttendanceOverviewData(
|
|
selectedProject,
|
|
dayRange
|
|
);
|
|
|
|
const { tableData, roles, dates } = useMemo(() => {
|
|
if (!attendanceOverviewData || attendanceOverviewData.length === 0) {
|
|
return { tableData: [], roles: [], dates: [] };
|
|
}
|
|
|
|
const map = new Map();
|
|
|
|
attendanceOverviewData.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 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 };
|
|
}, [attendanceOverviewData,isLoading,selectedProject,dayRange]);
|
|
|
|
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]),
|
|
};
|
|
|
|
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">
|
|
{/* 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; |