mirror of
https://github.com/Tony0410/nextstep.git
synced 2026-05-25 05:41:39 +08:00
Features added: - Emergency Info Card: Full-screen emergency view with patient info - Refill Tracker: Track pill counts with auto-decrement on dose - Activity Feed: View caregiver activity with filtering - Symptom Tracker: Log symptoms with severity and offline sync - Print Views: Daily meds, appointments, doctor visit summaries - iCal Export: Calendar subscription for appointments - PDF Export: Medical summary for doctor visits - Calendar View: Monthly calendar for appointments - Appointment Preparation: Checklist for upcoming appointments - Medication Reminders: PWA push notifications with quiet hours Bug fixes: - Fix invite workflow: Register/login now properly redirect back - Add undo for doctor questions (can unmark "asked" questions) - Fix API route type annotations for Next.js 14 compatibility - Add Suspense boundary for useSearchParams in login/register Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
175 lines
6.0 KiB
TypeScript
175 lines
6.0 KiB
TypeScript
'use client'
|
|
|
|
import { useRouter } from 'next/navigation'
|
|
import { format, isToday, isTomorrow, parseISO, startOfDay } from 'date-fns'
|
|
import { toZonedTime } from 'date-fns-tz'
|
|
import { Plus, Calendar, MapPin, Clock, ChevronRight, CalendarDays } from 'lucide-react'
|
|
import { useLiveQuery } from 'dexie-react-hooks'
|
|
|
|
import { db } from '@/lib/sync'
|
|
import { Card, LoadingState, EmptyState, Button } from '@/components/ui'
|
|
import { Header, PageContainer } from '@/components/layout/header'
|
|
import { useApp } from '../provider'
|
|
|
|
const TIMEZONE = 'Australia/Perth'
|
|
|
|
export default function AppointmentsPage() {
|
|
const router = useRouter()
|
|
const { currentWorkspace } = useApp()
|
|
|
|
const appointments = useLiveQuery(
|
|
() =>
|
|
db.appointments
|
|
.where('workspaceId')
|
|
.equals(currentWorkspace.id)
|
|
.and((a) => !a.deletedAt)
|
|
.sortBy('datetime'),
|
|
[currentWorkspace.id]
|
|
)
|
|
|
|
// Group appointments by date
|
|
const groupedAppointments = appointments?.reduce(
|
|
(groups, appt) => {
|
|
const date = toZonedTime(parseISO(appt.datetime), TIMEZONE)
|
|
const dateKey = format(startOfDay(date), 'yyyy-MM-dd')
|
|
|
|
if (!groups[dateKey]) {
|
|
groups[dateKey] = {
|
|
date,
|
|
appointments: [],
|
|
}
|
|
}
|
|
groups[dateKey].appointments.push(appt)
|
|
return groups
|
|
},
|
|
{} as Record<string, { date: Date; appointments: typeof appointments }>
|
|
)
|
|
|
|
const sortedDates = Object.keys(groupedAppointments || {}).sort()
|
|
|
|
const formatDateHeader = (date: Date) => {
|
|
if (isToday(date)) return 'Today'
|
|
if (isTomorrow(date)) return 'Tomorrow'
|
|
return format(date, 'EEEE, MMMM d')
|
|
}
|
|
|
|
if (!appointments) {
|
|
return (
|
|
<>
|
|
<Header
|
|
title="Appointments"
|
|
rightAction={{
|
|
icon: <CalendarDays className="w-6 h-6 text-secondary-700" />,
|
|
label: 'Calendar view',
|
|
onClick: () => router.push('/appointments/calendar'),
|
|
}}
|
|
/>
|
|
<PageContainer>
|
|
<LoadingState message="Loading appointments..." />
|
|
</PageContainer>
|
|
</>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Header
|
|
title="Appointments"
|
|
rightAction={{
|
|
icon: <CalendarDays className="w-6 h-6 text-secondary-700" />,
|
|
label: 'Calendar view',
|
|
onClick: () => router.push('/appointments/calendar'),
|
|
}}
|
|
/>
|
|
<PageContainer className="pt-4">
|
|
{appointments.length === 0 ? (
|
|
<EmptyState
|
|
type="appointments"
|
|
title="No appointments"
|
|
description="Add your upcoming appointments to keep track of them."
|
|
action={{
|
|
label: 'Add Appointment',
|
|
onClick: () => router.push('/appointments/new'),
|
|
}}
|
|
/>
|
|
) : (
|
|
<div className="space-y-6">
|
|
{sortedDates.map((dateKey) => {
|
|
const group = groupedAppointments![dateKey]
|
|
const isPast = group.date < startOfDay(new Date())
|
|
|
|
return (
|
|
<div key={dateKey}>
|
|
<h2
|
|
className={`text-sm font-semibold mb-3 ${
|
|
isPast ? 'text-secondary-400' : 'text-secondary-600'
|
|
}`}
|
|
>
|
|
{formatDateHeader(group.date)}
|
|
</h2>
|
|
<div className="space-y-3">
|
|
{group.appointments.map((appt) => (
|
|
<Card
|
|
key={appt.id}
|
|
onClick={() => router.push(`/appointments/${appt.id}`)}
|
|
className={`${isPast ? 'opacity-60' : ''}`}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<div
|
|
className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
|
|
isPast
|
|
? 'bg-secondary-100'
|
|
: 'bg-primary-100'
|
|
}`}
|
|
>
|
|
<Calendar
|
|
className={`w-5 h-5 ${
|
|
isPast
|
|
? 'text-secondary-400'
|
|
: 'text-primary-600'
|
|
}`}
|
|
/>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-semibold text-secondary-900 truncate">
|
|
{appt.title}
|
|
</h3>
|
|
<p className="text-sm text-secondary-500 flex items-center gap-1 mt-1">
|
|
<Clock className="w-4 h-4" />
|
|
{format(
|
|
toZonedTime(parseISO(appt.datetime), TIMEZONE),
|
|
'h:mm a'
|
|
)}
|
|
</p>
|
|
{appt.location && (
|
|
<p className="text-sm text-secondary-400 flex items-center gap-1 mt-0.5">
|
|
<MapPin className="w-4 h-4" />
|
|
<span className="truncate">{appt.location}</span>
|
|
</p>
|
|
)}
|
|
</div>
|
|
<ChevronRight className="w-5 h-5 text-secondary-300" />
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* Add appointment FAB */}
|
|
<div className="fixed bottom-20 right-4 z-30">
|
|
<Button
|
|
onClick={() => router.push('/appointments/new')}
|
|
className="w-14 h-14 rounded-full shadow-lg flex items-center justify-center"
|
|
>
|
|
<Plus className="w-6 h-6" />
|
|
</Button>
|
|
</div>
|
|
</PageContainer>
|
|
</>
|
|
)
|
|
}
|