rapidUI

Examples

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

Case management

A case officer’s overview: navigation, workload at a glance, a weekly chart and the case list.

Good morning, Sofia

Tuesday 29 September 2026 · 5 open cases, 1 overdue

Open cases

5

Increase, bad: 2 more than last week

Due this week

3

Increase, bad: 1 already overdue

Waiting for resident

1

Decrease, good: 1 fewer than last week

Average handling time

34days

Decrease, good: 3 days faster than August

Received and decided cases

Building and Environment, per week, weeks 30–39 2026

Cases

Mine cases, open cases first, by due date
CaseStatusAssigneeDueActions
Demolition of barn, Lillgården 2
In progress
SBSofia Berg (you)
26 Sept· 3 days overdue
Carport, Storgatan 14
In progress
SBSofia Berg (you)
30 Sept· tomorrow
Illuminated sign on facade, Torget 1
Received
SBSofia Berg (you)
2 Oct· in 3 days
Extension of single-family house, Björkvägen 7
Waiting for resident
SBSofia Berg (you)
14 Oct
Change of use, office to housing, Kungsgatan 3
Received
SBSofia Berg (you)
27 Oct
Solar panels on listed building, Kyrkogatan 9
Decided
SBSofia Berg (you)
22 Sept

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

import {
	AlertTriangleIcon,
	BellIcon,
	Building2Icon,
	ChevronsUpDownIcon,
	CircleCheckIcon,
	ClockIcon,
	FileTextIcon,
	FolderOpenIcon,
	GavelIcon,
	HomeIcon,
	HourglassIcon,
	InboxIcon,
	LandmarkIcon,
	LeafIcon,
	ListFilterIcon,
	LogOutIcon,
	MoreHorizontalIcon,
	SearchIcon,
	SearchXIcon,
	SettingsIcon,
	UserIcon,
	UserPlusIcon,
	UsersIcon,
	XIcon,
} from 'lucide-react'
import * as React from 'react'
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts'
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 { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import {
	type ChartConfig,
	ChartContainer,
	ChartLegend,
	ChartLegendContent,
	ChartTooltip,
	ChartTooltipContent,
} from '@/components/ui/chart'
import {
	Command,
	CommandDialog,
	CommandEmpty,
	CommandGroup,
	CommandInput,
	CommandItem,
	CommandList,
	CommandSeparator,
} from '@/components/ui/command'
import {
	DropdownMenu,
	DropdownMenuContent,
	DropdownMenuGroup,
	DropdownMenuItem,
	DropdownMenuLabel,
	DropdownMenuRadioGroup,
	DropdownMenuRadioItem,
	DropdownMenuSeparator,
	DropdownMenuSub,
	DropdownMenuSubContent,
	DropdownMenuSubTrigger,
	DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import {
	type Organization,
	OrganizationSwitcher,
	OrganizationSwitcherContent,
	OrganizationSwitcherItem,
	OrganizationSwitcherLabel,
	OrganizationSwitcherList,
	OrganizationSwitcherTrigger,
	OrganizationSwitcherValue,
} from '@/components/ui/organization-switcher'
import {
	Select,
	SelectContent,
	SelectGroup,
	SelectItem,
	SelectSeparator,
	SelectTrigger,
	SelectValue,
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet'
import {
	Sidebar,
	SidebarContent,
	SidebarFooter,
	SidebarGroup,
	SidebarGroupContent,
	SidebarGroupLabel,
	SidebarHeader,
	SidebarMenu,
	SidebarMenuBadge,
	SidebarMenuButton,
	SidebarMenuItem,
	SidebarProvider,
	SidebarRail,
	SidebarTrigger,
} from '@/components/ui/sidebar'
import {
	StatCard,
	StatCardFooter,
	StatCardIcon,
	StatCardLabel,
	StatCardTrend,
	StatCardUnit,
	StatCardValue,
} from '@/components/ui/stat-card'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'

/* -------------------------------------------------------------------------------------------------
 * Data
 * -----------------------------------------------------------------------------------------------*/

/** The example is frozen on this date so due-date warnings always look the same. */
const TODAY = new Date(2026, 8, 29)
type OfficerId = 'sofia' | 'mikko' | 'omar' | 'ingrid' | 'anna'

/** The signed-in case officer */
const ME: OfficerId = 'sofia'

const OFFICERS: Record<OfficerId, { name: string; initials: string; unit: string; team: boolean }> = {
	sofia: { name: 'Sofia Berg', initials: 'SB', unit: 'Building permits', team: true },
	mikko: { name: 'Mikko Virtanen', initials: 'MV', unit: 'Building permits', team: true },
	omar: { name: 'Omar Haddad', initials: 'OH', unit: 'Building permits', team: true },
	ingrid: { name: 'Ingrid Hansen', initials: 'IH', unit: 'Environment and health', team: false },
	anna: { name: 'Anna Lindqvist', initials: 'AL', unit: 'Environment and health', team: false },
}

const OFFICER_IDS = Object.keys(OFFICERS) as OfficerId[]

type Status = 'new' | 'in-progress' | 'waiting' | 'decided'

const STATUS: Record<
	Status,
	{
		label: string
		icon: React.ComponentType<React.SVGProps<SVGSVGElement>>
		variant: 'default' | 'secondary' | 'outline'
		className?: string
	}
> = {
	new: { label: 'Received', icon: InboxIcon, variant: 'default', className: 'bg-primary/10 text-primary' },
	'in-progress': { label: 'In progress', icon: ClockIcon, variant: 'secondary' },
	waiting: { label: 'Waiting for resident', icon: HourglassIcon, variant: 'outline' },
	decided: { label: 'Decided', icon: CircleCheckIcon, variant: 'default' },
}

const STATUS_IDS = Object.keys(STATUS) as Status[]

const statusFilterItems: Record<string, string> = {
	all: 'All statuses',
	...Object.fromEntries(STATUS_IDS.map((id) => [id, STATUS[id].label])),
}

type Case = {
	id: string
	title: string
	type: string
	applicant: string
	address: string
	status: Status
	assignee: OfficerId
	received: string
	due: string
}

const INITIAL_CASES: Case[] = [
	{
		id: '2026-04812',
		title: 'Carport',
		type: 'Building permit',
		applicant: 'Erik Johansson',
		address: 'Storgatan 14',
		status: 'in-progress',
		assignee: 'sofia',
		received: '2026-08-31',
		due: '2026-09-30',
	},
	{
		id: '2026-04877',
		title: 'Extension of single-family house',
		type: 'Building permit',
		applicant: 'Karin Holm',
		address: 'Björkvägen 7',
		status: 'waiting',
		assignee: 'sofia',
		received: '2026-09-08',
		due: '2026-10-14',
	},
	{
		id: '2026-04901',
		title: 'Change of use, office to housing',
		type: 'Building permit',
		applicant: 'Fastighets AB Ekdalen',
		address: 'Kungsgatan 3',
		status: 'new',
		assignee: 'sofia',
		received: '2026-09-24',
		due: '2026-10-27',
	},
	{
		id: '2026-04655',
		title: 'Demolition of barn',
		type: 'Demolition notice',
		applicant: 'Lars Nilsson',
		address: 'Lillgården 2',
		status: 'in-progress',
		assignee: 'sofia',
		received: '2026-08-12',
		due: '2026-09-26',
	},
	{
		id: '2026-04930',
		title: 'Illuminated sign on facade',
		type: 'Sign permit',
		applicant: 'Café Kanelen',
		address: 'Torget 1',
		status: 'new',
		assignee: 'sofia',
		received: '2026-09-25',
		due: '2026-10-02',
	},
	{
		id: '2026-04720',
		title: 'Solar panels on listed building',
		type: 'Building permit',
		applicant: 'Maria Ek',
		address: 'Kyrkogatan 9',
		status: 'decided',
		assignee: 'sofia',
		received: '2026-08-20',
		due: '2026-09-22',
	},
	{
		id: '2026-04788',
		title: 'Attefall house',
		type: 'Notification',
		applicant: 'Johan Lund',
		address: 'Ängsvägen 21',
		status: 'in-progress',
		assignee: 'mikko',
		received: '2026-09-02',
		due: '2026-10-01',
	},
	{
		id: '2026-04811',
		title: 'Garage conversion',
		type: 'Building permit',
		applicant: 'Nora Svensson',
		address: 'Tallstigen 5',
		status: 'waiting',
		assignee: 'mikko',
		received: '2026-09-04',
		due: '2026-10-09',
	},
	{
		id: '2026-04699',
		title: 'Balcony glazing',
		type: 'Building permit',
		applicant: 'BRF Kungsgatan 3',
		address: 'Kungsgatan 3',
		status: 'in-progress',
		assignee: 'omar',
		received: '2026-08-17',
		due: '2026-09-28',
	},
	{
		id: '2026-04945',
		title: 'New detached house',
		type: 'Building permit',
		applicant: 'Peter Dahl',
		address: 'Ekdalen 4:12',
		status: 'new',
		assignee: 'omar',
		received: '2026-09-28',
		due: '2026-11-10',
	},
	{
		id: '2026-04602',
		title: 'Private sewage system',
		type: 'Environmental permit',
		applicant: 'Anders Berglund',
		address: 'Sjövik 1:7',
		status: 'waiting',
		assignee: 'ingrid',
		received: '2026-08-05',
		due: '2026-10-05',
	},
	{
		id: '2026-04760',
		title: 'Food business registration',
		type: 'Food registration',
		applicant: 'Pizzeria Roma',
		address: 'Storgatan 22',
		status: 'in-progress',
		assignee: 'ingrid',
		received: '2026-09-01',
		due: '2026-10-03',
	},
	{
		id: '2026-04588',
		title: 'Noise complaint, fans',
		type: 'Complaint',
		applicant: 'Lena Åberg',
		address: 'Industrivägen 8',
		status: 'decided',
		assignee: 'anna',
		received: '2026-07-30',
		due: '2026-09-18',
	},
	{
		id: '2026-04915',
		title: 'Ground-source heat pump',
		type: 'Notification',
		applicant: 'Olof Strand',
		address: 'Fjällgatan 11',
		status: 'new',
		assignee: 'anna',
		received: '2026-09-26',
		due: '2026-10-20',
	},
]

const weeklyData = [
	{ week: 'Week 30', received: 18, decided: 15 },
	{ week: 'Week 31', received: 22, decided: 17 },
	{ week: 'Week 32', received: 27, decided: 19 },
	{ week: 'Week 33', received: 31, decided: 24 },
	{ week: 'Week 34', received: 29, decided: 28 },
	{ week: 'Week 35', received: 34, decided: 26 },
	{ week: 'Week 36', received: 30, decided: 31 },
	{ week: 'Week 37', received: 28, decided: 33 },
	{ week: 'Week 38', received: 32, decided: 29 },
	{ week: 'Week 39', received: 26, decided: 30 },
]

const chartConfig = {
	received: { label: 'Received', color: 'var(--chart-1)' },
	decided: { label: 'Decided', color: 'var(--chart-2)' },
} satisfies ChartConfig

// A flat list: the departments are sub-organisations of the municipality
const ORGANIZATIONS: Organization[] = [
	{ id: 'ekdalen', name: 'Ekdalen Municipality', description: 'All departments', logo: <LandmarkIcon /> },
	{ id: 'building-environment', name: 'Building and Environment', parentId: 'ekdalen' },
	{ id: 'social-services', name: 'Social Services', parentId: 'ekdalen' },
	{ id: 'education', name: 'Education', parentId: 'ekdalen' },
]

type TabValue = 'mine' | 'team' | 'all'
type NavId = 'overview' | 'mine' | 'team' | 'inbox' | 'decisions' | 'overdue' | 'waiting' | 'received'

/* -------------------------------------------------------------------------------------------------
 * Helpers
 * -----------------------------------------------------------------------------------------------*/

function parseDate(value: string) {
	const [year, month, day] = value.split('-').map(Number)
	return new Date(year, month - 1, day)
}

function daysUntil(value: string) {
	return Math.round((parseDate(value).getTime() - TODAY.getTime()) / 86_400_000)
}

const dateFormat = new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'short' })

function formatDate(value: string) {
	return dateFormat.format(parseDate(value))
}

function isOpen(item: Case) {
	return item.status !== 'decided'
}

function isOverdue(item: Case) {
	return isOpen(item) && daysUntil(item.due) < 0
}

function inTab(item: Case, tab: TabValue) {
	if (tab === 'mine') return item.assignee === ME
	if (tab === 'team') return OFFICERS[item.assignee].team
	return true
}

/* -------------------------------------------------------------------------------------------------
 * Small pieces
 * -----------------------------------------------------------------------------------------------*/

function StatusBadge({ status }: { status: Status }) {
	const config = STATUS[status]
	const Icon = config.icon
	return (
		<Badge variant={config.variant} className={config.className}>
			<Icon data-icon="inline-start" aria-hidden="true" />
			{config.label}
		</Badge>
	)
}

function DueDate({ item }: { item: Case }) {
	const days = daysUntil(item.due)
	const date = formatDate(item.due)

	if (!isOpen(item)) {
		return <span className="text-muted-foreground">{date}</span>
	}
	if (days < 0) {
		return (
			<span className="inline-flex items-center gap-1.5 font-medium text-destructive">
				<AlertTriangleIcon className="size-4" aria-hidden="true" />
				{date}
				<span className="font-normal">· {Math.abs(days) === 1 ? '1 day overdue' : `${Math.abs(days)} days overdue`}</span>
			</span>
		)
	}
	if (days <= 3) {
		return (
			<span className="inline-flex items-center gap-1.5">
				<ClockIcon className="size-4 text-muted-foreground" aria-hidden="true" />
				{date}
				<span className="text-muted-foreground">
					· {days === 0 ? 'due today' : days === 1 ? 'tomorrow' : `in ${days} days`}
				</span>
			</span>
		)
	}
	return <span>{date}</span>
}

function OfficerAvatar({ id, size = 'sm' }: { id: OfficerId; size?: 'sm' | 'default' }) {
	return (
		<Avatar size={size}>
			<AvatarFallback>{OFFICERS[id].initials}</AvatarFallback>
		</Avatar>
	)
}

function Stat({
	label,
	value,
	unit,
	trend,
	goodDirection,
	trendLabel,
	icon: Icon,
}: {
	label: string
	value: React.ReactNode
	unit?: string
	trend: 'up' | 'down'
	goodDirection: 'up' | 'down' | 'none'
	trendLabel: string
	icon: React.ComponentType<React.SVGProps<SVGSVGElement>>
}) {
	return (
		<StatCard>
			<StatCardLabel>{label}</StatCardLabel>
			<StatCardIcon>
				<Icon />
			</StatCardIcon>
			<StatCardValue>
				{value}
				{unit ? <StatCardUnit>{unit}</StatCardUnit> : null}
			</StatCardValue>
			<StatCardFooter>
				<StatCardTrend direction={trend} goodDirection={goodDirection}>
					{trendLabel}
				</StatCardTrend>
			</StatCardFooter>
		</StatCard>
	)
}

/* -------------------------------------------------------------------------------------------------
 * Sidebar
 * -----------------------------------------------------------------------------------------------*/

function CaseSidebar({
	organizationId,
	onOrganizationChange,
	activeNav,
	onNavigate,
	counts,
}: {
	organizationId: string
	onOrganizationChange: (value: string) => void
	activeNav: NavId
	onNavigate: (id: NavId) => void
	counts: Record<'mine' | 'team' | 'overdue' | 'waiting' | 'received', number>
}) {
	const workspace: { id: NavId; title: string; icon: React.ComponentType; badge?: number }[] = [
		{ id: 'overview', title: 'Overview', icon: HomeIcon },
		{ id: 'mine', title: 'My cases', icon: FolderOpenIcon, badge: counts.mine },
		{ id: 'team', title: 'Team cases', icon: UsersIcon, badge: counts.team },
		{ id: 'inbox', title: 'Inbox', icon: InboxIcon, badge: 4 },
		{ id: 'decisions', title: 'Decisions', icon: GavelIcon },
	]
	const views: { id: NavId; title: string; icon: React.ComponentType; badge: number }[] = [
		{ id: 'overdue', title: 'Overdue', icon: AlertTriangleIcon, badge: counts.overdue },
		{ id: 'waiting', title: 'Waiting for resident', icon: HourglassIcon, badge: counts.waiting },
		{ id: 'received', title: 'Received, not started', icon: InboxIcon, badge: counts.received },
	]

	return (
		// On a real page, drop className: the sidebar is fixed to the window. Here it stays inside the example frame.
		<Sidebar collapsible="icon" className="absolute h-full">
			<SidebarHeader>
				<SidebarMenu>
					<SidebarMenuItem>
						<OrganizationSwitcher items={ORGANIZATIONS} value={organizationId} onValueChange={onOrganizationChange}>
							<OrganizationSwitcherTrigger aria-label="Organisation" render={<SidebarMenuButton size="lg" />}>
								<OrganizationSwitcherValue />
							</OrganizationSwitcherTrigger>
							<OrganizationSwitcherContent>
								<OrganizationSwitcherLabel>Switch department</OrganizationSwitcherLabel>
								<OrganizationSwitcherList>
									{(organization) => (
										<OrganizationSwitcherItem
											key={organization.id}
											value={organization.id}
											level={organization.depth}
											path={organization.path}
											description={organization.description}
										>
											{organization.name}
										</OrganizationSwitcherItem>
									)}
								</OrganizationSwitcherList>
							</OrganizationSwitcherContent>
						</OrganizationSwitcher>
					</SidebarMenuItem>
				</SidebarMenu>
			</SidebarHeader>

			<SidebarContent>
				<nav aria-label="Case management">
					<SidebarGroup>
						<SidebarGroupLabel>Workspace</SidebarGroupLabel>
						<SidebarGroupContent>
							<SidebarMenu>
								{workspace.map((item) => (
									<SidebarMenuItem key={item.id}>
										<SidebarMenuButton
											tooltip={item.title}
											isActive={activeNav === item.id}
											aria-current={activeNav === item.id ? 'page' : undefined}
											onClick={() => onNavigate(item.id)}
										>
											<item.icon />
											<span>{item.title}</span>
										</SidebarMenuButton>
										{item.badge ? <SidebarMenuBadge>{item.badge}</SidebarMenuBadge> : null}
									</SidebarMenuItem>
								))}
							</SidebarMenu>
						</SidebarGroupContent>
					</SidebarGroup>

					<SidebarGroup>
						<SidebarGroupLabel>Saved views</SidebarGroupLabel>
						<SidebarGroupContent>
							<SidebarMenu>
								{views.map((item) => (
									<SidebarMenuItem key={item.id}>
										<SidebarMenuButton
											tooltip={item.title}
											isActive={activeNav === item.id}
											aria-current={activeNav === item.id ? 'page' : undefined}
											onClick={() => onNavigate(item.id)}
										>
											<item.icon />
											<span>{item.title}</span>
										</SidebarMenuButton>
										<SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
									</SidebarMenuItem>
								))}
							</SidebarMenu>
						</SidebarGroupContent>
					</SidebarGroup>

					<SidebarGroup>
						<SidebarGroupLabel>Registers</SidebarGroupLabel>
						<SidebarGroupContent>
							<SidebarMenu>
								{[
									{ title: 'Building permits', icon: Building2Icon },
									{ title: 'Environment and health', icon: LeafIcon },
									{ title: 'Documents', icon: FileTextIcon },
								].map((item) => (
									<SidebarMenuItem key={item.title}>
										<SidebarMenuButton tooltip={item.title}>
											<item.icon />
											<span>{item.title}</span>
										</SidebarMenuButton>
									</SidebarMenuItem>
								))}
							</SidebarMenu>
						</SidebarGroupContent>
					</SidebarGroup>
				</nav>
			</SidebarContent>

			<SidebarFooter>
				<SidebarMenu>
					<SidebarMenuItem>
						<DropdownMenu>
							<DropdownMenuTrigger render={<SidebarMenuButton size="lg" aria-label="Sofia Berg, account menu" />}>
								<Avatar className="rounded-lg after:rounded-lg">
									<AvatarFallback className="rounded-lg">SB</AvatarFallback>
								</Avatar>
								<div className="grid flex-1 text-left leading-tight">
									<span className="truncate font-medium">Sofia Berg</span>
									<span className="truncate text-xs text-muted-foreground">Case officer, Building permits</span>
								</div>
								<ChevronsUpDownIcon className="ml-auto" aria-hidden="true" />
							</DropdownMenuTrigger>
							<DropdownMenuContent side="top" className="w-60">
								<DropdownMenuGroup>
									<DropdownMenuLabel className="text-muted-foreground">sofia.berg@ekdalen.se</DropdownMenuLabel>
									<DropdownMenuItem>
										<UserIcon aria-hidden="true" />
										Profile
									</DropdownMenuItem>
									<DropdownMenuItem>
										<BellIcon aria-hidden="true" />
										Notifications
									</DropdownMenuItem>
									<DropdownMenuItem>
										<SettingsIcon aria-hidden="true" />
										Settings
									</DropdownMenuItem>
								</DropdownMenuGroup>
								<DropdownMenuSeparator />
								<DropdownMenuItem>
									<LogOutIcon aria-hidden="true" />
									Sign out
								</DropdownMenuItem>
							</DropdownMenuContent>
						</DropdownMenu>
					</SidebarMenuItem>
				</SidebarMenu>
			</SidebarFooter>
			<SidebarRail />
		</Sidebar>
	)
}

/* -------------------------------------------------------------------------------------------------
 * Case table
 * -----------------------------------------------------------------------------------------------*/

function CaseTable({
	rows,
	caption,
	onOpen,
	onAssign,
	onStatusChange,
	onClearFilters,
}: {
	rows: Case[]
	caption: string
	onOpen: (id: string) => void
	onAssign: (id: string, officer: OfficerId) => void
	onStatusChange: (id: string, status: Status) => void
	onClearFilters: () => void
}) {
	if (rows.length === 0) {
		return (
			<Empty className="rounded-lg border border-dashed py-10">
				<EmptyHeader>
					<EmptyMedia variant="icon">
						<SearchXIcon aria-hidden="true" />
					</EmptyMedia>
					<EmptyTitle>No cases match</EmptyTitle>
					<EmptyDescription>Try another status or clear the filters.</EmptyDescription>
				</EmptyHeader>
				<Button variant="outline" onClick={onClearFilters}>
					Clear filters
				</Button>
			</Empty>
		)
	}

	return (
		<div className="overflow-hidden rounded-lg border">
			<Table>
				<caption className="sr-only">{caption}</caption>
				<TableHeader>
					<TableRow>
						<TableHead className="pl-4">Case</TableHead>
						<TableHead className="hidden @4xl/main:table-cell">Applicant</TableHead>
						<TableHead>Status</TableHead>
						<TableHead>Assignee</TableHead>
						<TableHead>Due</TableHead>
						<TableHead className="w-12 pr-4">
							<span className="sr-only">Actions</span>
						</TableHead>
					</TableRow>
				</TableHeader>
				<TableBody>
					{rows.map((item) => (
						<TableRow key={item.id}>
							<TableCell className="pl-4">
								<div className="flex flex-col gap-0.5">
									<button
										type="button"
										onClick={() => onOpen(item.id)}
										className="w-fit rounded-sm text-left font-medium tabular-nums underline-offset-4 outline-none hover:text-primary hover:underline focus-visible:ring-3 focus-visible:ring-ring"
									>
										{item.id}
									</button>
									<span className="text-muted-foreground">
										{item.title}, {item.address}
									</span>
								</div>
							</TableCell>
							<TableCell className="hidden @4xl/main:table-cell">
								<div className="flex flex-col gap-0.5">
									<span>{item.applicant}</span>
									<span className="text-muted-foreground">{item.type}</span>
								</div>
							</TableCell>
							<TableCell>
								<StatusBadge status={item.status} />
							</TableCell>
							<TableCell>
								<div className="flex items-center gap-2">
									<OfficerAvatar id={item.assignee} />
									<span>
										{OFFICERS[item.assignee].name}
										{item.assignee === ME ? <span className="text-muted-foreground"> (you)</span> : null}
									</span>
								</div>
							</TableCell>
							<TableCell>
								<DueDate item={item} />
							</TableCell>
							<TableCell className="pr-4 text-right">
								<RowActions item={item} onOpen={onOpen} onAssign={onAssign} onStatusChange={onStatusChange} />
							</TableCell>
						</TableRow>
					))}
				</TableBody>
			</Table>
		</div>
	)
}

function RowActions({
	item,
	onOpen,
	onAssign,
	onStatusChange,
}: {
	item: Case
	onOpen: (id: string) => void
	onAssign: (id: string, officer: OfficerId) => void
	onStatusChange: (id: string, status: Status) => void
}) {
	return (
		<DropdownMenu>
			<DropdownMenuTrigger render={<Button variant="ghost" size="icon-sm" aria-label={`Actions for case ${item.id}`} />}>
				<MoreHorizontalIcon aria-hidden="true" />
			</DropdownMenuTrigger>
			<DropdownMenuContent align="end" className="w-52">
				<DropdownMenuGroup>
					<DropdownMenuLabel className="text-muted-foreground">Case {item.id}</DropdownMenuLabel>
					<DropdownMenuItem onClick={() => onOpen(item.id)}>
						<FolderOpenIcon aria-hidden="true" />
						Open
					</DropdownMenuItem>
					<DropdownMenuSub>
						<DropdownMenuSubTrigger>
							<UserPlusIcon aria-hidden="true" />
							Assign
						</DropdownMenuSubTrigger>
						<DropdownMenuSubContent className="w-56">
							<DropdownMenuRadioGroup value={item.assignee} onValueChange={(value) => onAssign(item.id, value as OfficerId)}>
								<DropdownMenuLabel className="text-muted-foreground">Case officer</DropdownMenuLabel>
								{OFFICER_IDS.map((id) => (
									<DropdownMenuRadioItem key={id} value={id}>
										{OFFICERS[id].name}
										{id === ME ? ' (you)' : ''}
									</DropdownMenuRadioItem>
								))}
							</DropdownMenuRadioGroup>
						</DropdownMenuSubContent>
					</DropdownMenuSub>
					<DropdownMenuSub>
						<DropdownMenuSubTrigger>
							<ListFilterIcon aria-hidden="true" />
							Change status
						</DropdownMenuSubTrigger>
						<DropdownMenuSubContent className="w-56">
							<DropdownMenuRadioGroup value={item.status} onValueChange={(value) => onStatusChange(item.id, value as Status)}>
								<DropdownMenuLabel className="text-muted-foreground">Status</DropdownMenuLabel>
								{STATUS_IDS.map((id) => (
									<DropdownMenuRadioItem key={id} value={id}>
										{STATUS[id].label}
									</DropdownMenuRadioItem>
								))}
							</DropdownMenuRadioGroup>
						</DropdownMenuSubContent>
					</DropdownMenuSub>
				</DropdownMenuGroup>
			</DropdownMenuContent>
		</DropdownMenu>
	)
}

/* -------------------------------------------------------------------------------------------------
 * Screen
 * -----------------------------------------------------------------------------------------------*/

const TAB_LABELS: Record<TabValue, string> = { mine: 'Mine', team: 'Team', all: 'All' }

export default function CaseManagementExample() {
	const id = React.useId()
	const [cases, setCases] = React.useState(INITIAL_CASES)
	const [organizationId, setOrganizationId] = React.useState('building-environment')
	const [activeNav, setActiveNav] = React.useState<NavId>('mine')
	const [tab, setTab] = React.useState<TabValue>('mine')
	const [status, setStatus] = React.useState('all')
	const [overdueOnly, setOverdueOnly] = React.useState(false)
	const [assigneeFilter, setAssigneeFilter] = React.useState<OfficerId | null>(null)
	const [searchOpen, setSearchOpen] = React.useState(false)
	// The id is kept after closing so the sheet keeps its content while it animates out
	const [openCaseId, setOpenCaseId] = React.useState<string | null>(null)
	const [sheetOpen, setSheetOpen] = React.useState(false)
	const [announcement, setAnnouncement] = React.useState('')

	// ⌘K / Ctrl K opens the search. The docs site has its own ⌘K, so this one only reacts while focus is in the
	// example (or in its open search). In an app, drop the check and listen on `document` without capture.
	const rootRef = React.useRef<HTMLDivElement>(null)
	const searchOpenRef = React.useRef(searchOpen)
	React.useEffect(() => {
		searchOpenRef.current = searchOpen
	}, [searchOpen])
	React.useEffect(() => {
		function onKeyDown(event: KeyboardEvent) {
			if (event.key.toLowerCase() !== 'k' || !(event.metaKey || event.ctrlKey)) return
			const inside = event.target instanceof Node && rootRef.current?.contains(event.target)
			if (!inside && !searchOpenRef.current) return
			event.preventDefault()
			event.stopPropagation()
			setSearchOpen((current) => !current)
		}
		document.addEventListener('keydown', onKeyDown, true)
		return () => document.removeEventListener('keydown', onKeyDown, true)
	}, [])

	const matchesFilters = (item: Case) =>
		(status === 'all' || item.status === status) &&
		(!overdueOnly || isOverdue(item)) &&
		(!assigneeFilter || item.assignee === assigneeFilter)

	const rowsFor = (value: TabValue) =>
		cases
			.filter((item) => inTab(item, value) && matchesFilters(item))
			.sort((a, b) => Number(!isOpen(a)) - Number(!isOpen(b)) || a.due.localeCompare(b.due))

	const mine = cases.filter((item) => item.assignee === ME)
	const mineOpen = mine.filter(isOpen)
	const teamOpen = cases.filter((item) => OFFICERS[item.assignee].team && isOpen(item))
	const dueThisWeek = mineOpen.filter((item) => daysUntil(item.due) <= 5)
	const overdueMine = mineOpen.filter(isOverdue)

	const counts = {
		mine: mineOpen.length,
		team: teamOpen.length,
		overdue: cases.filter(isOverdue).length,
		waiting: teamOpen.filter((item) => item.status === 'waiting').length,
		received: teamOpen.filter((item) => item.status === 'new').length,
	}

	const openCase = cases.find((item) => item.id === openCaseId) ?? null

	function showCase(caseId: string) {
		setOpenCaseId(caseId)
		setSheetOpen(true)
	}

	function clearFilters() {
		setStatus('all')
		setOverdueOnly(false)
		setAssigneeFilter(null)
	}

	function navigate(target: NavId) {
		setActiveNav(target)
		clearFilters()
		if (target === 'mine' || target === 'overview') setTab('mine')
		if (target === 'team') setTab('team')
		if (target === 'overdue') {
			setTab('all')
			setOverdueOnly(true)
		}
		if (target === 'waiting') {
			setTab('team')
			setStatus('waiting')
		}
		if (target === 'received') {
			setTab('team')
			setStatus('new')
		}
	}

	function changeTab(value: TabValue) {
		setTab(value)
		setActiveNav(value === 'all' ? 'overview' : value)
	}

	function assign(caseId: string, officer: OfficerId) {
		setCases((current) => current.map((item) => (item.id === caseId ? { ...item, assignee: officer } : item)))
		setAnnouncement(`Case ${caseId} assigned to ${OFFICERS[officer].name}.`)
	}

	function changeStatus(caseId: string, next: Status) {
		setCases((current) => current.map((item) => (item.id === caseId ? { ...item, status: next } : item)))
		setAnnouncement(`Case ${caseId} is now ${STATUS[next].label.toLowerCase()}.`)
	}

	const activeFilterCount = (status !== 'all' ? 1 : 0) + (overdueOnly ? 1 : 0) + (assigneeFilter ? 1 : 0)

	return (
		<SidebarProvider className="relative min-h-[760px] overflow-hidden bg-background" ref={rootRef}>
			<CaseSidebar
				organizationId={organizationId}
				onOrganizationChange={setOrganizationId}
				activeNav={activeNav}
				onNavigate={navigate}
				counts={counts}
			/>

			{/* In an app, use SidebarInset here: it renders <main>, and the docs page already has one. */}
			<div className="@container/main relative flex w-full min-w-0 flex-1 flex-col bg-background">
				<header className="flex h-14 shrink-0 items-center gap-2 border-b px-4">
					<SidebarTrigger className="-ml-1" aria-label="Toggle navigation" />
					<Separator orientation="vertical" className="mr-1 h-4" />
					<Breadcrumb className="hidden min-w-0 @xl/main:block">
						<BreadcrumbList>
							<BreadcrumbItem>
								<BreadcrumbLink href="#" onClick={(event) => event.preventDefault()}>
									{ORGANIZATIONS.find((organization) => organization.id === organizationId)?.name}
								</BreadcrumbLink>
							</BreadcrumbItem>
							<BreadcrumbSeparator />
							<BreadcrumbItem>
								<BreadcrumbPage>Case overview</BreadcrumbPage>
							</BreadcrumbItem>
						</BreadcrumbList>
					</Breadcrumb>
					<div className="ml-auto flex items-center gap-2">
						<Button
							variant="outline"
							className="justify-start text-muted-foreground @2xl/main:w-72"
							onClick={() => setSearchOpen(true)}
						>
							<SearchIcon data-icon="inline-start" aria-hidden="true" />
							<span>Search cases and people</span>
							<KbdGroup className="ml-auto hidden @2xl/main:inline-flex" aria-hidden="true">
								<Kbd>⌘</Kbd>
								<Kbd>K</Kbd>
							</KbdGroup>
						</Button>
						<Button variant="ghost" size="icon" aria-label="Notifications, 3 unread" className="relative">
							<BellIcon aria-hidden="true" />
							<span className="absolute top-2 right-2 size-2 rounded-full bg-primary ring-2 ring-background" />
						</Button>
					</div>
				</header>

				<div className="flex flex-1 flex-col gap-6 p-4 @3xl/main:p-6">
					<div className="flex flex-col gap-1">
						<h3 className="font-heading text-xl font-semibold">Good morning, Sofia</h3>
						<p className="text-muted-foreground">
							Tuesday 29 September 2026 · {mineOpen.length} open cases, {overdueMine.length} overdue
						</p>
					</div>

					<section aria-label="Workload" className="grid grid-cols-2 gap-4 @4xl/main:grid-cols-4">
						<Stat
							label="Open cases"
							value={mineOpen.length}
							icon={FolderOpenIcon}
							trend="up"
							goodDirection="down"
							trendLabel="2 more than last week"
						/>
						<Stat
							label="Due this week"
							value={dueThisWeek.length}
							icon={ClockIcon}
							trend="up"
							goodDirection="down"
							trendLabel={`${overdueMine.length} already overdue`}
						/>
						<Stat
							label="Waiting for resident"
							value={mine.filter((item) => item.status === 'waiting').length}
							icon={HourglassIcon}
							trend="down"
							goodDirection="down"
							trendLabel="1 fewer than last week"
						/>
						<Stat
							label="Average handling time"
							value={34}
							unit="days"
							icon={GavelIcon}
							trend="down"
							goodDirection="down"
							trendLabel="3 days faster than August"
						/>
					</section>

					<Card>
						<CardHeader>
							<CardTitle as="h4">Received and decided cases</CardTitle>
							<CardDescription>Building and Environment, per week, weeks 30–39 2026</CardDescription>
						</CardHeader>
						<CardContent>
							<ChartContainer
								config={chartConfig}
								className="aspect-auto h-56 w-full"
								role="img"
								aria-label="Bar chart of received and decided cases per week. Decisions have outpaced new cases in three of the last four weeks; in week 39, 26 cases were received and 30 decided."
							>
								<BarChart accessibilityLayer data={weeklyData} margin={{ left: -16 }}>
									<CartesianGrid vertical={false} />
									<XAxis
										dataKey="week"
										tickLine={false}
										axisLine={false}
										tickMargin={8}
										tickFormatter={(value: string) => value.replace('Week ', 'W')}
									/>
									<YAxis tickLine={false} axisLine={false} tickMargin={8} width={40} />
									<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="dashed" />} />
									<ChartLegend itemSorter={null} content={<ChartLegendContent />} />
									<Bar dataKey="received" fill="var(--color-received)" radius={4} />
									<Bar dataKey="decided" fill="var(--color-decided)" radius={4} />
								</BarChart>
							</ChartContainer>
						</CardContent>
					</Card>

					<section aria-labelledby={`${id}-cases`} className="flex flex-col gap-4">
						<h4 id={`${id}-cases`} className="font-heading text-base font-medium">
							Cases
						</h4>
						<Tabs value={tab} onValueChange={(value) => changeTab(value as TabValue)} className="gap-4">
							<div className="flex flex-col gap-4 @3xl/main:flex-row @3xl/main:items-end @3xl/main:justify-between">
								<TabsList aria-label="Whose cases">
									{(Object.keys(TAB_LABELS) as TabValue[]).map((value) => (
										<TabsTrigger key={value} value={value}>
											{TAB_LABELS[value]}
											<Badge className="tabular-nums">{rowsFor(value).length}</Badge>
										</TabsTrigger>
									))}
								</TabsList>
								<Select
									value={status}
									onValueChange={(value) => {
										setStatus(String(value ?? 'all'))
									}}
									items={statusFilterItems}
								>
									<SelectTrigger aria-label="Filter by status" className="w-full @3xl/main:w-56">
										<ListFilterIcon aria-hidden="true" />
										<SelectValue />
									</SelectTrigger>
									<SelectContent>
										<SelectGroup>
											<SelectItem value="all">All statuses</SelectItem>
										</SelectGroup>
										<SelectSeparator />
										<SelectGroup>
											{STATUS_IDS.map((value) => (
												<SelectItem key={value} value={value}>
													{STATUS[value].label}
												</SelectItem>
											))}
										</SelectGroup>
									</SelectContent>
								</Select>
							</div>

							{overdueOnly || assigneeFilter ? (
								<div className="flex flex-wrap items-center gap-2">
									<span className="text-muted-foreground">Also filtered by</span>
									{overdueOnly ? (
										<Button variant="outline" size="sm" onClick={() => setOverdueOnly(false)} aria-label="Remove filter: overdue">
											Overdue
											<XIcon data-icon="inline-end" aria-hidden="true" />
										</Button>
									) : null}
									{assigneeFilter ? (
										<Button
											variant="outline"
											size="sm"
											onClick={() => setAssigneeFilter(null)}
											aria-label={`Remove filter: assignee ${OFFICERS[assigneeFilter].name}`}
										>
											Assignee: {OFFICERS[assigneeFilter].name}
											<XIcon data-icon="inline-end" aria-hidden="true" />
										</Button>
									) : null}
									{activeFilterCount > 1 ? (
										<Button variant="ghost" size="sm" onClick={clearFilters}>
											Clear all
										</Button>
									) : null}
								</div>
							) : null}

							{(Object.keys(TAB_LABELS) as TabValue[]).map((value) => (
								<TabsContent key={value} value={value}>
									<CaseTable
										rows={rowsFor(value)}
										caption={`${TAB_LABELS[value]} cases, open cases first, by due date`}
										onOpen={showCase}
										onAssign={assign}
										onStatusChange={changeStatus}
										onClearFilters={clearFilters}
									/>
								</TabsContent>
							))}
						</Tabs>
					</section>

					<p className="sr-only" aria-live="polite">
						{announcement}
					</p>
				</div>
			</div>

			<CommandDialog
				className="sm:max-w-lg"
				open={searchOpen}
				onOpenChange={setSearchOpen}
				title="Search cases and people"
				description="Find a case by number, address or applicant, or a colleague"
			>
				<Command>
					<CommandInput placeholder="Case number, address or name…" aria-label="Search cases and people" />
					<CommandList>
						<CommandEmpty>No cases or people found.</CommandEmpty>
						<CommandGroup heading="Cases">
							{cases.map((item) => (
								<CommandItem
									key={item.id}
									value={`${item.id} ${item.title} ${item.address} ${item.applicant}`}
									onSelect={() => {
										setSearchOpen(false)
										showCase(item.id)
									}}
								>
									<FileTextIcon aria-hidden="true" />
									<span className="shrink-0 tabular-nums">{item.id}</span>
									<span className="truncate text-muted-foreground">
										{item.title}, {item.address}
									</span>
								</CommandItem>
							))}
						</CommandGroup>
						<CommandSeparator />
						<CommandGroup heading="People">
							{OFFICER_IDS.map((officer) => (
								<CommandItem
									key={officer}
									value={`${OFFICERS[officer].name} ${OFFICERS[officer].unit}`}
									onSelect={() => {
										setSearchOpen(false)
										clearFilters()
										setAssigneeFilter(officer)
										setTab('all')
										setActiveNav('overview')
									}}
								>
									<OfficerAvatar id={officer} />
									<span>{OFFICERS[officer].name}</span>
									<span className="truncate text-muted-foreground">{OFFICERS[officer].unit}</span>
								</CommandItem>
							))}
						</CommandGroup>
					</CommandList>
				</Command>
			</CommandDialog>

			<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
				<SheetContent>
					{openCase ? (
						<>
							<SheetHeader className="pr-12">
								<SheetTitle className="tabular-nums">Case {openCase.id}</SheetTitle>
								<SheetDescription>
									{openCase.title}, {openCase.address}
								</SheetDescription>
							</SheetHeader>
							<dl className="grid grid-cols-[auto_1fr] gap-x-6 gap-y-3 px-4">
								<dt className="text-muted-foreground">Status</dt>
								<dd>
									<StatusBadge status={openCase.status} />
								</dd>
								<dt className="text-muted-foreground">Type</dt>
								<dd>{openCase.type}</dd>
								<dt className="text-muted-foreground">Applicant</dt>
								<dd>{openCase.applicant}</dd>
								<dt className="text-muted-foreground">Property</dt>
								<dd>{openCase.address}</dd>
								<dt className="text-muted-foreground">Received</dt>
								<dd>{formatDate(openCase.received)} 2026</dd>
								<dt className="text-muted-foreground">Due</dt>
								<dd>
									<DueDate item={openCase} />
								</dd>
								<dt className="text-muted-foreground">Case officer</dt>
								<dd className="flex items-center gap-2">
									<OfficerAvatar id={openCase.assignee} />
									{OFFICERS[openCase.assignee].name}
								</dd>
							</dl>
							<SheetFooter className="mt-auto flex-row justify-end gap-2 p-4">
								{openCase.assignee !== ME ? (
									<Button variant="outline" onClick={() => assign(openCase.id, ME)}>
										Assign to me
									</Button>
								) : null}
								<Button onClick={() => setSheetOpen(false)}>Go to case</Button>
							</SheetFooter>
						</>
					) : null}
				</SheetContent>
			</Sheet>
		</SidebarProvider>
	)
}