Zum Inhalt springen
SuperScheduler

GrundbegriffeGilt fürLite und Pro

Ressourcen, Ereignisse und Intervalle

Zeilen sind ResourceData-Objekte mit einer id und einem name; Balken sind EventData-Objekte mit id, text, start, end und der ID der Ressource, zu der sie gehören. IDs sind Strings oder Zahlen und werden strikt verglichen, 1 und '1' sind also verschieden. Intervalle sind halboffen, [start, end), und Datumsangaben sind bürgerliche Zeitwerte (lokale Datum-Uhrzeit), geschrieben als ISO-Strings mit Sekunden; die Bibliothek rechnet nie Zeitzonen um. Eigene Felder ergänzen Sie mit EventData<YourFields> und grenzen sie ein, wenn sie vom Control zurückkommen.

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

SuperScheduler zeichnet zwei Arrays: Ressourcen, die Zeilen, und Ereignisse, die Balken in diesen Zeilen. Beides sind einfache Objekte, die Sie aus Ihren eigenen Daten erzeugen. Wer vier Regeln beachtet, vermeidet fast jedes Problem der Art „mein Ereignis erscheint nicht“: IDs sind strikt typisiert, Intervalle schließen ihr Ende aus, Datumsstrings enthalten Sekunden, und Zeiten sind lokale Uhrzeiten ohne Zeitzone.

Die Regeln auf dieser Seite gelten für beide Editionen, sofern ein Abschnitt nichts anderes sagt. Lite akzeptiert eine Teilmenge der Felder; siehe Schnellstart mit Lite.

Ressourcen

Eine Ressource braucht eine id und einen name. In Pro ist ResourceData offen: Sie können eigene Felder (floor, kind, capacity) im selben Objekt behalten und sie in Callbacks und Render-Hooks wieder auslesen.

Die Felder, die Sie in Pro am häufigsten verwenden:

FeldZweck
idString oder Zahl; eindeutig unter den Ressourcen
nameText im Zeilenkopf
backColor, cssClass, html, toolTipAussehen des Zeilenkopfs (html ist vertrauenswürdiges Markup)
minHeight, eventHeightGeometrie nur dieser Zeile
cellsDisabledJede Zelle der Zeile lehnt Ablegen und Auswahl ab
columnsZellen für zusätzliche Spalten im Zeilenkopf (mit rowHeaderColumns)
children, expandedEin Ressourcenbaum (mit treeEnabled)
frozen'top' oder 'bottom': Die Zeile bleibt beim Scrollen sichtbar

Ressourcenbäume

Um Zeilen zu gruppieren, verschachteln Sie Ressourcen in children und setzen treeEnabled am Planer. Ohne treeEnabled werden Kinder ignoriert, und die Liste bleibt flach. Ein Elternknoten startet eingeklappt, sofern sein Feld expanded nicht true ist. Elternzeilen können wie jede Zeile Ereignisse enthalten; setzen Sie treePreventParentUsage, damit sie reine Gruppenüberschriften bleiben. Bäume, Zeilenspalten und Zeilenauswahl behandelt Bäume, Spalten und Auswahl. Lite akzeptiert nur flache Listen.

Ereignisse

Ein Ereignis braucht id, text, start, end und, um in einer Zeile zu erscheinen, resource. Optionale Felder ändern Aussehen und Verhalten:

FeldZweck
backColor, fontColor, borderColor, barColorFarben des Balkens, seines Texts, seines Rahmens und des Dauerbalkens
cssClassKlassen für Ihr eigenes CSS
htmlInhalt als vertrauenswürdiges HTML (Nutzerdaten mit SuperScheduler.Util.escapeHtml escapen)
toolTip, bubbleHtmlNativer Tooltip oder Inhalt der Hover-Blase
moveDisabled, resizeDisabledDieses Ereignis gegen Verschieben oder Dauer ändern sperren
moveHDisabled, moveVDisabledVerschieben nur zwischen Zeilen oder nur in der Zeit erlauben
clickDisabled, deleteDisabledDieses Ereignis von Klicks oder Löschen ausnehmen
tagsEin beliebiger Wert für Ihre eigene Verwendung

