Zum Inhalt springen
SuperScheduler

InteraktionGilt fürSuperScheduler Pro

Ressourcenbäume, Spalten und Auswahl

Setzen Sie treeEnabled und verschachteln Sie Ressourcen in children; ein Elternknoten bleibt eingeklappt, sofern er nicht expanded: true hat. Spalten im Zeilenkopf ergänzen Sie mit rowHeaderColumns, deren Zellen über display aus Feldern der Ressource gefüllt werden. Filtern Sie mit control.rows.filter() und control.events.filter() zusammen mit onRowFilter und onEventFilter, und wählen Sie aus mit eventClickHandling: 'Select', Rechteckauswahl, rowClickHandling: 'Select' sowie den APIs multiselect und multirange.

Geprüft mit v0.1.0 · überarbeitet am 7. Oktober 2026.md

Echte Planungen haben selten eine flache Liste von Ressourcen. Zimmer gehören zu Etagen, Techniker zu Regionen, Maschinen zu Linien. Dieser Leitfaden behandelt die Pro-Funktionen, die Zeilen ordnen und mit mehreren Elementen gleichzeitig arbeiten: Ressourcenbäume, Spalten im Zeilenkopf, Filter und die drei Arten der Auswahl (Ereignisse, Zeilen und Zeiträume).

Alles hier erfordert SuperScheduler Pro (super-scheduler). Die Edition Lite rendert eine flache Liste und lehnt children und columns an Ressourcen ab.

Ressourcen in einem Baum gruppieren

Ein Baum besteht aus einfachen Daten: Eine Ressource mit einem children-Array wird zur Elternzeile. Zwei Dinge schalten ihn ein:

  • treeEnabled: true am Planer. Ohne diese Option werden children ignoriert, und der Zeilenkopf hat keine Umschalter.
  • expanded: true an jedem Elternknoten, der geöffnet starten soll. Elternknoten sind eingeklappt, sofern expanded nicht genau true ist.
