# Gekoppelte Bereiche und gespeicherte Ansichten

> Eine Zeitleiste mit super-scheduler/panes in synchrone Bereiche teilen, Ereignisse dazwischen verschieben und Ansichten mit super-scheduler/views sichern.

Source: https://superscheduler.org/de/docs/panes-saved-views/
Reviewed: 2026-10-07

Ersetzen Sie SuperSchedulerComponent durch SchedulerPanes aus super-scheduler/panes und beschreiben Sie jeden Bereich mit einer id plus resources oder einem rowFilter; die Bereiche teilen horizontales Scrollen, Zoom und die Breite des Zeilenkopfs, scrollen vertikal unabhängig voneinander, und Ereignisse lassen sich zwischen ihnen ziehen. Für gespeicherte Ansichten gibt getViewState(control) ein JSON-taugliches Objekt mit Zoom, Scrollposition, Dichte, zugeklappten Zeilen und Spalten zurück, und applyViewState stellt es wieder her; wo es gespeichert wird, entscheidet Ihre Anwendung.

Zwei Anforderungen tauchen in jedem großen Planungsbildschirm auf. Die erste: einen Teil der Zeilen im Blick behalten, während der Rest scrollt, etwa eine Ablage „nicht zugewiesen“ unter den Zimmern oder ein Team über seinen Maschinen. Die zweite: später zur selben Ansicht zurückkehren, also zum Zoom, zum Datum und zu den Zeilen, die der Nutzer gerade angesehen hat. `super-scheduler/panes` und `super-scheduler/views` decken beides ab, und beide erfordern SuperScheduler Pro.

## Eine Zeitleiste in Bereiche teilen
`SchedulerPanes` rendert mehrere Planer übereinander auf einer gemeinsamen Zeitleiste. Sie teilen die horizontale Scrollposition, den Zoom und die Breite des Zeilenkopfs; jeder Bereich scrollt vertikal für sich und hat seine eigene Höhe. Trennleisten zwischen den Bereichen ändern deren Größe.

Die Komponente ersetzt `SuperSchedulerComponent`: Sie übergeben dieselben Planer-Props einmal, dazu ein `panes`-Array und eine Gesamthöhe `height`.

```tsx
// src/RoomsWithTray.tsx
import { useCallback, useMemo, useRef, useState } from 'react'
import type { SchedulerEventsChangeArgs, SchedulerProps, SuperScheduler } from 'super-scheduler'
import { SchedulerPanes } from 'super-scheduler/panes'
import type { SchedulerPane, SchedulerPanesHandle } from 'super-scheduler/panes'
import 'super-scheduler/styles.css'

const isTray = (resource: SuperScheduler.ResourceData) => resource.kind === 'tray'

// Module-level (or memoized): a new `panes` array resets the sizes the user dragged.
const PANES: SchedulerPane[] = [
  { id: 'rooms', rowFilter: (resource) => !isTray(resource), minSize: 200 },
  {
    id: 'tray',
    rowFilter: isTray,
    size: 140,
    minSize: 96,
    // Per-pane overrides: smaller events in the unassigned tray.
    props: { eventHeight: 28 },
  },
]

export function RoomsWithTray(props: {
  resources: SuperScheduler.ResourceData[]
  initial: SuperScheduler.EventData[]
}) {
  const [events, setEvents] = useState(props.initial)
  const panesRef = useRef<SchedulerPanesHandle>(null)

  // One list for every pane: each event appears in the pane that holds its resource.
  const onEventsChange = useCallback(
    (args: SchedulerEventsChangeArgs) => setEvents([...args.events]),
    [],
  )

  const shared = useMemo<Partial<SchedulerProps>>(
    () => ({
      onEventMove: (args) => {
        // `pane` is where the event lands; `sourcePane` is set only for a move between panes.
        if (args.sourcePane !== 'rooms' || args.pane !== 'tray') return
        // Unassigning a booking asks first; the drop waits for the answer.
        args.async = true
        void confirmWithUser(`Unassign ${args.e.text()}?`).then((ok) => {
          if (!ok) args.preventDefault()
          args.loaded()
        })
      },
    }),
    [],
  )

  return (
    <>
      <button type="button" onClick={() => panesRef.current?.scrollTo('2026-10-01', 'left')}>
        Go to 1 October
      </button>
      <SchedulerPanes
        {...shared}
        panesRef={panesRef}
        panes={PANES}
        // Total height of every pane, the splitter and the shared header.
        height={640}
        resources={props.resources}
        events={events}
        onEventsChange={onEventsChange}
        splitter={{ size: 6, step: 8 }}
        startDate="2026-10-01"
        days={60}
        scale="Day"
        cellWidth={44}
      />
    </>
  )
}
```
Sie sollten oben die Zimmer sehen und darunter eine 140 px hohe Ablage „nicht zugewiesen“, mit einem einzigen Zeitkopf ganz oben. Scrollen Sie einen der Bereiche seitwärts, und der andere folgt. Ziehen Sie eine Buchung aus der Ablage in ein Zimmer, und sie wandert dorthin; ziehen Sie eine aus einem Zimmer in die Ablage, und die Anwendung fragt vorher nach.

