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
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é.
/**
* 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} />
}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.