Findings as data

Lists what is wrong with a plan - double bookings on a lane, dependencies that do not fit, work in blocked time - from the same data the schedule draws. Reach for it to list, count or filter the problems beside the picture.

import { findings, overlaps, violatedDependencies, inBlockedTime } from "@umriss-ui/schedule";

findings(subtasks, dependencies, blocked) returns all three kinds; overlaps, violatedDependencies and inBlockedTime return one each. They are pure functions: run them wherever the data changes, with or without a schedule on screen.

Lead-in and lead-out count as occupied time; touching at a shared edge is no overlap.

Examples

List the findings

findings returns what the schedule marks - overlaps, violated dependencies, work in blocked time - as data for a list beside it.

import { Stack, Text } from "@umriss-ui/core";
import { findings, useSchedule } from "@umriss-ui/schedule";
import type { BlockedTime, Dependency, Subtask, Task } from "@umriss-ui/schedule";

const day = (d: number, hours = 9) => new Date(2026, 2, d, hours).getTime();

const PROJECTS: readonly Task[] = [
  { id: "shop", name: "Online shop relaunch", color: "light-dark(#2563eb, #6b9bff)" },
  { id: "portal", name: "Member portal", color: "light-dark(#7c3aed, #a98bfa)" },
];

const WORK: readonly Subtask[] = [
  { id: "design", task: "shop", lane: "noah", from: day(16), to: day(17, 17), name: "Search results layout" },
  { id: "build", task: "shop", lane: "chloe", from: day(17), to: day(19, 17), name: "Search results page" },
  { id: "address", task: "portal", lane: "chloe", from: day(18), to: day(19, 17), name: "Change of address form" },
  { id: "profile", task: "portal", lane: "arjun", from: day(17), to: day(19, 17), name: "Profile page" },
];

/* The build may start only a morning after the design is done. */
const HANDOVERS: readonly Dependency[] = [{ id: "design-to-build", from: "design", to: "build", lag: 4 * 60 * 60_000 }];

const LEAVE: readonly BlockedTime[] = [{ id: "arjun-holiday", lane: "arjun", from: day(18, 0), to: day(21, 0), label: "Holiday" }];

const when = (instant: number) => new Date(instant).toLocaleString("en-GB", { weekday: "short", hour: "2-digit", minute: "2-digit" });
const hours = (ms: number) => Math.round(ms / 3_600_000);

export default function ListTheFindings() {
  const { Schedule, Lane, Subtasks, Dependencies, BlockedTimes } = useSchedule({ initialView: { domain: [day(16, 0), day(21, 0)] } });
  const found = findings(WORK, HANDOVERS, LEAVE);

  return (
    <Stack gap={3}>
      <Schedule ariaLabel="Three people's week with its findings" height={184}>
        <Lane id="noah" label="Noah Fischer" />
        <Lane id="chloe" label="Chloe Durand" />
        <Lane id="arjun" label="Arjun Mehta" />
        <BlockedTimes data={LEAVE} />
        <Dependencies data={HANDOVERS} />
        <Subtasks data={WORK} tasks={PROJECTS} />
      </Schedule>
      <ul style={{ margin: 0, paddingLeft: 18 }}>
        {found.overlaps.map((o) => (
          <Text as="li" size="sm" key={`${o.first}/${o.second}`}>
            Double-booked: {o.lane} has {o.first} and {o.second} from {when(o.from)} to {when(o.to)}
          </Text>
        ))}
        {found.violatedDependencies.map((v) => (
          <Text as="li" size="sm" key={v.dependency}>
            Too early: {v.dependency} leaves {when(v.departure)} for a start at {when(v.arrival)}, {hours(v.shortBy)} hours short
          </Text>
        ))}
        {found.inBlockedTime.map((b) => (
          <Text as="li" size="sm" key={`${b.subtask}/${b.blocked}`}>
            In leave: {b.subtask} covers {b.blocked} from {when(b.from)} to {when(b.to)}
          </Text>
        ))}
      </ul>
    </Stack>
  );
}

Say when there is nothing to fix

When all three lists findings returns are empty, say so plainly instead of showing an empty list.

import { EmptyState, Stack } from "@umriss-ui/core";
import { findings, useSchedule } from "@umriss-ui/schedule";
import type { Subtask, Task } from "@umriss-ui/schedule";

const day = (d: number, hours = 9) => new Date(2026, 2, d, hours).getTime();

const PROJECTS: readonly Task[] = [{ id: "booking", name: "Booking app", color: "light-dark(#0d9488, #3cc7b8)" }];

const WORK: readonly Subtask[] = [
  { id: "w-111", task: "booking", lane: "hana", from: day(16), to: day(18, 17), name: "Push notifications" },
  { id: "w-113", task: "booking", lane: "kofi", from: day(16), to: day(18, 17), name: "Calendar sync" },
];

