Mobile Responsiveness: - Viewport meta tags toegevoegd in layout.tsx - EPD header: patient details verborgen op mobiel, zoekbalk responsive - Agenda: auto-switch naar dagview op mobiel, mini-kalender verborgen - Agenda toolbar: verticale stacking, Week/Werkweek knoppen verborgen - Patiëntenlijst: compact icoon-only button op mobiel - Verpleegrapportage: verticale flow i.p.v. twee-koloms layout - Cortex: artifact overlay met slide-in animatie op mobiel - Cortex: "Terug" knop toegevoegd aan artifact panels - Toast feedback bij succesvolle afspraak creatie Code Quality Fixes (KISS/DRY): - Resize listener vervangen door bestaande useMediaQuery hook - Dubbele flex class opgeschoond in epd-header.tsx - userScalable: false verwijderd (accessibility) Type Fixes: - actions.ts: status literal type met 'as const' - agenda-block.tsx: dateRange start/end optioneel - chat-empty-state.tsx: framer-motion ease type - cortex-store.ts: ChatEntities.dateRange consistent met Zod schema 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
273 lines
7.3 KiB
TypeScript
273 lines
7.3 KiB
TypeScript
'use server';
|
|
|
|
/**
|
|
* Agenda Server Actions
|
|
*
|
|
* Server-side data fetching and mutations for the agenda module.
|
|
*/
|
|
|
|
import { createClient } from '@/lib/auth/server';
|
|
import { revalidatePath } from 'next/cache';
|
|
import type { CalendarEvent, AppointmentTypeCode, APPOINTMENT_TYPE_COLORS } from './types';
|
|
|
|
const TYPE_COLORS: Record<string, { bg: string; border: string; text: string }> = {
|
|
intake: { bg: '#dbeafe', border: '#3b82f6', text: '#1e40af' },
|
|
behandeling: { bg: '#dcfce7', border: '#22c55e', text: '#166534' },
|
|
'follow-up': { bg: '#f3e8ff', border: '#a855f7', text: '#6b21a8' },
|
|
telefonisch: { bg: '#fef3c7', border: '#f59e0b', text: '#92400e' },
|
|
huisbezoek: { bg: '#ffedd5', border: '#f97316', text: '#9a3412' },
|
|
online: { bg: '#e0e7ff', border: '#6366f1', text: '#3730a3' },
|
|
crisis: { bg: '#fee2e2', border: '#ef4444', text: '#991b1b' },
|
|
overig: { bg: '#f1f5f9', border: '#64748b', text: '#334155' },
|
|
};
|
|
|
|
interface GetEncountersParams {
|
|
startDate: string;
|
|
endDate: string;
|
|
practitionerId?: string;
|
|
}
|
|
|
|
export async function getEncounters({
|
|
startDate,
|
|
endDate,
|
|
practitionerId,
|
|
}: GetEncountersParams): Promise<CalendarEvent[]> {
|
|
const supabase = await createClient();
|
|
|
|
let query = supabase
|
|
.from('encounters')
|
|
.select(`
|
|
*,
|
|
patients:patient_id (
|
|
id,
|
|
name_family,
|
|
name_given,
|
|
birth_date
|
|
)
|
|
`)
|
|
.gte('period_start', startDate)
|
|
.lte('period_start', endDate)
|
|
.neq('status', 'cancelled')
|
|
.order('period_start', { ascending: true });
|
|
|
|
if (practitionerId) {
|
|
query = query.eq('practitioner_id', practitionerId);
|
|
}
|
|
|
|
const { data, error } = await query;
|
|
|
|
if (error) {
|
|
console.error('Error fetching encounters:', error);
|
|
return [];
|
|
}
|
|
|
|
return (data || []).map((encounter) => {
|
|
const patient = encounter.patients as {
|
|
id: string;
|
|
name_family: string;
|
|
name_given: string[];
|
|
birth_date: string;
|
|
} | null;
|
|
|
|
const patientName = patient
|
|
? `${patient.name_given?.[0] || ''} ${patient.name_family || ''}`.trim() || 'Onbekende patiënt'
|
|
: 'Onbekende patiënt';
|
|
|
|
const typeCode = encounter.type_code as AppointmentTypeCode;
|
|
const colors = TYPE_COLORS[typeCode] || TYPE_COLORS.overig;
|
|
|
|
return {
|
|
id: encounter.id,
|
|
title: patientName,
|
|
start: encounter.period_start,
|
|
end: encounter.period_end || undefined,
|
|
backgroundColor: colors.bg,
|
|
borderColor: colors.border,
|
|
textColor: colors.text,
|
|
extendedProps: {
|
|
encounter,
|
|
patient: patient || undefined,
|
|
},
|
|
} as CalendarEvent;
|
|
});
|
|
}
|
|
|
|
interface CreateEncounterParams {
|
|
patientId: string;
|
|
practitionerId?: string;
|
|
periodStart: string;
|
|
periodEnd?: string;
|
|
typeCode: string;
|
|
typeDisplay: string;
|
|
classCode: string;
|
|
classDisplay: string;
|
|
notes?: string;
|
|
}
|
|
|
|
export async function createEncounter(params: CreateEncounterParams) {
|
|
const supabase = await createClient();
|
|
|
|
console.log('[createEncounter] Starting with params:', {
|
|
patientId: params.patientId,
|
|
periodStart: params.periodStart,
|
|
periodEnd: params.periodEnd,
|
|
typeCode: params.typeCode,
|
|
});
|
|
|
|
const insertData = {
|
|
patient_id: params.patientId,
|
|
practitioner_id: params.practitionerId || null,
|
|
period_start: params.periodStart,
|
|
period_end: params.periodEnd,
|
|
type_code: params.typeCode,
|
|
type_display: params.typeDisplay,
|
|
class_code: params.classCode,
|
|
class_display: params.classDisplay,
|
|
notes: params.notes,
|
|
status: 'planned' as const,
|
|
};
|
|
|
|
console.log('[createEncounter] Insert data:', insertData);
|
|
|
|
const { data, error } = await supabase
|
|
.from('encounters')
|
|
.insert(insertData)
|
|
.select()
|
|
.single();
|
|
|
|
console.log('[createEncounter] Result:', { data, error });
|
|
|
|
if (error) {
|
|
console.error('[createEncounter] Error:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
|
|
if (!data) {
|
|
console.error('[createEncounter] No data returned after insert');
|
|
return { success: false, error: 'Geen data teruggegeven na insert' };
|
|
}
|
|
|
|
console.log('[createEncounter] Success! Created encounter:', data.id);
|
|
revalidatePath('/epd/agenda');
|
|
return { success: true, data };
|
|
}
|
|
|
|
export async function updateEncounter(
|
|
encounterId: string,
|
|
updates: {
|
|
periodStart?: string;
|
|
periodEnd?: string | null;
|
|
status?: string;
|
|
notes?: string;
|
|
typeCode?: string;
|
|
typeDisplay?: string;
|
|
classCode?: string;
|
|
classDisplay?: string;
|
|
}
|
|
) {
|
|
const supabase = await createClient();
|
|
|
|
const updateData: Record<string, unknown> = {};
|
|
if (updates.periodStart) updateData.period_start = updates.periodStart;
|
|
if (updates.periodEnd !== undefined) updateData.period_end = updates.periodEnd;
|
|
if (updates.status) updateData.status = updates.status;
|
|
if (updates.notes !== undefined) updateData.notes = updates.notes;
|
|
if (updates.typeCode) updateData.type_code = updates.typeCode;
|
|
if (updates.typeDisplay) updateData.type_display = updates.typeDisplay;
|
|
if (updates.classCode) updateData.class_code = updates.classCode;
|
|
if (updates.classDisplay) updateData.class_display = updates.classDisplay;
|
|
|
|
const { data, error } = await supabase
|
|
.from('encounters')
|
|
.update(updateData)
|
|
.eq('id', encounterId)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) {
|
|
console.error('Error updating encounter:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
|
|
revalidatePath('/epd/agenda');
|
|
return { success: true, data };
|
|
}
|
|
|
|
export async function cancelEncounter(encounterId: string) {
|
|
return updateEncounter(encounterId, { status: 'cancelled' });
|
|
}
|
|
|
|
export async function rescheduleEncounter(
|
|
encounterId: string,
|
|
newStart: string,
|
|
newEnd: string | null
|
|
) {
|
|
return updateEncounter(encounterId, {
|
|
periodStart: newStart,
|
|
periodEnd: newEnd || undefined,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get encounters for a specific patient (for linking reports to appointments)
|
|
*/
|
|
export async function getPatientEncounters(patientId: string) {
|
|
const supabase = await createClient();
|
|
|
|
const { data, error } = await supabase
|
|
.from('encounters')
|
|
.select('id, period_start, period_end, type_code, type_display, status, notes')
|
|
.eq('patient_id', patientId)
|
|
.neq('status', 'cancelled')
|
|
.order('period_start', { ascending: false })
|
|
.limit(50);
|
|
|
|
if (error) {
|
|
console.error('Error fetching patient encounters:', error);
|
|
return [];
|
|
}
|
|
|
|
return data || [];
|
|
}
|
|
|
|
/**
|
|
* Get reports linked to a specific encounter
|
|
*/
|
|
export async function getEncounterReports(encounterId: string) {
|
|
const supabase = await createClient();
|
|
|
|
const { data, error } = await supabase
|
|
.from('reports')
|
|
.select('id, type, content, created_at')
|
|
.eq('encounter_id', encounterId)
|
|
.is('deleted_at', null)
|
|
.order('created_at', { ascending: false });
|
|
|
|
if (error) {
|
|
console.error('Error fetching encounter reports:', error);
|
|
return [];
|
|
}
|
|
|
|
return data || [];
|
|
}
|
|
|
|
/**
|
|
* Get a single encounter by ID (for navigation from report to appointment)
|
|
*/
|
|
export async function getEncounterById(encounterId: string) {
|
|
const supabase = await createClient();
|
|
|
|
const { data, error } = await supabase
|
|
.from('encounters')
|
|
.select('id, period_start, period_end, type_code, type_display, status')
|
|
.eq('id', encounterId)
|
|
.single();
|
|
|
|
if (error) {
|
|
console.error('Error fetching encounter:', error);
|
|
return null;
|
|
}
|
|
|
|
return data;
|
|
}
|