Zum Inhalt springen
SuperScheduler

Live-VeranstaltungenAurora Live (Fiktives Unternehmen)

Bühnenplanung für ein Festival

Ein Linecheck ragt in den Puffer vor einem Auftritt. Auf fünf Minuten zoomen, kürzen und sehen, welche Räume und Crews am Act hängen.

Zeitskala
Demo mit synthetischen Daten
Minuten / Stunden
Pro-Funktionen
AbhängigkeitenStufenloser ZoomMinutenrasterDetailpanelTheming
Aurora Live
BühnenAurora-BühneZeltClubRäumeBackstage ABackstage BPresseraumCrewsTon-Crew ATon-Crew BLicht-CrewBühnentechnikLumen ArcLinecheck · Glass OrchardGlass OrchardNova TideNorthern HumLinecheck · Saltwater ChoirSaltwater ChoirLinecheck · Velvet StaticVelvet StaticLinecheck · Kilo BloomGlass OrchardNova TideNorthern HumSaltwater ChoirVelvet StaticOra LuxSaltwater ChoirLumen ArcNova TideTagschichtNachtschichtTagschichtNachtschichtShowschichtUmbauUmbauSamstag, 24. Oktober16:3016:4517:0017:1517:3017:4518:0018:1518:3018:4519:0019:1519:3019:4520:0020:1520:3020:4521:0021:15

SuperScheduler Pro wird erst geladen, wenn Sie es anfordern.

Die Ausgangslage

Aurora Live ist ein fiktives Eintagesfestival mit drei Bühnen, zwei Backstage-Räumen, einem Presseraum und vier Crews. Die Running Order steht Wochen vorher fest; was die Produktion am Tag selbst noch plant, sind die Linechecks, eingeklemmt zwischen Auftritten und jeweils mit einem Puffer, bevor der Act auf die Bühne geht.

In einer Tabelle verschwinden fünf Minuten: Ein Check, der zehn Minuten vor einem Auftritt endet, sieht auf Stundenbasis gut aus. Das Board muss bis auf die Minute zoomen, jeden Check mit seinem Auftritt verbinden, unsinnige Verschiebungen ablehnen (ein Check auf einer fremden Bühne, ein veröffentlichter Auftritt) und zeigen, welche Räume und Crews ein Act betrifft.

Was Sie tun werdenSie öffnen das Ablaufblatt eines Acts im Seitenpanel, zoomen auf Fünf-Minuten-Zellen und kürzen einen Linecheck, bis seine Verknüpfung zum Auftritt wieder neutral ist.

Wo die Bibliothek endet und Ihre Anwendung beginnt

SuperScheduler liefert

  • Zeitachse innerhalb eines Tages ohne die Nacht (Geschäftszeiten, Samstag eingeschlossen) und Zoomstufen von Stunden bis zu 15- und 5-Minuten-Zellen, animiert über control.zoom.
  • Gruppierte Zeilen für Bühnen, Räume und Crews sowie die Zeilenauswahl-API, die sie hervorhebt.
  • Geschwungene Ende-Anfang-Verknüpfungen zwischen jedem Linecheck und seinem Auftritt.
  • Ziehen und Dauer ändern im Raster der aktiven Zelle, keine Überschneidung pro Bühne, Ablehnungsmeldungen auf der Drag-Karte, Rückgängig und Wiederholen sowie Tastaturbedienung.
  • Theming über CSS-Custom-Properties und Regeln ohne Layer am Root des Controls (festival.css).

Der Code dieses Beispiels entscheidet

  • Welche Blöcke sich bewegen (nur Linechecks), auf welcher Bühne und wie kurz ein Check sein darf.
  • Der 20-Minuten-Puffer zwischen Check und Auftritt, wann seine Verknüpfung rot wird und welche Verknüpfungen gezeichnet werden (zu kurze Puffer und der gewählte Act, oder alle).
  • Welche Räume und Crews zu einem Act gehören, und das Seitenpanel, das zu Check oder Auftritt springt.
  • Die Kopfzeilen bei fünf Minuten, die Mission und alle Texte dieser Seite.

Der Code hinter dieser Demo

Das sind die Dateien, die diese Seite ausführt, keine vereinfachte Kopie. Sie kompilieren gegen die öffentlichen Exporte von SuperScheduler Pro.

