Zum Inhalt springen
SuperScheduler

MedienproduktionFramehouse (Fiktives Unternehmen)

Planung einer Videoproduktion

Ein Dreh dauert länger. Verschieben Sie den abhängigen Schnitt, verstehen Sie warum, und nehmen Sie es zurück.

Zeitskala
Demo mit synthetischen Daten
Stunden / Tage
Pro-Funktionen
RessourcenbaumZiehen und Dauer ändernAbhängigkeitenDetailpanelRückgängig und Wiederholen
Framehouse
StudiosStudio A · HohlkehleStudio B · KüchensetPostproduktionSchnitt 1Schnitt 2FarbkorrekturMischungTeamsKamerateam 1Kamerateam 2Frühjahrskampagne · Offline-SchnittHafen-Doku · Offline-SchnittFrühjahrskampagne · FarbkorrekturKüchenspot · FarbkorrekturFrühjahrskampagne · MischungKüchenspot · DrehKüchenspot · DrehStudiobetrieb · Rigging-WartungKüchenspot · TeameinsatzKüchenspot · TeameinsatzKüchenspot · Offline-SchnittHafen-Doku · DrehHafen-Doku · TeameinsatzDienstag, 13. OktoberMittwoch, 14. OktoberDonnerstag, 15. OktoberFreitag, 16. Oktober08:0012:0016:0008:0012:0016:0008:0012:0016:0008:0012:0016:00

SuperScheduler Pro wird erst geladen, wenn Sie es anfordern.

Die Ausgangslage

Framehouse ist eine fiktive Produktionsfirma mit zwei Studios, zwei Schnitträumen, einer Farbkorrektur, einer Mischung und zwei Kamerateams. Die Produktion jongliert drei Kunden in derselben Woche, und jede Phase wartet auf die vorherige: kein Offline-Schnitt vor Drehschluss, keine Farbkorrektur vor dem Picture Lock.

In einer Tabelle sieht die Woche gut aus, bis ein Dreh überzieht. Die Planungstafel muss zeigen, was die Verzögerung kaputt macht, unsinnige Verschiebungen ablehnen (ein Dreh in der Mischung) und jede Änderung leicht umkehrbar machen.

Was Sie tun werdenSie verlängern einen überziehenden Dreh, sehen den abhängigen Schnitt rot werden, verschieben ihn in einen freien Raum nach Drehschluss und machen eine Änderung mit einem Tastendruck rückgängig.

Wo die Bibliothek endet und Ihre Anwendung beginnt

SuperScheduler liefert

  • Gruppierter Ressourcenbaum mit Studios, Postproduktionsräumen und Teams.
  • Ziehen, Dauer ändern, Einrasten auf Vier-Stunden-Blöcke und Überlappungsschutz pro Raum.
  • Geschwungene Ende-Anfang-Verknüpfungen zwischen Sessions.
  • Rückgängig/Wiederholen mit Strg/⌘+Z und kontrollierte Ereignisse über onEventsChange.
  • Zoomstufen von Stunden bis Tagen mit ausgeblendeter Nacht, Tastaturbedienung und Ansagen für Screenreader.

Der Code dieses Beispiels entscheidet

  • Welche Phase in welchen Raum passt (ein Dreh braucht ein Studio, die Farbkorrektur ihren eigenen Raum).
  • Welche Sessions voneinander abhängen und wann eine Abhängigkeit verletzt ist.
  • Gesperrte Wartungsblöcke und die Meldungen bei abgelehnten Verschiebungen.
  • Das Detailpanel, die Aufgabe 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: Zoomstufen, ausgeblendete Nachtstunden, Render-Hooks und die Callbacks, in denen die Regeln über Verschiebungen entscheiden.

scheduler.tsxtsx
/**
 * Framehouse planning board: stages, edit suites and crews over one production week.
 *
 * Library: timeline, tree rows, drag/resize, dependency links, undo history, keyboard, zoom.
 * Application: which rooms accept which phases, which sessions are locked, and what counts as a
 * dependency problem. Those rules live in ./rules and run inside the scheduler callbacks below.
 */
import { useEffect, useMemo, type RefObject } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventsChangeArgs, SchedulerProps } from 'super-scheduler'
import type { SchedulerHistory } from 'super-scheduler/history'
import { createHeaderHook } from '../../scheduler/live-headers'
import { toneClass } from '../../scheduler/tones'
import {
  PROJECT_TONES,
  readSessionFields,
  roomKind,
  toResources,
  type SessionEvent,
} from './adapter'
import { BUSINESS_HOURS, INITIAL_VIEW_START, WEEK } from './model'
import { acceptsPhase, type DependencyIssue } from './rules'
import type { FramehouseText, ZoomId } from './text'

