Bauzeitenplanung
Regen stoppt eine Betonage. Verschieben Sie sie, ziehen Sie den Rohbau des nächsten Geschosses in einem Zug nach und halten Sie den Kran in seiner Schicht.
- Bau
- Plumbline
- Ressourcenbaum
- Abhängigkeiten
- Summenzeilen
- Mehrfachauswahl
- Rechteckauswahl
- Rückgängig und Wiederholen
- Minimap
- Fortschrittsbalken
- Zoom-Regler
- Demo mit synthetischen Daten
Aufgabe
0 von 3 Schritten erledigtMittwoch, 21. Oktober, 07:00 Uhr: Von Donnerstag bis Montag ist Regen angesagt, und die Decke im 3. OG sollte morgen betoniert werden.
Holen Sie die Betonage im 3. OG aus dem Regen: Di., 27. Okt. — Offen
Ziehen Sie in 3. OG · Rohbau den roten Balken Betonage drei Arbeitstage nach rechts, auf Dienstag, 27. Oktober (das Wochenende wird übersprungen). Oder öffnen Sie sein Menü (Rechtsklick oder Umschalt+F10) und wählen Sie den ersten möglichen Tag.
Verschieben Sie den ganzen Rohbau im 4. OG um drei Arbeitstage. — Offen
Die Schalung im 4. OG beginnt jetzt, bevor die Decke im 3. OG ausgehärtet ist. Ziehen Sie über das leere Raster ein Fenster um die drei Balken im 4. OG (oder Strg/⌘-Klick auf jeden) und ziehen Sie dann einen davon: Sie bewegen sich gemeinsam. Oder wählen Sie Rohbau 4. OG auswählen im Menü eines Balkens und drücken Sie Verschieben im Seitenfeld.
Beseitigen Sie die Überlastung des Turmdrehkrans. — Offen
Die Betonage im 4. OG fällt jetzt auf Mi., 11. Nov., zusammen mit der Fassade und der Lieferung der Aufzugsschienen: 11 Kranstunden für eine 8-Stunden-Schicht. Ziehen Sie Aufzugsschienen in EG · Ausbau auf Do., 12. Nov., oder wählen Sie Nächster Tag mit freiem Kran in seinem Menü.
Etwa 30 Sekunden. Erledigte Schritte werden abgehakt.
Tastatur: Raster fokussieren, dann Pfeiltasten, Eingabe und Alt+Pfeile zum Verschieben.
Vorschau des ersten BildesLive: die echte Pro-Engine, in Ihrem BrowserEtwa 30 Sekunden. Erledigte Schritte werden abgehakt. Tastatur: Raster fokussieren, dann Pfeiltasten, Eingabe und Alt+Pfeile zum Verschieben.
Die Ausgangslage.
Plumbline ist ein fiktiver Generalunternehmer, der Linden Yard, Haus C baut: sechs Geschosse auf einem Stahlbetonskelett. Der Bauzeitenplan ist ortsbezogen: Jedes Geschoss im Schnitt hat einen Rohbau (Schalung, Bewehrung, Betonage, danach die Fassade) und einen Ausbau (Haustechnik, Abnahme, Innenwände, Oberflächen), und je Gewerk zieht eine Kolonne von Geschoss zu Geschoss. Eine Decke braucht sieben Tage Aushärtung, bevor die Schalung des nächsten Geschosses darauf stehen darf.
Das Wetter ist der übliche Gegner. Verschiebt sich eine Betonage, verschiebt sich die Aushärtung, dann das Geschoss darüber, und der Puffer vor dem Richtfest ist weg, ohne dass es jemand merkt. Ein Balkenplan verbirgt diese Kette; dieser Bauzeitenplan berechnet den kritischen Pfad aus der Logik, zeichnet ihn mit Rotstift, lehnt ab, was die Baustelle nicht kann, und behält Turmdrehkran und Personal im Blick.
Was Sie tun werden
Sie holen eine Betonage aus dem Regen, verschieben den Rohbau eines Geschosses in einem Zug, sobald die Aushärtungsregel ihn markiert, und beseitigen die Kranüberlastung, die die Verschiebung erzeugt.
Wo die Bibliothek endet und Ihre Anwendung beginnt.
SuperScheduler liefert
- Ein dreistufiger Ressourcenbaum (Haus C, seine Geschosse, deren Bereiche) mit rowHeaderColumns und zusammengeführten Elternzeilen; die Zeilenköpfe zeichnen den Gebäudeschnitt mit Kote und Decke jedes Geschosses.
- Eine fixierte Zeile oben (frozen: 'top', cellsDisabled), die onBeforeCellRender mit den Kranstunden pro Tag füllt; Zellen schraffiert für Wochenenden, aushärtende Decken und Regen, und gesperrt während der Weihnachtspause.
- Drei Zeitkopfzeilen aus onBeforeTimeHeaderRender: Wochen als Maßkette, Tage und die Wetterprognose, mit einem Kopfbereich an der Baustellenuhr; Trennlinien für die Baustellenuhr und den letzten Krantag.
- Meilensteine, PercentComplete-Fortschrittsbalken als Materialschraffuren, Ende-Anfang-Verknüpfungen mit Stil aus onBeforeLinkRender und Verknüpfungs-Bubble, und zeitverankerte Bereiche über den Tagen, an denen ein Vorgang zu früh beginnt.
- Mehrfachauswahl per Klick, Strg/Cmd-Klick oder einem über das Raster gezogenen Fenster (rectangleSelectHandling, onGridMouseDown), allowMultiMove mit jedem Mitglied pro Frame in onEventMoving neu geschrieben, und eventMoveSkipNonBusiness.
- Der Zoom-Slider von super-scheduler/zoom-ui als Maßstabsleiste des Plans, die Minimap von super-scheduler/minimap als Übersichtsplan, Rückgängig-Verlauf, Tastaturbewegungen, die Hover-Karte, die Drag-Karte und ein Kontextmenü im Look des Produkts.
Der Code dieses Beispiels entscheidet
- Der Baustellenkalender (Wochenenden, Weihnachtspause), die Aushärtungsregel, Ende-Anfang-Logik, kein Betonieren bei Regen, eine Kolonne je Gewerk und feste Termine: Ablehnungen in onEventMoving und onEventMove, mit Grund und genanntem Tag.
- Die Methode des kritischen Pfads: späteste Termine rückwärts von den festen Terminen über Arbeitstage, Gesamtpuffer je Vorgang, die kritischen Vorgänge und die Verknüpfungen, die sie treiben.
- Kranstunden und Personal auf der Baustelle pro Tag, die Befehle (erster möglicher Tag, nächster Tag mit freiem Kran, Auswahl um Arbeitstage verschieben) und die Indexbuchstaben aus dem Verlauf.
- Der Plan selbst: Schraffuren je Gewerk, Maßlinien, die Schnittlinie an der Baustellenuhr, Revisionswolken und das Schriftfeld.
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: der Baum und seine Spalten, die fixierte Kranzeile, drei Kopfzeilen, Trennlinien, Meilensteine und Fortschrittsbalken, Verknüpfungen im Stil des kritischen Pfads mit ihrer Bubble, Fensterauswahl, Mehrfachverschiebung mit pro Frame neu geschriebenen Mitgliedern, das Drag-Urteil, die Hover-Karte und das Kontextmenü, und die Callbacks, in denen die Baustellenregeln eine Verschiebung ablehnen.
/**
* Plumbline programme board: Linden Yard, Block C, drawn as its section. Levels from the roof
* down, the faces of each level as rows, fifteen weeks of days on a drafting grid, the critical
* path in red pencil and the tower crane booked day by day on top.
*
* Library: a three-level resource tree with rowHeaderColumns (merged parent rows), a frozen
* summary row painted by onBeforeCellRender, three time-header rows written by
* onBeforeTimeHeaderRender (a dimension chain of weeks, days, the forecast) with header areas,
* disabled cells, separators, milestones, PercentComplete bars drawn as material hatches,
* finish-to-start links styled by onBeforeLinkRender with a link bubble, multi-select (click,
* Ctrl/Cmd+click, a window drawn over the grid with rectangleSelectHandling), allowMultiMove with
* members rewritten per frame, eventMoveSkipNonBusiness, conflict highlighting, the zoom slider
* (super-scheduler/zoom-ui), keyboard and undo history, the event hover card and the drag card
* (super-scheduler/react-render) and a context menu. Application: the site calendar, curing,
* logic, rain, crews and fixed dates, the critical path and float, crane hours and every figure.
* Those rules live in ./rules; ./paint writes the markup.
*/
import { useCallback, useEffect, useMemo, type RefObject } from 'react'
import { SuperScheduler } from 'super-scheduler'
import type { SchedulerEventsChangeArgs } from 'super-scheduler'
import type { SchedulerHistory } from 'super-scheduler/history'
import { SuperSchedulerComponent, type SchedulerRenderProps } from 'super-scheduler/react-render'
import { createZoomSlider } from 'super-scheduler/zoom-ui'
import { format, plural } from '~/i18n/format'
import { DAY, civil, parseCivil } from '../../scheduler/civil'
import { createEventMenu, type MenuEntry } from '../kit/context-menu'
import { useEventHover } from '../kit/hover-card'
import { KEYS } from '../kit/keys'
import { useDropRefusal } from '../kit/use-drop-refusal'
import { createDragCard } from '../kit/verdict'
import { skinClass, skinMenuTheme } from '../registry'
import type { DemoCommon } from '../types'
import {
BOARD,
BUILDING_ID,
CRANE_ROW,
ROW_HEADER_WIDTH,
activityName,
isLevelRowId,
isRowId,
levelOfRow,
openingScroll,
readActivityFields,
toResources,
type ActivityEvent,
} from './adapter'
import {
activityCard,
craneDayOf,
firstValidOf,
refusalText,
type ActivityAction,
type PlanView,
} from './card'
import {
CRANE_DISMANTLED,
CRANE_HOURS,
INITIAL_VIEW_START,
NOW,
PLAN,
ROWS,
type Activity,
} from './model'
import {
DAY_LETTERS_FROM,
DAY_NUMBERS_FROM,
buildingHtml,
cardHtml,
cellPainter,
craneCell,
craneHeaderHtml,
dayHeader,
doneHtml,
faceHtml,
keyDay,
levelHtml,
milestoneHtml,
shortDay,
tradeHtml,
weatherHeader,
weekHeader,
type HeaderPaint,
} from './paint'
import {
addWorkingDays,
brokenSuccessors,
craneHours,
curingWindows,
dayOf,
duration,
frameOf,
groupRefusal,
isMovable,
isWorkingDay,
linkId,
nextWorkingStart,
placementRefusal,
pourOf,
rowProgress,
shiftStart,
signedWorkingDays,
slabState,
tradeWork,
workAt,
type Span,
} from './rules'
import type { LogicView, PlumblineText } from './text'
const SLUG = 'construction-site'
const NOW_TICKS = parseCivil(NOW)
const PLAN_FROM = parseCivil(PLAN.start)
export interface PlumblineSchedulerProps {
readonly events: ActivityEvent[]
readonly view: PlanView
readonly text: PlumblineText
readonly common: DemoCommon
/** BCP 47 tag for labels, and the scheduler locale id. */
readonly tag: string
readonly schedulerLocale: string
readonly logic: LogicView
/** Narrow stages keep the location column only. */
readonly narrow: boolean
readonly history: SchedulerHistory
readonly controlRef: RefObject<SuperScheduler.Scheduler | null>
/** Where the product bar keeps the zoom slider (the drawing's scale bar). */
readonly zoomHost: RefObject<HTMLDivElement | null>
readonly onEventsChange: (args: SchedulerEventsChangeArgs) => void
/** A click (or Enter) on an activity: its sheet opens. */
readonly onOpen: (id: string) => void
readonly onMoved: (ids: readonly string[]) => void
readonly onRefused: (id: string, message: string) => void
readonly onAction: (action: ActivityAction, id: string) => void
/** The settled cell width after a zoom, for the title block's scale. */
readonly onScale: (cellWidth: number) => void
}
/** The last working day at or before `ticks` (a keyboard move to the left over a weekend). */
function previousWorkingStart(ticks: number): number {
let day = dayOf(ticks)
while (!isWorkingDay(day)) day -= DAY
return day
}
/** onBeforeTimeHeaderRender: weeks, days and the forecast, cached per cell and width bucket. */
function createTimeHeaderHook(text: PlumblineText, tag: string) {
const cache = new Map<string, HeaderPaint>()
const nowLabel = format(text.chrome.now, { day: shortDay(NOW_TICKS, tag), time: '07:00' })
return (args: SuperScheduler.SchedulerBeforeTimeHeaderRenderArgs) => {
const header = args.header
const start = header.start.getTime()
const span = header.end.getTime() - start
const width = args.control.cellWidth ?? BOARD.cellWidth
const bucket = width >= DAY_LETTERS_FROM ? 'w' : width >= DAY_NUMBERS_FROM ? 'm' : 'n'
const key = `${header.level}:${start}:${span}:${bucket}`
let paint = cache.get(key)
if (paint === undefined) {
paint =
header.level === 0
? weekHeader(start, span, text, tag)
: header.level === 1
? dayHeader(start, span, width, tag)
: weatherHeader(start, span, width, text, nowLabel)
cache.set(key, paint)
}
header.html = paint.html
header.cssClass = paint.cssClass
header.areas = paint.areas
header.toolTip = paint.toolTip
}
}
export function PlumblineScheduler(props: PlumblineSchedulerProps) {
const { events, view, text, common, tag, schedulerLocale, logic, narrow, history } = props
const { controlRef, zoomHost } = props
const { onEventsChange, onOpen, onMoved, onRefused, onAction, onScale } = props
const resources = useMemo(() => toResources(text), [text])
const refusal = useDropRefusal(onRefused)
const name = useCallback((activity: Activity) => activityName(activity, text), [text])
// Hover card: a drawing callout with the logic on both sides, the float and the revision.
const card = useCallback(
(event: SuperScheduler.Event) => {
const activity = view.net.byId.get(String(event.id()))
return activity === undefined ? null : activityCard(activity, view, text, common, tag)
},
[view, text, common, tag],
)
const { eventHover, closeHover, suppressHover } = useEventHover(card)
// Drag verdict: the site's refusal, or the successors the drop would push out of their logic.
const dragCard = useMemo(
() =>
createDragCard({
tag,
common,
days: true,
rowName: (id) => {
const row = ROWS.find((candidate) => candidate.id === id)
return row === undefined
? null
: `${text.levelShort[row.level]} · ${text.faces[row.face]}`
},
warn: (info) => {
const activity = view.net.byId.get(String(info.eventId))
if (activity === undefined) return null
const span = { start: info.start.getTime(), end: info.end.getTime() }
const broken = brokenSuccessors(activity, span, view.net)
if (broken.length === 0) return null
const names = broken
.flatMap((item) => {
const next = view.net.byId.get(item.id)
return next === undefined ? [] : [`${name(next)} (${shortDay(item.until, tag)})`]
})
.join(', ')
return plural(text.card.pushes, broken.length, tag, { names })
},
}),
[view, text, common, tag, name],
)
// Context menu: rebuilt when the plan changes, so its labels name the real day.
const contextMenu = useMemo(
() =>
createEventMenu({
theme: skinMenuTheme(SLUG),
title: (event) => format(common.kit.menuTitle, { name: event.text() }),
onOpenChange: (open) => suppressHover(open),
entries: (event) => {
const id = String(event.id())
const activity = view.net.byId.get(id)
if (activity === undefined) return []
const entries: MenuEntry[] = [
{
label: common.kit.openDetails,
icon: 'details',
keys: KEYS.details,
run: () => onAction('details', id),
},
]
if (view.issueOf.has(id) && isMovable(activity)) {
const first = firstValidOf(activity, view)
entries.push({
label:
first === null
? text.actions.noValid
: format(text.actions.firstValid, { date: shortDay(first, tag) }),
icon: 'slot',
primary: true,
disabled: first === null,
run: () => onAction('first-valid', id),
})
}
if (activity.trade === 'lift' && isMovable(activity)) {
const day = craneDayOf(activity, view)
entries.push({
label:
day === null
? text.actions.noCraneDay
: format(text.actions.craneDay, { date: shortDay(day, tag) }),
icon: 'after',
primary: !view.issueOf.has(id),
disabled: day === null,
run: () => onAction('crane-day', id),
})
}
const row = ROWS.find((candidate) => candidate.id === activity.row)
const frame = frameOf(view.activities, activity.row).filter(isMovable)
const trade = activity.trade === null ? [] : tradeWork(view.activities, activity.trade)
const selections: MenuEntry[] = []
if (row !== undefined && frame.some((member) => member.id === id) && frame.length > 1)
selections.push({
label: format(text.actions.selectFrame, {
level: text.levelShort[row.level],
count: String(frame.length),
}),
icon: 'focus',
run: () => onAction('select-frame', id),
})
if (activity.trade !== null && trade.length > 1)
selections.push({
label: format(text.actions.selectTrade, {
trade: text.trades[activity.trade],
count: String(trade.length),
}),
icon: 'layers',
run: () => onAction('select-trade', id),
})
if (selections.length > 0) entries.push('separator', ...selections)
entries.push('separator', {
label: logic === 'all' ? text.actions.showCritical : text.actions.showAll,
icon: 'link',
run: () => onAction('toggle-logic', id),
})
return entries
},
}),
[view, logic, common, text, tag, suppressHover, onAction],
)
// The logic from the model; the critical path in red pencil, broken links as clashes. Abutting
// bars of one face read as a sequence already (like a dimension chain): their link is drawn only
// when it breaks. The bubble (bubbleHtml) is part of each link's data: lag and the float left.
const links = useMemo<SuperScheduler.LinkData[]>(() => {
const escape = SuperScheduler.Util.escapeHtml
const broken = new Set(
view.issues.flatMap((issue) =>
issue.other === undefined ? [] : [`${issue.other}->${issue.activity}`],
),
)
return view.net.links.flatMap((link): SuperScheduler.LinkData[] => {
const id = linkId(link)
const critical = view.plan.criticalLinks.has(id)
const clash = broken.has(id)
if (logic === 'critical' && !critical && !clash) return []
const from = view.net.byId.get(link.from)
const to = view.net.byId.get(link.to)
if (from === undefined || to === undefined) return []
if (!clash && link.lag === 0 && from.row === to.row && from.end === to.start) return []
const required = parseCivil(from.end) + link.lag * DAY
const gap = signedWorkingDays(nextWorkingStart(required), parseCivil(to.start))
const lagText =
link.lag > 0 ? format(text.card.curedOn, { date: shortDay(required, tag) }) : ''
const gapText =
gap < 0
? format(text.card.negative, { count: String(-gap) })
: gap === 0
? text.card.critical
: format(text.card.float, { count: String(gap) })
return [
{
id,
from: link.from,
to: link.to,
type: 'FinishToStart',
cssClass: clash
? 'pl-link pl-link--clash'
: critical
? 'pl-link pl-link--critical'
: 'pl-link',
bubbleHtml:
`<span class="pl-lb"><b>${escape(name(from))} → ${escape(name(to))}</b>` +
(lagText === '' ? '' : `<span>${escape(lagText)}</span>`) +
`<span class="pl-lb__gap" data-gap="${gap < 0 ? 'clash' : gap === 0 ? 'zero' : 'free'}">${escape(gapText)}</span></span>`,
},
]
})
}, [view, logic, text, tag, name])
// The link bubble shows a link's lag and the float it leaves (one instance, the menu's skin).
const linkBubble = useMemo(
() =>
new SuperScheduler.Bubble({
theme: skinMenuTheme(SLUG),
showAfter: 120,
position: 'Mouse',
hideOnClick: true,
}),
[],
)
// The separators: the site clock as a section cut, and the day the tower crane comes down.
const separators = useMemo<SuperScheduler.SeparatorData[]>(
() => [
{
location: `${CRANE_DISMANTLED}T00:00:00`,
layer: 'BelowEvents',
width: 1,
cssClass: 'pl-sep-crane',
toolTip: format(text.kpi.toppingTip, { date: shortDay(parseCivil(CRANE_DISMANTLED), tag) }),
},
{
location: NOW,
layer: 'AboveEvents',
width: 2,
cssClass: 'pl-sep-now',
toolTip: format(text.chrome.now, {
day: shortDay(NOW_TICKS, tag),
time: new Intl.DateTimeFormat(tag, {
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
timeZone: 'UTC',
}).format(new Date(NOW_TICKS)),
}),
},
],
[text, tag],
)
// Header markup per cell, cached for the board's life (the hook runs for every header cell).
const timeHeaders = useMemo(() => createTimeHeaderHook(text, tag), [text, tag])
const columns = useMemo<SuperScheduler.RowHeaderColumnData[]>(
() =>
narrow
? [{ text: text.columns.location, width: BOARD.narrowColumn, nonresizable: true }]
: [
{ text: text.columns.location, width: BOARD.locationColumn, nonresizable: true },
{ text: text.columns.trade, width: BOARD.tradeColumn, nonresizable: true },
{ text: text.columns.done, width: BOARD.doneColumn, nonresizable: true },
],
[narrow, text],
)
const config = useMemo<SchedulerRenderProps>(() => {
const escape = SuperScheduler.Util.escapeHtml
const activities = view.activities
const byId = view.net.byId
// Row figures, once per plan: progress, the trade at work, critical faces, slabs.
const progress = new Map(ROWS.map((row) => [row.id, rowProgress(activities, row.id)]))
const overall =
ROWS.reduce((sum, row) => sum + (progress.get(row.id) ?? 0), 0) / Math.max(1, ROWS.length)
const criticalRows = new Set(
activities.flatMap((activity) => (view.plan.critical.has(activity.id) ? [activity.row] : [])),
)
const paintCell = cellPainter(curingWindows(activities))
const crane = craneHours(activities, PLAN_FROM, PLAN.days)
const cards = new Map<string, string>()
/** Members of a multi-move, each shifted by the dragged activity's working days. */
const groupSpans = (
master: Activity,
start: number,
items: readonly SuperScheduler.SchedulerMultiMoveItem[],
) => {
const delta = signedWorkingDays(parseCivil(master.start), start)
const spans = new Map<string, Span>()
const members: Activity[] = []
for (const item of items) {
const member = byId.get(String(item.event.id()))
if (member === undefined) continue
const from = shiftStart(parseCivil(member.start), delta)
spans.set(member.id, { start: from, end: addWorkingDays(from, duration(member)) })
members.push(member)
}
return { delta, spans, members }
}
return {
locale: schedulerLocale,
cssClass: skinClass(SLUG),
startDate: PLAN.start,
days: PLAN.days,
// ISO weeks in every locale: the week row is a dimension chain from Monday to Monday.
weekStarts: 1,
scale: 'Day',
cellWidth: BOARD.cellWidth,
zoomGesture: { min: BOARD.zoom.min, max: BOARD.zoom.max, scales: false },
// The drawing keeps its lettering down to a narrow day; it simplifies further out.
lod: { zoomedOut: 20, eventFull: 26, eventText: 20, cellContent: 12, dayNumber: 13 },
timeHeaders: [
{ groupBy: 'Week', height: BOARD.header.week },
{ groupBy: 'Day', height: BOARD.header.day },
{ groupBy: 'Day', height: BOARD.header.weather },
],
// Weekends are the site's: drawn by the cells, and skipped by moves.
cellsMarkBusiness: false,
businessWeekends: false,
eventMoveSkipNonBusiness: true,
useEventBoxes: 'Never',
height: '100%',
treeEnabled: true,
treePreventParentUsage: true,
treeIndent: narrow ? 4 : BOARD.treeIndent,
rowHeaderColumns: columns,
rowHeaderColumnsMergeParents: true,
rowHeaderWidth: narrow ? BOARD.narrowColumn : ROW_HEADER_WIDTH,
rowHeaderWidthAutoFit: false,
eventHeight: BOARD.eventHeight,
rowMarginTop: BOARD.rowMargin,
rowMarginBottom: BOARD.rowMargin,
allowEventOverlap: false,
conflictHighlight: true,
// Progress fills the trade's hatch from the left: the bar spans the whole card.
durationBarVisible: true,
durationBarMode: 'PercentComplete',
durationBarHeight: 3,
// Selection: a click, Ctrl/Cmd+click, or a window drawn over the empty grid (CAD style).
eventClickHandling: 'Select',
allowMultiSelect: true,
allowMultiMove: true,
rectangleSelectHandling: 'EventSelect',
timeRangeSelectedHandling: 'Disabled',
linkShape: 'RightAngled',
linkLayer: 'Below',
linkBubble,
crosshairType: 'Full',
separators,
showToolTip: false,
keyboardEnabled: true,
keyboardTarget: 'component',
keyboardMode: 'Full',
history,
controlRef,
onEventsChange,
// The corner names the drawing: section A–A of Block C.
onBeforeCornerRender: (args) => {
args.html =
`<span class="pl-corner"><b>${escape(text.section)}</b>` +
`<span>${escape(text.building)}</span></span>`
},
onBeforeRowHeaderRender: (args) => {
const row = args.row
const id = row.id
const [first, second, third] = row.columns
if (id === CRANE_ROW) {
row.cssClass = 'pl-row pl-row--crane'
if (first !== undefined) first.html = craneHeaderHtml(text)
if (second !== undefined) second.html = ''
if (third !== undefined)
third.html = `<span class="pl-cap" aria-hidden="true"><i></i>${CRANE_HOURS} h</span>`
return
}
if (id === BUILDING_ID) {
row.cssClass = 'pl-row pl-row--building'
row.html = buildingHtml(text, overall)
if (first !== undefined) first.html = row.html
return
}
if (isLevelRowId(id)) {
const level = levelOfRow(id)
row.cssClass = 'pl-row pl-row--level'
row.html = levelHtml(level, slabState(pourOf(activities, level)), text, tag)
if (first !== undefined) first.html = row.html
return
}
if (!isRowId(id)) return
const critical = criticalRows.has(id)
row.cssClass = 'pl-row pl-row--face'
if (first !== undefined) first.html = faceHtml(String(row.name), critical)
if (second !== undefined) second.html = tradeHtml(workAt(activities, id), text, tag)
if (third !== undefined) third.html = doneHtml(progress.get(id) ?? 0)
},
onBeforeCellRender: (args) => {
const cell = args.cell
const day = cell.start.getTime()
const wide = (args.control.cellWidth ?? BOARD.cellWidth) >= 26
if (cell.resource === CRANE_ROW) {
const hours = crane[Math.round((day - PLAN_FROM) / DAY)] ?? 0
const painted = craneCell(hours, wide, text)
cell.properties.cssClass = painted.css
cell.properties.html = painted.html
return
}
if (cell.resource === BUILDING_ID || isLevelRowId(cell.resource)) {
cell.properties.cssClass = isWorkingDay(day) ? 'pl-c-slab' : 'pl-c-slab pl-c-off'
return
}
if (!isRowId(cell.resource)) return
const painted = paintCell(cell.resource, day, wide, text)
if (painted.css !== '') cell.properties.cssClass = painted.css
if (painted.html !== '') cell.properties.html = painted.html
if (painted.disabled) cell.properties.disabled = true
},
onBeforeEventRender: (args) => {
const data = args.data
const fields = readActivityFields(data)
const id = String(data.id)
const activity = byId.get(id)
if (fields === null || activity === undefined) return
const issue = view.issueOf.get(id) ?? null
const critical = view.plan.critical.has(id)
const revision = view.revisionOf.get(id) ?? null
const classes = ['pl-ev', `pl-k-${activity.kind}`]
if (activity.trade !== null) classes.push(`pl-t-${activity.trade}`)
if (critical) classes.push('pl-ev--critical')
if (issue !== null) classes.push('pl-ev--clash')
if (revision !== null) classes.push('pl-ev--revised')
if (activity.complete >= 100) classes.push('pl-ev--built')
else if (activity.complete > 0) classes.push('pl-ev--running')
data.cssClass = classes.join(' ')
const label = activityName(activity, text)
if (activity.kind === 'milestone') {
// A key date: a diamond at the end of its day, its name and day beside it.
data['type'] = 'Milestone'
data['width'] = 18
data.html = ''
data.barHidden = true
const note = issue === null ? null : text.card.conflict
data.htmlRight =
(args.control.cellWidth ?? BOARD.cellWidth) >= DAY_NUMBERS_FROM
? milestoneHtml(label, keyDay(parseCivil(activity.start), tag), note)
: ''
data.ariaLabel = `${label}, ${keyDay(parseCivil(activity.start), tag)}`
return
}
const days = duration(activity)
const width = Math.round(
((parseCivil(activity.end) - parseCivil(activity.start)) / DAY) *
(args.control.cellWidth ?? BOARD.cellWidth),
)
const bucket =
width >= 110
? 110
: width >= 96
? 96
: width >= 64
? 64
: width >= 58
? 58
: width >= 30
? 30
: 0
const key = `${id}|${bucket}|${revision ?? ''}|${activity.complete}`
let html = cards.get(key)
if (html === undefined) {
html = cardHtml(activity, label, days, { critical, issue, revision, width: bucket }, text)
cards.set(key, html)
}
data.html = html
data.complete = activity.complete
if (activity.kind !== 'work') data.barHidden = true
// The days an activity starts too early: hatched in red pencil, from its start.
const areas: SuperScheduler.AreaData[] = []
if (issue !== null) {
const end =
issue.kind === 'rain'
? activity.end
: civil(Math.min(issue.until, parseCivil(activity.end)))
areas.push({
start: activity.start,
end,
top: 0,
bottom: 0,
cssClass: 'pl-clash',
action: 'Default',
})
}
data.areas = areas
const state =
issue !== null ? `, ${text.card.conflict}` : critical ? `, ${text.card.critical}` : ''
data.ariaLabel = `${label}, ${shortDay(parseCivil(activity.start), tag)}, ${plural(text.days, days, tag)}${state}`
},
// The critical path is drawn heavier than the leader lines of the rest of the logic.
onBeforeLinkRender: (args) => {
if ((args.data.cssClass ?? '').includes('critical')) args.data.width = 2
},
// A plain drag over the empty grid draws a selection window, as in a drawing program.
onGridMouseDown: (args) => {
if (args.button === 'left') args.action = 'RectangleSelect'
},
// Every frame: the site's rules for the dragged activity or the whole selection moved
// together (each member shifted by the same working days, rewritten here).
onEventMoving: (args) => {
const id = String(args.e.id())
const activity = byId.get(id)
if (activity === undefined) return
if (args.multimove.length > 1) {
const group = groupSpans(activity, args.start.getTime(), args.multimove)
for (const item of args.multimove) {
const span = group.spans.get(String(item.event.id()))
if (span === undefined) continue
item.start = new SuperScheduler.Date(span.start)
item.end = new SuperScheduler.Date(span.end)
}
const refused = groupRefusal(group.members, group.spans, view.net)
if (refused !== null) {
const member = byId.get(refused.activity)
const message = `${member === undefined ? '' : `${name(member)}: `}${refusalText(refused, view, text, tag)}`
args.allowed = false
args.message = message
refusal.track(id, message)
return
}
refusal.track(id, null)
return
}
const refused = placementRefusal(
activity,
{ start: args.start.getTime(), end: args.end.getTime() },
view.net,
)
if (refused !== null) {
const message = refusalText(refused, view, text, tag)
args.allowed = false
args.message = message
refusal.track(id, message)
return
}
refusal.track(id, null)
},
// Final check before the commit; keyboard moves (Alt+arrows) arrive here without a drag and
// are kept on working days in the direction they travel.
onEventMove: (args) => {
const id = String(args.e.id())
const activity = byId.get(id)
if (activity === undefined) return
if (args.multimove.length > 1) {
const group = groupSpans(activity, args.newStart.getTime(), args.multimove)
const refused = groupRefusal(group.members, group.spans, view.net)
if (refused === null) return
args.preventDefault()
refusal.clear()
onRefused(id, refusalText(refused, view, text, tag))
return
}
const before = parseCivil(activity.start)
let start = args.newStart.getTime()
if (!isWorkingDay(dayOf(start)))
start = start >= before ? nextWorkingStart(start) : previousWorkingStart(start)
const end = addWorkingDays(start, duration(activity))
if (start !== args.newStart.getTime() || end !== args.newEnd.getTime()) {
args.newStart = new SuperScheduler.Date(start)
args.newEnd = new SuperScheduler.Date(end)
}
const refused = placementRefusal(activity, { start, end }, view.net)
if (refused === null) return
args.preventDefault()
refusal.clear()
onRefused(id, refusalText(refused, view, text, tag))
},
onEventMoved: (args) => {
refusal.clear()
const ids =
args.multimove.length > 0
? args.multimove.map((item) => String(item.event.id()))
: [String(args.e.id())]
onMoved(ids)
},
// A click (or Enter) selects the activity and docks its sheet; the hover card steps aside.
onEventClick: (args) => {
closeHover()
if (args.ctrl || args.meta) return
onOpen(String(args.e.id()))
},
onZoom: (args) => {
if (args.phase === 'end') onScale(args.cellWidth)
},
}
}, [
view,
columns,
narrow,
text,
tag,
schedulerLocale,
history,
controlRef,
separators,
linkBubble,
refusal,
name,
closeHover,
onEventsChange,
onMoved,
onOpen,
onRefused,
onScale,
])
// Imperative and once: a scroll prop would be re-applied on every update. The board opens on
// the week of the slip, scrolled down to Level 4.
useEffect(() => {
const control = controlRef.current
control?.scrollTo(INITIAL_VIEW_START, false, 'left')
control?.setScrollY(openingScroll())
}, [controlRef])
// The zoom slider lives in the product bar, styled as the drawing's scale bar.
useEffect(() => {
const control = controlRef.current
const host = zoomHost.current
if (control === null || host === null) return
const slider = createZoomSlider(control, host, {
min: BOARD.zoom.min,
max: BOARD.zoom.max,
scale: 'log',
detents: BOARD.zoom.detents,
label: text.chrome.scale,
})
return () => slider.dispose()
}, [controlRef, zoomHost, text])
return (
<SuperSchedulerComponent
{...config}
onBeforeTimeHeaderRender={timeHeaders}
resources={resources}
events={events}
links={links}
eventHover={eventHover}
dragCard={dragCard}
contextMenu={contextMenu}
/>
)
}Das Archiv enthält die Dateien dieses Beispiels und eine README. SuperScheduler Pro und Download-Schlüssel sind nicht enthalten.