Alle Felder dieser Tabelle gibt es nur in Pro, außer backColor, fontColor, cssClass, toolTip und tags, die auch Lite akzeptiert.

IDs sind Strings oder Zahlen und werden strikt verglichen

ResourceId und EventId sind string | number, und Vergleiche berücksichtigen Wert und Typ. Die Zahl 101 und der String '101' sind verschiedene IDs. Ein Ereignis mit resource: '101' wird nicht in einer Zeile mit der ID 101 gezeichnet, und control.events.find('7') findet das Ereignis mit der ID 7 nicht.

Das ist vor allem wichtig, wenn Daten aus mehreren Quellen kommen: Ein Datenbanktreiber liefert vielleicht numerische Zimmer-IDs, während ein Formular oder eine URL Strings liefert. Normalisieren Sie IDs an der Stelle, an der Daten in Ihre Anwendung gelangen, und verwenden Sie pro Art von ID einen einzigen Typ.

Intervalle sind halboffen

Ein Ereignis belegt [start, end): Der Anfangszeitpunkt gehört dazu, der Endzeitpunkt nicht. Drei Folgen:

  • Direkt aufeinanderfolgende Ereignisse überlappen sich nicht. Ein Aufenthalt, der um 11:00 Uhr endet, und der nächste, der um 11:00 Uhr im selben Zimmer beginnt, sind vereinbar, auch wenn Überlappungen abgelehnt werden.
  • Ein reines Datum als Ende ist der erste freie Tag. start: '2026-10-02', end: '2026-10-05' umfasst den 2., 3. und 4. Oktober. Soll auch der 5. erscheinen, ist das Ende '2026-10-06'.
  • Dauern sind einfache Differenzen. end - start ist die Dauer, ohne Korrekturen um „plus einen Tag“.

Ihr Backend sollte dieselbe Regel verwenden. Zwei Intervalle überlappen sich, wenn a.start < b.end && b.start < a.end; eine Abfrage nach dem, was zwischen from und to sichtbar ist, lautet start < to AND end > from.

In Pro schaltet eventEndSpec: 'Date' für tagesbasierte Planungen auf inklusive Enden ohne Uhrzeit um: Ein Ereignis, das auf '2026-10-05' endet, umfasst dann den 5. Die Bibliothek rechnet den Wert intern um und gibt ihn in derselben Konvention zurück. Verwenden Sie pro Planer eine einzige Konvention.

Datumsstrings

start, end und jede Datumsoption akzeptieren ein SuperScheduler.Date oder einen ISO-8601-String:

EingabeAkzeptiertGelesen als
'2026-10-02'jaMitternacht zu Beginn des 2. Oktober
'2026-10-02T14:00:00'ja14:00 Uhr
'2026-10-02 14:00:00'ja14:00 Uhr (ein Leerzeichen statt T)
'2026-10-02T14:00:00.250'jaMit Millisekunden
'2026-10-02T14:00'nein, wirft einen FehlerSekunden sind Pflicht
'2026-10-02T14:00:00+02:00'ja, mit Vorsicht12:00 Uhr: Ein Offset rechnet den Wert in die UTC-Uhrzeit um
new Date()neinEin natives Date besteht die Typprüfung als start oder end nicht

Zwei Regeln verhindern die meisten Überraschungen: Geben Sie immer Sekunden an, und senden Sie keine Offsets oder Z, es sei denn, Sie wollen die UTC-Uhrzeit. Formatieren Sie Werte als yyyy-MM-ddTHH:mm:ss in der Zeitzone des Ortes, der geplant wird.

Bürgerliche Zeit, keine Zeitzonen

SuperScheduler arbeitet mit bürgerlicher Zeit (lokale Datum-Uhrzeit): 2026-10-25T02:30:00 ist „halb drei am 25.“, genau so, wie es dasteht, ohne Zeitzone und ohne Sommerzeitverschiebungen. Der Planer zeigt an, was Sie ihm geben, und gibt Werte in derselben Form zurück.

