Skip to content

Part 2 of 7 · Timesheet validator series ~6 min read

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

Three timesheet lanes converging on one row-per-day shapeThree boxes stacked on the left. Web form: one person submitting one week. Crew spreadsheet: a supervisor uploading a grid for several people. Photo of paper: a handwritten sheet photographed on a phone. Their arrows are labelled rows, grid and image, converging on One row per day on the right, holding person, date, start, finish, break and job. Below it, connected by a downward arrow, is the Comparer, which checks against the roster, the jobs and the rules. A note says the row shape is the contract and nothing downstream knows which lane a row came from.Web formone person, one weekrowsCrew spreadsheeta supervisor's uploadgridPhoto of papera handwritten sheetimageOne row per dayperson, date, start,finish, break, jobComparerroster, jobs, rulesThe row shape is the contract. Nothing downstream knows which lane it came from.
Fig 1. Three ways to submit, one row shape. Everything after this point works on rows, which is why adding a fourth lane later costs nothing.
  • 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

How a photographed paper timesheet becomes rowsA vertical chain of six steps inside the AWS account, entered by a box labelled Photo uploaded from a phone. Step one stores the original in S3 for the record. Step two asks whether this person's week has been submitted before, checking a DynamoDB sheets table; a resubmission exits to Replace the draft rather than creating a second sheet. Step three pulls the grid using Amazon Textract table extraction. Step four reads the grid into rows with a single Bedrock call, grounded by the roster for that week so names and dates come from a list. Step five asks whether anything was unreadable, exiting to Ask about that cell, showing the crop of the original. Step six hands the rows to the comparer with their sources recorded. A note says the crop of the actual cell goes in the question because people recognise their own handwriting.AWS ACCOUNTPhoto uploadedfrom a phoneStore the originalS3, kept for the recordThis week before?person + week endingDynamoDB sheetsdraft, replaceableReplace the draftnot a second sheetresubmitPull the gridTextract tablesRead into rowsone Bedrock callRoster for the weeknames and datesAnything unreadable?Ask about that cellshow the croplow confidenceHand to the comparerrows, with sourcesThe crop of the actual cell goes in the question. People recognise their own handwriting.
Fig 2. A photographed paper sheet, end to end. Textract finds the grid, the model reads it into rows grounded by the roster, and any cell it is unsure about becomes a question with the original crop attached.
  • 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