Context
Rafiq Jamil Textile Industries runs spinning, towel, stitching and finishing divisions. Several hundred staff across shifts. Certified GOTS, OEKO-TEX, BSCI and Sedex — meaning the business is regularly audited on labour practice, and has to be able to evidence how it treats its workforce.
The company could manufacture to international standards. It could not reconstruct, from records, who worked which shift last Tuesday.
The problem
HR ran on paper and WhatsApp.
Attendance was recorded on printed sheets. Leave was requested verbally or in chat. Overtime was agreed between a supervisor and a worker and remembered rather than logged. Shift changes were a message. Approvals happened in conversation, then travelled by hand to whoever needed to know.
The visible symptom was payroll disputes — an employee saying they worked overtime that payroll had no record of, with no way for either party to prove it. The underlying problem was that the company had no auditable record of its own decisions. Not because anyone was careless, but because the process produced no artifact that could be checked afterwards.
Three consequences followed:
- Nothing could be reconstructed. A disputed month meant asking people to remember. Whoever remembered more confidently won.
- Nothing could be delegated safely.Because approval authority lived in people's heads rather than in a rule, work concentrated on whoever was trusted to remember correctly.
- Nothing could be audited. For a business whose certifications depend on demonstrable labour practice, the absence of a record is itself the risk.
Why the previous approach failed
The company had tried spreadsheets before. They failed for a reason worth stating plainly: a spreadsheet records state, not events. It can tell you that an employee has six leave days left. It cannot tell you who approved the four they took, when, on what basis, or what the balance was before the decision was made.
Every question that mattered was a question about history. A spreadsheet stores the present and overwrites the past.
What I did
I led requirements discovery, designed the data model and workflow logic, specified the system, and built it with AI-assisted development. This was not a purchased product configured to fit — it was designed around how the factory actually operates.
Discovery
I mapped the real process before designing anything: sat with the current paper flow, traced what actually happened to a leave request from the moment a worker asked to the moment it affected their pay, and documented where decisions were made and where they evaporated.
The important finding was that the process was not undocumented because it was informal. It was undocumented because nobody had ever been asked to write down who is allowed to approve what. The rules existed — they lived in seniority and habit.
Most of the work was writing those rules down, checking them with the people who held them, and encoding them.
The system
More than 45 tables, with row-level security switched on for every one, across people, attendance and leave, payroll, loans and settlements, hiring, discipline, and infrastructure. A React and TypeScript frontend runs against a PostgreSQL database, with Python serverless functions handling every privileged operation server-side.
The ledger decision
The central architectural decision was to model leave as a transaction ledger rather than a balance field.
A balance field is the obvious design and the wrong one. If an employee's record simply says casual_leave_remaining: 6, then approving a request decrements a number and the reason is gone. Every dispute becomes irreconcilable.
Instead, leave balance is derived: a baseline snapshot plus earn rows minus use rows, each linked to the request that caused it. The balance is always a computation over history, never a stored figure. Any number the system displays can be traced to the decisions that produced it.
This is the difference between a system that reports and a system that can be audited.
Encoded rules
The workflow logic that previously lived in people's judgment is now explicit:
- Five leave categories, each with its own accrual model — some annual, some scaling with length of service, some accruing from compensatory earn rows within a rolling window
- Accrual cycles anchored to each employee's own join-date anniversary rather than a shared calendar year
- A service eligibility gate before any request is accepted
- Half-day handling, applicable to some categories and not others
- Overtime claims requiring supporting documentation where the category demands it, with approved claims writing to the ledger for payroll reconciliation
- Eleven roles, plus per-user permission keys that can be switched off one at a time. A build check fails the deploy if the permission editor and the backend ever disagree
Requests that would exceed an employee's balance are flagged and surfaced to the approver, not blocked. This was a deliberate decision. Real operations include justified exceptions; a system that makes them impossible gets worked around, and the workaround is invisible. Better to permit the exception and record who authorised it.
Safeguards
Five engineering decisions worth naming, because each came from a real failure mode:
Idempotency on approvals.A double-click on an approval button could insert two ledger rows for one request, silently corrupting the balance. Fixed structurally with a uniqueness constraint on the request reference, not with a disabled button. UI guards fail; database constraints don't.
Audit rules depend on what is being written. Producing an output, such as a PDF, never fails because the audit write failed. Creating or changing a record does: an application or a document added with no trail is worse than a failed save. Where the line falls is a decision, not a default.
A stale tab cannot overwrite payroll. Every payroll row carries the timestamp it was loaded with. If anyone saved a row after the tab opened, the whole save is refused and nothing is written, so a batch is never half old and half new.
Audited figures are frozen. Once a payroll batch leaves draft, the server rejects every edit. An auditor can flag a single entry and discuss it, and the batch cannot be approved until every flag is closed.
No cascading deletes on audited records. Deleting a user nulls the reference rather than removing the history attached to it. Audit integrity survives personnel changes.
The audit trail
Every consequential action writes an entry capturing the action, the module, the affected record, who performed it, their role at the time, and full before-and-after snapshots of the changed data.
The “role at the time” detail matters more than it appears. Roles change. An audit log that resolves the performer's current role misrepresents history — it shows what someone is now, not the authority they held when they acted.
What is live
Authentication and role-based access with per-user permission overrides. Employee records with document management and ID cards. Attendance from machine counts, checked against shift-incharge verification sheets and imported through Excel with a preview first; every manual correction is logged and marked on the printout. The full leave lifecycle across all categories. Overtime claims, attendance amendments, shift changes. Monthly payroll with an audit stage, CFO approval, and payslips. A bank salary register that splits pay between bank and cash, with account changes needing approval and a scanned letter. Loans from request to instalment tracking, with release and payout held by different roles. Final settlements. Daily wage workers. Division cost allocation. Hiring from requisition to onboarding, plus job applications on a printed form with QR verification and a probation stage. Disciplinary cases, complaints, and performance ratings. Task assignment with extension requests. Reporting across all modules. A complete, filterable audit trail.
What is not
Stated plainly, because a case study that claims completeness invites the question it can't survive:
- Live machine attendance is in progress. The old attendance machines are being replaced with new ones. Until they are connected, machine counts arrive through an exported sheet.
- Leave deductions are entered as payroll adjustments. They are not yet calculated automatically from the leave ledger.
- Inspections is a placeholder. The design questions are written down; the workflow is not built.
- Automatic income tax is switched off. It stays off until the current tax slabs are loaded and checked.
Outcome
The honest framing: this system is live and in daily use, and the outcome is structural rather than numerical.
What changed is that the company now has a record. Attendance, approvals, overtime, and disciplinary decisions produce artifacts instead of memories. A disputed month is now a query rather than an argument. Approval authority is encoded rather than assumed, which means it can be delegated without being lost.
I have not published time-saved or error-reduction figures because the before-state was paper and chat — there is no baseline to measure against honestly. Producing one would mean inventing it.
What I would do differently
The leave ledger should have been the design from the first version rather than something I arrived at. I started with balance fields because they were simpler, and rebuilt when the first disputed request proved they couldn't answer the question being asked. The lesson generalises: when the questions a business asks are historical, the data model has to store events, not state.
Details of internal policy — specific entitlement figures, division codes, and role structures — are omitted. This case study describes system design and engineering decisions, not the client's internal policy.