# timum booking

## Introduction

timum BookingJs is a fully customizable appointment booking frontend app, intended to be integrated into every web page or web app or mobile app. It is ready to use out-of-the-box. And it provides the ability to customize it.
This documentation guides you through all the customization capabilities.

## Content

- [Features](#features)
- [How to Integrate](#how-to-integrate)
  - [In a Script Tag](#in-a-script-tag)
  - [As ESM import](#as-esm-import)
  - [As React component import](#as-react-component-import)
  - [Entity Referencing](#entity-referencing)
    - [Channel Reference](#channel-reference)
    - [Resource Reference](#resource-reference)
    - [Resource Reference with channelKey](#resource-reference-with-channelKey)
      - [Example](#example)
    - [Customer pre-identification](#customer-pre-identification)
      - [Identification via pDataId](#identification-via-pdataid)
      - [Identification via logged-in User](#identification-via-logged-in-user)
- [Advanced Customisation](#advanced-usage)
  - [muiTheme](#muitheme)
  - [fcConfig](#fcconfig)
  - [appConfig](#appconfig)
  - [Font Loading](#font-loading)
  - [How to use Callbacks](#how-to-use-callbacks)
    - [Booking Related](#booking-related)
    - [Cancelation Related](#cancelation-related)
    - [Data Fetching Related](#data-fetching-related)
  - [Localisation](#localisation)
  - [Booking Form Fields](#booking-form-fields)
    - [Custom Fields](#custom-fields)
  - [Global config override](#global-config-override)  
- [Examples](#examples)

  - [Multi-Resource Mode for CondensedView](#multi-resource-mode-example)
  - [Restrict BookingJS Access to Invited Customers](#restrict-bookingjs-access-to-invited-customers)
  - [Conditionally Hide BookingJS When No Bookables Are Availables](#conditionally-hide-bookingjs-when-no-bookables-are-available)
  - [Custom Field Example](#custom-field-example)

  <a id="features"></a>

## Features

- Real time availability and booking
- Booking requests for public appointments - approval required
- Multiple participant appointments
- Visitor legitimation - only invited visitors see the booking widget on public pages, listings, exposés
- Visitor identification - via customer ID from CRM
- Show/hide fully booked appointments option
- Prebooking period option - prevents booking if appointment is too soon from now
- Double booking prevention
- Cancelling appointment by customer with proper authentication
- Multiple instances on the same page - display multiple booking widgets without conflicts
- Multi-resource mode for CondensedView - display overlapping timeslots from multiple resources side-by-side with color-coded differentiation
- Fully customizable look and feel with mui theming (available with professional plans). This includes font customization; see Advanced Customisation → Font loading for details.
- Customizable wording across languages (availabe with professional plans)
- Dynamic definition of input fields with localized labels and validation (available with professional plans)
  - Therefore, ability to request additional information from customers during booking process
- Support for multiple languages (English, Italian, Spanish, French and German included) - add your own language
- Callbacks for custom code execution and event response (e.g. booking created, appointments loaded, booking canceled)
- Responsive layout handling for embedded views - EyeCandy and PureListView automatically adapt to available space and viewport constraints

<a id="how-to-integrate"></a>

## How to Integrate

<a id="in-a-script-tag"></a>

These are some minimal examples. BookingJs is highly customizable.
An overview over all configuration options can be found in [Advanced Customisation](#advanced-usage)

### In a Script Tag

Add the following code to your webpage:

```html
<div id="bookingjs" style="margin: 32px"></div>
<script type="module">
  import * as timum from 'https://cdn.timum.de/bookingjs/1/booking.js';

  timum.init({ ref: 'booking-widget-demo-resource@timum' });
</script>
```

A working fiddle can be found [here](https://jsfiddle.net/timum/3swq1tdy/).

Alternatively, you can add `ref` to your url as a parameter. This allows you to omit `ref` in the javascript portion. Like this:

`https://your.website.nic?ref=<enter resource reference here>`

```html
<div id="bookingjs" style="margin: 32px"></div>
<script type="module">
  import * as timum from 'https://cdn.timum.de/bookingjs/1/booking.js';

  timum.init(); //<- no need for the reference here
</script>
```

<a id="as-esm-import"></a>

### As ESM import

1. Add timum booking to your project with
   `yarn add @timum/booking`
   or use npm with
   `npm install @timum/booking`
   <br>
2. Add `<div id="bookingjs" style="margin: 32px"></div>` where you want timum to be displayed (the margin is just a suggestion, not mandatory).
   <br>
3. Add the following code to one of your .js files:

```javascript
import { init } from '@timum/booking';

init({ ref: 'booking-widget-demo-resource@timum' }); // <- the ref used here is just an example. Use a reference to one of your own resources.
```

A example project can be found [here](https://stackblitz.com/edit/react-8q6r8b?file=src/index.js)

### As React component import

1. Add timum booking to your project with
   `yarn add @timum/booking`
   or use npm with
   `npm install @timum/booking`
   <br>
2. Add the following code anywhere in your jsx:

```javascript
import { TimumBooking } from '@timum/booking';

// other code

<TimumBooking
  appConfig={{
    ref: 'booking-widget-demo-resource@timum',
    // other options
  }}
/>;

// other code
```

#### Multiple Instances

TimumBooking fully supports displaying multiple booking widgets on the same page. Each instance is automatically assigned a unique iframe ID and maintains its own isolated configuration, preventing any conflicts between instances. This is particularly useful when you need to display booking options for different resources or with different configurations on the same page.

```javascript
import { TimumBooking } from '@timum/booking';

// Display multiple booking widgets for different resources
<div>
  <h2>Resource A Bookings</h2>
  <TimumBooking
    appConfig={{
      ref: 'resource-a@timum',
      height: '500px',
    }}
  />

  <h2>Resource B Bookings</h2>
  <TimumBooking
    appConfig={{
      ref: 'resource-b@timum',
      height: '500px',
    }}
  />
</div>;
```

Each instance:

- Gets a unique iframe ID (e.g., `timum_booking_iframe_0`, `timum_booking_iframe_1`)
- Maintains its own configuration separately from other instances
- Can have different props, themes, and configurations
- Receives configuration updates and postMessages independently
- Is properly cleaned up when unmounted, preventing memory leaks

<a id="entity-referencing"></a>

### Entity Referencing

As shown in [How to Integrate](#how-to-integrate), there are two ways in which to provide bookingjs with the necessary references to retrieve public appointments from the backend.

Additionally, there are three different kinds of references you can provide, each having their own implications concerning the delivered appointments or whether the booking frontend is shown at all.

<a id="channel-reference"></a>

#### Channel Reference

Resource channels are entities in Timum, each storing a configuration that changes the booking experience for customers. Each individual resource has four channels that users can distribute to their customers through a link.
To use these channels in an embed scenario, provide a channel reference in the ref property (either in the URL or as part of the config). In this case, nothing else is required.

<a id="resource-reference"></a>

#### Resource Reference

A reference that refers to a plain resource, where the channel is unknown. The default channel (and its corresponding configuration) is used instead.

<a id="resource-reference-with-channelKey"></a>

#### Resource Reference with ChannelKey

If you don't have a channel reference but still want to use a different channel than the default one, you can provide a combination of resource reference and `channelKey` to uniquely identify the channel (and resource) to be used.

The following table gives an overview of the different permutations and how bookingjs behaves when encountering each of them:

| url RESref OR CHref | timum.init RESref OR CHref | timum.init chKey |          show OR hide          |
| :-----------------: | :------------------------: | :--------------: | :----------------------------: |
|          -          |             -              |     ignored      |            **HIDE**            |
|  ResourceReference  |          ignored           |       YES        | **show widget for channelKey** |
|  ResourceReference  |          ignored           |        -         |    **show default Channel**    |
|          -          |     ResourceReference      |        -         |    **show default Channel**    |
|          -          |     ResourceReference      |       YES        | **show widget for channelKey** |
|  ChannelReference   |          ignored           |     ignored      |        **show Channel**        |
|          -          |      ChannelReference      |     ignored      |        **show Channel**        |

- RESref: short for resource reference.
- CHref: short for channel reference.

<a id="example"></a>

##### Example

- First row: If neither a resource reference nor a channel reference is provided, not in the URL nor in the config object, any given channel key is ignored, and bookingjs is hidden.

- Third row: If a resource reference is given in the URL and in the config object, and no channelKey is provided, the ref in the URL takes priority. Since it is a resource reference and no channel key is provided, the default channel is used.

- Fifth row: If a resource reference and a `channelKey` are provided in the config object, and there is nothing in the URL, the config object is used. BookingJs gets displayed using the channel uniquely identified by the resource reference and the `channelKey`.

<a id="customer-pre-identification"></a>

#### Customer pre-identification

BookingJs can identify customers automatically if configured correctly.
This means they don't need to input their personal data prior to booking.

<a id="identification-via-pdataid"></a>

##### Identification via pDataId

Detection method: Is there a valid pDataId paired with a supported pDataPlatform (platform) in parent url matching an entity (contact, customer) in given CRM?
<em>
General behaviour: If there is, then visitor is pre identified. If not: provide standard booking dialog.
</em>
See [appConfig](#appconfig) for more information about pData.

<a id="identification-via-logged-in-user"></a>

##### Identification via Logged-in User

Even customers can register at timum and become registered and logged-in users. If the booking system identifies a logged-in user, the booking dialog does not provide personal data input fields but informs the visitor that they are identified.

> Note that if the customer is logged in to timum and there is a pData config/url param as well then the auth cookie takes priority.

---

<a id="advanced-usage"></a>

## Advanced usage

timum booking has a lot of customisation options.

The init accepts the following arguements in the order listed here.
The following table gives a rough overview over each.

| Arguments | Description                                                                                        |
| --------- | -------------------------------------------------------------------------------------------------- |
| appConfig | object, containing various options like callbacks, localiation, input fields etc.                  |
| muiTheme  | object, mui theme allowing you to change the look and feel of timum. Requires a professional plan. |
| fcConfig  | object, only used if you use full calendar as a booking frontend (settable in appConfig).          |

<a id="muitheme"></a>

### muiTheme

timum uses mui components for all frontends. The muiTheme object allows global design changes.
See [here](https://mui.com/) for a general overview. <br> See [here](https://mui.com/material-ui/customization/default-theme/) for theme related documentation.

When you open the file `config.js` in [this](https://stackblitz.com/edit/react-8q6r8b?file=src/index.js) example, you can find an elaborate custom configuration which includes a redesign of the standard timum theme.

Needs professional plan.

#### Fonts

You can set global or per-component fonts via the MUI theme (e.g., typography.fontFamily or component styleOverrides). BookingJS can also load the corresponding font files for you; see [Font Loading](#font-loading) for how to declare font resources in the theme and control the loading strategy. Requires a professional plan for custom theming.

<a id="fcconfig"></a>

### fcConfig

FullCalendar can be set as one of the frontends in appConfig.
See [here](https://fullcalendar.io/docs#toc) for a full list of configuration options.

> Note that since fullCalendar isn't mui based, it does not consider the muiTheme object.

There are also some options specific to bookingjs. In addition to the configuration options of fullcalendar the following options are also available (all of them are optional):

<table>
   <tr>
      <th>Property</th>
      <th>Description</th>
   </tr>
   <tr>
      <td>largeView</td>
      <td>string, Determines which <code>view</code> is displayed for screen widths <i>greater</i> 601px. <br>Please refer to the fullcalendar docs linked above for an overview of applicable values. </td>
   </tr>
   <tr>
      <td>smallView</td>
      <td>string, Determines which <code>view</code> is displayed for screen widths <i>smaller</i> 601px. <br> Please refer to the fullcalendar docs linked above for an overview of applicable values. 
   </td>
   </tr>
   <tr>
      <td>useCustomTimumCss</td>
      <td>boolean, to increase responsiveness, timum overrides some fullcalendar css classes. Set this to <code>false</code> to gain total control over fullcalendar's css.
   </td>
   </tr>
</table>

<a id="appconfig"></a>

### appConfig

This object's options directly affect timum's behaviour or allow you to react to it.

<table>
   <tr>
      <th>Property</th>
      <th>Description</th>
   </tr>
   <tr>
      <td>ref</td>
      <td>string/array, Reference of the resource to show the appointments of. <br><em> Either this or tslRefs (see below) must be defined.</em><br> Everything else is optional. <br>Can also be a url parameter.
      <br>You can also provide a list of string references. Booking.js will then allow your customer to choose for which resource appointments should be displayed.</td>
   </tr>
   <tr>
      <td>tslRefs</td>
        <td>string/array, Reference(s) of specific appointments. For when you only want to share a selection of a resource's appointments.<br><em>Either this or refs (see above) must be defined.</em><br> Everything else is optional. <br>Can also be a url parameter.
        <br>You can also provide a list of string references. Booking.js will then load all appointments and corresponding resource information. If they belong to different resources, then bookingjs allows your customer to choose for which resource appointments should be displayed.
        <blockquote>
          Note: If this and ref are defined in conjunction: 
          The following behaviour is not yet set in stone and may<b> change in the future. </b><br>
          First, bookingjs will load all appointments of the given resources. It then loads all the appointments defined in tslRefs. It then tries to match both results in the following way:<br><br>
          If a ref has none of it's appointments referenced in tslRef: <br>
          <ul>
            <li>allow the corresponding resource to be selected by the customer.</li> 
            <li>show all free and bookable appointments of that resource.</li>
          </ul> 
          <br>
          If a tslRef belongs to a resource also defined in ref:
          <ul>
            <li>allow the corresponding resource to be selected by the customer.</li> 
            <li>show only the appointments defined in tslRefs. Discard all other appointments which would have otherwise been bookable.</li>
          </ul> 
          <br>
          If a tslRef belongs to no resource defined in ref:
          <ul>
            <li>allow the corresponding resource to be selected by the customer.</li> 
            <li>show (and load) only the corresponding appointment.</li>
          </ul>
        </blockquote>   
      </td>
    </tr>
    <tr>
      <td>prdRefs</td>
      <td>string/array, Reference(s) of specific products. For when you only want your customer to choose from a subset of your otherwise fully available list of active products.
      <blockquote>
        Note: If this and <code>tslRefs</code> are defined in conjunction: 
        To facilitate a seamless booking process, all products linked to appointments or availabilities are made selectable for customers. This ensures that customers can always choose a product that grants access to the corresponding bookable timeslots referenced in <code>tslRefs</code>.
        Without this step, customers might be restricted by <code>prdRefs</code> from selecting certain products, rendering related appointments or availabilities unselectable. In this way, <code>tslRefs</code> has priority over this field.
      </blockquote>  
    </td>
   </tr>
       <tr>
      <td>allResourcesOption</td>
      <td>boolean, This option determines whether the resource selection screen presents an "All" option for customers. When this flag is true, customers can choose to see and book from all available appointments across different resources in a single interface. <br><br>
      If the customer selects the "All" option, the system displays all bookable appointments for each resource that match their selected time and product.<br><br>
      To ensure good performance, the system will filter out duplicate appointments. For instance, if there are multiple appointments that start at the same time and offer the same service, only one of these will be shown, until booked at which point the next appointment in line will be loaded and displayed.
      <br><br>
      This feature is particularly useful when it's more important for customers to secure an appointment rather than choosing a specific resource. It allows for greater flexibility and ease in booking, especially during peak times or when specific resources may be limited.
    </td>
   </tr>
   <tr>
      <td>platform</td>
      <td>string, A fully qualified reference (<code>id@someUuid@platform</code>) can be unwieldy especially if used as a url param. This prop allows you to hardcode the <code>platform</code> part.
      <br><br>Note that this prop is ignored if <code>ref</code> is both:
      <ul>
        <li>used as a url param </li>
        <li>contains an @.</li>
      </ul>  
   </td>
   </tr>
   <tr>
      <td>prvUuid</td>
      <td>string, A fully qualified reference (<code>id@someUuid@platform</code>) can be unwieldy especially if used as a url param. This prop allows you to hardcode the <code>someUuid</code> part. This part is only sometimes necessary to uniquely identify a resource in timum. <br><br>Note that this prop is ignored if <code>ref</code> is both:
      <ul>
        <li>used as a url param </li>
        <li>contains an @.</li>
      </ul>  
   </td>
   </tr>
   <tr>
      <td>height</td>
      <td>string, Height of timum on your page. `500px` is standard.</td>
   </tr>
   <tr>
      <td>host</td>
      <td>string, to which server requests are send. Possible values are <code>https://www.timum.de</code> (production server) or <code>https://staging.timum.de (test server)</code></td>
   </tr>
   <tr>
      <td>allowCloseOnBooking</td>
      <td>boolean, whether the confirmation view is closeable once the booking was successfull.</td>
   </tr>
   <tr>
      <td>allowCloseOnCancel</td>
      <td>boolean, whether the cancel view is closeable once it has been opened. If false, it only closes after previous appointment was cancelled.</td>
   </tr>
   <tr>
      <td>constrainDialogsToContainer</td>
      <td>boolean, controls where dialogs from BookingJs are rendered. When enabled, all dialogs are confined to the BookingJs' designated container and are clipped/positioned within its bounds, behaving as part of it. 
      When disabled, dialogs are rendered at the page level and overlay the entire site, behaving like native site modals. 
      <blockquote>
      Note: this setting is ignored for small screens
      </blockquote>
      </td>
   </tr>
    <tr>
      <td>hideTimumFooter</td>
      <td>boolean, whether 'powered by timum' can be hidden or not. Needs professional plan.</td>
   </tr>
   <tr>
      <td>hiddenForAnonymous</td>
      <td>boolean, whether bookingjs should hide itself when no customer identifying pData props can be read from the config or the url. Useful for when you only want explicitly invited customers to see the booking frontend.</td>
   </tr>
    <tr>
      <td>sendCustomValuesInMessage</td>
      <td>boolean, whether values of custom fields are concatenated and comma-separated into a single string which is then sent to the backend as <code>message</code>. If field <code>message</code> is defined its input is concatenated to the generated string as well.</td>
   </tr>
   <tr>
      <td>channelKey</td>
      <td>string, timum has, as of now, 4 different channels through which you can share your resource's appointments. <br> You can find them in the timum frontend under &lt;resource name&gt; -&gt; Calendar Sharing (Terminbuchung freigeben). <br> Every channel has its own settings allowing you to control whom of your customers can see certain appointments, whether they book directly or create a request first and other settings. <br> Valid values for this attribute are: <br> 
         - RESOURCE_PUBLIC <br> referring to "Public Booking Link" (Öffentlicher Buchungs-Link) <br> 
         - RESOURCE_EXCLUSIVE <br> referring to "Exclusive Booking Access" (Exklusiver Buchungs-Zugang) <br> 
         - RESOURCE_REFERENCE <br> referring to "Embedded booking calendars" (Eingebettete Buchungskalender) <br>
         - CALENDAR_PUBLIC <br> referring to "In all Website Plugin Views and your General Calendar" (In Website-Plugin Ansichten sowie Ihrem Gesamtkalender) <br>RESOURCE_PUBLIC is the default, used if you specify nothing else. <br><br>Can also be a url parameter. 
      </td>
   </tr>
   <tr>
      <td>calendarFrontend</td>
      <td>string, one of 
      <br>
      <code>fullCalendar</code>, <code>detailsFullCalendar</code> (these are what `init`'s 3rd parameter is for),
      <br>
      <code>pureListView</code>, <code>detailsListView</code>,
      <br>
      <code>monthView</code>, 
      <code>detailsMonthView</code>,
      <br>
      <code>condensedView</code>, 
      <code>detailsCondensedView</code>
      <br>
      <br>
       <blockquote>
      Note: the <code>details</code> variants show details about the resource, like the description and the external url, as well as public information about the assigned consultant, if the channel allows that.
       </blockquote>
      </td>
   </tr>
   <tr>
      <td>calendarFrontendOptions</td>
      <td>object, Contains view-specific configuration options. Currently supports:
      <br><br>
      <strong>condensedView</strong> (object):
      <ul>
        <li><code>forceResourceSelectorDialog</code> (boolean, default: false) - If true, the resource selection is always rendered as a dialog rather than a sidebar.</li>
        <li><code>multiResourceMode</code> (boolean, default: false) - When enabled with multiple resources, renders bookables from all resources side-by-side when they overlap in time. Each resource receives a distinct pastel color for easy differentiation, and a color legend is displayed above the calendar. The resource selector is suppressed in this mode. Gracefully degrades to standard mode with single resource or no overlaps.</li>
      </ul>
      </td>
   </tr>
   <tr>
      <td>culture</td>
      <td>string, The localisation to use. If not specified the browser language is used. Can also be a url parameter.</td>
   </tr>
   <tr>
      <td>pData</td>
      <td>object, <br> Data indentifying the customer, so that they don't have to input their data again. <br> Works in conjunction with timum and select, supported plaforms like OnOffice, Immosolve etc. <br> Properties: <br> 
         <code>{ platform: string, personId: string }</code> 
         <br> Can also be a url parameter. -&gt; the params are named differently though: pDataPlatform, pDataId and pDataIdPropName which hold the name of the platform (e.g 'onoffice') the id value (e.g 27) and the name of the id property within the chosen platform: (e.g personId)
      <blockquote>
      Note: pData is ignored in favor of a timum auth cookie.
      Use incognito mode, another browser or sign out to circumvent this.
       </blockquote>   
      </td>
   </tr>
   <tr>
      <td>callbacks</td>
      <td>object, may contain various functions which allow to to run custom code in reaction to certain events. See below for a guide on how to do this. </td>
   </tr>
 <tr>
   <td>postMessageTarget</td>
   <td>
       string, URL of the parent site that receives callback events via the postMessage API. This property enables all callback events to be sent using the postMessage() method, which is particularly useful when using bookingjs within an <code>iframe</code>, as it restricts access to the parent site's JavaScript code.
       <br>
       The <code>event.data</code> object includes all parameters typically passed to callback functions, along with an <code>origin</code> field (always "bookingjs") and a <code>type</code> field that indicates the event's name. This <code>type</code> matches the name of the callback function you would use in a non-iframe scenario. Refer to the guide on callback functions for specifics on available event names.<br>
      <blockquote>
         <strong>Note:</strong> Setting this property overrides the <code>callback</code> property; all standard callbacks will instead trigger postMessage() events.
      </blockquote>
   </td>
</tr>
   
   <tr>
      <td>fields</td>
      <td>object, You can customise what information is demanded of your customers prior to booking. timum requires certain fields to work and has some optional fields it can parse. Fields other than those supported by timum can be evaluated in a callback (see the callback guide below for further info). All fields (yours too!) support localization and input validation. 
      <br>
      Needs professional plan. 
      </td>
   </tr>
   <tr>
      <td>localization</td>
      <td>object. Contains all localization variables and their standard texts. timum nativly supports English, German, French, Spanish and Italian. Use this to override the standard text and/or add translations for e.g your custom field labels and input validations (see the localisation guide for more info.)
      <br>
      Needs professional plan. 
      </td>
   </tr>
</table>

<a id="font-loading"></a>

#### Font Loading

BookingJS supports two font-loading strategies, so you can either reuse an existing remote CSS (e.g., Google Fonts) or reference font files directly (no extra CSS round-trip). The widget detects your font declarations inside the MUI theme and loads fonts accordingly. This complements theming (muiTheme) and lets you apply custom fonts across the UI. Requires a professional plan for custom theming. See the theming entry points described under [Advanced usage](#advanced-usage) (init accepts appConfig, muiTheme, fcConfig) and the integration methods in [How to Integrate](#how-to-integrate) (Script Tag / ESM / React) .

- Where to declare fonts in the theme

  - Inline faces: add fontFace (single) or fontFaces (array) anywhere in your theme object (e.g., top-level, typography, or component defaultProps/styleOverrides).
  - Link CSS: add fontSource (string or string[]) anywhere in your theme. The widget will inject <link rel="stylesheet"> for each unique URL (loading duplicates is prevented).

- Applying the font
  - Set typography.fontFamily to your desired stack, or override per component (muiTheme → components.\*.styleOverrides.root.fontFamily). BookingJS will load the faces you declared and use them if referenced in the theme (subject to your professional plan).

##### Examples

1. Inline variable font (preferred for performance)

```js
// passed as the "muiTheme" argument to init
<div id="bookingjs" style="margin: 32px"></div>
<script type="module">
  import * as timum from 'https://cdn.timum.de/bookingjs/1/booking.js';

  timum.init(
    { ref: 'booking-widget-demo-resource@timum' }, // appConfig
    {
      // other theme props...

      typography: {
        fontFamily:
          '"My Font", Roboto, Helvetica, Arial',
        // BookingJS will load these files directly via @font-face rules
        fontFaces: [
          {
            family: 'My Font',
            style: 'normal',
            weight: '100 900',   // variable axis range or single value like '400'
            display: 'swap',
            src: [
              { local: 'Inter' },
              { url: '/fonts/inter-var.woff2', format: 'woff2' },
            ],
          },
          {
            family: 'My Font',
            style: 'italic',
            weight: '100 900',
            display: 'swap',
            src: [
              { local: 'Inter Italic' },
              { url: '/fonts/inter-italic-var.woff2', format: 'woff2' },
            ],
          },
        ],
      },

      // other theme props...
    }
  );
</script>
```

2. Link-based CSS (reuse existing CSS bundles)

```js
// passed as the "muiTheme" argument to init
<div id="bookingjs" style="margin: 32px"></div>
<script type="module">
  import * as timum from 'https://cdn.timum.de/bookingjs/1/booking.js';

  timum.init(
    { ref: 'booking-widget-demo-resource@timum' }, // appConfig
    {
      // other theme props...

      typography: {
        fontFamily:
          '"Roboto", "Helvetica", "Arial", sans-serif',
        // BookingJS will inject <link rel="stylesheet"> for these URLs
        // Note: this is just an example. We host our fonts on our own servers
        fontSource: [
            'https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap',
        ],
      },

      // other theme props...
    }
  );
</script>
```

Notes and best practices

- Use WOFF2 where possible (smaller, widely supported).
- Prefer variable fonts (weight: "100 900") instead of many static weights.
- Use display: 'swap' to avoid FOIT.
- If fonts are hosted on a different domain, ensure proper CORS headers so the browser can load them.
- You can still use per-component overrides to mix families (e.g., Sora for Buttons only) by setting components.MuiButton.styleOverrides.root.fontFamily; just ensure the face is declared under fontFaces or provided via fontSource as shown above.

<a id="how-to-use-callbacks"></a>

#### How to use callbacks

The callbacks object may contain any of the following functions.

<a id="booking-related"></a>

##### Booking related

- `openedBookingPage`
- `closedBookingPage`,
- `createBookingStarted`
- `createBookingSuccessful`
- `createBookingFailed`

All Booking related callbacks receive a <em>single obejct</em> as argument containing the following properties:

- a `timeslot` looking like this:
  ```
     {
        start: luxon Datetime object. Internal state. "2023-01-27T09:05:00.000Z",
        end: luxon Datetime object. Internal state. "2023-01-27T09:35:00.000Z",
        timeslot_uuid: uuid of the booked timeslot. e.g. "82ec5220-9d55-11ed-8617-e4a7a0ca32e8",
        product_uuid: uuid of the booked product e.g. "92867f70-4836-11e5-bc04-021a52c25043",
        product_name: string. e.g. "Meeting",
        resource_name: string. e.g. "Booking Widget DEMO",
        capacity: number e.g. 1,
        capacity_left: number e.g. 1,
        kind: either "models.Bookable" or "models.LotAppointment",
        untouchedStart: ISO String e.g: "2023-01-27T09:05:00+01:00" as the server sent it,
        untouchedEnd: ISO String e.g: "2023-01-27T09:35:00+01:00" as the server sent it
        }
  ```
- a `data` object looking like this:

```javascript
 {
   firstName: 'string',
   lastName: 'string',
   email: 'string',
   agbs: 'bool'

   // plus whatever optional and/or custom fields you yourself have defined.
   // -> so the makeup of this object changes depending on your settings.
 }

```

Additionally, `createBookingFailed` and `createBookingSuccessful` also have a RTKQ `response` object in their respective argument. See [here](https://redux-toolkit.js.org/rtk-query/usage-with-typescript#type-safe-error-handling) for a reference.

<a id="cancelation-related"></a>

##### Cancelation Related

- `openedCancelPage`
- `closedCancelPage`
- `cancelationStarted`
- `cancelationSuccessful`
- `cancelationFailed`

All cancelation related callbacks receive a single obejct as argument containing the same properties as described in [Booking related](#booking-related)

Additionally, `cancelationSuccessful` and `cancelationFailed` also have a RTKQ response object in their respecive argument. See here for a reference.

<a id="dialog_related"></a>

##### Dialog related

In addition to the dialog related callbacks already mentioned in [Cancelation Related](#cancelation-related) and [Booking related](#booking-related), there are callbacks for all other dialogs as well.

- `openedProductSelection`
- `openedResourceSelection`
- `openedConfirmationPage`
- `closedProductSelection`
- `closedResourceSelection`
- `closedConfirmationPage`

These do not receive any parameters.

<a id="data-fetching-related"></a>

##### Data Fetching Related

In order to display public appointments, resource and calendar information timum sends several requests.

- `fetchingPublicDataSucceeded`
  Gets passed in a single object looking like this:

  ```javascript
    {
      contact: {
        name: 'string',
        email: 'string',
        mobile: 'string',
        phone: 'string',
      },
      resource: {
        uuid: 'string',
        name: 'string', //<- 80 chars, max length of names in timum
        description: 'string',
        msgHelpText: 'string',
        type: 'string',
        url: 'string',
        imgUrl: 'string',
      },
      provider: {
        name: 'string',
        description: 'string',
        isThemingAllowed: 'boolean',
        isLocalisationAllowed: 'boolean',
        areCustomFieldsAllowed: 'boolean'
      },
      channel: {
        bookingProcess: 'string' // One of 'IMMEDIATE' or 'REQUESTED',
      },
    }
  ```

- `fetchingPublicDataFailed`
  receives a single RTKQ error object. See [here](https://redux-toolkit.js.org/rtk-query/usage-with-typescript#type-safe-error-handling) for a reference.
  <br>
- `fetchingProductsSucceeded`
  Gets passed in a single object looking like this:
  ```javascript
  {
    products: [
      {
        description: string,
        minDuration: number,
        name: string,
        uuid,
      },
      //...
    ];
  }
  ```
- `fetchingProductsFailed`
  receives a single RTKQ error object. See [here](https://redux-toolkit.js.org/rtk-query/usage-with-typescript#type-safe-error-handling) for a reference.
  <br>
- `fetchingBookablesSucceeded`
  Gets passed in a single object looking like this:
  ```javascript
    {
      2023-02-06: [ //<- this obviously changes depending on when appointments are available.
        0: {
            timeslot_uuid: uuid of the booked timeslot,
            product_uuid: uuid of the booked product,
            product_name: string,
            resource_name: string,
            capacity: number,
            capacity_left: number,
            kind: one of "models.Bookable" or "models.LotAppointment",
            start: ISO String e.g: "2023-02-06T09:05:00+01:00",
            end: ISO String e.g: "2023-02-06T09:35:00+01:00"
        },
        //... more appointments ...
      ],
      //... more dates ...
    }
  ```
  - `fetchingBookablesFailed`
    receives a single RTKQ error object. See [here](https://redux-toolkit.js.org/rtk-query/usage-with-typescript#type-safe-error-handling) for a reference.
    <br>

<a id="localisation"></a>

#### Localisation

timum booking has the ability to change all of its text. It comes with English, Italian, Spanish, French and German pre-installed, but you can add additional translations as well. You can also change the text for these languages to your preference. You don't have to redefine the entire localization object; you can just add or change the specific texts you want. Any missing texts will be taken from the default.
The code snippet provided shows the complete localization object and the standard text for both English and German. For each language, it includes text for different parts of the booking process such as product selection, appointment booking, appointment request, appointment cancellation, form fields, and more.

```javascript
localization: {
  de: {
    resource_selection_headline: 'Resource wählen',
    product_selection_headline: 'Terminart wählen',
    booked_successfully_header: 'Termin gebucht',
    booked_successfully_message:
      'Sie erhalten eine E-Mail mit den Termindetails an {{mail}}',
    requested_successfully_header: 'Termin angefragt',
    requested_successfully_message:
      'Sie erhalten eine E-Mail mit den Termindetails an {{mail}}. Sie werden unter der gleichen Adresse benachrichtigt, sobald Ihre Anfrage bearbeitet wurde.',
    submit_button_book: 'Buchen',
    submit_button_request: 'Verbindlich Anfragen',

    noEventsMessage:
      'Zur Zeit sind leider keine buchbaren Termine verfügbar.',
    appoinment_at_capacity: 'belegt',
    add_to_calendar_btn: 'Zu Kalender hinzufügen',
    until_reservation_expiration:
      '{{expiration}} bis zum Ablauf der Reservierung',
    reservation_expired: 'Reservierung abgelaufen.',
    identified_customer_hint:
      'Sie wurden mit persönlichem Link eingeladen und können direkt Ihren Termin buchen.',
    reservation_failed: {
      title: 'Hohe Nachfrage',
      mesage:
        'Dieser Termin wurde gerade durch jemanden reserviert. Bitte wählen' +
        'Sie einen anderen Termin. Oder schauen Sie später noch einmal, ob' +
        'ein Termin frei wird.',
    },
    cancellation: {
      cancelation_successfull_message: 'Termin erfolgreich abgesagt',
      cancellable_appointment_highlight: 'Mein Termin',
      submit_button_cancel: 'Absagen',
      cancel_appointment_header: 'Ihr Termin',
      message_label: 'Nachricht zur Terminabsage',
    },
    validation: {
      field_required: 'Notwendig',
      privacy_field_required:
        'Sie müssen die Datenschutzbestimmungen akzeptieren bevor Sie buchen können.',
      email_field_must_be_valid: 'Geben Sie eine valide E-Mail Adresse ein',
    },
    fields: { // field labels are markdown capable. That's why you can use basic html as well.
      firstName: 'Vorname',
      lastName: 'Nachname',
      name: 'Name',
      email: 'E-Mail',
      mobile: 'Mobil',
      message: 'Ihre Nachricht',
      accept_timum_privacy:
            '<a href="https://info.timum.de/datenschutz" target="_blank">Datenschutzbestimmungen</a> gelesen und akzeptiert',

    },
  },
  // the same for english.
  en: {
    resource_selection_headline: 'Choose Resource',
    product_selection_headline: 'Choose Product',
    booked_successfully_header: 'Appoinment Booked',
    booked_successfully_message:
      'You will receive an email with appointment details to {{mail}}',
    requested_successfully_header: 'Appointment Requested',
    requested_successfully_message:
      'You will receive an email with appointment details to {{mail}}. You will be notified at the same address once your request has been processed.',
    submit_button_book: 'Book',
    submit_button_request: 'Request',

    noEventsMessage:
      'Unfortunately, there are no bookable dates available at the moment.',
    appoinment_at_capacity: 'fully booked',
    add_to_calendar_btn: 'Add to Calendar',
    until_reservation_expiration:
      '{{expiration}} until reservation expiration.',
    reservation_expired: 'Reservation expired.',
    identified_customer_hint:
      'You have been invited with a personal link and can book your appointment directly.',
    reservation_failed: {
      title: 'High Demand',
      mesage:
        'This appointment has just been reserved by someone. Please choose another appointment. Or check back later to see if an appointment becomes available.',
    },
    cancellation: {
      cancelation_successfull_message: 'Appointment canceled sucessfully.',
      cancellable_appointment_highlight: 'My Appointment',
      submit_button_cancel: 'Cancel',
      cancel_appointment_header: 'Your Appointment',
      message_label: 'You may enter a reason here.',
    },
    validation: {
      field_required: 'Required',
      privacy_field_required:
        'You must accept the privacy policy prior to booking.',
      email_field_must_be_valid: 'Enter a valid email address',
    },
    fields: { // field labels are markdown capable. That's why you can use basic html as well.
      firstName: 'First name',
      lastName: 'Last name',
      name: 'name',
      email: 'E-mail',
      mobile: 'Mobile',
      message: 'Your Message',
      accept_timum_privacy:
            '<a href="https://info.timum.de/datenschutz" target="_blank">Privacy policy</a> read and accepted.',

    },
  },
},
```

<a id="booking-form-fields"></a>

#### Booking Form Fields

You can customize the information you request from customers before booking by defining fields. The minimum required fields for timum are <code>firstName</code>, <code>lastName</code>, <code>email</code>, and <code>agbs</code>. By default, timum also requests <code>mobile</code> and <code>message</code> fields, but you can add your own fields as well.

Fields, including custom ones, support validation and localization.
Validation is realized using the yup library, which is included with timum. You can import it using the following code:

```javascript
import { yup } from '@timum/booking';
```

Localization is achieved by using the title property in the field definition. You can specify a translation key and add the corresponding translation to the localization object. The same process can be applied to custom field validations.

The standard configuration can be overridden except for the aforementioned, required fields. The following is the standard configuration:

```javascript
fields: {
  firstName: {
    index: 0,
    title: 'fields.firstName',
    validation: yup.string().required('validation.field_required'), // <- compare with key in localisation
  },
  lastName: {
    index: 1,
    title: 'fields.lastName',
    validation: yup.string().required('validation.field_required'),
  },
  email: {
    index: 2,
    title: 'fields.email',
    format: 'email',
    type: 'text',
    validation: yup
      .string()
      .email('validation.email_field_must_be_valid')
      .required('validation.field_required'),
  },
  mobile: {
    index: 3,
    title: 'fields.mobile',
    type: 'phoneNumber',
    isRequired: false,
    defaultCountry: 'DE',
    preferredCountries: ['DE', 'CH', 'AT'],
    // validation: is in built and ignored
  },
  message: {
    index: 7,
    title: 'fields.message',
    type: 'textarea',
    validation: yup
      .string()
      .max(600),
    limit: 600,
  },
  agbs: {
    index: 8,
    title: 'fields.accept_timum_privacy',
    type: 'checkbox',
    validation: yup
      .boolean()
      .required('validation.field_required')
      .test(
        'privacyAccepted',
        'validation.privacy_field_required',
        (value) => value === true,
      ),
  },
  locale: {
    index: 9,
    preventRendering: true,
  },
},
```

<a id="custom-fields"></a>

##### Custom Fields

If the values retrieved via custom fields need to be transferred to timum backend as additional booking information, set the parameter `sendCustomValuesInMessage`. With this, the values of custom fields are attached to the customer message. They will be visible to the customer as well in booking confirmation mailings.

`sendCustomValuesInMessage: true, // necessary to transmit custom field values as part of the customer message`

Anatomy of a custom booking form field:

`<fieldName> : {`

  <blockquote>
      <code>index</code>: number, index defines the order in which the fields get displayed.
  (If you want to reorder the default fields or
  want to inject a custom field between them,
  you must redefine them as custom fields and manually change their indexes.)
  </blockquote>
  <br>
    <blockquote>
      <code>title</code>: string or JSXElement;
    </blockquote>
  <br>  
    <blockquote>
      <code>validation</code>: yup based validation. See <a href="https://github.com/jquense/yup">yup docu</a>. Re-exported and accessible via timum.yup
    </blockquote>
  <br>
    <blockquote>
    <code>type</code>: string; either 'text' (default), 'phoneNumber', 'textarea', 'checkbox' or 'select'. Depending on the chosen type additional properties are available. See below. 
  </blockquote>
  <br>
    <blockquote>
    <code>onChange</code>: function, allows to execute code whenever the user changes the fields value. Gets 'value' as function parameter.
  </blockquote>
  <br>
    <blockquote>
    <code>prefilled</code>: string; value a field is initiated with.
                            Note that for fields of type 'select' you must use the 'key' of one of it's options.
  </blockquote>
  <br>
  <blockquote>
    <code>preventRendering</code>: boolean, default false;
    if true the field is not visible to the user.
    Useful in conjunction with sendCustomValuesInMessage allowing you to
    enrich booked appointments with internal data
    without the customer becoming aware of your internal processes.
  </blockquote>
  <br>
  <blockquote>
    <code>preventRenderingFor</code>: array, prevents rendering of the field on the set booking screens. Valid values are
    'identifiedCustomers' or 'unknownCustomers', which prevents rendering on the booking
    screen shown to identified/unidentified customers, respectively. If both
    'identifiedCustomers' and 'unknownCustomers' are set the behaviour is equal to
    preventRendering.
    Note that fields hidden in this manner get their validation disabled. This
    ensures that the booking doesn't fail with a validation error the end user is unable to fix.
  </blockquote>
`}`

Depending on the type there are additional properties you can/must specify:

- Type `text`:

  - `format`: string; the native input field's 'type' (e.g. 'email', 'number', etc.).

- Type `phoneNumber`:

  - does NOT support `validation`. Phone number validation is complex, so timum handles it for you.
    This does mean that field validation localisation is currently not supported for fields of this type.
    This will be fixed in a future update.
  - <code>isRequired</code>: boolean; if true, this field must be filled with a valid number
  - <code>defaultCountry</code>: string, denotes the country code which is preselected when first rendering the component. Default is 'DE',
  - <code>preferredCountries</code>: list of strings; denotes which countries are displayed first in the country drop down. Defaults are ['DE', 'CH', 'AT'],

- Type `textArea`:

  - <code>limit</code>: number; sets the maximum number of characters customers can enter. Standard is 600 characters. 1024 is maximum. But if fully used by the customer, this may lead to error in combination with sendCustomValuesInMessage. Sending any more characters to the backend leads to error.

- Type `checkbox`:

  - no special properties.

- Type `select`:

  - options: array of objects with structure { title: string, key: string }. The title is displayed and the key is passed in the data object to callbacks.

<a id="global-config-override"></a>

### Global config via `window.timumBookingConfig`

On top of the configs you pass directly to `init()` / `initialise()`, BookingJS also reads an optional global object from the host page: `window.timumBookingConfig`. This lets a page operator override BookingJS configuration without having to edit the script that initialises the widget — useful for environments where the embed code is generated by a CMS plugin or served by a backend snippet that you can't easily modify, but where you still control the surrounding HTML.

#### Cascade order

Configs are resolved in this order — later sources override earlier ones for the same key:

1. Built-in defaults from BookingJS
2. `appConfig` / `muiTheme` / `fcConfig` passed in to `init()` / `initialise()` directly
3. `window.timumBookingConfig.appConfig` / `.muiTheme` / `.fcConfig` — applied to **every** instance on the page
4. `window.timumBookingConfig[<rootElId>].appConfig` / `.muiTheme` / `.fcConfig` — applied only to the instance whose `appConfig.rootElId` matches the key
5. URL parameters (highest precedence)

Deep merging is used at every step, so you can override individual nested keys without replacing the whole object.

#### Shape

```js
window.timumBookingConfig = {
  // Applied to every BookingJS instance on the page
  appConfig: { culture: 'de' },
  muiTheme:  { palette: { primary: { main: '#abc' } } },
  fcConfig:  { /* … */ },

  // Per-instance overrides: the key must match appConfig.rootElId
  // of the instance you want to target
  'bookingjs-dd8d32cc-b53b-4ab4-af52-41325000592b': {
    appConfig: { ref: 'special-ref' },
  },
  'bookingjs-other-uuid': {
    muiTheme: { palette: { primary: { main: '#def' } } },
  },
};
```

All four slots are optional. You can set only top-level slots, only per-instance entries, or any mix of both. The global object must be set **before** BookingJS initialises — setting it after `init()` runs will not retroactively re-merge config.

#### Per-instance keying via `rootElId`

When multiple BookingJS instances live on the same page and you want to give them different overrides, set `appConfig.rootElId` to a stable identifier (typically the ID of the DOM element that hosts the widget) and use that same string as the key in `window.timumBookingConfig`.

If you omit `rootElId`, only the top-level slots apply — per-instance lookup is skipped entirely.

#### Scoping config to provider channel snippets

Each embed snippet you create in the Timum interface (found in "Appointment booking" ("Terminbuchung") in the top bar) represents a **provider channel** identified by a UUID. The generated snippet already stamps `rootElId: "bookingjs-<provider-channel-uuid>"` into its `init()` call. You can use that same key in `window.timumBookingConfig` to scope configuration to a specific snippet:

```js
window.timumBookingConfig = {
  // Shared across all snippets on the page
  appConfig: { culture: 'de' },

  // Only affects the snippet with this provider channel UUID
  'bookingjs-dd8d32cc-b53b-4ab4-af52-41325000592b': {
    appConfig: { height: 700 },
    muiTheme: { palette: { primary: { main: '#ff5722' } } },
  },

  // A different snippet on the same page gets its own config
  'bookingjs-a1b2c3d4-e5f6-7890-abcd-ef1234567890': {
    appConfig: { height: 500 },
  },
};
```

This is useful when you embed multiple booking widgets on the same page and want each one to have different styling or behavior, without having to configure all of that through the Timum interface. You find the provider channel UUID in the embed code that the Custom URL entry generates for you.

The configs get merged. Meaning the specific snippet configuration gets properties it doesn't override itself from the general top level config. Any properties not defined in there come either from the config of the snippet, provider or account wide configs or, if nothing else can be found, the timum default values. 
This is a cascade from most specific to least specific.   

#### Reserved names

The strings `'appConfig'`, `'muiTheme'`, and `'fcConfig'` are reserved as top-level slot names. If you accidentally set `appConfig.rootElId` to one of these values, BookingJS will log a `console.warn` and skip the per-instance lookup for that instance (only the top-level slots will apply). Pick a less generic identifier for your widgets.

#### The React component bypasses this layer

The `<TimumBooking>` React component intentionally **ignores** `window.timumBookingConfig`. React consumers pass `appConfig` / `muiTheme` / `fcConfig` directly as props per call and are expected to use React idioms (context, providers) for sharing config across components. The global override layer is therefore a script-tag / snippet feature only.

<a id="examples"></a>

# Examples

In this section you find examples of common patterns.

## Customizing Fonts in timum's BookingJS

This guide demonstrates how to **override the default font** for the entire BookingJS widget and how to **set specific fonts for individual UI components**.

The example below shows how to set a new default font (`Ysabeau Office`) and then apply different fonts (`Tangerine`) to `MuiButton` and `MuiLink` components.

[**View Live Example on jsFiddle**](https://jsfiddle.net/timum/pdmnyr0L)

```html
<div id="bookingjs" style="margin: 32px"></div>
<script type="module">
  import * as timum from 'https://cdn.timum.de/bookingjs/1/booking.js';

  // --- 1. Initialize BookingJS with Resource Reference and Configuration ---
  timum.init(
    {
      // Your resource reference
      ref: 'booking-widget-demo-resource@timum',
    },
    {
      // --- Typography Configuration ---
      typography: {
        // **Default font** for the entire widget: A comma-separated list of font families.
        fontFamily: '"Ysabeau Office", Roboto, Helvetica, Arial',

        // **Font sources** to load via <link rel="stylesheet">.
        fontSource: [
          'https://fonts.googleapis.com/css2?family=Tangerine:wght@400;700&family=Ysabeau+Office:ital,wght@0,1..1000;1,1..1000&display=swap',
        ],
      },

      // --- Component-Specific Font Overrides ---
      // This section uses the underlying MUI (Material-UI) component names.
      components: {
        // Override the font for all MUI Buttons
        MuiButton: {
          styleOverrides: {
            root: {
              fontFamily: '"Tangerine", Roboto, Helvetica, Arial',
            },
          },
        },
        // Override the font for all MUI Links
        MuiLink: {
          styleOverrides: {
            root: {
              fontFamily: '"Tangerine", Roboto, Helvetica, Arial',
            },
          },
        },
      },
    },
  );
</script>
```

<a id="restrict-bookingjs-access-to-invited-customers"></a>

## Restrict BookingJS Access to Invited Customers

You can configure BookingJS to only display the booking interface for customers who have been **explicitly invited** via a unique link. This provides a layer of access control, ensuring that only qualified individuals (e.g., registered clients, specific leads) can proceed with a booking.

### How it Works

This approach relies on **URL query parameters** to authenticate the user and passes these parameters into the `timum.init` configuration.

1.  **Invitation Link:** You generate unique invitation links containing a `ref` (resource identifier) and an `invitee` (customer identifier).
    - **Example Link:**
      `https://www.your-site.com/immo?ref=[uuid]&invitee=[addressId]`
2.  **Validation:** The embedded JavaScript checks for the presence of these parameters. If they are missing, the widget is not initialized.
3.  **Authentication:** The `invitee` ID is passed to BookingJS via the `pData` object, allowing the backend to verify the customer against your platform data (e.g., onOffice, FlowFact, Immosolve).

### Implementation

The following code demonstrates how to extract the necessary parameters from the URL and pass them to `timum.init` only when both are present.

```html
<div id="bookingjs" style="margin: 32px"></div>
<script type="module">
  import * as timum from 'https://cdn.timum.de/bookingjs/1/booking.js';

  // Function to hide the element
  const hideBookingJs = () => {
    const el = document.getElementById('bookingjs');
    if (!el) return;

    // Hide the element if initialized but fails to find bookables
    el.style.display = 'none';
  };

  // --- 1. Extract Parameters from the URL ---
  const url = new URL(window.location.href);
  const ref = url.searchParams.get('ref');
  const inviteeId = url.searchParams.get('invitee');

  // Stop execution if required invitation parameters are missing
  if (!ref || !inviteeId) {
    // Optionally, hide the empty container immediately if you want to be certain
    document.getElementById('bookingjs')?.style.display = 'none';
    return;
  }

  // --- 2. Initialize BookingJS with Customer Data ---
  timum.init({
    // Use the dynamic resource 'ref' from the URL
    ref: ref,

    // Pass the customer's identifier via pData for verification
    pData: {
      // Use the platform you are integrating with (e.g., 'onoffice')
      platform: 'onoffice',

      // 'personId' is the required key for onOffice/addressId matching
      personId: inviteeId,
    },

    // --- 3. Optional Callbacks ---
    callback: {
      // If data fetching succeeds but returns no available slots for the user
      fetchingBookablesSucceeded: (bookables) => {
        const hasBookables = bookables && Object.values(bookables).length > 0;
        if (!hasBookables) {
          hideBookingJs();
        }
      },

      // If data fetching fails (e.g., server error)
      fetchingBookablesFailed: () => {
        hideBookingJs();
      },
    },
  });
</script>
```

### Note on `pData`

The key used within the `pData` object (e.g., `personId`) must match the expected parameter name for your connected platform (onOffice, FlowFact, etc.) to correctly authenticate the user. Consult the platform-specific integration guide for the correct parameter name. (See also: [pData Configuration](#appconfig)

<a id="conditionally-hide-bookingjs-when-no-bookables-are-available"></a>

## Conditionally Hide BookingJS When No Bookables Are Available

When the BookingJS widget loads, you may wish to automatically hide the widget container if no bookable resources are found or if the data retrieval fails.

You might want to hide the widget to:

- Improve UX: Avoid showing customers an empty element or informing them about a service they cannot currently book.
- Reduce Clutter: Keep your website clean by removing elements that serve no purpose at the moment.
- Display a Custom Message: Use the space to show your own fallback message (e.g., "All services are fully booked! Check back soon.").

The timum.init function provides callback hooks that fire after the widget attempts to fetch resources.

The following snippet demonstrates how to hide the main bookingjs container by checking if the bookables object is empty or if the fetch operation failed entirely.

```HTML
<div id="bookingjs" style="margin: 32px"></div>
<script type="module">
  import * as timum from 'https://cdn.timum.de/bookingjs/1/booking.js';

  // Function to hide the element by setting its display style
  const hideBookingJs = () => {
    const el = document.getElementById('bookingjs');
    if (!el) return; // Exit if the element wasn't found

    //Set the style to hide element
    el.style.display = 'none';

  };

  timum.init({
    ref: 'booking-widget-demo-resource@timum',
    callback: {
      // 1. Check if the fetch succeeded but returned no bookables
      fetchingBookablesSucceeded: (bookables) => {
        // Use optional chaining for safety, then check the number of items
        const hasBookables = bookables && Object.values(bookables).length > 0;

        if (!hasBookables) {
          hideBookingJs();
        }
      },

      // 2. Hide the widget if the fetch failed (e.g., server error)
      fetchingBookablesFailed: () => {
        hideBookingJs();
      },

      // NOTE: Consider adding other related callbacks if needed.
    },
  });
</script>
```

<a id="multi-resource-mode-example"></a>

## Multi-Resource Mode for CondensedView

When managing multiple resources (e.g., different service providers, meeting rooms, or equipment), you may want customers to see available slots across all resources simultaneously. The multi-resource mode displays overlapping timeslots side-by-side with color-coded resource differentiation.

### How it Works

When enabled, multi-resource mode:

- Loads bookables from all available resources
- Groups overlapping timeslots from different resources
- Displays overlapping bookables with intelligent layout: bookables from different resources are shown side-by-side, but when multiple bookables share the same timeslot UUID, they stack vertically in a single column
- Assigns each resource a color for easy differentiation
- Shows a color legend above the calendar for easy reference
- Automatically adjusts font size based on the number of parallel slots
- Suppresses the resource selector, streamlining the booking interface

The mode gracefully degrades to standard behavior when there is only one resource or no time overlaps exist.

### Color Assignment

Resource colors are dynamically derived from your theme's primary color using the OKLCH color space, which provides perceptually uniform hue distribution. A 60-degree exclusion zone ensures resource colors never resemble the primary color (used for buttons, borders, and other UI elements), maintaining clear visual distinction.

When no primary color is configured, the system automatically falls back to a stable 10-color pastel palette. Color assignment is deterministic based on sorted resource UUIDs, ensuring consistent colors across sessions and page reloads.

### Configuration

Enable multi-resource mode for CondensedView:

```javascript
import { TimumBooking } from '@timum/booking';

<TimumBooking
  appConfig={{
    ref: 'your-resource-reference@timum',
    calendarFrontend: 'condensedView', // or 'detailsCondensedView'
    calendarFrontendOptions: {
      condensedView: {
        multiResourceMode: true, // Enable multi-resource display
      },
    },
  }}
/>;
```

### Example Scenario

Suppose you manage three consultants offering consultation services. With multi-resource mode enabled, customers see:

- Each day column shows all available slots across the three consultants
- Slot at 2:00 PM: If Consultants A, B, and C all have distinct timeslot UUIDs, they appear side-by-side (three columns)
- Slot at 2:15 PM: If all three consultants share the same timeslot UUID (e.g., a group session), they stack vertically in a single column for a compact display
- A legend above the calendar showing: "Consultant A (blue), Consultant B (orange), Consultant C (green)"

Customers can click any slot to book with their preferred consultant, without needing to switch between resource selection screens. The layout automatically optimizes space by stacking same-UUID bookables vertically while keeping different-UUID bookables side-by-side.

### Notes

- Requires at least one product with availability across multiple resources
- The color assignment is deterministic and consistent across sessions (based on sorted resource UUIDs)
- Works seamlessly with other CondensedView options like `forceResourceSelectorDialog`
- Professional plan may be required for additional customizations (custom theming, form fields, etc.)

<a id="eyecandy-responsive-layout"></a>

## EyeCandy Responsive Layout in DetailsView

When using the EyeCandy calendar frontend inside DetailsView (with `details` prefix in `calendarFrontend` option), the component implements intelligent responsive layout management to ensure optimal display across all viewport sizes.

### How It Works

**Mobile-First Responsive Decision**: EyeCandy determines its layout mode (mobile or desktop) using a hierarchical approach:

1. **DetailsView Precedence** (when applicable):

   - If DetailsView has already determined the viewport is too small for optimal desktop display (`isViewportSmall: true`), EyeCandy immediately switches to mobile layout
   - This prevents conflicts where DetailsView is in mobile mode but EyeCandy tries to display in desktop mode

2. **Container-Based Measurement**:
   - When DetailsView is not constraining the layout, EyeCandy measures its own container width directly
   - If the container width is below 500px, EyeCandy switches to mobile layout for optimal spacing

**Layout Modes**:

- **Mobile Layout** (`isMobileLayout: true`):

  - Calendar occupies full width with centered alignment
  - Timeslot list appears below the calendar as an accordion with smooth height animation
  - Tapping the selected date toggles the accordion closed for compact display

- **Desktop Layout** (`isMobileLayout: false`):
  - Calendar (320px) and timeslot list display side-by-side
  - PureListView animates in from the left with width expansion
  - DateCalendar shifts position smoothly via FLIP animation for polished transitions

### PureListView Integration

When EyeCandy renders in desktop layout, it computes the remaining available space and communicates this to PureListView via the `forceSmallContainer` prop:

- If the remaining width (after DateCalendar's 320px) falls below the threshold (300px), PureListView uses compact layout (stacked time over product name)
- Otherwise, PureListView uses horizontal layout (time left, product centered)
- This ensures PureListView's responsive behavior is always based on accurate available space, not unreliable self-measurements from animated containers

### Configuration

EyeCandy's responsive behavior is automatic and requires no configuration:

```javascript
import { TimumBooking } from '@timum/booking';

<TimumBooking
  appConfig={{
    ref: 'your-resource-reference@timum',
    calendarFrontend: 'detailsEyeCandy', // Automatically handles responsive layout
    // ... other config
  }}
/>;
```

The component respects both DetailsView's viewport assessment and its own container measurements to deliver consistent, responsive layouts across all screen sizes.

<a id="custom-field-example"></a>

## Custom Field Example

Here is an example tying all of the aforementioned concepts together.
And [here](https://jsfiddle.net/timum/wov6pLk7/) is a fiddle containing the very example detailed in this chapter.

<a id="the-scenario"></a>

### The Scenario

It is necessary to determine the gender of customers for a specific purpose. The `mobile` and `message` fields are not necessary and will be removed. The new `gender` field must be filled, therefore a validation check is required to ensure it is completed. Both the name of the `gender` field and the validation messages must be translated into different languages.

<a id="the-implementation"></a>

### The Implementation

This is the base configuration. As it uses the standard field configuration shown in [Booking form fields](#booking-form-fields) :

```html
<div id="bookingjs" style="margin: 32px"></div>
<script type="module">
  import * as timum from 'https://cdn.timum.de/bookingjs/1/booking.js';

  timum.init({ ref: 'booking-widget-demo-resource@timum' });
</script>
```

Let's add the necessary changes:

```html
<div id="bookingjs" style="margin: 32px"></div>
<script type="module">
  import * as timum from 'https://cdn.timum.de/bookingjs/1/booking.js';

  timum.init({
    ref: 'booking-widget-demo-resource@timum',

    fields: {
      // set these fields to undefined explicitly in order to remove them
      message: undefined,
      mobile: undefined,
      firstName: {
        // index defines the order in which the fields get displayed
        index: 0,
        title: 'fields.firstName',
        validation: yup.string().required('validation.field_required'), // <- compare with key in localisation
      },
      lastName: {
        index: 1,
        title: 'fields.lastName',
        validation: yup.string().required('validation.field_required'),
      },
      email: {
        index: 2,
        title: 'fields.email',
        format: 'email',
        type: 'text',
        validation: yup
          .string()
          .email('validation.email_field_must_be_valid')
          .required('validation.field_required'),
      },
      gender: {
        index: 3,
        // new language key
        title: 'fields.gender.title',
        // best for a limited number of predefined choices
        type: 'select',
        // could use 'validation.field_required' but
        // for this example let's create a new  key
        validation: yup.string().required('validation.gender_field_required'),
        options: [
          { key: 'm', title: 'fields.gender.male' },
          { key: 'w', title: 'fields.gender.female' },
          { key: 'd', title: 'fields.gender.other' },
        ],
      },
      agbs: {
        index: 4,
        title: 'Datenschutzbestimmungen gelesen und akzeptiert.',
        type: 'checkbox',
        validation: yup
          .boolean()
          .required('validation.field_required')
          .test(
            'privacyAccepted',
            'validation.privacy_field_required',
            (value) => value === true,
          ),
      },
    },
    sendCustomValuesInMessage: true, // necessary to transmit custom field values as part of the customer message

    // now we need to add the new translation keys and their translations.

    localization: {
      de: {
        validation: {
          gender_field_required: 'Bitte wählen.',
        },
        fields: {
          gender: {
            title: 'Geschlecht',
            male: 'Männlich',
            female: 'Weiblich',
            other: 'Divers',
          },
        },
      },
      en: {
        validation: {
          gender_field_required: 'Please select an option.',
        },
        fields: {
          gender: {
            title: 'Gender',
            male: 'Male',
            female: 'Female',
            other: 'Others',
          },
        },
      },
    },

    // the following callback consumes the customers input, allowing you to act on
    //the entered gender value.
    callback: {
      createBookingStarted: ({ data }) => {
        console.log(data.gender);
      },
    },
  });
</script>
```
