Terminplanung und -buchung

@dayflow-pro/appointment-schedule leistet zweierlei:

  1. Organisierende legen fest, wann sie buchbar sind – direkt in der Wochenansicht von DayFlow bearbeitet;
  2. daraus werden auswählbare Zeitfenster für Teilnehmende, ganz ohne CalendarApp.

Es ist keine Buchungsplattform. Es gibt kein Backend, keinen Buchungs-Lebenszyklus, keinen Benachrichtigungsdienst und kein Bezahlsystem. Die Auswahl eines Zeitfensters löst einen Callback aus – ab da übernimmt Ihre Anwendung.

Installation

npm install @dayflow-pro/appointment-schedule
pnpm add @dayflow-pro/appointment-schedule
yarn add @dayflow-pro/appointment-schedule
bun add @dayflow-pro/appointment-schedule

Die Installationsschritte finden Sie im Leitfaden Pro-Installation.

temporal-polyfill ist erforderlich. @dayflow/core wird nur vom Organisator-Plugin und vom Wochen-Overlay-Layout benötigt. React, Vue, Svelte und Angular sind optionale Peer-Abhängigkeiten: Installieren Sie nur das Framework des Adapters, den Sie importieren.

import '@dayflow-pro/appointment-schedule/styles.css';
// Alternatively, for Tailwind projects that already load the core theme:
import '@dayflow-pro/appointment-schedule/styles.components.css';

Organisator-Plugin

appointmentPlugin.ts
import { createAppointmentSchedulePlugin } from '@dayflow-pro/appointment-schedule/plugin';
import type { AppointmentSchedule } from '@dayflow-pro/appointment-schedule/engine';
import '@dayflow-pro/appointment-schedule/styles.css';

let schedules: AppointmentSchedule[] = [];

const upsertSchedule = (schedule: AppointmentSchedule) => {
  schedules = [
    ...schedules.filter(current => current.id !== schedule.id),
    schedule,
  ];
  appointmentPlugin.updateConfig?.({ schedules });
};

export const appointmentPlugin = createAppointmentSchedulePlugin({
  schedules,
  drawerPlacement: 'calendar',
  drawerWidth: 420,
  onCreateSchedule: upsertSchedule,
  onUpdateSchedule: upsertSchedule,
});

Fügen Sie dieselbe Plugin-Instanz einer Wochenansicht in Ihrem Framework hinzu:

import { createWeekView } from '@dayflow/core';
import { DayFlowCalendar, useCalendarApp } from '@dayflow/react';
import { appointmentPlugin } from './appointmentPlugin';

export function App() {
  const calendar = useCalendarApp({
    views: [createWeekView()],
    plugins: [appointmentPlugin],
  });

  return (
    <>
      <button onClick={() => appointmentPlugin.api.openCreate()}>
        New appointment schedule
      </button>
      <DayFlowCalendar calendar={calendar} />
    </>
  );
}
<script setup lang="ts">
import { createWeekView } from '@dayflow/core';
import { DayFlowCalendar, useCalendarApp } from '@dayflow/vue';
import { appointmentPlugin } from './appointmentPlugin';

const calendar = useCalendarApp({
  views: [createWeekView()],
  plugins: [appointmentPlugin],
});
</script>

<template>
  <button @click="appointmentPlugin.api.openCreate()">
    New appointment schedule
  </button>
  <DayFlowCalendar :calendar="calendar" />
</template>
import { Component } from '@angular/core';
import { DayFlowCalendarModule } from '@dayflow/angular';
import { createWeekView } from '@dayflow/core';
import { appointmentPlugin } from './appointmentPlugin';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [DayFlowCalendarModule],
  template: `
    <button (click)="appointmentPlugin.api.openCreate()">
      New appointment schedule
    </button>
    <dayflow-calendar [calendar]="calendar"></dayflow-calendar>
  `,
})
export class AppComponent {
  readonly appointmentPlugin = appointmentPlugin;
  readonly calendar = {
    views: [createWeekView()],
    plugins: [appointmentPlugin],
  };
}
<script lang="ts">
  import { createWeekView } from '@dayflow/core';
  import { DayFlowCalendar, useCalendarApp } from '@dayflow/svelte';
  import { appointmentPlugin } from './appointmentPlugin';

  const calendar = useCalendarApp({
    views: [createWeekView()],
    plugins: [appointmentPlugin],
  });
</script>

<button onclick={() => appointmentPlugin.api.openCreate()}>
  New appointment schedule
</button>
<DayFlowCalendar {calendar} />

schedules sind kontrollierte Daten – das Plugin speichert nichts selbst. Die Verfügbarkeit wird als Hintergrundebene unter den Terminen gezeichnet und taucht daher nie in getEvents(), in der Suche, im Druck oder in der Remote-Synchronisierung auf.

Datenmodell

AppointmentSchedule ist der gemeinsame Datenvertrag von Organisator-Oberfläche, Buchungskomponenten und Headless-Engine.

EigenschaftTypZweck
idstringStabile Kennung des Zeitplans.
titlestringName, der in Organisator- und Buchungsoberflächen erscheint.
durationMinutesnumberDauer eines Termins.
slotIntervalMinutes?numberAbstand zwischen den Startzeiten. Voreingestellt ist die Dauer.
beforeBufferMinutes?numberBelegter Puffer vor jeder Buchung. Voreingestellt 0.
afterBufferMinutes?numberBelegter Puffer nach jeder Buchung. Voreingestellt 0.
timeZonestringIANA-Zone, in der die Verfügbarkeit definiert ist.
calendarId?stringZugehöriger Host-Kalender, genutzt für Farbe und angelegte Termine.
recurrence?AppointmentRecurrenceWöchentliche, einmalige oder Alle-N-Wochen-Regel.
availabilityWeeklyAvailability[]Wiederkehrende buchbare Zeiträume, nach Wochentag gruppiert. Erforderlich.
unavailableIntervals?WeeklyAvailability[]Wiederkehrende Pausen, die sichtbar bleiben, aber nie Zeitfenster erzeugen.
dateOverrides?DateAvailabilityOverride[]Zeiträume je Datum, die die Wochenregel ersetzen.
location?AppointmentLocationConfigEigener Link, Konferenzanbieter, Adresse oder Telefon.
meta?Record<string, unknown>Serialisierbare Metadaten, die Ihrer Anwendung gehören.

