Zum Inhalt springen
SuperScheduler

InteraktionGilt fürSuperScheduler Pro

Ziehen, Dauer ändern und Geschäftsregeln

Steuern Sie jeden Frame eines Ziehvorgangs in onEventMoving und onEventResizing: Setzen Sie args.allowed = false und args.message, um eine Position mit Begründung abzulehnen. Verhindern Sie Überlappungen mit allowEventOverlap={false} (oder pro Frame mit args.allowOverlap), sperren Sie Zeiten mit gesperrten Zellen, fixieren Sie einzelne Ereignisse mit moveDisabled und resizeDisabled, und treffen Sie die endgültige Entscheidung in onEventMove oder onEventResize, bei Bedarf asynchron mit args.async = true und args.loaded().

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

Beim Ziehen beweist eine Planungstafel ihren Wert, und hier stecken die meisten Geschäftsregeln: Dieser Auftrag braucht eine Hebebühne, jener Aufenthalt darf nicht in die Vergangenheit verschoben werden, der OP-Saal ist über Mittag geschlossen. SuperScheduler Pro fragt Ihren Code zu zwei Zeitpunkten. Während der Nutzer zieht, bei jeder Änderung des Schattens, können Sie die Position akzeptieren, ablehnen oder anpassen und den Grund nennen. Beim Ablegen, einmalig, können Sie die Änderung abbrechen, abwandeln oder bestätigen, auch nach einer Rückfrage bei Ihrem Server.

Dieser Leitfaden baut diese Regeln an einer Werkstattplanung mit Arbeitsplätzen auf und behandelt danach Überlappungen, gesperrte Zeiten, Sperren, asynchrone Bestätigung, die Drag-Karte und das Anlegen von Ereignissen durch Auswahl eines Zeitraums. Alles hier erfordert Pro; Lite ist schreibgeschützt.

Wie über einen Ziehvorgang entschieden wird

  1. Der Nutzer greift ein Ereignis. Gesperrte Ereignisse (moveDisabled) starten keinen Ziehvorgang.
  2. Bei jeder Zeigerbewegung, die Zielzeit oder Zielzeile ändert, läuft onEventMoving (onEventResizing beim Ändern der Dauer). Ihre Regel setzt args.allowed, darf args.start und args.end anpassen und setzt args.message.
  3. Danach wendet die Bibliothek ihre eigenen Prüfungen an: Überlappung mit anderen Ereignissen, wenn allowEventOverlap false ist, und gesperrte Zellen. Ein abgelehnter Schatten wird als verboten gezeichnet, und die Drag-Karte zeigt den Grund.
  4. Beim Loslassen über einer abgelehnten Position passiert nichts: Das Ereignis kehrt zurück, und kein weiterer Callback läuft.
  5. Beim Loslassen über einer akzeptierten Position läuft onEventMove (onEventResize) einmal, bevor sich der Speicher ändert. Es kann das Übernehmen abbrechen, abwandeln oder aufschieben.
  6. Der Speicher wird aktualisiert, onEventMoved (onEventResized) läuft, und onEventsChange folgt im nächsten Microtask, wie in Kontrollierte Ereignisse und Callbacks beschrieben.

Dieselbe Abfolge beim Übernehmen gilt für Verschiebungen und Dauer-Änderungen per Tastatur mit keyboardMode: 'Full'.

Beim Ziehen prüfen

onEventMoving erhält die vorgeschlagene Position und schreibt die Entscheidung in seine Argumente zurück:

BeschreibbarWirkung
allowedfalse zeichnet den Schatten als verboten; Ablegen dort bewirkt nichts
messageText, den die Drag-Karte zeigt, solange allowed false ist
start, endPassen den Schatten an, zum Beispiel um die ursprünglichen Zeiten zu behalten, wenn sich nur die Zeile ändert
allowOverlapÜberschreibt allowEventOverlap nur für diesen Frame
cssClass, htmlKlasse und Inhalt des Schattens

Außerdem liest es den Kontext: args.e (das gezogene Ereignis, Ihre Daten in args.e.data), args.resource und args.row (die Zielzeile), args.duration, args.conflicts, args.external (von außerhalb des Planers gezogen) und die Modifikatortasten. onEventResizing arbeitet genauso mit start, end, allowed, message und allowOverlap und ergänzt args.what, den gezogenen Rand ('start' oder 'end').

src/WorkshopPlanning.tsxtsx
import { useMemo } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventsChangeArgs, SchedulerProps } from 'super-scheduler'

