Rota, wages & profit app for a cleaning company
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.
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.
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.
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 nothingneeds 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.