Skip to content
SuperScheduler

Media productionFramehouse (Fictional business)

Video production planning

A shoot runs long. Move the edit that depended on it, see why, and take it back.

Time scale
Demo with synthetic data
hours / days
Pro capabilities
Resource treeDrag and resizeDependenciesDetail panelUndo and redo
Framehouse
StagesStage A · CycloramaStage B · Kitchen setPost-productionEdit 1Edit 2Grading suiteMix stageCrewsCamera unit 1Camera unit 2Spring campaign · Offline editHarbor documentary · Offline editSpring campaign · GradeKitchen spot · GradeSpring campaign · MixKitchen spot · ShootKitchen spot · ShootFacility · Rigging maintenanceKitchen spot · Crew callKitchen spot · Crew callKitchen spot · Offline editHarbor documentary · ShootHarbor documentary · Crew callTuesday, October 13Wednesday, October 14Thursday, October 15Friday, October 168 AM12 PM4 PM8 AM12 PM4 PM8 AM12 PM4 PM8 AM12 PM4 PM

Loads SuperScheduler Pro only when you ask for it.

The situation

Framehouse is a fictional production company with two stages, two edit suites, a grading room, a mix stage and two camera crews. Producers juggle three clients in the same week, and every phase waits for the one before it: no offline edit before the shoot wraps, no grade before the offline locks.

In a spreadsheet the week looks fine until a shoot overruns. The planning board has to show what the overrun breaks, refuse the moves that make no sense (a shoot in the mix stage) and make every change easy to reverse.

What you will doYou will extend an overrunning shoot, watch its dependent edit turn red, move that edit to a free suite after the shoot, and undo a change with one keystroke.

Where the library ends and your application begins

SuperScheduler provides

  • Grouped resource tree with stages, post-production rooms and crews.
  • Drag, resize, snapping to four-hour blocks and overlap prevention per room.
  • Curved finish-to-start links drawn between sessions.
  • Undo/redo history with Ctrl/⌘+Z, and controlled events through onEventsChange.
  • Zoom levels from hours to days with a hidden night, keyboard navigation and announcements.

This example’s code decides

  • Which phase fits which room (a shoot needs a stage, a grade needs the grading room).
  • Which sessions depend on which, and when a dependency is broken.
  • Locked maintenance blocks and the messages shown when a move is refused.
  • The detail panel, the mission and every label in this page.

The code behind this demo

These are the files this page runs, not a simplified copy. They compile against the public SuperScheduler Pro exports.

The integration: zoom levels, hidden night hours, rendering hooks, and the callbacks where the rules decide whether a move is allowed.

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} />
}
Download the example source (.zip)The archive contains this example’s files and a README. It does not include SuperScheduler Pro or any download key.

Share this scenario

The link opens this example with the current view (time scale and options). It never contains your edits or personal data.

A different business, a related problemCreative agency resource planningA designer is double-booked. Hand two tasks to a colleague in one drag, check what they unblock, and take it back.Read the business caseCreative production