const BAYS: SuperScheduler.ResourceData[] = [
  { id: 'bay-1', name: 'Bay 1 (lift)' },
  { id: 'bay-2', name: 'Bay 2 (lift)' },
  { id: 'bay-3', name: 'Bay 3' },
  { id: 'waiting', name: 'Waiting list' },
]
const BAYS_WITH_LIFT: ReadonlySet<SuperScheduler.ResourceId> = new Set(['bay-1', 'bay-2'])

const TIME_HEADERS: SuperScheduler.TimeHeaderData[] = [
  { groupBy: 'Day', format: 'dddd d MMMM' },
  { groupBy: 'Hour', format: 'HH:mm' },
]

/** A custom field of the job data (see "Custom fields" in the data model guide). */
function needsLift(data: SuperScheduler.EventData): boolean {
  return 'needsLift' in data && data.needsLift === true
}

interface Props {
  readonly jobs: SuperScheduler.EventData[]
  readonly onEventsChange: (args: SchedulerEventsChangeArgs) => void
}

export function WorkshopPlanning({ jobs, onEventsChange }: Props) {
  const config = useMemo<SchedulerProps>(
    () => ({
      startDate: '2026-10-12',
      days: 5,
      scale: 'CellDuration',
      cellDuration: 30,
      cellWidth: 48,
      timeHeaders: TIME_HEADERS,
      businessBeginsHour: 8,
      businessEndsHour: 18,
      showNonBusiness: false,
      useEventBoxes: 'Never',
      allowEventOverlap: false,
      conflictHighlight: true,

      // Runs on every shadow change: keep it synchronous and cheap.
      onEventMoving: (args) => {
        if (args.start.getTime() < SuperScheduler.Date.now().getTime()) {
          args.allowed = false
          args.message = 'Jobs cannot be moved into the past.'
          return
        }
        if (needsLift(args.e.data) && !BAYS_WITH_LIFT.has(args.resource)) {
          args.allowed = false
          args.message = 'This job needs a bay with a lift.'
          return
        }
        // The waiting list may hold overlapping jobs; the bays may not (allowEventOverlap above).
        args.allowOverlap = args.resource === 'waiting'
      },

      onEventResizing: (args) => {
        const minutes = (args.end.getTime() - args.start.getTime()) / 60_000
        if (minutes < 30) {
          args.allowed = false
          args.message = 'A job takes at least 30 minutes.'
        }
      },
    }),
    [],
  )

  const owned = useMemo(() => jobs.slice(), [jobs])

  return (
    <SuperSchedulerComponent
      {...config}
      resources={BAYS}
      events={owned}
      onEventsChange={onEventsChange}
    />
  )
}

Sie sollten eine Fünf-Tage-Planung in Halbstundenzellen von 08:00 bis 18:00 Uhr sehen. Ziehen Sie einen Auftrag, der eine Hebebühne braucht, auf Bay 3: Der Schatten wird als verboten markiert, und die Karte zeigt „This job needs a bay with a lift.“. Ziehen Sie einen beliebigen Auftrag auf einem Arbeitsplatz über einen anderen: Das wird als Überlappung abgelehnt. Legen Sie ihn auf der Warteliste ab: Dort stapeln sich beide Aufträge. Kürzen Sie einen Auftrag auf unter 30 Minuten: Die Dauer-Änderung wird abgelehnt.

Überlappungen verhindern

allowEventOverlap={false} lehnt jede Verschiebung, Dauer-Änderung oder Zeitraumauswahl ab, die sich mit einem anderen Ereignis in derselben Zeile überlappen würde. Intervalle sind halboffen; direkt aufeinanderfolgende Ereignisse (eines endet um 11:00 Uhr, das nächste beginnt um 11:00 Uhr) gelten also nie als Überlappung.

Zwei Werkzeuge verfeinern die Regel je nach Situation:

  • args.allowOverlap in onEventMoving und onEventResizing überschreibt die Option für den aktuellen Frame, wie es die Warteliste oben tut. Der Wert wird bei jedem Aufruf zurückgesetzt.
  • args.conflicts listet die bestehenden Ereignisse, mit denen der Schatten in der Zielzeile kollidiert (bis zu acht), als SuperScheduler.Event-Wrapper. Damit unterscheiden Sie weiche von harten Konflikten:
src/soft-conflicts.tsts
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'

function isTentative(data: SuperScheduler.EventData): boolean {
  return 'status' in data && data.status === 'tentative'
}

/** Tentative jobs may be double-booked; confirmed ones may not. */
export const softConflicts: Pick<
  SchedulerProps,
  'allowEventOverlap' | 'conflictHighlight' | 'onEventMoving'