export interface FramehouseSchedulerProps {
  readonly events: SessionEvent[]
  readonly issues: readonly DependencyIssue[]
  readonly text: FramehouseText
  /** 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
  readonly showLinks: boolean
  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, end: SuperScheduler.Date) => void
  readonly onRefused: (message: string) => void
  readonly onZoomLevel: (level: ZoomId) => void
}

const ZOOM_LEVELS: SuperScheduler.ZoomLevel[] = [
  {
    id: 'hours',
    properties: {
      scale: 'Hour',
      cellWidth: 46,
      timeHeaders: [{ groupBy: 'Day' }, { groupBy: 'Hour' }],
    },
  },
  {
    id: 'shifts',
    properties: {
      scale: 'CellDuration',
      cellDuration: 240,
      cellWidth: 104,
      timeHeaders: [{ groupBy: 'Day' }, { groupBy: 'Cell' }],
    },
  },
  {
    id: 'days',
    properties: {
      scale: 'Day',
      cellWidth: 220,
      timeHeaders: [{ groupBy: 'Week' }, { groupBy: 'Day' }],
    },
  },
]

const ZOOM_IDS: readonly ZoomId[] = ['hours', 'shifts', 'days']

export function FramehouseScheduler(props: FramehouseSchedulerProps) {
  const {
    events,
    issues,
    text,
    tag,
    schedulerLocale,
    initialZoom,
    showLinks,
    history,
    controlRef,
  } = props
  const { onEventsChange, onSelect, onMoved, onResized, onRefused, onZoomLevel } = props

  const resources = useMemo(() => toResources(text), [text])

  // Finish-to-start links come from the business model, not from the scheduler.
  const links = useMemo<SuperScheduler.LinkData[]>(() => {
    if (!showLinks) return []
    const late = new Set(issues.map((issue) => issue.session))
    return events.flatMap((event) =>
      event.after === undefined
        ? []
        : [
            {
              id: `${event.after}->${String(event.id)}`,
              from: event.after,
              to: event.id,
              type: 'FinishToStart',
              ...(late.has(String(event.id)) ? { cssClass: 'ss-link--late' } : {}),
            },
          ],
    )
  }, [events, issues, showLinks])

  const config = useMemo<SchedulerProps>(() => {
    const late = new Set(issues.map((issue) => issue.session))

    return {
      locale: schedulerLocale,
      startDate: WEEK.start,
      days: 5,
      zoomLevels: ZOOM_LEVELS,
      zoom: initialZoom,
      zoomPosition: 'left',
      // Nights stay hidden: the board shows the studio's working day only.
      businessBeginsHour: BUSINESS_HOURS.begin,
      businessEndsHour: BUSINESS_HOURS.end,
      showNonBusiness: false,
      useEventBoxes: 'Never',
      height: '100%',
      treeEnabled: true,
      rowHeaderWidth: 172,
      rowHeaderWidthAutoFit: false,
      eventHeight: 36,
      rowMarginTop: 3,
      rowMarginBottom: 3,
      allowEventOverlap: false,
      dragCard: { duration: false },
      durationBarVisible: false,
      showToolTip: false,
      eventHoverHandling: 'Disabled',
      keyboardEnabled: true,
      keyboardTarget: 'component',
      keyboardMode: 'Full',
      history,
      controlRef,
      onEventsChange,

      onBeforeTimeHeaderRender: createHeaderHook(tag),

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

      onBeforeEventRender: (args) => {
        const fields = readSessionFields(args.data)
        if (fields === null) return
        const { project, phase, locked } = fields
        const name = SuperScheduler.Util.escapeHtml(text.projects[project])
        const detail = SuperScheduler.Util.escapeHtml(text.phases[phase])
        args.data.html = `<span class="ss-evt"><span class="ss-evt__title">${name}</span><span class="ss-evt__meta">${detail}</span></span>`
        const classes = ['ss-ev', toneClass(PROJECT_TONES[project])]
        if (locked === true) classes.push('ss-ev--hatched')
        if (phase === 'crew') classes.push('ss-ev--ghost')
        if (late.has(String(args.data.id))) classes.push('ss-ev--late')
        args.data.cssClass = classes.join(' ')
      },

      // Room compatibility and locks are application rules; overlap is enforced by the library.
      onEventMoving: (args) => {
        const fields = readSessionFields(args.e.data)
        if (fields === null) return
        const { phase, locked } = fields
        if (locked === true) {
          args.allowed = false
          args.message = text.rules.locked
          return
        }
        const kind = roomKind(args.resource)
        if (kind === null || !acceptsPhase(kind, phase)) {
          args.allowed = false
          args.message = text.rules.wrongRoom[phase]
        }
      },

      // Final check before the commit (also covers keyboard moves).
      onEventMove: (args) => {
        const fields = readSessionFields(args.e.data)
        const kind = roomKind(args.newResource)
        if (fields !== null && (kind === null || !acceptsPhase(kind, fields.phase))) {
          args.preventDefault()
          onRefused(text.rules.wrongRoom[fields.phase])
        }
      },

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

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

  // 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 ProblemRessourcenplanung für eine KreativagenturEine Designerin ist doppelt gebucht. Übergeben Sie zwei Aufgaben mit einem Ziehen, prüfen Sie, was sie freigeben, und nehmen Sie es zurück.Zum AnwendungsfallKreativproduktion