Genau das braucht eine Planungstafel: Ein Hotel in Madrid zeigt den Check-in für alle Nutzer um 14:00 Uhr Ortszeit an, egal wo deren Browser steht. Es bedeutet aber auch, dass die Umrechnungen Ihre Sache sind:

  • Speichert Ihr Backend Zeitpunkte (UTC-Zeitstempel), rechnen Sie sie vor der Übergabe in die Ortszeit des Standorts der Ressource um und beim Speichern zurück in Zeitpunkte.
  • Liegen verschiedene Ressourcen in verschiedenen Zeitzonen, entscheiden Sie, welche Ortszeit die Ansicht zeigt; der Planer hat eine einzige Zeitachse.
  • Wiederkehrende Ereignisse (jeden Montag um 9:00 Uhr) müssen den Planer als konkrete Vorkommen erreichen, die Ihre Anwendung expandiert hat.

Eigene Felder mit EventData<T>

Ihre Ereignisse tragen meist mehr als eine Beschriftung: einen Gastcode, einen Status, einen Preis. In Pro hat EventData keine Indexsignatur, daher scheitert ein Objektliteral mit zusätzlichen Eigenschaften an der Excess-Property-Prüfung von TypeScript. Deklarieren Sie Ihre Felder einmal und verwenden Sie den generischen Typ SuperScheduler.EventData<YourFields>:

src/bookings.tsts
import type { SuperScheduler } from 'super-scheduler'

/** Fields your application adds to every booking. */
export interface BookingFields {
  guestCode: string
  status: 'tentative' | 'confirmed' | 'checkedIn'
  adults: number
}

export type BookingEvent = SuperScheduler.EventData<BookingFields>

// ResourceData accepts extra properties: keep your own row fields next to id and name.
export const rooms: SuperScheduler.ResourceData[] = [
  { id: 101, name: 'Room 101', floor: 1, kind: 'double' },
  { id: 102, name: 'Room 102', floor: 1, kind: 'suite' },
]

// EventData has no index signature: type the array so literals may carry custom fields.
export const bookings: BookingEvent[] = [
  {
    id: 'bk-1042',
    resource: 101, // the same type as the room id: 101 and '101' are different ids
    start: '2026-10-02T14:00:00',
    end: '2026-10-05T11:00:00',
    text: 'Booking 1042',
    guestCode: 'G-1042',
    status: 'confirmed',
    adults: 2,
  },
]

const STATUSES: ReadonlySet<string> = new Set(['tentative', 'confirmed', 'checkedIn'])

/**
 * Objects that come back from the control (handler arguments, onEventsChange) are typed as plain
 * EventData. Narrow them instead of casting, so a malformed object is caught where it appears.
 */
export function isBooking(data: SuperScheduler.EventData): data is BookingEvent {
  return (
    'guestCode' in data &&
    typeof data.guestCode === 'string' &&
    'status' in data &&
    typeof data.status === 'string' &&
    STATUSES.has(data.status) &&
    'adults' in data &&
    typeof data.adults === 'number'
  )
}

Daten, die vom Control zurückkommen, sind als einfaches EventData typisiert: Handler-Argumente, onEventsChange, control.events.list. Die Bibliothek behält Ihre Felder, aber TypeScript kann nicht wissen, dass sie da sind. Grenzen Sie mit einem Type Guard wie isBooking ein, statt zu casten; ein Guard fängt auch Objekte ab, die Ihr eigener Code fehlerhaft gebaut hat. Render-Hooks erhalten eine offene Kopie (args.data in onBeforeEventRender), dort können Sie Felder also direkt lesen, aber ein Guard hält die Typen exakt:

src/TypedPlanning.tsxtsx
import { useMemo } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerProps } from 'super-scheduler'
import { bookings, isBooking, rooms } from './typed-data'

export function TypedPlanning({ onOpen }: { onOpen: (guestCode: string) => void }) {
  const owned = useMemo(() => bookings.slice(), [])

  const config = useMemo<SchedulerProps>(
    () => ({
      startDate: '2026-10-01',
      days: 31,
      scale: 'Day',
      onBeforeEventRender: (args) => {
        // args.data is a per-render copy: start and end are always SuperScheduler.Date here.
        if (!isBooking(args.data)) return
        const nights = Math.round(
          (args.data.end.getTime() - args.data.start.getTime()) / 86_400_000,
        )
        // html is trusted markup: escape every value that came from users.
        const code = SuperScheduler.Util.escapeHtml(args.data.guestCode)
        args.data.html = `${code} · ${nights} night${nights === 1 ? '' : 's'}`
        args.data.cssClass = `booking booking--${args.data.status}`
      },
      onEventClick: (args) => {
        // args.e is a wrapper: id(), start(), text() are methods; data is the raw object.
        const data = args.e.data
        if (isBooking(data)) onOpen(data.guestCode)
      },
    }),
    [onOpen],
  )

  return <SuperSchedulerComponent {...config} resources={rooms} events={owned} />
}