## Optionen der Bereiche
| Feld | Standard | Wirkung |
|---|---|---|
| `id` | erforderlich | Identifiziert den Bereich in Handlern (`args.pane`), in `panesRef` und im DOM (`data-pane`) |
| `resources` | | Die Zeilen dieses Bereichs |
| `rowFilter` | | Wählt die Zeilen dieses Bereichs aus den gemeinsamen `resources`; verwenden Sie entweder dies oder `resources` |
| `size` | `'auto'` | Pixel, ein Prozentsatz der freien Höhe (`'30%'`) oder `'auto'` für einen Anteil am Rest |
| `minSize` | `48` | Kleinste Höhe in Pixeln; reicht die Gesamthöhe nicht, haben die Mindestwerte Vorrang |
| `hidden` | `false` | Blendet den Bereich aus, lässt ihn aber gemountet, sodass erneutes Einblenden nichts kostet |
| `props` | | Props nur für diesen Bereich; Handler hier ersetzen die gemeinsamen |

Zeilen werden nach Ressourcen der obersten Ebene zugeordnet: Eine Elternressource nimmt ihre Kinder in ihren Bereich mit. Der erste Bereich ohne `resources` und ohne `rowFilter` erhält jede Ressource der obersten Ebene, die kein anderer Bereich übernommen hat.

## Layout und Trennleiste
| Prop | Standard | Wirkung |
|---|---|---|
| `height` | erforderlich | Gesamthöhe in Pixeln: alle Bereiche, die Trennleisten und der gemeinsame Kopf |
| `timeHeader` | `'first'` | `'first'` zeigt den Zeitkopf nur im ersten sichtbaren Bereich; `'all'` in jedem Bereich |
| `scrollbar` | `'last'` | Horizontale Bildlaufleiste nur im letzten Bereich, oder `'all'` |
| `splitter` | `true` | `{ size, step }` legt ihre Dicke (6 px) und den Tastaturschritt (8 px) fest; `false` entfernt sie |
| `onPaneResize` | | `{ sizes }` nach einer übernommenen Größenänderung, nach Bereichs-ID |

Die Trennleiste ist fokussierbar und hat `role="separator"`; ihr Wert ist die Höhe des Bereichs darunter. Pfeil oben und Pfeil unten verschieben sie um `step`, Umschalt+Pfeil oben und Umschalt+Pfeil unten um 40 px, Pos1 und Ende führen zu den Grenzen, und Eingabe oder ein Doppelklick stellt die deklarierten Größen wieder her. Während des Ziehens wird eine Vorschau der Bereiche gezeigt; ihre Höhen ändern sich beim Loslassen. In 0.1.0 ist ihr barrierefreier Name das englische „Pane size“, ohne Option zur Übersetzung.

> **Behavior:**
> Vom Nutzer geänderte Höhen bleiben erhalten, bis sich `panes` oder `height` ändert. Definieren Sie `panes` auf Modulebene oder memoisieren Sie es, wie im Snippet; ein neues Array bei jedem Rendern würde die Trennleiste zurücksetzen. Um Größen über Sitzungen hinweg zu behalten, speichern Sie sie aus `onPaneResize` und übergeben sie als `size` des jeweiligen Bereichs zurück.