WeeklyAvailability enthält dayOfWeek (0 Sonntag bis 6 Samstag) und ein intervals-Array. Jedes AvailabilityInterval hat id, startTime und endTime als Wanduhrzeit im Format HH:mm. Ein DateAvailabilityOverride besteht aus einem ISO-date und ersetzenden intervals; ein leeres Array schließt dieses Datum.

Öffnen Sie den Editor aus Ihrer eigenen Oberfläche:

appointmentPlugin.api.openCreate();
appointmentPlugin.api.openEdit('product-demo');

Das Plugin ergänzt außerdem einen Eintrag im Schnellanlegen-Popup des Kalenders. Kalender ohne dieses Plugin behalten ihr gewohntes Schnellanlegen.

Gängige Optionen

Diese Felder bilden AppointmentSchedulePluginConfig.

OptionStandardZweck
schedulesErforderlichKontrollierte Liste der Terminpläne.
activeScheduleIdKeinerÖffnet den Editor mit einem bestimmten ausgewählten Zeitplan.
availabilitySnapMinutes15Minutenraster beim Bearbeiten der Verfügbarkeit.
drawerPlacement'viewport'Verankert den Editor am Ansichtsfenster oder am Kalender.
drawerWidth420Breite des Drawers in Pixeln oder als beliebige CSS-Länge.
drawerTargetErster KalenderElement oder Selektor für die Platzierung calendar.
drawerRendererEingebauter DrawerErsetzt den Organisator-Editor vollständig.
timeFormatFormat der aktiven AnsichtVerwendet das 12- oder 24-Stunden-Format.
conferenceProviders[]Ergänzt benannte Konferenzanbieter im Ortsauswahlfeld.
onCreateScheduleKeinerSpeichert einen neu angelegten Zeitplan in Ihrer Anwendung.
onUpdateScheduleKeinerSpeichert Änderungen an einem bestehenden Zeitplan.
onDeleteScheduleRequestKeinerBittet die Host-Anwendung, das Löschen zu bestätigen und auszuführen.
onExternalUpdateConflictKeinerMeldet eine während der Bearbeitung eingetroffene Aktualisierung der kontrollierten Daten.

Plugin-API

Das Plugin stellt unter appointmentPlugin.api eine AppointmentScheduleApi bereit.

MethodeZweck
openCreate(initial?)Öffnet einen neuen Entwurf, optional mit vorbelegten Feldern.
openEdit(scheduleId)Öffnet einen bestehenden kontrollierten Zeitplan.
closeEditor() / cancelDraft()Verwirft den aktuellen Entwurf und schließt den Drawer.
saveDraft()Führt den Erstellen- oder Aktualisieren-Callback aus und schließt bei Erfolg.
setActiveSchedule(scheduleId)Wechselt den aktiven Zeitplan, ohne den Editor zu öffnen.
getActiveSchedule()Gibt den aktiven kontrollierten Zeitplan zurück.
getDraft()Gibt den aktuellen bearbeitbaren Entwurf zurück, sofern bearbeitet wird.
draftManagerBietet Operationen zum Bearbeiten von Feldern und Verfügbarkeiten.
subscribeDraft(listener)Abonniert Entwurfsänderungen und gibt eine Funktion zum Abbestellen zurück.

Den Organisator-Editor anpassen

Mit drawerRenderer ersetzen Sie den Organisator-Drawer vollständig. Das Plugin behält Platzierung, aktiven Entwurf, Verfügbarkeitsbearbeitung im Kalender sowie Speichern und Abbrechen weiterhin in der Hand. Ihre Anwendung rendert im übergebenen Host eine ganz normale Framework-Komponente.

Der Renderer erhält AppointmentScheduleDrawerRenderArgs:

EigenschaftZweck
draftAktueller AppointmentSchedule-Entwurf.
isCreatingUnterscheidet einen neuen Zeitplan von einer Bearbeitung.
draftManagerAktualisiert Felder und liefert toggleDay, addInterval, updateInterval, removeInterval sowie Kopierhilfen.
calendarsVerfügbare Kalender in der Form { id, name, color? }.
conferenceProvidersRegistrierte Anbieter in der Form { id, name, icon? }.
placementAufgelöste Platzierung: 'calendar' oder 'viewport'.
drawerWidthAufgelöste CSS-Breite als Zeichenkette.
timeFormat / localeAnzeigeeinstellungen, geerbt aus Konfiguration und Kalender.
translateSchlägt eine Paketübersetzung mit Rückfallwert nach.
save()Führt den Erstellen- oder Aktualisieren-Callback der Host-Anwendung aus und schließt bei Erfolg.
cancel()Verwirft den Entwurf und schließt den Editor.

Der Callback ist eine Montagegrenze für Ihr Framework – kein Grund, das Formular per Hand über DOM-Operationen zu bauen. Die folgenden Beispiele rendern dieselben Steuerelemente für Titel, Speichern und Abbrechen als native Komponenten:

organiserDrawer.tsx
import { createRoot } from 'react-dom/client';
import type {
  AppointmentScheduleDrawerRenderArgs,
  AppointmentScheduleDrawerRenderer,
} from '@dayflow-pro/appointment-schedule/plugin';

