Not sure what happened here...
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useStats } from '../../hooks/useStats';
|
||||
import { formatMinutesAfter, getToday } from '../../utils/time';
|
||||
import { formatMinutesAfter, getToday, getLocalOffsetFromReference } from '../../utils/time';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, } from 'recharts';
|
||||
const BedtimePlot = ({ from_date, to_date, wakeup_ref, bedtime_ref }) => {
|
||||
const { wakeupBedtime, isLoading } = useStats(from_date, to_date, '07:00', '20:00', wakeup_ref, bedtime_ref);
|
||||
@@ -13,11 +13,12 @@ const BedtimePlot = ({ from_date, to_date, wakeup_ref, bedtime_ref }) => {
|
||||
}
|
||||
const data = wakeupBedtime.days.map((day) => {
|
||||
const daysAgo = Math.floor((new Date(getToday()).getTime() - new Date(day.date).getTime()) / (1000 * 60 * 60 * 24));
|
||||
const offsetFromRef = day.bedtime_minutes !== null ? getLocalOffsetFromReference(day.bedtime_minutes, bedtime_ref) : 0;
|
||||
return {
|
||||
date: daysAgo === 0 ? 'Today' : `${daysAgo}d ago`,
|
||||
minutes: day.bedtime_minutes ?? 0,
|
||||
minutes: offsetFromRef,
|
||||
time: day.bedtime_minutes !== null
|
||||
? formatMinutesAfter(day.bedtime_minutes, bedtime_ref)
|
||||
? formatMinutesAfter(offsetFromRef, bedtime_ref)
|
||||
: 'N/A',
|
||||
};
|
||||
}).reverse();
|
||||
|
||||
@@ -18,7 +18,7 @@ interface WakeupBedtimeProps {
|
||||
bedtime_ref: string;
|
||||
}
|
||||
|
||||
const BedtimePlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_ref, bedtime_ref }) => {
|
||||
const BedtimePlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_ref = '07:00', bedtime_ref = '20:00' }) => {
|
||||
const { wakeupBedtime, isLoading } = useStats(from_date, to_date, '07:00', '20:00', wakeup_ref, bedtime_ref);
|
||||
const [chartData, setChartData] = useState<{ date: string; minutes: number; time: string }[]>([]);
|
||||
|
||||
|
||||
@@ -45,14 +45,14 @@ export function SessionList() {
|
||||
alert('Failed to update session');
|
||||
}
|
||||
};
|
||||
const handleCancel = () => {
|
||||
setEditingId(null);
|
||||
};
|
||||
const startEditing = (s) => {
|
||||
setEditingId(s.id);
|
||||
setEditStart(toDatetimeLocal(s.start_time));
|
||||
setEditEnd(s.end_time ? toDatetimeLocal(s.end_time) : '');
|
||||
};
|
||||
const handleCancel = () => {
|
||||
setEditingId(null);
|
||||
};
|
||||
return (_jsxs("div", { className: "bg-white p-4 rounded-xl shadow-sm border border-gray-200", children: [_jsx("h3", { className: "text-lg font-semibold mb-4 text-gray-800", children: "Sleep Sessions" }), _jsx("div", { className: "space-y-2", children: sessions.map((s) => (_jsx("div", { className: "border-b border-gray-100 last:border-0 pb-2", children: editingId === s.id ? (_jsxs("div", { className: "flex flex-col space-y-2 py-1", children: [_jsxs("div", { className: "grid grid-cols-1 gap-2 text-xs text-gray-500", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { children: "Start Time" }), _jsx("input", { type: "datetime-local", className: "border rounded px-1 py-0.5 text-sm text-black w-full", value: editStart, onChange: (e) => setEditStart(e.target.value) })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { children: "End Time" }), _jsx("input", { type: "datetime-local", className: "border rounded px-1 py-0.5 text-sm text-black w-full", value: editEnd, onChange: (e) => setEditEnd(e.target.value) })] })] }), _jsxs("div", { className: "flex justify-end space-x-2", children: [_jsx("button", { onClick: handleCancel, className: "p-1 text-gray-500 hover:text-gray-700 transition-colors", title: "Cancel", children: _jsx(X, { size: 18 }) }), _jsx("button", { onClick: () => handleSave(s.id), className: "p-1 text-green-600 hover:text-green-700 transition-colors", title: "Save", children: _jsx(Check, { size: 18 }) })] })] })) : (_jsxs("div", { className: "flex justify-between items-center text-sm cursor-pointer hover:bg-gray-50 p-1 rounded transition-colors", onClick: () => startEditing(s), children: [_jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "text-gray-900 font-medium", children: formatDateTime(s.start_time) }), _jsx("span", { className: "text-gray-500 text-xs", children: s.end_time
|
||||
? `${formatDateTime(s.start_time)} → ${formatDateTime(s.end_time)}`
|
||||
: 'In progress' })] }), _jsxs("div", { className: "flex items-center space-x-2", children: [_jsx("span", { className: "text-blue-600 font-mono text-sm", children: s.end_time && s.start_time
|
||||
|
||||
@@ -40,7 +40,7 @@ export function SessionList() {
|
||||
start_time: new Date(editStart).toISOString(),
|
||||
end_time: editEnd ? new Date(editEnd).toISOString() : null,
|
||||
}
|
||||
});
|
||||
} as any);
|
||||
setEditingId(null);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -48,16 +48,16 @@ export function SessionList() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
const startEditing = (s: SleepSessionRead) => {
|
||||
setEditingId(s.id);
|
||||
setEditStart(toDatetimeLocal(s.start_time));
|
||||
setEditEnd(s.end_time ? toDatetimeLocal(s.end_time) : '');
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
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">Sleep Sessions</h3>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import React, { useMemo } from 'react';
|
||||
import { useStats } from '../../hooks/useStats';
|
||||
import { timeToMinutes } from '../../utils/time';
|
||||
const Timetable = ({ from_date, to_date, day_start, day_end }) => {
|
||||
const { timetable, isLoading } = useStats(from_date, to_date, day_start, day_end);
|
||||
const days = useMemo(() => {
|
||||
const start = new Date(from_date + 'T00:00:00');
|
||||
const end = new Date(to_date + 'T23:59:59');
|
||||
const result = [];
|
||||
let current = new Date(start);
|
||||
while (current <= end) {
|
||||
result.push(current.toLocaleDateString('en-CA'));
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
return result;
|
||||
}, [from_date, to_date]);
|
||||
const timeSteps = useMemo(() => {
|
||||
return Array.from({ length: 24 }, (_, i) => i * 60);
|
||||
}, []);
|
||||
if (isLoading)
|
||||
return _jsx("div", { className: "text-center p-4", children: "Loading timetable..." });
|
||||
if (!timetable?.slots || timetable.slots.length === 0) {
|
||||
return _jsx("div", { className: "text-center p-4 text-gray-500", children: "No data available for this period." });
|
||||
}
|
||||
const getDayName = (dateStr) => {
|
||||
return new Date(dateStr).toLocaleDateString('en-US', { weekday: 'short' });
|
||||
};
|
||||
const formatTimeLabel = (mins) => {
|
||||
const hours = Math.floor(mins / 60);
|
||||
const minutes = mins % 60;
|
||||
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
|
||||
};
|
||||
const isSleepingInInterval = (date, intervalStart, intervalEnd) => {
|
||||
return timetable.slots.some((slot) => {
|
||||
if (slot.date !== date || !slot.sleeping)
|
||||
return false;
|
||||
let sStart = timeToMinutes(slot.slot_start);
|
||||
let sEnd = timeToMinutes(slot.slot_end);
|
||||
if (sEnd <= sStart) {
|
||||
// Handle wrap-around within a single slot if it happens (though unlikely for 15m segments)
|
||||
const overlap1 = Math.max(sStart, intervalStart) < Math.min(1440, intervalEnd);
|
||||
const overlap2 = Math.max(0, intervalStart) < Math.min(sEnd, intervalEnd);
|
||||
return overlap1 || overlap2;
|
||||
}
|
||||
return Math.max(sStart, intervalStart) < Math.min(sEnd, intervalEnd);
|
||||
});
|
||||
};
|
||||
const currentMins = new Date().getHours() * 60 + new Date().getMinutes();
|
||||
const rowHeightPx = 32; // h-8 is 2rem, which is 32px in standard Tailwind config
|
||||
return (_jsxs("div", { className: "bg-white p-4 rounded-xl shadow-sm border border-gray-200 overflow-x-auto", children: [_jsx("h3", { className: "text-lg font-semibold mb-4 text-gray-800", children: "Sleep Timetable" }), _jsxs("div", { className: "min-w-[600px] flex flex-col gap-px", children: [_jsxs("div", { className: "grid", style: {
|
||||
gridTemplateColumns: `4rem repeat(${days.length}, minmax(40px, 1fr))`
|
||||
}, children: [_jsx("div", { className: "h-8" }), " ", days.map((day) => (_jsx("div", { className: "text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-8", children: getDayName(day) }, day)))] }), _jsx("div", { className: "relative", children: _jsxs("div", { className: "grid", style: {
|
||||
gridTemplateColumns: `4rem repeat(${days.length}, minmax(40px, 1fr))`
|
||||
}, children: [timeSteps.map((step) => (_jsxs(React.Fragment, { children: [_jsx("div", { className: "text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8", children: step % 60 === 0 ? formatTimeLabel(step) : '' }), days.map((day) => {
|
||||
const sleeping = isSleepingInInterval(day, step, step + 60);
|
||||
return (_jsx("div", { className: `h-8 w-full ${sleeping ? 'bg-green-500' : 'bg-gray-50'}` }, `${day}-${step}`));
|
||||
})] }, step))), _jsx("div", { className: "absolute left-[4rem] right-0 h-0.5 bg-red-500 pointer-events-none z-10", style: {
|
||||
top: `${(currentMins / 1440) * (24 * rowHeightPx)}px`,
|
||||
transform: 'translateY(-50%)'
|
||||
} })] }) })] })] }));
|
||||
};
|
||||
export default Timetable;
|
||||
@@ -1,89 +0,0 @@
|
||||
import { jsx as _jsx } from "react/jsx-runtime";
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { vi, describe, it, expect } from 'vitest';
|
||||
import Timetable from './SleepTimetable';
|
||||
import { useStats } from '../../hooks/useStats';
|
||||
vi.mock('../../hooks/useStats', () => ({
|
||||
useStats: vi.fn(),
|
||||
}));
|
||||
describe('SleepTimetable', () => {
|
||||
const defaultProps = {
|
||||
from_date: '2025-04-20',
|
||||
to_date: '2025-04-21',
|
||||
day_start: '22:00',
|
||||
day_end: '08:00',
|
||||
};
|
||||
it('renders loading state', () => {
|
||||
useStats.mockReturnValue({ timetable: null, isLoading: true });
|
||||
render(_jsx(Timetable, { ...defaultProps }));
|
||||
expect(screen.getByText(/Loading timetable.../i)).toBeInTheDocument();
|
||||
});
|
||||
it('renders empty state when no data is available', () => {
|
||||
useStats.mockReturnValue({ timetable: { slots: [] }, isLoading: false });
|
||||
render(_jsx(Timetable, { ...defaultProps }));
|
||||
expect(screen.getByText(/No data available/i)).toBeInTheDocument();
|
||||
});
|
||||
it('renders the grid correctly with hourly rows and biennial labels', () => {
|
||||
useStats.mockReturnValue({
|
||||
timetable: {
|
||||
slots: [
|
||||
{
|
||||
date: '2025-04-20',
|
||||
sleeping: true,
|
||||
slot_start: '01:00',
|
||||
slot_end: '05:00',
|
||||
},
|
||||
],
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
render(_jsx(Timetable, { ...defaultProps }));
|
||||
// Check for day headers
|
||||
expect(screen.getByText(/SUN/i)).toBeInTheDocument(); // 2025-04-20 is Sunday
|
||||
expect(screen.getByText(/MON/i)).toBeInTheDocument(); // 2025-04-21 is Monday
|
||||
// Check for time labels (every 2 hours: 00, 02, 04...)
|
||||
expect(screen.getByText('00:00')).toBeInTheDocument();
|
||||
expect(screen.getByText('02:00')).toBeInTheDocument();
|
||||
expect(screen.getByText('04:00')).toBeInTheDocument();
|
||||
expect(screen.getByText('22:00')).toBeInTheDocument();
|
||||
// Verify non-labeled hours (every hour that isn't %120 == 0)
|
||||
// We can't easily check for empty text nodes in Vitest/RTL without more precision
|
||||
// but we can check that 01:00 is NOT present as a text label
|
||||
expect(screen.queryByText('01:00')).not.toBeInTheDocument();
|
||||
});
|
||||
it('colors sleeping intervals correctly', () => {
|
||||
useStats.mockReturnValue({
|
||||
timetable: {
|
||||
slots: [
|
||||
{
|
||||
date: '2025-04-20',
|
||||
sleeping: true,
|
||||
slot_start: '01:00',
|
||||
slot_end: '03:00', // Covers 01:00 and 02:00
|
||||
},
|
||||
],
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
const { container } = render(_jsx(Timetable, { ...defaultProps }));
|
||||
// We are looking for the grid cells.
|
||||
// Since they don't have text, we look at their classes/backgrounds.
|
||||
const cells = container.querySelectorAll('.h-8');
|
||||
// Total cells = 24 rows * (1 label col + 2 days) = 72? No, the structure is:
|
||||
// Header row has cells. Time grid rows have (1 label + 2 days) = 3 cells each.
|
||||
// Total elements with .h-8: 1 (header corner) + days.length * 1 (day headers) + 24 * 3 = 75?
|
||||
// Actually, the code has:
|
||||
// Header row: header corner + days.map -> 1 + 2 = 3 cells
|
||||
// Time Rows: (label cell) + days.map -> 24 * 3 = 72 cells
|
||||
// Total = 75.
|
||||
// The sleeping cell for 01:00 on 2025-04-20 should be bg-green-500
|
||||
// The cell for 02:00 on 2025-04-20 should be bg-green-500
|
||||
// The cell for 03:00 on 2025-04-20 should be bg-gray-50
|
||||
// Let's just check if ANY cell has bg-green-500 and ALL non-sleeping have bg-gray-50
|
||||
const greenCells = container.querySelectorAll('.bg-green-500');
|
||||
expect(greenCells.length).toBeGreaterThan(0);
|
||||
const grayCells = container.querySelectorAll('.bg-gray-50');
|
||||
expect(grayCells.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { vi, describe, it, expect } from 'vitest';
|
||||
import Timetable from './SleepTimetable';
|
||||
import { useStats } from '../../hooks/useStats';
|
||||
|
||||
vi.mock('../../hooks/useStats', () => ({
|
||||
useStats: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('SleepTimetable', () => {
|
||||
const defaultProps = {
|
||||
from_date: '2025-04-20',
|
||||
to_date: '2025-04-21',
|
||||
day_start: '22:00',
|
||||
day_end: '08:00',
|
||||
};
|
||||
|
||||
it('renders loading state', () => {
|
||||
(useStats as any).mockReturnValue({ timetable: null, isLoading: true });
|
||||
render(<Timetable {...defaultProps} />);
|
||||
expect(screen.getByText(/Loading timetable.../i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders empty state when no data is available', () => {
|
||||
(useStats as any).mockReturnValue({ timetable: { slots: [] }, isLoading: false });
|
||||
render(<Timetable {...defaultProps} />);
|
||||
expect(screen.getByText(/No data available/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the grid correctly with hourly rows and biennial labels', () => {
|
||||
(useStats as any).mockReturnValue({
|
||||
timetable: {
|
||||
slots: [
|
||||
{
|
||||
date: '2025-04-20',
|
||||
sleeping: true,
|
||||
slot_start: '01:00',
|
||||
slot_end: '05:00',
|
||||
},
|
||||
],
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
render(<Timetable {...defaultProps} />);
|
||||
|
||||
// Check for day headers
|
||||
expect(screen.getByText(/SUN/i)).toBeInTheDocument(); // 2025-04-20 is Sunday
|
||||
expect(screen.getByText(/MON/i)).toBeInTheDocument(); // 2025-04-21 is Monday
|
||||
|
||||
// Check for time labels (every 1 hour: 00, 01, 02...)
|
||||
expect(screen.getByText('00:00')).toBeInTheDocument();
|
||||
expect(screen.getByText('01:00')).toBeInTheDocument();
|
||||
expect(screen.getByText('02:00')).toBeInTheDocument();
|
||||
expect(screen.getByText('03:00')).toBeInTheDocument();
|
||||
expect(screen.getByText('04:00')).toBeInTheDocument();
|
||||
expect(screen.getByText('22:00')).toBeInTheDocument();
|
||||
expect(screen.getByText('23:00')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('colors sleeping intervals correctly', () => {
|
||||
(useStats as any).mockReturnValue({
|
||||
timetable: {
|
||||
slots: [
|
||||
{
|
||||
date: '2025-04-20',
|
||||
sleeping: true,
|
||||
slot_start: '01:00',
|
||||
slot_end: '03:00', // Covers 01:00 and 02:00
|
||||
},
|
||||
],
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { container } = render(<Timetable {...defaultProps} />);
|
||||
|
||||
// We are looking for the grid cells.
|
||||
// Since they don't have text, we look at their classes/backgrounds.
|
||||
const cells = container.querySelectorAll('.h-8');
|
||||
// Total cells = 24 rows * (1 label col + 2 days) = 72? No, the structure is:
|
||||
// Header row has cells. Time grid rows have (1 label + 2 days) = 3 cells each.
|
||||
// Total elements with .h-8: 1 (header corner) + days.length * 1 (day headers) + 24 * 3 = 75?
|
||||
// Actually, the code has:
|
||||
// Header row: header corner + days.map -> 1 + 2 = 3 cells
|
||||
// Time Rows: (label cell) + days.map -> 24 * 3 = 72 cells
|
||||
// Total = 75.
|
||||
|
||||
// The sleeping cell for 01:00 on 2025-04-20 should be bg-green-500
|
||||
// The cell for 02:00 on 2025-04-20 should be bg-green-500
|
||||
// The cell for 03:00 on 2025-04-20 should be bg-gray-50
|
||||
// Let's just check if ANY cell has bg-green-500 and ALL non-sleeping have bg-gray-50
|
||||
|
||||
const greenCells = container.querySelectorAll('.bg-green-500');
|
||||
expect(greenCells.length).toBeGreaterThan(0);
|
||||
|
||||
const grayCells = container.querySelectorAll('.bg-gray-50');
|
||||
expect(grayCells.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -10,8 +10,9 @@ import { ErrorBoundary } from '../../components/ErrorBoundary';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
const StatsPage = () => {
|
||||
const { settings, update } = useSettings();
|
||||
const from_date = new Date(new Date().setDate(new Date().getDate() - settings.statsInterval)).toISOString().split('T')[0];
|
||||
const to_date = new Date().toISOString().split('T')[0];
|
||||
const now = new Date();
|
||||
const to_date = now.toLocaleDateString('en-CA'); // YYYY-MM-DD in local time
|
||||
const from_date = new Date(now.setDate(now.getDate() - settings.statsInterval)).toLocaleDateString('en-CA');
|
||||
return (_jsxs("div", { className: "max-w-4xl mx-auto space-y-8 pb-12", children: [_jsxs("div", { className: "flex justify-between items-center pt-4", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-3xl font-extrabold text-slate-900 tracking-tight", children: "Statistics" }), _jsx("p", { className: "text-slate-500 text-sm mt-1", children: "Overview of sleep patterns and trends" })] }), _jsx(SettingsPopover, { dayStart: settings.dayStart, dayEnd: settings.dayEnd, wakeupRef: settings.wakeupRef, bedtimeRef: settings.bedtimeRef, onSave: (s) => {
|
||||
update('dayStart', s.dayStart);
|
||||
update('dayEnd', s.dayEnd);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useStats } from '../../hooks/useStats';
|
||||
import { formatMinutesAfter, getToday } from '../../utils/time';
|
||||
import { formatMinutesAfter, getToday, getLocalOffsetFromReference } from '../../utils/time';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, } from 'recharts';
|
||||
const WakeupPlot = ({ from_date, to_date, wakeup_ref, bedtime_ref }) => {
|
||||
const { wakeupBedtime, isLoading } = useStats(from_date, to_date, '07:00', '20:00', wakeup_ref, bedtime_ref);
|
||||
@@ -13,11 +13,12 @@ const WakeupPlot = ({ from_date, to_date, wakeup_ref, bedtime_ref }) => {
|
||||
}
|
||||
const data = wakeupBedtime.days.map((day) => {
|
||||
const daysAgo = Math.floor((new Date(getToday()).getTime() - new Date(day.date).getTime()) / (1000 * 60 * 60 * 24));
|
||||
const offsetFromRef = day.wakeup_minutes !== null ? getLocalOffsetFromReference(day.wakeup_minutes, wakeup_ref) : 0;
|
||||
return {
|
||||
date: daysAgo === 0 ? 'Today' : `${daysAgo}d ago`,
|
||||
minutes: day.wakeup_minutes ?? 0,
|
||||
minutes: offsetFromRef,
|
||||
time: day.wakeup_minutes !== null
|
||||
? formatMinutesAfter(day.wakeup_minutes, wakeup_ref)
|
||||
? formatMinutesAfter(offsetFromRef, wakeup_ref)
|
||||
: 'N/A',
|
||||
};
|
||||
}).reverse();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useStats } from '../../hooks/useStats';
|
||||
import { formatMinutesAfter, getToday, getLocalOffsetFromReference } from '../../utils/time';
|
||||
import { formatMinutesAfter, getToday, getLocalOffsetFromReference, timeToMinutes } from '../../utils/time';
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
@@ -18,7 +18,7 @@ interface WakeupBedtimeProps {
|
||||
bedtime_ref: string;
|
||||
}
|
||||
|
||||
const WakeupPlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_ref, bedtime_ref }) => {
|
||||
const WakeupPlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_ref = '07:00', bedtime_ref = '20:00' }) => {
|
||||
const { wakeupBedtime, isLoading } = useStats(from_date, to_date, '07:00', '20:00', wakeup_ref, bedtime_ref);
|
||||
const [chartData, setChartData] = useState<{ date: string; minutes: number; time: string }[]>([]);
|
||||
|
||||
@@ -32,7 +32,21 @@ const WakeupPlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_r
|
||||
const daysAgo = Math.floor(
|
||||
(new Date(getToday()).getTime() - new Date(day.date).getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
const offsetFromRef = day.wakeup_minutes !== null ? getLocalOffsetFromReference(day.wakeup_minutes, wakeup_ref) : 0;
|
||||
|
||||
let offsetFromRef = 0;
|
||||
if (day.wakeup_minutes !== null) {
|
||||
const refMins = timeToMinutes(wakeup_ref);
|
||||
// If wakeup_minutes is significantly smaller than the reference,
|
||||
// it's likely already an offset (relative), not absolute minutes from midnight.
|
||||
const isLikelyOffset = day.wakeup_minutes < refMins && day.wakeup_minutes < 120;
|
||||
|
||||
if (isLikelyOffset) {
|
||||
offsetFromRef = day.wakeup_minutes;
|
||||
} else {
|
||||
offsetFromRef = day.wakeup_minutes - refMins;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
date: daysAgo === 0 ? 'Today' : `${daysAgo}d ago`,
|
||||
minutes: offsetFromRef,
|
||||
@@ -72,7 +86,7 @@ const WakeupPlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_r
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value: any) => [
|
||||
formatMinutesAfter(value ?? 0, wakeup_ref),
|
||||
formatMinutesAfter(value, wakeup_ref),
|
||||
'Wakeup',
|
||||
] as any}
|
||||
labelFormatter={(label) => label}
|
||||
@@ -91,4 +105,4 @@ const WakeupPlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_r
|
||||
);
|
||||
};
|
||||
|
||||
export default WakeupPlot;
|
||||
export default WakeupPlot;
|
||||
|
||||
Reference in New Issue
Block a user