A staff-facing shift management app. Rotas published once, staff apply for what suits them, and the checks that matter enforced before anyone gets confirmed. Live at vincentwap.co.uk/app/workforce.
PHP 8, MySQL, Tailwind, plain JavaScript. Installable as a PWA, wrappable as an Android app.

The problem
A manager posts the week’s shifts into a message thread. Three people reply at once claiming the same slot. Somebody’s licence lapsed two months ago and nobody noticed. The rota lives in a spreadsheet, the claims live in WhatsApp, and the qualifications live in a folder someone has to remember to check.
Workforce puts all three in one place. Shifts are published once to a single list everyone reads from. Staff apply for what suits them rather than waiting to be assigned. The qualification, capacity and clash checks run server side before an application is accepted, so the rules are enforced rather than remembered.
What it does
- Staff self-register, and an admin approves the account before it goes active.
- Browse and apply for open shifts, with guards on capacity, qualifications and clashes.
- Multi-day events collapse into a single card. The detail page lists the days and the staff member applies for whichever ones they want.
- Shift detail carries a photo, the venue description, the straight-line distance from where the staff member is standing, and a comment thread.
- An availability calendar where tapping a day cycles it through available, unavailable and unset.
- A profile holding details, qualifications with expiry tracking, an emergency contact and password.
- Admin tools to approve accounts, confirm bookings, verify qualifications, publish and edit shifts, and manage venues.
- Device features: camera capture, push notifications and passkey sign-in.
Decisions worth explaining
Push notifications carry no payload
The service worker is woken by a bare push, then fetches the content from the app’s own API using the user’s cookies. That means no aes128gcm encryption to implement, and notification text never passes through the push service at all. The trade is one extra round trip before the notification appears.
Distance is calculated in the browser
The staff member’s position never leaves their device. There is no geocoding service to pay for, and the permission prompt appears next to the thing that needs it. It is a straight-line haversine figure and it says so, with a Maps link covering real travel distance. Venues without coordinates show no figure rather than a wrong one.
Multi-day runs are derived, not stored
A three-day event stays three rows, one per day, so each day keeps its own slots, rate, role and bookings. The grouping is computed at read time from title and venue. Nothing in the schema changed, admin still edits days individually, and retitling one day correctly splits it out of the run. The cost is that two unrelated jobs sharing a title at the same venue would merge, which is fixed by renaming one.
Capacity is admin-only
Staff see whether a shift is open, full, or already theirs. They do not see slot counts or how many places are left. The counts still drive the server-side guards, so nothing about enforcement changes, but a rota is not a countdown and showing the numbers invites people to treat it like one.
Text size scales the whole interface
A four-step control in the profile, applied before first paint. It uses zoom rather than font sizes alone, because every size in the stylesheet is in pixels and controls carry fixed heights. Growing the text inside them would clip descenders and burst pills at exactly the sizes someone actually needs.
Uploads are re-encoded server side
Images are processed rather than stored as received, which strips EXIF and anything hidden in a file claiming to be a photograph. The uploads directory denies script execution.
Booking guards live in one file
The list and the detail page both offer an Apply button, and both call the same guard code. Two copies of a capacity check drift apart eventually, and the day they do is the day two people are confirmed for one slot.
How it is put together
PHP 8 with PDO, MySQL, Tailwind and plain JavaScript. No build step, which keeps deployment to a file copy. It shares the WordPress database using a table prefix of its own and reads credentials from the existing configuration at runtime rather than duplicating them.
Sessions use a random token in an HttpOnly Secure cookie rather than PHP sessions, so the same authentication works for the API endpoints and inside an Android wrapper. CSRF tokens follow the same pattern, in their own cookie, after PHP session cookies proved unreliable on some mobile browsers and produced spurious invalid-token errors.
Thirteen tables cover staff, sessions, qualifications, emergency contacts, venues, shifts, bookings, availability, comments, push subscriptions, notifications, passkey credentials and an admin audit log.
Not built yet
Pay and payslips, leave requests, a document library, meeting attendance, and shift check-in and check-out. The Android build needs icon generation, a wrapper build against the manifest, and a digital asset links file at the domain root before the wrapper drops the browser address bar.
Full write-up: vincentwap.co.uk/app/workforce-overview

Leave a Reply