Die Integration: drei Zoomstufen, ausgeblendete Nachtstunden, die Kopfzeilen bei fünf Minuten, das Hervorheben von Zeilen über rows.selection und die Callbacks, in denen die Regeln eine Verschiebung oder Änderung der Dauer ablehnen.

scheduler.tsxtsx
/**
 * Aurora Live running order: stages, rooms and crews over one festival day, down to five minutes.
 *
 * Library: timeline with hidden night, zoom levels from hours to 5-minute cells, tree rows, row
 * selection (used to highlight related rows), dependency links, drag/resize with overlap
 * prevention, undo history, keyboard, and theming through CSS custom properties (./festival.css).
 * Application: which blocks can move, the line-check buffer before each set, the shortest check,
 * and which rows relate to an act. Those rules live in ./rules and run inside the callbacks below.
 */
import { useEffect, useMemo, useState, type RefObject } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type {
  SchedulerBeforeTimeHeaderRenderArgs,
  SchedulerEventsChangeArgs,
  SchedulerProps,
} from 'super-scheduler'
import type { SchedulerHistory } from 'super-scheduler/history'
import { formatCivil } from '../../scheduler/civil'
import { headerLabel, usesTwelveHours } from '../../scheduler/header-format'
import { createHeaderHook } from '../../scheduler/live-headers'
import { toneClass } from '../../scheduler/tones'
import { KIND_TONES, readSlotFields, slotLabel, toResources, type SlotEvent } from './adapter'
import {
  FESTIVAL_DAY,
  INITIAL_VIEW_START,
  OPEN_HOURS,
  type ActId,
  type RowId,
  type Slot,
} from './model'
import { checkTooShort, moveProblem, type MoveProblem } from './rules'
import type { AuroraText, ZoomId } from './text'
import './festival.css'

export interface AuroraSchedulerProps {
  readonly events: SlotEvent[]
  readonly slots: readonly Slot[]
  /** Acts whose line check ends too close to their set. */
  readonly shortBuffers: readonly ActId[]
  /** Rows that relate to the act in the side panel (highlighted). */
  readonly relatedRows: readonly RowId[]
  readonly text: AuroraText
  /** BCP 47 tag for labels, and the scheduler locale id. */
  readonly tag: string
  readonly schedulerLocale: string
  /** Level on mount. Later changes go through control.zoom so the view animates from where it is. */
  readonly initialZoom: ZoomId
  /** Every check-to-set link, or only those of the selected act and of short buffers. */
  readonly showAllLinks: boolean
  readonly selectedAct: ActId | null
  readonly history: SchedulerHistory
  readonly controlRef: RefObject<SuperScheduler.Scheduler | null>
  readonly onEventsChange: (args: SchedulerEventsChangeArgs) => void
  readonly onSelect: (id: string) => void
  readonly onMoved: (id: string) => void
  readonly onResized: (id: string) => void
  readonly onRefused: (message: string) => void
  readonly onZoomLevel: (level: ZoomId) => void
}

const ZOOM_LEVELS: SuperScheduler.ZoomLevel[] = [
  {
    id: 'hours',
    properties: {
      scale: 'Hour',
      cellWidth: 80,
      timeHeaders: [{ groupBy: 'Day' }, { groupBy: 'Hour' }],
    },
  },
  {
    id: 'quarter',
    properties: {
      scale: 'CellDuration',
      cellDuration: 15,
      cellWidth: 64,
      timeHeaders: [{ groupBy: 'Day' }, { groupBy: 'Cell' }],
    },
  },
  {
    id: 'five',
    properties: {
      scale: 'CellDuration',
      cellDuration: 5,
      cellWidth: 26,
      timeHeaders: [{ groupBy: 'Hour' }, { groupBy: 'Cell' }],
    },
  },
]

const ZOOM_IDS: readonly ZoomId[] = ['hours', 'quarter', 'five']

/**
 * Time headers: the shared Intl labels, except at five minutes where the hour sits above and the
 * minutes (":05") below, so 26 px cells stay readable.
 */
