rapidUI

Examples

Whole screens for municipal services, built only from rapidUI components. Copy the source into your app.

Case detail

One case: documents, messages with the resident, history and the decision.

2026-04812In progress

Extension of single-family home, Björken 4:12

Due
8 Jun 2026(in 35 days)

Key facts

Applicant
Anna Lindqvist
Property
Björken 4:12, Storgatan 14
Case type
Building permit – extension
Received
16 Mar 2026
Complete since
30 Mar 2026
Fee
24 680 kr
Detailed plan
DP 1284 (2011)
Extension area
38 m² gross floor area

Handling

Current step: Assessment

Handling steps
x
  1. RegisteredDone
  2. Completeness checkDone
  3. Neighbour consultationDone
  4. AssessmentCurrent
  5. DecisionUpcoming

Neighbour consultation

3 of 3 responses received · reply deadline was 27 Apr 2026

Received 22 Apr 2026

The extension will shade our terrace in the afternoon. We ask that the roof height be lowered or the extension moved 2 metres further from the boundary.

View source components/examples/case-detail.tsx
'use client'

import {
	ArrowDownLeftIcon,
	ArrowUpRightIcon,
	BuildingIcon,
	CalendarClockIcon,
	CheckCheckIcon,
	CheckCircle2Icon,
	CheckIcon,
	CircleDotIcon,
	CircleIcon,
	ClockIcon,
	FileTextIcon,
	GavelIcon,
	HomeIcon,
	ImageIcon,
	LinkIcon,
	MailIcon,
	MessageSquareIcon,
	MoreHorizontalIcon,
	PauseCircleIcon,
	PhoneIcon,
	PlayCircleIcon,
	SendIcon,
	UserCogIcon,
	UserIcon,
	XCircleIcon,
} from 'lucide-react'
import * as React from 'react'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
import { Alert, AlertAction, AlertDescription, AlertTitle } from '@/components/ui/alert'
import {
	AlertDialog,
	AlertDialogAction,
	AlertDialogCancel,
	AlertDialogContent,
	AlertDialogDescription,
	AlertDialogFooter,
	AlertDialogHeader,
	AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import {
	Attachment,
	AttachmentContent,
	AttachmentDescription,
	AttachmentGroup,
	AttachmentMedia,
	AttachmentTitle,
} from '@/components/ui/attachment'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import {
	Breadcrumb,
	BreadcrumbItem,
	BreadcrumbLink,
	BreadcrumbList,
	BreadcrumbPage,
	BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'
import { Bubble, BubbleContent } from '@/components/ui/bubble'
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { DateTimePicker } from '@/components/ui/datetime-picker'
import {
	DescriptionList,
	DescriptionListDetails,
	DescriptionListItem,
	DescriptionListTerm,
} from '@/components/ui/description-list'
import {
	Dialog,
	DialogClose,
	DialogContent,
	DialogDescription,
	DialogFooter,
	DialogHeader,
	DialogTitle,
} from '@/components/ui/dialog'
import {
	DropdownMenu,
	DropdownMenuContent,
	DropdownMenuGroup,
	DropdownMenuItem,
	DropdownMenuLabel,
	DropdownMenuSeparator,
	DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
import {
	Field,
	FieldDescription,
	FieldError,
	FieldGroup,
	FieldLabel,
	FieldLegend,
	FieldSet,
} from '@/components/ui/field'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from '@/components/ui/item'
import {
	Message,
	MessageAvatar,
	MessageContent,
	MessageFooter,
	MessageGroup,
	MessageHeader,
} from '@/components/ui/message'
import { Progress, ProgressLabel, ProgressValue } from '@/components/ui/progress'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet'
import { SubTabs, SubTabsContent, SubTabsList, SubTabsTrigger } from '@/components/ui/sub-tabs'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Textarea } from '@/components/ui/textarea'

// ——— Data ———————————————————————————————————————————————————————————————————————————————————————————

type Status =
	| 'in-progress'
	| 'awaiting-supplement'
	| 'on-hold'
	| 'granted'
	| 'granted-conditions'
	| 'refused'
	| 'closed'

const statusMeta: Record<Status, { label: string; icon: React.ElementType; className: string }> = {
	'in-progress': { label: 'In progress', icon: CircleDotIcon, className: 'bg-primary/10 text-primary' },
	'awaiting-supplement': { label: 'Awaiting supplement', icon: ClockIcon, className: 'bg-muted text-foreground' },
	'on-hold': { label: 'On hold', icon: PauseCircleIcon, className: 'bg-muted text-foreground' },
	granted: { label: 'Granted', icon: CheckCircle2Icon, className: 'bg-primary/10 text-primary' },
	'granted-conditions': {
		label: 'Granted with conditions',
		icon: CheckCircle2Icon,
		className: 'bg-primary/10 text-primary',
	},
	refused: { label: 'Refused', icon: XCircleIcon, className: 'bg-destructive/10 text-destructive' },
	closed: { label: 'Closed', icon: XCircleIcon, className: 'bg-muted text-muted-foreground' },
}

type Officer = { id: string; name: string; initials: string; role: string; phone: string; email: string }

const officers: Officer[] = [
	{
		id: 'sofia',
		name: 'Sofia Berg',
		initials: 'SB',
		role: 'Building permit officer',
		phone: '021-39 14 22',
		email: 'sofia.berg@kommun.example',
	},
	{
		id: 'mikko',
		name: 'Mikko Virtanen',
		initials: 'MV',
		role: 'Building inspector',
		phone: '021-39 14 37',
		email: 'mikko.virtanen@kommun.example',
	},
	{
		id: 'omar',
		name: 'Omar Haddad',
		initials: 'OH',
		role: 'Senior building permit officer',
		phone: '021-39 14 05',
		email: 'omar.haddad@kommun.example',
	},
]

const officerItems = Object.fromEntries(officers.map((o) => [o.id, o.name]))

const facts = [
	{ term: 'Applicant', value: 'Anna Lindqvist' },
	{ term: 'Property', value: 'Björken 4:12, Storgatan 14' },
	{ term: 'Case type', value: 'Building permit – extension' },
	{ term: 'Received', value: '16 Mar 2026' },
	{ term: 'Complete since', value: '30 Mar 2026' },
	{ term: 'Fee', value: '24 680 kr' },
	{ term: 'Detailed plan', value: 'DP 1284 (2011)' },
	{ term: 'Extension area', value: '38 m² gross floor area' },
]

const steps = ['Registered', 'Completeness check', 'Neighbour consultation', 'Assessment', 'Decision']

const neighbours = [
	{
		id: 'n1',
		property: 'Björken 4:11',
		name: 'Ingrid Hansen',
		verdict: 'Objection',
		date: '22 Apr 2026',
		text:
			'The extension will shade our terrace in the afternoon. We ask that the roof height be lowered or the extension moved 2 metres further from the boundary.',
	},
	{
		id: 'n2',
		property: 'Björken 4:13',
		name: 'Omar Haddad',
		verdict: 'Comments',
		date: '20 Apr 2026',
		text:
			'No objection to the extension itself, but we would like construction traffic to use Kungsgatan and not the shared driveway.',
	},
	{
		id: 'n3',
		property: 'Storgatan 16',
		name: 'Mikko Virtanen',
		verdict: 'No objection',
		date: '18 Apr 2026',
		text: 'We have seen the drawings and have no objections.',
	},
]

type Direction = 'incoming' | 'outgoing'
type Doc = {
	id: string
	title: string
	direction: Direction
	type: string
	date: string
	by: string
	files: { name: string; meta: string; kind: 'pdf' | 'image' }[]
}

const initialDocuments: Doc[] = [
	{
		id: 'd1',
		title: 'Application for building permit',
		direction: 'incoming',
		type: 'Application',
		date: '16 Mar 2026',
		by: 'Anna Lindqvist',
		files: [
			{ name: 'application.pdf', meta: 'PDF · 412 kB', kind: 'pdf' },
			{ name: 'site-plan.pdf', meta: 'PDF · 1.2 MB', kind: 'pdf' },
		],
	},
	{
		id: 'd2',
		title: 'Request for supplementary documents',
		direction: 'outgoing',
		type: 'Letter',
		date: '19 Mar 2026',
		by: 'Sofia Berg',
		files: [{ name: 'request-supplement.pdf', meta: 'PDF · 88 kB', kind: 'pdf' }],
	},
	{
		id: 'd3',
		title: 'Facade and section drawings',
		direction: 'incoming',
		type: 'Supplement',
		date: '30 Mar 2026',
		by: 'Anna Lindqvist',
		files: [
			{ name: 'facades-rev-b.pdf', meta: 'PDF · 2.4 MB', kind: 'pdf' },
			{ name: 'photo-garden.jpg', meta: 'JPG · 3.1 MB', kind: 'image' },
		],
	},
	{
		id: 'd4',
		title: 'Neighbour consultation letter',
		direction: 'outgoing',
		type: 'Consultation',
		date: '6 Apr 2026',
		by: 'Sofia Berg',
		files: [{ name: 'consultation.pdf', meta: 'PDF · 156 kB', kind: 'pdf' }],
	},
	{
		id: 'd5',
		title: 'Neighbour statement – Björken 4:11',
		direction: 'incoming',
		type: 'Statement',
		date: '22 Apr 2026',
		by: 'Ingrid Hansen',
		files: [{ name: 'statement-4-11.pdf', meta: 'PDF · 64 kB', kind: 'pdf' }],
	},
]

type ChatMessage = {
	id: string
	from: 'applicant' | 'officer'
	text: string
	time: string
	status?: 'Read' | 'Delivered'
	files?: { name: string; meta: string }[]
}

const initialMessages: ChatMessage[] = [
	{
		id: 'm1',
		from: 'applicant',
		text: 'Hi! I have uploaded the revised facade drawings you asked for. Is anything else missing?',
		time: '30 Mar, 08:14',
		files: [{ name: 'facades-rev-b.pdf', meta: 'PDF · 2.4 MB' }],
	},
	{
		id: 'm2',
		from: 'officer',
		text:
			'Thank you, Anna. The application is now complete. Next we will send it to your neighbours for consultation – they have three weeks to reply.',
		time: '30 Mar, 13:02',
		status: 'Read',
	},
	{
		id: 'm3',
		from: 'applicant',
		text: 'Great. I heard that the neighbour at 4:11 is worried about shade. Can I send a sun study?',
		time: '24 Apr, 19:40',
	},
	{
		id: 'm4',
		from: 'officer',
		text: 'Yes, please do. A sun study for 20 March and 21 June would help us assess the objection.',
		time: '27 Apr, 09:15',
		status: 'Read',
	},
]

type HistoryEvent = { id: string; icon: React.ElementType; title: string; who: string; when: string }

const initialHistory: HistoryEvent[] = [
	{
		id: 'h7',
		icon: MessageSquareIcon,
		title: 'Message sent to applicant',
		who: 'Sofia Berg',
		when: '27 Apr 2026, 09:15',
	},
	{
		id: 'h6',
		icon: ArrowDownLeftIcon,
		title: 'Neighbour statement registered (Björken 4:11)',
		who: 'Registry',
		when: '22 Apr 2026, 10:31',
	},
	{
		id: 'h5',
		icon: ArrowUpRightIcon,
		title: 'Neighbour consultation sent to 3 properties',
		who: 'Sofia Berg',
		when: '6 Apr 2026, 14:20',
	},
	{ id: 'h4', icon: CheckIcon, title: 'Application marked complete', who: 'Sofia Berg', when: '30 Mar 2026, 13:00' },
	{ id: 'h3', icon: ArrowUpRightIcon, title: 'Supplement requested', who: 'Sofia Berg', when: '19 Mar 2026, 11:47' },
	{ id: 'h2', icon: UserCogIcon, title: 'Case assigned to Sofia Berg', who: 'Omar Haddad', when: '17 Mar 2026, 08:30' },
	{ id: 'h1', icon: FileTextIcon, title: 'Case registered via e-service', who: 'System', when: '16 Mar 2026, 21:05' },
]

const relatedCases = [
	{ id: '2025-11207', title: 'Pre-application advice, Björken 4:12', status: 'Closed' },
	{ id: '2026-04390', title: 'Tree felling permit, Björken 4:11', status: 'In progress' },
	{ id: '2024-02918', title: 'Garage, Björken 4:12', status: 'Granted' },
]

const deadlines = [
	{ label: 'Supplement from applicant', date: '14 May 2026', note: 'Sun study' },
	{ label: 'Statutory decision deadline', date: '8 Jun 2026', note: '10 weeks from complete' },
	{ label: 'Appeal period ends', date: '3 weeks after notice', note: 'After decision' },
]

const decisionTypes = [
	{ value: 'granted', label: 'Granted', description: 'The permit is granted as applied for.' },
	{
		value: 'granted-conditions',
		label: 'Granted with conditions',
		description: 'The permit is granted, subject to the conditions below.',
	},
	{ value: 'refused', label: 'Refused', description: 'The application is refused. State the reasons below.' },
] as const

type DecisionType = (typeof decisionTypes)[number]['value']

function nowLabel() {
	return new Date().toLocaleString('en-GB', {
		day: 'numeric',
		month: 'short',
		year: 'numeric',
		hour: '2-digit',
		minute: '2-digit',
	})
}

function formatDate(value: string) {
	return new Date(`${value}T00:00:00`).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })
}

