MobilityFleetline (Fictional business)
Fleet rental planning
A compact is grounded on pickup day. Hand its rental to another car, keep the cleaning slot and see where the fleet runs out.
- Time scale
- Demo with synthetic data
- days / weeks
- Pro capabilities
- Resource treeMulti-day eventsContinuous zoomMinimapDetail panel
Loads SuperScheduler Pro only when you ask for it.
The situation
Fleetline is a fictional rental branch with ten vehicles: three compacts, two estates, two electric cars and three vans. Customers book a category and dates; the branch decides which vehicle goes out, and every return needs cleaning and checks (and charging, for the electric cars) before the next pickup.
A spreadsheet hides that turnaround: the next customer arrives while the van is still being cleaned. The board has to keep each rental’s dates fixed, refuse a downgrade or a van for a compact booking, show the preparation after every return, and tell the planner which weeks the fleet is almost sold out.
What you will doYou will move a stranded rental to a free car (or a free upgrade), extend a van rental from its detail panel, and use the zoom and the minimap to find the week the whole fleet is out.
Where the library ends and your application begins
SuperScheduler provides
- Categories and their vehicles as a resource tree, with group rows that take no rentals.
- Multi-day bars at exact pickup and return times, vertical-only moves (moveHDisabled) and overlap refusal per vehicle.
- Time-anchored areas inside each bar for the preparation after a return.
- Day and week zoom levels with an animated switch and Ctrl/⌘ + wheel, and a minimap whose window pans and zooms the board.
- Viewport and zoom notifications through super-scheduler/hooks, keyboard moves and announcements.
This example’s code decides
- Which vehicle may serve which booking: the same category, or a free upgrade from compact to estate.
- Preparation time per category, and the clash check that keeps it clear of the next pickup.
- The fleet-load series the minimap draws, and its amber and red thresholds.
- Extensions from the detail panel, and every label on this page.
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: the category tree, day and week zoom, the preparation area in onBeforeEventRender, the move checks, and the minimap and viewport subscription created once the control is mounted.
/**
* Fleetline rental board: vehicles grouped by category over seven weeks, with a fleet-load minimap.
*
* Library: resource tree, multi-day bars at exact times, vertical-only moves, overlap refusal with
* conflict highlight, day/week zoom (buttons, Ctrl/⌘+wheel, the minimap's edges), time-anchored
* areas for the preparation tail, the minimap widget and viewport subscriptions.
* Application: which vehicle may serve which booking, how long preparation takes, which rentals
* clash and what the minimap measures. 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 { format, plural } from '~/i18n/format'
import { DAY, formatCivil, type CivilDateTime } from '../../scheduler/civil'
import { toneClass } from '../../scheduler/tones'
import { createSpanHeaderHook } from '../kit/span-header-hook'
import {
civilOf,
isVehicleId,
itemTone,
readFleetFields,
toResources,
type FleetEvent,
} from './adapter'
import { PERIOD, type FleetItem } from './model'
import { assignment, clashes, fleetLoad, rentalDays } from './rules'
import type { FleetlineText, ZoomId } from './text'
import './fleet.css'
export interface FleetlineSchedulerProps {
readonly events: FleetEvent[]
/** The same data read back as the branch's model, for the rules and the minimap. */
readonly items: readonly FleetItem[]
readonly text: FleetlineText
/** 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 controlRef: RefObject<SuperScheduler.Scheduler | null>
readonly onEventsChange: (args: SchedulerEventsChangeArgs) => void
readonly onSelect: (id: string) => void
readonly onMoved: (id: string) => void
/** A refusal while dragging (null once the target is valid again) or after a keyboard move. */
readonly onRefused: (message: string | null) => void
readonly onZoomLevel: (level: ZoomId) => void
/** The visible span, published once scrolling or zooming settles. */
readonly onViewport: (start: CivilDateTime, end: CivilDateTime) => void
}
const ZOOM_LEVELS: SuperScheduler.ZoomLevel[] = [
{
id: 'days',
properties: {
scale: 'Day',
cellWidth: 60,
timeHeaders: [{ groupBy: 'Week' }, { groupBy: 'Day' }],
},
},
{
id: 'weeks',
properties: {
scale: 'Week',
cellWidth: 150,
timeHeaders: [{ groupBy: 'Month' }, { groupBy: 'Week' }],
},
},
]
const ZOOM_IDS: readonly ZoomId[] = ['days', 'weeks']
/** Minimap colours: amber from 75 % of the fleet busy, red from 90 %. */
const loadTone = (value: number) => (value >= 0.9 ? 'danger' : value >= 0.75 ? 'warn' : 'base')
export function FleetlineScheduler(props: FleetlineSchedulerProps) {
const { events, items, text, tag, schedulerLocale, initialZoom, controlRef } = props
const { onEventsChange, onSelect, onMoved, onRefused, onZoomLevel, onViewport } = props
const resources = useMemo(() => toResources(text), [text])
const clashing = useMemo(() => new Set(clashes(items)), [items])
// Day cells under week groups, or week cells under month groups (this period has no month shorter than a week).
const headerHook = useMemo(
() =>
createSpanHeaderHook(tag, (span, level) =>
level === 'group' ? (span > 7 * DAY ? 'month' : 'week') : span > DAY ? 'week' : 'day',
),
[tag],
)
const config = useMemo<SchedulerProps>(() => {
const refusal = (
data: object,
resource: SuperScheduler.ResourceId,
conflicts: number,
): string | null => {
const fields = readFleetFields(data)
if (fields === null) return null
if (fields.kind === 'service') return text.rules.fixed
if (!isVehicleId(resource)) return text.rules.categoryRow
if (assignment(resource, fields.booked) === null)
return text.rules.wrongCategory[fields.booked]
return conflicts > 0 ? text.rules.busy : null
}
return {
locale: schedulerLocale,
startDate: PERIOD.start,
days: PERIOD.days,
zoomLevels: ZOOM_LEVELS,
zoom: initialZoom,
zoomPosition: 'left',
// The branch plans Monday-to-Sunday weeks, whatever the page language.
weekStarts: 1,
height: '100%',
treeEnabled: true,
// Category rows group vehicles; nothing is rented to a category row.
treePreventParentUsage: true,
rowHeaderWidth: 176,
rowHeaderWidthAutoFit: false,
eventHeight: 22,
rowMarginTop: 3,
rowMarginBottom: 3,
// Pickups and returns keep their hour: bars are drawn at exact times, not whole days.
useEventBoxes: 'Never',
allowEventOverlap: false,
conflictHighlight: true,
dragCard: { duration: false },
durationBarVisible: false,
showToolTip: false,
eventHoverHandling: 'Disabled',
keyboardEnabled: true,
keyboardTarget: 'component',
keyboardMode: 'Full',
controlRef,
onEventsChange,
onBeforeTimeHeaderRender: headerHook,
onBeforeRowHeaderRender: (args) => {
const id = args.row.id
if (!isVehicleId(id)) {
args.row.cssClass = 'ss-rh--group'
return
}
const plate = SuperScheduler.Util.escapeHtml(args.row.name)
const spec = SuperScheduler.Util.escapeHtml(text.specs[id])
args.row.html = `<span class="fl-rh"><span class="fl-rh__plate">${plate}</span><span class="fl-rh__spec">${spec}</span></span>`
},
onBeforeEventRender: (args) => {
const fields = readFleetFields(args.data)
if (fields === null) return
const classes = ['ss-ev', 'fl-ev', toneClass(itemTone(fields))]
if (fields.kind === 'service') {
args.data.html = `<span class="ss-evt"><span class="ss-evt__title">${SuperScheduler.Util.escapeHtml(text.services[fields.reason])}</span></span>`
classes.push('ss-ev--hatched')
} else {
const resource = args.data.resource
const upgraded =
resource !== undefined &&
isVehicleId(resource) &&
assignment(resource, fields.booked) === 'upgrade'
const days = plural(
text.days,
rentalDays({ pickup: civilOf(args.data.start), return: fields.returnAt }),
tag,
)
const badge = upgraded
? `<span class="ss-evt__badge">${SuperScheduler.Util.escapeHtml(text.upgrade)}</span>`
: ''
args.data.html = `<span class="ss-evt"><span class="ss-evt__title">${SuperScheduler.Util.escapeHtml(fields.code)}</span><span class="ss-evt__meta">${SuperScheduler.Util.escapeHtml(days)}</span>${badge}</span>`
// The preparation after the return, drawn as a hatched tail anchored to its times.
args.data.areas = [
{
start: fields.returnAt,
end: args.data.end,
top: 0,
bottom: 0,
cssClass: 'fl-prep',
action: 'None',
},
]
if (clashing.has(String(args.data.id))) classes.push('fl-ev--clash')
}
args.data.cssClass = classes.join(' ')
},
// Only the row can change (moveHDisabled): check the category; the library checks overlaps.
onEventMoving: (args) => {
const message = refusal(args.e.data, args.resource, args.conflicts.length)
if (message !== null && message !== text.rules.busy) {
args.allowed = false
args.message = message
} else if (message !== null) {
args.message = message
}
onRefused(message)
},
// Final check before the commit (also covers keyboard moves).
onEventMove: (args) => {
const message = refusal(args.e.data, args.newResource, 0)
if (message !== null) {
args.preventDefault()
onRefused(message)
}
},
onEventMoved: (args) => 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)
},
}
}, [
clashing,
controlRef,
headerHook,
initialZoom,
onEventsChange,
onMoved,
onRefused,
onSelect,
onZoomLevel,
schedulerLocale,
tag,
text,
])
// The minimap and the viewport subscription need the mounted control; both go with it.
const minimapHost = useRef<HTMLDivElement>(null)
const minimap = useRef<Minimap | null>(null)
const latestItems = useRef(items)
useEffect(() => {
latestItems.current = items
minimap.current?.refresh()
}, [items])
useEffect(() => {
const control = controlRef.current
const host = minimapHost.current
if (control === null || host === null) return
const date = (value: SuperScheduler.Date) =>
formatCivil(value.getTime(), tag, { day: 'numeric', month: 'short' })
const widget = createMinimap(control, host, {
height: 30,
peak: 'absolute',
max: 1,
// One value per bucket: the share of the fleet that is out, in preparation or in the workshop.
series: (range) => {
const values = new Float32Array(range.buckets)
const start = range.start.getTime()
for (let index = 0; index < values.length; index++) {
const from = start + index * range.bucketMs
values[index] = fleetLoad(latestItems.current, from, from + range.bucketMs)
}
return values
},
tone: loadTone,
marks: { today: false, months: true, past: false },
labels: {
label: text.minimap.label,
zoom: text.minimap.zoom,
valueText: (start, end) => format(text.minimap.range, { from: date(start), to: date(end) }),
},
})
minimap.current = widget
const unsubscribe = subscribeScheduler(control, 'viewport', (state) =>
onViewport(civilOf(state.start), civilOf(state.end)),
)
return () => {
unsubscribe()
widget.dispose()
minimap.current = null
}
}, [controlRef, onViewport, 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} />
</div>
<div
ref={minimapHost}
className="fl-minimap shrink-0 border-t border-line bg-paper-2/60 px-2 py-1.5"
/>
</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.