function OrganiserDrawer({
  args,
}: {
  args: AppointmentScheduleDrawerRenderArgs;
}) {
  return (
    <form onSubmit={event => { event.preventDefault(); void args.save(); }}>
      <input
        value={args.draft.title}
        onChange={event =>
          args.draftManager.updateDraft({ title: event.target.value })
        }
      />
      <button type="submit">Save</button>
      <button type="button" onClick={args.cancel}>Cancel</button>
    </form>
  );
}

export const drawerRenderer: AppointmentScheduleDrawerRenderer =
  (initial, host) => {
    const root = createRoot(host);
    const render = (args: AppointmentScheduleDrawerRenderArgs) =>
      root.render(<OrganiserDrawer args={args} />);

    render(initial);
    // Closing the drawer from a React effect — flipping `drawerRenderer`,
    // for instance — reaches `destroy` while React is still rendering, and
    // unmounting a root there races that render. Defer it by a microtask.
    return {
      update: render,
      destroy: () => queueMicrotask(() => root.unmount()),
    };
  };
OrganiserDrawer.vue
<script setup lang="ts">
import type { AppointmentScheduleDrawerRenderArgs } from '@dayflow-pro/appointment-schedule/plugin';

const props = defineProps<{ args: AppointmentScheduleDrawerRenderArgs }>();
const updateTitle = (event: Event) => {
  props.args.draftManager.updateDraft({
    title: (event.target as HTMLInputElement).value,
  });
};
</script>

<template>
  <form @submit.prevent="args.save()">
    <input :value="args.draft.title" @input="updateTitle" />
    <button type="submit">Save</button>
    <button type="button" @click="args.cancel()">Cancel</button>
  </form>
</template>
organiserDrawerRenderer.ts
import { createApp, h, reactive } from 'vue';
import type {
  AppointmentScheduleDrawerRenderArgs,
  AppointmentScheduleDrawerRenderer,
} from '@dayflow-pro/appointment-schedule/plugin';
import OrganiserDrawer from './OrganiserDrawer.vue';

export const drawerRenderer: AppointmentScheduleDrawerRenderer =
  (initial, host) => {
    const state = reactive({ args: initial });
    const app = createApp({
      render: () => h(OrganiserDrawer, { args: state.args }),
    });
    app.mount(host);

    return {
      update: (args: AppointmentScheduleDrawerRenderArgs) => {
        state.args = args;
      },
      destroy: () => app.unmount(),
    };
  };
organiser-drawer.ts
import {
  ApplicationRef,
  Component,
  EnvironmentInjector,
  Input,
  createComponent,
} from '@angular/core';
import type {
  AppointmentScheduleDrawerRenderArgs,
  AppointmentScheduleDrawerRenderer,
} from '@dayflow-pro/appointment-schedule/plugin';

@Component({
  selector: 'app-organiser-drawer',
  standalone: true,
  template: `
    <input [value]="args.draft.title" (input)="updateTitle($event)" />
    <button type="button" (click)="args.save()">Save</button>
    <button type="button" (click)="args.cancel()">Cancel</button>
  `,
})
export class OrganiserDrawerComponent {
  @Input({ required: true }) args!: AppointmentScheduleDrawerRenderArgs;

  updateTitle(event: Event) {
    this.args.draftManager.updateDraft({
      title: (event.target as HTMLInputElement).value,
    });
  }
}

export const createDrawerRenderer = (
  appRef: ApplicationRef,
  environmentInjector: EnvironmentInjector
): AppointmentScheduleDrawerRenderer => (initial, host) => {
  const component = createComponent(OrganiserDrawerComponent, {
    hostElement: host,
    environmentInjector,
  });
  appRef.attachView(component.hostView);
  const update = (args: AppointmentScheduleDrawerRenderArgs) => {
    component.setInput('args', args);
    component.changeDetectorRef.detectChanges();
  };
  update(initial);

  return {
    update,
    destroy: () => {
      appRef.detachView(component.hostView);
      component.destroy();
    },
  };
};
OrganiserDrawer.svelte
<script lang="ts">
  import type { Readable } from 'svelte/store';
  import type { AppointmentScheduleDrawerRenderArgs } from '@dayflow-pro/appointment-schedule/plugin';

  let { state }: { state: Readable<AppointmentScheduleDrawerRenderArgs> } = $props();
  const updateTitle = (event: Event) => {
    $state.draftManager.updateDraft({
      title: (event.target as HTMLInputElement).value,
    });
  };
</script>

<form onsubmit={(event) => { event.preventDefault(); void $state.save(); }}>
  <input value={$state.draft.title} oninput={updateTitle} />
  <button type="submit">Save</button>
  <button type="button" onclick={$state.cancel}>Cancel</button>
</form>
organiserDrawerRenderer.ts
import { mount, unmount } from 'svelte';
import { writable } from 'svelte/store';
import type { AppointmentScheduleDrawerRenderer } from '@dayflow-pro/appointment-schedule/plugin';
import OrganiserDrawer from './OrganiserDrawer.svelte';

export const drawerRenderer: AppointmentScheduleDrawerRenderer =
  (initial, host) => {
    const state = writable(initial);
    const component = mount(OrganiserDrawer, {
      target: host,
      props: { state },
    });

    return {
      update: next => state.set(next),
      destroy: () => void unmount(component),
    };
  };

Übergeben Sie den entstandenen Renderer an createAppointmentSchedulePlugin({ drawerRenderer }). Änderungen am Entwurf rufen update auf; das Schließen des Drawers oder ein Austausch des Renderers ruft destroy auf. Die Render-Argumente liefern außerdem Kalender- und Konferenz-Metadaten, die Locale, timeFormat, translate, isCreating, save() und cancel().

Drawer-Host, Animation und Ebenen

