Compare commits
2 Commits
6979f1685a
...
8852cf202e
| Author | SHA1 | Date | |
|---|---|---|---|
| 8852cf202e | |||
| 91184cfc96 |
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta, timezone
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
@@ -71,6 +71,7 @@ async def get_timetable(
|
|||||||
adj_from_date = from_date - timedelta(days=1)
|
adj_from_date = from_date - timedelta(days=1)
|
||||||
sessions, _ = await get_sessions(session, from_date=adj_from_date, to_date=to_date)
|
sessions, _ = await get_sessions(session, from_date=adj_from_date, to_date=to_date)
|
||||||
slots = []
|
slots = []
|
||||||
|
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
current_date = from_date.date()
|
current_date = from_date.date()
|
||||||
while current_date <= to_date.date():
|
while current_date <= to_date.date():
|
||||||
date_str = current_date.strftime("%Y-%m-%d")
|
date_str = current_date.strftime("%Y-%m-%d")
|
||||||
@@ -83,9 +84,8 @@ async def get_timetable(
|
|||||||
slot_end_str = slot_end_time.strftime("%H:%M")
|
slot_end_str = slot_end_time.strftime("%H:%M")
|
||||||
is_sleeping = False
|
is_sleeping = False
|
||||||
for s in sessions:
|
for s in sessions:
|
||||||
if s.start_time <= slot_end_time and (
|
effective_end_time = s.end_time if s.end_time is not None else now
|
||||||
s.end_time is None or s.end_time >= slot_start_time
|
if s.start_time <= slot_end_time and effective_end_time >= slot_start_time:
|
||||||
):
|
|
||||||
is_sleeping = True
|
is_sleeping = True
|
||||||
break
|
break
|
||||||
slots.append(
|
slots.append(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState, useEffect, Fragment } from 'react';
|
import { useMemo, useState, useEffect, Fragment } from 'react';
|
||||||
import { useStats } from '../../hooks/useStats';
|
import { useStats } from '../../hooks/useStats';
|
||||||
import { timeToMinutes } from '../../utils/time';
|
import { timeToMinutes, adjustForTimezone } from '../../utils/time';
|
||||||
|
|
||||||
interface TimetableProps {
|
interface TimetableProps {
|
||||||
from_date: string;
|
from_date: string;
|
||||||
@@ -106,8 +106,8 @@ const Timetable = ({ from_date, to_date, day_start, day_end }: TimetableProps) =
|
|||||||
return timetable.slots.some((slot) => {
|
return timetable.slots.some((slot) => {
|
||||||
if (slot.date !== date || !slot.sleeping) return false;
|
if (slot.date !== date || !slot.sleeping) return false;
|
||||||
|
|
||||||
const sStart = timeToMinutes(slot.slot_start);
|
const sStart = adjustForTimezone(timeToMinutes(slot.slot_start));
|
||||||
const sEnd = timeToMinutes(slot.slot_end);
|
const sEnd = adjustForTimezone(timeToMinutes(slot.slot_end));
|
||||||
|
|
||||||
if (sEnd <= sStart) {
|
if (sEnd <= sStart) {
|
||||||
const overlap1 = Math.max(sStart, intervalStart) < Math.min(1440, intervalEnd);
|
const overlap1 = Math.max(sStart, intervalStart) < Math.min(1440, intervalEnd);
|
||||||
|
|||||||
@@ -68,7 +68,11 @@ export function formatElapsedTime(utcTimestamp: string): string {
|
|||||||
* @returns formatted datetime string
|
* @returns formatted datetime string
|
||||||
*/
|
*/
|
||||||
export function formatDateTime(date: Date | string): string {
|
export function formatDateTime(date: Date | string): string {
|
||||||
const d = typeof date === 'string' ? new Date(date) : date;
|
let dateToParse = date;
|
||||||
|
if (typeof date === 'string' && !date.endsWith('Z') && !date.includes('+')) {
|
||||||
|
dateToParse = date + 'Z';
|
||||||
|
}
|
||||||
|
const d = typeof dateToParse === 'string' ? new Date(dateToParse) : dateToParse;
|
||||||
return d.toLocaleString('sv-SE', {
|
return d.toLocaleString('sv-SE', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ describe('SleepTimetable', () => {
|
|||||||
fireEvent(window, new Event('resize'));
|
fireEvent(window, new Event('resize'));
|
||||||
|
|
||||||
// Check for 3-day interval tabs
|
// Check for 3-day interval tabs
|
||||||
// In reversed order (25, 24, 23...), the first group is Fri - Wed
|
// In reversed order (25, 24, 23...), the first group is Wed - Fri
|
||||||
expect(screen.getByRole('button', { name: /Fri \d+ - Wed \d+/i })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /Wed \d+ - Fri \d+/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders in small mode (single-day tabs and 1-column grid)', () => {
|
it('renders in small mode (single-day tabs and 1-column grid)', () => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
timeToMinutes,
|
timeToMinutes,
|
||||||
adjustForTimezone,
|
adjustForTimezone,
|
||||||
getLocalOffsetFromReference,
|
getLocalOffsetFromReference,
|
||||||
|
formatDateTime,
|
||||||
} from '../../src/utils/time';
|
} from '../../src/utils/time';
|
||||||
|
|
||||||
describe('formatMinutesAfter', () => {
|
describe('formatMinutesAfter', () => {
|
||||||
@@ -125,6 +126,21 @@ describe('adjustForTimezone', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('formatDateTime', () => {
|
||||||
|
it('treats string without Z as UTC', () => {
|
||||||
|
const dateStr = '2024-01-15T10:00:00';
|
||||||
|
const dateWithZ = '2024-01-15T10:00:00Z';
|
||||||
|
expect(formatDateTime(dateStr)).toBe(formatDateTime(dateWithZ));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats a Date object correctly', () => {
|
||||||
|
const date = new Date('2024-01-15T10:00:00Z');
|
||||||
|
// The output depends on the local timezone, so we check for part of it
|
||||||
|
const result = formatDateTime(date);
|
||||||
|
expect(result).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getLocalOffsetFromReference', () => {
|
describe('getLocalOffsetFromReference', () => {
|
||||||
it('returns local offset from reference in minutes', () => {
|
it('returns local offset from reference in minutes', () => {
|
||||||
const utcMins = 600; // 10:00 UTC
|
const utcMins = 600; // 10:00 UTC
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# Sleep Timetable Bug Fixes
|
|
||||||
|
|
||||||
## Identified Issues
|
|
||||||
1. **Pre-selection**: Component doesn't always pre-select the latest date across all tiers.
|
|
||||||
2. **Grid Layout (Medium Tier)**: Grid columns are hardcoded to 3, causing breakage when fewer than 3 days are displayed at the end of a range.
|
|
||||||
3. **Selector Information**: Tabs only show weekday names, making it hard to distinguish dates.
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### 1. Fix Pre-selection
|
|
||||||
- Add a `useEffect` to set `selectedDayIndex` to the last index of the `days` array whenever the `days` list is initialized or updated. This ensures "today" (the latest date) is selected by default.
|
|
||||||
|
|
||||||
### 2. Fix Grid Alignment
|
|
||||||
- Refactor the `gridTemplateColumns` logic for both the header and time grid.
|
|
||||||
- Instead of hardcoding tiers (e.g., `repeat(3, 1fr)`), use the actual number of days in the current view: `4rem repeat(${visibleDays.length}, 1fr)`. This allows the grid to gracefully handle 1, 2, or 3 columns.
|
|
||||||
|
|
||||||
### 3. Enhance Selector Labels
|
|
||||||
- Implement a `formatDayLabel` helper that combines the short weekday name and the day of the month (e.g., "FRI 24").
|
|
||||||
- Update `tabGroups` generation for both 'medium' (range) and 'small' (single day) tiers to use this descriptive label.
|
|
||||||
|
|
||||||
### 4. UI Compactness
|
|
||||||
- Adjust spacing (margins/paddings) in the mobile/medium view to reduce overall component height and keep selectors closer to the grid.
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Things I need to tell my AI to do
|
||||||
|
|
||||||
|
- [ ] Check so that all tests actually make sense, maybe some are now obsolete, and some are missing
|
||||||
|
- [ ] Add a test coverage metric to both frontend and backend so that we can check how much of the code is tested. I would like >=90%
|
||||||
|
- [ ] Go over Claude's slop-cleanup-stuff
|
||||||
Reference in New Issue
Block a user