Add 11 major features for caregiver health management

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>
This commit is contained in:
Gemini Agent
2026-01-23 09:42:46 +00:00
parent 515376e126
commit dd4ef2c4cd
70 changed files with 7322 additions and 79 deletions

View File

@@ -0,0 +1,48 @@
import { NextResponse } from 'next/server'
import { sendDueNotifications } from '@/lib/notifications/scheduler'
// This endpoint should be called by a cron job every minute
// You can set up a cron service like:
// - Vercel Cron Jobs
// - AWS EventBridge
// - A simple setInterval in a long-running process
// POST /api/notifications/send - Trigger sending due notifications
export async function POST(req: Request) {
try {
// Verify cron secret to prevent unauthorized access
const authHeader = req.headers.get('authorization')
const cronSecret = process.env.CRON_SECRET
// If CRON_SECRET is set, verify it
if (cronSecret && authHeader !== `Bearer ${cronSecret}`) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
const { sent, failed } = await sendDueNotifications()
return NextResponse.json({
success: true,
sent,
failed,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Send notifications error:', error)
return NextResponse.json(
{ error: 'Failed to send notifications' },
{ status: 500 }
)
}
}
// GET endpoint for health checks
export async function GET() {
return NextResponse.json({
status: 'ok',
message: 'Notification sender is ready',
})
}

View File

@@ -0,0 +1,106 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db/prisma'
import { checkWorkspaceAccess } from '@/lib/db/workspace-access'
import { withAuth, type AuthenticatedRequest } from '@/lib/auth'
import { getPublicVAPIDKey } from '@/lib/notifications/push'
// GET /api/notifications/subscribe - Get VAPID public key
export const GET = withAuth(async () => {
const publicKey = getPublicVAPIDKey()
if (!publicKey) {
return NextResponse.json(
{ error: 'Push notifications not configured' },
{ status: 503 }
)
}
return NextResponse.json({ publicKey })
})
// POST /api/notifications/subscribe - Subscribe to push notifications
export const POST = withAuth(async (req: AuthenticatedRequest) => {
try {
const body = await req.json()
const { subscription, workspaceId } = body
if (!subscription || !subscription.endpoint || !subscription.keys) {
return NextResponse.json(
{ error: 'Invalid subscription data' },
{ status: 400 }
)
}
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id)
if (!access) {
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
// Upsert the subscription (update if exists, create if not)
const existing = await prisma.pushSubscription.findFirst({
where: {
endpoint: subscription.endpoint,
userId: req.session.user.id,
},
})
if (existing) {
await prisma.pushSubscription.update({
where: { id: existing.id },
data: {
p256dh: subscription.keys.p256dh,
auth: subscription.keys.auth,
workspaceId,
},
})
} else {
await prisma.pushSubscription.create({
data: {
userId: req.session.user.id,
workspaceId,
endpoint: subscription.endpoint,
p256dh: subscription.keys.p256dh,
auth: subscription.keys.auth,
},
})
}
return NextResponse.json({ success: true })
} catch (error) {
console.error('Subscribe error:', error)
return NextResponse.json(
{ error: 'Failed to subscribe' },
{ status: 500 }
)
}
})
// DELETE /api/notifications/subscribe - Unsubscribe from push notifications
export const DELETE = withAuth(async (req: AuthenticatedRequest) => {
try {
const { searchParams } = new URL(req.url)
const endpoint = searchParams.get('endpoint')
if (!endpoint) {
return NextResponse.json(
{ error: 'Endpoint required' },
{ status: 400 }
)
}
await prisma.pushSubscription.deleteMany({
where: {
endpoint,
userId: req.session.user.id,
},
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Unsubscribe error:', error)
return NextResponse.json(
{ error: 'Failed to unsubscribe' },
{ status: 500 }
)
}
})