// ——— Small pieces ————————————————————————————————————————————————————————————————————————————————

function StatusBadge({ status }: { status: Status }) {
	const meta = statusMeta[status]
	const Icon = meta.icon
	return (
		<Badge className={meta.className}>
			<Icon aria-hidden="true" />
			{meta.label}
		</Badge>
	)
}

function HandlerCard({ officer }: { officer: Officer }) {
	return (
		<HoverCard>
			<HoverCardTrigger
				delay={200}
				closeDelay={100}
				href={`#officer-${officer.id}`}
				onClick={(event) => event.preventDefault()}
				className="inline-flex items-center gap-2 rounded-md font-medium text-foreground underline-offset-4 outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring"
			>
				<Avatar size="sm">
					<AvatarFallback>{officer.initials}</AvatarFallback>
				</Avatar>
				{officer.name}
			</HoverCardTrigger>
			<HoverCardContent className="w-72 p-0" align="start">
				<div className="flex gap-3 p-4">
					<Avatar size="lg">
						<AvatarFallback>{officer.initials}</AvatarFallback>
					</Avatar>
					<div className="flex min-w-0 flex-col gap-0.5">
						<p className="font-medium">{officer.name}</p>
						<p className="text-muted-foreground">{officer.role}</p>
					</div>
				</div>
				<Separator />
				<DescriptionList className="gap-y-2 p-4">
					<DescriptionListItem className="flex-row items-center gap-2">
						<DescriptionListTerm className="flex shrink-0">
							<BuildingIcon className="size-4 text-muted-foreground" aria-hidden="true" />
							<span className="sr-only">Department</span>
						</DescriptionListTerm>
						<DescriptionListDetails>Planning and Building Department</DescriptionListDetails>
					</DescriptionListItem>
					<DescriptionListItem className="flex-row items-center gap-2">
						<DescriptionListTerm className="flex shrink-0">
							<PhoneIcon className="size-4 text-muted-foreground" aria-hidden="true" />
							<span className="sr-only">Phone</span>
						</DescriptionListTerm>
						<DescriptionListDetails>{officer.phone}</DescriptionListDetails>
					</DescriptionListItem>
					<DescriptionListItem className="flex-row items-center gap-2">
						<DescriptionListTerm className="flex shrink-0">
							<MailIcon className="size-4 text-muted-foreground" aria-hidden="true" />
							<span className="sr-only">Email</span>
						</DescriptionListTerm>
						<DescriptionListDetails className="truncate">{officer.email}</DescriptionListDetails>
					</DescriptionListItem>
				</DescriptionList>
			</HoverCardContent>
		</HoverCard>
	)
}

