An interactive demonstration of crew availability and capacity utilization metrics. Explore stacked bar breakdowns, monthly heat maps, and technician grid loads.
Schedule Capacity Laboratory
Analyze weekly labor capacities, daily utilization trends, and individual tech schedules.
Mar 24 – Mar 30, 2026
Busiest: Thu 3/27
Thu 3/27 — scheduled hours exceed team capacity
Scheduled hours
Remaining capacity
59h
/ 64h
92%
50h
/ 64h
78%
56h
/ 64h
88%
68h
/ 64h
42h
/ 64h
65%
14h
/ 32h
44%
--
Closed
Week utilization: 82% — 289h scheduled of 352h available (Mon–Sat)
Variant1_StackedBarByDay.tsx (Widget Implementation)
import { Paper, Group, Stack, Text, Badge, Title, Box } from "@mantine/core";
import { BarChart } from "@mantine/charts";
import { HiCalendar, HiFire } from "react-icons/hi2";
import { capacityData } from "./sampleData";
import type { DayCapacity } from "./types";
// ---------------------------------------------------------------------------
// Chart data shape
// ---------------------------------------------------------------------------
interface ChartRow {
day: string;
scheduled: number;
remaining: number;
rawScheduled: number;
available: number;
utilization: number;
isOverbooked: boolean;
openSlots: number;
}
// For the stacked bar we cap the "scheduled" segment at available capacity so
// the stacked total always equals availableHours. Overbooked days get a
// visual callout via the day strip below rather than an overflowing bar.
const chartData: ChartRow[] = capacityData.dailyCapacity.map((d: DayCapacity) => {
const capped = Math.min(d.scheduledHours, d.availableHours);
const remaining = Math.max(d.availableHours - capped, 0);
return {
day: d.dayLabel,
scheduled: capped,
remaining,
rawScheduled: d.scheduledHours,
available: d.availableHours,
utilization: d.utilizationPercent,
isOverbooked: d.isOverbooked,
openSlots: d.openSlots,
};
});
// ---------------------------------------------------------------------------
// Custom tooltip
// recharts' ContentType generics are complex; we accept unknown and cast.
// ---------------------------------------------------------------------------
function CustomTooltip(props: unknown) {
const { active, payload, label } = props as {
active?: boolean;
payload?: Array<{ payload: ChartRow }>;
label?: string;
};
if (!active || !payload || payload.length === 0) return null;
const row = payload[0].payload;
const utilizationColor = row.isOverbooked
? "#fa5252"
: row.utilization >= 80
? "#e67700"
: "#2f9e44";
return (
<Paper withBorder shadow="sm" radius="md" p="xs" style={{ minWidth: 168 }}>
<Text fw={700} size="sm" mb={6}>
{label}
</Text>
<Stack gap={3}>
<Group justify="space-between" gap="xl">
<Text size="xs" c="dimmed">
Scheduled
</Text>
<Text size="xs" fw={600}>
{row.rawScheduled}h
</Text>
</Group>
<Group justify="space-between" gap="xl">
<Text size="xs" c="dimmed">
Available
</Text>
<Text size="xs" fw={600}>
{row.available}h
</Text>
</Group>
<Group justify="space-between" gap="xl">
<Text size="xs" c="dimmed">
Utilization
</Text>
<Text size="xs" fw={700} style={{ color: utilizationColor }}>
{row.utilization}%{row.isOverbooked ? " (OVER)" : ""}
</Text>
</Group>
{row.openSlots > 0 && (
<Group justify="space-between" gap="xl">
<Text size="xs" c="dimmed">
Open slots
</Text>
<Text size="xs" fw={600}>
{row.openSlots}
</Text>
</Group>
)}
{row.available === 0 && (
<Text size="xs" c="dimmed" ta="center" mt={2}>
Closed
</Text>
)}
</Stack>
</Paper>
);
}
// ---------------------------------------------------------------------------
// Main component
// ---------------------------------------------------------------------------
export function Variant1_StackedBarByDay() {
const nonZeroDays = capacityData.dailyCapacity.filter((d) => d.availableHours > 0);
const totalScheduled = nonZeroDays.reduce((sum, d) => sum + d.scheduledHours, 0);
const totalAvailable = nonZeroDays.reduce((sum, d) => sum + d.availableHours, 0);
const weekPct = Math.round((totalScheduled / totalAvailable) * 100);
const overbookedDays = capacityData.dailyCapacity
.filter((d) => d.isOverbooked)
.map((d) => d.dayLabel);
return (
<Paper withBorder shadow="sm" radius="md" p="md" style={{ width: "100%" }}>
{/* Header */}
<Group justify="space-between" mb="md" align="flex-start">
<Stack gap={2}>
<Group gap="xs">
<HiCalendar size={16} color="#868e96" />
<Title order={5} style={{ letterSpacing: -0.3 }}>
Schedule Capacity
</Title>
</Group>
<Text size="xs" c="dimmed">
{capacityData.weekOf}
</Text>
</Stack>
<Group gap="xs">
<Group gap={4} align="center">
<HiFire size={14} color="#fa5252" />
<Text size="xs" c="dimmed">
Busiest:{" "}
<Text span fw={600} c="dark">
{capacityData.busiestDay}
</Text>
</Text>
</Group>
<Badge color="blue" variant="light" size="sm" radius="sm">
Week: {weekPct}%
</Badge>
</Group>
</Group>
{/* Overbooked alert banner */}
{overbookedDays.length > 0 && (
<Group
gap="xs"
mb="sm"
p="xs"
style={{
background: "#fff5f5",
borderRadius: 6,
border: "1px solid #ffc9c9",
}}
>
<Badge color="red" size="xs" radius="sm" variant="filled">
OVERBOOKED
</Badge>
<Text size="xs" c="red.7">
{overbookedDays.join(", ")} — scheduled hours exceed team capacity
</Text>
</Group>
)}
{/* Stacked bar chart */}
<Box mb="md">
<BarChart
h={220}
data={chartData}
dataKey="day"
type="stacked"
withLegend={false}
withTooltip
tooltipProps={{ content: CustomTooltip }}
series={[
{ name: "scheduled", label: "Scheduled", color: "#228be6" },
{ name: "remaining", label: "Remaining", color: "#dee2e6" },
]}
barProps={{ radius: [2, 2, 0, 0] }}
yAxisProps={{ tick: { fontSize: 11 }, domain: [0, 70] }}
xAxisProps={{ tick: { fontSize: 11 } }}
gridAxis="y"
tickLine="none"
/>
</Box>
{/* Manual legend */}
<Group gap="lg" mb="md" justify="center">
<Group gap={6}>
<Box style={{ width: 12, height: 12, borderRadius: 2, background: "#228be6" }} />
<Text size="xs" c="dimmed">
Scheduled hours
</Text>
</Group>
<Group gap={6}>
<Box style={{ width: 12, height: 12, borderRadius: 2, background: "#dee2e6" }} />
<Text size="xs" c="dimmed">
Remaining capacity
</Text>
</Group>
</Group>
{/* Per-day number strip */}
<Box
style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: 4,
}}
>
{capacityData.dailyCapacity.map((d) => {
const isOver = d.isOverbooked;
const noWork = d.availableHours === 0;
const pctColor =
d.utilizationPercent >= 80
? "#e67700"
: d.utilizationPercent < 50
? "#868e96"
: "#2f9e44";
return (
<Box
key={d.date}
style={{
textAlign: "center",
padding: "6px 4px",
borderRadius: 6,
background: isOver ? "#fff5f5" : noWork ? "#f8f9fa" : "transparent",
border: isOver
? "1px solid #ffc9c9"
: noWork
? "1px solid #dee2e6"
: "1px solid transparent",
}}
>
<Text size="xs" fw={600} c={isOver ? "red" : noWork ? "dimmed" : "dark"}>
{noWork ? "--" : `${d.scheduledHours}h`}
</Text>
<Text size="xs" c="dimmed" style={{ fontSize: 10 }}>
{noWork ? "Closed" : `/ ${d.availableHours}h`}
</Text>
{isOver ? (
<Badge
size="xs"
color="red"
variant="filled"
radius="sm"
mt={2}
style={{ fontSize: 9, padding: "0 4px" }}
>
OVER
</Badge>
) : !noWork ? (
<Text size="xs" fw={500} style={{ fontSize: 10, color: pctColor }}>
{d.utilizationPercent}%
</Text>
) : null}
</Box>
);
})}
</Box>
{/* Summary line */}
<Text size="xs" c="dimmed" ta="center" mt="sm">
Week utilization:{" "}
<Text span fw={700} c={weekPct >= 95 ? "red" : weekPct >= 80 ? "orange" : "teal"}>
{weekPct}%
</Text>{" "}
— {totalScheduled}h scheduled of {totalAvailable}h available (Mon–Sat)
</Text>
</Paper>
);
}