How a timesheet arrives
There is always somebody who will not use the form. On a site crew it might be most of them. Any timesheet system that only accepts its own input format ends up with a supervisor transcribing paper into it on a Sunday, which is the original problem with an app bolted on. So this one takes a photo of a paper sheet as a first-class input, and this post is mostly about making that safe.
Key takeaways
- Three lanes: a web form, a spreadsheet upload, and a photo of a paper grid.
- All three produce the same thing: one row per person per day, with a source recorded.
- A digit the reader is not sure about is marked unreadable, never rounded to a guess.
- A supervisor’s crew upload becomes many people’s rows, each independently checkable.
- A resubmitted week replaces the previous draft rather than creating a second one.
Three lanes, one row shape
- Database
- Analytics
- Front-end & mobile
The row
person sam@example.com from the submitter, never inferred
date 2026-07-09 the working day, not the submission day
start 07:00 24-hour, local
finish 15:30
break_min 30
job J-4471 or null, with a reason
source form | sheet | photo
confidence 1.0 | 0.0-1.0 below the floor means unreadable
The confidence field only ever moves off 1.0 in the photo lane, and it is the field that makes that lane safe. A form submission is what somebody typed; there is nothing to be unsure about. A photograph of a biro 7 that might be a 1 is exactly the situation where a system should say so.
The photo lane, in detail
- Storage
- Database
- App integration
- Machine learning
- Management
- Front-end & mobile
- People
Why the roster grounds the read
A handwritten timesheet has names down one side and dates across the top, both in handwriting, both frequently abbreviated. “S Patel” and “Sam P” and “SP” are the same person; “9/7” is the ninth of July or the seventh of September depending on which side of the Atlantic wrote it. Asking a model to resolve those from the image alone is asking for a confident wrong answer.
So the roster for that week goes in the prompt: these are the people, these are the dates. The model matches to a list rather than generating. A name it cannot match to somebody on the roster is not a new employee — it is an unreadable cell, and it goes back as a question with the crop attached.
The crew spreadsheet lane
A supervisor with a crew of eight is the case that breaks most timesheet apps, because the app models one person submitting for themselves. Here the upload simply produces eight people’s rows at once, each of which is then an ordinary row with an ordinary provenance.
- Each person’s rows are checked independently. A gap on one crew member’s Wednesday is that person’s question, not the supervisor’s, and it goes to them.
- The supervisor gets a summary, not the questions. “Uploaded 8 people, 39 rows, 2 questions sent.” They do not become the routing layer for their own crew.
- A person who is not on the roster is flagged, not created. An unexpected name in a crew sheet is usually somebody covering, and it needs a manager rather than an automatic new record.
- The upload is replaceable. Reuploading a corrected sheet replaces the draft rows for that week, keyed on the supervisor and the week ending, rather than doubling everybody’s hours.
What the reader refuses to do
- It does not round an unreadable digit to the nearest plausible one. Unreadable is an answer, and it produces a question with the crop attached.
- It does not infer a job reference from who the person is or what they usually do.
- It does not create a person. A name that is not on the roster is a flag for a manager.
- It does not carry hours forward from last week to fill a gap, even when last week is identical. That is the single most tempting and most dangerous shortcut available here.
Next: the five comparisons the rows are put through, and which of them can close a sheet on their own.
All posts