Sie sollten jede Buchung mit ihrem Gastcode und der Anzahl der Nächte beschriftet sehen, und beim Klick wird onOpen mit dem Gastcode aufgerufen.

Das EventData von Lite ist nicht generisch und akzeptiert in TypeScript keine zusätzlichen Felder: Legen Sie Anwendungsdaten in tags ab, die onEventClick in e.data.tags zurückgibt.

Werte nach dem Ziehen oder Ändern der Dauer

Wenn ein Nutzer in Pro ein Ereignis verschiebt oder seine Dauer ändert, bearbeitet die Bibliothek Ihr Objekt nicht. Sie ersetzt es durch ein neues Objekt, { ...old, start, end, resource }, in dem start und end Instanzen von SuperScheduler.Date sind. Ereignisse, die niemand angefasst hat, behalten die Strings, die Sie übergeben haben. Code, der Ereignisse liest, muss daher beide Formen akzeptieren:

src/event-dates.tsts
import { SuperScheduler } from 'super-scheduler'

/**
 * After a drag or a resize, the committed event holds SuperScheduler.Date values in start and end;
 * events nobody touched keep the strings you passed. Normalize both to one canonical string.
 */
export function toIso(value: SuperScheduler.DateInput): string {
  // `value` is `yyyy-MM-ddTHH:mm:ss` (plus `.fff` when milliseconds are not zero).
  return typeof value === 'string' ? new SuperScheduler.Date(value).value : value.value
}

/** The part of an event your backend stores. */
export function toSavePayload(event: SuperScheduler.EventData) {
  if (event.resource === undefined) throw new Error(`Event ${String(event.id)} has no resource`)
  return {
    id: String(event.id),
    resource: String(event.resource),
    start: toIso(event.start),
    end: toIso(event.end),
  }
}

/** Half-open overlap, the rule the scheduler applies: touching intervals do not overlap. */
export function overlaps(a: SuperScheduler.EventData, b: SuperScheduler.EventData): boolean {
  const date = (value: SuperScheduler.DateInput) => new SuperScheduler.Date(value)
  return SuperScheduler.Util.overlaps(date(a.start), date(a.end), date(b.start), date(b.end))
}

Einige Details helfen, wenn Sie diese Werte speichern oder vergleichen:

  • JSON.stringify schreibt ein SuperScheduler.Date als yyyy-MM-ddTHH:mm:ss, ein ganzes Ereignisobjekt wird also zu gültigen ISO-Strings serialisiert.
  • String(date) und date.value liefern denselben Text; date.toString('d MMM HH:mm', 'en-us') formatiert mit einem Muster und einer Locale.
  • Vergleichen Sie Datumsangaben mit a.equals(b) oder über getTime(). === vergleicht die Objektidentität.
  • getDay() ist der Tag im Monat (1 bis 31), anders als beim nativen Date. Den Wochentag liefern getDayOfWeek() (0 ist Sonntag) oder dayOfWeekISO() (1 ist Montag).
  • Prüfen Sie den Typ mit value instanceof SuperScheduler.Date, nie über den Namen des Konstruktors. Verwenden Sie in Lite das eigene SuperScheduler.Date von Lite; sind beide Editionen installiert, tauschen Sie ISO-Strings zwischen ihnen aus.

Planung einer MietwagenflotteEin Kleinwagen fällt am Abholtag aus. Die Miete auf ein anderes Auto legen, die Aufbereitung einhalten und sehen, wo die Flotte knapp wird. Gerätebuchung im LaborWer ein Gerät bucht, bekommt die Kalibrierung mit. Eine Sitzung aus dem Servicefenster holen, dann den eigenen Lauf verlängern.

Nächste Schritte