function FileChip({ name, meta, kind = 'pdf' }: { name: string; meta: string; kind?: 'pdf' | 'image' }) {
	return (
		<Attachment size="xs">
			<AttachmentMedia>{kind === 'image' ? <ImageIcon /> : <FileTextIcon />}</AttachmentMedia>
			<AttachmentContent>
				<AttachmentTitle>{name}</AttachmentTitle>
				<AttachmentDescription>{meta}</AttachmentDescription>
			</AttachmentContent>
		</Attachment>
	)
}

// ——— Tabs ————————————————————————————————————————————————————————————————————————————————————————

function OverviewTab({ stepIndex }: { stepIndex: number }) {
	const done = Math.min(stepIndex, steps.length)
	return (
		<div className="flex flex-col gap-4">
			<Card>
				<CardHeader>
					<CardTitle as="h4">Key facts</CardTitle>
				</CardHeader>
				<CardContent>
					<DescriptionList columns={2}>
						{facts.map((fact) => (
							<DescriptionListItem key={fact.term} className="gap-0.5">
								<DescriptionListTerm>{fact.term}</DescriptionListTerm>
								<DescriptionListDetails className="font-medium">{fact.value}</DescriptionListDetails>
							</DescriptionListItem>
						))}
					</DescriptionList>
				</CardContent>
			</Card>

			<Card>
				<CardHeader>
					<CardTitle as="h4">Handling</CardTitle>
					<CardDescription>
						{done >= steps.length ? 'All steps are complete.' : `Current step: ${steps[done]}`}
					</CardDescription>
				</CardHeader>
				<CardContent className="flex flex-col gap-4">
					<Progress value={(done / steps.length) * 100}>
						<ProgressLabel>Handling steps</ProgressLabel>
						<ProgressValue>{() => `${done} of ${steps.length} done`}</ProgressValue>
					</Progress>
					<ol className="grid gap-2 @xl:grid-cols-5">
						{steps.map((step, index) => {
							const state = index < done ? 'Done' : index === done ? 'Current' : 'Upcoming'
							return (
								<li
									key={step}
									aria-current={state === 'Current' ? 'step' : undefined}
									className="flex items-center gap-2 rounded-lg border px-3 py-2 data-[state=Current]:border-primary data-[state=Current]:bg-primary/5 @xl:flex-col @xl:items-start @xl:gap-1"
									data-state={state}
								>
									{state === 'Done' ? (
										<CheckCircle2Icon className="size-4 shrink-0 text-primary" aria-hidden="true" />
									) : state === 'Current' ? (
										<CircleDotIcon className="size-4 shrink-0 text-primary" aria-hidden="true" />
									) : (
										<CircleIcon className="size-4 shrink-0 text-muted-foreground" aria-hidden="true" />
									)}
									<span className="font-medium">{step}</span>
									<span className="ml-auto text-muted-foreground @xl:ml-0">{state}</span>
								</li>
							)
						})}
					</ol>
				</CardContent>
			</Card>

			<Card>
				<CardHeader>
					<CardTitle as="h4">Neighbour consultation</CardTitle>
					<CardDescription>3 of 3 responses received · reply deadline was 27 Apr 2026</CardDescription>
				</CardHeader>
				<CardContent>
					<Accordion defaultValue={['n1']}>
						{neighbours.map((n) => (
							<AccordionItem key={n.id} value={n.id}>
								<AccordionTrigger>
									<span className="flex flex-1 flex-wrap items-center gap-x-2 gap-y-1">
										<span>
											{n.property} · {n.name}
										</span>
										<Badge
											variant={n.verdict === 'Objection' ? 'destructive' : n.verdict === 'Comments' ? 'secondary' : 'outline'}
										>
											{n.verdict}
										</Badge>
									</span>
								</AccordionTrigger>
								<AccordionContent>
									<p className="text-muted-foreground">Received {n.date}</p>
									<p className="mt-1">{n.text}</p>
								</AccordionContent>
							</AccordionItem>
						))}
					</Accordion>
				</CardContent>
			</Card>
		</div>
	)
}

