Allen Padilla

Computers For Schools Manitoba • Shipped 2020

Operations Platform

Replaced an aging Microsoft Access database with one Laravel app for donations, inventory, warehousing, orders, RMAs, and recycling. Over 100,000 inventory records, thousands of orders, and still running the organization today.

Role
Sole developer, built from scratch
Timeline
Built Aug 2020 – Jun 2022, supported on contract to Aug 2024, still in use today
Team
Solo, with the Executive Director and warehouse, refurbishment, and admin staff
Stack
Laravel 8, MySQL, mPDF

Context

Computers For Schools Manitoba takes in donated computers, refurbishes them, and provides them at affordable prices to eligible clients. When I started in 2020, all of that ran through an old Microsoft Access database that could hardly be updated anymore.

This was my first real project. I started with a plain PHP app to replace it, but as I learned more about application security I felt it needed a proper framework, so I moved it to Laravel. I'd never used it before. The Executive Director saw what it could do, and it grew into a full app used across every Manitoba location.

DatabaseWEB home dashboard with weekly cataloguing and production counts
The home dashboard in the test environment. Weekly counts for cataloguing, pre-production, and orders, with a leaderboard so staff could see how their week compared.

Problem

10 operational areas, each with its own workflow and its own people.

  • Donation intake and donor records
  • Asset tracking for donated equipment, with custom material attributes and subtypes
  • Stock transfers between locations, with a full history for chain of custody
  • Client records, orders, tax and payment handling, and order assignment
  • RMAs and returns
  • Received shipments and recycling loads, with regulatory reporting

The people doing the work needed to see just their part. Leadership needed reporting across all of it.

Approach

  • Laravel 8
  • MySQL
  • Blade
  • mPDF

Built with the people using it. I got requirements straight from the Executive Director and the warehouse, refurb, and admin staff. I modelled each department's workflow with the people who actually did that work, and kept adjusting as they used it.

Asset tracking. Every piece of donated equipment gets its own record with custom attributes and subtypes. Stock transfers between locations are logged with a full history, so each item has a chain of custody.

Cataloguing a material from a donation load
Cataloguing one item out of a donation load. Storage devices get attached to the material so they can be tracked and wiped separately.
Material lookup with a computer's details open
Material lookup. Each item has its own record with specs, storage, pre-production status, history, and its current location.

Orders. Client records, tax and payments, order assignment, file attachments, CSV bulk import, and PDF invoices with mPDF.

Viewing an order with its product counts and print options
An order in production. Product counts on the left, the products themselves on the right, and one-click printing for labels, contents, and the order sheet.

Access. I wrote the role and permission layer from scratch, so admins, supervisors, and floor staff each only see the modules that matter to them.

RMAs and recycling. Returns go through their own RMA flow. Received shipments get logged as a whole, then broken down item by item, so one shipment of recycled equipment can average over 100 entries and still trace back to where it came from.

New RMA form
Opening an RMA. The material, the reported problem, who brought it in, whether data needs recovering, and a checklist of what came with it.

Reporting and feedback. Quarterly and annual dashboards, plus an in-app issue reporter so staff could flag problems without leaving the system.

Hosting. It ran on a Windows Server on site. I wrote batch scripts to back up MySQL every day and pull new versions whenever I pushed them.

Decisions

  • It started as plain PHP and moved to Laravel once I'd learned enough about application security to want a proper framework behind it.
  • Storage devices are attached to a material instead of being part of it, so a drive can be tracked and wiped on its own.
  • A received shipment is logged as a whole and then broken down item by item, so hundreds of recycled entries still trace back to one load.
  • The role and permission layer is written for this app, so admins, supervisors, and floor staff each see only their modules.
  • It ran on a Windows Server on site, with batch scripts for the daily MySQL backup and for pulling each new version.

Scale

Rough numbers from the last time I checked in on them.

0+

Inventory records

0+

Devices placed with eligible clients

0+

Orders

Codebase

0

Database tables

0

Controllers

0+

Views

0

Commits

Outcome

It's the system the organization still runs on for donations, inventory, warehousing, orders, returns, and recycling. It passed 100,000 records while I was there. After I moved to my next job I kept supporting it on contract until August 2024, shipping new reports and dashboard tweaks whenever they needed them.

Retrospective

I went back through the repo before writing this. Here's what I'd change.

Foreign keys. There are 46 migrations and not one foreign key. Every foreignId column is just an unsigned integer, so nothing in the database stops an order from pointing at a client that was deleted. I'd chain constrained() with an onDelete rule on every one of them from the first migration, and I'd use Postgres while I was at it. MySQL was what I knew from phpMyAdmin, not a choice.

Migrations. Each table was created once and never touched by a migration again, because I was changing columns straight in phpMyAdmin on the server. I'd make every schema change a migration now and check in a schema dump, so the repo is the record of what the database looks like.

Models. 49 models and not one relationship on any of them. Every join is a hand-written DB::table query in a controller. I'd define the relationships on the models, eager load with with(), and let Eloquent do the joins.

Writes. Saving an order hits three tables and none of it is in a transaction, so a failure on the second insert leaves an order with no line items. Client numbers like C-000123 are built by reading the next auto-increment off SHOW TABLE STATUS before the insert, so two people opening the form at once get the same number. I'd wrap multi-table writes in DB::transaction and format the number from the row's id after the insert.

Controllers. OrderController is 1,500 lines. Validation is 105 separate Validator::make calls spread through the controllers instead of Form Requests, and PDF merges through mPDF run inside the request because the queue driver was sync. I'd pull validation into Form Requests, move the slow work into queued jobs, and keep controllers to routing and responses.

Imports. The CSV importer reads the whole file into memory, then calls session_start() and parks the rows in PHP's own $_SESSION between the preview step and the approve step, going around Laravel's session entirely. I didn't know the framework well enough yet to know that was wrong. I'd stage the rows in a table and validate them one at a time.

Access. There are three permission systems in the codebase. An access_level integer that the middleware checks, a permits table the controllers query by name, and spatie/laravel-permission installed with its trait on the Employee model but never called. Only the integer gates anything. I'd actually use spatie, write Policies, and delete the other two.

Schema. I decoupled a lot of the data model, storage devices off materials, shipments broken into line items, that kind of thing. I've come full circle on some of it since. Nearly every table got its own controller and set of views, and most of that was ceremony around things that could have been columns.

Tests. The tests folder is the two ExampleTest files Laravel ships with. Every change above is a refactor I could only make safely with feature tests around the order and intake flows, so those would come first.