- Add architecture overview, implementation plan, and intent overview - Add intake intent process specs (gap analyse, bouwplan, testplan) - Add swift architecture specs and visualization prompts - Remove obsolete aispeedrun-manifesto template Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
895 lines
27 KiB
Markdown
895 lines
27 KiB
Markdown
# Cortex Block Template & Patterns
|
|
|
|
**Datum:** 03-02-2026
|
|
**Status:** Ontwerp
|
|
**Doel:** Herbruikbaar patroon voor alle intake-gerelateerde Cortex blocks
|
|
|
|
---
|
|
|
|
## 1. Anatomie van een Cortex Block
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ [Icon] Titel [Close] │ ← Header (via BlockContainer)
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌─────────────────────────────────────────────────────┐ │
|
|
│ │ Loading State │ │ ← State: Loading
|
|
│ │ [Spinner] Data laden... │ │
|
|
│ └─────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌─────────────────────────────────────────────────────┐ │
|
|
│ │ Error State │ │ ← State: Error
|
|
│ │ [!] Foutmelding + [Retry] │ │
|
|
│ └─────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌─────────────────────────────────────────────────────┐ │
|
|
│ │ Empty State │ │ ← State: Empty
|
|
│ │ [Icon] Geen data + [Actie] │ │
|
|
│ └─────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌─────────────────────────────────────────────────────┐ │
|
|
│ │ Data State │ │ ← State: Data
|
|
│ │ │ │
|
|
│ │ Section 1: [Icon] Label │ │
|
|
│ │ ├── Item 1 │ │
|
|
│ │ └── Item 2 │ │
|
|
│ │ │ │
|
|
│ │ Section 2: [Icon] Label │ │
|
|
│ │ └── Content │ │
|
|
│ │ │ │
|
|
│ └─────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ───────────────────────────────────────────────────────── │
|
|
│ [Secondary Action] [Primary Action] │ ← Footer Actions
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Block Types
|
|
|
|
### 2.1 Query Block (Read-only)
|
|
|
|
**Doel:** Data tonen, navigeren naar EPD voor bewerken
|
|
|
|
```typescript
|
|
// Voorbeeld: RisicoQueryBlock
|
|
// Input: "Wat zijn de risico's?"
|
|
// Output: Lijst risico's met levels
|
|
|
|
interface QueryBlockPattern {
|
|
// Data ophalen
|
|
fetch: () => Promise<Data>;
|
|
|
|
// Tonen
|
|
render: (data: Data) => JSX.Element;
|
|
|
|
// Navigatie
|
|
onViewInEPD: () => void; // "Bekijk in dossier"
|
|
}
|
|
```
|
|
|
|
### 2.2 Action Block (Create/Update)
|
|
|
|
**Doel:** Snelle invoer, bevestiging, opslaan
|
|
|
|
```typescript
|
|
// Voorbeeld: RisicoToevoegenBlock
|
|
// Input: "Risico suïcidaliteit matig"
|
|
// Output: Prefilled form, confirm, save
|
|
|
|
interface ActionBlockPattern {
|
|
// Prefill van intent entities
|
|
prefill: ExtractedEntities;
|
|
|
|
// Form state
|
|
form: FormState;
|
|
|
|
// Opslaan
|
|
onSubmit: () => Promise<void>;
|
|
|
|
// Bevestiging
|
|
confirmationRequired: boolean;
|
|
}
|
|
```
|
|
|
|
### 2.3 Status Block (Progress/Checklist)
|
|
|
|
**Doel:** Voortgang tonen, navigeren naar onvolledige items
|
|
|
|
```typescript
|
|
// Voorbeeld: IntakeStatusBlock
|
|
// Input: "Wat moet ik nog doen?"
|
|
// Output: Checklist met links
|
|
|
|
interface StatusBlockPattern {
|
|
// Status per onderdeel
|
|
status: Record<string, boolean>;
|
|
|
|
// Navigatie naar onderdeel
|
|
onNavigate: (section: string) => void;
|
|
}
|
|
```
|
|
|
|
### 2.4 Navigation Block (Route)
|
|
|
|
**Doel:** Direct navigeren naar EPD pagina
|
|
|
|
```typescript
|
|
// Voorbeeld: IntakeNavigatieBlock
|
|
// Input: "Ga naar diagnose"
|
|
// Output: Router.push() of openArtifact()
|
|
|
|
interface NavigationBlockPattern {
|
|
// Doel bepalen
|
|
destination: string;
|
|
|
|
// Navigeren
|
|
onNavigate: () => void;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Shared Components
|
|
|
|
### 3.1 Block States
|
|
|
|
```typescript
|
|
// components/cortex/shared/block-states.tsx
|
|
|
|
interface BlockLoadingProps {
|
|
message?: string;
|
|
}
|
|
|
|
export function BlockLoading({ message = 'Laden...' }: BlockLoadingProps) {
|
|
return (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="h-6 w-6 animate-spin text-slate-400 mr-2" />
|
|
<span className="text-sm text-slate-500">{message}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface BlockErrorProps {
|
|
message: string;
|
|
onRetry?: () => void;
|
|
}
|
|
|
|
export function BlockError({ message, onRetry }: BlockErrorProps) {
|
|
return (
|
|
<div className="text-center py-8">
|
|
<AlertCircle className="h-8 w-8 text-red-500 mx-auto mb-2" />
|
|
<p className="text-sm text-red-700 mb-3">{message}</p>
|
|
{onRetry && (
|
|
<Button variant="outline" size="sm" onClick={onRetry}>
|
|
<RefreshCw className="h-4 w-4 mr-1.5" />
|
|
Opnieuw proberen
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface BlockEmptyProps {
|
|
icon: LucideIcon;
|
|
message: string;
|
|
action?: {
|
|
label: string;
|
|
onClick: () => void;
|
|
};
|
|
}
|
|
|
|
export function BlockEmpty({ icon: Icon, message, action }: BlockEmptyProps) {
|
|
return (
|
|
<div className="text-center py-8 text-slate-400">
|
|
<Icon className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
|
<p className="text-sm">{message}</p>
|
|
{action && (
|
|
<Button variant="outline" size="sm" onClick={action.onClick} className="mt-3">
|
|
{action.label}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 3.2 Block Section
|
|
|
|
```typescript
|
|
// components/cortex/shared/block-section.tsx
|
|
|
|
interface BlockSectionProps {
|
|
icon: LucideIcon;
|
|
iconColor?: string;
|
|
title: string;
|
|
count?: number;
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function BlockSection({
|
|
icon: Icon,
|
|
iconColor = 'text-slate-600',
|
|
title,
|
|
count,
|
|
children
|
|
}: BlockSectionProps) {
|
|
return (
|
|
<section className="bg-white rounded-lg border border-slate-200 p-4">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Icon className={cn('h-4 w-4', iconColor)} />
|
|
<h3 className="text-sm font-medium text-slate-700">{title}</h3>
|
|
{count !== undefined && (
|
|
<span className="text-xs text-slate-500">({count})</span>
|
|
)}
|
|
</div>
|
|
{children}
|
|
</section>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 3.3 Block Item (List Row)
|
|
|
|
```typescript
|
|
// components/cortex/shared/block-item.tsx
|
|
|
|
interface BlockItemProps {
|
|
title: string;
|
|
subtitle?: string;
|
|
badge?: {
|
|
label: string;
|
|
variant: 'default' | 'success' | 'warning' | 'danger';
|
|
};
|
|
onClick?: () => void;
|
|
}
|
|
|
|
const BADGE_STYLES = {
|
|
default: 'bg-slate-100 text-slate-700',
|
|
success: 'bg-green-50 text-green-700',
|
|
warning: 'bg-amber-50 text-amber-700',
|
|
danger: 'bg-red-50 text-red-700',
|
|
};
|
|
|
|
export function BlockItem({ title, subtitle, badge, onClick }: BlockItemProps) {
|
|
const Wrapper = onClick ? 'button' : 'div';
|
|
|
|
return (
|
|
<Wrapper
|
|
onClick={onClick}
|
|
className={cn(
|
|
'flex items-center justify-between p-3 rounded-lg bg-slate-50 border border-slate-200 w-full text-left',
|
|
onClick && 'hover:bg-slate-100 cursor-pointer transition-colors'
|
|
)}
|
|
>
|
|
<div>
|
|
<p className="text-sm font-medium text-slate-900">{title}</p>
|
|
{subtitle && (
|
|
<p className="text-xs text-slate-500 mt-0.5">{subtitle}</p>
|
|
)}
|
|
</div>
|
|
{badge && (
|
|
<span className={cn(
|
|
'px-2 py-0.5 rounded-full text-xs font-medium',
|
|
BADGE_STYLES[badge.variant]
|
|
)}>
|
|
{badge.label}
|
|
</span>
|
|
)}
|
|
</Wrapper>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 3.4 Block Footer
|
|
|
|
```typescript
|
|
// components/cortex/shared/block-footer.tsx
|
|
|
|
interface BlockFooterProps {
|
|
secondaryAction?: {
|
|
label: string;
|
|
icon?: LucideIcon;
|
|
onClick: () => void;
|
|
};
|
|
primaryAction?: {
|
|
label: string;
|
|
icon?: LucideIcon;
|
|
onClick: () => void;
|
|
loading?: boolean;
|
|
};
|
|
}
|
|
|
|
export function BlockFooter({ secondaryAction, primaryAction }: BlockFooterProps) {
|
|
return (
|
|
<div className="flex items-center justify-between pt-4 border-t border-slate-200 mt-4">
|
|
{secondaryAction ? (
|
|
<Button variant="ghost" size="sm" onClick={secondaryAction.onClick}>
|
|
{secondaryAction.icon && <secondaryAction.icon className="h-4 w-4 mr-1.5" />}
|
|
{secondaryAction.label}
|
|
</Button>
|
|
) : <div />}
|
|
|
|
{primaryAction && (
|
|
<Button
|
|
size="sm"
|
|
onClick={primaryAction.onClick}
|
|
disabled={primaryAction.loading}
|
|
>
|
|
{primaryAction.loading ? (
|
|
<Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
|
|
) : primaryAction.icon ? (
|
|
<primaryAction.icon className="h-4 w-4 mr-1.5" />
|
|
) : null}
|
|
{primaryAction.label}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Custom Hook Pattern
|
|
|
|
### 4.1 useBlockData (Generic Data Fetching)
|
|
|
|
```typescript
|
|
// lib/cortex/hooks/use-block-data.ts
|
|
|
|
interface UseBlockDataOptions<T> {
|
|
endpoint: string;
|
|
params?: Record<string, string>;
|
|
enabled?: boolean;
|
|
onError?: (error: Error) => void;
|
|
}
|
|
|
|
interface UseBlockDataResult<T> {
|
|
data: T | null;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
refetch: () => Promise<void>;
|
|
}
|
|
|
|
export function useBlockData<T>({
|
|
endpoint,
|
|
params,
|
|
enabled = true,
|
|
onError,
|
|
}: UseBlockDataOptions<T>): UseBlockDataResult<T> {
|
|
const [data, setData] = useState<T | null>(null);
|
|
const [isLoading, setIsLoading] = useState(enabled);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const { toast } = useToast();
|
|
|
|
const fetchData = useCallback(async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const url = new URL(endpoint, window.location.origin);
|
|
if (params) {
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
url.searchParams.set(key, value);
|
|
});
|
|
}
|
|
|
|
const response = await safeFetch(url.toString());
|
|
const result = await response.json();
|
|
setData(result);
|
|
} catch (err) {
|
|
const errorInfo = getErrorInfo(err);
|
|
setError(errorInfo.description);
|
|
onError?.(err as Error);
|
|
toast({
|
|
variant: 'destructive',
|
|
title: errorInfo.title,
|
|
description: errorInfo.description,
|
|
});
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [endpoint, params, onError, toast]);
|
|
|
|
useEffect(() => {
|
|
if (enabled) {
|
|
fetchData();
|
|
}
|
|
}, [enabled, fetchData]);
|
|
|
|
return { data, isLoading, error, refetch: fetchData };
|
|
}
|
|
```
|
|
|
|
### 4.2 useIntakeContext (Intake-specific)
|
|
|
|
```typescript
|
|
// lib/cortex/hooks/use-intake-context.ts
|
|
|
|
interface UseIntakeContextResult {
|
|
patientId: string | null;
|
|
intakeId: string | null;
|
|
patientName: string | null;
|
|
hasContext: boolean;
|
|
}
|
|
|
|
export function useIntakeContext(prefill?: BlockPrefillData): UseIntakeContextResult {
|
|
const { activePatient } = useCortexStore();
|
|
|
|
// TODO: Add activeIntake to cortex-store
|
|
// For now, we need intakeId from prefill or URL
|
|
|
|
const patientId = prefill?.patientId || activePatient?.id || null;
|
|
const patientName = prefill?.patientName ||
|
|
(activePatient ? formatPatientName(activePatient) : null);
|
|
const intakeId = prefill?.intakeId || null;
|
|
|
|
return {
|
|
patientId,
|
|
intakeId,
|
|
patientName,
|
|
hasContext: Boolean(patientId),
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Template: Query Block
|
|
|
|
```typescript
|
|
// components/cortex/blocks/[name]-query-block.tsx
|
|
|
|
'use client';
|
|
|
|
/**
|
|
* [Name] Query Block
|
|
*
|
|
* Block voor het tonen van [beschrijving].
|
|
* Intent: [intent_name]
|
|
*/
|
|
|
|
import { useEffect } from 'react';
|
|
import { useCortexStore } from '@/stores/cortex-store';
|
|
import { useToast } from '@/hooks/use-toast';
|
|
import { BlockContainer } from './block-container';
|
|
import { BlockLoading, BlockError, BlockEmpty } from '../shared/block-states';
|
|
import { BlockSection } from '../shared/block-section';
|
|
import { BlockItem } from '../shared/block-item';
|
|
import { BlockFooter } from '../shared/block-footer';
|
|
import { useBlockData } from '@/lib/cortex/hooks/use-block-data';
|
|
import { useIntakeContext } from '@/lib/cortex/hooks/use-intake-context';
|
|
import type { BlockPrefillData } from '@/stores/cortex-store';
|
|
import { BLOCK_CONFIGS } from '@/lib/cortex/types';
|
|
import { [Icon], ExternalLink } from 'lucide-react';
|
|
|
|
// ============================================================================
|
|
// Types
|
|
// ============================================================================
|
|
|
|
interface [Name]QueryBlockProps {
|
|
prefill?: BlockPrefillData;
|
|
}
|
|
|
|
interface [Name]Data {
|
|
items: Array<{
|
|
id: string;
|
|
// ... fields
|
|
}>;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Component
|
|
// ============================================================================
|
|
|
|
export function [Name]QueryBlock({ prefill }: [Name]QueryBlockProps) {
|
|
const config = BLOCK_CONFIGS['[block-type]'];
|
|
const { closeBlock } = useCortexStore();
|
|
const { patientId, intakeId, patientName, hasContext } = useIntakeContext(prefill);
|
|
|
|
// Fetch data
|
|
const { data, isLoading, error, refetch } = useBlockData<[Name]Data>({
|
|
endpoint: `/api/cortex/intake/${intakeId}/[endpoint]`,
|
|
enabled: Boolean(intakeId),
|
|
});
|
|
|
|
// Handle no context
|
|
if (!hasContext) {
|
|
return (
|
|
<BlockContainer title={config.title} size={config.size}>
|
|
<BlockEmpty
|
|
icon={[Icon]}
|
|
message="Selecteer eerst een patiënt"
|
|
/>
|
|
</BlockContainer>
|
|
);
|
|
}
|
|
|
|
// Handle loading
|
|
if (isLoading) {
|
|
return (
|
|
<BlockContainer title={config.title} size={config.size}>
|
|
<BlockLoading message="[Name] laden..." />
|
|
</BlockContainer>
|
|
);
|
|
}
|
|
|
|
// Handle error
|
|
if (error) {
|
|
return (
|
|
<BlockContainer title={config.title} size={config.size}>
|
|
<BlockError message={error} onRetry={refetch} />
|
|
</BlockContainer>
|
|
);
|
|
}
|
|
|
|
// Handle empty
|
|
if (!data?.items?.length) {
|
|
return (
|
|
<BlockContainer title={config.title} size={config.size}>
|
|
<BlockEmpty
|
|
icon={[Icon]}
|
|
message="Geen [items] gevonden"
|
|
action={{
|
|
label: 'Toevoegen in dossier',
|
|
onClick: () => {
|
|
// Navigate to EPD
|
|
window.location.href = `/epd/patients/${patientId}/intakes/${intakeId}/[tab]`;
|
|
},
|
|
}}
|
|
/>
|
|
</BlockContainer>
|
|
);
|
|
}
|
|
|
|
// Render data
|
|
return (
|
|
<BlockContainer title={config.title} size={config.size}>
|
|
<div className="space-y-4">
|
|
<BlockSection icon={[Icon]} iconColor="text-[color]-600" title="[Section Title]" count={data.items.length}>
|
|
<div className="space-y-2">
|
|
{data.items.map((item) => (
|
|
<BlockItem
|
|
key={item.id}
|
|
title={item.title}
|
|
subtitle={item.subtitle}
|
|
badge={{
|
|
label: item.status,
|
|
variant: getBadgeVariant(item.status),
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
</BlockSection>
|
|
|
|
<BlockFooter
|
|
secondaryAction={{
|
|
label: 'Bekijk in dossier',
|
|
icon: ExternalLink,
|
|
onClick: () => {
|
|
window.location.href = `/epd/patients/${patientId}/intakes/${intakeId}/[tab]`;
|
|
},
|
|
}}
|
|
primaryAction={{
|
|
label: 'Toevoegen',
|
|
onClick: () => {
|
|
// Open action block or navigate
|
|
},
|
|
}}
|
|
/>
|
|
</div>
|
|
</BlockContainer>
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helpers
|
|
// ============================================================================
|
|
|
|
function getBadgeVariant(status: string): 'default' | 'success' | 'warning' | 'danger' {
|
|
switch (status) {
|
|
case 'laag': return 'success';
|
|
case 'gemiddeld': return 'warning';
|
|
case 'hoog':
|
|
case 'zeer_hoog': return 'danger';
|
|
default: return 'default';
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Template: Status Block
|
|
|
|
```typescript
|
|
// components/cortex/blocks/intake-status-block.tsx
|
|
|
|
'use client';
|
|
|
|
/**
|
|
* Intake Status Block
|
|
*
|
|
* Block voor het tonen van intake voortgang/checklist.
|
|
* Intent: intake_status
|
|
*/
|
|
|
|
import { useCortexStore } from '@/stores/cortex-store';
|
|
import { BlockContainer } from './block-container';
|
|
import { BlockLoading, BlockError, BlockEmpty } from '../shared/block-states';
|
|
import { useBlockData } from '@/lib/cortex/hooks/use-block-data';
|
|
import { useIntakeContext } from '@/lib/cortex/hooks/use-intake-context';
|
|
import type { BlockPrefillData } from '@/stores/cortex-store';
|
|
import { BLOCK_CONFIGS } from '@/lib/cortex/types';
|
|
import {
|
|
CheckCircle2,
|
|
Circle,
|
|
ClipboardList,
|
|
ChevronRight,
|
|
} from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
// ============================================================================
|
|
// Types
|
|
// ============================================================================
|
|
|
|
interface IntakeStatusBlockProps {
|
|
prefill?: BlockPrefillData;
|
|
}
|
|
|
|
interface IntakeStatusData {
|
|
intakeId: string;
|
|
intakeTitle: string;
|
|
completedCount: number;
|
|
totalCount: number;
|
|
sections: Array<{
|
|
key: string;
|
|
label: string;
|
|
completed: boolean;
|
|
required: boolean;
|
|
path: string;
|
|
}>;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Constants
|
|
// ============================================================================
|
|
|
|
const SECTION_ORDER = [
|
|
{ key: 'algemeen', label: 'Algemeen', required: true },
|
|
{ key: 'contactmomenten', label: 'Contactmomenten', required: false },
|
|
{ key: 'kindcheck', label: 'Kindcheck', required: true },
|
|
{ key: 'risicotaxatie', label: 'Risicotaxatie', required: true },
|
|
{ key: 'anamnese', label: 'Anamnese', required: true },
|
|
{ key: 'onderzoeken', label: 'Onderzoeken', required: false },
|
|
{ key: 'rom', label: 'ROM', required: false },
|
|
{ key: 'diagnose', label: 'Diagnose', required: true },
|
|
{ key: 'behandeladvies', label: 'Behandeladvies', required: true },
|
|
];
|
|
|
|
// ============================================================================
|
|
// Component
|
|
// ============================================================================
|
|
|
|
export function IntakeStatusBlock({ prefill }: IntakeStatusBlockProps) {
|
|
const config = BLOCK_CONFIGS['intake-status']; // Need to add this
|
|
const { patientId, intakeId, hasContext } = useIntakeContext(prefill);
|
|
|
|
const { data, isLoading, error, refetch } = useBlockData<IntakeStatusData>({
|
|
endpoint: `/api/cortex/intake/${intakeId}/status`,
|
|
enabled: Boolean(intakeId),
|
|
});
|
|
|
|
if (!hasContext) {
|
|
return (
|
|
<BlockContainer title="Intake Status" size="md">
|
|
<BlockEmpty
|
|
icon={ClipboardList}
|
|
message="Selecteer eerst een patiënt met een actieve intake"
|
|
/>
|
|
</BlockContainer>
|
|
);
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<BlockContainer title="Intake Status" size="md">
|
|
<BlockLoading message="Status laden..." />
|
|
</BlockContainer>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<BlockContainer title="Intake Status" size="md">
|
|
<BlockError message={error} onRetry={refetch} />
|
|
</BlockContainer>
|
|
);
|
|
}
|
|
|
|
if (!data) {
|
|
return (
|
|
<BlockContainer title="Intake Status" size="md">
|
|
<BlockEmpty
|
|
icon={ClipboardList}
|
|
message="Geen actieve intake gevonden"
|
|
/>
|
|
</BlockContainer>
|
|
);
|
|
}
|
|
|
|
const progress = Math.round((data.completedCount / data.totalCount) * 100);
|
|
const incompleteSections = data.sections.filter(s => !s.completed && s.required);
|
|
|
|
return (
|
|
<BlockContainer title={`Intake Status - ${data.intakeTitle}`} size="md">
|
|
<div className="space-y-4">
|
|
{/* Progress Bar */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-slate-600">Voortgang</span>
|
|
<span className="font-medium text-slate-900">
|
|
{data.completedCount}/{data.totalCount} ({progress}%)
|
|
</span>
|
|
</div>
|
|
<div className="h-2 bg-slate-100 rounded-full overflow-hidden">
|
|
<div
|
|
className={cn(
|
|
'h-full rounded-full transition-all',
|
|
progress === 100 ? 'bg-green-500' : 'bg-teal-500'
|
|
)}
|
|
style={{ width: `${progress}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Incomplete Items (priority) */}
|
|
{incompleteSections.length > 0 && (
|
|
<div className="p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
|
<p className="text-sm font-medium text-amber-800 mb-2">
|
|
Nog te voltooien ({incompleteSections.length})
|
|
</p>
|
|
<div className="space-y-1">
|
|
{incompleteSections.map((section) => (
|
|
<button
|
|
key={section.key}
|
|
onClick={() => {
|
|
window.location.href = `/epd/patients/${patientId}/intakes/${intakeId}/${section.path}`;
|
|
}}
|
|
className="flex items-center justify-between w-full p-2 rounded hover:bg-amber-100 transition-colors text-left"
|
|
>
|
|
<span className="text-sm text-amber-900">{section.label}</span>
|
|
<ChevronRight className="h-4 w-4 text-amber-600" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* All Sections */}
|
|
<div className="space-y-1">
|
|
{data.sections.map((section) => (
|
|
<button
|
|
key={section.key}
|
|
onClick={() => {
|
|
window.location.href = `/epd/patients/${patientId}/intakes/${intakeId}/${section.path}`;
|
|
}}
|
|
className={cn(
|
|
'flex items-center justify-between w-full p-3 rounded-lg border transition-colors text-left',
|
|
section.completed
|
|
? 'bg-green-50 border-green-200 hover:bg-green-100'
|
|
: 'bg-slate-50 border-slate-200 hover:bg-slate-100'
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
{section.completed ? (
|
|
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
|
) : (
|
|
<Circle className="h-5 w-5 text-slate-400" />
|
|
)}
|
|
<span className={cn(
|
|
'text-sm',
|
|
section.completed ? 'text-green-900' : 'text-slate-700'
|
|
)}>
|
|
{section.label}
|
|
</span>
|
|
{section.required && !section.completed && (
|
|
<span className="text-xs px-1.5 py-0.5 bg-red-100 text-red-700 rounded">
|
|
Verplicht
|
|
</span>
|
|
)}
|
|
</div>
|
|
<ChevronRight className={cn(
|
|
'h-4 w-4',
|
|
section.completed ? 'text-green-600' : 'text-slate-400'
|
|
)} />
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Complete Message */}
|
|
{progress === 100 && (
|
|
<div className="p-3 bg-green-50 border border-green-200 rounded-lg text-center">
|
|
<CheckCircle2 className="h-6 w-6 text-green-600 mx-auto mb-2" />
|
|
<p className="text-sm font-medium text-green-800">
|
|
Intake is compleet!
|
|
</p>
|
|
<p className="text-xs text-green-600 mt-1">
|
|
Je kunt de intake nu afsluiten
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</BlockContainer>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Checklist voor nieuwe Block
|
|
|
|
Bij het bouwen van een nieuwe block:
|
|
|
|
- [ ] **Types definiëren** - Props interface, Data interface
|
|
- [ ] **Intent toevoegen** aan `lib/cortex/types.ts`
|
|
- [ ] **Patterns toevoegen** aan `lib/cortex/reflex-classifier.ts`
|
|
- [ ] **Block config toevoegen** aan `BLOCK_CONFIGS`
|
|
- [ ] **API route maken** (indien nodig) in `app/api/cortex/`
|
|
- [ ] **Block component bouwen** met shared components
|
|
- [ ] **Canvas-area updaten** om block te renderen
|
|
- [ ] **Testen** met voice input en prefill
|
|
|
|
---
|
|
|
|
## 8. Bestandsstructuur
|
|
|
|
```
|
|
components/cortex/
|
|
├── blocks/
|
|
│ ├── block-container.tsx # Wrapper (bestaat)
|
|
│ ├── dagnotitie-block.tsx # Bestaat
|
|
│ ├── zoeken-block.tsx # Bestaat
|
|
│ ├── overdracht-block.tsx # Bestaat
|
|
│ ├── patient-dashboard-block.tsx # Bestaat
|
|
│ │
|
|
│ ├── # NIEUW - Intake Blocks
|
|
│ ├── intake-status-block.tsx # Checklist
|
|
│ ├── risico-query-block.tsx # Risico's tonen
|
|
│ ├── diagnose-query-block.tsx # Diagnoses tonen
|
|
│ ├── kindcheck-query-block.tsx # Kindcheck status
|
|
│ └── screening-query-block.tsx # Screening overzicht
|
|
│
|
|
├── shared/
|
|
│ ├── patient-list-item.tsx # Bestaat
|
|
│ ├── linked-evidence.tsx # Bestaat
|
|
│ │
|
|
│ ├── # NIEUW - Shared Block Components
|
|
│ ├── block-states.tsx # Loading/Error/Empty
|
|
│ ├── block-section.tsx # Section wrapper
|
|
│ ├── block-item.tsx # List item
|
|
│ └── block-footer.tsx # Footer actions
|
|
│
|
|
lib/cortex/
|
|
├── hooks/
|
|
│ ├── use-patient-search.ts # Bestaat
|
|
│ ├── use-patient-selection.ts # Bestaat
|
|
│ │
|
|
│ ├── # NIEUW
|
|
│ ├── use-block-data.ts # Generic data fetching
|
|
│ └── use-intake-context.ts # Intake context
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Volgende Stappen
|
|
|
|
1. **Shared components bouwen** (`block-states.tsx`, etc.)
|
|
2. **Hooks bouwen** (`use-block-data.ts`, `use-intake-context.ts`)
|
|
3. **Eerste block:** `IntakeStatusBlock` (meest waardevolle quick win)
|
|
4. **API route:** `/api/cortex/intake/[id]/status`
|
|
5. **Intent + patterns** toevoegen voor `intake_status`
|