# Admin guide (back office)

The back office lives at `/app` in the application. Everything in this guide is Dutch in the UI; the
Dutch labels are quoted so you can find them. For how the engine behaves underneath, see
[`02-concepts.md`](02-concepts.md).

## Contents

- [Roles and what each one sees](#roles-and-what-each-one-sees)
- [First-time setup](#first-time-setup)
- [Daily work: the calendar](#daily-work-the-calendar)
- [Appointments](#appointments)
- [Services](#services)
- [Staff](#staff)
- [Customers](#customers)
- [Packages](#packages)
- [Events](#events)
- [Payments](#payments)
- [Coupons](#coupons)
- [Notifications](#notifications)
- [Settings](#settings)
- [Public pages your customers see](#public-pages-your-customers-see)
- [Known limitations](#known-limitations)

## Roles and what each one sees

Four roles exist: `owner`, `admin`, `front_desk`, `staff`. The nav shows only what the role may use;
everything else redirects to the role's home (the dashboard, or the calendar for roles without one).

| Menu item (Dutch) | Route | owner | admin | front_desk | staff |
|---|---|---|---|---|---|
| Overzicht | `/app` | ✓ | ✓ | – | – |
| Agenda | `/app/calendar` | ✓ | ✓ | ✓ | ✓ (own agenda) |
| Afspraken | `/app/appointments` | ✓ | ✓ | ✓ | ✓ (own agenda) |
| Klanten | `/app/customers` | ✓ | ✓ | ✓ | – |
| Pakketten | `/app/packages` | ✓ | ✓ | ✓ | – |
| Evenementen | `/app/events` | ✓ | ✓ | view only | – |
| Diensten | `/app/services` | ✓ | ✓ | – | – |
| Medewerkers | `/app/staff` | ✓ | ✓ | – | – |
| Betalingen | `/app/payments` | ✓ | ✓ | ✓ | – |
| Kortingscodes | `/app/coupons` | ✓ | ✓ | – | – |
| Meldingen | `/app/notifications` | ✓ | ✓ | – | – |
| Instellingen | `/app/settings` | ✓ | ✓ | – | – |

Two extra gates:

- **Payment settings and the Mollie key** can only be changed by owner/admin. Everyone else sees the
  payments tab disabled with "Alleen een eigenaar of beheerder kan dit wijzigen."
- A **staff** login is linked to one or more `staff` records (`staff.user_id`); they only see and
  change appointments of those staff members, and cannot move an appointment to another staff member.
  Unlinked they see "Je account is nog niet aan een medewerker gekoppeld."

> There is **no user-management screen yet**: you cannot invite colleagues or change roles in the UI.
> Contact nisebookings to add a colleague (with one of the four roles) or to change a role.

## First-time setup

After signing up (`/signup`) you land on `/onboarding` ("Je bedrijf"): fill in **Bedrijfsnaam**,
**Webadres** (slug: lowercase letters, digits, hyphens) and **Tijdzone**, then "Verder". This creates
your account, makes you owner, and installs the default Dutch e-mail templates. You start with no
services or staff.

Recommended order:

1. **Instellingen → Algemeen** — company name, time zone, time step, how far ahead customers may book.
2. **Instellingen → Openingstijden** — the weekly template that new staff inherit.
3. **Instellingen → Vrije dagen** — company-wide closing days.
4. **Medewerkers** — add the people/resources. Each new staff member gets the opening hours as their
   weekly schedule.
5. **Diensten** — add what can be booked, and tick which staff perform it (Medewerkers → Diensten).
6. **Instellingen → Bedrijf en e-mail** — sender name, reply-to, phone/address/website (used in mails).
7. **Instellingen → Boekingsformulier** — cart, chain, recurrence, occupancy display, waiting list.
8. **Instellingen → Betalingen** — VAT inclusive/exclusive, deposit mode, local/Mollie, Mollie API key.
9. **Instellingen → Insluiten** — copy the embed snippet to your website, restrict allowed websites.
10. Test: book something yourself through the widget, then watch **Meldingen → Verzendlog**.

## Daily work: the calendar

`/app/calendar` — the default landing page for front desk and staff.

- Views: **Dag** / **Week** / **Maand** (week by default). **Vorige** / **Vandaag** / **Volgende**,
  a date field, and selects for **Medewerker** and **Dienst**. Keyboard: ← / → / T (today).
- Click an empty grid cell → **Afspraak maken** prefilled with staff, date and time (snapped to the
  time step).
- Colours follow the booking status; cancelled/rejected appointments are muted and struck through.
  Mixed-status appointments are labelled "Gemengd"; an appointment without customers shows
  "Geen klant"; a recurring appointment carries a "reeks" marker; groups show "N pers."
- Events of staff linked to them appear as blocks ("Evenement").
- A rental (Verhuur ≥ 1 day) fills the whole day on every covered day and is labelled "N dagen"
  instead of a clock time.
- Grey areas are times outside the staff's schedule, breaks, and holidays.
- Week view hides non-business days when *Alleen werkdagen tonen* is on; day view hides staff without
  appointments when *Medewerkers zonder afspraken verbergen* is on; visible hours follow opening hours
  when *Alleen de uren binnen de openingstijden tonen* is on (all in **Instellingen → Agenda**).

## Appointments

### List — `/app/appointments`

Tabs **Komend** / **Voorbij**, status tabs (Alle statussen, In afwachting, Bevestigd, Geannuleerd,
Afgewezen, Afgerond, Wachtlijst). Each row shows date/time, service, staff and note, then per customer:
name, "N personen", a payment badge (Betaald / Aanbetaling / Open), an inline status select, the
**Klant mailen** checkbox (on by default) and **Opslaan**. The appointment itself can be deleted
("Deze afspraak en de boekingen erin verwijderen?"). 50 rows per page.

### Create — `/app/appointments/new`

Needs at least one simple service and one staff member. Fields: **Dienst**, **Medewerker**, **Datum**,
**Begintijd**, **Personen**, **Status** (default = Instellingen → Afspraken), a customer block
("Bestaande klant" from the first 200 matches, or "Nieuwe klant" with Naam/E-mailadres/Telefoon — an
existing e-mail address is matched and reused), **Interne notitie**, and two checkboxes:
**Toch inplannen bij een waarschuwing** and **Klant mailen**.

With `?package=<id>` (from a package page) the service, customer and persons are fixed to the package.

### Edit — `/app/appointments/[id]`

- **Details**: staff, service, date, start and end time (end follows start + service duration until
  you change it), internal note. A rental shows only the date — it always starts at 00:00 and runs the
  service's number of days.
- **Live warnings** after you change something: overlap with another appointment, duration differing
  from the service, outside the schedule/breaks, or a holiday. These are non-blocking: the button
  becomes **Toch opslaan**. Blocking errors are capacity ("Dat past niet: deze medewerker heeft bij
  deze dienst plek voor maximaal N personen, dit worden er M."), end before start, or a staff member
  that does not offer the service.
- **Klanten**: shows "X van maximaal Y plaatsen bezet" (only In afwachting and Bevestigd count) and the
  waiting-list count. Per customer: persons, status, **Klant mailen**, Opslaan, and **Verwijderen**
  (no mail is sent). Under the customer you also see the answers to the service's **Velden**
  ("Vraag: antwoord", per-person answers joined per person; a deleted question shows its raw id).
  A file answer is a link to the upload, opening in a new tab; the link is short-lived, and once the
  file is deleted the stored name is shown without one.
  Waitlisted customers get **Plaats geven** (refused when the slot is full:
  "Er is geen plek: N van M plaatsen zijn bezet."). Adding a customer uses the same existing/new block.
  Empty state: "Nog geen klanten. De tijd blijft wel bezet." — an appointment without bookings still
  blocks its time.
- **Recurring**: when the booking belongs to a series, a scope select appears — **Alleen deze** /
  **Deze en volgende** / **Hele reeks**. Other occurrences shift by the same number of days and adopt
  the new start time, keeping their own duration, staff and service. Partial results are reported
  ("N van M andere afspraken in de reeks gewijzigd; de rest mocht je niet wijzigen of bestond niet meer.").
- **Compound/collaborative**: "Onderdeel van {service} — N delen. Een statuswijziging geldt voor alle delen."
- **Delete**: checkbox **Klanten informeren** (on): approved bookings are cancelled, pending ones
  rejected, each mailed; other statuses get no mail. For series/appointments you choose the scope, and
  a booking that was moved out of the series is kept.

## Services

`/app/services` — grouped by category ("Zonder categorie" for uncategorised). Badges: Samengesteld,
Samenwerkend, Pakket, "Verborgen", "tot N personen". **Dienst toevoegen** starts with a type, which
cannot be changed later:

| Type (UI) | What it is | Needs |
|---|---|---|
| **Gewone dienst** | one appointment with one staff member, optional extras | duration, price, capacity |
| **Samengesteld** | parts one after another (wash → cut → dry), with spare-time rows | ordered parts |
| **Samenwerkend** | parts at the same time by different staff (skipper + boat) | ordered parts, one per staff |
| **Pakket** | prepaid bundle of N appointments of one simple service | base service, size, validity |

Common fields: **Naam**, **Categorie**, **Omschrijving**, **Zichtbaarheid** (Zichtbaar voor klanten /
Verborgen, alleen intern te boeken), **Aanbetaling** ("100%", "30%" or a fixed amount per person),
**Btw-tarief** (21 / 9 / 0 / Ander tarief).

Simple services add: **Duur (minuten)**, **Verhuur (dagen)** (0 = an ordinary appointment with times;
1 or more = whole-day rentals — see below), **Tijdstap (minuten)** ("Leeg = standaard (N min)"),
**Prijs (EUR)**, **Minimaal/Maximaal aantal personen**, **Tijd vooraf** and **Tijd achteraf**
(preparation/cleanup; blocks the calendar but is invisible to customers), **Eén boeking per
tijdslot**, **Herhalen** (Dagelijks / Wekelijks / Om de week / Maandelijks; none ticked = no repeats),
**Wachtlijst (maximaal aantal personen)** (empty = unlimited, 0 = off), and **Als de klant geen
medewerker kiest** (Goedkoopste / Duurste / Volgorde van de medewerkerslijst).

**Verhuur (dagen) ≥ 1** turns the service into a rental: the customer picks a start date, the
appointment covers that whole day plus the following days, and the price is the daily price times the
number of days. Business hours and the weekly schedule never apply to a rental — days off and existing
appointments do. An appointment you make yourself for a rental always starts at 00:00 on the chosen
date; extra's, chains and the waiting list do not apply, and the calendar shows the span as "N dagen"
on every covered day.

Sections below the form:

- **Onderdelen** (compound/collaborative): ordered rows of service or **Vrije tijd (minuten)**, plus
  the flags *Alle onderdelen bij dezelfde medewerker* / *Alle onderdelen even lang bezet*.
- **Extra's** (simple services): Naam, Prijs, Extra minuten, Min., Max., Volgorde. Minimaal 1 makes it
  mandatory. Existing bookings keep the extras they were booked with.
- **Velden** (every service): the intake questions the customer answers in the booking form. Per row:
  **Vraag** (the label), **Soort** (Tekst / Tekstvak / Keuzelijst / Aanvinkvakje / Getal / Datum /
  Bestand), **Keuzes** (one per line, only for a Keuzelijst: 1–20 choices of 1–100 characters),
  **Volgorde**, **Verplicht** (the customer cannot book without an answer) and **Per persoon** (one
  answer per person), plus **Toon alleen als** + **Antwoord**: pick an earlier question and the answer
  that should reveal this one (e.g. "Kleur = rood", a checkbox with "aangevinkt" = `true`, a number as
  `3`). A question whose condition does not hold is not shown, not required and not stored; the
  controlling question must be earlier in the list, unconditional and not per-person or a file.
  Answers show on the appointment and can be mailed with `{fields}`.
  A **Bestand** question takes one file of at most 5 MB (JPG, PNG, WebP or PDF). The file is stored
  with the booking and opens from the appointment page through a short-lived link; in a mail
  `{fields}` shows the file name only. Deleting a question keeps the answers on existing bookings;
  without a label such an answer shows its raw id.
- **Categorieën** (on the list page): rename, delete (services survive), add.

Prices are per staff member (Medewerkers → Diensten); the service price is the fallback.

## Staff

Staff is a generic "bookable resource": a person, a room, a boat, a device. `/app/staff` has **Actief**
and **Gearchiveerd** tabs, with **Verborgen** for private staff.

On a staff page:

- **Gegevens**: naam, e-mail, telefoon, omschrijving, zichtbaarheid.
- **Diensten**: which services this staff member performs, with per-staff **Prijs**, **Min./Max.
  personen** and **Aanbetaling** (empty = follow the service).
- **Rooster**: fixed weekly hours (Open / Van / Tot / Pauzes per weekday). Breaks are written as
  `12:00-12:30`, several separated by commas. A day with Open off is a day off. New staff start with
  the company opening hours — later changes to opening hours do not touch existing staff.
- **Vrije dagen**: per-date rows, "Eenmalig" / "Elk jaar" / "Toch open (uitzondering op een jaarlijkse
  vrije dag)". These come on top of the company holidays. Deleting an archived staff member is not
  possible; archiving keeps history.

## Customers

`/app/customers` — search on name, e-mail or phone. Detail page shows and edits Naam, E-mailadres,
Telefoon, Notities, plus the appointment history and **Klant verwijderen**
("Deze klant en al zijn boekingen verwijderen? Dit kun je niet ongedaan maken.").

Customers are created automatically on booking (matched on e-mail within your account).

## Packages

`/app/packages` lists bought packages: customer, package, staff, scheduled visits, validity
("Vanaf de eerste afspraak" until the first visit is planned).

- **Pakket aanmaken**: package service, optional staff ("Nog niet kiezen"), customer, **Betaling**
  ("Ter plaatse betalen (open betaling)" or "Geen betaling" — an open payment is completed later at
  Betalingen), note, **Klant mailen** (sends the `package_new` mail with the personal link).
- Package detail: **Afspraak inplannen** (only while visits remain and it has not expired) opens
  `/app/appointments/new?package=<id>`; **Persoonlijke link** with a copy button; the visit list.
- Validity starts at the first planned visit: that date + the package's life days. Persons are fixed
  to 1 and only the package's own service can be planned. Errors: "Alle N afspraken van dit pakket
  zijn al ingepland.", "Dit pakket was geldig tot en met {datum}. Kies een eerdere datum."

## Events

`/app/events` — Komend / Voorbij. An event has **Titel**, **Status** (Gepubliceerd, te koop /
Verborgen), **Omschrijving**, **Afbeelding (webadres)**, start/end, **Verkoop stopt op** (empty = until
the start), a **Ticketcode** mask (each `*` becomes a random character, at least 4), and
**Medewerkers** (Niet betrokken / Organisator / Ondersteuning (agenda geblokkeerd)) — organisers simply
show in the calendar; only **Ondersteuning** has their time blocked for bookings while the event runs.

**Tickettypes**: Naam, Prijs, Btw %, Aantal, Gereserveerd ("Gereserveerde tickets worden niet online
verkocht"), Volgorde. Deleting an event is refused once tickets are sold — hide it instead.

**Deelnemers** (`/app/events/[id]/attendees`): Code, Klant, Ticket, Status (Wacht op betaling /
Geldig / Geannuleerd), Betaling. **Annuleren** frees the ticket for resale and sends no mail.

## Payments

`/app/payments` — newest first, filter by status (Open / Voltooid / Mislukt / Terugbetaald) and type
(Ter plaatse / Online (Mollie) / Gratis). Footer totals use the whole filtered set, not just the page.

Payment detail:

- **Betaling voltooien** — local, open payments: marks it fully paid.
- **Terugbetalen** — completed Mollie payments with a reference: refunds through Mollie. Appointments
  are **not** cancelled automatically.
- **Te laat betaald**: the payment arrived after the booking was rejected and the slot is gone. The
  alert says "Te laat betaald: de tijd was al vergeven. Betaal het bedrag terug." — refund it.
- **Bedragen** repeats the stored breakdown (subtotal, discount, gateway surcharge, total, VAT,
  deposit/later te betalen, paid); **Boekingen** links the appointments.

## Coupons

`/app/coupons` — code (case-insensitive), Korting (%) and Aftrek (EUR) (both zero is rejected),
**Aantal keer te gebruiken** (account-wide), **Eén keer per klant**, validity dates (the *booking* date
counts, not the appointment date), min/max appointments per order, and restrictions to services
and/or staff (none ticked = all). **Genereer code** expands a mask like `WELKOM-****`.

## Notifications

`/app/notifications` groups templates by type (Nieuwe afspraak, Statuswijziging, Herinnering,
Wachtlijst, Plek vrij (wachtlijst), Nieuw pakket, Tickets), with an Aan/Uit toggle. The editor covers
recipients (Klant / Medewerker / Beheerders / Andere adressen), status filter, service filter,
reminder timing, subject, body with `{codes}` and `{#if …}` blocks, an optional .ics attachment, a
**Test versturen** button (to your own address) and the code reference. `/app/notifications/log` is the
send log with statuses Verstuurd / In wachtrij / Bezig / Mislukt and retry count.

Full detail: [`06-notifications.md`](06-notifications.md).

## Settings

`/app/settings/<tab>`:

| Tab | What you set |
|---|---|
| **Algemeen** | Bedrijfsnaam, Tijdzone, Tijdstap (minutes between start times), Minimaal vooraf (minutes), Maximaal vooruit (days), Taal van het boekingsformulier (Nederlands / Engels — the language customers see in the widgets; an individual embed can override it with `data-lang`). |
| **Bedrijf en e-mail** | Naam afzender (empty = company name), Antwoordadres, Telefoonnummer, Website, Adres, Verzendmarge herinneringen (uur), Annuleren tot (minutes before the appointment; 0 = until the start). |
| **Afspraken** | Status van nieuwe boekingen (Direct bevestigd / Eerst door jou goedkeuren) and the automatic status change (Van → Naar, after the appointment has passed). |
| **Agenda** | Alleen werkdagen tonen, Alleen de uren binnen de openingstijden tonen, Medewerkers zonder afspraken verbergen. |
| **Boekingsformulier** | Cart, chain ("direct na elkaar boeken"), extras × persons, Herhalende afspraken + Betalen bij een reeks (Hele reeks / Alleen de eerste afspraak), Bezetting tonen (Niet tonen / Bezette plaatsen 3/8 / Vrije plaatsen "nog 5"), Wachtlijst + "Wachtlijst telt mee als bezet", Prijs van samengestelde/samenwerkende diensten (Eigen prijs / Optelsom van de onderdelen) and "Klanten kiezen geen medewerker bij samenwerkende diensten". |
| **Betalingen** | Prijzen inclusief btw, Aanbetaling (alleen aanbetaling / klant kiest, aanbetaling voor / klant kiest, volledig voor), Ter plaatse betalen, Online betalen via Mollie + API-sleutel (write-only, shows a hint afterwards), Toeslag Mollie (%) en vast bedrag, Betaaltermijn (minuten), Status na betaling. Owner/admin only. |
| **Openingstijden** | Weekly template for new staff schedules. |
| **Vrije dagen** | Company-wide closing days. |
| **Insluiten** | Publieke sleutel, HTML snippet (on WordPress, paste it into a **Custom HTML** block; the shortcode shown there is not needed), and **Toegestane websites** (one origin per line; empty = every site may embed). |
| **Webhooks** | Owner/admin. Add an endpoint (URL + the events to send; the signing secret is shown **once**), **Test versturen**, **Uitzetten / Aanzetten**, **Verwijderen**, and the last 20 deliveries per endpoint with status, attempts and error. Rotating the secret and resending a delivery go through the API — see [Webhooks](10-webhooks.md). |
| **API** | Create and revoke API keys for the [management API](../api/README.md): name, scopes (catalogue/settings), shown once, plus the most recent writes per key. Server-side use only — the key is not for browsers. |

## Public pages your customers see

No login, `noindex`, styled minimally in the same design language:

- `/b/<booking token>` — **Je afspraak**: service, date/time, staff, persons, and "Afspraak
  annuleren" while the cancellation window (Instellingen → Bedrijf en e-mail) is open. "Afspraak
  verzetten" offers the next free times of the same service and staff (14 days, max 40 options);
  moving an appointment with several bookings moves them all. Friendly states
  for already cancelled, too late, already started, rejected and done.
- `/p/<package token>` — **Je pakket**: remaining visits, validity, planned visits, and the booking
  widget in package mode to plan the next one.
- `/w/<waitlist token>` — **Plek vrijgekomen**: "Plek innemen", first come first served; states for
  taken ("Helaas, iemand anders was je net voor.") and gone.
- `/o/<order token>` — **Je afspraken**: all appointments of one order with their statuses and one
  button ("Alle afspraken annuleren") that cancels everything still inside the cancellation window;
  appointments that are too late stay and are explained instead.

## Known limitations

- No user/role management UI, no invitations, no account switcher (the first membership wins).
- No CSV import/export, no print views, no activity log, no remembered filters.
- Customer pickers search as you type (20 matches at a time); type more to narrow down, or pick "Nieuwe klant" and enter the e-mail — an existing address is matched and reused.
- Warnings (overlap, schedule, holiday, duration) never block saving — "Toch opslaan" is always possible.
- Language of the admin UI is Dutch only.
