Files
triqura-ecd/app/epd/patients/[id]/intakes/actions.ts
colinislit a789bebe96 feat: rapportage API, speech recorder refactor, docs reorganisatie
Multiple features and improvements:

Reports/Rapportage API:
- Created REST API endpoints: /api/reports (GET/POST), /api/reports/[id] (GET/PATCH/DELETE)
- Added /api/reports/classify endpoint for AI classification
- Supabase migrations: reports table + RLS policies
- Server utilities: api-client.ts for DRY fetch logic
- Type definitions: lib/types/report.ts with Zod schemas
- Removed old rapportage-modal component (replaced by split-view)

Speech Recorder Refactor:
- Moved speech-recorder from intake-specific to shared components/
- Updated treatment-advice-form to use new location
- Updated intake actions for speech functionality

UI Components (shadcn):
- Added dialog, dropdown-menu, toast, toaster components
- Added use-toast hook for toast notifications

Documentation:
- Reorganized docs/release/ → docs/reports/ for better structure
- Archived old specs to docs/specs/archive/
- Added screening-system.mdx documentation
- Added rapportage-split-view-design.md
- Added UI screenshots for troubleshooting

Dependencies:
- Updated package.json and pnpm-lock.yaml
- Regenerated database.types.ts from Supabase

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 14:14:06 +01:00

283 lines
8.6 KiB
TypeScript

