The problem
The firm ran daily reporting on a Google Form. Hours arrived as free text, so partners couldn’t tell which client an hour belonged to — and supervisors chased late reports by hand.
What I built
A Laravel/PostgreSQL platform where every entry is attributed to a client, project and department, with a compliance engine that classifies reports on-time, late or missing on its own.
The result
300+ reports a week from 60+ users across all 6 departments, running at 91.7% compliance. Handed over with in-app documentation — and still running today.
Engineering detail
- Attribution from day one. Consolidated 4 separate admin screens into one System Configuration area and loaded the firm’s full catalogue — 100+ clients and 1,000+ projects — before go-live, so hours accumulated per engagement rather than leaving the backfill to someone else.
- Compliance that holds up. ComplianceService classifies every report automatically, honouring approved leave, holidays and weekend deadlines. I later closed a gap where any submission counted as compliant regardless of supervisor approval — which would have made the whole measure meaningless.
- Reconcilable numbers. Built Excel and PDF export for the accounting side, then switched PDF rendered-time from decimal hours to exact minutes so reports could be matched line-by-line against physical time records.
- The record survives turnover. A reporting-line transfer reassigns a departing employee’s open reports to a named successor with a full audit trail, backed by a lifecycle workflow covering all 5 employment types.
- Two ways to clear a backlog. Approval used to mean working through Gmail. I kept that route — rebuilt over Brevo SMTP with proper action pages — and added in-system approval beside it, plus bulk approve.
- Built for how people actually work. Shipped as an installable PWA because most staff file from their phone at the end of a shift, and wrote 16 in-app guides so the firm wasn’t dependent on me being reachable.
- Treated as production, not coursework. PR-reviewed Git workflow with staging and production deploys on Railway, regression tests via Laravel Boost. When an intermittent 419 error appeared, I traced it to how the framework refreshes CSRF tokens instead of blindly extending token lifetime.