diff --git a/src/app/(app)/appointments/[id]/edit/page.tsx b/src/app/(app)/appointments/[id]/edit/page.tsx new file mode 100644 index 0000000..858fd84 --- /dev/null +++ b/src/app/(app)/appointments/[id]/edit/page.tsx @@ -0,0 +1,215 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useRouter, useParams } from 'next/navigation' +import { format, parseISO } from 'date-fns' +import { toZonedTime } from 'date-fns-tz' +import { Button, Input, Textarea, Card, LoadingState, showToast } from '@/components/ui' +import { Header, PageContainer } from '@/components/layout/header' +import { useApp } from '../../../provider' + +const TIMEZONE = 'Australia/Perth' + +interface Appointment { + id: string + title: string + datetime: string + location: string | null + mapUrl: string | null + notes: string | null +} + +export default function EditAppointmentPage() { + const router = useRouter() + const params = useParams() + const appointmentId = params.id as string + const { currentWorkspace, refreshData } = useApp() + + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [error, setError] = useState('') + + const [title, setTitle] = useState('') + const [date, setDate] = useState('') + const [time, setTime] = useState('') + const [location, setLocation] = useState('') + const [mapUrl, setMapUrl] = useState('') + const [notes, setNotes] = useState('') + + useEffect(() => { + async function fetchAppointment() { + try { + const response = await fetch( + `/api/workspaces/${currentWorkspace.id}/appointments/${appointmentId}` + ) + if (response.ok) { + const data = await response.json() + const appt: Appointment = data.appointment + + // Parse datetime and convert to local timezone + const apptDate = toZonedTime(parseISO(appt.datetime), TIMEZONE) + setTitle(appt.title) + setDate(format(apptDate, 'yyyy-MM-dd')) + setTime(format(apptDate, 'HH:mm')) + setLocation(appt.location || '') + setMapUrl(appt.mapUrl || '') + setNotes(appt.notes || '') + } else { + setError('Appointment not found') + } + } catch (err) { + console.error('Failed to fetch appointment:', err) + setError('Failed to load appointment') + } finally { + setLoading(false) + } + } + + fetchAppointment() + }, [currentWorkspace.id, appointmentId]) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError('') + setSaving(true) + + try { + // Combine date and time + const datetime = new Date(`${date}T${time}:00`) + + const response = await fetch( + `/api/workspaces/${currentWorkspace.id}/appointments/${appointmentId}`, + { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title, + datetime: datetime.toISOString(), + location: location || null, + mapUrl: mapUrl || null, + notes: notes || null, + }), + } + ) + + if (!response.ok) { + const data = await response.json() + throw new Error(data.error || 'Failed to update appointment') + } + + await refreshData() + showToast('Appointment updated', 'success') + router.push(`/appointments/${appointmentId}`) + } catch (err) { + setError(err instanceof Error ? err.message : 'Something went wrong') + } finally { + setSaving(false) + } + } + + if (loading) { + return ( + <> + + + + + > + ) + } + + if (error && !title) { + return ( + <> + + + + {error} + + + > + ) + } + + return ( + <> + + + + + setTitle(e.target.value)} + placeholder="e.g., Oncology Appointment" + required + /> + + + setDate(e.target.value)} + required + /> + setTime(e.target.value)} + required + /> + + + setLocation(e.target.value)} + placeholder="e.g., Level 3, Cancer Centre" + /> + + setMapUrl(e.target.value)} + placeholder="https://maps.google.com/..." + helperText="Paste a Google Maps or Apple Maps link" + /> + + setNotes(e.target.value)} + placeholder="Any notes for this appointment..." + rows={3} + /> + + {error && ( + + {error} + + )} + + + router.back()} + > + Cancel + + + Save Changes + + + + + + > + ) +} diff --git a/src/lib/calendar/ical-generator.ts b/src/lib/calendar/ical-generator.ts index fc4c435..682ec4b 100644 --- a/src/lib/calendar/ical-generator.ts +++ b/src/lib/calendar/ical-generator.ts @@ -1,5 +1,7 @@ import { format, addHours } from 'date-fns' +const TIMEZONE = 'Australia/Perth' + interface Appointment { id: string title: string @@ -35,8 +37,19 @@ export function generateICalendar( 'VERSION:2.0', 'PRODID:-//NextStep//Health Management//EN', `X-WR-CALNAME:${escapeICalText(workspaceName)}`, + `X-WR-TIMEZONE:${TIMEZONE}`, 'CALSCALE:GREGORIAN', 'METHOD:PUBLISH', + // Add timezone definition for Australia/Perth + 'BEGIN:VTIMEZONE', + `TZID:${TIMEZONE}`, + 'BEGIN:STANDARD', + 'DTSTART:19700101T000000', + 'TZOFFSETFROM:+0800', + 'TZOFFSETTO:+0800', + 'TZNAME:AWST', + 'END:STANDARD', + 'END:VTIMEZONE', ] for (const appt of appointments) { @@ -45,9 +58,9 @@ export function generateICalendar( lines.push('BEGIN:VEVENT') lines.push(`UID:${appt.id}@nextstep`) - lines.push(`DTSTAMP:${formatICalDate(new Date())}`) - lines.push(`DTSTART:${formatICalDate(startDate)}`) - lines.push(`DTEND:${formatICalDate(endDate)}`) + lines.push(`DTSTAMP:${formatICalDateUTC(new Date())}`) + lines.push(`DTSTART;TZID=${TIMEZONE}:${formatICalDateLocal(startDate)}`) + lines.push(`DTEND;TZID=${TIMEZONE}:${formatICalDateLocal(endDate)}`) lines.push(`SUMMARY:${escapeICalText(appt.title)}`) if (appt.location) { @@ -116,9 +129,9 @@ export function generateMedicationEvents( lines.push('BEGIN:VEVENT') lines.push(`UID:med-${med.id}-${dateStr}-${time}@nextstep`) - lines.push(`DTSTAMP:${formatICalDate(new Date())}`) - lines.push(`DTSTART:${formatICalDate(startDate)}`) - lines.push(`DTEND:${formatICalDate(endDate)}`) + lines.push(`DTSTAMP:${formatICalDateUTC(new Date())}`) + lines.push(`DTSTART;TZID=${TIMEZONE}:${formatICalDateLocal(startDate)}`) + lines.push(`DTEND;TZID=${TIMEZONE}:${formatICalDateLocal(endDate)}`) lines.push(`SUMMARY:Take ${escapeICalText(med.name)}`) lines.push('CATEGORIES:MEDICATION') @@ -161,11 +174,16 @@ function getMedicationTimes(med: Medication): string[] { } } -function formatICalDate(date: Date): string { - // Format: YYYYMMDDTHHMMSSZ +function formatICalDateUTC(date: Date): string { + // Format: YYYYMMDDTHHMMSSZ (UTC) return format(date, "yyyyMMdd'T'HHmmss'Z'") } +function formatICalDateLocal(date: Date): string { + // Format: YYYYMMDDTHHMMSS (local time, no Z suffix) + return format(date, "yyyyMMdd'T'HHmmss") +} + function escapeICalText(text: string): string { return text .replace(/\\/g, '\\\\')
{error}
+ {error} +