# Appointment bookings and calendar

Visitors choose a service, a day and a free time; the booking is saved in the website's database and can never
collide with another one. Times are worked out in the business's time zone, including daylight-saving changes.

Requires the `base` package. Emails (confirmation, day-before reminder, owner notice) are sent when the base email
settings are set up; without them bookings still work.

## Install

Use the verified installer in the website container. It installs dependencies, checks checksums, and refuses to replace customer changes:

```sh
python /opt/runalio/catalog_install.py bookings
python /opt/runalio/catalog_install.py --status
```

Review the preview and click Publish to promote backend code and pages. Production data remains separate.

To undo the last installation before publishing: `python /opt/runalio/catalog_install.py --rollback`.

## Settings

The customer enters these in Runalio, Settings → Website modules → this module (never in files or chat):

| Name | Required | Secret | Purpose |
|---|---|---|---|
| `OWNER_EMAIL` | no | no | Receives a notice for each booking and cancellation (needs the base email settings). |
| `CALENDAR_FEED_KEY` | no | yes | A long random value; enables the private calendar feed the owner subscribes to. |
| `BUSINESS_TIMEZONE` | no | no | IANA time zone for local schedules and messaging, such as America/Chicago. |
| `BOOKING_HOURS_JSON` | no | no | Weekly hours as JSON, e.g. {"mon":[["09:00","17:00"]],"tue":[],"wed":[],"thu":[],"fri":[],"sat":[],"sun":[]}. |
| `BOOKING_STEP_MINUTES` | no | no | Whole minutes between available start times, 1 to 1440. |
| `BOOKING_LEAD_MINUTES` | no | no | Minimum notice in whole minutes, 0 to 525600. |
| `BOOKING_HORIZON_DAYS` | no | no | Days ahead customers can book, 1 to 366. |

## Configure

Edit `/workspace/server/config/bookings.json` (created from `bookings.example.json`):

```json
{"timezone": "America/New_York",
 "services": [{"id": "haircut", "name": "Haircut", "minutes": 45, "description": "Wash, cut and style"}],
 "hours": {"mon": [["09:00", "12:00"], ["13:00", "18:00"]], "tue": [["09:00", "18:00"]], "wed": [], "thu": [["09:00", "18:00"]],
           "fri": [["09:00", "18:00"]], "sat": [["10:00", "14:00"]], "sun": []},
 "step_minutes": 15, "buffer_minutes": 10, "lead_minutes": 120, "horizon_days": 60,
 "closed_dates": ["2026-12-25"], "location": "12 Main Street"}
```

`timezone` must be an IANA name. `step_minutes` is how often a start time is offered, `buffer_minutes` the gap kept
between appointments, `lead_minutes` the minimum notice, `horizon_days` how far ahead visitors can book.

## Add it to a page

Copy `site/booking.js` into `/workspace/site`, then on the booking page:

```html
<div data-runalio-booking></div>
<script src="booking.js" defer></script>
```

The same page handles cancellation links (`?booking=<id>&token=<token>`) sent in confirmation emails.

## API

| Request | Purpose |
|---|---|
| `GET api/bookings/services` | Services, time zone, location |
| `GET api/bookings/availability?service=&date=YYYY-MM-DD` | Free start times for that day |
| `POST api/bookings` | `{service, start, name, email, phone?, notes?}`, returns the booking with a private token |
| `GET api/bookings/<id>?token=` and `POST api/bookings/<id>/cancel` | Visitor view and cancellation |
| `GET api/bookings/<id>/event.ics?token=` | Add-to-calendar file |
| `GET api/bookings/calendar.ics?key=CALENDAR_FEED_KEY` | Owner's calendar feed |

The owner sees upcoming bookings in the admin area (`<website>/api/admin`), with CSV export.

## What the customer needs to do

1. Give the agent their services, hours, time zone, minimum notice and closed dates.
2. To see bookings in their own calendar: add `CALENDAR_FEED_KEY` (a long random value) in Settings → Website modules → Bookings,
   then copy the calendar feed link from the admin area into Google Calendar (Other calendars, From URL), Apple
   Calendar (New Calendar Subscription) or Outlook (Add calendar, Subscribe from web). Calendars refresh the feed
   every few hours.
3. For emails: set up the base email settings and add `OWNER_EMAIL`.
4. A paid Runalio package keeps bookings open around the clock.

Two-way sync (blocking times that are busy in Google Calendar) is not included; it needs the owner's Google OAuth app.