src/TeamPlanner.tsxtsx
import { useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import 'super-scheduler/styles.css'

// Regions are parent rows and technicians their children. A parent stays collapsed
// unless its resource says `expanded: true`.
const RESOURCES: SuperScheduler.ResourceData[] = [
  {
    id: 'north',
    name: 'North region',
    expanded: true,
    children: [
      { id: 'ana', name: 'Ana Ruiz' },
      { id: 'ben', name: 'Ben Ortiz' },
    ],
  },
  {
    id: 'south',
    name: 'South region',
    children: [
      { id: 'cai', name: 'Cai Moreno' },
      { id: 'dee', name: 'Dee Patel' },
    ],
  },
]

const JOBS: SuperScheduler.EventData[] = [
  {
    id: 'job-1',
    resource: 'ana',
    start: '2026-10-05T00:00:00',
    end: '2026-10-07T00:00:00',
    text: 'Panel upgrade',
  },
  {
    id: 'job-2',
    resource: 'cai',
    start: '2026-10-06T00:00:00',
    end: '2026-10-09T00:00:00',
    text: 'Boiler service',
  },
]

export function TeamPlanner() {
  const { controlRef, control } = useSchedulerControl()
  // The control adopts the events array and mutates it: hand it a copy.
  const events = useMemo(() => JOBS.slice(), [])

  return (
    <section aria-labelledby="team-plan-title">
      <h2 id="team-plan-title">Field teams</h2>
      <div role="toolbar" aria-label="Rows">
        <button type="button" disabled={control === null} onClick={() => control?.rows.expandAll()}>
          Expand all
        </button>
        <button
          type="button"
          disabled={control === null}
          onClick={() => control?.rows.collapseAll()}
        >
          Collapse all
        </button>
      </div>
      <SuperSchedulerComponent
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        cellWidth={56}
        treeEnabled
        treeIndent={16}
        // Region rows group technicians: no jobs and no time selection on them.
        treePreventParentUsage
        resources={RESOURCES}
        events={events}
      />
    </section>
  )
}

Sie sollten „North region“ geöffnet mit beiden Technikern sehen und „South region“ geschlossen; beide Buttons funktionieren, sobald der Planer gemountet ist (bis dahin ist control null).

Zugehörige Optionen:

OptionStandardWirkung
treeEnabledfalseBaut Zeilen aus children auf.
treeIndent20Einrückung pro Ebene, in Pixeln.
treePreventParentUsagefalseElternzeilen nehmen weder Ereignisse noch eine Zeitauswahl an.
treeAutoExpandtrueEin eingeklappter Elternknoten öffnet sich, wenn eine halbe Sekunde lang ein Ereignis über ihn gezogen wird.
rowFilterParentsAlwaysVisibletrueBehält die Vorfahren einer Zeile, die einem Filter entspricht.

Elternknoten können auch Ereignisse enthalten (eine Buchung für das „ganze Team“), sofern Sie nicht treePreventParentUsage setzen. Eine einzelne Ressource lehnt abgelegte Ereignisse mit preventUsage: true ab, und Ressourcen der obersten Ebene lassen sich mit frozen: 'top' oder frozen: 'bottom' über oder unter den scrollenden Zeilen fixieren, was sich für Summenzeilen eignet.

Aus Code auf- und zuklappen

Das Control stellt den Baum über control.rows bereit:

  • control.rows.expandAll() und control.rows.collapseAll();
  • control.rows.expand(level) öffnet die Elternknoten oberhalb einer Ebene (standardmäßig 1, -1 für alle Ebenen);
  • control.rows.find(id) gibt eine Zeile zurück; row.expand(), row.collapse() und row.toggle() ändern sie.

onResourceExpand und onResourceCollapse melden ein Umschalten, nachdem es passiert ist, mit der Zeile als args.resource. Nutzen Sie sie, um sich zu merken, was der Nutzer geöffnet hat, oder um andere Teile Ihrer Oberfläche synchron zu halten.

Spalten im Zeilenkopf ergänzen

Ohne rowHeaderColumns zeigt der Zeilenkopf den name jeder Ressource. Mit der Option wird der Kopf zu einer kleinen Tabelle:

  • text ist der Spaltentitel (name und title werden ebenfalls akzeptiert);
  • display benennt ein Feld der Ressource, dessen Wert die Zelle füllt (ein gleichnamiges Feld in tags wird zuerst gelesen);
  • die erste Spalte zeigt den name der Ressource, wenn sie kein display hat;
  • width legt die Breite in Pixeln fest (Standard rowHeaderColumnDefaultWidth, 80);
  • sort macht den Titel klickbar: Ein Klick sortiert die Zeilen nach diesem Feld, erst aufsteigend, dann absteigend, und setzt aria-sort am Titel.

Eine Ressource kann auch eigene columns: [{ text }, { html }] mitbringen. Diese Einträge werden nach Position zugeordnet, einer pro Spalte.

src/TechnicianColumns.tsxtsx
import { useState } from 'react'
import { type SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import 'super-scheduler/styles.css'

type Status = 'available' | 'off'

const TECHNICIANS: SuperScheduler.ResourceData[] = [
  { id: 'ana', name: 'Ana Ruiz', skill: 'Electrical', shift: 'Early', status: 'available' },
  { id: 'ben', name: 'Ben Ortiz', skill: 'HVAC', shift: 'Late', status: 'off' },
  { id: 'cai', name: 'Cai Moreno', skill: 'Plumbing', shift: 'Early', status: 'available' },
]

// `display` reads a resource field, so a column keeps its values when it is reordered.
// `sort` makes the title clickable: it sorts the rows by that field, ascending then descending.
const INITIAL_COLUMNS: SuperScheduler.RowHeaderColumnData[] = [
  { text: 'Technician', width: 150, sort: 'name' },
  { text: 'Skill', display: 'skill', width: 110, sort: 'skill' },
  { text: 'Shift', display: 'shift', width: 70, nonresizable: true },
  { text: 'Status', display: 'status', width: 100 },
]

const STATUS_LABEL: Record<Status, string> = { available: 'Available', off: 'Day off' }

// Runs with `this` = the control, whose rowHeaderColumns are always in the current order.
function decorateStatus(
  this: SuperScheduler.SchedulerApi,
  args: SuperScheduler.SchedulerBeforeRowHeaderRenderArgs,
): void {
  const index = this.rowHeaderColumns?.findIndex((column) => column.display === 'status') ?? -1
  const cell = args.row.columns[index]
  const status = args.row.data.status
  if (cell === undefined || (status !== 'available' && status !== 'off')) return
  // With rowHeaderColumns, row.html is ignored: write the column's own html (trusted markup).
  cell.html = `<span class="tech-status tech-status--${status}">${STATUS_LABEL[status]}</span>`
}

export function TechnicianColumns({ events }: { events: SuperScheduler.EventData[] }) {
  // Kept in state so a parent re-render never hands the control the original order again.
  const [columns, setColumns] = useState(INITIAL_COLUMNS)

  return (
    <SuperSchedulerComponent
      startDate="2026-10-05"
      days={14}
      scale="Day"
      cellWidth={56}
      resources={TECHNICIANS}
      events={events}
      rowHeaderColumns={columns}
      rowHeaderColumnsReorderable
      onRowHeaderColumnsChange={(args) => setColumns([...args.columns])}
      onBeforeRowHeaderRender={decorateStatus}
    />
  )
}

Um eine Zelle zu gestalten, verwenden Sie onBeforeRowHeaderRender. Mit Spalten wird das eigene html der Zeile ignoriert: Schreiben Sie stattdessen args.row.columns[i].html. Dieses Markup ist vertrauenswürdiges HTML; escapen Sie Nutzertext also mit SuperScheduler.Util.escapeHtml. Für React-Inhalte im Zeilenkopf siehe React-Render-Slots.

Spaltenbreite ändern und Spalten umordnen

Die Breite von Spalten lässt sich standardmäßig ändern (rowHeaderColumnsResizable: true); nonresizable: true schaltet das für eine einzelne Spalte ab. Nach einer Größenänderung schreibt das Control die neue Breite in dieses Spaltenobjekt Ihres rowHeaderColumns-Arrays und ruft onRowHeaderColumnResized({ column }) auf.

Das Umordnen ist standardmäßig aus. Mit rowHeaderColumnsReorderable ziehen Nutzer die Spaltentitel; die Ränder zur Größenänderung haben dabei Vorrang vor dem Ziehen. In keyboardMode: 'Full' verschieben Alt + Umschalt + Links und Alt + Umschalt + Rechts den fokussierten Titel und sagen die Änderung an. Aus Code verschiebt control.moveRowHeaderColumn(from, to) eine Spalte anhand ihres Index in der aktuellen Reihenfolge.

Jede Größenänderung und jedes Umordnen ruft onRowHeaderColumnsChange auf mit:

  • columns: den Spalten in ihrer neuen Reihenfolge, mit ihren Breiten;
  • order: order[i] ist der ursprüngliche Index (in der übergebenen Prop) der Spalte, die jetzt an Position i steht;
  • widths und reason ('resize' oder 'reorder').

Halten Sie die Spalten wie im Snippet im React-State und übernehmen Sie args.columns. Andernfalls würde eine Elternkomponente, die mit einem neuen Array-Literal neu rendert, dem Control die ursprüngliche Reihenfolge zurückgeben.

Zeilen und Ereignisse filtern

Filter bestehen aus einem Parameter und einem Callback. control.rows.filter(param) speichert den Parameter und ruft onRowFilter für jede Zeile auf; setzen Sie args.visible = false, um sie auszublenden. control.events.filter(param) und onEventFilter funktionieren für Ereignisse genauso. Ein Parameter, der falsy ist ('', null, 0), hebt den Filter auf.

src/FilteredJobs.tsxtsx
import { useEffect, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'

type JobStatus = 'scheduled' | 'done' | 'cancelled'
type Job = { status: JobStatus }

// Module-level handlers keep their identity: a new onRowFilter or onEventFilter
// function makes the control evaluate the active filter again.
const FILTERS: Pick<SchedulerProps, 'onRowFilter' | 'onEventFilter'> = {
  onRowFilter: (args) => {
    const query = String(args.filterParam).toLowerCase()
    args.visible = args.row.name.toLowerCase().includes(query)
  },
  onEventFilter: (args) => {
    const job = args.e.data as SuperScheduler.EventData<Job>
    args.visible = job.status === args.filterParam
  },
}

export function FilteredJobs(props: {
  resources: SuperScheduler.ResourceData[]
  jobs: SuperScheduler.EventData<Job>[]
}) {
  const { controlRef, control } = useSchedulerControl()
  const [query, setQuery] = useState('')
  const [status, setStatus] = useState<JobStatus | ''>('')
  const events = useMemo(() => props.jobs.slice(), [props.jobs])

  // A falsy parameter ('' or null) clears the filter.
  useEffect(() => control?.rows.filter(query.trim() || null), [control, query])
  useEffect(() => control?.events.filter(status || null), [control, status])

  return (
    <>
      <label>
        Technician <input type="search" value={query} onChange={(e) => setQuery(e.target.value)} />
      </label>
      <label>
        Status
        <select value={status} onChange={(e) => setStatus(e.target.value as JobStatus | '')}>
          <option value="">All</option>
          <option value="scheduled">Scheduled</option>
          <option value="done">Done</option>
          <option value="cancelled">Cancelled</option>
        </select>
      </label>
      <SuperSchedulerComponent
        {...FILTERS}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        treeEnabled
        // Keeps the region row of every matching technician (default true).
        rowFilterParentsAlwaysVisible
        resources={props.resources}
        events={events}
      />
    </>
  )
}

Die Eingabe „ben“ lässt „North region“ und „Ben Ortiz“ übrig: rowFilterParentsAlwaysVisible behält die Vorfahren jeder passenden Zeile. Die Wahl eines Status blendet die anderen Ereignisse aus, ohne die Zeilen anzutasten.

Filter laufen im Browser über die Daten, die das Control hält. Eine Suche über Daten, die nicht geladen sind (ein anderer Monat, ein anderer Standort), ist eine Abfrage an Ihr Backend, gefolgt von neuen resources oder events.

Ereignisse auswählen

Standardmäßig führt ein Klick auf ein Ereignis nur onEventClick aus. Für die Auswahl braucht es eventClickHandling: 'Select':

  • Ein Klick schaltet die Auswahl des Ereignisses um und hebt die übrigen auf.
  • Strg + Klick (Cmd + Klick unter macOS) schaltet es um und behält die übrigen, solange allowMultiSelect true ist (der Standard).
  • Mit eventMultiSelectRange wählt Umschalt + Klick die Ereignisse zwischen dem zuletzt angeklickten und diesem aus, über die sichtbaren Zeilen hinweg; Strg/Cmd + Umschalt fügt sie hinzu.

onEventSelect läuft vor der Änderung und kann sie mit args.preventDefault() abbrechen; onEventSelected läuft danach.

Die API control.multiselect liest und ändert die Auswahl: get(), add(e), remove(e), clear(), isSelected(e) und selectAll({ scope, filter }). selectAll wählt standardmäßig die sichtbaren Ereignisse aus; scope: 'all' durchsucht jedes geladene Ereignis, mit Kosten proportional zur Größe des Speichers.

src/SelectableJobs.tsxtsx
import { useEffect, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import { subscribeScheduler } from 'super-scheduler/hooks'

type Job = { status: 'scheduled' | 'done' }

export function SelectableJobs(props: {
  resources: SuperScheduler.ResourceData[]
  jobs: SuperScheduler.EventData<Job>[]
  onReschedule: (ids: SuperScheduler.EventId[]) => void
}) {
  const { controlRef, control } = useSchedulerControl()
  const [count, setCount] = useState(0)
  const events = useMemo(() => props.jobs.slice(), [props.jobs])

  // Rectangle selections and multiselect calls (selectAll, clear) publish the
  // 'selection' topic; clicks are reported by onEventSelected below.
  useEffect(() => {
    if (control === null) return
    return subscribeScheduler(control, 'selection', (selected) => setCount(selected.length))
  }, [control])

  const config = useMemo<SchedulerProps>(
    () => ({
      // A click toggles the event; Ctrl+click (Cmd+click on macOS) keeps the others.
      eventClickHandling: 'Select',
      // Shift+click selects every event between the last clicked one and this one.
      eventMultiSelectRange: true,
      // Shift+drag over the grid selects the events inside the rectangle.
      rectangleSelectHandling: 'EventSelect',
      // With the rectangle: Shift adds to the selection, Alt removes from it.
      rectangleSelectModifiers: true,
      onEventSelected() {
        setCount(this.multiselect.get().length)
      },
      // Marks selected events for CSS. The hook runs again for every event whose
      // selection changes.
      onBeforeEventRender: (args) => {
        const e = args.control.events.find(args.data.id)
        if (e !== null && args.control.multiselect.isSelected(e)) {
          args.data.cssClass = [args.data.cssClass, 'is-selected'].filter(Boolean).join(' ')
        }
      },
    }),
    [],
  )

  const selectScheduled = () =>
    control?.multiselect.selectAll({
      scope: 'view',
      filter: (e) => (e.data as SuperScheduler.EventData<Job>).status === 'scheduled',
    })

  const reschedule = () => {
    const ids = control?.multiselect.get().map((e) => e.id()) ?? []
    if (ids.length > 0) props.onReschedule(ids)
  }

  return (
    <>
      <div role="toolbar" aria-label="Selection">
        <button type="button" onClick={selectScheduled}>
          Select scheduled in view
        </button>
        <button type="button" onClick={() => control?.multiselect.clear()}>
          Clear
        </button>
        <button type="button" disabled={count === 0} onClick={reschedule}>
          Reschedule {count} {count === 1 ? 'job' : 'jobs'}
        </button>
      </div>
      <SuperSchedulerComponent
        {...config}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        resources={props.resources}
        events={events}
      />
    </>
  )
}

Der Zähler im Button sollte Klicks, Rechtecken und den Aktionen der Werkzeugleiste folgen, und ausgewählte Aufträge sollten durch dieses CSS umrandet sein:

csscss
.super-scheduler__event.is-selected {
  z-index: 2;
  box-shadow: 0 0 0 2px var(--super-scheduler-accent), var(--super-scheduler-shadow-2);
}

Mit allowMultiMove verschiebt das Ziehen eines ausgewählten Ereignisses die ganze Auswahl; jedes Ereignis durchläuft dieselben Verschieberegeln (siehe Ziehen, Dauer ändern und Geschäftsregeln). Im vollständigen Tastaturmodus wählt Strg/Cmd + A jedes sichtbare Ereignis aus.

Rechteckauswahl

rectangleSelectHandling bestimmt, was ein aufgezogenes Rechteck bewirkt:

  • 'EventSelect' wählt die Ereignisse darin aus (und führt die Callbacks aus);
  • 'Enabled' führt nur die Callbacks aus, für Anwendungen, die selbst auf das Rechteck reagieren;
  • 'Disabled' (Standard) schaltet es ab.

Nutzer ziehen ein Rechteck auf, indem sie bei gedrückter Umschalttaste über das Raster ziehen. Um ohne Umschalttaste zu beginnen, setzen Sie args.action = 'RectangleSelect' in onGridMouseDown oder rufen vor dem nächsten Drücken control.multiselect.startRectangle() auf. rectangleSelectMode: 'Row' hält das Rechteck in der Zeile, in der das Drücken begann. Mit rectangleSelectModifiers fügt Umschalt zur aktuellen Auswahl hinzu und Alt entfernt daraus; ohne eine dieser Tasten ersetzt das Rechteck die Auswahl.

Die Callbacks sind onRectangleSelecting (jeder Frame; args.visible = false blendet das Rechteck aus), onRectangleSelect (abbrechbar, mit events, start, end und resources) und onRectangleSelected.

Zeilen auswählen

rowClickHandling: 'Select' sorgt dafür, dass ein Klick auf den Zeilenkopf die Zeile auswählt. Strg/Cmd schaltet eine Zeile um, und Umschalt wählt alle Zeilen ab der zuletzt angeklickten aus. Ausgewählte Zeilen tragen aria-selected="true".

src/RosterRows.tsxtsx
import { useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'

export function RosterRows(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
  onNotify: (resourceIds: SuperScheduler.ResourceId[]) => void
}) {
  const { controlRef, control } = useSchedulerControl()
  const [selected, setSelected] = useState<readonly SuperScheduler.ResourceId[]>([])
  const events = useMemo(() => props.events.slice(), [props.events])

  const config = useMemo<SchedulerProps>(
    () => ({
      // A click on a row header selects the row; Ctrl/Cmd toggles, Shift selects a range.
      rowClickHandling: 'Select',
      onRowSelected() {
        setSelected(this.rows.selection.get().map((row) => row.id))
      },
    }),
    [],
  )

  return (
    <>
      <button
        type="button"
        disabled={selected.length === 0}
        onClick={() => props.onNotify([...selected])}
      >
        Notify {selected.length} technicians
      </button>
      <button
        type="button"
        onClick={() => {
          control?.rows.selection.clear()
          setSelected([])
        }}
      >
        Clear rows
      </button>
      <SuperSchedulerComponent
        {...config}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        resources={props.resources}
        events={events}
      />
    </>
  )
}

control.rows.selection bietet get(), add(row), remove(row), isSelected(row) und clear(); diese Aufrufe heben die Zeilen sofort hervor und lösen keinen Callback aus, weshalb das Snippet nach clear() seinen eigenen State zurücksetzt. onRowSelect (abbrechbar) und onRowSelected laufen vor bzw. nach jeder Auswahl per Klick.

Die Prop selectedRows enthält die IDs der ausgewählten Ressourcen. Übergeben Sie sie, um mit einer Auswahl zu starten; das Control aktualisiert genau dieses Array, wenn sich die Auswahl ändert.

Zeiträume auswählen

Ziehen über leere Zellen wählt einen Zeitraum aus und ruft beim Loslassen einmal onTimeRangeSelected auf, mit start, end (exklusiv), resource und origin: 'drag'. Weitere Ursprünge:

  • 'click': ein einfacher Klick auf eine leere Zelle wählt diese Zelle aus;
  • 'keyboard': bei aktivierter Tastatur die Eingabetaste auf einer fokussierten Zelle oder Umschalt + Links/Rechts, um einen Zeitraum zu erweitern;
  • 'api': control.selectTimeRange(start, end, resource).

Prüfen Sie args.origin, wenn nur ein Ziehen etwas anlegen soll. Die Auswahl bleibt nach dem Loslassen gezeichnet, bis zur nächsten Auswahl oder bis control.clearSelection(). Auch ein Klick an anderer Stelle hebt sie auf, außer mit timeRangeSelectedHandling: 'HoldForever'; 'Disabled' schaltet die Zeitraumauswahl ab.

onTimeRangeSelecting läuft bei jeder Änderung während des Ziehens. Setzen Sie args.allowed = false, um einen Zeitraum abzulehnen (der Schatten wird markiert, und das Loslassen wählt nichts aus), oder schreiben Sie args.start und args.end um, um ihn zu begrenzen. Gesperrte Zellen und Elternknoten unter treePreventParentUsage lehnen Auswahlen ohne jeden Code ab.

Mehrere Zeiträume gleichzeitig

Mit allowMultiRange kann ein neues Ziehen einen Zeitraum hinzufügen, statt den aktuellen zu ersetzen: solange Strg/Cmd gedrückt ist (multiRangeMode: 'CtrlOrMeta', der Standard) oder immer ('Always'). args.multirange listet jeden ausgewählten Zeitraum auf, und control.multirange liest oder ändert sie: get(), clear() und add(new SuperScheduler.Selection(start, end, resource)).

src/RangeBlocker.tsxtsx
import { useMemo, useState } from 'react'
import { SuperScheduler, SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps } from 'super-scheduler'

export interface BlockRequest {
  readonly resource: SuperScheduler.ResourceId
  /** ISO wall-clock values; `end` is exclusive. */
  readonly start: string
  readonly end: string
}

export function RangeBlocker(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
  onBlock: (requests: BlockRequest[]) => void
}) {
  const { controlRef, control } = useSchedulerControl()
  const [ranges, setRanges] = useState<readonly SuperScheduler.SchedulerRange[]>([])
  const events = useMemo(() => props.events.slice(), [props.events])

  const config = useMemo<SchedulerProps>(
    () => ({
      // Ctrl/Cmd + drag adds a range instead of replacing the current one.
      allowMultiRange: true,
      multiRangeMode: 'CtrlOrMeta',
      onTimeRangeSelecting: (args) => {
        // Past days cannot be blocked: the shadow turns red and the drop is refused.
        args.allowed = args.start.getTime() >= SuperScheduler.Date.today().getTime()
      },
      onTimeRangeSelected: (args) => {
        // A plain click on an empty cell also selects it (origin 'click').
        setRanges([...args.multirange])
      },
    }),
    [],
  )

  const block = () => {
    props.onBlock(
      ranges.map((range) => ({
        resource: range.resource,
        start: range.start.value,
        end: range.end.value,
      })),
    )
    control?.clearSelection()
    setRanges([])
  }

  return (
    <>
      <button type="button" disabled={ranges.length === 0} onClick={block}>
        Block {ranges.length} selected {ranges.length === 1 ? 'range' : 'ranges'}
      </button>
      <SuperSchedulerComponent
        {...config}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        resources={props.resources}
        events={events}
      />
    </>
  )
}

Ziehen Sie über freie Tage eines Zimmers und dann bei gedrückter Strg-Taste (Cmd unter macOS) über die eines anderen: Der Button zeigt „Block 2 selected ranges“. Die Zeiträume erreichen onBlock als ISO-Werte in lokaler Uhrzeit mit exklusivem Ende, bereit zum Senden an Ihr Backend.

Was Ihrer Anwendung gehört

Die Bibliothek hält Bäume, Filter und Auswahlen im Browser, für die aktuelle Sitzung. Ihre Anwendung entscheidet:

  • woher der Baum kommt: Zeilen sind die resources, die Sie übergeben, und das Control fragt nie selbst nach Kindern (onLoadNode ist reserviert);
  • was ein Filter bedeutet, wenn Daten nicht geladen sind, und wie die Suche das Backend erreicht;
  • was eine Auswahl bewirkt: Massenaktionen, die Berechtigung, sie auszuführen, und ihre Speicherung;
  • ob aufgeklappte Zeilen und Spaltenlayouts ein Neuladen überstehen (siehe gespeicherte Ansichten).

Auswahlen werden nirgends gespeichert. Der Aufklappzustand lebt in den übergebenen Ressourcenobjekten, bis Sie sie ersetzen.

Zimmerplanung im HotelIn Zimmer 104 tropft die Dusche. Buchen Sie den nächsten Gast um, sperren Sie das Zimmer für den Installateur und finden Sie die ausgebuchten Nächte. Planung von SchulungsräumenDie Anmeldungen sprengen den Raum. Beide Termine wählen, sehen, was für beide frei ist, zusammen verschieben und die Ansicht behalten. Disposition im technischen AußendienstEin dringender Auftrag kommt herein. Finden Sie das Team, das ihn rechtzeitig übernehmen kann.