Das Plugin besitzt das Host-Element, das es dem Renderer übergibt. Es trägt die Klasse df-appointment-custom-drawer-host plus einen Modifikator --calendar oder --viewport; Position, Breite und Stapelreihenfolge setzt das Plugin inline. Stylen Sie den Host über diese Klassen – verschieben Sie ihn nicht im DOM und ändern Sie seine Position nicht, denn das Plugin setzt beides bei jedem Render neu.

Ersatz-Drawer fahren genauso ein und aus wie der eingebaute. Beim Schließen markiert das Plugin den Host mit data-df-drawer-exiting, wartet, bis dessen Keyframe-Animationen beendet sind, und ruft erst dann destroy auf und entfernt den Host – der Drawer animiert also mit noch eingehängtem Inhalt hinaus, statt einfach zu verschwinden:

/* Defaults shipped by the package; override to change the motion. */
.df-appointment-custom-drawer-host {
  animation: df-slide-in-left 200ms cubic-bezier(0.16, 1, 0.3, 1);
}

.df-appointment-custom-drawer-host[data-df-drawer-exiting] {
  animation: df-slide-out-left 180ms cubic-bezier(0.7, 0, 0.84, 0) forwards;
}

Drei Details lohnen sich zu wissen. Gewartet wird nur auf Keyframe-Animationen; CSS-Transitions, die ein Framework an Hover- und Fokusringe hängt, verzögern den Drawer also nie. Endlose Animationen werden übersprungen, und ein Timer, begrenzt durch die Dauer der Animation selbst, sichert das Warten ab – ein Spinner im Drawer oder ein Tab im Hintergrund, in dem Animationsframes aussetzen, kann den Host somit nie im Dokument stranden lassen. Und wird die Animation entfernt oder greift prefers-reduced-motion: reduce, verschwindet der Host im selben Frame, in dem geschlossen wurde – genau das tut das Paket bereits für alle, die weniger Bewegung wünschen.

Der Drawer liegt bei z-index: 900, unterhalb des Schnellanlegen-Popups und der Dialoge des Kalenders bei 1000. So verschwindet das Hinzufügen-Menü nie hinter einem geöffneten Editor. Hebt man den Drawer über 1000, kehrt sich das um. Soll der Drawer diese Flächen überdecken, heben Sie diese ebenfalls an, statt nur den Drawer.

Wiederholung

Die Verfügbarkeit wiederholt sich standardmäßig wöchentlich. Der Drawer bietet zusätzlich einen einmaligen Zeitplan und eine eigene Alle-N-Wochen-Regel:

type AppointmentRecurrence = {
  frequency: 'weekly' | 'none' | 'custom';
  startDate?: string; // YYYY-MM-DD anchor
  intervalWeeks?: number; // custom: every N weeks
  endsOnDate?: string;
  endsAfterOccurrences?: number;
};

frequency: 'none' wendet die gewählten Wochenzeiten nur auf die Montag-bis-Sonntag-Woche des Ankerdatums an; die Verfügbarkeit schrumpft nicht auf das Ankerdatum allein zusammen.

Komplexere Regeln – monatlich, n-ter Wochentag oder RRULE – gehören in Ihre Anwendung. Bilden Sie sie über dateOverrides ab, die immer Vorrang vor der Wiederholungsregel haben.

Ort und Videokonferenz

Ein Zeitplan kann festlegen, wo der Termin stattfindet. Das ist Konfiguration, keine gebuchte Besprechung. Da ein Zeitplan vielfach gebucht werden kann, speichert er nie eine Beitritts-URL für eine einzelne Buchung:

type AppointmentLocationConfig =
  | { type: 'custom-link'; url: string; label?: string }
  | { type: 'conference'; providerId: string }
  | { type: 'in-person'; address: string }
  | { type: 'phone'; phone?: string };

Die Auswahl Ort im Drawer bietet immer „Eigener Meeting-Link“, „Vor Ort“ und „Telefonat“. Benannte Konferenz-Apps erscheinen erst, wenn Sie sie registrieren:

createAppointmentSchedulePlugin({
  schedules,
  conferenceProviders: [googleMeet, zoom], // ← the picker lists these first
});

Konferenzanbieter

DayFlow kommuniziert nie mit Google, Zoom oder Microsoft. Es definiert eine Schnittstelle mit einer einzigen Methode und ruft sie auf; OAuth-Token, API-Secret und Anbieter-SDK bleiben in Ihrem Backend:

import type { ConferenceProvider } from '@dayflow-pro/appointment-schedule/engine';

const googleMeet: ConferenceProvider = {
  id: 'google-meet',
  name: 'Google Meet',
  icon: '/icons/meet.svg',
  createConference: input =>
    fetch('/api/dayflow/google-meet', {
      method: 'POST',
      body: JSON.stringify({ ...input, start: input.start.toString() }),
    }).then(response => response.json()),
};
// → { provider, joinUrl, meetingId?, hostUrl?, password?, meta? }

createConference erhält ein CreateConferenceInput: scheduleId, title, Temporal-start und -end, timeZone sowie optional host und attendees. Zurück kommt eine Conference mit provider und joinUrl; meetingId, hostUrl, password und meta sind optional.

Nennen Sie dieselben Anbieter auch an der Buchungskomponente, wird die Ortszeile beschriftet und das Anbietersymbol angezeigt:

<AppointmentBooking schedule={schedule} conferenceProviders={[googleMeet]} />

Die Besprechung anlegen

Legen Sie die Videokonferenz an, wenn Teilnehmende bestätigen – nicht schon, wenn sie ein Zeitfenster markieren. Wer auf 10:00 Uhr klickt und dann weggeht, sollte keine Besprechung zurücklassen.

import {
  createBookingEvent,
  createConferenceForBooking,
} from '@dayflow-pro/appointment-schedule/engine';

const conference = await createConferenceForBooking({
  slot,
  schedule,
  providers: [googleMeet],
  attendees: [{ name: 'Ada Lovelace', email: 'ada@example.com' }],
});