> = {
  allowEventOverlap: false,
  // Outlines the events the shadow collides with (data-conflict) while dragging.
  conflictHighlight: true,
  onEventMoving: (args) => {
    // Up to eight colliding events in the target row: feedback, not exhaustive validation.
    const hard = args.conflicts.find((event) => !isTentative(event.data))
    if (hard === undefined) {
      // For this frame only; the instance option stays false.
      args.allowOverlap = true
      return
    }
    args.allowed = false
    args.message = `Overlaps ${hard.text()}, which is confirmed.`
  },
}

conflictHighlight umrandet die kollidierenden Ereignisse, während der Nutzer zieht (sie erhalten ein Attribut data-conflict und eine Umrandung in der Warnfarbe). So sieht der Nutzer, was im Weg ist, und nicht nur, dass etwas im Weg ist.

Zeiten mit gesperrten Zellen blockieren

Eine gesperrte Zelle wird schraffiert gezeichnet und lehnt Verschiebungen, Dauer-Änderungen und Zeitraumauswahlen ab, die sie berühren. Zellen lassen sich auf zwei Arten sperren:

  • Eine ganze Zeile: cellsDisabled: true an der Ressource, für einen Arbeitsplatz in Reparatur oder ein Zimmer außer Betrieb.
  • Eine beliebige Zelle: Setzen Sie args.cell.properties.disabled = true in onBeforeCellRender, für Mittagspausen, Feiertage oder Öffnungszeiten pro Ressource.
src/ClosedTime.tsxtsx
import { useMemo } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerBeforeCellRenderArgs, SuperScheduler } from 'super-scheduler'

const BAYS: SuperScheduler.ResourceData[] = [
  { id: 'bay-1', name: 'Bay 1' },
  { id: 'bay-2', name: 'Bay 2' },
  // Closed for refurbishment: every cell of the row is disabled.
  { id: 'bay-3', name: 'Bay 3', cellsDisabled: true },
]

/**
 * Module level, so its identity never changes: a new function per render would invalidate the
 * per-cell cache. Disabled cells are hatched and reject drops, resizes and range selection.
 */
function closeLunchBreak(args: SchedulerBeforeCellRenderArgs): void {
  if (args.cell.start.getHours() === 13) {
    args.cell.properties.disabled = true
    args.cell.properties.cssClass = 'lunch-break'
  }
}

export function ClosedTime({ jobs }: { jobs: SuperScheduler.EventData[] }) {
  const owned = useMemo(() => jobs.slice(), [jobs])
  return (
    <SuperSchedulerComponent
      startDate="2026-10-12"
      days={5}
      scale="CellDuration"
      cellDuration={30}
      resources={BAYS}
      events={owned}
      onBeforeCellRender={closeLunchBreak}
    />
  )
}

Sie sollten Bay 3 über die ganze Zeile schraffiert sehen und jeden Arbeitsplatz von 13:00 bis 14:00 Uhr. Aufträge lassen sich weder in die Mittagspause ablegen noch über sie hinweg verlängern.

Weitere Möglichkeiten, geschlossene Zeiten auszudrücken: Blenden Sie sie mit showNonBusiness={false} oder onIncludeTimeCell aus der Achse aus (siehe Stunden, Minuten, Tage und Zoom), oder stellen Sie sie als Ereignisse dar, die sich weder verschieben noch in der Dauer ändern lassen (moveDisabled, resizeDisabled), etwa einen Wartungsblock; solche Ereignisse zählen ebenfalls als Überlappung, wenn allowEventOverlap false ist.

Einzelne Ereignisse sperren

Feld des EreignissesWirkung
moveDisabledDas Ereignis lässt sich nicht verschieben
resizeDisabledDie Dauer des Ereignisses lässt sich nicht ändern
moveHDisabledEs kann die Zeile wechseln, aber nicht die Zeit
moveVDisabledEs kann die Zeit wechseln, aber nicht die Zeile
clickDisabled, deleteDisabledEs ignoriert Klicks bzw. hat keinen Lösch-Button

Für den ganzen Planer schalten eventMoveHandling="Disabled" und eventResizeHandling="Disabled" die Gesten ab. Regeln, die davon abhängen, wer hinsieht (die Rezeption darf verschieben, ein Gast nicht), sind Berechtigungen: Berechnen Sie diese Felder aus der Rolle des Nutzers, bevor Sie die Ereignisse übergeben.

Beim Ablegen bestätigen, auch asynchron

