Javis OS
Javis OS는 기업가를 돕는 에이전시 AI로, 업무를 지원하고 함께 성장합니다
공정하고 예측 기반 로타, 팀이 쓰는 직원 앱, 관리자를 위한 복지 신호, 급여처럼 인건비 계산
Fair, forecast-led rotas, a staff app your team will use, wellbeing signals for managers, and labour costed like payroll. By Preshift and Williams, Stanley & Co.
비즈니스TypeScript · CSSGitHub @AAtithe온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기rota platform
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗The rota built by finance people who have run hospitality floors. A joint venture between Williams, Stanley & Co. and Preshift.
Every rota tool on the market answers "who is working when". Preshift answers the question an operator actually cares about: "what will this week's labour cost me against what I will take, and is it legal". It builds the rota from the sales forecast, costs every shift the way payroll will, blocks anything that breaks UK employment law, and hands finance a ready-to-post labour accrual so the P&L carries true labour before payroll has even run.
A one-page card for the team. Read it in two minutes, then open the app and try each item once.
Past cards live in docs/WHATS_NEW.md.
| For | What changed | Why it matters |
|---|---|---|
| GMs | Coaching for this draft: while a week is a draft, a panel under the decisions reads the site's last four weeks of rotas, takings and sales forecast. Again marks a mistake repeated (a day over the labour target again after running over before, a weekday that keeps taking 10% or more above or below forecast, a role stacked over demand on the same hours as before). Tip marks something to fix in this draft (three chefs on at once when the forecast needs two, a role all starting at the same minute, a day with more hours than its usual sales per labour hour needs). Keep doing marks a day pulled back inside target. Gone once the week is published; switched with Coach. | The same mistakes cost the same money every week until someone points at them while the rota can still change. This points at them before publish, not at month end. |
| GMs | By person: copy and paste a shift (Copy in the editor, or Ctrl+C), shift types set once per site and picked from Build from, cover against demand on each day heading, live while dragging, and key dates along the top of every rota view. | The rota is written faster, and the cover gap is seen on the grid it is fixed on. |
| GMs | Day notes: several per day by type and time, weekly if needed, and told to the team in the staff app and on the kiosk when marked for them. | The big booking, the delivery and the road closure reach the people working that day, not just the manager who wrote them down. |
| GMs | Weddings, buyouts and functions on the demand, from Tripleseat, SevenRooms or by hand on Calendar. Confirmed events add their own team by guest ratio; exclusive hires take public trade out; provisional ones move nothing. | An event day is staffed for the event, not guessed from a normal Saturday, and it does not distort what the forecast learns. |
| GMs | A week that is over reads as traded: takings, clocked labour, labour % and the budget on what was taken, with the full P&L setting clocked labour against the rota by day. | The review of last week starts from what happened, not from the plan. |
| Everyone | Ask Shifty is the one place to ask (Ctrl K); the Tell Preshift bar has gone. A narrated walkthrough opens from How to, Support and the staff app. | One place for questions, and a how do I answered by seeing it done. |
The test for this week: Before you publish next week, open Coaching for this draft at your site and deal with every Again card: fix it, or write down why it stays. At the weekly review each GM names one card they acted on and what it changed on the labour line.
| Area | What it does | Where |
|---|---|---|
| Demand-led build | Turns hourly forecast sales and covers into headcount per role (sales per labour hour or covers per person, with floors and caps), then fills the gaps peak-first, cheapest legal person first, and trims overstaffed edges. Gaps it cannot fill legally are reported as a hiring or cross-training signal. | src/engine/demand.ts, src/engine/autoschedule.ts |
| True cost per shift | Gross pay, holiday accrual (12.07%), employer NI at 15% over the £96 weekly threshold, auto-enrolment pension on qualifying earnings, guaranteed contracted hours. On-costs are worked on the whole week then spread over shifts, so the rota reconciles to payroll. | src/engine/cost.ts, src/engine/ukRates.ts |
| Marginal cost | The shift editor shows what a shift adds to the week, not a naive rate x hours. Someone on guaranteed hours costs nothing extra; someone already over the NI threshold costs 15% more than the headline rate. | src/ui/ShiftEditor.tsx |
| UK compliance | Working Time Regulations breaks, 11 hour daily rest (across week boundaries), weekly rest, 48 hour average without opt-out, young worker limits (8h/day, 40h/week, 30 minute break, 12h rest, never between midnight and 04:00), night workers (the 8 hours in 24 average and the health assessment offer), minimum wage by age, double booking, leave, availability, training. Blocking issues stop publication. | src/engine/compliance.ts |
| Live labour % | Forecast sales, loaded labour, labour % against the site target, pound variance, SPLH, by day and week. | src/engine/metrics.ts, src/ui/PnlStrip.tsx |
| People | Employee records: details, employment dates, roles, contract and pay, right to work with the copies of the documents checked, availability and leave. Each record is checked before it reaches the rota: right to work missing or expired, checked with no copy held or no date recorded, pay below the legal minimum, upcoming birthdays that move the minimum wage band, salaried pay that falls below minimum wage at maximum hours, under 18 opt-outs, auto-enrolment eligibility. Starters and leavers are pro-rated in cost, kept off the auto-build outside their dates, and exported for payroll. | src/engine/employees.ts, src/ui/PeopleView.tsx, src/ui/EmployeeEditor.tsx, server/routes/documents.ts |
| Holiday | Entitlement balances per person for each company's leave year: 5.6 weeks (or the contract) earned evenly for fixed hours staff, 12.07% of hours worked for irregular hours staff (paid travel between sites included), from approved clocked time where it exists and the rota otherwise. Taken and booked leave, carry over, opening balances at go-live, and per-day hours. Flags overbooking, unused leave near year end, and leavers' final holiday pay. The untaken balance is valued as the holiday pay provision, with employer NI, to match the balance sheet accrual. |
The product is meant to read like management accounts, not like a rota app, in Williams, Stanley & Co. colours: WS navy (#003359) on white over a pale blue gradient, ledger rules and double rules under totals, set in Geist with Geist Mono for figures and labels. Beyond the navy, colour is reserved for money and the law: teal inside budget, coral over budget or blocked, gold to review, each checked for colour-blind separation and always shown with a label or mark. Roles and departments are told apart by position and label, never by colour, so anything coloured on screen is something to act on.
The detail behind the feature list. Every figure here is in the code; if they disagree, the code is right and this section is out of date.
src/engine/cost.ts, src/engine/ukRates.ts)| Parameter | 2026/27 value |
|---|---|
| National Living Wage (21+) | £12.71 an hour |
| NMW 18 to 20 | £10.85 an hour |
| NMW 16 to 17 and apprentice rate | £8.00 an hour |
| Employer NI | 15% above £96 a week (secondary threshold) |
| Auto-enrolment pension | 3% employer on qualifying earnings £120 to £967 a week; trigger £192 a week; ages 22 to 66 |
| Holiday accrual | 12.07% of hours worked for hourly and irregular-hours staff (5.6 / 46.4) |
| Employment Allowance | £10,500 a year, one claim per connected group |
On-costs are calculated on the whole week, then spread across that week's shifts, so the rota total reconciles to payroll. The shift editor shows marginal cost: what the shift adds to the week.
src/engine/compliance.ts)Blocking issues stop publication and are refused by the assistant. Warnings are shown and allowed.
| Rule | Severity |
|---|---|
| Minimum wage for the person's age band | Block |
| Rest breaks under the Working Time Regulations; 30 minutes over 4.5 hours for under 18s | Block |
| Under 18s: 8 hours a day, 40 hours a week | Block |
| Under 18s between midnight and 04:00, which the hospitality exemption never allows | Block |
| Double booking across any site, on leave, outside employment dates, right to work missing or expired | Block |
| 11 hours daily rest, checked across week boundaries; 12 hours for under 18s | Block |
| Weekly rest: one clear day in the rota week, two for under 18s | Block |
| Not enough time to travel between two sites' shifts, by the TfL journey time or estimate | Block |
| Under 18 working between midnight and 04:00, which the hospitality exemption never covers | Block |
| Under 18 working between 22:00 and 06:00 (allowed in hospitality only when supervised, with compensatory rest) | Warn |
| Outside stated availability, or not trained for the role | Warn |
| Move between sites within 10 minutes of the journey time, or sites with no postcode set | Warn |
| Over 48 hours without an opt-out (must average 48 or less over 17 weeks) | Warn |
| Over the person's own maximum weekly hours | Warn |
| Below contracted hours on a full week (the gap is paid regardless) | Warn |
| Night worker opted out of the 48 hour week averaging over 8 hours in 24 across 17 weeks (the opt-out does not cover night work) | Warn |
| Night worker with no current health assessment offer on the personnel file | Warn |
Rest breaches, and only rest breaches, can be overridden by a person who may publish the rota at that site, with a reason of at
least 15 characters (why, and the compensatory rest the person gets). The breach then publishes as a
warning carrying the manager's name and reason. An override covers that breach exactly as it stood:
move either shift, or change the days worked, and it blocks again. System keys cannot override, the
auto-build and the rota assistant never rely on one, and every override, withdrawal and publish
through one is in the audit log. The Sites view shows overrides per site for the selected week and the
three before it, with a group total and a "Recurring" tag on any site that overrode in three of those
four weeks (overrideTrend in src/engine/compliance.ts). A daily rest override counts at the site
of the shift worked short of rest, a weekly one at the person's home site. Routes: GET, POST /v1/rota/overrides, DELETE /v1/rota/overrides/:id.
src/engine/demand.ts, src/engine/autoschedule.ts)PUT /v1/forecast.src/ai/, server/routes/assistant.ts)src/engine/journal.ts)basis=rota|actual|auto).src/engine/messaging.ts, server/messaging.ts, server/routes/messages.ts)| Item | Specification |
|---|---|
| Record | Every message, announcement and rota notice is stored in Preshift, one direct thread per person, with read times. Channels only carry it. Kept 13 months by default, longer up to 72 where the client sets it in its retention periods under Privacy, at most 5,000 per organisation, oldest first out. |
| Who | Managers with rota.edit or team.manage at the person's home site read and send; announcements need rota.publish at the site. A key with no person behind it is refused. Staff see their own thread and announcements to them only, never delivery details. |
| Rota notices | Sent on publish to each person whose published shifts for the week differ from what they were last told, including being taken off. A channel failing never stops the publish. |
| Staff channels | WhatsApp (Meta Cloud API v21), text (Twilio), email (Resend). One notice goes by the first of the person's choices that works. WhatsApp and text need the person's own number and consent, given in the staff app; a new number needs consent again; STOP on WhatsApp withdraws it. |
Free text within 24 hours of the person's last message, otherwise the approved template named in Settings with the message as its one body variable. Replies need Meta's x-hub-signature-256 with the app secret, are taken once by message id, text only, and only from a number someone agreed from. Delivery and read reports update the message. | |
| Quiet hours | Default 22:00 to 07:00 London time, per person. Non-urgent messages are held and sent by the quarter-hourly run (/internal/webhooks, or the daily /internal/dispatch) once the hours end. Urgent ones go at once. |
| Team channels | Slack incoming webhooks (hooks.slack.com) and Teams Workflows webhooks (*.logic.azure.com, *.api.powerplatform.com, *.webhook.office.com) per site or for every site. Notices name the person and site with a link, never message text. |
| Keys | Saved under Settings in the settings store, never in organisation data; shown by their last four characters. Refused in the demo. |
| Item | Specification |
|---|---|
| Phone tags | A secure NFC tag (NTAG 424 DNA) at a site. Staff touch it with their own phone, which opens the staff app at an address the tag writes on every read: its serial and read counter encrypted under one key, and a MAC under another (Secure Unique NFC, NXP AN12196). The staff session proves who and the tag proves where, so no location is needed. A read is accepted once and only while its counter is above the last accepted, so a copied link is refused once anyone has tapped since. A tag's first read fixes its serial; another chip with the same keys is refused. Only at the person's shift, home or cover sites. Keys shown once when the tag is added and kept in the settings store. Entries carry method tag. |
| Geofence | Per site. Fix accuracy is allowed on top of the radius, up to the radius again. Fixes worse than 250 m are refused. |
| Card taps | A reader fixed on site under its own API key is not location checked, and may send the time of a tap it held offline. A card read on a signed-in tablet or phone needs the device inside the geofence, like any device clock. Repeat reads of one tap are ignored. A tap during a break ends the break. |
| Clock times | A clock from a person's screen is timed by the server: a time sent more than two minutes off is refused (time_set_by_server), so no one can clock a colleague in for a shift they never started. Missed clocks go in as manual entries for approval. |
| Personal details | Date of birth, right to work evidence and expiry, holiday record and card number go only to those who manage the team or read HR at the person's site. Others, such as a supervisor, see whether someone is under 18 and whether their right to work is checked, with an expiry only when it falls within the next eight weeks of rota. |
| Phone clocking | By location: requires the site's geofence, for clock-in and clock-out, and is refused at a site with no fence. By phone tag: no location needed. |
| Breaks | Not location checked. Recorded as {start, end} and set unpaid minutes at clock-out. Clocking out ends a running break. |
| Approval | Manager approval, rota against clock by person, period locked once sent to payroll. |
| Pay periods | Per company payroll: weekly, fortnightly, four_weekly, monthly, last_friday. Default weekly, Monday to Sunday, paid Friday, due Tuesday 12:00. Paydays and cutoffs on a non-working day move to the working day before; a cutoff that would land inside the period moves to the first working day after it. Lock rota needs at every site the period reaches; time cards need the rota locked and ; passing needs , the period ended and every card approved. See . |
| Item | Specification |
|---|---|
| Manager sessions | 12 hours, HttpOnly cookie, checked against the database on every request. |
| Staff sessions | 30 days, separate signing key, never accepted by the manager API. |
| Passwords | scrypt hashes. Five wrong attempts lock the account for 15 minutes. |
| Password reset | Single-use email link valid 30 minutes, at most three emails an hour per account, token after #. Admin-issued link valid 24 hours. |
| Staff invite | One-time link valid 7 days; a new invite resets a forgotten staff password. |
| Roles | Admin and member. At least one admin per organisation; nobody can remove themselves. |
| API | Scoped API keys, per-organisation tenancy, OpenAPI docs at /docs. TeamPay webhooks HMAC signed. |
| Item | Specification |
|---|---|
| Stack | TypeScript throughout. React 18 and Vite for the web and staff apps; Fastify 4 for the API. |
| Storage | Postgres (Neon) via PRESHIFT_DATABASE_URL; in memory without it. Never reads DATABASE_URL. |
| Payroll | TeamPay, through an idempotent, retrying outbox; daily Vercel Cron retry. |
| Hosting | Vercel Build Output API: marketing site, one Node API function, app served to signed-in people only. |
| Quality | 1360 tests across 138 files (clocking by phone tag, the night average from the first week, day totals by trading day, a new sign-up's first steps, night workers and their health assessment offers, trading round the clock and the cut call past midnight, open shifts kept out of locked pay periods, pay calendars and the pay period close, customers, Xero journal posting, open shifts offered cheapest first and approved automatically, shifts going in rounds, messaging and channels, under 18s at night, message retention, signing the written statement and handbook in the staff app, tips and payslips in the staff app, tips through payroll, pay premiums, escalation and Seen it, shift reminders, no-shows and adoption, labour cash calendar, phone push, wellbeing and Hospitality Rewards, pricing, support desk, staff requests, swaps, open shifts, notifications and the managers' morning email, engine, sign-up and billing, staff login close review, forecasting from takings, forecast accuracy, staff app invitation emails, clocked against rota, cut call, compliance pricing, tronc, prime cost, retention, demo trading history, clients and onboarding, access and permissions, rota assistant and chat, travel, reports, right to work documents, HR, GDPR, UI rules, assistant, API, clocking, breaks, staff, payroll, privacy, demo, Postgres store, reset, MCP connector). CI typechecks and tests every pull request and every push to main. |
npm install
npm run dev # API on :8787, web app on http://localhost:5173, API docs on /docs
npm test # engine, UI rules, assistant and API tests
npm run build # typecheck and production build
npm run test:e2e # browser checks of the management bar and iPad touch (Playwright), against a local dev server
To run the browser checks against a deployed site instead, set PRESHIFT_E2E_URL to its address and
PRESHIFT_E2E_EMAIL and PRESHIFT_E2E_PASSWORD to an admin account there, or set PRESHIFT_E2E_DEMO=1
to go in through its public demo with no account (for example
PRESHIFT_E2E_URL=https://preshift.williamsstanley.co PRESHIFT_E2E_DEMO=1 npm run test:e2e). They only navigate; they
never build, clear, publish or save.
The Live checks workflow (.github/workflows/live.yml) does exactly that against
https://preshift.williamsstanley.co after every successful production deployment, nightly, and on
demand from the Actions tab. A failure opens a "Live checks failing" issue, or adds to the open one,
and the issue closes itself when the checks pass again. Set the PRESHIFT_LIVE_URL repository
variable to point it somewhere else.
Locally, sign in as [email protected] with password preshift-dev-password. That account only
exists when the API runs without a database, and is never created on the live system. To try the
labour programme's sign-offs, act as Sam Okoro, the WS client partner, from the demo people. The demo carries twelve
weeks of till history, so the forecast model and accuracy work straight away.
src/ai/evals/cases.ts holds 16 realistic requests on the demo group, from "build the week" to
"put Chloe on while she is on leave" (which must be refused) and "check, but change nothing". Each
is graded on the rota the assistant leaves behind, with separate pass/fail checks: legal, history
untouched, cover kept, labour at target, the instruction met, and a short plain note. Every case
has a reference solution that must pass and a do-nothing answer that must fail unless doing nothing
is right; npm test proves both, so the grader is trusted before any money is spent.
To run it against Claude, on a machine with a Claude API key (ANTHROPIC_API_KEY or an
ant auth login profile; this is a developer tool, separate from the key the app keeps in Settings):
npm run eval:assistant -- --reps 1 # every case once, roughly $5 to $15
npm run eval:assistant -- --cases follow-up --reps 3 # one case, three times
It writes eval-results/<time>/ with summary.md (pass rates with 95% intervals, per case and per
check, and the cost), results.jsonl, errors.jsonl (API errors, timeouts and wrong-model runs,
never counted as wrong answers) and every conversation under transcripts/. Run it before any change
to the assistant's prompt, tools or model, and compare the summaries.
Everything about access is managed inside the app, under Settings. Nothing is configured in environment variables.
Accounts, password hashes (scrypt), the session signing key and the Claude key all live in the Preshift database. Sessions last 12 hours in an HttpOnly cookie and are checked against the database on every request, so suspending someone or resetting their password takes effect at once. Five wrong passwords lock an account for 15 minutes. The web app carries no API key: it calls the API with the session. On Vercel the app's files are served by the API function only to signed-in people.
An account is how a person signs in; what they may do is their access record on the Access tab, found by the same email. People are invited there, and set their own password from the invitation link (seven days, single use) on the sign-in page. "Reset password" on the Access tab ends the old password and every session at once, and sends a link, valid a day, to choose a new one. Nobody ever chooses or learns another person's password. Someone who signs in with an account but no access record keeps full access in the account's own organisation, as every account had before roles, and the Access overview lists them as Fix now until they have one.
One person can work in several organisations, as a Williams, Stanley & Co. manager does across clients: one account, one password, one authenticator, and an access record in each organisation. Each organisation controls only its own record of them.
Roles that touch pay, payroll, the journal, settings or access need an authenticator app. After
signing in, the app asks for its 6-digit code before switching those rights on for the session, and
walks anyone without one through setting it up (QR code, then ten single-use recovery codes, shown
once). The first owner sets theirs up before inviting anyone. Authenticator secrets are encrypted
with a key kept in the database, or, stronger, with PRESHIFT_SECRET_KEY (64 hex characters,
openssl rand -hex 32) so the key never sits in the database it protects; never change it once
people have enrolled. The demo's enrolled people share the published secret
JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP locally, never in production.
Email goes through Resend once someone with settings rights saves a Resend key, a from address on a
verified domain and the app's address under Settings. Invitations and resets are then emailed to the
person and the link is never shown to whoever sent it, so nobody else can set their password; until
then the link is shown once, to hand over in person. Two rules stop a reset becoming a takeover. Resetting
someone's password or authenticator, or sending them a fresh invitation, needs the authority to give them
every role they hold: an operations director can reset a GM, never the people manager or the owner, and
Williams, Stanley & Co. never changes the accounts of a client's own people. And the link for anyone
holding rights that move money or reach confidential records (pay, payroll, locking, the journal, access,
settings, cases, staff data requests) only ever goes to their own email, never handed over, even to the
owner. People who forget their password use "Forgot your
password?" on the sign-in page: a single-use link, valid for 30 minutes, emailed only once email is set
up. Its reply never says whether an account exists, and one account gets at most three emails an hour;
invitations are capped at three sends an hour too. Every link's token sits after #, so it never
reaches a server log, and only its hash is kept.
What became of each email shows on the Access tab against the person: sent, delivered, delayed,
bounced, marked as spam or failed. Resend reports it to a webhook at
/v1/integrations/resend/webhook/<organisation>; Settings shows the address to give Resend and takes
its signing secret (whsec_...), and a report without a valid signature from the last five minutes
is refused. Without the webhook, "Check now" on the person asks Resend. A bounce, a spam complaint or
a failure goes on the Access overview with the receiving server's reason until it stops mattering
(the invitation is accepted or withdrawn, or the person signs in after the reset), and the person's
panel offers to hand a new link over instead of emailing it again. Preshift keeps who each email went
to and its reports for 180 days, never its content or link, and does not track opens or clicks.
Known limit: a reset link is built on the home organisation's saved app address and sent through its Resend account, so the home organisation's admins are trusted with the account; a person who works for several clients is best made at home in the organisation they belong to.
The demo runs a small group: West End Dining Ltd (Marylebone and Fitzrovia) and East Canteen &
Kitchen Ltd (Shoreditch), with realistic teams, rates and forecasts, against a TeamPay simulator.
A few people cover sister sites, and one Shoreditch bartender covers Marylebone, which is a
cross-company recharge. Choose an entity and site in the top bar, or leave both on "All" for the
group view, and press "Build" to generate the week. The rota assistant and the Payroll tab work on
one site, so they appear once a single site is selected. On the Payroll tab: set people up in
TeamPay, simulate clocking, approve, send hours, run payroll. Set the TEAMPAY_* variables to
connect to real TeamPay and PRESHIFT_DATABASE_URL for Postgres; see docs/API.md, which also
covers the Vercel deployment.
Organisation, then Set-up, holds the business the rota runs on. It needs settings.manage, and so an
authenticator (src/ui/SetupView.tsx, rules in
src/engine/setup.ts, routes in server/routes/setup.ts):
A new live system starts on the sample group. Admins see a banner saying so, and set-up offers "Remove sample data" while nothing real exists: no own companies, sites or people, nothing in payroll, no clocked time, no staff logins, no HR records or data requests, and nobody real with access tied to a sample site. Clearing also removes the sample's fictional people and HR files. After that it is refused, and the sample can only be edited. Each site's typical week forecasts the next eight weeks, and the build fills any later week on demand. Figures from a sales system replace it week by week and are never overwritten. A company, site or role that people, rotas or clocked time still use cannot be removed.
Sites can trade past midnight, closing as late as opening time the next day, or open round the clock. The trading day then runs on: a bar open 16:00 to 02:00 has Friday's demand, staffing and forecast run to 02:00, with the hours after midnight stored against Saturday's date (as a till or sales feed reports them) and read back as Friday night. The auto-build rosters shifts that finish at close, and each night's forecast still adds back to the day. Every daily total does the same: labour % by day, the rota's day P&L, the timeline's sales curve, takings entry, scorecards and forecast accuracy count the hours after midnight to the night they belong to, as the shifts already are. A day's takings keyed in or sent by the till are taken as the till's trading day; only hourly till figures are moved.
A business can start on Preshift without a set-up call. Switch it on with PRESHIFT_SIGNUP=1 once the
first organisation has email set up under Settings, since its Resend account sends the confirmation
links. The sign-in page then offers "Start a free trial", and the marketing site links to it.
PRESHIFT_TRIAL_DAYS, default 14), and they
land in Set-up. Like any owner, they set up an authenticator before settings and billing open.Published on the marketing site, charged by Stripe and quoted on the Billing page from one source,
src/engine/pricing.ts:
| Per site a month, before VAT | |
|---|---|
| Sites 1 to 5 | £119 |
| Sites 6 to 20 | £99 |
| More than 20 sites | Group plan, agreed with us |
To change a price, edit src/engine/pricing.ts, bump its version, update the PRICING block in
site/main.js and the figures in the page copy (server/pricing.test.ts fails until they agree),
then run npm run stripe:prices and set the new price ids. Existing subscribers stay on the price
they bought until moved.
STRIPE_SECRET_KEY=sk_test_... npm run stripe:prices creates the product, the monthly and yearly
graduated prices and the founding coupon from src/engine/pricing.ts (add -- --dry-run to see what
it would do; run again safely, it reuses what exists). Set STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET, and the STRIPE_PRICE_ID and STRIPE_PRICE_ID_ANNUAL it prints, and point a
Stripe webhook at
/v1/integrations/stripe/webhook (events listed in docs/API.md). Organisation, then Billing:
| Standing | When | What works |
|---|---|---|
| Billed by WS | No billing record (the first organisation and every WS client), or moved to a WS engagement and past the date it took effect | Everything, always |
| Free trial | Trial days left and no subscription in force | Everything |
| Subscribed | Stripe reports active or trialing | Everything |
| Payment failed | Stripe reports past_due and is retrying the card | Everything, with a banner |
| Lapsed | Trial over with no subscription, or cancelled, unpaid or expired | Every read, Billing, your profile and authenticator, GDPR requests. Every other change is refused with 402 billing_required until someone pays |
Nothing is paused on a system without Stripe configured: nobody is cut off who has no way to pay. Stripe's events are believed only with its signature over the exact bytes, from the last five minutes, and are applied to the organisation Preshift recorded when it made the Stripe customer, never one named in the event. Late or out-of-order events cannot undo a newer status. Preshift keeps the customer and subscription ids, status, sites paid for and the last invoice's amount and link: never a card. Demo sandboxes are never billed.
Organisation, then Customers, is the commercial view of self sign-up: who signed up, who reached a published rota, who converted, what that is worth and who needs a call this week. It lists every organisation with a billing record (self sign-ups), never those billed under a WS engagement, which stay under Clients.
ws_customers, permission customers.view), with an
authenticator, working in the system's own organisation, the first one. A WS client partner there
grants it on the Access tab. Keep it to the fewest people: it is the one screen that shows every
customer at once. Granted inside a customer's organisation it shows nothing, so no client can grant
itself a view of the others. Never an API key.src/engine/pricing.ts; founding and promotion discounts show on
Stripe's invoices. Trial-to-paid is the share of customers whose trial has ended who are paying now.A customer can start on a card and be taken on by the firm, or leave an engagement and carry on by card. Both moves are recorded with who made them and when, in the customer's audit trail and the firm's, and show on Clients, Customers and the customer's own Billing page.
customers.billing, with an authenticator). Stripe is told only to stop renewing the subscription
(cancel_at_period_end): nothing is charged or refunded, so the customer never pays twice. They keep
the period already paid for; from its end WS bills them, they show as Billed by WS, they leave MRR and
nothing pauses them. With no subscription in force (a trial, or lapsed) the move takes effect at once.
While it is pending, the customer's Manage billing is closed so the ending subscription cannot be
renewed; once it has happened, it opens again for past VAT invoices. The move counts towards trial
to paid, as a customer won, and Customers counts every customer taken on by the firm.The live system also hosts a public demo, kept apart from every customer. With PRESHIFT_DEMO=1,
the marketing site's "Try the live demo" button opens /demo. Pressing "Start the demo" gives the
visitor their own private copy of the demo group (a sandbox organisation) and signs them in with
its full access. They can use the product as a customer would: build from forecast, edit shifts,
compliance, people, HR, clock in at the site, payroll against the TeamPay simulator, the Close
journals, GDPR, and the access controls on a fictional team, none of whom has an account.
Every sandbox is wiped at midnight UK time.
Segregation and security controls, all in server/demo.ts and server/sandbox.ts:
sandbox-...), which only "Start the demo" creates. Its
visitor account has an .invalid address and a random password nobody knows. Password sign-in,
password changes and reset links all refuse sandbox accounts, demo accounts never count towards
first-run owner setup, and integration keys cannot name a sandbox. A sandbox is reached only by
its own visitor account, which works nowhere else, and no invitation into a sandbox can be
accepted, so demo and customer accounts never mix.true in SANDBOX_ROUTES. Anything
else is refused, including any route added later: a test fails until each new API route is
listed as allowed or refused, so every feature gets a deliberate decision about the demo.
Refused: anything that issues an invitation or reset link or touches an authenticator, settings,
the Claude and email keys, right to work copies (no real passport should go into a public demo),
postcode and TfL lookups (they run on the live system's quota), and staff app logins (their
emails are unique system-wide, so a visitor could otherwise find out whether an address is a
customer's). The guard runs before a request body is even validated./demo creates nothing; only a same-site form post does. Up to 500
sandboxes a day in total and 10 per visitor address (kept only as a keyed hash). The rota
assistant is off unless PRESHIFT_DEMO_CLAUDE_KEY is set, then capped at 40 Claude calls per
sandbox and 1,500 a day; set a monthly spend limit on that key in the Claude Console as well./internal/demo-reset at 23:05 and 00:05 UTC (just after midnight in summer and in
winter), and visitors starting the demo trigger it too. It deletes each sandbox with its visitor,
staff logins, documents and counters. Needs CRON_SECRET.PRESHIFT_DEMO and redeploy: nothing new starts, existing visitors are
refused on their next request, and the reset still clears what they left.Starting the demo replaces any Preshift session in that browser, which the start page says.
Locally: npm run dev:demo, then open http://localhost:5173/demo. The dev account still works.
npm run build:vercel (set in vercel.json) writes .vercel/output with the Build Output API:
| Path | What |
|---|---|
/ | Marketing site, from site/ |
/v1, /docs, /health | The API, bundled into one Node function from server/vercel.ts |
/login, /auth/* | Sign-in, first-run owner setup and sign-out, served by the API function |
/app/ | Web app, served by the API function to signed-in people only (server/routes/web.ts) |
/api/anthropic/v1/messages | The rota assistant's route to Claude, using the key saved in Settings |
/internal/dispatch | Daily Vercel Cron retry of payroll changes TeamPay has not confirmed (needs CRON_SECRET) |
/internal/webhooks | Vercel Cron every 15 minutes: webhook deliveries waiting on a retry, and messages held for staff quiet hours (needs CRON_SECRET, and a Vercel plan that allows cron jobs more often than daily) |
/demo, /demo/start | The public demo's start page and the form post that starts a sandbox (with PRESHIFT_DEMO=1) |
/internal/demo-reset | The demo's midnight reset, Vercel Cron at 23:05 and 00:05 UTC (needs CRON_SECRET) |
/internal/digest | Managers' morning email of staff requests, Vercel Cron at 06:35 UTC, once a day per organisation with email set up (needs CRON_SECRET) |
/internal/reminders | Shift reminders and not-clocked-in alerts, Vercel Cron every 5 minutes, each sent once (needs CRON_SECRET, and a Vercel plan that runs cron jobs more often than daily) |
/v1/integrations/stripe/webhook | Stripe's signed billing events (with STRIPE_* set) |
Set PRESHIFT_DATABASE_URL to a Neon database that belongs to Preshift and the API keeps its data
in Postgres across cold starts and instances. Without it, data is held in memory and resets when
the function cold starts: fine for a demo, not for customers. Preshift never reads the generic
DATABASE_URL, so it cannot write into another system's database by accident. The hosted API has
no public key: people reach it by signing in, and integrations use a key an admin makes under Settings.
Set PRESHIFT_DOCUMENT_KEY (openssl rand -base64 32) before anyone uploads a right to work copy.
Copies are encrypted with it and it is never stored in the database; without it, a database
deployment refuses uploads and views. Keep a copy of the key outside Vercel: losing it loses every
stored copy. Every view of a copy is logged, and admins see the log on the employee record.
src/engine Pure TypeScript domain logic, shared by API, browser and assistant. Fully unit tested.
src/ai The rota assistant: tools over the engine, and the Claude loop that drives them.
server The Preshift API: routes, the MCP connector, sign-in, people and settings, API keys, tenancy, Postgres store, payroll outbox, TeamPay client and simulator.
src/ui React web app, a client of the API.
site Marketing site (static, no build step).
scripts Local dev runner and the Vercel build.
docs API and integration guide, strategy and roadmap.
The engine runs in both places: the server is the source of truth, and the browser runs the same code for instant cost and compliance feedback while editing.
All UK parameters live in src/engine/ukRates.ts and are set for 2026/27. They change every
April and must be checked against GOV.UK before each tax year.
See docs/STRATEGY.md for the competitive position, JV model and roadmap.
src/engine/holiday.ts, src/ui/HolidayView.tsx |
| Onboarding | One checklist per new person, from 60 days before their first day to 90 days after it, in the order a GM should work it: right to work checked, with a copy and the date; roles, date of birth and pay checked against the minimum wage band; written statement issued and signed; NI number; address and emergency contact; probation end date; the training their role requires; set up in TeamPay; a clock card or the staff app. Legal steps are due by day one and block shifts until done, the rest fall due in the first week or month and turn late after. Steps on the personnel file or pay are hidden, not reported missing, for anyone who may not see them. CSV export. | src/engine/onboarding.ts, src/ui/OnboardingView.tsx |
| Wellbeing | One screen per scope under People. Signals per person over the last four weeks of worked time (approved clocked time, else the rota) and the fortnight ahead: weeks over 48 hours or averaging over 45, six or seven days in a row across rota weeks, turnarounds under 12 hours, late finishes, no full weekend off in four, shifts without the full rest break, no leave in three months and none booked, next week's hours cut by 40% or more for hourly staff (only against a week already built), three short sickness spells in 26 weeks for those who read HR (protected absence left out), and a chat asked for from the staff app. Each says what was found and what to do. A safe way home lists late finishes ahead, under 18s after 22:00 always. Staff meals by site with the s317 note. Milestones (work anniversaries, birthdays where the reader sees dates of birth, probation passed) with suggested points. Recognition by a person who manages the person's team, never to themselves, up to 250 points an award within the site's monthly budget. Team pulse by site and week, shown only from five answers. Policy (late finish, travel allowance, meals, points budget, support line) under settings. | src/engine/welfare.ts, server/routes/welfare.ts, src/ui/WelfareView.tsx, src/ui/WelfareSettings.tsx, src/staff/StaffApp.tsx |
| Hospitality Rewards | Staff benefits and recognition points. Connected by someone with settings.manage with the API address and key Hospitality Rewards gives the account; the key is checked before it is saved and never shown again. Sync now, then every morning: enrols everyone started and not opted out (name, work email, site, company, start date: never pay, age or HR data), removes leavers and opt-outs, credits waiting points once, by recognition id. A simulator in the demo and development; a demo sandbox can connect to nothing else. An eligibility CSV for accounts without API access. Hospitality Rewards has no published API, so the calls are the contract set out in server/integrations/rewards/hospitalityRewards.ts, to confirm with them before going live. | server/integrations/rewards/, server/routes/welfare.ts |
| HR | The personnel file behind every person, with everything a GM must act on in one list. Contract type, written statement issued and signed (a day one right), probation dates and outcome, fixed-term end, notice (contract against statutory: a week after a month, then a week a year to twelve), NI number, address, emergency contact and line manager. Sickness and other absence with fit notes after seven days, return to work conversations and the Bradford factor over 52 weeks; disability or pregnancy related absence is kept out of triggers. A shift on an absence day is blocked like leave, and the auto-build steers round it. A training matrix of the certificates an EHO, licensing officer or insurer asks for, required by role (food hygiene, allergens, Challenge 25, personal licence), with expiry warnings. Probation and appraisal tracking, and disciplinary, grievance and capability cases with outcomes and live warnings, visible to admins only. Headcount, turnover, average service and sick days per head across any scope, with directory, absence and training CSVs. Rota and payroll keys never see the file: they get absence dates only, without reasons. Every change to an HR record keeps the values it replaced and who made it; a case stays with its person and is closed, never deleted. Nobody changes their own staff record or answers a data request about themselves. Subject access exports include the HR records (cases only for those who handle them); contact erasure removes address and emergency contact; anonymisation removes the NI number, sickness reasons and every HR record; the record of processing lists personnel files and health data. | src/engine/hr.ts, server/routes/hr.ts, src/ui/HrOverview.tsx, src/ui/HrFile.tsx, src/ui/AbsenceView.tsx, src/ui/TrainingView.tsx, src/ui/ReviewsView.tsx |
| Messages and channels | One thread per member of staff with the managers of their site, and announcements to a site's whole team with read receipts; replies always go to the person's own thread, never to everyone. Publishing the rota tells each person whose week changed, in their thread, and again only if it changes (taken off the rota included), so republishing an unchanged week tells nobody. Staff choose in the staff app whether work also reaches them by WhatsApp (Meta's Cloud API, free text inside WhatsApp's 24 hour window, an approved template outside it), text (Twilio) or email (Resend), and give consent there with their own number; nobody can choose for them, a new number needs consent again, and replying STOP on WhatsApp withdraws it. WhatsApp replies come back into the thread, signed by Meta and taken once; photos and voice notes stay in WhatsApp. Quiet hours (22:00 to 07:00 unless the person changes them) hold anything not marked urgent until they end. Slack and Teams get a line and a link for each publish, staff message and announcement, never what staff wrote, and only to the providers' own webhook hosts. Read and written by managers who build the rota or manage the team at the site, never by a system key; kept 13 months unless the client sets longer under Privacy, up to six years; in the subject access export; the number and choices go on erasure; the processors appear in the record of processing once connected. | src/engine/messaging.ts, server/messaging.ts, server/routes/messages.ts, src/ui/MessagesView.tsx, src/ui/MessagingSetup.tsx, src/staff/StaffMessages.tsx |
| Shifts going | One record, the open shift, for every shift put out. Eligible means the claim rule everywhere: trained for the role, set up for the site, employed that day and no blocking rule; warnings such as availability are shown, not hidden. Untargeted (the default, and the API as before): everyone eligible is told and each claim waits for a manager. employeeIds puts it to those people only, cheapest first; rounds asks a few at a time, every 15, 30, 60 or 120 minutes, on the quarter-hourly run, at once when everyone asked has said no or been refused, and everyone left when there is not time for another round; each is checked again when their round comes and passed over with the reason. fill: "first_yes" gives it to the first legal yes, checked again at that moment. fromShiftId takes the shift off its person now; with keepUntilTaken it stays theirs until someone takes it, then moves to them, and is withdrawn if a manager changes it meanwhile. Nobody further down the line hears anything; those asked hear when it has gone. YES on WhatsApp takes the one shift going it can mean. Notices go through the staff feed, by email, or by WhatsApp or text for those who chose them, through quiet hours when urgent. | src/engine/cover.ts, server/cover.ts, server/routes/requests.ts, src/ui/CoverPanel.tsx, src/ui/RequestsView.tsx, src/staff/StaffMessages.tsx, src/staff/SelfService.tsx |
| Shifty | Shifty, Ask Shifty in the bottom right (Ctrl K opens it from anywhere, Escape closes it), is the one place to ask: it answers questions about the rota, labour, the team and holiday from the app's own data, and takes one-line instructions such as "please add a new starter". It runs one agent: it looks things up, and every change (a starter, record changes, a leaver, holiday, an absence, a shift, publishing, asking for cover) comes back as a card the manager confirms before it is carried out through the same actions and API checks as the screens. It enforces minimum wage for age and the rota rules, never sees NI numbers, addresses or absence reasons, hands whole-rota work to the rota assistant, and counts towards the same spend limits. | src/ai/appTools.ts, src/ai/appAgent.ts, src/ui/Shifty.tsx, src/ui/proposals.tsx |
| Rota assistant | The GM tells it what they need in plain English ("Ben is off Saturday, re-cover the bar without going over target") and Claude drafts the week by calling the engine's own tools, for one site, a company's sites or the whole group, then takes follow-up changes to the same draft. Every shift is costed by the payroll model and checked by the compliance engine, so it cannot roster illegally, roster a leaver or someone with no right to work check, or borrow anyone who has not agreed to cover a site. It uses each home team first, then sister sites, then other companies in the group (an intercompany recharge), and leaves shifts that have started alone. From the last four weeks it spreads weekends and closes fairly, respects each person's rota notes, avoids relying on people with recent late arrivals or no-shows for solo shifts, and suggests leave to people with holiday to use before the year end. The GM sees each site's labour % before and after, the changes, cover borrowed, a note and the Claude cost, then applies the draft in one all-or-nothing save that is refused if anyone changed the week meanwhile, with the instructions kept in the audit log. Spending is capped per organisation per month and per person per day in Settings. | src/ai/, src/engine/insights.ts, src/ui/AssistantPanel.tsx, server/assistantLimits.ts |
| Forecast model | Builds each site's forecast from its own hourly till sales: the same weekday and hour over the last eight weeks, recent weeks weighted most, a damped trend, bank holidays from past bank holidays (or the Sunday pattern), and adjustments for weather, events and closures. Overrides keep the model figure underneath. Once traded, accuracy is measured by hour, day and daypart, and each person's overrides are scored against the model. | src/engine/forecast.ts, src/ui/ForecastView.tsx |
| Calendar, weather and events | The UK trading calendar computed for any year: Easter, Mothering Sunday, Shrove Tuesday, Valentine's, Father's Day, Halloween, Bonfire Night, Black Friday, party season, Mad Friday, Christmas to New Year, every bank holiday, and the clock changes (an hour more or less pay for overnight staff). Weather from Open-Meteo and local events from iCal feeds (stadiums, venues, councils), Ticketmaster or entered by hand, fetched every morning. Each site learns from its own history how it trades on each occasion, in warm or wet weather, and around each feed's events, and the forecast allows for it by itself with the evidence shown. Where there is no history, the GM is reminded in time to set an expectation. People's adjustments always win; automatic ones are capped in total. | src/engine/calendar.ts, src/engine/weather.ts, src/engine/events.ts, src/engine/plan.ts, server/integrations/feeds.ts, src/ui/CalendarView.tsx |
| Bookings | Covers on the books, by site and daypart, from a booking system's CSV export (ResDiary, SevenRooms, OpenTable or similar, columns found by name) or pushed by a connector. Only date, time, covers, status and when each was made or cancelled are read: guest details are never stored, and past days are compacted into pace history (covers on the books at set lead times) within two days. Each site learns its pickup from comparable days: walk-ins and late bookings, less no-shows. Expected covers (booked now plus pickup, with the weather applied to the walk-in part) replace the model's covers for that daypart and stand in for occasion, event and weather effects there. People's adjustments still win. Reminders for large parties and for pace well ahead of or behind a normal day. | src/engine/bookings.ts, src/engine/bookingImport.ts, server/routes/bookings.ts |
| School holidays | The usual England and Wales pattern for any school year (half terms, Christmas, Easter, summer), marked as an estimate, until a site has its council's dates from an iCal feed or entered by hand; each confirmed holiday replaces the estimate for that holiday. Each site learns what holidays do to each daypart, weekdays and weekends apart, and the forecast allows for it. A reminder three weeks before each holiday: parents' leave, students' hours. The council feed reader refuses anything doubtful (implausibly long gaps, clubs, early closures) and says what it left out. | src/engine/school.ts, src/ui/CalendarView.tsx |
| Child employment | Anyone still of compulsory school age (by nation, from gov.uk) is held to the child employment rules: 12 hours a week in term time, 2 on school days and Sundays, 5 or 8 on Saturdays by age; 25 or 35 a week in the holidays; never in school hours, before 07:00 or after 19:00; an hour's break after four hours; daily limits counted across the day's shifts. Estimated holidays count as term time: the longer holiday hours apply only once the council's dates are in. Permit and bar work flagged. The auto-build, the weekly moves and the assistant all follow them. | src/engine/compliance.ts |
| Weekly moves | The three changes to a site's week that save most without leaving the floor short or breaking a rule: trim an overstaffed edge, take off a shift that is over demand all through, or give a shift to someone cheaper who is trained and free. Each is priced at its true marginal cost on the person's whole week, applied in one tap, re-checked when taken, and measured against approved time once the week is worked. | src/engine/coach.ts, src/ui/CoachView.tsx |
| Scorecard and ladder | Per site per week: notice given, legal, labour planned and actual, hours over demand, rota-to-payroll gap, forecast error, moves taken. A four-rung ladder (on time and legal, on target, daypart precision, owns the forecast), each rung held three weeks in four, with the next step in one sentence. | src/engine/scorecard.ts |
| Labour programme | The twelve-week WS programme: Baseline, Control, Optimise, Embed. Milestones proved by the site's data or signed off by the WS client partner, targets from the strategy, the saving against a baseline from the accounts, verified only when signed off from the management accounts, and a gain share only on a verified saving. See docs/PROGRAMME.md. | src/engine/programme.ts, src/ui/ProgrammeView.tsx |
| Benchmarks | Anonymised quartiles across WS clients who opt in: labour %, sales per labour hour, hours over demand, forecast error. Nothing from fewer than ten sites or three operators, or where one operator dominates. | src/engine/benchmark.ts, server/routes/insights.ts |
| Xero journal posting | The weekly labour accrual posted into Xero by its accounting API, one manual journal per company into that company's own Xero organisation, without tax, each line tagged by site tracking. Connected over Xero's OAuth 2.0 sign-in by someone who manages settings; the organisations Preshift may reach are chosen in Xero, and each company is matched to the organisation of the same name, or chosen by hand. Draft or posted per company, and the reversal on the Monday after by default, so payroll's own journal carries the real cost. Each journal is checked first against the company's Xero chart (missing or archived accounts), its tracking (a missing Site category or option), balance and intercompany agreement. A week is posted once: posting again is refused unless the journal has changed and replace is asked for, which voids the earlier journal and its reversal. Idempotency keys stop a retry posting twice; refresh tokens are rotated and saved at once. A simulated Xero runs the demo. For people who run the close; connecting for those who manage settings. | src/engine/ledger.ts, server/integrations/accounting/, server/routes/accounting.ts, src/ui/XeroPanel.tsx |
| Labour cash | Cash out for labour by company and date over the next 13 weeks. Pay is costed as the accrual costs it: approved clocked time for weeks gone where there is any, the rota otherwise, the rota for weeks being built, and each person's last four weeks for weeks not yet rostered (salaries as salaries). Net pay is worked per pay period on the standard 1257L code at rest of UK rates, with employee NI and the employee's 4% auto-enrolment contribution under relief at source. PAYE and NI are grouped by tax month (6th to 5th) and due on the 22nd of the month after; pensions are grouped by month of deduction and due on the 22nd of the month after; any date at a weekend or bank holiday moves to the working day before. Employment Allowance is set against the claiming company's employer NI from the month after its last recorded payroll, and starts again each tax year. Each company has its own pay schedule (weekly, fortnightly or four-weekly counted from an anchor Monday and paid a set number of days after the period ends, or monthly on a set day or the last working day). Tronc is each person's share of the tips as the tronc run shares it, and their last four weeks of tronc for days still to trade, paid on the scheme's monthly payout day or in the pay run for the days the tips were taken; tips past that date and still unpaid go in the next pay run and are flagged once past the legal deadline. Income tax on tronc is worked as if spread over the pay periods it was earned in, as cumulative PAYE does; through an independent troncmaster it carries no NI or pension, and through an employer allocation it carries both, worked on the pay period. For people who see payroll or run the close; schedules for those who manage settings. | src/engine/cashflow.ts, server/routes/cash.ts, src/ui/CashView.tsx |
| Cut call and labour flash | Hours and pay, then Cut call, during service. Trading so far from the till against forecast gives the pace; the rest of the day is reforecast at that pace and turned back into headcount by role. Each person not needed for the rest of their shift is offered as an early finish at the next hour, dearest saving first, with the saving worked on their whole week, so someone inside guaranteed hours stays. Once short notice pay under the Employment Rights Act has started, what a cut owes is deducted from its saving; before then it is shown. The labour flash beside it: labour so far from the clock against takings so far, labour by close on the reforecast, with the cuts taken, and headroom against target. Apply one cut or all of them: each shift stays published and the person is told on their phone at once, and the cut is logged as short notice like any other change. Every five minutes while a site trades, whoever publishes its rota is emailed once a day if trading falls behind with at least £25 to save, and once if it runs 15% ahead with hours short. The screen refreshes every two minutes. | src/engine/cutcall.ts, src/engine/labourFlash.ts, server/cutCall.ts, server/routes/cutCall.ts, src/ui/CutCallView.tsx |
| Employment rights | The Employment Rights Act 2025 measures for zero and low hours workers, under People: short notice pay and guaranteed hours offers. The Act leaves the numbers to regulations not yet made, so the low hours threshold, what counts as short notice, how much is paid and the reference period are settings, and nothing is owed until the organisation sets the date each measure starts; before then the same figures show what it would cost. Every write is checked: a published shift cancelled, cut or moved inside the notice window (measured to the first minute the worker loses, so sending someone home mid-shift counts) is logged against what the worker was first told, however it was changed (the editor, a drag, clearing the week, the assistant, a coaching move, putting it out for cover), and giving the time back clears it. Changes the worker asked for (swaps, holiday, sickness) and leavers are not counted. The shift editor warns before saving or cancelling, with the amount. Owed pay goes to TeamPay with the next pay run as a pay adjustment and is accrued with its on-costs, so the pay run still reconciles; finance can waive one with a reason before it is paid. Guaranteed hours: who works beyond their contract over the reference weeks, the offer worked out to the half hour, its fixed cost a week, a plain English letter that says they may say no, and the answer recorded; accepting raises contracted hours and tells TeamPay. The rota also warns when a week would take someone's rolling 17 week average over 48 hours without an opt-out. | src/engine/employmentRights.ts, server/employmentRights.ts, server/routes/employmentRights.ts, src/ui/EmploymentRightsView.tsx |
| Night work and overnight trading | A site can close after midnight, as late as opening time the next day, or open round the clock (Set-up, Trading hours). Forecasts, demand cover, the auto-build and the cut call follow the trading day past midnight: the cut call at 00:20 for a bar open until 03:00 still reads that evening's trading, and alerts go out under the day that started trading. Under the Working Time Regulations a night worker is anyone who normally works at least 3 hours between 23:00 and 06:00: Preshift counts someone as one when 3 or more of this week's shifts are nights, or nights make up at least a third of their days (and at least 3) over the last 17 weeks and this week's rota, each earlier week read from clocked time where any was clocked and from that week's rota where none was, so sites that do not clock in are covered. A night worker's normal hours must average no more than 8 in each 24 over 17 weeks, and an opt-out from the 48 hour week does not cover this, so the rota warns for anyone opted out who goes over, from their first week: until three weeks are in, the warning is on the pattern so far and says the 17 week average goes over if it carries on. Every night worker must be offered a free health assessment before starting nights and regularly after: the rota and the HR overview warn until an offer is recorded on the personnel file, which keeps only whether it was offered, taken or declined, never the findings. Under 18s are blocked between midnight and 04:00 in every view, the auto-build and the API, with the supervised hospitality exemption still a warning from 22:00 to 06:00. | src/engine/night.ts, src/engine/nightWork.ts, src/engine/compliance.ts, src/engine/cutcall.ts, server/cutCall.ts, src/ui/SetupView.tsx, src/ui/HrFile.tsx |
| Pay premiums | Each company sets the enhanced rates its contracts pay hourly staff, under Finance, Pay: days of the week such as Sundays, England and Wales bank holidays, night hours in a window that can run past midnight, and overtime after a weekly threshold or after contracted hours. Each pays a multiple of the person's rate (time and a half, double time) or a fixed amount per hour on top, optionally for some roles only. Where two apply to the same hour only the higher is paid. The cost engine pays premiums on top of guaranteed hours, so holiday, NI and pension follow them; each shift carries its own premium and its share of on-costs, so a Sunday shift costs what it pays everywhere: the rota, the shift editor (which names the premium), the P&L, the auto-build and the accrual. Approved hours go to TeamPay with a premium line for each premium on top of the normal hours line, as a payslip shows it, so payroll pays what was accrued. Only someone who approves pay can change premiums, and every change is audited in words. Premiums do not count towards the minimum wage, and salaried staff are not affected. | src/engine/premiums.ts, src/engine/cost.ts, src/engine/timesheet.ts, server/routes/payroll.ts, src/ui/PremiumsPanel.tsx |
| Close | Labour accrual journals by role nominal and site tracking category, one per entity, from approved actuals once available, otherwise the rota, exported in Xero manual journal format. | src/engine/journal.ts, src/ui/CloseView.tsx |
| Multi-site | Group and entity views put every site side by side with entity subtotals and a sales-weighted group target. One press builds every site in scope: each site's own team first, then cover from people who have agreed to work other sites. Cross-site cover is reported each week. | src/engine/metrics.ts, src/engine/autoschedule.ts, src/ui/GroupView.tsx |
| Multi-entity | Sites trade in an entity and people are employed by one, which need not be the same (a central employing company, or cover at a sister company's site). Cost lands where the work was done; the wages accrual stays with the employer; the difference posts as an intercompany recharge on both sides, checked to agree before posting. | src/engine/types.ts, src/engine/journal.ts |
| Payroll per PAYE scheme | Each company has its own TeamPay employer. Starters, leavers and approved hours go to the scheme of the company that employs the person, wherever they worked; hours go as one batch per company, and each pay run comes back to its company and is reconciled against that company's accrual only. Nothing is sent until every company with hours has an employer, and moving a synced person between schemes is refused because it is a leaver and a starter. | server/payrollEmployers.ts, server/routes/payroll.ts, server/outbox.ts |
| Employment Allowance | One claim per connected group, set on the claiming entity with what the last payroll left unused. Used against that entity's employer NI week by week until it runs out, as payroll applies it, and posted to its books only (Cr employer NI, Dr wages accrual) split across its sites. Resets each tax year. Site labour % stays before the allowance. | src/engine/allowance.ts, src/engine/journal.ts |
| Reports | Labour trend over 4, 8 or 13 weeks on actual takings, with the target flexed to what each week took, labour from clocked time where a site clocks and the rota where it does not, and weeks over target counted by site. Cost by role: loaded labour by department and role with every on-cost, tying to the KPI bar and each wages nominal in the journal. Rota vs actual: the planned week against the latest estimate (clocked time for days worked, the rota for days left), per person hours and cost variance, shifts not clocked and unrostered clocking. Takings: net sales by site and day from a till feed (PUT /v1/sales), keyed in, or imported from the till's CSV export. Payroll accuracy: every pay run's accrual against what payroll cost, runs inside 1%, average miss, reruns replacing earlier runs, and people who miss the same way run after run. Every report exports to CSV and works for a site, an entity or the group. | src/engine/reports.ts, src/ui/ReportsView.tsx, src/ui/TakingsEntry.tsx, src/ui/PayrollAccuracy.tsx |
| EPOS (Square) | Someone with settings.manage connects the group's Square account in Settings with an access token, which is checked with Square before it is saved and never shown again, then matches each site to its Square location. Completed orders come in net of VAT, tips and service charge (tips belong to the team under the Employment (Allocation of Tips) Act 2023) and net of refunds, bucketed into trading days in UK local time with sales after midnight kept on the night before, by hour as well as by day. Only finished trading days are written, a day with no orders is recorded as nil, and a figure a manager keyed in by hand is never overwritten. The last three days sync every morning; any range up to a quarter syncs on demand from Settings or the Takings report. The demo and development use a simulated till. Other tills implement the same two calls. | server/integrations/epos/, server/routes/epos.ts, src/engine/epos.ts, src/ui/EposSetup.tsx |
| Integrations | One page under Admin showing every connection and whether it is working: TeamPay per PAYE scheme, Xero journal export, card readers and geofences, the staff app, Claude and Resend keys, and the API. EPOS, Talent Funnel, other ATS and TrustID are shown as not connected yet, each with what it would remove and the live numbers it would fix (sites with a forecast, starters, right to work checks missing or expiring). | src/ui/IntegrationsView.tsx |
| API | Fastify REST API with OpenAPI docs at /docs, scoped API keys and per-organisation tenancy. The web app runs entirely on it. | server/ |
| Open API and webhooks | Any system can plug in without Preshift issuing anything. A person with the settings permission and their authenticator makes a key per system under Settings, ticks what it may do (never more than they can do across the organisation), sees it once (only its hash is kept), sees when it was last used and every call it has made (kept 13 months, refused calls and attempts after revocation included, downloadable as CSV for a breach investigation), and revokes it at once. Keys are rate limited. Webhooks post signed, thin events (ids, never pay or personal data) to a partner's https endpoint the moment a rota is published, a shift or starter changes, hours are approved, a pay run reconciles, or (only when named, and only for someone who reads personnel files) absence, training, reviews and right to work copies change, with absence reasons and disciplinary cases never sent; raised in the same save as the change and retried in order if the partner is down. A partner can register its own endpoint with a webhooks:write key. | server/routes/developers.ts, server/webhooks.ts, src/ui/DeveloperSettings.tsx |
| MCP connector | Claude, or any MCP client, plugs into Preshift at /mcp with an API key and works the rota in plain English: sites, staff, the week with cost and compliance, forecast, takings, clocked time, rota against clocked, the labour accrual journal and pay runs; build, add or remove shifts, publish and approve time. Every tool calls the public API with the caller's own key, so scopes, site access, compliance blocks, audit and webhooks apply unchanged, and a key only sees the tools it may run. In claude.ai it needs no key: the person signs in to Preshift once, ticks the organisations they work in and what Claude may do, and the connection acts as them in each, their sites only, never with authenticator-only rights such as pay approval; it ends when they disconnect, leave or reset their password. | server/mcp.ts, server/oauth.ts, docs/API.md |
| TeamPay, native | Starters, changes and leavers sync to TeamPay as they are saved. Approved clocked hours go to TeamPay through an idempotent, retrying outbox. Signed payroll.run.completed webhooks bring the pay run back and reconcile it to the accrual. A team already on TeamPay is linked, never set up twice: a starter matching exactly one record on name and date of birth is linked automatically, a near match is held, and Payroll, under "Link existing TeamPay people", links the rest. Where the two disagree nothing is overwritten until someone decides which is right, and hours wait for that decision on pay. Pay keeps its two-person rule: taking TeamPay's pay is a pay change finance approves, and overriding what TeamPay pays is finance's own decision. | server/integrations/payroll/, server/outbox.ts, server/routes/payrollLink.ts, src/engine/payrollMatch.ts, src/ui/PayrollView.tsx, src/ui/PayrollLink.tsx |
| Sign-up and billing | A business signs itself up from the sign-in page: business name, its own name and email, then a single-use link (24 hours, token after #) to prove the inbox before any account exists, where it chooses its password. That makes a new empty organisation named after the business, with the person as owner, on a free trial, and opens Set-up. The page answers the same whether or not the email already has an account; an existing account is told in its own inbox. Billing (Organisation, then Billing, for settings.manage) subscribes through Stripe Checkout, priced per site and carrying any trial left, collects the billing address and VAT number, and opens Stripe's portal for cards, invoices and cancelling. Stripe's signed webhooks keep the subscription's status, sites and last invoice. When a trial ends unpaid, or a subscription is cancelled or stays unpaid after Stripe's retries, changes are refused (402 billing_required) while every read, paying, the person's own sign-in and GDPR requests carry on. Organisations billed under a WS engagement have no billing record and are never paused, nor is anyone on a system without Stripe configured. | server/routes/signup.ts, server/billing.ts, server/routes/billing.ts, src/ui/BillingView.tsx, server/billing.test.ts |
| Customers | For Williams, Stanley & Co.: every business that signed itself up on one page, with its standing (trial, paying, payment failed, lapsed), days of trial left, sites set up against sites billed, monthly or yearly, MRR at list price, onboarding steps done and the next one, and what needs a call (trial ending unpaid, a week in with no rota, failed payment, lapsed, more sites than billed, cancelled). Totals: MRR and ARR, paying, trials and how many have published a rota, trial-to-paid conversion and sign-ups in the last 7 and 30 days. Filters and a CSV. Only for the WS customer operations role (customers.view), with an authenticator, in the organisation that runs Preshift: granted in a client's organisation it shows nothing. Counts and billing standing only, never a customer's staff or pay; every view audited. One action: when the firm takes a customer on, move it to WS engagement billing, which stops the Stripe subscription renewing at the period end so nobody pays twice (customers.billing). Former WS clients moving to card billing show here with their date to subscribe by. | server/routes/customers.ts, src/ui/CustomersView.tsx, server/customers.test.ts, server/engagement.test.ts |
| Clients | For Williams, Stanley & Co.: each WS person sees the client organisations they hold a WS role in, never anyone else's, with counts only (sites, people, starters, people not in payroll, accounts), a seven step onboarding checklist and what needs attention (owner invitation expired, still on the sample group, payroll gaps, nothing changed for 14 days). A WS client partner with their authenticator, working in their home organisation, adds a client: a new empty organisation named after its trading company, with them as its WS client partner and its owner invited. The owner's link only ever goes to the owner's own email, through the home organisation's Resend; without email set up nothing is created. While the owner has not accepted, the partner can send a fresh invitation (three an hour); once they accept, the client runs its own access and WS appoints nobody inside it. Until anyone at the client has signed in, and before any site or person is set up, the partner can remove the client: its owner link stops working, everyone's access there is suspended and it leaves every list and switcher. The organisation and its audit trail are kept, never deleted; once the client has signed in, only its owner can part company with WS. | server/routes/clients.ts, src/ui/ClientsView.tsx, server/clients.test.ts |
| Users and access | Roles for operators, Williams, Stanley & Co. and outsiders, scoped by site with start and end dates. Nobody grants beyond their own access or changes their own. Paired duties (pay rates with payroll, starters with payroll) are refused or need a recorded reason. Strong rights need an authenticator code in each session (TOTP, with single-use recovery codes, encrypted secrets and a lockout). The last administrator is protected. Overview of what needs attention, site coverage of key duties, self-approval detection, quarterly access reviews, bulk actions, CSV export and a full audit trail. Every rota, time and payroll route enforces the same rules per site: a GM's key reaches only their site, pay is withheld from those who may not see it, nobody approves their own hours or changes their own pay. Rostering another site's person needs their agreement to cover it, unless the caller also runs their home site. A CI test fails any API route that does not visibly check access. | src/engine/access.ts, server/routes/users.ts, src/ui/access/, server/accessCoverage.test.ts |
| Pay approvals | A change to someone's rate, salary or pay basis by a person is a proposal: their pay in force stays until the finance director approves it (pay.approve, with an authenticator). Nobody approves their own proposal or their own pay. Each proposal shows its reason and what it adds over a year, gross and at full cost with holiday, employer NI and pension, on contracted hours or the last four weeks of rota. A proposal can be dated from when the rise was agreed: approved before that day it takes effect on the day; approved after it, it applies at once and back pay for the hours worked in between goes to payroll with the next hours sent. Each organisation sets its rule, the working days finance has to decide and the day hours go to payroll; each waiting change shows its decide-by date and whether the cut-off is near, and finance is emailed each morning with what waits (counts and dates, never names or rates), the owner copied in once a change is overdue or the cut-off is a day away. The screen says when nobody can approve, or when the only approver has no authenticator yet. A starter's proposed rate goes the same way, starting at nought. Approval refuses pay that has moved since the proposal; a newer proposal replaces a waiting one; rates below the legal minimum are refused at once. Every decision is kept, and payroll and webhook subscribers hear of new pay only on approval. In a business where nobody else can approve (a new one run by its owner alone) a change applies at once and its record says so, and the Access overview flags that nobody can approve pay. Pasting in a team at set-up proposes each rate, and finance can approve them all in one confirmed step. Pay set without approval, by a deployment's own key or with no approver, is still on the pay history, marked as such. | server/routes/pay.ts, src/engine/payChanges.ts, src/ui/PayApprovalsView.tsx |
| Time and attendance | Clock in and out (UK wall-clock time, overnight shifts), manual entries, manager approval, rota against clock by person, period locking once sent to payroll. | server/routes/time.ts, src/engine/timesheet.ts |
| Pay periods | Each company runs one or more payrolls, each on its own calendar: weekly, fortnightly or four weekly from an anchor date, monthly to a set day (a calendar month or a mid-month cut), or the last working Friday with whole weeks of hours. The client sets the payday and the cutoff (days before payday and a time); paydays and cutoffs move off weekends and England and Wales bank holidays, and a calendar that would want hours before they are worked is refused. People are placed on a payroll; the first is the default. Each period closes in three steps: lock the rota (no shift added, moved or removed for those people in it, unlocked only with a reason and never after passing), approve time cards against the locked rota, and pass to payroll with the payday. Passes after the cutoff are marked late, a correction needs a reason and supersedes the last batch, and each hour is stamped with the period that paid it so no other payroll pays it again. | src/engine/payPeriods.ts, server/routes/payPeriods.ts, src/ui/PayPeriodsView.tsx |
| Shift sign-in | Clock in tab for a tablet at the site entrance: staff tap an RFID card (USB keyboard reader, or a phone's NFC on Android Chrome) or sign in by name. Each site can have a geofence; signing in by name on a device must happen inside it, and the location is kept on the time entry. Repeat reads of one tap are ignored. | src/ui/ClockView.tsx, server/routes/clocking.ts, src/engine/clocking.ts |
| Staff app | Staff clock in and out on their own phones at /staff/, record their breaks, and see their own published shifts and hours. Breaks are not location checked (they only lower paid time); recorded breaks set the entry's unpaid minutes at clock-out, the phone shows the legal minimum for the shift, and a card tap during a break ends the break. A manager sends a one-time invite link from Clock in setup (valid 7 days; a new one resets a forgotten password). Phone clocking needs the site's geofence, both ways. Staff logins are separate from manager accounts and reach nothing else; closing a login or marking a leaver ends it at once. | src/staff/, staff/index.html, server/routes/staff.ts |
| Kiosk for shared iPads | /kiosk/ turns a shared iPad into a screen that only clocks people in and out at one site: card taps (USB reader, or a tablet's NFC) and sign-in by name for people on today's rota there. A manager pairs it on the iPad itself from Clock-in setup and is signed out in the same step; the iPad then holds its own credential for a year, which reaches nothing in the manager app or API. Removing it in setup stops it at once. A site can require a PIN for sign-in by name: staff set their own in the staff app or a manager issues one to hand over; weak PINs and birth dates are refused, five wrong tries lock that person's PIN for 15 minutes, and PINs are kept hashed in the settings store and erased with a leaver's data. Guide under Help, How to. | src/kiosk/, kiosk/index.html, server/routes/kiosk.ts |
| Break check | Finished shifts with less rest break than the Working Time Regulations give (20 minutes in one block over 6 hours of work; 30 over 4.5 hours for under 18s) are flagged on the Payroll tab before approval, as too short or split. A manager can enter a break that was taken but not recorded; nothing is changed automatically, and approval is never blocked but is recorded against the flagged shifts. | src/engine/breaks.ts, src/ui/BreakShortfalls.tsx |
| How to | Guides for GMs under Help in the management bar, starting with what to do about breaks below the legal minimum. The same steps appear as pop-ups on the Payroll tab: "What do I do?" beside the break flags, and a check before Approve all when flagged shifts are waiting. Add a guide in src/ui/guides/index.ts. | src/ui/HowToView.tsx, src/ui/guides/ |
| Support | Questions, bug reports and feature ideas under Help, answered by the client's Williams, Stanley & Co. client partner. The answers that fit appear as someone types (the same quick answers are in How to), a bug is triaged by what it stops, and each ticket shows its first response target in London working hours. A new ticket emails the partner; only their reply stops the clock and only they move a ticket on. Ideas collect one vote a person. | src/engine/support.ts, server/routes/support.ts, src/ui/SupportView.tsx |
| Staff self-service | The staff app's Rota, Requests and Inbox tabs. Staff see who they work with, offer a shift to a named colleague (or in exchange for one of theirs) or to anyone eligible, and pick up open shifts; ask for holiday against their balance, for other time off, and propose weekly availability; mark one-off days they cannot work, which the rota warns on and the auto-build avoids; and report sickness and upload a fit note. A colleague agrees to a swap first, then a manager approves it. Every swap and claim is checked by the compliance engine for both people when it is asked, agreed and approved, so anything that would double book, break rest, young worker or child rules, or put someone on a role they are not trained for is refused. Open shifts are offered in waves, cheapest first: people short of their contracted hours that week, then the three cheapest to add by marginal loaded cost, then everyone eligible, each wave two hours after the last while no claim waits, and everyone at once inside a day of the start; a site can approve clean claims automatically while the week's labour stays within its target. The written statement and the handbook are sent from the personnel file to sign in the app, opened before signing, signed in the person's own name, and kept with the time, IP, device and the SHA-256 of the copy signed. Holiday goes to whoever manages the team, time off and sickness to whoever keeps personnel files (only they read the reasons and fit notes), and swaps and open shifts to whoever publishes the rota. Nobody decides their own. Sickness is recorded the moment it is reported, so its shifts block publication until covered, and the manager can put each one out as an open shift. Each person is told in the app, and by email where it is set up, when their published week changes (only what changed), when a shift is cancelled, an open shift is posted for them, a swap is offered or agreed, and a request is decided. Each morning every manager is emailed what is theirs to decide (sickness to cover, requests and how long each has waited, open shifts unfilled in the week ahead), only when something is waiting, with no reasons or notes, and can turn it off for themselves. | src/engine/requests.ts, src/engine/offers.ts, server/routes/requests.ts, server/routes/signing.ts, server/notify.ts, server/digest.ts, src/staff/SelfService.tsx, src/ui/RequestsView.tsx |
| Drag to move and resize | Drag a shift to another person or day, or drag either edge to change the hours in 15 minute steps. A finish can run on past midnight, as late as the end of the next day, and the timeline widens to show it as you drag. Labour cost, labour %, compliance flags and every chart recalculate live while you drag, and a readout beside the pointer shows the change to the week. Drops on approved leave or outside stated availability are refused with the reason, and resizes stop at the edge of availability (the shift editor remains the place for an agreed exception). On a tablet, press and hold a shift to lift it, so a swipe still scrolls; the edge grips work straight away. The grid scrolls when a shift is held near its edge. A first-use hint explains the gestures. Escape cancels, Undo reverses, and Alt with the arrow keys does the same from the keyboard. | src/ui/RotaGrid.tsx, src/ui/drag.ts |
| Service timeline | The default rota view. Each day is drawn over its own forecast sales curve, with shifts as bars grouped front of house, bar and back of house, and a strip under each day reading hours short or over against demand. Bars drag along the day, by their ends, or up and down to another day, with the same live costing and leave and availability rules as the person grid, which stays one click away. | src/ui/Timeline.tsx, src/ui/useShiftDrag.ts |
| Add a shift | On the timeline, drag across empty space on a day to draw a shift, click or tap for a four hour block, or click a red short hour to fill that gap with the role that is shortest. A picker asks who should work it: the site's team ranked trained and clear first, then cheapest, where cost is what the shift adds to that person's loaded week (nothing for guaranteed hours, more for anyone over the NI threshold). Leave, availability, double booking and other blocks are shown with the reason. Hovering a name previews them across the whole page, including the P&L. | src/ui/assign.ts, src/ui/AssignPanel.tsx |
| Borrow across sites | The picker also offers people from the group's other sites who are set up to cover this one, under their own heading, with the reason anyone cannot take it. Cost is shown twice: what the shift adds to the group, and what it puts on the host site's labour, because cost follows the work. It says when borrowing is cheaper for the group or the only option, flags a same-day shift at another site with under an hour to get across, and rest and hours rules count the person's whole week at every site. Borrowed people are tagged on the timeline and grid. Borrowing follows each person's cover sites from their People record, and anyone employed by another company is marked as an intercompany recharge. Close lists cross-site labour by pair of sites, with a CSV, beside the intercompany recharges the journal books. | src/ui/assign.ts, src/ui/AssignPanel.tsx, src/ui/CloseView.tsx |
| Travel between sites | Each site has a location from its postcode (postcodes.io), and journey times between every pair come from the TfL journey planner for a weekday at 17:00 by public transport or on foot, with times set by hand kept and a labelled straight-line estimate where TfL has no answer. The compliance check blocks a same-day move between sites that cannot be made in time and flags one with less than ten minutes to spare, everywhere: drag, the editor, the picker, the auto-build and the API. Borrowed people in the picker show their journey from their home site. Set postcodes and refresh on the Sites overview. The journey is also paid: travel between workplaces within a working day is working time for the National Minimum Wage (only home to work is excluded), so the cost engine adds it to the person's week, where it carries holiday, NI and pension and can tip them over a threshold, and charges it to the shift travelled to, so it lands on the site that asked for the move. The P&L shows it as a memo line, the picker and shift editor include it in the cost, and the same rule runs on clocked actuals and adds the travel hours to the payroll line for the site travelled to, so the accrual and TeamPay agree. Each company sets what it pays for travel on the Sites overview: the person's normal rate (the default), the minimum wage for their age, or a fixed rate, which is never allowed below the minimum wage. Travel at a different rate goes to payroll as a line of its own at that rate, and the cost engine charges each site by what its share of the week pays. | src/engine/travel.ts, server/routes/travel.ts, server/integrations/travel/journeys.ts, src/ui/TravelPanel.tsx |
| Rota views | Six lenses on the week beside the timeline and the grid by person: cover against demand for every trading hour, rest per person across a fortnight, positions by hour, shift cost bricks against the day target, what the next publish will tell each person (GET /v1/rota/changes, worked from what each person was last told), and four weeks of how weekends, closes, late finishes and hours under contract are shared. In the staff app, a fortnight strip with hours, pay before tax and rest, a crew line per shift, and what each open shift would do to the person's week. | src/engine/rotaViews.ts, src/ui/RotaLenses.tsx, src/staff/RotaPictures.tsx, server/routes/rota.ts, server/routes/staff.ts, server/routes/requests.ts |
| Rota P&L | On Sites, Rota and Demand, where the week is planned: forecast sales, wages, holiday, employer NI and pension in brackets, labour cost tied to the journal, contribution after labour, labour % against target, day by day, compliance, and sign-off to publish. | src/ui/PnlStrip.tsx |
| GDPR | UK GDPR for staff data under Admin. A request log for access, erasure, rectification, restriction, portability and objection, with the one calendar month deadline (three when extended) and overdue flags. One-click subject access export of everything held on a person, with how it is used and their rights. Erasure for leavers only, keeping what the law requires (Article 17(3)). A retention schedule that strips clocking locations after 3 months, contact details, card and staff login 3 months after leaving, right to work detail 2 years after leaving, and anonymises the person 6 years after the tax year they left, keeping shifts and pay for the accounts; statutory floors cannot be undercut. An Article 30 record of processing built from what is held, with lawful basis, retention and processors (TeamPay, Anthropic, Resend, hosting), exportable as CSV. Acting on personal data is admin only and audited by id. | src/engine/gdpr.ts, server/routes/privacy.ts, src/ui/PrivacyView.tsx |
time.locktime.approvepayroll.senddocs/API.md| Staff requests | Holiday: from today, up to 31 days a request, hours from the person's own day hours. Time off: compassionate, dependants, family, unpaid or other, up to seven days back. Sickness: from up to three days ago to tomorrow, recorded at once, the last day told when back. Availability: a new weekly pattern replaces any still waiting. Every decision is checked again when made, and refused if it would now break a rule. |
| Swaps and open shifts | Only published shifts that have not started or been clocked. A named colleague, anyone eligible, or an exchange for one of the colleague's shifts. Eligible means trained for the role, set up for the site, employed that day and free of any blocking rule. An approved swap or claim is published as it is approved; an open shift's break is raised to what the person is owed. |
| Notifications | In the staff app always; pushed to every phone the person turned notifications on for (up to five, kept on their staff login, so closing it or marking a leaver ends them); by email through the organisation's Resend once email is set up, to the staff app address or else the record's. Emailed only after the change saves. Names, times and decisions only: never pay, never an absence reason. A person's feed keeps 100 messages and 90 days. |
| Push | Web Push with no third-party service: each message is encrypted for the one phone (RFC 8291) and signed with this deployment's own VAPID key (RFC 8292), made on first use and kept in the database, so nothing needs setting up. Only Apple, Google, Mozilla and Microsoft push addresses over https are accepted. A push service that says a phone has gone removes it. Lock screens show names, times and decisions only. PRESHIFT_PUSH_CONTACT (default mailto:[email protected]) is who push services may contact. |
| Fit notes | PDF, JPEG, PNG or WebP up to 4 MB, read from the bytes, encrypted with the right to work key, five per absence. Read only by those who read the personnel file, each view logged; removed with the absence, and by retention and anonymisation. |