Aller au contenu
SuperScheduler

Production audiovisuelleFramehouse (Entreprise fictive)

Planification de production audiovisuelle

Un tournage déborde. Déplacez le montage qui en dépendait, comprenez pourquoi, puis revenez en arrière.

Échelle de temps
Démo avec données fictives
heures / jours
Fonctionnalités Pro
Arborescence de ressourcesGlisser et redimensionnerDépendancesPanneau de détailAnnuler et rétablir
Framehouse
PlateauxPlateau A · CycloPlateau B · Décor cuisinePost-productionMontage 1Montage 2Salle d’étalonnageAuditorium de mixageÉquipesUnité caméra 1Unité caméra 2Campagne de printemps · Montage offlineDocumentaire du port · Montage offlineCampagne de printemps · ÉtalonnageSpot cuisine · ÉtalonnageCampagne de printemps · MixageSpot cuisine · TournageSpot cuisine · TournageStudio · Maintenance du grillSpot cuisine · Convocation équipeSpot cuisine · Convocation équipeSpot cuisine · Montage offlineDocumentaire du port · TournageDocumentaire du port · Convocation équipemardi 13 octobremercredi 14 octobrejeudi 15 octobrevendredi 16 octobre08:0012:0016:0008:0012:0016:0008:0012:0016:0008:0012:0016:00

SuperScheduler Pro n’est chargé que lorsque vous le demandez.

La situation

Framehouse est une société de production fictive avec deux plateaux, deux salles de montage, une salle d’étalonnage, un auditorium de mixage et deux équipes caméra. La production jongle avec trois clients la même semaine et chaque étape attend la précédente : pas de montage offline avant la fin du tournage, pas d’étalonnage avant le verrouillage du montage.

Dans un tableur, la semaine semble tenir jusqu’à ce qu’un tournage déborde. Le planning doit montrer ce que ce retard casse, refuser les déplacements absurdes (un tournage dans l’auditorium de mixage) et rendre chaque modification facile à annuler.

Ce que vous allez faireVous allez prolonger un tournage, voir le montage qui en dépend passer au rouge, le déplacer dans une salle libre après le tournage, puis annuler une modification d’une seule touche.

Où s’arrête la bibliothèque et où commence votre application

SuperScheduler fournit

  • Arborescence de ressources groupée : plateaux, salles de post-production et équipes.
  • Glisser, redimensionner, magnétisme par blocs de quatre heures et prévention des chevauchements par salle.
  • Liens courbes fin-début dessinés entre les sessions.
  • Historique annuler/rétablir avec Ctrl/⌘+Z et événements contrôlés via onEventsChange.
  • Niveaux de zoom des heures aux jours avec la nuit masquée, navigation clavier et annonces accessibles.

Le code de cet exemple décide

  • Quelle étape va dans quelle salle (un tournage exige un plateau, un étalonnage la salle d’étalonnage).
  • Quelles sessions dépendent de quelles autres, et quand une dépendance est rompue.
  • Les blocs de maintenance verrouillés et les messages affichés en cas de refus.
  • Le panneau de détail, la mission et tous les textes de cette page.

Le code de cette démo

Ce sont les fichiers exécutés par cette page, pas une copie simplifiée. Ils compilent avec les exports publics de SuperScheduler Pro.

L’intégration : niveaux de zoom, heures de nuit masquées, hooks de rendu et callbacks où les règles décident si un déplacement est autorisé.

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} />
}
Télécharger le code de l’exemple (.zip)L’archive contient les fichiers de cet exemple et un README. Elle n’inclut ni SuperScheduler Pro ni aucune clé de téléchargement.

Partager ce scénario

Le lien ouvre cet exemple avec la vue actuelle (échelle et options). Il ne contient jamais vos modifications ni de données personnelles.

Un autre métier, un problème voisinPlanification des ressources d’une agence créativeUne graphiste est surréservée. Confiez deux tâches à un collègue d’un seul glisser, vérifiez ce qu’elles débloquent, puis annulez.Lire le cas métierProduction créative