function DocumentsTab({ documents }: { documents: Doc[] }) {
	const [filter, setFilter] = React.useState<'all' | Direction>('all')
	const counts = {
		all: documents.length,
		incoming: documents.filter((d) => d.direction === 'incoming').length,
		outgoing: documents.filter((d) => d.direction === 'outgoing').length,
	}
	const visible = filter === 'all' ? documents : documents.filter((d) => d.direction === filter)

	return (
		<SubTabs value={filter} onValueChange={(value) => setFilter(value as 'all' | Direction)}>
			<div className="flex flex-wrap items-center justify-between gap-2">
				<SubTabsList aria-label="Filter documents">
					<SubTabsTrigger value="all">All ({counts.all})</SubTabsTrigger>
					<SubTabsTrigger value="incoming">Incoming ({counts.incoming})</SubTabsTrigger>
					<SubTabsTrigger value="outgoing">Outgoing ({counts.outgoing})</SubTabsTrigger>
				</SubTabsList>
				<p className="text-muted-foreground">Diary 2026-04812 · {documents.length} registered</p>
			</div>
			{(['all', 'incoming', 'outgoing'] as const).map((key) => (
				<SubTabsContent key={key} value={key} className="rounded-xl border">
					<Table>
						<TableHeader>
							<TableRow>
								<TableHead className="pl-4">Document</TableHead>
								<TableHead>Direction</TableHead>
								<TableHead>Type</TableHead>
								<TableHead className="pr-4">Registered</TableHead>
							</TableRow>
						</TableHeader>
						<TableBody>
							{visible.map((doc) => (
								<TableRow key={doc.id}>
									<TableCell className="min-w-72 py-3 pl-4 align-top whitespace-normal">
										<div className="flex flex-col gap-2">
											<span className="font-medium">{doc.title}</span>
											<AttachmentGroup className="flex-wrap gap-2 overflow-visible py-0">
												{doc.files.map((file) => (
													<FileChip key={file.name} {...file} />
												))}
											</AttachmentGroup>
										</div>
									</TableCell>
									<TableCell className="py-3 align-top">
										<span className="inline-flex items-center gap-1.5">
											{doc.direction === 'incoming' ? (
												<ArrowDownLeftIcon className="size-4 text-muted-foreground" aria-hidden="true" />
											) : (
												<ArrowUpRightIcon className="size-4 text-muted-foreground" aria-hidden="true" />
											)}
											{doc.direction === 'incoming' ? 'Incoming' : 'Outgoing'}
										</span>
									</TableCell>
									<TableCell className="py-3 align-top">
										<Badge variant="outline">{doc.type}</Badge>
									</TableCell>
									<TableCell className="py-3 pr-4 align-top">
										<div className="flex flex-col">
											<span>{doc.date}</span>
											<span className="text-muted-foreground">{doc.by}</span>
										</div>
									</TableCell>
								</TableRow>
							))}
						</TableBody>
					</Table>
				</SubTabsContent>
			))}
		</SubTabs>
	)
}

function MessagesTab({
	messages,
	officer,
	onSend,
}: {
	messages: ChatMessage[]
	officer: Officer
	onSend: (text: string) => void
}) {
	const id = React.useId()
	const [draft, setDraft] = React.useState('')
	const [error, setError] = React.useState<string | null>(null)

	function send(event: React.FormEvent) {
		event.preventDefault()
		if (!draft.trim()) {
			setError('Write a message before sending.')
			return
		}
		onSend(draft.trim())
		setDraft('')
		setError(null)
	}

	return (
		<Card>
			<CardHeader>
				<CardTitle as="h4">Conversation with Anna Lindqvist</CardTitle>
				<CardDescription>Messages are registered on the case and sent to the applicant’s digital mailbox.</CardDescription>
			</CardHeader>
			<CardContent className="flex flex-col gap-6">
				<MessageGroup className="gap-5" role="log" aria-label="Messages">
					{messages.map((m) => {
						const isOfficer = m.from === 'officer'
						return (
							<Message key={m.id} align={isOfficer ? 'end' : 'start'}>
								<MessageAvatar>
									<Avatar size="sm">
										<AvatarFallback>{isOfficer ? officer.initials : 'AL'}</AvatarFallback>
									</Avatar>
								</MessageAvatar>
								<MessageContent>
									<MessageHeader>{isOfficer ? officer.name : 'Anna Lindqvist'}</MessageHeader>
									<Bubble variant={isOfficer ? 'default' : 'muted'}>
										<BubbleContent>{m.text}</BubbleContent>
									</Bubble>
									{m.files?.map((file) => (
										<FileChip key={file.name} name={file.name} meta={file.meta} />
									))}
									<MessageFooter className="gap-1">
										{m.time}
										{m.status ? (
											<>
												<span aria-hidden="true">·</span>
												{m.status === 'Read' ? (
													<CheckCheckIcon className="size-3.5" aria-hidden="true" />
												) : (
													<CheckIcon className="size-3.5" aria-hidden="true" />
												)}
												{m.status}
											</>
										) : null}
									</MessageFooter>
								</MessageContent>
							</Message>
						)
					})}
				</MessageGroup>
				<Separator />
				<form onSubmit={send} noValidate>
					<Field data-invalid={error ? true : undefined}>
						<FieldLabel htmlFor={`${id}-reply`}>Reply to Anna Lindqvist</FieldLabel>
						<Textarea
							id={`${id}-reply`}
							value={draft}
							onChange={(event) => {
								setDraft(event.target.value)
								if (error) setError(null)
							}}
							placeholder="Write a message…"
							rows={3}
							aria-invalid={error ? true : undefined}
							aria-describedby={error ? `${id}-reply-error` : `${id}-reply-hint`}
						/>
						{error ? (
							<FieldError id={`${id}-reply-error`}>{error}</FieldError>
						) : (
							<FieldDescription id={`${id}-reply-hint`}>The applicant is notified by email.</FieldDescription>
						)}
						<div className="flex justify-end">
							<Button type="submit">
								<SendIcon data-icon="inline-start" />
								Send
							</Button>
						</div>
					</Field>
				</form>
			</CardContent>
		</Card>
	)
}

