FertigungForge (Fiktives Unternehmen)
Planung von Fertigungsaufträgen
Die Wartung wurde vorgezogen. Verschieben Sie den Auftrag und halten Sie die Arbeitsfolge ein.
- Zeitskala
- Demo mit synthetischen Daten
- Schichten / Stunden
- Pro-Funktionen
- RessourcenbaumAbhängigkeitenGesperrte ZeitenMinimap
SuperScheduler Pro wird erst geladen, wenn Sie es anfordern.
Die Ausgangslage
Forge ist ein fiktiver Lohnfertiger für Zerspanung: zwei CNC-Fräsen und eine Drehmaschine, zwei Montagezellen, eine Lackierkabine und ein Prüflabor, betrieben von vier Teams in drei Schichten. Jeder Auftrag folgt einem Arbeitsplan (Fräsen, Drehen, Montage, Lackieren, Prüfen), und kein Arbeitsgang darf beginnen, bevor der vorherige endet.
Wartung wartet nicht auf den Plan. Wird der Spindelservice der CNC-Fräse 2 vorgezogen, muss der dort geplante Fräsgang weichen, und was im Arbeitsplan folgt, beginnt womöglich zu früh. Eine Planungstafel muss unmögliche Verschiebungen ablehnen, Arbeitsgänge außerhalb der Reihenfolge zeigen und sichtbar machen, wo die Woche schon voll ist.
Was Sie tun werdenSie verschieben einen Fräsgang aus einem Wartungsfenster, bringen den Drehgang zurück in die Reihenfolge und springen über den Auslastungsstreifen zur nächsten Wartung am Donnerstag.
Wo die Bibliothek endet und Ihre Anwendung beginnt
SuperScheduler liefert
- Baum aus Linien und Maschinen, Ziehen zwischen Maschinen und Überlappungsschutz pro Maschine.
- Deaktivierte Zellen, die das Ablegen in Wartungsfenstern verweigern, mit Vetos und Meldungen pro Frame.
- Ende-Anfang-Verknüpfungen zwischen Arbeitsgängen, einzeln gestaltet.
- Stunden- und Schichtskala mit animiertem Zoom, Tastaturbedienung und Ansagen.
- Die Minimap aus super-scheduler/minimap und der Viewport-Zustand aus super-scheduler/hooks.
Der Code dieses Beispiels entscheidet
- Welcher Maschinentyp welchen Arbeitsgang ausführt, und der Wartungskalender.
- Der Arbeitsplan jedes Auftrags und die Prüfung, die einen zu früh beginnenden Arbeitsgang markiert.
- Die Auslastungsreihe: belegte Maschinenzeit geteilt durch verfügbare Maschinenzeit, ohne Wartung.
- Feste Laufzeiten (keine Änderung der Dauer), das Detailpanel, die Aufgabe und alle Texte.
Der Code hinter dieser Demo
Das sind die Dateien, die diese Seite ausführt, keine vereinfachte Kopie. Sie kompilieren gegen die öffentlichen Exporte von SuperScheduler Pro.
Die Integration: Stunden- und Schichtskala, Wartung als deaktivierte Zellen, Reihenfolge-Verknüpfungen, Ablehnungen und der Auslastungsstreifen mit seiner berechneten Reihe.
/**
* 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>
)
}Dieses Szenario teilen
Der Link öffnet dieses Beispiel mit der aktuellen Ansicht (Zeitskala und Optionen). Er enthält nie Ihre Änderungen oder persönliche Daten.