Appearance
Booking Calendars
Each staff member's Google Calendar in step with their bookings: a booking is an event in it, and what is busy there is not offered on the site.
kitloom/booking-calendars · admins: kitloom/booking-calendars-filament, kitloom/booking-calendars-nova
Each staff member's Google Calendar in step with their bookings. A booking that holds time — waiting or confirmed — is an event in the staff member's calendar, moved when it moves and taken out when it is cancelled; what is busy in the calendar is read back every few minutes and is not offered on the site. The staff keep one calendar, the salon never double-books, and nobody signs in anywhere: the site reaches Google with one service account the staff share their calendars with. The admin is kitloom/booking-calendars-filament or kitloom/booking-calendars-nova.
Set up once in Google Cloud
One service account for the site, made once by whoever has a Google account; it takes ten minutes and never expires.
- Open console.cloud.google.com and make a project (Select a project → New project), or pick one the site has already.
- APIs & Services → Library, search Google Calendar API, open it and press Enable.
- APIs & Services → Credentials → Create credentials → Service account. A name such as Booking calendars; the rest is left as it is — no role, no users. Done.
- Open the account you just made, tab Keys → Add key → Create new key → JSON → Create. The browser downloads a file; keep it, it is the only copy.
- In the admin, Booking Calendars → Settings, paste the whole file into Service account key and save. The page then shows the account's address,
booking-calendars@<project>.iam.gserviceaccount.com: that is what each staff member shares their calendar with.
There is no OAuth consent screen to fill in and nothing to publish: a service account is a machine's identity, not a person's sign-in. (An OAuth client in a consent screen left in Testing hands out tokens that die after seven days — that trap is not on this road.) Domain-wide delegation is not needed either: the account sees only the calendars shared with it.
Share a calendar with it
Each staff member, once, in their own Google Calendar on a computer:
- Settings (the gear) → Settings for my calendars → the calendar → Share with specific people or groups → Add people and groups.
- The service account's address from the Settings page, permission Make changes to events, Send. (See all event details is enough for reading what is busy, but then no booking can be written into the calendar.)
- For their main calendar, their address is the calendar's address. For a secondary one — a calendar named Work — the address is under Integrate calendar → Calendar ID:
…@group.calendar.google.com.
A calendar shared with the account shows up nowhere else: the account has no inbox and no interface, it only answers the site.
Connect it in the admin
Booking Calendars → Calendars → New: the staff member, the calendar's address, and two switches — Bookings go into the calendar and What is busy there is not offered, both on. Saving looks at the calendar through Google at once:
- Connected: Google sees it. It is read right away, and every waiting or confirmed booking of the staff member from now on goes in, so they open their calendar and find their week there.
- Not shared: Google cannot see it — not shared yet, shared with another address, or the address is mistyped. The row says which address to share it with. Share it, then Check access on the row.
The list shows every calendar with its status, when it was last read and the last error. On each row: Check access after a change in Google, Read now instead of waiting for the next reading, Send the bookings again to put the staff member's upcoming bookings into the calendar once more — made where one is missing, brought up to date where it is. Connected off keeps the row and its settings but stops both ways. Removing a calendar leaves the events already in Google: they are the staff member's to keep or clear.
What the staff see
A booking is an event in their calendar: the service and the client's name in the title (Haircut — Anna K.), the booking's code, the client's phone, email and note in the description, the salon's address as the place. A booking waiting for the staff member's word is a tentative event; confirmed, it is a normal one. Moved by the salon, the event moves; given to another staff member, it leaves one calendar and lands in the other; cancelled, declined or marked a no-show, it is gone. A visit marked done stays as it was.
Settings decide how much the event says: the client's name in the title or only in the description, the phone and note included or not — for a calendar the staff member shares with their family, less is better.
An event the staff member deletes by hand is made again on the next change to the booking: the client was promised the time. To free the time, the booking is cancelled on the site. What the staff member writes in the event otherwise — a colour, a reminder — stays.
What comes back
Every few minutes the site reads what is busy in each calendar — a dentist appointment, a day off, a lesson — and does not offer those times on the booking form, to clients and to the front desk alike. Only the times are read: what the staff member's own appointments are stays with them, and nothing of them is written anywhere on the site. The site's own events are busy times too, which harms nothing.
At the moment of booking the site looks at the calendar once more, so an appointment added a minute ago is not booked over; three seconds at most, and past them the last reading answers. A slot freed in Google reopens on the site at the next reading.
The reading needs the scheduler running (php artisan schedule:run every minute, as every kitloom site runs it); the bookings go out from the queue, and without a worker at once.
When something goes wrong
A call that fails lands on the calendar's row with Google's words. Five failures in a row stop pushing to that calendar until a call gets through — a reading, or Check access. Site Health warns while a connected calendar is not shared or failing, and the notification topic Booking calendar not working (off by default) tells a channel once in a while. Nothing of this reaches a client: a booking is taken whether or not the calendar answered.
The application key changed? The saved key cannot be read any more and every calendar says so: paste the key file on the Settings page again.
Settings
Booking Calendars → Settings:
- Service account key — the JSON file from Google Cloud, pasted once. Kept encrypted with the application key, never shown back; an empty field keeps the saved one, Forget the key clears it. The address to share calendars with is shown next to it.
- Event title — the service and the client (Haircut — Anna K.), or the service alone with the client in the description.
- Put the client's phone and note into the event (on) — off, the event names the client and the booking code only.
- Read the calendars every (5 minutes) — how old a reading may be before the scheduler reads again.
- Look at the calendar once more at the moment of booking (on) — one call to Google inside the booking, a few seconds at most.
- Read this many days ahead (0) — 0 is as far as clients may book (Bookings → Settings), and a week more.
Beyond the screens
- Site Health (kitloom/health): "Booking calendars" warns while a connected calendar is not shared or keeps failing.
- Notifications (kitloom/notifications): the topic Booking calendar not working, off by default, tells a channel once in a while per calendar what Google said and whether pushing has stopped.
- AI agents (kitloom/ai):
bookings_calendars_listandbookings_calendars_syncfor whoever manages the calendars — "Why is Mira's booking not in her calendar?" — connecting one and saving the key stay in the admin. - Erase Personal Data (kitloom/privacy): the events of the address's bookings in the staff's calendars are rewritten to name nobody. Google keeps its own copies by its own rules, and the staff member's own calendar is theirs.
What is inside
| What | Where |
|---|---|
| Calendars | table booking_calendars (the site's prefix in front), model Kitloom\BookingCalendars\Models\BookingCalendar |
| The site's events in them | booking_calendar_events — Google's id per booking and calendar |
| What is busy there | booking_calendar_blocks — stretches of time only, never what the staff member's own appointments are |
| Settings | options booking_calendars_*: the key, the event's title and details, how often to read, the look at the moment of booking, how far ahead |
| The console | booking-calendars:sync (--all, --staff=), every five minutes on the scheduler |
| Permission | manage_booking_calendars, given to the roles that manage bookings |
| Configuration | config/kitloom-booking-calendars.php |
Google is reached on Laravel's HTTP client with a JWT signed by the service account's key — no SDK. Bookings are pushed from the queue (PushBooking), three tries with a pause between; a failure lands on the calendar's row. The busy times reach kitloom/bookings through its BusyTimes contract, so another calendar — a Microsoft 365 module, a second job — can bring its own.
What the application overrides
- The model:
ModelRegistry::use(BookingCalendar::KEY, App\Models\BookingCalendar::class). - The queue, the timeouts and the token's cache store, and the admin sections and the tags for agents, notifications, health and privacy:
php artisan vendor:publish --tag=kitloom-booking-calendars-config.
Tests
From the application that installs the package: php vendor/bin/phpunit --testsuite BookingCalendars.