Both Numbers, Side by Side
A queued approval that lost a race gets refused, and the operator has to decide whether to throw away their own capture. That decision needs both numbers on one screen, and it needs a live connection to be worth anything.
- Written by
- AWRA OpsHub TeamOperations Insights
- Issued
- Build
- Initial
Two people approve the same transfer. One of them is in a yard with no signal, so their approval waits in a queue. By the time it lands, the movement has already been rejected by somebody at a desk. The queued copy is refused — correctly — and now the person holding the phone has to decide what to do with a capture they made in good faith.
For a long time all we could tell them was the classification: somebody else already decided this. Which is true, and is an instruction to discard, and answers none of the questions that matter. Decided how? By whom? On what grounds? A person is about to destroy their own record of what they saw, and they are being asked to do it on a sentence.
The screen the sentence could not replace
What that decision needs is not better wording. It is the two things that disagree, on one screen, labelled.
| What this device captured | What the server holds now | |
|---|---|---|
| Decision | Approve | Rejected |
| Decided by | — | A named colleague |
| Quantity | 40 | 36 |
| Captured | Yesterday, 16:20 | Updated this morning |
Now the decision is ordinary. If the movement was rejected for a reason that no longer holds, that is a conversation with a colleague rather than a discard. If the quantity has moved because stock genuinely moved, discarding is right and the operator can see why.
Never ask somebody to destroy a record on the strength of a sentence when you could show them both versions.
It needs a connection, and it says so
This is the one screen in the offline queue that refuses to work offline, and the refusal is the point. The entire claim of the right-hand column is here is what the server has now. A cached copy cannot make that claim: it would put a second stale number beside the operator's own and label it as the truth, which is a worse outcome than showing nothing.
So with no connection the screen says so and offers nothing. Not a greyed-out table, not a cached approximation. A sentence explaining that the comparison needs a live connection, and the same sentence again when the connection comes back and it can be fetched.
Five things the fetch can conclude
| Outcome | What it means | What the operator does |
|---|---|---|
| Ready | Both columns, side by side | Reads the disagreement and decides. |
| Unsupported | This kind of entry has no comparison, and the reason is named | Falls back to the classification and the record itself. |
| Offline | The claim cannot be made right now | Waits for a connection. Nothing is guessed at. |
| Gone | The record no longer exists on the server | This is itself the answer — the one that means discard. |
| Error | The fetch failed | Retries. A failure to look is not evidence of anything. |
"Gone" is a result, not a failure
A record that has been deleted is the clearest possible outcome: there is nothing left for the queued write to apply to, and the only sensible action is to discard. Reporting that as an error would leave an operator retrying a fetch for a record that will never come back.
Where a comparison exists, and where it honestly does not
Nine kinds of queued entry have a comparison: the four decision families, count lines, inventory status actions, stock transfers, check-outs and point-of-sale entries. Everything else does not, and the screen says which case it is in.
One of those refusals is specific rather than generic, and it is the one worth reading twice. A check-in adds stock rather than drawing it down, so there is no balance for it to have outrun. There is nothing to compare, not because nobody has written the comparison yet, but because the disagreement this screen exists to show cannot arise. Saying that is much better than an empty table.
An empty table is the one thing it must never be
A comparison with no rows and no explanation reads as nothing changed — which is the precise opposite of what a conflict screen is for. Every case where there is nothing to show carries a sentence saying why, and that sentence is checked before a request is spent on the fetch.
Two limits, both deliberate
The first is about history. The left column is what this device captured — not a snapshot of what the server looked like at the moment of capture. Nothing records that anywhere, and reconstructing it from the payload would be a guess presented as history, which on a screen somebody uses to destroy their own record is the worst kind of thing to ship.
So where a field genuinely exists on both sides — a quantity, a counted value, a decision, a reason — the two columns are directly comparable and the row is marked as changed or not. Where it does not, the row is informational and never claims a change.
The second is about how "changed" is decided. It is computed per row by the code that understands that row, not by comparing the two strings in the table. That distinction is invisible until you see what a string comparison does to real data.
Why comparing the display text would be wrong
Three of the four above are false alarms that a string comparison would raise, on a screen whose entire job is telling somebody which rows to look at. A conflict view that cries wolf on formatting is one people stop reading, which returns them to deciding on a sentence.
Eight lines, and the ninth
A count or a transfer can carry many lines, and checking every one against live stock is a request per line. The comparison checks the first eight and says so plainly when there are more: how many were not compared, and that only the first eight were checked.
Which is a compromise, and is stated as one. The alternative of silently comparing eight and presenting it as the whole entry would be a conflict screen that misses conflicts — exactly the failure the screen exists to prevent, dressed up as a complete answer.
Questions about conflict handling
What does an operator see when a queued action is refused?
What you will hear
"An error message."
How to read it
Ask whether it contains the server's own reason and the values that disagree. A classification alone tells somebody to discard their capture without telling them what they are discarding it in favour of.
Can I see what the record looked like when I captured it?
What you will hear
"Yes, it is all in the audit trail."
How to read it
Worth confirming, because most systems record what the device sent and not what the server held at that moment. If nobody stored it, a screen showing it is reconstructing it — which is a guess with the authority of a table.
What does the comparison show when the device is offline?
What you will hear
"It works offline like everything else."
How to read it
It cannot. The right-hand column claims to be what the server holds now, and a cached copy cannot make that claim. A vendor who says otherwise is showing a stale number labelled as current, on the screen where that matters most.
How do you decide which fields have actually changed?
What you will hear
"We compare the values."
How to read it
Compare them how? Case, formatting and codes-versus-labels all produce differences that are not disagreements, and a screen that flags them is one operators stop trusting within a week.
Our take
Every offline system eventually asks somebody to throw away work they captured in good faith, and that moment deserves more care than it usually gets. A classification is not enough — it names the situation and hides the evidence. The version worth building shows both columns, marks only genuine disagreements, states plainly which entries have no comparison and why, treats a deleted record as an answer rather than an error, and refuses to pretend it can make the claim offline. It costs one screen and it changes the conflict from a leap of faith into an ordinary decision.
The comparison is a per-type builder, which is what makes it extensible
Each supported entry type has its own small builder that knows what its rows mean. Adding to that set is a defined job rather than a redesign.
Every line compared, not the first eight
Batched line-level checks for long counts and transfers, so a fifty-line entry is compared whole rather than sampled.
The record as it was at capture
A server-side snapshot taken when a device fetches a record, turning the two columns into three and making the middle one real history rather than an inference.
Comparisons for more entry types
The same treatment for entry types that currently fall back to the classification, each with a builder that understands its own fields.
Resolve from the comparison itself
Adjusting the captured values to the current ones and resubmitting in place, for the conflicts where the right answer is visible on the screen.
The support map, the builders and the fetch each live in one place, so extending any of them touches a known surface rather than the queue as a whole.
Tell us where your approvals collideMake two people decide the same thing
Approve a transfer on a phone in flight mode, reject it on the web, then reconnect. What the phone shows you at that moment is the whole of how a system treats work it has to refuse.
Talk to us about offline approvalsFrequently asked questions
What happens when somebody else decides a record before my queued approval lands?
The queued copy is refused, and the entry opens on a screen showing what your device captured beside what the server holds now — the decision, who made it, the quantities, and the times. You then discard your capture or take it up with the colleague who decided differently, with the evidence in front of you rather than a sentence describing it.
Why does the comparison need an internet connection?
Because its whole claim is "this is what the server has now", and a cached copy cannot make that claim. Showing a stale number beside your own and labelling it as current would be worse than showing nothing, so offline the screen says the comparison needs a connection and offers nothing else.
Some entries say there is no comparison. Is that a gap?
Sometimes it is a comparison nobody has written yet, and the screen says so. Sometimes it is a statement about the operation: a check-in adds stock rather than drawing it down, so there is no balance for it to have outrun and nothing to compare. Both cases carry a sentence, because an empty table with no explanation reads as "nothing changed", which is the one thing it must never mean.
What if the record has been deleted?
That is reported as its own outcome rather than as an error, because it is the clearest possible answer: there is nothing left for your queued write to apply to, so the entry should be discarded. Treating it as a failure would leave you retrying a fetch for a record that will never come back.
How do you decide which rows actually disagree?
Per row, by the code that understands that row, rather than by comparing the displayed text. "Approve" against "Rejected" is a real disagreement; "approve" against "approved", 40 against 40.00, and a reason code against its label are not. A screen that flags formatting as a conflict is one people stop reading, which puts them back to deciding on a sentence.
Does it compare every line of a large count?
The first eight, and it tells you how many were not compared. Each line costs a check against live stock, so the compromise is stated rather than hidden — silently comparing eight and presenting it as the whole entry would be a conflict screen that misses conflicts. Comparing every line in a batch is a defined piece of commissionable work.
Can I see what the record looked like when I captured it?
Not today: the left column is what your device captured, and nothing records what the server held at that moment. Reconstructing it from the payload would be a guess presented as history, on the screen where somebody decides whether to destroy their own record — so instead the two columns are what they say they are. A capture-time snapshot is one of the more requested extensions here.