function HistoryTab({ history }: { history: HistoryEvent[] }) {
	return (
		<Card>
			<CardHeader>
				<CardTitle as="h4">Case history</CardTitle>
				<CardDescription>Newest first. All events are logged automatically.</CardDescription>
			</CardHeader>
			<CardContent>
				<ol className="relative flex flex-col gap-1 before:absolute before:top-4 before:bottom-4 before:left-7 before:w-px before:bg-border">
					{history.map((event) => {
						const Icon = event.icon
						return (
							<Item key={event.id} render={<li />} size="sm" className="relative">
								<ItemMedia className="z-10 size-8 rounded-full border bg-card">
									<Icon className="size-4 text-muted-foreground" aria-hidden="true" />
								</ItemMedia>
								<ItemContent>
									<ItemTitle>{event.title}</ItemTitle>
									<ItemDescription>
										{event.who} · <time>{event.when}</time>
									</ItemDescription>
								</ItemContent>
							</Item>
						)
					})}
				</ol>
			</CardContent>
		</Card>
	)
}

// ——— Aside ———————————————————————————————————————————————————————————————————————————————————————

function CaseAside({ status }: { status: Status }) {
	return (
		<aside aria-label="Case details" className="flex flex-col gap-4">
			<Card size="sm">
				<CardHeader>
					<CardTitle as="h4">Applicant</CardTitle>
				</CardHeader>
				<CardContent className="flex flex-col gap-3">
					<div className="flex items-center gap-3">
						<Avatar>
							<AvatarFallback>AL</AvatarFallback>
						</Avatar>
						<div className="flex min-w-0 flex-col">
							<span className="font-medium">Anna Lindqvist</span>
							<span className="text-muted-foreground">Private individual</span>
						</div>
					</div>
					<DescriptionList className="gap-y-2">
						<DescriptionListItem className="flex-row items-center gap-2">
							<DescriptionListTerm className="flex shrink-0">
								<HomeIcon className="size-4 text-muted-foreground" aria-hidden="true" />
								<span className="sr-only">Address</span>
							</DescriptionListTerm>
							<DescriptionListDetails>Storgatan 14, 722 15</DescriptionListDetails>
						</DescriptionListItem>
						<DescriptionListItem className="flex-row items-center gap-2">
							<DescriptionListTerm className="flex shrink-0">
								<PhoneIcon className="size-4 text-muted-foreground" aria-hidden="true" />
								<span className="sr-only">Phone</span>
							</DescriptionListTerm>
							<DescriptionListDetails>070-123 45 67</DescriptionListDetails>
						</DescriptionListItem>
						<DescriptionListItem className="flex-row items-center gap-2">
							<DescriptionListTerm className="flex shrink-0">
								<MailIcon className="size-4 text-muted-foreground" aria-hidden="true" />
								<span className="sr-only">Email</span>
							</DescriptionListTerm>
							<DescriptionListDetails className="truncate">anna.lindqvist@example.se</DescriptionListDetails>
						</DescriptionListItem>
						<DescriptionListItem className="flex-row items-center gap-2">
							<DescriptionListTerm className="flex shrink-0">
								<UserIcon className="size-4 text-muted-foreground" aria-hidden="true" />
								<span className="sr-only">Preferred contact</span>
							</DescriptionListTerm>
							<DescriptionListDetails>Prefers digital mailbox</DescriptionListDetails>
						</DescriptionListItem>
					</DescriptionList>
				</CardContent>
			</Card>

			<Card size="sm">
				<CardHeader>
					<CardTitle as="h4">Deadlines</CardTitle>
				</CardHeader>
				<CardContent>
					<ul className="flex flex-col gap-3">
						{deadlines.map((d, index) => {
							const met = index < 2 && ['granted', 'granted-conditions', 'refused', 'closed'].includes(status)
							return (
								<li key={d.label} className="flex gap-2">
									{met ? (
										<CheckCircle2Icon className="mt-0.5 size-4 shrink-0 text-primary" aria-hidden="true" />
									) : (
										<CalendarClockIcon className="mt-0.5 size-4 shrink-0 text-muted-foreground" aria-hidden="true" />
									)}
									<div className="flex min-w-0 flex-col">
										<span className="font-medium">{d.label}</span>
										<span className="text-muted-foreground">
											{d.date} · {met ? 'Met' : d.note}
										</span>
									</div>
								</li>
							)
						})}
					</ul>
				</CardContent>
			</Card>

			<Card size="sm">
				<CardHeader>
					<CardTitle as="h4">Related cases</CardTitle>
				</CardHeader>
				<CardContent className="px-1.5">
					<ul className="flex flex-col">
						{relatedCases.map((c) => (
							<li key={c.id}>
								{/* biome-ignore lint/a11y/useAnchorContent: the children are rendered inside the anchor */}
								<Item size="xs" onClick={(event) => event.preventDefault()} render={<a href={`#case-${c.id}`} />}>
									<ItemMedia variant="icon">
										<LinkIcon className="text-muted-foreground" aria-hidden="true" />
									</ItemMedia>
									<ItemContent>
										<ItemTitle>{c.id}</ItemTitle>
										<ItemDescription>{c.title}</ItemDescription>
									</ItemContent>
									<Badge variant="outline">{c.status}</Badge>
								</Item>
							</li>
						))}
					</ul>
				</CardContent>
			</Card>
		</aside>
	)
}

// ——— Decision sheet ——————————————————————————————————————————————————————————————————————————————

type DecisionErrors = Partial<Record<'type' | 'conditions' | 'date', string>>

