Problem
Two people book the same room for the same hour, and nobody can say which rooms are actually used.
LiveWorkplace scheduling1-minute read
Meeting-room booking where availability, approvals and check-in follow one rule set.

01
Two people book the same room for the same hour, and nobody can say which rooms are actually used.
02
A bilingual booking system asks three questions (when, how many people and what the room needs) and returns a shortlist of rooms that fit, each with the reason. Behind it sit a day, week and agenda calendar, approvals, QR check-in and utilisation reports for the people who run the space.
03
Product design and full-stack delivery: decisions, interface, Arabic and English content, implementation, tests, server and deployment.
Double bookings are rejected by the database itself, and rooms nobody checks into go back to the waitlist.
Verified by its own test suite, including concurrent booking attempts with no double booking. It is a portfolio demo, so there are no adoption figures.



RoomFlow is a bilingual meeting-room booking system. Someone who needs a room answers three questions — when, how many people, and what they need in the room — and gets a shortlist of rooms that actually fit, each one explaining why it was suggested. Behind that simple surface sit a day/week/agenda calendar, approvals, QR check-in, no-show release, a waitlist, maintenance windows and utilisation reporting for the people who run the space.
It runs as a public demo with fictional data. Every account, room, meeting and audit record you see in it was invented for the demo.
Everything: product decisions, information architecture, interface and interaction design, the Arabic and English content, the PHP and MySQL implementation, the automated test suite, the server configuration, and the deployment.
The first build was complete and hard to use. Everything a room administrator might ever need sat on the same screens as the one thing an ordinary employee wants. These were the decisions that fixed it.
Five steps, each asking for only what it needs to ask for.

On a phone the menu button was there, correctly sized and correctly labelled — and rendered as an empty box. Two separate faults, either of which alone would have hidden the menu.
.nav-ic rule lost the solid square that had been standing in for an icon. The menu button still used the old markup — an empty <span> — and no #ic-menu symbol existed. A 44×44 button with a 1px border and nothing inside it.html[dir="rtl"] .sidebar scores 0,2,1 in CSS specificity; .sidebar.is-open scores 0,2,0. The closed state outranked the open state, so in Arabic — the default language — the panel stayed parked off the right edge even when the button reported it as expanded.Both are fixed, and the drawer was rebuilt as a proper dialog: it slides in from the edge the button sits on, traps focus while open, closes on Escape, on the backdrop and on its own close button, returns focus to the menu button every time, locks background scrolling, and leaves the tab order entirely when closed.

The availability board answers the question without a search. Free rooms sort first, and every room states its condition in words next to an icon — never colour alone, which is unreadable to a colour-blind visitor and to anyone outdoors.
One subtlety cost a bug: a ‘free until 09:00’ label that was quoting tomorrow morning. Every ‘next booking’ lookup is now bounded to the same day, so a time shown to a reader always belongs to the day they are looking at.

Availability answers “is anything free now”. A calendar answers the other question: what does the day actually look like, and where are the gaps. Three presentations, chosen because they are genuinely different jobs rather than one layout at three widths.
Blocks are positioned from their real start and end times, computed on the server, so the grid is correct before any JavaScript runs. Overlapping bookings split into lanes side by side — and the lane count is scoped to each cluster of overlaps, so a booking that clashes with nothing keeps the full column rather than shrinking because two others collided earlier that morning.
Every state is distinguishable without relying on colour: confirmed, awaiting approval, completed, no-show, cancelled and maintenance each have their own treatment, a legend, and words in the accessible name. A red line marks the current time, and only ever on today.

Occupancy is shared; content is not. An employee can see that a room is taken from 13:00 to 14:00 — otherwise the calendar could not help them find a gap — but another person’s meeting shows only as Busy, with no title, no organiser and no link. A coordinator sees the detail their role requires. The rule is enforced server-side, in the same place that governs opening a booking directly.