'use server';
/**
* Intake Server Actions (API-based)
*
* Server-side actions that interact with Intake API endpoints
* Refactored from direct Supabase queries to use Custom API
*/
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { authFetch, getBaseUrl } from '@/lib/server/api-client';
import type {
Intake,
CreateIntakeInput,
UpdateIntakeInput,
IntakeListResponse,
} from '@/lib/types/intake';
/**
* Get the base URL for API calls in server actions
* Uses headers() to get the host from the request
*/
/**
* Get all intakes for a specific patient
* @param patientId - UUID of the patient
* @returns Array of intakes for the patient
*/
export async function getIntakesByPatientId(patientId: string): Promise<Intake[]> {
try {
const baseUrl = getBaseUrl();
const url = new URL('/api/intakes', baseUrl);
url.searchParams.set('patientId', patientId);
const response = await authFetch(url.toString(), {
cache: 'no-store',
});
if (!response.ok) {
const errorText = await response.text();
let errorData: any = {};
try {
errorData = JSON.parse(errorText);
} catch {
// If not JSON, check if it's HTML (redirect)
if (errorText.trim().startsWith('<!DOCTYPE') || errorText.trim().startsWith('<html')) {
throw new Error('Niet geautoriseerd. Log opnieuw in.');
}
}
console.error('Error fetching intakes:', response.statusText, errorData);
throw new Error(`Failed to fetch intakes: ${errorData.error || response.statusText}`);
}
const data: IntakeListResponse = await response.json();
return data.intakes;
} catch (error) {
console.error('Error in getIntakesByPatientId:', error);
throw error instanceof Error ? error : new Error('Failed to fetch intakes');
}
}
/**
* Get a specific intake by ID
* @param intakeId - UUID of the intake
* @returns Intake object or null if not found
*/
export async function getIntakeById(intakeId: string): Promise<Intake | null> {
try {
const baseUrl = getBaseUrl();
const url = `${baseUrl}/api/intakes/${intakeId}`;
const response = await authFetch(url, {
cache: 'no-store',
});
if (response.status === 404) {
return null;
}
if (!response.ok) {
const errorText = await response.text();
let errorData: any = {};
try {
errorData = JSON.parse(errorText);
} catch {
// If not JSON, check if it's HTML (redirect)
if (errorText.trim().startsWith('<!DOCTYPE') || errorText.trim().startsWith('<html')) {
throw new Error('Niet geautoriseerd. Log opnieuw in.');
}
}
console.error('Error fetching intake:', response.statusText, errorData);
throw new Error(`Failed to fetch intake: ${errorData.error || response.statusText}`);
}
const data: Intake = await response.json();
return data;
} catch (error) {
console.error('Error in getIntakeById:', error);
// Return null instead of throwing for not found cases
if (error instanceof Error && error.message.includes('404')) {
return null;
}
throw error instanceof Error ? error : new Error('Failed to fetch intake');
}
}
/**
* Create a new intake
* @param input - Intake creation data
* @returns Created intake object
*/
export async function createIntake(input: CreateIntakeInput): Promise<Intake> {
try {
const baseUrl = getBaseUrl();
const url = `${baseUrl}/api/intakes`;
const response = await authFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
const errorText = await response.text();
let errorData: any = {};
try {
errorData = JSON.parse(errorText);
} catch {
// If not JSON, check if it's HTML (redirect)
if (errorText.trim().startsWith('<!DOCTYPE') || errorText.trim().startsWith('<html')) {
throw new Error('Niet geautoriseerd. Log opnieuw in.');
}
}
console.error('Error creating intake:', response.statusText, errorData);
// Handle validation errors
if (response.status === 400 && errorData.details) {
const validationErrors = errorData.details
.map((d: { field: string; message: string }) => `${d.field}: ${d.message}`)
.join(', ');
throw new Error(`Validatiefout: ${validationErrors}`);
}
throw new Error(`Failed to create intake: ${errorData.error || response.statusText}`);
}
const data: Intake = await response.json();
// Revalidate paths
revalidatePath(`/epd/patients/${input.patient_id}/intakes`);
revalidatePath(`/epd/patients/${input.patient_id}`);
// Redirect to intakes list
redirect(`/epd/patients/${input.patient_id}/intakes`);
// This will never be reached due to redirect, but TypeScript needs it
return data;
} catch (error) {
console.error('Error in createIntake:', error);
throw error instanceof Error ? error : new Error('Failed to create intake');
}
}
/**
* Update an existing intake
* @param intakeId - UUID of the intake to update
* @param input - Partial intake data to update
* @returns Updated intake object
*/
export async function updateIntake(
intakeId: string,
input: UpdateIntakeInput
): Promise<Intake> {
try {
const baseUrl = getBaseUrl();
const url = `${baseUrl}/api/intakes/${intakeId}`;
const response = await authFetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
const errorText = await response.text();
let errorData: any = {};
try {
errorData = JSON.parse(errorText);
} catch {
// If not JSON, check if it's HTML (redirect)
if (errorText.trim().startsWith('<!DOCTYPE') || errorText.trim().startsWith('<html')) {
throw new Error('Niet geautoriseerd. Log opnieuw in.');
}
}
console.error('Error updating intake:', response.statusText, errorData);
if (response.status === 404) {
throw new Error('Intake niet gevonden');
}
if (response.status === 400 && errorData.details) {
const validationErrors = errorData.details
.map((d: { field: string; message: string }) => `${d.field}: ${d.message}`)
.join(', ');
throw new Error(`Validatiefout: ${validationErrors}`);
}
throw new Error(`Failed to update intake: ${errorData.error || response.statusText}`);
}
const data: Intake = await response.json();
// Revalidate paths (we need to get patient_id from the intake)
revalidatePath(`/epd/patients/${data.patient_id}/intakes`);
revalidatePath(`/epd/patients/${data.patient_id}/intakes/${intakeId}`);
revalidatePath(`/epd/patients/${data.patient_id}`);
return data;
} catch (error) {
console.error('Error in updateIntake:', error);
throw error instanceof Error ? error : new Error('Failed to update intake');
}
}
/**
* Delete an intake
* @param intakeId - UUID of the intake to delete
* @param patientId - UUID of the patient (for revalidation)
*/
export async function deleteIntake(intakeId: string, patientId: string): Promise<void> {
try {
const baseUrl = getBaseUrl();
const url = `${baseUrl}/api/intakes/${intakeId}`;
const cookieHeader = await getCookieHeader();
const response = await fetch(url, {
method: 'DELETE',
headers: {
...(cookieHeader && { Cookie: cookieHeader }),
},
});
if (!response.ok && response.status !== 204) {
const errorText = await response.text();
let errorData: any = {};
try {
errorData = JSON.parse(errorText);
} catch {
// If not JSON, check if it's HTML (redirect)
if (errorText.trim().startsWith('<!DOCTYPE') || errorText.trim().startsWith('<html')) {
throw new Error('Niet geautoriseerd. Log opnieuw in.');
}
}
console.error('Error deleting intake:', response.statusText, errorData);
if (response.status === 404) {
throw new Error('Intake niet gevonden');
}
throw new Error(`Failed to delete intake: ${errorData.error || response.statusText}`);
}
// Revalidate paths
revalidatePath(`/epd/patients/${patientId}/intakes`);
revalidatePath(`/epd/patients/${patientId}`);
} catch (error) {
console.error('Error in deleteIntake:', error);
throw error instanceof Error ? error : new Error('Failed to delete intake');
}
}