ManufacturingForge (Fictional business)
Manufacturing order scheduling
Maintenance was brought forward. Move the order out of the way and keep its operations in sequence.
- Time scale
- Demo with synthetic data
- shifts / hours
- Pro capabilities
- Resource treeDependenciesBlocked timeMinimap
Loads SuperScheduler Pro only when you ask for it.
The situation
Forge is a fictional contract machining plant: two CNC mills and a lathe, two assembly cells, a paint booth and a quality lab, run by four crews over three shifts. Every work order follows a routing (milling, turning, assembly, painting, inspection), and no operation may start before the previous one ends.
Maintenance does not wait for the plan. When the spindle service of CNC mill 2 is brought forward, the milling planned on it has to move, and whatever follows it in the routing may now start too early. A planning board has to refuse impossible moves, show which operations are out of sequence and tell the planner where the week is already full.
What you will doYou will move a milling operation off a maintenance window, put its turning operation back in sequence, and use the load strip to jump to the next maintenance on Thursday.
Where the library ends and your application begins
SuperScheduler provides
- A line and machine tree, dragging between machines and overlap prevention per machine.
- Disabled cells that refuse drops on maintenance windows, with per-frame vetoes and messages.
- Finish-to-start links drawn between operations, styled link by link.
- Hour and shift scales with animated zoom, keyboard moves and announcements.
- The minimap from super-scheduler/minimap and viewport state from super-scheduler/hooks.
This example’s code decides
- Which kind of machine runs each operation, and the maintenance calendar.
- Each order’s routing and the check that flags an operation starting too early.
- The load series: busy machine time over available machine time, with maintenance taken out.
- Fixed run times (resizing is off), the detail panel, the mission and every label.
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: hour and shift scales, maintenance as disabled cells, sequence links, the refusals, and the load strip with its computed series.
/**
* Forge production board: production lines and their machines over one week of three shifts,
* with each work order's operations linked in sequence and a plant-load strip underneath.
*
* Library: tree rows, drag between machines, disabled cells, finish-to-start links, hour and
* shift scales, the minimap (super-scheduler/minimap) and viewport state (super-scheduler/hooks).
* Application: which machine runs which operation, maintenance windows, what counts as out of
* sequence, and the load series the strip draws. Those rules live in ./rules.
*/
import { useEffect, useMemo, useRef, type RefObject } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventsChangeArgs, SchedulerProps } from 'super-scheduler'
import { subscribeScheduler } from 'super-scheduler/hooks'
import { createMinimap, type Minimap } from 'super-scheduler/minimap'
import { civil, formatCivil } from '../../scheduler/civil'
import { createHeaderHook } from '../../scheduler/live-headers'
import { toneClass } from '../../scheduler/tones'
import { useDropRefusal } from '../kit/use-drop-refusal'
import {
ORDER_TONES,
civilOf,
isOperationEvent,
machineOf,
readOperationFields,
toResources,
type OperationEvent,
} from './adapter'
import { INITIAL_VIEW_START, MACHINES, MAINTENANCE, WEEK, type Operation } from './model'
import { loadSeries, maintenanceAt, slotRefusal, type Refusal, type SequenceIssue } from './rules'
import { ZOOM_IDS, type ForgeText, type ZoomId } from './text'
export interface ForgeSchedulerProps {
readonly events: OperationEvent[]
/** The same operations in model form: the load strip's series is computed from them. */
readonly operations: readonly Operation[]
readonly issues: readonly SequenceIssue[]
/** Operations that sit on a maintenance window. */
readonly conflicts: readonly string[]
readonly text: ForgeText
/** BCP 47 tag for labels, and the scheduler locale id. */
readonly tag: string
readonly schedulerLocale: string
/** Scale on mount. Later changes go through control.zoom so the view animates. */
readonly initialZoom: ZoomId
readonly controlRef: RefObject<SuperScheduler.Scheduler | null>
readonly onEventsChange: (args: SchedulerEventsChangeArgs) => void
readonly onSelect: (id: string) => void
readonly onMoved: (id: string) => void
readonly onRefused: (id: string, refusal: Refusal) => void
readonly onZoomLevel: (level: ZoomId) => void
/** The visible range once scrolling settles (civil ISO strings). */
readonly onViewport: (start: string, end: string) => void
}
/** Hour cells for precise moves; one cell per eight-hour shift (00, 08, 16) for the week view. */
const SCALES: Readonly<Record<ZoomId, SuperScheduler.ZoomLevelProperties>> = {
hours: { scale: 'Hour', cellWidth: 48, timeHeaders: [{ groupBy: 'Day' }, { groupBy: 'Hour' }] },
shifts: {
scale: 'CellDuration',
cellDuration: 480,
cellWidth: 112,
timeHeaders: [{ groupBy: 'Day' }, { groupBy: 'Cell' }],
},
}
// Same order as ZOOM_IDS, so onZoom's level index maps back to an id.
const ZOOM_LEVELS: SuperScheduler.ZoomLevel[] = ZOOM_IDS.map((id) => ({
id,
properties: SCALES[id],
}))
/** Every operation the control holds right now, in the shape the rules read. */
function operationsIn(
control: SuperScheduler.Scheduler,
): Pick<Operation, 'id' | 'machine' | 'start' | 'end'>[] {
return control.events.list.flatMap((event) => {
const machine = event.resource === undefined ? null : machineOf(event.resource)
if (machine === null || !isOperationEvent(event)) return []
return [
{
id: String(event.id),
machine: machine.id,
start: civilOf(event.start),
end: civilOf(event.end),
},
]
})
}
/** Checks one proposed slot of an operation against the plant's rules. */
function refusalFor(
control: SuperScheduler.Scheduler | null,
data: object,
id: string,
resource: SuperScheduler.ResourceId,
start: SuperScheduler.Date,
end: SuperScheduler.Date,
): Refusal | null {
const fields = readOperationFields(data)
const machine = machineOf(resource)
if (control === null || fields === null || machine === null) return null
return slotRefusal(
{ id, kind: fields.kind, start: civil(start.ticks), end: civil(end.ticks) },
machine,
MAINTENANCE,
operationsIn(control),
)
}
export function ForgeScheduler(props: ForgeSchedulerProps) {
const {
events,
operations,
issues,
conflicts,
text,
tag,
schedulerLocale,
initialZoom,
controlRef,
} = props
const { onEventsChange, onSelect, onMoved, onRefused, onZoomLevel, onViewport } = props
const resources = useMemo(() => toResources(text), [text])
const refusal = useDropRefusal(onRefused)
const strip = useRef<HTMLDivElement>(null)
const minimap = useRef<Minimap | null>(null)
// Each order's routing is a chain of finish-to-start links; late ones are drawn in red.
const links = useMemo<SuperScheduler.LinkData[]>(() => {
const late = new Set(issues.map((issue) => issue.operation))
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])
const config = useMemo<SchedulerProps>(() => {
const flagged = new Set([...issues.map((issue) => issue.operation), ...conflicts])
const escape = SuperScheduler.Util.escapeHtml
const messageFor = (reason: Refusal, data: object) => {
const fields = readOperationFields(data)
if (reason === 'machine') return fields === null ? '' : text.rules.wrongMachine[fields.kind]
return text.rules[reason]
}
return {
locale: schedulerLocale,
startDate: WEEK.start,
days: WEEK.days,
zoomLevels: ZOOM_LEVELS,
zoom: initialZoom,
zoomPosition: 'left',
// Three shifts: every hour is production time, so nothing is shaded as non-business.
cellsMarkBusiness: false,
useEventBoxes: 'Never',
height: '100%',
treeEnabled: true,
treePreventParentUsage: true,
rowHeaderWidth: 184,
rowHeaderWidthAutoFit: false,
eventHeight: 28,
rowMarginTop: 5,
rowMarginBottom: 5,
allowEventOverlap: false,
// Run time follows quantity and cycle time: planners move operations, they do not stretch them.
eventResizeHandling: 'Disabled',
durationBarVisible: false,
showToolTip: false,
eventHoverHandling: 'Disabled',
dragCard: { duration: false },
keyboardEnabled: true,
keyboardTarget: 'component',
keyboardMode: 'Full',
controlRef,
onEventsChange,
onBeforeTimeHeaderRender: createHeaderHook(tag),
onBeforeRowHeaderRender: (args) => {
const machine = machineOf(args.row.id)
if (machine === null) {
args.row.cssClass = 'ss-rh--group'
return
}
args.row.html =
`<span class="flex w-full min-w-0 items-center justify-between gap-2">` +
`<span class="truncate font-medium">${escape(args.row.name)}</span>` +
`<span class="shrink-0 text-[11px] text-ink-3">${escape(text.crews[machine.crew])}</span></span>`
},
// Maintenance windows become disabled cells: the library refuses drops on them.
onBeforeCellRender: (args) => {
const machine = machineOf(args.cell.resource)
if (machine === null) return
const cellStart = civil(args.cell.start.ticks)
const window = maintenanceAt(MAINTENANCE, machine.id, cellStart, civil(args.cell.end.ticks))
if (window === null) return
args.cell.properties.disabled = true
args.cell.properties.cssClass = 'ss-cell--maintenance'
if (cellStart <= window.start) {
// The first cell carries the label. The engine clips cells with an inline style, so the
// important utility lets the label run over the rest of the window.
args.cell.properties.cssClass = 'ss-cell--maintenance overflow-visible!'
args.cell.properties.html = `<span class="relative z-[1] block whitespace-nowrap px-1.5 pt-0.5 text-[10.5px] font-semibold text-ink-2">${escape(text.maintenance[window.kind])}</span>`
}
},
onBeforeEventRender: (args) => {
const fields = readOperationFields(args.data)
if (fields === null) return
args.data.html =
`<span class="ss-evt"><span class="ss-evt__title">${escape(fields.order)}</span>` +
`<span class="ss-evt__meta">${escape(text.operations[fields.kind])}</span></span>`
const classes = ['ss-ev', toneClass(ORDER_TONES[fields.order])]
if (flagged.has(String(args.data.id))) classes.push('ss-ev--late')
args.data.cssClass = classes.join(' ')
},
onEventMoving: (args) => {
const id = String(args.e.id())
const reason = refusalFor(
controlRef.current,
args.e.data,
id,
args.resource,
args.start,
args.end,
)
refusal.track(id, reason)
if (reason !== null) {
args.allowed = false
args.message = messageFor(reason, args.e.data)
}
},
// Final check before the commit; keyboard moves (Alt+arrows) arrive here without a drag.
onEventMove: (args) => {
const id = String(args.e.id())
const reason = refusalFor(
args.control,
args.e.data,
id,
args.newResource,
args.newStart,
args.newEnd,
)
if (reason !== null) {
args.preventDefault()
refusal.clear()
onRefused(id, reason)
}
},
onEventMoved: (args) => {
refusal.clear()
onMoved(String(args.e.id()))
},
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)
},
}
}, [
conflicts,
controlRef,
initialZoom,
issues,
onEventsChange,
onMoved,
onRefused,
onSelect,
onZoomLevel,
refusal,
schedulerLocale,
tag,
text,
])
// Imperative and once: a scroll prop would be re-applied on every update.
useEffect(() => {
controlRef.current?.scrollTo(INITIAL_VIEW_START, false, 'left')
}, [controlRef])
// The visible range, published once scrolling (or a minimap drag) settles.
useEffect(() => {
const control = controlRef.current
if (control === null) return
return subscribeScheduler(control, 'viewport', (viewport) =>
onViewport(civil(viewport.start.ticks), civil(viewport.end.ticks)),
)
}, [controlRef, onViewport])
// The load strip reads the latest operations through a ref and redraws when they change.
const latest = useRef(operations)
useEffect(() => {
latest.current = operations
minimap.current?.refresh()
}, [operations])
useEffect(() => {
const control = controlRef.current
const host = strip.current
if (control === null || host === null) return
// The brush reports exact instants (with milliseconds): format their ticks.
const when = (date: SuperScheduler.Date) =>
formatCivil(date.ticks, tag, { weekday: 'short', hour: 'numeric', minute: '2-digit' })
const created = createMinimap(control, host, {
height: 28,
// Absolute scale: a full bar means every available machine is busy.
peak: 'absolute',
max: 1,
series: (range) =>
loadSeries(
latest.current,
MAINTENANCE,
MACHINES,
range.start.ticks,
range.bucketMs,
range.buckets,
),
tone: (value) => (value >= 0.9 ? 'danger' : value >= 0.7 ? 'warn' : 'base'),
// A fixed week of fixtures: no "today" tick, no washed-out past.
marks: { today: false, months: false, past: false },
labels: {
label: text.load.label,
zoom: text.load.zoom,
valueText: (start, end) =>
text.load.range.replace('{start}', when(start)).replace('{end}', when(end)),
},
})
minimap.current = created
return () => {
created.dispose()
minimap.current = null
}
}, [controlRef, tag, text])
return (
<div className="flex h-full flex-col">
<div className="min-h-0 flex-1 [&>div]:h-full">
<SuperSchedulerComponent {...config} resources={resources} events={events} links={links} />
</div>
<div className="flex shrink-0 items-center gap-3 border-t border-line bg-paper-2/60 px-3 py-2">
<span className="w-[10.75rem] shrink-0 truncate text-xs font-medium text-ink-2 max-sm:w-auto">
{text.load.title}
</span>
<div ref={strip} className="min-w-0 flex-1" />
</div>
</div>
)
}Share this scenario
The link opens this example with the current view (time scale and options). It never contains your edits or personal data.