diff --git a/docker/init.sql b/docker/init.sql index b514d5e..814cb98 100644 --- a/docker/init.sql +++ b/docker/init.sql @@ -209,6 +209,9 @@ ADD COLUMN IF NOT EXISTS pii_redacted_at TIMESTAMPTZ; ALTER TABLE IF EXISTS bookings ALTER COLUMN booking_token SET DEFAULT app_random_token_64(); +ALTER TABLE IF EXISTS bookings +ADD COLUMN IF NOT EXISTS session_notes TEXT; + CREATE TABLE IF NOT EXISTS waitlist_entries ( id BIGSERIAL PRIMARY KEY, tenant_id INTEGER NOT NULL DEFAULT 1 REFERENCES tenants(id), diff --git a/packages/client/src/pages/ProjectDetailPage.tsx b/packages/client/src/pages/ProjectDetailPage.tsx index 18c9e54..6223673 100644 --- a/packages/client/src/pages/ProjectDetailPage.tsx +++ b/packages/client/src/pages/ProjectDetailPage.tsx @@ -1,8 +1,9 @@ -import { useCallback, useEffect, useMemo, useState, type FormEvent } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { CalendarX, ClipboardCopy, Inbox, Pencil, Plus, Save, Trash2 } from 'lucide-react'; import type { + Booking, ProjectDetail, ProjectDetailResponse, ProjectResponse, @@ -45,6 +46,7 @@ interface ProjectSignupRow { end_time: string; booked_at: string; cancelled_at: string | null; + session_notes: string | null; } interface EditTimeBlockState { @@ -77,6 +79,60 @@ function toFormState(project: ProjectDetail): ProjectFormState { }; } +function SessionNotesCell({ bookingId, initialNotes }: { bookingId: number; initialNotes: string | null }): JSX.Element { + const [notes, setNotes] = useState(initialNotes ?? ''); + const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved'>('idle'); + const debounceRef = useRef | null>(null); + + const saveNotes = useCallback(async (value: string) => { + setSaveStatus('saving'); + try { + await apiFetch(`/bookings/${bookingId}/notes`, { + method: 'PUT', + body: JSON.stringify({ session_notes: value }) + }); + setSaveStatus('saved'); + } catch { + setSaveStatus('idle'); + } + }, [bookingId]); + + const handleChange = useCallback((value: string) => { + setNotes(value); + setSaveStatus('idle'); + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } + debounceRef.current = setTimeout(() => { + void saveNotes(value); + }, 800); + }, [saveNotes]); + + const handleBlur = useCallback(() => { + if (debounceRef.current) { + clearTimeout(debounceRef.current); + debounceRef.current = null; + } + void saveNotes(notes); + }, [notes, saveNotes]); + + return ( +
+