const draft = createBookingEvent({ slot, schedule, conference });
// draft.location  → 'https://meet.google.com/abc-defg-hij'
// draft.conference → { provider: 'google-meet', joinUrl, meetingId }

Ein custom-link-Zeitplan wird ohne Netzwerkaufruf zum dauerhaften Raum der organisierenden Person aufgelöst; in-person- und phone-Zeitpläne ergeben undefined. Eine providerId ohne registrierten Anbieter löst einen Fehler aus, denn eine Buchung sollte ihren Meeting-Link nicht stillschweigend verlieren.

Warum nicht immer denselben Link verwenden?

Ziehen Sie für Google Meet, Zoom und Teams conference dem custom-link vor. Google empfiehlt eine neue Konferenz je Termin statt einer wiederverwendeten, weil gemeinsam genutzte Konferenzdaten über mehrere Termine hinweg Zugriffs- und Datenschutzprobleme verursachen.

Um einen Ort an anderer Stelle darzustellen – etwa in einer E-Mail oder auf einer Bestätigungsseite – verwenden Sie denselben reinen Resolver wie die Zeile mit den Besprechungsinformationen:

import { resolveLocation } from '@dayflow-pro/appointment-schedule/engine';

resolveLocation(schedule.location, { providers: [googleMeet] });
// → { kind: 'video', label: 'Google Meet', icon: '/icons/meet.svg', config }

Der Resolver liefert ein ResolvedLocation, sobald ein Ort konfiguriert ist.

Buchungskomponenten für Teilnehmende

import { AppointmentBooking } from '@dayflow-pro/appointment-schedule/booking';

<AppointmentBooking
  schedule={schedule}
  presentation={{ organiserName: 'Alex Morgan', locationLabel: 'Zoom meeting' }}
  busyIntervals={busyIntervals}
  layout='calendar-day-slots'
  onSelectSlot={slot => console.log(slot.start.toString())}
/>;

presentation enthält optionale, rein darstellende Inhalte für das Informationsfeld zur Besprechung. Verfügbarkeit und Buchungsverhalten kommen weiterhin aus schedule.

EigenschaftTypZweck
organiserNamestringZeigt den Namen der organisierenden Person und liefert den Ersatz-Anfangsbuchstaben, wenn kein Avatar vorhanden ist.
organiserAvatarstringBild-URL für den Avatar.
organiserUrlstringProfil-URL, die über den Avatar geöffnet wird.
titlestringÜberschrift über Dauer, Ort und Zeitzone. Übergeben Sie schedule.title, um dessen Titel zu übernehmen.
descriptionstringBegleittext unterhalb der Besprechungsdaten.
locationLabelstringÜberschreibt den aus schedule.location abgeleiteten Ortstext. Normalerweise ist das Feld des Zeitplans vorzuziehen.

Buchungsoptionen

Alle Framework-Adapter akzeptieren letztlich AppointmentBookingProps; Vue, Angular und Svelte benennen den entsprechenden Mount-Typ in ihren eigenen Einstiegspunkten.

EigenschaftsgruppeZweck
scheduleErforderlicher AppointmentSchedule, aus dem die Zeitfenster erzeugt werden.
calendarAppLiest DayFlow-Termine – und abonniert sie – als Belegtzeit der organisierenden Person.
presentationOptionaler AppointmentPresentation-Inhalt, siehe oben.
conferenceProvidersLöst Anbieternamen und -symbole für Konferenzorte auf.
busyIntervals / attendeeBusyIntervalsTemporal-Zeiträume, die Organisator-Zeitfenster entfernen oder Konflikte der Teilnehmenden überlagern.
layoutEin BookingLayout: calendar-day-slots, multi-day-slots oder week-overlay.
renderWeekOverlay / weekOverlayOptionsAktivieren und justieren die Wochenansicht über WeekOverlayRenderArgs und WeekOverlayOptions.
availableLayouts / onLayoutChangeSteuern, welche Layouts der eingebaute Umschalter anbietet.
displayTimeZone / timeZoneOptions / onDisplayTimeZoneChangeSteuern die angezeigte Zeitzone und deren Auswahlfeld.
timeFormat / onTimeFormatChangeSteuern den Wert von TimeFormat, entweder 12h oder 24h.
theme / locale / startOfWeekSetzen Wochenthema, Locale und den Wert von StartOfWeek (0, 1 oder 6).
multiDayCount / skipEmptyDaysJustieren das Mehrtages-Layout.
now / rangeStart / rangeEndÜberschreiben die aktuelle Zeit und den Bereich der Zeitfenster-Erzeugung.
selectedDate / onSelectDateKontrolliertes fokussiertes Datum.
selectedSlotId / onSelectSlotKontrollierte Zeitfensterauswahl samt Auswahl-Callback.
loading / disabledZeigen den Ladezustand oder deaktivieren die Interaktion.
onError / onRetryBinden eigene Fehlerberichte und Wiederholungslogik ein.
className / styleErgänzen Styles am Wurzelelement.
labelsÜberschreiben beliebige der sichtbaren Texte.
slotsErsetzen oder erweitern die unten aufgeführten Bereiche.
onAnalyticsEventErhält ein AppointmentBookingAnalyticsEvent samt seiner nicht personenbezogenen Nutzdaten.

Wählen Sie das Layout danach, wie viel Verfügbarkeit Sie zeigen möchten:

LayoutAm besten geeignet für
calendar-day-slotsEine Monatsauswahl mit den Zeiten des gewählten Tages.
multi-day-slotsDen Vergleich freier Zeiten mehrerer Tage in Spalten.
week-overlayBuchbare Zeiten auf einer Wochen-Zeitleiste. Erfordert den unten gezeigten Renderer.

Das Wochenlayout ist optional, weil es als einziges @dayflow/core benötigt:

import { renderWeekOverlay } from '@dayflow-pro/appointment-schedule/booking/week-overlay';

<AppointmentBooking
  layout='week-overlay'
  renderWeekOverlay={renderWeekOverlay}
/>;

Framework-Adapter

Verwenden Sie den Adapter des Frameworks, in dem Ihre Buchungsseite liegt. Alle Adapter nehmen dieselben Optionen entgegen und kümmern sich um Einhängen, reaktive Aktualisierungen und Aufräumen. Der Import eines Adapters zieht die Runtimes der anderen Frameworks nicht in Ihr Bundle.

Legen Sie einen serialisierbaren Zeitplan an, den jeder Adapter verwenden kann:

schedule.ts
import type { AppointmentSchedule } from '@dayflow-pro/appointment-schedule/engine';

export const schedule: AppointmentSchedule = {
  id: 'product-demo',
  title: 'Product demo',
  durationMinutes: 30,
  slotIntervalMinutes: 30,
  timeZone: 'Europe/London',
  recurrence: { frequency: 'weekly' },
  availability: [
    {
      dayOfWeek: 1,
      intervals: [{ id: 'monday', startTime: '09:00', endTime: '17:00' }],
    },
    {
      dayOfWeek: 2,
      intervals: [{ id: 'tuesday', startTime: '09:00', endTime: '17:00' }],
    },
    {
      dayOfWeek: 3,
      intervals: [{ id: 'wednesday', startTime: '09:00', endTime: '17:00' }],
    },
    {
      dayOfWeek: 4,
      intervals: [{ id: 'thursday', startTime: '09:00', endTime: '17:00' }],
    },
    {
      dayOfWeek: 5,
      intervals: [{ id: 'friday', startTime: '09:00', endTime: '17:00' }],
    },
  ],
  location: {
    type: 'custom-link',
    url: 'https://zoom.us/j/1234567890',
    label: 'Zoom meeting',
  },
};

Importieren Sie styles.components.css einmalig im Einstiegspunkt Ihrer Anwendung und verwenden Sie dann den passenden Adapter:

import { AppointmentBooking } from '@dayflow-pro/appointment-schedule/react';
import '@dayflow-pro/appointment-schedule/styles.components.css';
import { schedule } from './schedule';

export function BookingPage() {
  return (
    <AppointmentBooking
      schedule={schedule}
      presentation={{
        organiserName: 'Alex Morgan',
        locationLabel: 'Zoom meeting',
      }}
      layout='calendar-day-slots'
      onSelectSlot={slot => console.log(slot.start.toString())}
    />
  );
}
<script setup lang="ts">
import { AppointmentBooking } from '@dayflow-pro/appointment-schedule/vue';
import type { MountAppointmentBookingProps } from '@dayflow-pro/appointment-schedule/vue';
import '@dayflow-pro/appointment-schedule/styles.components.css';
import { schedule } from './schedule';

const bookingOptions: MountAppointmentBookingProps = {
  schedule,
  presentation: {
    organiserName: 'Alex Morgan',
    locationLabel: 'Zoom meeting',
  },
  layout: 'calendar-day-slots',
  onSelectSlot: slot => console.log(slot.start.toString()),
};
</script>

<template>
  <AppointmentBooking :options="bookingOptions" />
</template>
import { Component } from '@angular/core';
import {
  AppointmentBookingDirective,
  type MountAppointmentBookingProps,
} from '@dayflow-pro/appointment-schedule/angular';
import '@dayflow-pro/appointment-schedule/styles.components.css';
import { schedule } from './schedule';

@Component({
  standalone: true,
  imports: [AppointmentBookingDirective],
  template: '<div [dfAppointmentBooking]="bookingOptions"></div>',
})
export class BookingPage {
  readonly bookingOptions: MountAppointmentBookingProps = {
    schedule,
    presentation: {
      organiserName: 'Alex Morgan',
      locationLabel: 'Zoom meeting',
    },
    layout: 'calendar-day-slots',
    onSelectSlot: slot => console.log(slot.start.toString()),
  };
}
<script lang="ts">
  import { appointmentBooking } from '@dayflow-pro/appointment-schedule/svelte';
  import type { SvelteAppointmentBookingOptions } from '@dayflow-pro/appointment-schedule/svelte';
  import '@dayflow-pro/appointment-schedule/styles.components.css';
  import { schedule } from './schedule';

  const options: SvelteAppointmentBookingOptions = {
    schedule,
    presentation: {
      organiserName: 'Alex Morgan',
      locationLabel: 'Zoom meeting',
    },
    layout: 'calendar-day-slots',
    onSelectSlot: slot => console.log(slot.start.toString()),
  };
</script>

<div use:appointmentBooking={options}></div>

Adapter mit options-Objekt stellen eigene Bereiche über options.slots bereit; Komponenten mit Props bieten denselben slots-Vertrag direkt an. Tauschen Sie das Options-Objekt aus, um die Buchung zu aktualisieren. Ein Wechsel der Renderer-Sammlung hängt sauber neu ein, und das Aushängen des Hosts zerstört die Buchungsinstanz.

Integrations-APIs

Einfache Mount-API

Verwenden Sie createAppointmentBooking, wenn Sie die Buchungsoberfläche selbst einhängen wollen – aus einem anderen Framework oder aus einer Seite ganz ohne Framework:

import { createAppointmentBooking } from '@dayflow-pro/appointment-schedule/booking';

const booking = createAppointmentBooking('#booking', {
  schedule,
  onSelectSlot: slot => console.log(slot.start.toString()),
});

booking.update({ disabled: true });
booking.destroy();

Vorhandene Kindelemente des Ziels bleiben unangetastet. Die Mount-API erzeugt genau ein eigenes Kindelement und entfernt es bei destroy().

DOM-Bereiche erhalten ihre aktuellen Argumente und einen Container, der dem Renderer gehört:

createAppointmentBooking('#booking', {
  schedule,
  slots: {
    slotButton: (args, host) => {
      host.textContent = `${args.formattedTime} · ${price(args.slot)}`;
    },
  },
});

Ein Renderer kann nichts, eine Aufräumfunktion oder { update, destroy } zurückgeben. Die Handle-Form ist für Framework-Mount-APIs gedacht, die anhängen statt zu patchen.

Headless-Buchungscontroller

Verwenden Sie createBookingController, wenn Sie das mitgelieferte Buchungsverhalten mit eigenem Markup möchten. Er verwaltet ausgewähltes Datum und Zeitfenster, Zeitzone und Zeitformat, die Sichtbarkeit der Belegtzeiten Teilnehmender sowie die Gruppierung der Zeitfenster – rührt das DOM aber nie an:

import { createBookingController } from '@dayflow-pro/appointment-schedule/controller';

const booking = createBookingController({ schedule });
const unsubscribe = booking.subscribe(() => render(booking.getState()));

booking.getState().selectSlot(slot);
booking.setOptions({ schedule, disabled: true });

unsubscribe();
booking.destroy();

Anpassung

Design-Tokens

.df-appointment-booking ist der Theme-Geltungsbereich der Teilnehmenden-Komponente, nicht ihr einziges Token. Das Paket unterstützt die folgenden Custom Properties. Laden Sie Ihre Überschreibungen nach dem Stylesheet des Pakets und setzen Sie sie auf .df-appointment-booking – oder übergeben Sie ein eigenes className und sprechen Sie beide Klassen an.

.df-appointment-booking.my-booking-theme {
  --df-ap-accent: #2563eb;
  --df-ap-radius: 6px;
  --df-ap-sidebar-width: 320px;
}
TokenStandardSteuert
--df-ap-surfaceHintergrund aus dem KernHauptflächen
--df-ap-surface-sunkenGedämpfter KerntonVertiefte Flächen
--df-ap-fill#e8eaefNeutrale Füllungen
--df-ap-borderRahmen aus dem KernRahmen und Trennlinien
--df-ap-fgVordergrund aus dem KernPrimärtext
--df-ap-fg-mutedGedämpfter VordergrundSekundärtext
--df-ap-fg-subtle#9ca3afZurückhaltender Text
--df-ap-accentPrimärfarbe aus dem KernAusgewählte Steuerelemente
--df-ap-accent-fgVordergrund auf der PrimärfarbeText auf Akzentflächen
--df-ap-accent-softVom Akzent abgeleitetWeiche Hover-Hintergründe
--df-ap-accent-ringVom Akzent abgeleitetFokus- und Auswahlringe
--df-ap-available#22c55eVerfügbarkeitsanzeige
--df-ap-radius14pxEckenradius der Karten
--df-ap-radius-md9pxEckenradius der Steuerelemente
--df-ap-radius-sm7pxEckenradius kompakter Elemente
--df-ap-max-width1440pxMaximale Breite der Buchung
--df-ap-sidebar-width296pxBreite der Seitenleiste
--df-ap-slots-width320pxBreite der Tages-Zeitfensterspalte
--df-ap-week-height620pxHöhe der Wochen-Zeitleiste
--df-ap-slot-scroll-heightnoneHöhe der Tages-Zeitfensterliste
--df-ap-pad1.5remWichtigster Innenabstand
--df-ap-fontSystem-SchriftstapelTypografie der Buchung

Farben für Hover, Auswahl und Fokus leiten sich aus --df-ap-accent ab. Fortgeschrittene Themes können zusätzlich --df-ap-accent-soft und --df-ap-accent-ring direkt überschreiben.

Der Organisator-Drawer gehört zum DayFlow-Kalender und nutzt das Kern-Theme, nicht die Tokens der Teilnehmenden-Komponente:

:root {
  --df-color-background: #ffffff;
  --df-color-card: #ffffff;
  --df-color-foreground: #172033;
  --df-color-muted: #f4f6f8;
  --df-color-muted-foreground: #667085;
  --df-color-border: #d0d5dd;
  --df-color-primary: #7c3aed;
  --df-color-primary-foreground: #ffffff;
  --df-color-destructive: #dc2626;
  --df-color-ring: #7c3aed;
}

Verwenden Sie :root, wenn drawerPlacement auf viewport steht, denn dann hängt der Drawer außerhalb des Kalenderelements. Bei der Platzierung calendar können die Variablen stattdessen am Kalendercontainer gesetzt werden.

Anpassbare Bereiche

Bereiche werden von allen Adaptern unterstützt, ihre Renderer-Signaturen unterscheiden sich jedoch:

  • React-Bereiche geben React-Inhalte zurück. slotButton und dayCell erhalten zusätzlich defaultContent und können den eingebauten Inhalt damit umschließen.
  • Vue, Angular und Svelte nutzen options.slots. Jeder Renderer erhält (args, host) und schreibt in das übergebene DOM-Element. Er darf eine Aufräumfunktion oder ein { update, destroy }-Handle zurückgeben, wenn er eine Framework-Komponente einhängt.

Die folgenden Beispiele passen denselben slotButton-Bereich in jedem Framework korrekt an:

<AppointmentBooking
  schedule={schedule}
  slots={{
    slotButton: ({ defaultContent }) => (
      <>
        {defaultContent}
        <span>$120</span>
      </>
    ),
  }}
/>
import type { DomBookingSlots } from '@dayflow-pro/appointment-schedule/vue';

const slots: DomBookingSlots = {
  slotButton: ({ formattedTime }, host) => {
    host.textContent = `${formattedTime} · $120`;
  },
};

const bookingOptions = { schedule, slots };
import type { DomBookingSlots } from '@dayflow-pro/appointment-schedule/angular';

