← Back to application blocks
admin-reservations-01
A reservations back-office dashboard: a KPI row (today's bookings, guests, occupancy, no-shows) over a bookings table with guest avatars, resource, time, party size, confirmed/held/cancelled status badges, per-row actions, live search and status filter tabs. Use as the admin panel of a booking system.
Code
"use client"
import * as React from "react"
import { MoreHorizontalIcon, PlusIcon, SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
type BookingStatus = "confirmed" | "held" | "cancelled"
type Reservation = {
id: string
guest: string
email: string
resource: string
time: string
party: number
status: BookingStatus
}
const KPIS = [
{ label: "Today's bookings", value: "38" },
{ label: "Guests", value: "112" },
{ label: "Occupancy", value: "82%" },
{ label: "No-shows", value: "3" },
]
const RESERVATIONS: Reservation[] = [
{ id: "r1", guest: "Olivia Martin", email: "olivia@example.com", resource: "Window table for 4", time: "19:00", party: 4, status: "confirmed" },
{ id: "r2", guest: "Jackson Lee", email: "jackson@example.com", resource: "Deluxe room 101", time: "15:00", party: 2, status: "held" },
{ id: "r3", guest: "Isabella Nguyen", email: "isabella@example.com", resource: "Workshop seat", time: "10:30", party: 1, status: "confirmed" },
{ id: "r4", guest: "William Kim", email: "william@example.com", resource: "Detailing bay", time: "13:00", party: 1, status: "cancelled" },
{ id: "r5", guest: "Sofia Davis", email: "sofia@example.com", resource: "Window table for 4", time: "20:30", party: 3, status: "confirmed" },
{ id: "r6", guest: "Liam Johnson", email: "liam@example.com", resource: "Deluxe room 101", time: "18:00", party: 2, status: "held" },
]
const STATUS_VARIANT: Record<BookingStatus, "default" | "secondary" | "outline"> = {
confirmed: "default",
held: "secondary",
cancelled: "outline",
}
const TABS: { value: BookingStatus | "all"; label: string }[] = [
{ value: "all", label: "All" },
{ value: "confirmed", label: "Confirmed" },
{ value: "held", label: "Held" },
{ value: "cancelled", label: "Cancelled" },
]
const initials = (name: string) =>
name
.split(" ")
.map((p) => p[0])
.slice(0, 2)
.join("")
export function AdminReservations({
className,
...props
}: React.ComponentProps<"div">) {
const [query, setQuery] = React.useState("")
const [status, setStatus] = React.useState<BookingStatus | "all">("all")
const filtered = RESERVATIONS.filter((r) => {
const matchesStatus = status === "all" || r.status === status
const q = query.toLowerCase()
const matchesQuery =
r.guest.toLowerCase().includes(q) || r.resource.toLowerCase().includes(q)
return matchesStatus && matchesQuery
})
return (
<div className={cn("mx-auto w-full max-w-5xl px-6 py-10", className)} {...props}>
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-1">
<h1 className="font-heading text-2xl font-bold tracking-tight">
Reservations
</h1>
<p className="text-muted-foreground text-sm">
Today · Tuesday, July 14 — manage bookings across every resource.
</p>
</div>
<Button>
<PlusIcon className="size-4" />
New booking
</Button>
</div>
<div className="mb-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{KPIS.map((kpi) => (
<Card key={kpi.label}>
<CardHeader className="pb-2">
<CardDescription>{kpi.label}</CardDescription>
<CardTitle className="text-2xl tabular-nums">{kpi.value}</CardTitle>
</CardHeader>
</Card>
))}
</div>
<Tabs value={status} onValueChange={(v) => setStatus(v as BookingStatus | "all")}>
<Card>
<CardHeader className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<TabsList>
{TABS.map((tab) => (
<TabsTrigger key={tab.value} value={tab.value}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
<div className="relative">
<SearchIcon className="text-muted-foreground absolute top-2.5 left-2.5 size-4" />
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search guests or resources..."
className="w-full pl-8 sm:w-[260px]"
aria-label="Search reservations"
/>
</div>
</CardHeader>
<CardContent>
{/* Filter tabs need a panel per value so each trigger's aria-controls
resolves; the filtered table lives in the active panel. */}
{TABS.map((tab) => (
<TabsContent
key={tab.value}
value={tab.value}
forceMount
className={cn("mt-0", tab.value !== status && "hidden")}
>
{tab.value === status && (
<Table>
<TableHeader>
<TableRow>
<TableHead>Guest</TableHead>
<TableHead>Resource</TableHead>
<TableHead>Time</TableHead>
<TableHead className="hidden sm:table-cell">Party</TableHead>
<TableHead>Status</TableHead>
<TableHead>
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((r) => (
<TableRow key={r.id}>
<TableCell>
<div className="flex items-center gap-2">
<Avatar className="size-7">
<AvatarFallback className="text-xs">
{initials(r.guest)}
</AvatarFallback>
</Avatar>
<div className="min-w-0">
<p className="truncate text-sm font-medium">{r.guest}</p>
<p className="text-muted-foreground truncate text-xs">
{r.email}
</p>
</div>
</div>
</TableCell>
<TableCell className="text-sm">{r.resource}</TableCell>
<TableCell className="tabular-nums">{r.time}</TableCell>
<TableCell className="hidden sm:table-cell tabular-nums">
{r.party}
</TableCell>
<TableCell>
<Badge variant={STATUS_VARIANT[r.status]}>{r.status}</Badge>
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
aria-label={`Actions for ${r.guest}`}
>
<MoreHorizontalIcon className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>View details</DropdownMenuItem>
<DropdownMenuItem>Check in</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">Cancel</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
{filtered.length === 0 && (
<TableRow>
<TableCell colSpan={6} className="text-muted-foreground h-24 text-center">
No reservations found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</TabsContent>
))}
</CardContent>
</Card>
</Tabs>
</div>
)
}