export default function NothingToFix() {
  const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [day(16, 0), day(21, 0)] } });
  const found = findings(WORK, []);
  const count = found.overlaps.length + found.violatedDependencies.length + found.inBlockedTime.length;

  return (
    <Stack gap={3}>
      <Schedule ariaLabel="Two developers' week, without findings" height={140}>
        <Lane id="hana" label="Hana Sato" />
        <Lane id="kofi" label="Kofi Mensah" />
        <Subtasks data={WORK} tasks={PROJECTS} />
      </Schedule>
      {count === 0 && <EmptyState title="Nothing to fix" description="Nobody is double-booked, every hand-over fits, and no work falls into leave." />}
    </Stack>
  );
}

Count the findings per kind

overlaps and violatedDependencies are the halves of findings on their own - here as counts above the day's legs.

import { Badge, Stack } from "@umriss-ui/core";
import { overlaps, useSchedule, violatedDependencies } from "@umriss-ui/schedule";
import type { Dependency, Subtask, Task } from "@umriss-ui/schedule";

const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const min = (n: number) => n * 60_000;

const VEHICLES = [
  { id: "van-402", label: "Van FP 402 R" },
  { id: "truck-520", label: "Truck FP 520 E" },
  { id: "van-455", label: "E-van FP 455 R" },
];

const CONSIGNMENTS: readonly Task[] = [
  { id: "c-2041", name: "C-2041 Holloway Garden Supplies", color: "light-dark(#2563eb, #6b9bff)" },
  { id: "c-2043", name: "C-2043 Oakridge Pharmacy", color: "light-dark(#c2410c, #f08a52)" },
];

const LEGS: readonly Subtask[] = [
  { id: "c-2041-2", task: "c-2041", lane: "van-402", from: at(8), to: at(10, 30), leadIn: min(30), leadOut: min(15) },
  { id: "c-2043-1", task: "c-2043", lane: "truck-520", from: at(6, 30), to: at(8), leadIn: min(30), leadOut: min(15) },
  { id: "c-2043-2", task: "c-2043", lane: "van-402", from: at(10), to: at(11, 30), leadIn: min(15) },
  { id: "c-2043-3", task: "c-2043", lane: "van-455", from: at(12), to: at(14), leadIn: min(20), leadOut: min(20) },
];

const TRANSFERS: readonly Dependency[] = [
  { id: "t-2043-1", from: "c-2043-1", to: "c-2043-2", lag: min(45) },
  { id: "t-2043-2", from: "c-2043-2", to: "c-2043-3", lag: min(25) },
];

export default function CountPerKind() {
  const { Schedule, Lane, Subtasks, Dependencies } = useSchedule({ initialView: { domain: [at(5, 30), at(18)] } });
  const doubleBooked = overlaps(LEGS).length;
  const late = violatedDependencies(LEGS, TRANSFERS).length;

  return (
    <Stack gap={3}>
      <Stack direction="row" gap={2}>
        <Badge tone={doubleBooked > 0 ? "warning" : "success"}>{doubleBooked} double-booked</Badge>
        <Badge tone={late > 0 ? "warning" : "success"}>{late} transfers too short</Badge>
      </Stack>
      <Schedule ariaLabel="The day's legs with their findings counted" height={196}>
        {VEHICLES.map((vehicle) => (
          <Lane key={vehicle.id} id={vehicle.id} label={vehicle.label} />
        ))}
        <Dependencies data={TRANSFERS} />
        <Subtasks data={LEGS} tasks={CONSIGNMENTS} />
      </Schedule>
    </Stack>
  );
}

Keyboard

The keys of First schedule apply here.

API

Overlap

NameTypeDefaultDescription
lanerequiredstring—The lane both sit on.

Shown in: List the findings, Refuse a move that double-books someone (Move and lane)

firstrequiredstring—The subtask that starts earlier (lead-in included); on a tie, the one that stands first in the data.

Shown in: List the findings, Refuse a move that double-books someone (Move and lane)

secondrequiredstring—The other one.

Shown in: List the findings, Refuse a move that double-books someone (Move and lane)

fromrequirednumber—Where the covering begins.

Shown in: List the findings

torequirednumber—Where it ends.

Shown in: List the findings

ViolatedDependency

NameTypeDefaultDescription
dependencyrequiredstring—The id of the dependency.

Shown in: List the findings

departurerequirednumber—When it leaves, by its anchor.

Shown in: List the findings

arrivalrequirednumber—When it has to have arrived, by its anchor.

Shown in: List the findings

shortByrequirednumber—How much time is missing: departure plus lag, minus arrival.

Shown in: List the findings

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/schedule

Getting started

Plan

Time

Reading

Editing

Measuring

API index