function DecisionSheet({
	open,
	onOpenChange,
	onDecide,
}: {
	open: boolean
	onOpenChange: (open: boolean) => void
	onDecide: (type: DecisionType, date: string) => void
}) {
	const id = React.useId()
	const [type, setType] = React.useState<DecisionType | ''>('')
	const [conditions, setConditions] = React.useState('')
	const [date, setDate] = React.useState('')
	const [errors, setErrors] = React.useState<DecisionErrors>({})
	const [confirmOpen, setConfirmOpen] = React.useState(false)

	const needsText = type === 'granted-conditions' || type === 'refused'
	const typeLabel = decisionTypes.find((d) => d.value === type)?.label

	function review() {
		const next: DecisionErrors = {}
		if (!type) next.type = 'Choose a decision.'
		if (needsText && !conditions.trim())
			next.conditions = type === 'refused' ? 'State the reasons for refusal.' : 'Write at least one condition.'
		if (!date) next.date = 'Choose the decision date.'
		setErrors(next)
		if (Object.keys(next).length === 0) setConfirmOpen(true)
	}

	function reset() {
		setType('')
		setConditions('')
		setDate('')
		setErrors({})
	}

	return (
		<>
			<Sheet
				open={open}
				onOpenChange={(next) => {
					onOpenChange(next)
					if (!next) reset()
				}}
			>
				<SheetContent className="sm:max-w-md">
					<SheetHeader className="pr-12">
						<SheetTitle>Make decision</SheetTitle>
						<SheetDescription>2026-04812 · Extension of single-family home, Björken 4:12</SheetDescription>
					</SheetHeader>
					<FieldGroup className="px-4">
						<FieldSet data-invalid={errors.type ? true : undefined}>
							<FieldLegend variant="label">Decision</FieldLegend>
							<RadioGroup
								value={type}
								onValueChange={(value) => {
									setType(value as DecisionType)
									setErrors((e) => ({ ...e, type: undefined }))
								}}
								aria-invalid={errors.type ? true : undefined}
								aria-describedby={errors.type ? `${id}-type-error` : undefined}
							>
								{decisionTypes.map((d) => (
									<FieldLabel key={d.value} htmlFor={`${id}-${d.value}`}>
										<Field orientation="horizontal">
											<RadioGroupItem value={d.value} id={`${id}-${d.value}`} />
											<div className="flex flex-col gap-0.5">
												<span className="font-medium">{d.label}</span>
												<span className="font-normal text-muted-foreground">{d.description}</span>
											</div>
										</Field>
									</FieldLabel>
								))}
							</RadioGroup>
							<FieldError id={`${id}-type-error`}>{errors.type}</FieldError>
						</FieldSet>

						<Field data-invalid={errors.conditions ? true : undefined}>
							<FieldLabel htmlFor={`${id}-conditions`}>
								{type === 'refused' ? 'Reasons for refusal' : 'Conditions'}
								{needsText ? null : <span className="font-normal text-muted-foreground">(optional)</span>}
							</FieldLabel>
							<Textarea
								id={`${id}-conditions`}
								rows={4}
								value={conditions}
								onChange={(event) => {
									setConditions(event.target.value)
									setErrors((e) => ({ ...e, conditions: undefined }))
								}}
								placeholder={
									type === 'refused'
										? 'The extension exceeds the permitted building height in DP 1284…'
										: 'The roof height may not exceed 5.6 m above ground…'
								}
								aria-invalid={errors.conditions ? true : undefined}
								aria-describedby={errors.conditions ? `${id}-conditions-error` : `${id}-conditions-hint`}
							/>
							{errors.conditions ? (
								<FieldError id={`${id}-conditions-error`}>{errors.conditions}</FieldError>
							) : (
								<FieldDescription id={`${id}-conditions-hint`}>
									Shown in the decision letter to the applicant and neighbours.
								</FieldDescription>
							)}
						</Field>

						<Field data-invalid={errors.date ? true : undefined}>
							<FieldLabel htmlFor={`${id}-date`}>Decision date</FieldLabel>
							<DateTimePicker
								id={`${id}-date`}
								value={date}
								onChange={(value) => {
									setDate(value)
									setErrors((e) => ({ ...e, date: undefined }))
								}}
								placeholder="Choose a date"
								disabledDates={{ dayOfWeek: [0, 6] }}
								aria-invalid={errors.date ? true : undefined}
								className="w-full"
							/>
							<FieldError>{errors.date}</FieldError>
						</Field>
					</FieldGroup>
					<SheetFooter className="border-t sm:flex-row sm:justify-end">
						<Button variant="outline" onClick={() => onOpenChange(false)}>
							Cancel
						</Button>
						<Button onClick={review}>
							<GavelIcon data-icon="inline-start" />
							Review and decide
						</Button>
					</SheetFooter>
				</SheetContent>
			</Sheet>

			<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
				<AlertDialogContent>
					<AlertDialogHeader>
						<AlertDialogTitle>Register decision “{typeLabel}”?</AlertDialogTitle>
						<AlertDialogDescription>
							The decision dated {date ? formatDate(date) : ''} is registered on case 2026-04812 and sent to Anna Lindqvist and
							the three neighbours. It can only be changed by a new decision.
						</AlertDialogDescription>
					</AlertDialogHeader>
					<AlertDialogFooter>
						<AlertDialogCancel>Go back</AlertDialogCancel>
						<AlertDialogAction
							variant={type === 'refused' ? 'destructive' : 'default'}
							onClick={() => {
								if (!type) return
								onDecide(type, date)
								onOpenChange(false)
								reset()
							}}
						>
							Register decision
						</AlertDialogAction>
					</AlertDialogFooter>
				</AlertDialogContent>
			</AlertDialog>
		</>
	)
}

// ——— Screen ——————————————————————————————————————————————————————————————————————————————————————