## Ereignisse in Bereichen
Übergeben Sie alle Ereignisse einmal. Jeder Bereich zeigt die Ereignisse, deren `resource` zu seinen Zeilen gehört, und ein Ereignis wechselt in einen anderen Bereich, wenn seine Ressource das tut.

- **Kontrolliert:** `events` plus `onEventsChange`. Der Handler erhält die vollständige, zusammengeführte Liste in `args.events` sowie `args.pane` für den Bereich, in dem die Änderung stattfand. Übernehmen Sie sie wie unter [kontrollierter Zustand](https://superscheduler.org/de/docs/controlled-state/) beschrieben.
- **Unkontrolliert:** `defaultEvents`; die Bereiche verwalten die Liste dann selbst.

Wenn Sie `control.events.list` eines Bereichs direkt ändern, wird das nicht mit den anderen Bereichen geteilt; gehen Sie über den State oder die API `control.events`.

### Verschieben zwischen Bereichen
Ziehen zwischen Bereichen ist standardmäßig aktiv (`crossPaneMove: true`); `false` hält jedes Ereignis in seinem Bereich. Mit dem Standard `eventMoveHandling: 'Update'` wird eine Verschiebung zwischen Bereichen einmal gemeldet, als Änderung `'move'` in `onEventsChange`.

Jeder gemeinsame Handler erhält `args.pane`. Bei einer Verschiebung zwischen Bereichen erhalten `onEventMove` und `onEventMoved` zusätzlich `args.sourcePane`, sodass eine Regel von der Richtung abhängen kann: Das Snippet verlangt nur bei Verschiebungen von den Zimmern in die Ablage eine Bestätigung, mit `args.async` und `args.loaded()`. Eine abgebrochene oder abgelehnte Verschiebung lässt die Daten unverändert.

## Auf das Control jedes Bereichs zugreifen
`SchedulerPanes` erzeugt die Planer und gibt Ihnen deren Controls über `panesRef`:

- `controls`: eine Map von Bereichs-ID zu Control, und `control(id)` für einen einzelnen Bereich;
- `forEach(run)`, um etwas in jedem Bereich aufzurufen;
- `scrollTo(date, position)`, um alle gemeinsam zu scrollen;
- `update(options)`, um Optionen auf jeden Bereich anzuwenden.

Um die [React-Render-Slots](https://superscheduler.org/de/docs/react-render-slots/) in Bereichen zu nutzen, übergeben Sie die Komponente dieses Einstiegspunkts: `component={SuperSchedulerComponent}`, importiert aus `super-scheduler/react-render`. Das Bereichsmodul importiert sie nur, wenn Sie das tun.

### Selbst platzierte Planer koppeln
Wenn die Planer nicht übereinander liegen (ein Personalplan oben auf der Seite und ein Raumplan weiter unten), behalten Sie Ihre eigenen Komponenten und koppeln deren Controls mit `linkPanes`:

```tsx
// src/LinkedBoards.tsx
import { useEffect, useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { linkPanes } from 'super-scheduler/panes'

// Two schedulers placed by your own layout (here, a page section apart) that move together.
export function LinkedBoards(props: {
  staff: SuperScheduler.ResourceData[]
  rooms: SuperScheduler.ResourceData[]
  shifts: SuperScheduler.EventData[]
  bookings: SuperScheduler.EventData[]
}) {
  const staff = useSchedulerControl()
  const rooms = useSchedulerControl()
  const shifts = useMemo(() => props.shifts.slice(), [props.shifts])
  const bookings = useMemo(() => props.bookings.slice(), [props.bookings])

  useEffect(() => {
    if (staff.control === null || rooms.control === null) return
    // Horizontal scroll always; zoom and row header width too unless turned off.
    const link = linkPanes([staff.control, rooms.control], { zoom: true, rowHeaderWidth: true })
    return () => link.dispose()
  }, [staff.control, rooms.control])

  return (
    <>
      <h2>Staff</h2>
      <SuperSchedulerComponent
        controlRef={staff.controlRef}
        startDate="2026-10-01"
        days={30}
        scale="Day"
        resources={props.staff}
        events={shifts}
      />
      <h2>Rooms</h2>
      <SuperSchedulerComponent
        controlRef={rooms.controlRef}
        startDate="2026-10-01"
        days={30}
        scale="Day"
        resources={props.rooms}
        events={bookings}
      />
    </>
  )
}
```
Horizontales Scrollen wird immer geteilt. Zoom und Breite des Zeilenkopfs werden geteilt, sofern Sie nicht `zoom: false` oder `rowHeaderWidth: false` übergeben. Rufen Sie `dispose()` auf, um die Kopplung aufzuheben.

## Eine Ansicht speichern und wiederherstellen
Eine Ansicht beschreibt, wie der Nutzer auf die Daten blickt, nicht die Daten selbst. `getViewState(control, include?)` erfasst sie als kleines, JSON-taugliches Objekt; `applyViewState(control, state, options?)` stellt sie wieder her.

| Schlüssel in `include` | Gespeicherte Felder | Hinweise |
|---|---|---|
| `'zoom'` | `cellWidth`, `zoomLevel` | `zoomLevel` ist der Index der aktiven Stufe in `zoomLevels`; halten Sie deren Reihenfolge daher stabil |
| `'scroll'` | `anchorDate`, `topRowId`, `topOffset` | Das Datum am linken Rand und die oberste Zeile, per ID, mit dem Versatz innerhalb dieser Zeile |
| `'density'` | `density` | Nur wenn Sie die Prop `density` setzen |
| `'collapsed'` | `collapsed` | IDs der zugeklappten Elternelemente im Baum |
| `'columns'` | `columnWidths`, `columnOrder` | Breiten und Reihenfolge der Spalten im Zeilenkopf |

Jeder Zustand hat `v: 1`. Ohne `include` werden alle fünf Schlüssel erfasst.

```ts
// src/savedView.ts
import type { SuperScheduler } from 'super-scheduler'
import { applyViewState, getViewState } from 'super-scheduler/views'
import type { SchedulerViewState, ViewStateKey } from 'super-scheduler/views'

// What this application restores from the view. Density and columns stay in React state here.
const KEYS: readonly ViewStateKey[] = ['zoom', 'scroll', 'collapsed']

const storageKey = (user: string, view: string) => `planning-view:${user}:${view}`

/** Saves the current view. The application owns storage: here localStorage, per user. */
export function saveView(control: SuperScheduler.Scheduler, user: string, view: string): void {
  const state = getViewState(control, KEYS)
  try {
    localStorage.setItem(storageKey(user, view), JSON.stringify(state))
  } catch {
    // Storage can be full or disabled; a view is a convenience, not data.
  }
}

/** Stored values are untrusted input: check the shape before using them. */
function isViewState(value: unknown): value is SchedulerViewState {
  return typeof value === 'object' && value !== null && (value as { v?: unknown }).v === 1
}

/** Restores a saved view. Resolves false when nothing was saved or the rows never appeared. */
export async function restoreView(
  control: SuperScheduler.Scheduler,
  user: string,
  view: string,
): Promise<boolean> {
  let saved: unknown = null
  try {
    saved = JSON.parse(localStorage.getItem(storageKey(user, view)) ?? 'null')
  } catch {
    return false
  }
  if (!isViewState(saved)) return false
  // Waits (up to 5 s) for the rows and the saved top row, for data that loads after mount.
  return applyViewState(control, saved, { when: 'rows', timeout: 5000 })
}
```
```tsx
// src/PlannerWithViews.tsx
import { useEffect, useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { restoreView, saveView } from './saved-view'

const ZOOM_LEVELS: SuperScheduler.ZoomLevel[] = [
  { id: 'weeks', properties: { scale: 'Week', cellWidth: 120 } },
  { id: 'days', properties: { scale: 'Day', cellWidth: 44 } },
]

export function PlannerWithViews(props: {
  user: string
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
}) {
  const { controlRef, control } = useSchedulerControl()
  const events = useMemo(() => props.events.slice(), [props.events])

  // Restore once the control exists; keep row ids stable so the top row can be found again.
  useEffect(() => {
    if (control !== null) void restoreView(control, props.user, 'default')
  }, [control, props.user])

  return (
    <>
      <button
        type="button"
        disabled={control === null}
        onClick={() => control && saveView(control, props.user, 'default')}
      >
        Save this view
      </button>
      <SuperSchedulerComponent
        controlRef={controlRef}
        startDate="2026-01-01"
        days={365}
        zoomLevels={ZOOM_LEVELS}
        zoom="days"
        treeEnabled
        resources={props.resources}
        events={events}
      />
    </>
  )
}
```
Scrollen Sie zu einem Datum, klappen Sie eine Etage zu, drücken Sie „Save this view“ und laden Sie die Seite neu: Der Planer kehrt zum selben Datum und zur selben Zeile zurück, mit zugeklappter Etage.

So funktioniert das Wiederherstellen:

- `when: 'rows'` (Standard) wartet, bis die Zeilen und die gespeicherte oberste Zeile existieren; das deckt Daten ab, die erst nach dem Mount eintreffen. Erscheinen sie nicht innerhalb von `timeout` (standardmäßig 5.000 ms), wird das Promise mit `false` aufgelöst.
- `when: 'now'` wendet sofort an; existiert die gespeicherte oberste Zeile nicht mehr, wird der gespeicherte Versatz als absolute Scrollposition verwendet.
- `animate: true` animiert die Zoomänderung.
- In `collapsed` aufgeführte Elternelemente werden zugeklappt, alle anderen Elternelemente aufgeklappt.
- Passen die gespeicherten Spalten nicht mehr zu `rowHeaderColumns` (andere Spaltenzahl), wird nichts angewendet, und das Promise wird mit `false` aufgelöst. Ein Zustand mit einer anderen Version als 1 wird ebenfalls mit `false` aufgelöst.
- Das Wiederherstellen verschiebt den Tastaturfokus nicht.

> **Tip:**
> Wenn Ihre Anwendung `density` oder `rowHeaderColumns` in React-State hält, stellen Sie diese über Ihren State wieder her und lassen Sie sie aus `include` heraus, wie im Snippet. `applyViewState` ändert sie am Control, und eine spätere Prop-Änderung aus React würde das überschreiben.

Mit Bereichen speichern und wiederherstellen Sie über das Control eines einzelnen Bereichs (`panesRef.current?.control('rooms')`): Zoom und horizontales Scrollen werden geteilt, während vertikales Scrollen und zugeklappte Zeilen zu diesem Bereich gehören.

## Was Ihre Anwendung verantwortet
- **Speicherung.** `localStorage` für einen Browser oder Ihr Backend, damit die Ansicht dem Nutzer auf alle Geräte folgt. Die Bibliothek speichert nie etwas.
- **Benennen und Teilen.** Benannte Ansichten, Standards pro Team, Links, die eine Ansicht öffnen.
- **Validierung.** Gespeicherte Ansichten sind nicht vertrauenswürdige Eingaben: Prüfen Sie Struktur und `v` vor dem Anwenden und verwerfen Sie Ansichten, die die Prüfung nicht bestehen.
- **Stabile IDs.** Zeilen-IDs müssen von einer Sitzung zur nächsten dieselben Zeilen bezeichnen, damit `topRowId` und `collapsed` funktionieren.
- **Bereichsgrößen und Auswahl.** Beides ist nicht Teil einer Ansicht; speichern Sie Bereichsgrößen aus `onPaneResize`, wenn Sie sie wiederhaben möchten.

## Verwandte Themen
→ https://superscheduler.org/de/examples/training-rooms/
- [Ressourcenbäume, Spalten und Auswahl](https://superscheduler.org/de/docs/trees-columns-selection/) zu den zugeklappten Zeilen und Spalten, die eine Ansicht speichert.
- [Zeitskalen und Zoom](https://superscheduler.org/de/docs/time-scales-zoom/) zu den Zoomstufen, die eine Ansicht wiederherstellt.