onEventMove und onEventResize laufen einmal pro Ablegen, bevor sich etwas ändert. Sie können:

  • Abbrechen mit args.preventDefault().
  • Das Ergebnis abwandeln, indem Sie args.newStart, args.newEnd oder args.newResource zuweisen.
  • Aufschieben mit args.async = true und dann args.loaded() aufrufen, sobald Sie eine Antwort haben. Ein Aufruf von args.preventDefault() vor loaded() bricht das Ablegen ab.
src/confirm-move.tsts
import type {
  SchedulerEventMoveArgs,
  SchedulerEventResizeArgs,
  SuperScheduler,
} from 'super-scheduler'

function inProgress(data: SuperScheduler.EventData): boolean {
  return 'status' in data && data.status === 'inProgress'
}

/**
 * onEventMove: called once on drop, before the store changes. The library never awaits a
 * handler, so an asynchronous decision defers the drop with `async` and finishes it with `loaded()`.
 */
export function confirmMove(args: SchedulerEventMoveArgs): void {
  // A synchronous veto needs no async: cancel and return. This final check also covers moves
  // made with the keyboard.
  if (inProgress(args.e.data) && args.newResource !== args.e.resource()) {
    args.preventDefault()
    args.control.message('A job in progress stays in its bay.')
    return
  }

  args.async = true
  const resource = String(args.newResource)
  void (async () => {
    try {
      const question = `Move ${args.e.text()} to ${resource}, ${args.newStart.toString('ddd d MMM HH:mm')}?`
      if (!(await confirmWithUser(question))) {
        args.preventDefault()
        return
      }
      await saveBooking({
        id: String(args.e.id()),
        resource,
        start: args.newStart.value,
        end: args.newEnd.value,
      })
    } catch {
      args.preventDefault()
      args.control.message('The move could not be saved.')
    } finally {
      // Always: completes the drop, or cancels it when preventDefault() was called first.
      args.loaded()
    }
  })()
}

/** The same protocol for resizing; `what` tells which edge moved. */
export function confirmResize(args: SchedulerEventResizeArgs): void {
  args.async = true
  void saveBooking({
    id: String(args.e.id()),
    resource: String(args.e.resource()),
    start: args.newStart.value,
    end: args.newEnd.value,
  })
    .catch(() => args.preventDefault())
    .finally(() => args.loaded())
}

Binden Sie sie als onEventMove={confirmMove} und onEventResize={confirmResize} ein. Solange die Entscheidung aussteht, bleibt das Ereignis an seiner ursprünglichen Position; es bewegt sich, wenn loaded() das Ablegen abschließt, oder bleibt, wo es ist, wenn das Ablegen abgebrochen wurde.

Die Drag-Karte

Während des Ziehens zeigt eine Karte neben dem Zeiger die Zieldaten, die Dauer (Nächte bei ganztägigen Zeiträumen, sonst Stunden und Minuten), die Zielzeile und den Grund, warum eine Position abgelehnt wird: Ihre args.message oder das Ereignis, mit dem sie sich überlappt. Zusätzlich erscheint eine Datumsmarke im Zeitkopf (headerMarker). Beides ist standardmäßig aktiv.

Konfigurieren Sie die Karte mit einem einzigen stabilen Objekt:

src/drag-card.tsts
import { SuperScheduler } from 'super-scheduler'

// One stable object at module level: a new object per render would reconfigure the card.
export const DRAG_CARD: SuperScheduler.DragCardOptions = {
  // Intraday work: show the time of both edges.
  dateFormat: 'ddd d MMM HH:mm',
  movingDateFormat: 'ddd d MMM HH:mm',
  // Whole-day ranges count nights by default; other ranges show hours and minutes.
  duration: 'auto',
  row: true,
  labels: { overlapping: 'Overlaps', forbidden: 'Not allowed' },
}

// Replace the content when a refusal needs more room. The string is trusted HTML.
export const DRAG_CARD_WITH_REASON: SuperScheduler.DragCardOptions = {
  ...DRAG_CARD,
  html: (info) => {
    if (info.refusal === null) return null // null keeps the default content
    const reason = SuperScheduler.Util.escapeHtml(info.refusal)
    const row = SuperScheduler.Util.escapeHtml(info.rowName ?? '')
    return `<strong>${reason}</strong><br>${row}`
  },
}

Übergeben Sie dragCard={DRAG_CARD} oder dragCard={false}, um sie zu entfernen. Die Standardbeschriftungen sind ins Englische, Spanische, Katalanische, Baskische, Galicische, Deutsche, Französische, Italienische und Portugiesische übersetzt und folgen der locale des Planers; labels überschreibt sie. Die Option html erhält die Daten, den Zeilennamen, den ersten Konflikt, die Ablehnungsmeldung und die Position des Ereignisses vor dem Ziehen.

