import { useEffect, useMemo, useState } from 'react'
import { deleteSleepEntry, fetchSleepDiary, saveSleepEntry, type SleepEntry, type SleepEntryInput,
type SleepEvent, type SleepEventType, type SleepQuality } from '../api/sleepDiary'
import type { AnalysisPeriod } from '../api/analysis'
import { buildSleepDiaryPdf, presentSleepDiaryPdf } from '../report/sleepDiaryPdf'
type Language = 'fr' | 'en'
const q: readonly SleepQuality[] = ['TB', 'B', 'Moy', 'M', 'TM']
const pointTypes: readonly SleepEventType[] = ['bed_time', 'final_get_up', 'night_get_up', 'daytime_sleepiness']
const copy = {
fr: { agenda: 'Agenda', edit: 'Saisie et édition', morning: 'Matin', day: 'Journée / Soir', night: 'Nuit du',
bed_time: 'Mise au lit', final_get_up: 'Lever', night_get_up: 'Lever nocturne', sleep: 'Sommeil', nap: 'Sieste',
long_awake: 'Long réveil', half_sleep: 'Demi-sommeil', daytime_sleepiness: 'Somnolence', sleepQuality: 'Qualité du sommeil',
wakeQuality: 'Qualité du réveil', dayForm: 'Forme de la journée', notes: 'Traitement et remarques', add: 'Ajouter',
save: 'Enregistrer', remove: 'Supprimer', observations: 'Observations', summary: 'Synthèse factuelle', empty: 'Aucune nuit enregistrée.',
preview: 'Prévisualiser', export: 'Exporter PDF', qualities: ['Très bon', 'Bon', 'Moyen', 'Mauvais', 'Très mauvais'] },
en: { agenda: 'Diary', edit: 'Entry editor', morning: 'Morning', day: 'Day / Evening', night: 'Night from',
bed_time: 'Bedtime', final_get_up: 'Final get-up', night_get_up: 'Night get-up', sleep: 'Sleep', nap: 'Nap',
long_awake: 'Long awakening', half_sleep: 'Half-sleep', daytime_sleepiness: 'Sleepiness', sleepQuality: 'Sleep quality',
wakeQuality: 'Wake quality', dayForm: 'Day form', notes: 'Treatment and notes', add: 'Add', save: 'Save', remove: 'Delete',
observations: 'Observations', summary: 'Factual summary', empty: 'No recorded nights.', preview: 'Preview', export: 'Export PDF',
qualities: ['Very good', 'Good', 'Average', 'Bad', 'Very bad'] },
} as const
const localInput = (iso: string) => iso ? iso.slice(0, 16) : ''
const absolute = (value: string) => value ? new Date(value).toISOString() : ''
const offset = (iso: string, startDate: string) => {
const start = new Date(`${startDate}T18:00:00`)
return Math.max(0, Math.min(100, ((new Date(iso).getTime() - start.getTime()) / 86_400_000) * 100))
}
const blank = (): SleepEntryInput => {
const now = new Date(); const start = new Date(now); if (now.getHours() < 18) start.setDate(start.getDate() - 1)
const end = new Date(start); end.setDate(end.getDate() + 1)
const date = (value: Date) => value.toLocaleDateString('en-CA')
return { entry_id: '', expected_revision: null, night_start_date: date(start), night_end_date: date(end),
created_at: now.toISOString(), updated_at: now.toISOString(), sleep_quality: null, wake_quality: null,
day_form: null, treatment_and_notes: '', events: [] }
}
function QualityPicker({ label, value, onChange, language }: { label: string; value: SleepQuality | null;
onChange: (value: SleepQuality | null) => void; language: Language }) {
return
}
export function SleepDiaryWorkspace({ period, language }: { period: AnalysisPeriod; language: Language }) {
const t = copy[language]; const [snapshot, setSnapshot] = useState> | null>(null); const entries = snapshot?.entries ?? []; const [draft, setDraft] = useState(blank)
const [eventType, setEventType] = useState('sleep'); const [pending, setPending] = useState(false); const [error, setError] = useState('')
const range = useMemo(() => { if (period === 'all') return {}; const days = Number(period.slice(0, -1)); const end = new Date(); const start = new Date(); start.setDate(end.getDate() - days + 1)
return { startDate: start.toLocaleDateString('en-CA'), endDate: end.toLocaleDateString('en-CA') } }, [period])
const reload = () => fetchSleepDiary(range.startDate, range.endDate).then(setSnapshot).catch(() => setError('sleep_diary_unavailable'))
useEffect(() => { void reload() }, [period])
const edit = (entry: SleepEntry) => setDraft({ ...entry, expected_revision: entry.revision_id, events: entry.events.map((event) => ({ ...event })) })
const change = (key: K, value: SleepEntryInput[K]) => setDraft((current) => ({ ...current, [key]: value }))
const addEvent = () => setDraft((current) => ({ ...current, events: [...current.events, { event_id: '', type: eventType,
start_at: new Date().toISOString(), end_at: pointTypes.includes(eventType) ? null : new Date(Date.now() + 3_600_000).toISOString() }] }))
const changeEvent = (index: number, patch: Partial) => setDraft((current) => ({ ...current,
events: current.events.map((event, currentIndex) => currentIndex === index ? { ...event, ...patch } : event) }))
const save = async () => { setPending(true); setError(''); try { await saveSleepEntry({ ...draft, updated_at: new Date().toISOString() }); setDraft(blank()); await reload() } catch (reason) { setError(reason instanceof Error ? reason.message : 'sleep_diary_mutation_failed') } finally { setPending(false) } }
return
{t.agenda}
{entries.length === 0 ? {t.empty}
:
{Array.from({ length: 25 }, (_, index) => {(18 + index) % 24})}
{entries.map((entry) =>
)}
}
{t.edit}
{t.morning}
change('sleep_quality', value)} language={language}/> change('wake_quality', value)} language={language}/>
{t.day}
change('day_form', value)} language={language}/>
{error && {error}
}{draft.expected_revision && }
{t.summary}
- {language === 'fr' ? 'Nuits enregistrées' : 'Recorded nights'}
- {entries.length}
- {language === 'fr' ? 'Sommeil déclaré' : 'Declared sleep'}
- {Math.round((snapshot?.summary.sleep_duration_seconds ?? 0) / 60)} min
- {language === 'fr' ? 'Longs réveils' : 'Long awakenings'}
- {snapshot?.summary.long_awake_count ?? 0}
- {language === 'fr' ? 'Siestes' : 'Naps'}
- {snapshot?.summary.nap_count ?? 0}
{t.observations}
{entries.filter((entry) => entry.treatment_and_notes).map((entry) => {entry.night_start_date} — {entry.treatment_and_notes}
)}
}