Every block is a real link with an accessible name that carries the title, the room, the status and both times — because a coloured bar at a position means nothing to a screen reader. Blocks a viewer may not open are not links at all.
Rooms follow one naming system so a reader can guess what a room is for before opening it: purpose-named at the small and large ends, neutral names in between. Each room carries an explicit type alongside its code.
| Room | Arabic | Type | Seats |
|---|---|---|---|
| Focus Room | قاعة التركيز | Focus room | 2 |
| Jasmine Room | قاعة الياسمين | Meeting room | 4 |
| Cedar Room | قاعة الأرز | Meeting room | 6 |
| Collaboration Room | قاعة التعاون | Collaboration room | 6 |
| Olive Room | قاعة الزيتون | Meeting room | 8 |
| Horizon Room | قاعة الأفق | Collaboration room | 12 |
| Executive Boardroom | قاعة الاجتماعات التنفيذية | Boardroom | 20 |
A result card shows name, code, type, capacity, wing, floor, step-free access, how long the room stays free, and its equipment. Anything beyond that lives on the room page rather than crowding the card. All seven rooms are fictional.
This is the part that has to be right. Two people pressing Confirm at the same moment must not both get the room, and no amount of client-side validation can guarantee that.
Every booking is created inside a database transaction. The candidate rows are locked with SELECT … FOR UPDATE before the overlap test runs, so a second request queues behind the first rather than reading a stale answer. The overlap test itself is buffer-aware: a room with a 15-minute turnaround rejects a booking that starts 10 minutes after the previous one ends.
newStart < existingEnd + buffer
newEnd > existingStart - buffer It is verified by racing twelve real operating-system processes at the same slot, five times over. Exactly one wins each round and the database ends with exactly one row — 60 concurrent attempts, zero double bookings. To prove the test is not vacuous, the same harness was pointed at a naive check-then-insert with no row lock: it produced twelve overlapping bookings out of twelve.
There is no machine learning here and no claim of any. The ranking is a small, deterministic score that a person can read and predict:
The reasons shown on each card are the same values the score was computed from, so the explanation cannot drift from the ranking. Reasons and caveats are separated deliberately: an approval requirement is a cost, and listing it under a heading that reads ‘Why this room’ would sell a drawback as a benefit.
When nothing fits, the same logic produces alternatives — the next free slot in each suitable room — rather than an empty result.
Three roles, each with a different first screen. Every route, every controller action and every direct object URL is checked on the server; hiding a link is presentation, not protection.
Find a room, book it, see and cancel their own bookings, check in by QR. Cannot open anyone else's booking, by URL or otherwise.
Everything an employee can do, plus today's schedule across all rooms, check-ins due, no-shows, approvals and maintenance windows.
Everything a coordinator can do, plus users and roles, utilisation reporting and the audit trail.
Approving a booking is a coordinator duty, not a user-administration privilege. That distinction was wrong in an earlier build — coordinators could manage maintenance but not approve anything, which is the one thing the role exists for — and the automated role tests now assert it.
Each booking carries a 128-bit random check-in token, generated independently of the booking id so it cannot be guessed or walked. The QR code is rendered by a Reed–Solomon encoder written for this project rather than pulled from a third-party service, so no booking data leaves the server to have a code drawn.
The demo is a real, fully interactive instance. Anyone can sign in and book a room. The guards are about protecting the shared dataset, not about disabling the product.
demo_blocked.Arabic is the default and is a first-class language, not a translation layer. The interface mirrors with CSS logical properties, so the drawer, the navigation, the card layouts and the form grids all flip without a second stylesheet.


:focus-visible so a mouse click does not leave a ring behind.aria-current on the active navigation item, and labelled form controls throughout.prefers-reduced-motion is honoured — the drawer, the chart bars and the button transitions all stop.HttpOnly, Secure and SameSite=Lax.X-Powered-By.PHP 8.3 and MySQL 8 with no framework. A front controller with a regex route table, PSR-style autoloading, thin controllers, and the booking rules in one place rather than scattered across views.
NOW() and PHP's date() cannot disagree — the class of bug that silently breaks conflict windows and lockout expiry.open_basedir confining it.Six suites run against the deployed application, not a mock of it.
| Suite | What it covers | Assertions |
|---|---|---|
| Unit and integration | Overlap and buffer logic, policy validation, approvals, maintenance, recurrence, ranking, alternatives, ICS, QR, bilingual coverage and plurals, timezone agreement, housekeeping | 143 |
| Concurrency | 12 real processes racing one slot, 5 rounds | 60 attempts, 0 double bookings |
| Live HTTP | Authentication, role enforcement, CSRF, direct object access, security headers, transport, demo guards, navigation contract, availability wording | 124 |
| Calendar | Three views, block positioning, overlap lanes, filters, states, current-time indicator, empty states, keyboard names, role-based privacy, escaping, bilingual | 56 |
| End-to-end journey | Sign in, search, select, confirm, view, conflict, export, QR check-in, cancel | 23 |
| Login throttling | Lockout, expiry, isolation between accounts | 5 |
Two of those tests exist because they caught me. The concurrency harness was checked against a deliberately unlocked implementation to prove it could detect the failure it claims to prevent. And an assertion that every state-changing action was demo-guarded was passing vacuously — it split methods on public function while the controllers declare public static function, so it never parsed a single method. Rewritten against the route table, it immediately found a real gap.
These are figures from the build itself. There are no user numbers, adoption figures or savings claims here, because RoomFlow is a portfolio demo and has none.