diff --git a/app/api/reports/[reportId]/route.ts b/app/api/reports/[reportId]/route.ts new file mode 100644 index 0000000..685fd87 --- /dev/null +++ b/app/api/reports/[reportId]/route.ts @@ -0,0 +1,138 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { createClient } from '@/lib/auth/server'; + +const uuidSchema = z.string().uuid('reportId moet een geldige UUID zijn'); + +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ reportId: string }> } +) { + try { + const { reportId } = await params; + + if (!uuidSchema.safeParse(reportId).success) { + return NextResponse.json( + { error: 'reportId moet een geldige UUID zijn' }, + { status: 400 } + ); + } + + const supabase = await createClient(); + const { data, error } = await supabase + .from('reports') + .select('*') + .eq('id', reportId) + .is('deleted_at', null) + .single(); + + if (error) { + if (error.code === 'PGRST116') { + return NextResponse.json({ error: 'Rapport niet gevonden' }, { status: 404 }); + } + + console.error('Error fetching report:', error); + return NextResponse.json( + { error: 'Fout bij ophalen rapport', details: error.message }, + { status: 500 } + ); + } + + return NextResponse.json(data); + } catch (error) { + console.error('Unexpected error in GET /api/reports/[reportId]:', error); + return NextResponse.json( + { error: 'Onverwachte serverfout' }, + { status: 500 } + ); + } +} + +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ reportId: string }> } +) { + try { + const { reportId } = await params; + const body = await request.json(); + + if (!uuidSchema.safeParse(reportId).success) { + return NextResponse.json( + { error: 'reportId moet een geldige UUID zijn' }, + { status: 400 } + ); + } + + const supabase = await createClient(); + const { data, error } = await supabase + .from('reports') + .update({ ...body, updated_at: new Date().toISOString() }) + .eq('id', reportId) + .select('*') + .single(); + + if (error) { + if (error.code === 'PGRST116') { + return NextResponse.json({ error: 'Rapport niet gevonden' }, { status: 404 }); + } + + console.error('Error updating report:', error); + return NextResponse.json( + { error: 'Fout bij updaten rapport', details: error.message }, + { status: 500 } + ); + } + + return NextResponse.json(data); + } catch (error) { + console.error('Unexpected error in PATCH /api/reports/[reportId]:', error); + if (error instanceof SyntaxError) { + return NextResponse.json( + { error: 'Ongeldige JSON in request body' }, + { status: 400 } + ); + } + return NextResponse.json( + { error: 'Onverwachte serverfout' }, + { status: 500 } + ); + } +} + +export async function DELETE( + _request: NextRequest, + { params }: { params: Promise<{ reportId: string }> } +) { + try { + const { reportId } = await params; + + if (!uuidSchema.safeParse(reportId).success) { + return NextResponse.json( + { error: 'reportId moet een geldige UUID zijn' }, + { status: 400 } + ); + } + + const supabase = await createClient(); + const { error } = await supabase + .from('reports') + .update({ deleted_at: new Date().toISOString() }) + .eq('id', reportId); + + if (error) { + console.error('Error deleting report:', error); + return NextResponse.json( + { error: 'Verwijderen mislukt', details: error.message }, + { status: 500 } + ); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Unexpected error in DELETE /api/reports/[reportId]:', error); + return NextResponse.json( + { error: 'Onverwachte serverfout' }, + { status: 500 } + ); + } +} diff --git a/app/api/reports/classify/route.ts b/app/api/reports/classify/route.ts new file mode 100644 index 0000000..dccac0f --- /dev/null +++ b/app/api/reports/classify/route.ts @@ -0,0 +1,79 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const CLASSIFICATION_PROMPT = ` +Je bent een classificatie-assistent voor een GGZ EPD-systeem. + +Classificeer de volgende rapportage als één van deze types: + +**Behandeladvies:** +- Bevat een concreet behandelplan of voorstel +- Noemt doelen, interventies, aanpak +- Woorden zoals: "advies", "plan", "traject", "sessies", "behandeling" + +**Vrije notitie:** +- Alles wat niet duidelijk een behandeladvies is +- Algemene observaties, opmerkingen, aantekeningen + +Return ALLEEN JSON: +{ + "type": "behandeladvies" | "vrije_notitie", + "confidence": 0.0-1.0, + "reasoning": "optionele uitleg" +}`; + +export async function POST(request: NextRequest) { + try { + const { content } = await request.json(); + + if (!content || content.length < 20) { + return NextResponse.json( + { error: 'Content moet minimaal 20 karakters bevatten' }, + { status: 400 } + ); + } + + const apiKey = process.env.ANTHROPIC_API_KEY; + if (!apiKey) { + return NextResponse.json( + { error: 'ANTHROPIC_API_KEY ontbreekt' }, + { status: 500 } + ); + } + + const anthropicResponse = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + }, + body: JSON.stringify({ + model: 'claude-3-5-sonnet-20240620', + max_tokens: 256, + temperature: 0, + system: CLASSIFICATION_PROMPT, + messages: [ + { role: 'user', content: content.trim() }, + ], + }), + }); + + if (!anthropicResponse.ok) { + const errorBody = await anthropicResponse.text(); + throw new Error(`Claude API error: ${errorBody}`); + } + + const data = await anthropicResponse.json(); + const rawText = data?.content?.[0]?.text ?? ''; + const parsed = JSON.parse(rawText); + + return NextResponse.json(parsed); + } catch (error) { + console.error('AI classification error:', error); + return NextResponse.json({ + type: 'vrije_notitie', + confidence: 0.5, + reasoning: 'AI classificatie mislukt. Kies handmatig een type.', + }); + } +} diff --git a/app/api/reports/route.ts b/app/api/reports/route.ts new file mode 100644 index 0000000..e9614d0 --- /dev/null +++ b/app/api/reports/route.ts @@ -0,0 +1,120 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { createClient } from '@/lib/auth/server'; +import { CreateReportSchema, type ReportListResponse } from '@/lib/types/report'; + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const patientId = searchParams.get('patientId'); + + if (!patientId) { + return NextResponse.json( + { error: 'patientId query parameter is verplicht' }, + { status: 400 } + ); + } + + if (!z.string().uuid().safeParse(patientId).success) { + return NextResponse.json( + { error: 'patientId moet een geldige UUID zijn' }, + { status: 400 } + ); + } + + const supabase = await createClient(); + const { data, error } = await supabase + .from('reports') + .select('*') + .eq('patient_id', patientId) + .is('deleted_at', null) + .order('created_at', { ascending: false }); + + if (error) { + console.error('Error fetching reports:', error); + return NextResponse.json( + { error: 'Fout bij ophalen rapportages', details: error.message }, + { status: 500 } + ); + } + + const response: ReportListResponse = { + reports: data ?? [], + total: data?.length ?? 0, + }; + + return NextResponse.json(response); + } catch (error) { + console.error('Unexpected error in GET /api/reports:', error); + return NextResponse.json( + { error: 'Onverwachte serverfout' }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const result = CreateReportSchema.safeParse(body); + + if (!result.success) { + return NextResponse.json( + { + error: 'Validatiefout', + details: result.error.issues.map((issue) => ({ + field: issue.path.join('.'), + message: issue.message, + })), + }, + { status: 400 } + ); + } + + const supabase = await createClient(); + const { data: authData } = await supabase.auth.getUser(); + + if (!authData?.user) { + return NextResponse.json( + { error: 'Niet geautoriseerd' }, + { status: 401 } + ); + } + + const { patient_id, type, content, ai_confidence, ai_reasoning } = result.data; + const { data, error } = await supabase + .from('reports') + .insert({ + patient_id, + type, + content, + ai_confidence, + ai_reasoning, + created_by: authData.user.id, + }) + .select('*') + .single(); + + if (error) { + console.error('Error creating report:', error); + return NextResponse.json( + { error: 'Opslaan mislukt', details: error.message }, + { status: 500 } + ); + } + + return NextResponse.json(data, { status: 201 }); + } catch (error) { + console.error('Unexpected error in POST /api/reports:', error); + if (error instanceof SyntaxError) { + return NextResponse.json( + { error: 'Ongeldige JSON in request body' }, + { status: 400 } + ); + } + return NextResponse.json( + { error: 'Onverwachte serverfout' }, + { status: 500 } + ); + } +} diff --git a/app/epd/patients/[id]/intakes/[intakeId]/behandeladvies/components/treatment-advice-form.tsx b/app/epd/patients/[id]/intakes/[intakeId]/behandeladvies/components/treatment-advice-form.tsx index ad9792b..7e8bf71 100644 --- a/app/epd/patients/[id]/intakes/[intakeId]/behandeladvies/components/treatment-advice-form.tsx +++ b/app/epd/patients/[id]/intakes/[intakeId]/behandeladvies/components/treatment-advice-form.tsx @@ -5,7 +5,7 @@ import { saveTreatmentAdvice } from '../../actions'; import { Loader2, Calendar, UserCircle, ClipboardList, Share2, CheckCircle2 } from 'lucide-react'; import Link from 'next/link'; import { RichTextEditor } from '@/components/rich-text-editor'; -import { SpeechRecorder } from './speech-recorder'; +import { SpeechRecorder } from '@/components/speech-recorder'; interface AdviceData { advice?: string; diff --git a/app/epd/patients/[id]/intakes/actions.ts b/app/epd/patients/[id]/intakes/actions.ts index 7c5dae7..183683b 100644 --- a/app/epd/patients/[id]/intakes/actions.ts +++ b/app/epd/patients/[id]/intakes/actions.ts @@ -9,7 +9,7 @@ import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; -import { headers, cookies } from 'next/headers'; +import { authFetch, getBaseUrl } from '@/lib/server/api-client'; import type { Intake, CreateIntakeInput, @@ -21,42 +21,6 @@ import type { * Get the base URL for API calls in server actions * Uses headers() to get the host from the request */ -function getBaseUrl(): string { - // Try environment variable first - if (process.env.NEXT_PUBLIC_APP_URL) { - return process.env.NEXT_PUBLIC_APP_URL; - } - - // Try to get from headers (works in server components/actions) - try { - const headersList = headers(); - const host = headersList.get('host'); - const protocol = headersList.get('x-forwarded-proto') || 'http'; - if (host) { - return `${protocol}://${host}`; - } - } catch { - // Headers not available, fallback to localhost - } - - // Fallback to localhost - return 'http://localhost:3000'; -} - -/** - * Get cookies as a string for fetch headers - */ -async function getCookieHeader(): Promise { - try { - const cookieStore = await cookies(); - return cookieStore - .getAll() - .map((cookie) => `${cookie.name}=${cookie.value}`) - .join('; '); - } catch { - return ''; - } -} /** * Get all intakes for a specific patient @@ -69,13 +33,8 @@ export async function getIntakesByPatientId(patientId: string): Promise { try { const baseUrl = getBaseUrl(); const url = `${baseUrl}/api/intakes/${intakeId}`; - const cookieHeader = await getCookieHeader(); - - const response = await fetch(url, { + const response = await authFetch(url, { cache: 'no-store', - headers: { - ...(cookieHeader && { Cookie: cookieHeader }), - }, }); if (response.status === 404) { @@ -163,13 +117,10 @@ export async function createIntake(input: CreateIntakeInput): Promise { try { const baseUrl = getBaseUrl(); const url = `${baseUrl}/api/intakes`; - const cookieHeader = await getCookieHeader(); - - const response = await fetch(url, { + const response = await authFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', - ...(cookieHeader && { Cookie: cookieHeader }), }, body: JSON.stringify(input), }); @@ -230,13 +181,10 @@ export async function updateIntake( try { const baseUrl = getBaseUrl(); const url = `${baseUrl}/api/intakes/${intakeId}`; - const cookieHeader = await getCookieHeader(); - - const response = await fetch(url, { + const response = await authFetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json', - ...(cookieHeader && { Cookie: cookieHeader }), }, body: JSON.stringify(input), }); @@ -332,4 +280,3 @@ export async function deleteIntake(intakeId: string, patientId: string): Promise throw error instanceof Error ? error : new Error('Failed to delete intake'); } } - diff --git a/app/epd/patients/[id]/rapportage/components/rapportage-modal.tsx b/app/epd/patients/[id]/rapportage/components/rapportage-modal.tsx deleted file mode 100644 index 92ea62a..0000000 --- a/app/epd/patients/[id]/rapportage/components/rapportage-modal.tsx +++ /dev/null @@ -1,196 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { SpeechRecorder } from '@/components/speech-recorder'; -import type { ClassificationResult } from '@/lib/types/report'; -import { createReport } from '../actions'; -import { toast } from '@/hooks/use-toast'; - -interface RapportageModalProps { - isOpen: boolean; - onClose: () => void; - patientId: string; - patientName: string; -} - -export function RapportageModal({ isOpen, onClose, patientId, patientName }: RapportageModalProps) { - const [content, setContent] = useState(''); - const [classification, setClassification] = useState(null); - const [selectedType, setSelectedType] = useState<'behandeladvies' | 'vrije_notitie'>('vrije_notitie'); - const [isAnalyzing, setIsAnalyzing] = useState(false); - const [isSaving, setIsSaving] = useState(false); - const [error, setError] = useState(null); - const router = useRouter(); - - useEffect(() => { - if (!isOpen) { - setContent(''); - setClassification(null); - setSelectedType('vrije_notitie'); - setError(null); - setIsAnalyzing(false); - setIsSaving(false); - } - }, [isOpen]); - - const handleAnalyze = async () => { - if (contentInvalid) return; - setIsAnalyzing(true); - setError(null); - try { - const response = await fetch('/api/reports/classify', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ content }), - }); - - if (!response.ok) { - throw new Error('AI-analyse mislukt'); - } - - const result: ClassificationResult = await response.json(); - setClassification(result); - setSelectedType(result.type); - } catch (err) { - setClassification(null); - setSelectedType('vrije_notitie'); - const message = err instanceof Error ? err.message : 'AI-analyse mislukt'; - setError(message); - toast({ - variant: 'destructive', - title: 'AI-analyse mislukt', - description: message, - }); - } finally { - setIsAnalyzing(false); - } - }; - - const handleSave = async () => { - setIsSaving(true); - setError(null); - try { - await createReport(patientId, { - type: selectedType, - content, - ai_confidence: classification?.confidence, - ai_reasoning: classification?.reasoning, - }); - toast({ - title: 'Rapportage opgeslagen', - description: `${patientName} heeft nu een nieuwe notitie in de tijdlijn.`, - }); - router.refresh(); - onClose(); - } catch (err) { - const message = err instanceof Error ? err.message : 'Opslaan mislukt'; - setError(message); - toast({ - variant: 'destructive', - title: 'Opslaan mislukt', - description: message, - }); - } finally { - setIsSaving(false); - } - }; - - const characterCount = content.length; - const contentInvalid = characterCount < 20 || characterCount > 5000; - - return ( - { - if (!open) { - onClose(); - } - }} - > - - - Nieuwe rapportage - - Leg een rapportage vast voor {patientName} - - - -
-
-