Fixed multiple TypeScript compilation errors that blocked production build:
Component Type Fixes:
- client-sidebar.tsx: Changed icon type from React.ElementType to
React.ComponentType<{ className?: string }> for proper prop typing
- patient-form.tsx: Added type casting for FHIR extension property access
- page.tsx: Added explicit Intake[] type annotation
- document-card.tsx: Added null check for file_size property
- rich-text-editor.tsx: Removed invalid false parameter from setContent()
Data Model Fixes:
- actions.ts (intakes): Changed encounter status 'finished' to 'completed'
(FHIR-compliant value)
- actions.ts (intakes): Set diagnosis clinical_status to always use 'active'
- actions.ts (intakes): Cast treatment_advice to Record<string, any>
Schema Extensions:
- lib/fhir/types/index.ts: Added extension property to FHIRPatient interface
to support custom FHIR extensions (john-doe, insurance, GP, episode-status)
Validation Fixes:
- lib/types/intake.ts: Fixed Zod enum errorMap syntax (changed to message)
All changes ensure type safety while maintaining runtime functionality.
Build now completes successfully with zero TypeScript errors.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
198 lines
6.7 KiB
TypeScript
198 lines
6.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useTransition } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import type { ScreeningDocument } from '@/lib/types/screening';
|
|
import { Loader2, Trash2, UploadCloud } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
const DOCUMENT_BUCKET = 'screening-documents';
|
|
const BASE_STORAGE_URL = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/${DOCUMENT_BUCKET}`;
|
|
|
|
const documentTypeOptions = [
|
|
'verwijsbrief',
|
|
'verhuisbericht',
|
|
'indicatie',
|
|
'overig',
|
|
];
|
|
|
|
interface DocumentCardProps {
|
|
patientId: string;
|
|
screeningId: string;
|
|
documents: ScreeningDocument[];
|
|
}
|
|
|
|
export function DocumentCard({ patientId, screeningId, documents }: DocumentCardProps) {
|
|
const router = useRouter();
|
|
const [file, setFile] = useState<File | null>(null);
|
|
const [documentType, setDocumentType] = useState('verwijsbrief');
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isUploading, startUpload] = useTransition();
|
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
|
|
const handleUpload = () => {
|
|
if (!file) {
|
|
setError('Selecteer eerst een bestand.');
|
|
return;
|
|
}
|
|
|
|
setError(null);
|
|
startUpload(async () => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('documentType', documentType);
|
|
|
|
try {
|
|
const response = await fetch(`/api/screenings/${screeningId}/documents`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
throw new Error(data.error || 'Upload mislukt');
|
|
}
|
|
|
|
setFile(null);
|
|
router.refresh();
|
|
} catch (err) {
|
|
console.error(err);
|
|
setError(err instanceof Error ? err.message : 'Upload mislukt');
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleDelete = async (documentId: string) => {
|
|
setDeletingId(documentId);
|
|
try {
|
|
const response = await fetch(
|
|
`/api/screenings/${screeningId}/documents/${documentId}`,
|
|
{
|
|
method: 'DELETE',
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
throw new Error(data.error || 'Verwijderen mislukt');
|
|
}
|
|
|
|
router.refresh();
|
|
} catch (err) {
|
|
console.error(err);
|
|
setError(err instanceof Error ? err.message : 'Verwijderen mislukt');
|
|
} finally {
|
|
setDeletingId(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<section className="bg-white rounded-lg border border-slate-200 p-6 shadow-sm">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="p-2 rounded-md bg-slate-100 text-slate-600">
|
|
<UploadCloud className="h-5 w-5" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-slate-900">Documenten</h3>
|
|
<p className="text-sm text-slate-500">
|
|
Upload verwijsbrieven, indicaties en andere stukken.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="border border-dashed border-slate-300 rounded-lg p-4">
|
|
<div className="flex flex-col gap-3">
|
|
<input
|
|
type="file"
|
|
onChange={(event) => {
|
|
const selected = event.target.files?.[0] ?? null;
|
|
setFile(selected);
|
|
setError(null);
|
|
}}
|
|
/>
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
|
|
<select
|
|
value={documentType}
|
|
onChange={(event) => setDocumentType(event.target.value)}
|
|
className="h-10 rounded-md border border-slate-300 px-3 text-sm"
|
|
>
|
|
{documentTypeOptions.map((option) => (
|
|
<option key={option} value={option}>
|
|
{option.charAt(0).toUpperCase() + option.slice(1)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<button
|
|
type="button"
|
|
onClick={handleUpload}
|
|
disabled={isUploading}
|
|
className={cn(
|
|
'inline-flex items-center justify-center gap-2 rounded-md bg-teal-600 px-4 py-2 text-sm font-medium text-white hover:bg-teal-700 disabled:opacity-60'
|
|
)}
|
|
>
|
|
{isUploading && <Loader2 className="h-4 w-4 animate-spin" />}
|
|
Upload document
|
|
</button>
|
|
</div>
|
|
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
{file && (
|
|
<p className="text-xs text-slate-500">
|
|
Geselecteerd: {file.name} • {(file.size / 1024).toFixed(1)} KB
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{documents.length === 0 && (
|
|
<p className="text-sm text-slate-500">
|
|
Nog geen documenten geregistreerd.
|
|
</p>
|
|
)}
|
|
{documents.map((doc) => {
|
|
const publicUrl = `${BASE_STORAGE_URL}/${doc.file_path}`;
|
|
return (
|
|
<div
|
|
key={doc.id}
|
|
className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border border-slate-200 rounded-lg p-3"
|
|
>
|
|
<div>
|
|
<p className="text-sm font-medium text-slate-900">{doc.file_name}</p>
|
|
<p className="text-xs text-slate-500">
|
|
{doc.document_type} • {doc.file_size ? `${(doc.file_size / 1024).toFixed(1)} KB` : 'Onbekend'} •{' '}
|
|
{doc.uploaded_by_name || 'Onbekend'}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<a
|
|
href={publicUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-sm text-teal-600 hover:text-teal-700"
|
|
>
|
|
Download
|
|
</a>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleDelete(doc.id)}
|
|
disabled={deletingId === doc.id}
|
|
className="inline-flex items-center gap-1 rounded-md border border-red-200 px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 disabled:opacity-60"
|
|
>
|
|
{deletingId === doc.id ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Trash2 className="h-4 w-4" />
|
|
)}
|
|
Verwijderen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|