const slots: DomBookingSlots = {
  slotButton: ({ formattedTime }, host) => {
    host.textContent = `${formattedTime} · $120`;
  },
};

export class BookingPage {
  readonly bookingOptions = { schedule, slots };
}
<script lang="ts">
  import type { DomBookingSlots } from '@dayflow-pro/appointment-schedule/svelte';

  const slots: DomBookingSlots = {
    slotButton: ({ formattedTime }, host) => {
      host.textContent = `${formattedTime} · $120`;
    },
  };

  const options = { schedule, slots };
</script>

<div use:appointmentBooking={options}></div>

AppointmentBookingSlots definiert die folgenden Bereiche. Zu den exportierten Argumenttypen zählen SidebarSlotArgs, MeetingInfoSlotArgs, MonthPickerSlotArgs, SlotListSlotArgs, ToolbarSlotArgs, SlotButtonSlotArgs, DayCellSlotArgs und SelectedSummarySlotArgs.

BereichZweckWichtige Argumente
sidebarErsetzt die komplette Seitenspalte mit Besprechungsdetails und Minikalender.schedule, presentation, location, layout, defaultContent
meetingInfoErsetzt das Informationsfeld zur Besprechung.schedule, presentation, location, timeZone, defaultContent
meetingInfoFooterErgänzt Inhalte unterhalb des Informationsfelds.schedule, presentation, location, timeZone
monthPickerErsetzt die Monatsauswahl im Hauptbereich und in der Seitenleiste.selectedDate, availableDates, onSelectDate, compact, defaultContent
slotListErsetzt die Zeitliste des gewählten Tages in calendar-day-slots.date, slots, selectedSlotId, onSelectSlot, defaultContent
toolbarErsetzt die Toolbar in multi-day-slots und week-overlay.layout, timeZone, timeFormat, rangeLabel, defaultContent
toolbarExtraErgänzt Inhalte rechts neben den Toolbar-Steuerelementen.layout, timeZone, timeFormat, rangeLabel
slotButtonErsetzt den Inhalt jeder buchbaren Zeit-Schaltfläche.slot, formattedTime, isSelected, disabled, defaultContent
dayCellErsetzt den Inhalt jeder Tageszelle der Monatsauswahl.date, Monat, Verfügbarkeits- und Auswahlzustand, defaultContent
emptyDayWird gerendert, wenn der gewählte Tag keine freien Zeiten hat.date
emptyRangeWird gerendert, wenn der aktuelle Zeitraum keine buchbaren Zeiten hat.Keine
loadingErsetzt den Ladezustand.Keine
errorErsetzt den Fehlerzustand und erhält den Fehler sowie eine optionale Wiederholungsaktion.error, retry
selectedSummaryErgänzt eine Zusammenfassung unterhalb des Inhalts, sobald ein Zeitfenster gewählt ist.slot, formattedDate, formattedRange, timeZone

Headless: eigene Oberfläche bauen

Wenn Ihnen keines der Layouts zusagt, lassen Sie die Komponenten ganz weg. Die Engine ist eine reine Funktion, die einen Zeitplan entgegennimmt und Zeitfenster zurückgibt. Nichts sonst aus dem Paket landet in Ihrem Bundle.

import { generateSlots } from '@dayflow-pro/appointment-schedule/engine';
import { Temporal } from 'temporal-polyfill';

const slots = generateSlots({
  schedule,
  rangeStart: Temporal.PlainDate.from('2026-08-03'),
  rangeEnd: Temporal.PlainDate.from('2026-08-09'),
  busyIntervals,
  displayTimeZone: 'Europe/London',
  now: Temporal.Now.zonedDateTimeISO('Australia/Sydney'),
});
// slots: { id, scheduleId, start, end, displayTimeZone }[]

Die Eingabe ist ein SlotQuery; rangeStart und rangeEnd sind inklusive Temporal.PlainDate-Werte. busyIntervals und attendeeBusyIntervals enthalten BusyInterval-Werte mit Temporal-start und -end. Das Ergebnis ist ein AppointmentSlot[].

Die Engine rührt weder window noch document noch die Systemzeitzone an und lässt sich daher gefahrlos auf einem Server importieren. Sie exportiert außerdem ihre Bausteine: expandAvailability, recurrenceAppliesOn, sortBusyIntervals, mergeBusyIntervals, hasConflict und eventsToBusyIntervals. Damit können Sie eine eigene Pipeline zusammensetzen.

Aus einer Buchung einen Termin machen

Das Modul schreibt nie Termine. Stattdessen gibt es Ihnen eine reine Abbildungsfunktion:

import { createBookingEvent } from '@dayflow-pro/appointment-schedule/engine';

const draft = createBookingEvent({
  slot,
  schedule, // schedule.calendarId decides which calendar (and colour)
  attendee: { name: 'Ada Lovelace' }, // you collect it, the module never stores it
  conference, // optional; see Location and conferencing
  // titleTemplate: ({ attendee }) => `1:1 · ${attendee?.name}`,
});
// → { id, title: 'Meeting with Ada Lovelace', start, end, calendarId, location?, conference?, meta }

calendar.addEvent(draft);

meta trägt appointmentScheduleId und appointmentSlotId, sodass sich ein Termin bis zu seinem Zeitfenster zurückverfolgen lässt – dazu appointmentLocation und appointmentConference, sofern der Zeitplan einen Ort hat.

Barrierefreiheit

Zielt auf WCAG 2.2 AA. Die Monatsauswahl ist ein echtes role="grid" mit Navigation über Pfeiltasten, Pos1, Ende, Bild auf und Bild ab; Zeitfenster-Schaltflächen bieten aria-pressed und einen zugänglichen Namen aus Datum, Uhrzeit und Zeitzone; und die Zustände verfügbar, nicht verfügbar und ausgewählt werden nie allein über Farbe signalisiert.

Auf dieser Seite