Ereignisse durch Auswahl von Zeit anlegen

Ziehen über leere Zellen wählt einen Zeitraum aus; onTimeRangeSelected meldet ihn mit start, end (exklusiv), resource und origin. Ob dort ein Ereignis entsteht, entscheidet Ihre Anwendung:

src/CreateOnSelect.tsxtsx
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type {
  SchedulerEventsChangeArgs,
  SchedulerTimeRangeSelectedArgs,
  SchedulerTimeRangeSelectingArgs,
  SuperScheduler,
} from 'super-scheduler'

const BAYS: SuperScheduler.ResourceData[] = [
  { id: 'bay-1', name: 'Bay 1' },
  { id: 'bay-2', name: 'Bay 2' },
]

/** Steers the selection while it is drawn, as onEventMoving does for moves: four hours at most. */
function limitToFourHours(args: SchedulerTimeRangeSelectingArgs): void {
  args.allowed = args.end.getTime() - args.start.getTime() <= 4 * 3_600_000
}

export function CreateOnSelect() {
  const [jobs, setJobs] = useState<SuperScheduler.EventData[]>([])
  const owned = useMemo(() => jobs.slice(), [jobs])
  const onEventsChange = useCallback(
    (args: SchedulerEventsChangeArgs) => setJobs([...args.events]),
    [],
  )

  const onTimeRangeSelected = useCallback((args: SchedulerTimeRangeSelectedArgs) => {
    // The selection shadow stays until cleared.
    args.control.clearSelection()
    // A plain click on an empty cell also selects it (origin 'click'): create only on a drag.
    if (args.origin !== 'drag') return
    setJobs((current) => [
      ...current,
      {
        id: crypto.randomUUID(),
        resource: args.resource,
        // Store strings: start and end arrive as SuperScheduler.Date (end exclusive).
        start: args.start.value,
        end: args.end.value,
        text: 'New job',
      },
    ])
  }, [])

  return (
    <SuperSchedulerComponent
      startDate="2026-10-12"
      days={5}
      scale="CellDuration"
      cellDuration={30}
      resources={BAYS}
      events={owned}
      allowEventOverlap={false}
      onTimeRangeSelecting={limitToFourHours}
      onTimeRangeSelected={onTimeRangeSelected}
      onEventsChange={onEventsChange}
    />
  )
}

Sie sollten sehen, wie ein Auswahlschatten dem Zeiger folgt, nicht über vier Stunden hinaus oder über einen anderen Auftrag wächst und beim Loslassen zu einem Ereignis „New job“ wird.

Drei Verhaltensweisen sollten Sie kennen:

  • Ein einfacher Klick ist ebenfalls eine Auswahl. Ein Klick auf eine leere Zelle löst onTimeRangeSelected mit origin: 'click' und einer Zelle aus. Prüfen Sie origin === 'drag', wenn ein Klick nichts anlegen darf.
  • Die Auswahl bleibt nach dem Loslassen sichtbar, bis zur nächsten Auswahl oder bis args.control.clearSelection().
  • Auswahlen folgen denselben Regeln wie Verschiebungen: Sie können weder gesperrte Zellen überqueren noch belegte Zeit, wenn allowEventOverlap false ist. onTimeRangeSelecting steuert sie Frame für Frame mit args.allowed.

Was in Ihrer Anwendung bleibt

Die Bibliothek setzt durch, was Sie konfigurieren, und meldet, was passiert. Ihre Anwendung verantwortet die Regeln selbst (welche Ressource welche Arbeit annimmt, wer was ändern darf), die serverseitige Validierung jeder Änderung und jede automatische Platzierung oder Optimierung. Das Verschieben einer Buchung plant nie die anderen um: Braucht Ihr Geschäft kaskadierende Änderungen, berechnen Sie sie und aktualisieren die Ereignisse selbst.

Termine in der PhysiotherapiepraxisEin Patient kann um 10 Uhr nicht. Finden Sie den nächsten Slot, der Pausen und Reinigung respektiert. Planung von FertigungsaufträgenDie Wartung wurde vorgezogen. Verschieben Sie den Auftrag und halten Sie die Arbeitsfolge ein. Platzbuchung im SportclubAm Vormittag reißt ein Padel-Netz. Kurs verschieben, Platz sperren und jeden Coach in seinem Dienst lassen.

Nächste Schritte