130 lines
4.1 KiB
TypeScript
130 lines
4.1 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useStats } from '../../hooks/useStats';
|
|
import { formatMinutes } from '../../utils/time';
|
|
import {
|
|
BarChart,
|
|
Bar,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
} from 'recharts';
|
|
|
|
|
|
interface DailySummaryProps {
|
|
from_date: string;
|
|
to_date: string;
|
|
day_start: string;
|
|
day_end: string;
|
|
}
|
|
|
|
interface ChartDataItem {
|
|
date: string;
|
|
daySleep: number;
|
|
nightSleep: number;
|
|
total: number;
|
|
}
|
|
|
|
interface AvgStats {
|
|
avgTotal: number;
|
|
avgDay: number;
|
|
avgNight: number;
|
|
count: number;
|
|
}
|
|
|
|
const DailySummary: React.FC<DailySummaryProps> = ({ from_date, to_date, day_start, day_end }) => {
|
|
const { dailySummary, isLoading } = useStats(from_date, to_date, day_start, day_end);
|
|
|
|
const { chartData, avgStats } = useMemo<{ chartData: ChartDataItem[]; avgStats: AvgStats }>(() => {
|
|
if (!dailySummary?.days) {
|
|
return { chartData: [] as ChartDataItem[], avgStats: { avgTotal: 0, avgDay: 0, avgNight: 0, count: 0 } };
|
|
}
|
|
|
|
const data = dailySummary.days.map((day) => ({
|
|
date: day.date,
|
|
daySleep: day.day_minutes / 60,
|
|
nightSleep: day.night_minutes / 60,
|
|
total: day.total_minutes / 60,
|
|
})).reverse();
|
|
|
|
let averages = { avgTotal: 0, avgDay: 0, avgNight: 0, count: 0 };
|
|
|
|
if (data.length) {
|
|
const totals = data.reduce(
|
|
(acc, d) => ({
|
|
total: acc.total + d.total,
|
|
day: acc.day + d.daySleep,
|
|
night: acc.night + d.nightSleep,
|
|
}),
|
|
{ total: 0, day: 0, night: 0 }
|
|
);
|
|
const count = data.length;
|
|
averages = {
|
|
avgTotal: totals.total / count,
|
|
avgDay: totals.day / count,
|
|
avgNight: totals.night / count,
|
|
count,
|
|
};
|
|
}
|
|
|
|
return { chartData: data, avgStats: averages };
|
|
}, [dailySummary]);
|
|
|
|
if (isLoading) return <div className="text-center p-4">Loading summary...</div>;
|
|
if (!chartData.length) {
|
|
return <div className="text-center p-4 text-gray-500">No data available for this period.</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200">
|
|
<h3 className="text-lg font-semibold mb-4 text-gray-800">Daily Summary</h3>
|
|
<ResponsiveContainer width="100%" height={Math.max(200, chartData.length * 30)}>
|
|
<BarChart
|
|
data={chartData}
|
|
layout="vertical"
|
|
margin={{ top: 5, right: 30, left: 60, bottom: 5 }}
|
|
>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
|
<XAxis
|
|
type="number"
|
|
tickFormatter={(value) => `${Math.round(value)}h`}
|
|
tick={{ fontSize: 12 }}
|
|
tickLine={false}
|
|
/>
|
|
<YAxis
|
|
type="category"
|
|
dataKey="date"
|
|
tick={{ fontSize: 12 }}
|
|
tickLine={false}
|
|
width={60}
|
|
/>
|
|
<Tooltip
|
|
formatter={(value: number, name: string) => [
|
|
formatMinutes(Math.round((value ?? 0) * 60)),
|
|
name === 'daySleep' ? 'Day' : 'Night',
|
|
]}
|
|
/>
|
|
<Bar dataKey="nightSleep" stackId="a" fill="#1e3a5f" />
|
|
<Bar dataKey="daySleep" stackId="a" fill="#fbbf24" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
<div className="mt-4 grid grid-cols-3 gap-4 text-center">
|
|
<div className="p-3 bg-gray-50 rounded-lg">
|
|
<div className="text-xs text-gray-500">Avg Total</div>
|
|
<div className="text-lg font-semibold text-gray-800">{formatMinutes(Math.round(avgStats.avgTotal * 60))}</div>
|
|
</div>
|
|
<div className="p-3 bg-gray-50 rounded-lg">
|
|
<div className="text-xs text-gray-500">Avg Day</div>
|
|
<div className="text-lg font-semibold text-gray-800">{formatMinutes(Math.round(avgStats.avgDay * 60))}</div>
|
|
</div>
|
|
<div className="p-3 bg-gray-50 rounded-lg">
|
|
<div className="text-xs text-gray-500">Avg Night</div>
|
|
<div className="text-lg font-semibold text-gray-800">{formatMinutes(Math.round(avgStats.avgNight * 60))}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DailySummary; |