export default function CaseDetailExample() {
	const id = React.useId()
	const [status, setStatus] = React.useState<Status>('in-progress')
	const [officerId, setOfficerId] = React.useState('sofia')
	const [stepIndex, setStepIndex] = React.useState(3)
	const [tab, setTab] = React.useState('overview')
	const [documents, setDocuments] = React.useState(initialDocuments)
	const [messages, setMessages] = React.useState(initialMessages)
	const [history, setHistory] = React.useState(initialHistory)
	const [notice, setNotice] = React.useState<{ title: string; text: string } | null>(null)

	const [decisionOpen, setDecisionOpen] = React.useState(false)
	const [supplementOpen, setSupplementOpen] = React.useState(false)
	const [supplementText, setSupplementText] = React.useState('')
	const [supplementError, setSupplementError] = React.useState<string | null>(null)
	const [reassignOpen, setReassignOpen] = React.useState(false)
	const [reassignTo, setReassignTo] = React.useState<string | null>('sofia')
	const [closeOpen, setCloseOpen] = React.useState(false)

	const officer = officers.find((o) => o.id === officerId) ?? officers[0]!
	const decided = ['granted', 'granted-conditions', 'refused', 'closed'].includes(status)

	function log(icon: React.ElementType, title: string) {
		setHistory((h) => [{ id: `h${Date.now()}`, icon, title, who: officer.name, when: nowLabel() }, ...h])
	}

	function sendMessage(text: string) {
		setMessages((m) => [
			...m,
			{
				id: `m${Date.now()}`,
				from: 'officer',
				text,
				time: new Date().toLocaleString('en-GB', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }),
				status: 'Delivered',
			},
		])
		log(MessageSquareIcon, 'Message sent to applicant')
	}

	function requestSupplement() {
		if (!supplementText.trim()) {
			setSupplementError('Describe what the applicant needs to send.')
			return
		}
		sendMessage(supplementText.trim())
		setDocuments((d) => [
			...d,
			{
				id: `d${Date.now()}`,
				title: 'Request for supplementary documents',
				direction: 'outgoing',
				type: 'Letter',
				date: formatDate(new Date().toISOString().slice(0, 10)),
				by: officer.name,
				files: [{ name: 'request-supplement-2.pdf', meta: 'PDF · 72 kB', kind: 'pdf' }],
			},
		])
		log(ArrowUpRightIcon, 'Supplement requested')
		setStatus('awaiting-supplement')
		setSupplementOpen(false)
		setSupplementText('')
		setNotice({ title: 'Supplement requested', text: 'Anna Lindqvist has been asked to send more documents.' })
	}

	function decide(type: DecisionType, date: string) {
		const label = decisionTypes.find((d) => d.value === type)?.label ?? ''
		setStatus(type)
		setStepIndex(steps.length)
		setDocuments((d) => [
			...d,
			{
				id: `d${Date.now()}`,
				title: `Decision – ${label.toLowerCase()}`,
				direction: 'outgoing',
				type: 'Decision',
				date: formatDate(date),
				by: officer.name,
				files: [{ name: 'decision-2026-04812.pdf', meta: 'PDF · 210 kB', kind: 'pdf' }],
			},
		])
		log(GavelIcon, `Decision registered: ${label}`)
		setNotice({
			title: `Decision registered: ${label}`,
			text: `Dated ${formatDate(date)}. The decision letter has been sent.`,
		})
	}

	function reassign() {
		if (!reassignTo || reassignTo === officerId) {
			setReassignOpen(false)
			return
		}
		const next = officers.find((o) => o.id === reassignTo)
		setOfficerId(reassignTo)
		setHistory((h) => [
			{
				id: `h${Date.now()}`,
				icon: UserCogIcon,
				title: `Case reassigned to ${next?.name}`,
				who: officer.name,
				when: nowLabel(),
			},
			...h,
		])
		setReassignOpen(false)
		setNotice({ title: 'Case reassigned', text: `${next?.name} is now handling the case.` })
	}

	return (
		<div className="@container flex min-h-[720px] flex-col gap-6 bg-background p-4 text-sm @3xl:p-6">
			<Breadcrumb>
				<BreadcrumbList>
					<BreadcrumbItem>
						<BreadcrumbLink href="#cases" onClick={(event) => event.preventDefault()}>
							Cases
						</BreadcrumbLink>
					</BreadcrumbItem>
					<BreadcrumbSeparator />
					<BreadcrumbItem>
						<BreadcrumbLink href="#building-permits" onClick={(event) => event.preventDefault()}>
							Building permits
						</BreadcrumbLink>
					</BreadcrumbItem>
					<BreadcrumbSeparator />
					<BreadcrumbItem>
						<BreadcrumbPage>2026-04812</BreadcrumbPage>
					</BreadcrumbItem>
				</BreadcrumbList>
			</Breadcrumb>

			<header className="flex flex-col gap-4 @4xl:flex-row @4xl:items-start @4xl:justify-between">
				<div className="flex min-w-0 flex-col gap-2">
					<div className="flex flex-wrap items-center gap-2">
						<span className="font-mono text-muted-foreground">2026-04812</span>
						<StatusBadge status={status} />
					</div>
					<h3 className="font-heading text-xl font-semibold text-balance">Extension of single-family home, Björken 4:12</h3>
					<DescriptionList className="flex flex-wrap items-center gap-x-6 gap-y-2">
						<DescriptionListItem className="flex-row items-center gap-2">
							<DescriptionListTerm>Handler</DescriptionListTerm>
							<DescriptionListDetails>
								<HandlerCard officer={officer} />
							</DescriptionListDetails>
						</DescriptionListItem>
						<DescriptionListItem className="flex-row items-center gap-2">
							<DescriptionListTerm>Due</DescriptionListTerm>
							<DescriptionListDetails className="inline-flex items-center gap-1.5 font-medium">
								<CalendarClockIcon className="size-4 text-muted-foreground" aria-hidden="true" />8 Jun 2026
								{decided ? null : <span className="font-normal text-muted-foreground">(in 35 days)</span>}
							</DescriptionListDetails>
						</DescriptionListItem>
					</DescriptionList>
				</div>

				<div className="flex flex-wrap items-center gap-2">
					<ButtonGroup aria-label="Case actions">
						<Button variant="outline" disabled={decided} onClick={() => setSupplementOpen(true)}>
							<FileTextIcon data-icon="inline-start" className="hidden @md:block" />
							<span className="@md:hidden">Supplement</span>
							<span className="hidden @md:inline">Request supplement</span>
						</Button>
						<Button disabled={decided} onClick={() => setDecisionOpen(true)}>
							<GavelIcon data-icon="inline-start" className="hidden @md:block" />
							Make decision
						</Button>
					</ButtonGroup>
					<DropdownMenu>
						<DropdownMenuTrigger render={<Button variant="outline" size="icon" aria-label="More case actions" />}>
							<MoreHorizontalIcon />
						</DropdownMenuTrigger>
						<DropdownMenuContent align="end" className="w-52">
							<DropdownMenuGroup>
								<DropdownMenuLabel>Case 2026-04812</DropdownMenuLabel>
								<DropdownMenuItem
									onClick={() => {
										setReassignTo(officerId)
										setReassignOpen(true)
									}}
								>
									<UserCogIcon />
									Reassign…
								</DropdownMenuItem>
								{status === 'on-hold' ? (
									<DropdownMenuItem
										onClick={() => {
											setStatus('in-progress')
											log(PlayCircleIcon, 'Case resumed')
											setNotice(null)
										}}
									>
										<PlayCircleIcon />
										Resume case
									</DropdownMenuItem>
								) : (
									<DropdownMenuItem
										disabled={decided}
										onClick={() => {
											setStatus('on-hold')
											log(PauseCircleIcon, 'Case put on hold')
											setNotice({ title: 'Case on hold', text: 'The statutory deadline is paused until you resume the case.' })
										}}
									>
										<PauseCircleIcon />
										Put on hold
									</DropdownMenuItem>
								)}
							</DropdownMenuGroup>
							<DropdownMenuSeparator />
							<DropdownMenuGroup>
								<DropdownMenuItem variant="destructive" disabled={status === 'closed'} onClick={() => setCloseOpen(true)}>
									<XCircleIcon />
									Close case…
								</DropdownMenuItem>
							</DropdownMenuGroup>
						</DropdownMenuContent>
					</DropdownMenu>
				</div>
			</header>

			{notice ? (
				<Alert>
					<CheckCircle2Icon />
					<AlertTitle>{notice.title}</AlertTitle>
					<AlertDescription>{notice.text}</AlertDescription>
					<AlertAction>
						<Button variant="ghost" size="sm" onClick={() => setNotice(null)}>
							Dismiss
						</Button>
					</AlertAction>
				</Alert>
			) : null}

			<div className="grid items-start gap-6 @4xl:grid-cols-[minmax(0,1fr)_300px]">
				<Tabs value={tab} onValueChange={(value) => setTab(String(value))} className="min-w-0 gap-4">
					<div className="-mx-4 overflow-x-auto px-4 @3xl:mx-0 @3xl:px-0">
						<TabsList aria-label="Case sections">
							<TabsTrigger value="overview">Overview</TabsTrigger>
							<TabsTrigger value="documents">
								Documents
								<Badge variant="secondary">{documents.length}</Badge>
							</TabsTrigger>
							<TabsTrigger value="messages">
								Messages
								<Badge variant="secondary">{messages.length}</Badge>
							</TabsTrigger>
							<TabsTrigger value="history">History</TabsTrigger>
						</TabsList>
					</div>
					<TabsContent value="overview">
						<OverviewTab stepIndex={stepIndex} />
					</TabsContent>
					<TabsContent value="documents">
						{documents.length ? (
							<DocumentsTab documents={documents} />
						) : (
							<Empty>
								<EmptyHeader>
									<EmptyMedia variant="icon">
										<FileTextIcon />
									</EmptyMedia>
									<EmptyTitle>No documents</EmptyTitle>
									<EmptyDescription>Registered documents appear here.</EmptyDescription>
								</EmptyHeader>
							</Empty>
						)}
					</TabsContent>
					<TabsContent value="messages">
						<MessagesTab messages={messages} officer={officer} onSend={sendMessage} />
					</TabsContent>
					<TabsContent value="history">
						<HistoryTab history={history} />
					</TabsContent>
				</Tabs>

				<CaseAside status={status} />
			</div>

			<DecisionSheet open={decisionOpen} onOpenChange={setDecisionOpen} onDecide={decide} />

			<Dialog
				open={supplementOpen}
				onOpenChange={(open) => {
					setSupplementOpen(open)
					if (!open) setSupplementError(null)
				}}
			>
				<DialogContent className="sm:max-w-md">
					<DialogHeader>
						<DialogTitle>Request supplement</DialogTitle>
						<DialogDescription>
							The request is sent to Anna Lindqvist and the case status changes to “Awaiting supplement”.
						</DialogDescription>
					</DialogHeader>
					<Field data-invalid={supplementError ? true : undefined}>
						<FieldLabel htmlFor={`${id}-supplement`}>What is missing?</FieldLabel>
						<Textarea
							id={`${id}-supplement`}
							rows={4}
							value={supplementText}
							onChange={(event) => {
								setSupplementText(event.target.value)
								setSupplementError(null)
							}}
							placeholder="Please send a sun study for 20 March and 21 June by 14 May 2026."
							aria-invalid={supplementError ? true : undefined}
							aria-describedby={supplementError ? `${id}-supplement-error` : undefined}
						/>
						<FieldError id={`${id}-supplement-error`}>{supplementError}</FieldError>
					</Field>
					<DialogFooter>
						<DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>
						<Button onClick={requestSupplement}>
							<SendIcon data-icon="inline-start" />
							Send request
						</Button>
					</DialogFooter>
				</DialogContent>
			</Dialog>

			<Dialog open={reassignOpen} onOpenChange={setReassignOpen}>
				<DialogContent>
					<DialogHeader>
						<DialogTitle>Reassign case</DialogTitle>
						<DialogDescription>The new handler is notified and the change is logged in the history.</DialogDescription>
					</DialogHeader>
					<Field>
						<FieldLabel htmlFor={`${id}-officer`}>Handler</FieldLabel>
						<Select value={reassignTo} onValueChange={(value) => setReassignTo(value)} items={officerItems}>
							<SelectTrigger id={`${id}-officer`} className="w-full">
								<SelectValue />
							</SelectTrigger>
							<SelectContent>
								{officers.map((o) => (
									<SelectItem key={o.id} value={o.id}>
										{o.name}
									</SelectItem>
								))}
							</SelectContent>
						</Select>
					</Field>
					<DialogFooter>
						<DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>
						<Button onClick={reassign}>Reassign</Button>
					</DialogFooter>
				</DialogContent>
			</Dialog>

			<AlertDialog open={closeOpen} onOpenChange={setCloseOpen}>
				<AlertDialogContent>
					<AlertDialogHeader>
						<AlertDialogTitle>Close case 2026-04812 without a decision?</AlertDialogTitle>
						<AlertDialogDescription>
							Use this when the applicant withdraws the application. The case is archived and can be reopened by the registry.
						</AlertDialogDescription>
					</AlertDialogHeader>
					<AlertDialogFooter>
						<AlertDialogCancel>Keep open</AlertDialogCancel>
						<AlertDialogAction
							variant="destructive"
							onClick={() => {
								setStatus('closed')
								log(XCircleIcon, 'Case closed without decision')
								setNotice({ title: 'Case closed', text: 'The case has been archived.' })
							}}
						>
							Close case
						</AlertDialogAction>
					</AlertDialogFooter>
				</AlertDialogContent>
			</AlertDialog>
		</div>
	)
}