Saltar al contenido
SuperScheduler

Producción audiovisualFramehouse (Negocio ficticio)

Planificación de producción audiovisual

Un rodaje se alarga. Mueve la edición que dependía de él, entiende por qué y vuelve atrás.

Escala temporal
Demo con datos sintéticos
horas / días
Capacidades Pro
Árbol de recursosArrastrar y redimensionarDependenciasPanel de detalleDeshacer y rehacer
Framehouse
PlatósPlató A · CicloramaPlató B · Set de cocinaPosproducciónEdición 1Edición 2Sala de etalonajeSala de mezclaEquiposUnidad de cámara 1Unidad de cámara 2Campaña de primavera · Montaje offlineDocumental del puerto · Montaje offlineCampaña de primavera · EtalonajeSpot de cocina · EtalonajeCampaña de primavera · MezclaSpot de cocina · RodajeSpot de cocina · RodajeInstalaciones · Mantenimiento de riggingSpot de cocina · Convocatoria de equipoSpot de cocina · Convocatoria de equipoSpot de cocina · Montaje offlineDocumental del puerto · RodajeDocumental del puerto · Convocatoria de equipomartes, 13 de octubremiércoles, 14 de octubrejueves, 15 de octubreviernes, 16 de octubre08:0012:0016:0008:0012:0016:0008:0012:0016:0008:0012:0016:00

Carga SuperScheduler Pro solo cuando lo pides.

La situación

Framehouse es una productora ficticia con dos platós, dos salas de edición, una sala de etalonaje, una sala de mezcla y dos equipos de cámara. Producción reparte tres clientes en la misma semana y cada fase espera a la anterior: no hay montaje offline antes de terminar el rodaje ni etalonaje antes de cerrar el montaje.

En una hoja de cálculo la semana parece correcta hasta que un rodaje se alarga. El tablero tiene que mostrar qué rompe ese retraso, rechazar los movimientos sin sentido (un rodaje en la sala de mezcla) y permitir deshacer cualquier cambio con facilidad.

Qué vas a hacerAlargarás un rodaje, verás cómo la edición que depende de él se marca en rojo, la moverás a una sala libre después del rodaje y desharás un cambio con una sola tecla.

Dónde termina la librería y empieza tu aplicación

SuperScheduler aporta

  • Árbol de recursos agrupado con platós, salas de posproducción y equipos.
  • Arrastrar, redimensionar, ajuste a bloques de cuatro horas y prevención de solapes por sala.
  • Vínculos curvos fin-inicio dibujados entre sesiones.
  • Historial de deshacer/rehacer con Ctrl/⌘+Z y eventos controlados mediante onEventsChange.
  • Niveles de zoom de horas a días con la noche oculta, navegación por teclado y anuncios accesibles.

El código de este ejemplo decide

  • Qué fase cabe en qué sala (un rodaje necesita plató, un etalonaje la sala de etalonaje).
  • Qué sesiones dependen de cuáles y cuándo se rompe una dependencia.
  • Los bloqueos de mantenimiento y los mensajes al rechazar un movimiento.
  • El panel de detalle, la misión y todos los textos de esta página.

El código de esta demo

Son los archivos que ejecuta esta página, no una copia simplificada. Compilan contra los exports públicos de SuperScheduler Pro.

La integración: niveles de zoom, horas nocturnas ocultas, hooks de render y los callbacks donde las reglas deciden si un movimiento se permite.

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} />
}
Descargar el código del ejemplo (.zip)El archivo contiene los ficheros de este ejemplo y un README. No incluye SuperScheduler Pro ni ninguna clave de descarga.

Comparte este escenario

El enlace abre este ejemplo con la vista actual (escala y opciones). Nunca contiene tus cambios ni datos personales.

Otro negocio, un problema relacionadoPlanificación de recursos en una agencia creativaUna diseñadora tiene doble reserva. Pasa dos tareas a un compañero con un solo arrastre, revisa qué desbloquean y deshazlo.Leer el caso de negocioProducción creativa