function festivalHeaders(tag: string): (args: SchedulerBeforeTimeHeaderRenderArgs) => void {
  const shared = createHeaderHook(tag)
  return (args) => {
    if (args.control.cellDuration !== 5) {
      shared(args)
      return
    }
    const ticks = args.header.start.getTime()
    const minutes = String(args.header.start.getMinutes()).padStart(2, '0')
    args.header.html =
      args.header.level === 0
        ? SuperScheduler.Util.escapeHtml(headerLabel(ticks, 'hour', tag, 'group'))
        : `:${minutes}`
  }
}

export function AuroraScheduler(props: AuroraSchedulerProps) {
  const {
    events,
    slots,
    shortBuffers,
    relatedRows,
    selectedAct,
    text,
    tag,
    schedulerLocale,
    initialZoom,
    showAllLinks,
    history,
    controlRef,
  } = props
  const { onEventsChange, onSelect, onMoved, onResized, onRefused, onZoomLevel } = props

  const resources = useMemo(() => toResources(text), [text])
  // Narrower row headers on phones, chosen once on mount.
  const rowHeaderWidth = useMemo(
    () => (window.matchMedia('(max-width: 639px)').matches ? 116 : 176),
    [],
  )
  // Header labels follow the active level. An animated zoom renders the new level's headers before
  // control.cellDuration reports it, so the hook is rebuilt (headers repainted) once a zoom lands.
  const [headerHook, setHeaderHook] = useState(() => festivalHeaders(tag))

  // Each act's line check leads to its set: a finish-to-start link, red when the buffer is short.
  // Morning checks would cross a whole stage row, so by default only the selected act and the
  // short buffers show theirs.
  const links = useMemo<SuperScheduler.LinkData[]>(() => {
    const short = new Set(shortBuffers)
    return slots.flatMap((check) => {
      const set =
        check.kind === 'check'
          ? slots.find((slot) => slot.kind === 'set' && slot.act === check.act)
          : undefined
      if (set === undefined || check.act === undefined) return []
      if (!showAllLinks && !short.has(check.act) && check.act !== selectedAct) return []
      return [
        {
          id: `${check.act}-buffer`,
          from: check.id,
          to: set.id,
          type: 'FinishToStart' as const,
          ...(short.has(check.act) ? { cssClass: 'ss-link--late' } : {}),
        },
      ]
    })
  }, [slots, shortBuffers, showAllLinks, selectedAct])

  const config = useMemo<SchedulerProps>(() => {
    const short = new Set<string>(shortBuffers)
    const related = new Set<string>(relatedRows)
    const pattern = usesTwelveHours(tag) ? 'h:mm tt' : 'HH:mm'
    const clock = (value: string) => formatCivil(value, tag, { hour: 'numeric', minute: '2-digit' })
    const refusal = (problem: MoveProblem) =>
      problem === 'published'
        ? text.rules.published
        : problem === 'follows'
          ? text.rules.follows
          : text.rules.wrongStage

    return {
      locale: schedulerLocale,
      startDate: FESTIVAL_DAY,
      days: 1,
      zoomLevels: ZOOM_LEVELS,
      zoom: initialZoom,
      zoomPosition: 'left',
      // The site opens at noon: the morning stays hidden. A Saturday is a working day here.
      businessBeginsHour: OPEN_HOURS.begin,
      businessEndsHour: OPEN_HOURS.end,
      businessWeekends: true,
      showNonBusiness: false,
      useEventBoxes: 'Never',
      height: '100%',
      cssClass: 'al-board',
      treeEnabled: true,
      treePreventParentUsage: true,
      rowHeaderWidth,
      rowHeaderWidthAutoFit: false,
      eventHeight: 32,
      rowMarginTop: 4,
      rowMarginBottom: 4,
      allowEventOverlap: false,
      linkLayer: 'Below',
      timeRangeSelectedHandling: 'Disabled',
      // Minute-level work: the drag card and header marker show clock times, not dates.
      dragCard: {
        duration: false,
        dateFormat: pattern,
        movingDateFormat: pattern,
        markerFormat: pattern,
      },
      durationBarVisible: false,
      showToolTip: false,
      eventHoverHandling: 'Disabled',
      keyboardEnabled: true,
      keyboardTarget: 'component',
      keyboardMode: 'Full',
      history,
      controlRef,
      onEventsChange,

      onBeforeTimeHeaderRender: headerHook,

      onBeforeRowHeaderRender: (args) => {
        args.row.cssClass = args.row.data.children !== undefined ? 'ss-rh--group' : ''
      },

      // The selected act's rows are also tinted across the grid (headers come from rows.selection).
      onBeforeCellRender: (args) => {
        if (related.has(String(args.cell.resource))) args.cell.properties.cssClass = 'al-related'
      },

      onBeforeEventRender: (args) => {
        const fields = readSlotFields(args.data)
        if (fields === null) return
        const label = SuperScheduler.Util.escapeHtml(slotLabel(fields, text))
        const time =
          fields.kind === 'set'
            ? `<span class="ss-evt__meta">${SuperScheduler.Util.escapeHtml(clock(args.data.start.value))}</span>`
            : ''
        args.data.html = `<span class="ss-evt"><span class="ss-evt__title">${label}</span>${time}</span>`
        const classes = ['ss-ev', toneClass(KIND_TONES[fields.kind])]
        if (fields.kind === 'set') classes.push('al-set')
        if (fields.kind === 'check') classes.push('ss-ev--ghost')
        if (fields.kind === 'changeover') classes.push('ss-ev--hatched')
        if (fields.kind === 'set' && fields.act !== undefined && short.has(fields.act))
          classes.push('ss-ev--late')
        args.data.cssClass = classes.join(' ')
      },

      // Only line checks move, and only on their act's stage; overlap is refused by the library.
      onEventMoving: (args) => {
        const fields = readSlotFields(args.e.data)
        const problem = fields === null ? null : moveProblem(fields, String(args.resource))
        if (problem !== null) {
          args.allowed = false
          args.message = refusal(problem)
        }
      },

      // Final check before the commit (also covers keyboard moves).
      onEventMove: (args) => {
        const fields = readSlotFields(args.e.data)
        const problem = fields === null ? null : moveProblem(fields, String(args.newResource))
        if (problem !== null) {
          args.preventDefault()
          onRefused(refusal(problem))
        }
      },

      onEventResizing: (args) => {
        if (checkTooShort(args.start.value, args.end.value)) {
          args.allowed = false
          args.message = text.rules.tooShort
        }
      },

      onEventResize: (args) => {
        if (checkTooShort(args.newStart.value, args.newEnd.value)) {
          args.preventDefault()
          onRefused(text.rules.tooShort)
        }
      },

      onEventMoved: (args) => onMoved(String(args.e.id())),
      onEventResized: (args) => onResized(String(args.e.id())),
      onEventClick: (args) => onSelect(String(args.e.id())),

      onZoom: (args) => {
        if (args.phase !== 'end') return
        setHeaderHook(() => festivalHeaders(tag))
        const level = ZOOM_IDS[args.level]
        if (level !== undefined) onZoomLevel(level)
      },
    }
  }, [
    history,
    controlRef,
    shortBuffers,
    relatedRows,
    rowHeaderWidth,
    onEventsChange,
    onMoved,
    onRefused,
    onResized,
    onSelect,
    onZoomLevel,
    schedulerLocale,
    headerHook,
    tag,
    text,
    initialZoom,
  ])

  // Highlighted rows are the control's row selection, driven by the act in the side panel.
  useEffect(() => {
    const control = controlRef.current
    if (control === null || control.disposed()) return
    control.rows.selection.clear()
    for (const id of relatedRows) {
      const row = control.rows.find(id)
      if (row !== null) control.rows.selection.add(row)
    }
  }, [controlRef, relatedRows])

  // Imperative and once: a scroll prop would be re-applied on every update.
  useEffect(() => {
    controlRef.current?.scrollTo(INITIAL_VIEW_START, false, 'left')
  }, [controlRef])

  return <SuperSchedulerComponent {...config} resources={resources} events={events} links={links} />
}
Quellcode des Beispiels herunterladen (.zip)Das Archiv enthält die Dateien dieses Beispiels und eine README. SuperScheduler Pro und Download-Schlüssel sind nicht enthalten.

Dieses Szenario teilen

Der Link öffnet dieses Beispiel mit der aktuellen Ansicht (Zeitskala und Optionen). Er enthält nie Ihre Änderungen oder persönliche Daten.

Eine andere Branche, ein verwandtes ProblemPlatzbuchung im SportclubAm Vormittag reißt ein Padel-Netz. Kurs verschieben, Platz sperren und jeden Coach in seinem Dienst lassen.Zum AnwendungsfallSpielstätten und Veranstaltungen