Sajjad Mahessar Good Merch Group Free audit
~/sajjad $ cat projects/cleaning-rota-app ← cd ../projects Client work

Rota, wages & profit app for a cleaning company

Google Play · Sept 202616 properties

An Android app for a domestic cleaning company. It schedules recurring cleans across sixteen properties, pushes a reminder at 7am for anything due, records hours per cleaner, and works out wages, invoice value and profit as the hours are typed in. Built in React Native, distributed privately through Play internal testing.

Type
Android app
Category
Client work
Built with
React Native / Expo · TypeScript · Supabase · pg_cron · Edge Functions · FCM
Project
25 of 37
TWO KINDS OF RATE, DELIBERATELY SEPARATE ON THE PROPERTYAND THE CLEANER what I charge and pay NOW ON THE COMPLETED JOBAND THE LOGGED HOURS what applied THEN RAISE A RATE NEXT YEAR FUTURE JOBS USE THE NEW RATE LAST AUTUMN IS UNCHANGED WITHOUT THIS, EVERY HISTORIC REPORT REWRITES ITSELF AND STOPS MATCHING WHAT PEOPLE WERE ACTUALLY PAID
Rates mean what I charge now; snapshots mean what applied then

The decision the whole build rests on

Rates stored on a property or a cleaner mean what I charge or pay now. Copies stamped onto the completed job and the logged hours mean what applied then.

Put a client up next spring, or give someone a pay rise, and last autumn’s figures stay exactly as they were. Without it, every historic report silently rewrites itself the moment a rate changes — and the numbers in the app stop matching the numbers people were actually paid.

Rules that protect a record of money

  • The database refuses to delete a cleaner who has logged hours, because those hours are what somebody was paid. Not a warning in the interface — a foreign key that says no.
  • Deleting a property would cascade and destroy its wage history, so the app checks for completed cleans first and offers to pause the property instead.
  • A unique index allows only one open clean per property at a time — which is what stops the rota forking or stacking duplicates when a clean runs late.
  • Every completion records which job generated it, so a mistake can be undone precisely rather than patched over.

The calculation

Three hours at a cleaner’s cost rate against a property charged by the hour gives wages, invoice value and the margin between them — updating live as the hours are typed, before anything is saved.

The same arithmetic runs in two places: once on the phone for the live figures, once in the database on save. Two copies of a money rule is exactly how figures drift apart, so both were written to identical logic deliberately, and that’s recorded in the handover.

What went wrong, and what it taught

  • Built with bottom tabs first, then moved to a drawer. Android’s gesture bar sits in the same strip and taps were landing on the wrong control.
  • Push notifications need two separate halves on Android — one file to let the app receive, a service key to let the server send. With only the first you get a valid token and a silent credentials failure.
  • A bulk insert with on conflict do nothing needs a unique constraint to have anything to conflict with. Without one it inserted a duplicate — which is how there came to be two of the same cleaner.
  • A free-text date field gets handed half-typed values; a partial date crashed the scheduler mid-keystroke. Fixed by deleting the text field rather than validating it — a calendar cannot produce an invalid date.
  • Reminders are sent server-side rather than scheduled on the phone, because Android kills background work aggressively and a reminder that might not fire is worse than no reminder at all.

Written down as not done

  • Cleaner logins. The database is ready — roles, the link to a cleaner record, and access policies written and dormant. Only the interface is missing, so turning it on needs no migration.
  • Photos on completion, payment tracking (profit currently assumes everyone pays), and a monthly wage export for payroll.
  • Over-the-air updates, so a text change still costs a store round trip.
Also flagged honestly: the store screenshots are from the old build and show mostly empty screens. Noted as worth replacing once there is a week of real data behind them.
iOS app

Cleaning company operations app

Sites, staff and joining for a cleaning firm

Swift / SwiftUISupabasePostgreSQL + RLSStorage
iOS app

Personal training & tasks app

Training log and tasks for a private client

SwiftSwiftUISwiftDataLocal notifications
No charge

I’ll find the money before you pay me anything.

Send me your software subscriptions and I’ll come back with what’s replaceable and what it’s worth — free, and yours to keep either way. How it works →

Contact

Want the same done to your cost base?

Open to operations, automation and technical operations roles — and to talking with anyone running a multi-site business who suspects they're paying for software they could own instead.