Leave approvals
2 requests waiting for you
- Mia Jensen Product managerParental leave2 Nov to 6 Nov 2026 · 5 daysPending
- Luis Ortega Frontend engineerAnnual leave23 Oct to 27 Oct 2026 · 3 daysPending
- Chloe Martin Data analystSick leave12 Oct 2026 · 1 dayApproved
leave-approval.tsx
"use client";
import { useState } from "react";
import { Avatar, AvatarLabel, Badge, Button, Drawer, Textarea } from "@nexera-ui/react";
interface LeaveItem {
id: string;
name: string;
initials: string;
role: string;
type: string;
dates: string;
days: number;
balance: number;
overlap: string;
reason: string;
status: "Pending" | "Approved" | "Declined";
}
const REQUESTS: LeaveItem[] = [
{
id: "r1",
name: "Mia Jensen",
initials: "MJ",
role: "Product manager",
type: "Parental leave",
dates: "2 Nov to 6 Nov 2026",
days: 5,
balance: 90,
overlap: "No one else in Product is away.",
reason: "First week of parental leave, as agreed with HR.",
status: "Pending",
},
{
id: "r2",
name: "Luis Ortega",
initials: "LO",
role: "Frontend engineer",
type: "Annual leave",
dates: "23 Oct to 27 Oct 2026",
days: 3,
balance: 14,
overlap: "Ahmed Raza is also away on 23 Oct.",
reason: "Family wedding in Seville.",
status: "Pending",
},
{
id: "r3",
name: "Chloe Martin",
initials: "CM",
role: "Data analyst",
type: "Sick leave",
dates: "12 Oct 2026",
days: 1,
balance: 7,
overlap: "No overlap.",
reason: "Doctor's note attached.",
status: "Approved",
},
];
/** Leave approval: a manager's queue of requests; each opens a drawer to review and approve or decline with a note. */
export default function LeaveApproval() {
const [requests, setRequests] = useState(REQUESTS);
const [openId, setOpenId] = useState<string | null>(null);
const [note, setNote] = useState("");
const [noteError, setNoteError] = useState(false);
const current = requests.find((r) => r.id === openId);
const decide = (status: "Approved" | "Declined") => {
if (status === "Declined" && note.trim() === "") {
setNoteError(true);
return;
}
setRequests((rs) => rs.map((r) => (r.id === openId ? { ...r, status } : r)));
setOpenId(null);
};
const tone = (s: LeaveItem["status"]) =>
s === "Approved" ? "success" : s === "Declined" ? "danger" : "warning";
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<section aria-labelledby="approvals-title" className="mx-auto grid w-full max-w-3xl gap-4">
<header className="grid gap-1">
<h2 id="approvals-title" className="text-heading-h3 text-primary">
Leave approvals
</h2>
<p className="text-body-default text-secondary">
{requests.filter((r) => r.status === "Pending").length} requests waiting for you
</p>
</header>
<ul className="m-0 grid list-none overflow-hidden rounded-xl border border-default bg-surface p-0 shadow-xs">
{requests.map((r) => (
<li
key={r.id}
className="flex flex-col gap-3 border-t border-default p-4 first:border-t-0 @3xl:flex-row @3xl:items-center"
>
<AvatarLabel
name={r.name}
subtitle={r.role}
size="md"
avatar={<Avatar size="md" initials={r.initials} isDecorative />}
className="@3xl:w-56"
/>
<div className="grid flex-1">
<span className="text-body-strong text-primary">{r.type}</span>
<span className="text-body-small text-secondary">
{r.dates} · {r.days} {r.days === 1 ? "day" : "days"}
</span>
</div>
<div className="flex items-center justify-between gap-3">
<Badge tone={tone(r.status)} label={r.status} />
<Button
variant={r.status === "Pending" ? "primary" : "ghost"}
aria-label={`Review ${r.name}'s request`}
onPress={() => {
setNote("");
setNoteError(false);
setOpenId(r.id);
}}
>
Review
</Button>
</div>
</li>
))}
</ul>
</section>
<Drawer
title="Review leave request"
subtitle={current ? `${current.type} · ${current.dates}` : undefined}
size="md"
isOpen={current !== undefined}
onOpenChange={(open) => {
if (!open) setOpenId(null);
}}
secondaryAction={
current?.status === "Pending" ? (
<Button
variant="destructive"
size="md"
onPress={() => {
decide("Declined");
}}
>
Decline
</Button>
) : undefined
}
primaryAction={
current?.status === "Pending" ? (
<Button
size="md"
onPress={() => {
decide("Approved");
}}
>
Approve
</Button>
) : (
<Button size="md" slot="close">
Close
</Button>
)
}
>
{current ? (
<div className="grid gap-5">
<AvatarLabel
name={current.name}
subtitle={current.role}
size="lg"
avatar={<Avatar size="lg" initials={current.initials} isDecorative />}
/>
<dl className="m-0 grid gap-3 text-body-small">
{[
[
"Dates",
`${current.dates} (${current.days} working ${current.days === 1 ? "day" : "days"})`,
],
["Balance after", `${current.balance - current.days} of ${current.balance} days`],
["Team cover", current.overlap],
["Reason", current.reason],
].map(([label, value]) => (
<div key={label} className="grid gap-0.5">
<dt className="text-secondary">{label}</dt>
<dd className="m-0 text-body-strong text-primary">{value}</dd>
</div>
))}
</dl>
{current.status === "Pending" ? (
<Textarea
label="Note to employee"
description="Required when you decline."
value={note}
onChange={(v) => {
setNote(v);
setNoteError(false);
}}
isInvalid={noteError}
errorMessage="Say why you are declining, so they can pick other dates."
maxLength={300}
/>
) : (
<Badge tone={tone(current.status)} label={current.status} />
)}
</div>
) : null}
</Drawer>
</div>
);
}
More scheduling and calendar blocks
- Month calendarEvents across a month, with a 'more' overflow.
- Week calendarHour-by-hour week view with events.
- Team availabilityWho is free when, side by side.
- Book a meetingPick a day, then a free time slot.
- Event detailsA popover with time, place and people.
- Leave requestRequest time off and see the balance.
- Shift pickerChoose a morning, evening or night shift.