Skip to content
← All work

TrimQ

Prototype

TrimQ manages a multi-branch barbershop queue: a live queue board, an intake form priced and timed per service, a self-refreshing waiting-room display, and per-branch revenue reporting, all built around the fact that barbershops in Ghana run on walk-ins rather than bookings. It was captured against a freshly seeded demo database rather than a live shop, and every name, phone number, and price in its screenshots is invented by the seed script that produced them.

Role
Sole author
Period
2025
Status note
Tested against a freshly seeded demo database. Never piloted in a live shop.

The problem

A barbershop that runs on walk-ins has no calendar to schedule against. Software built for appointment-based businesses assumes a booking exists before the customer does; a barbershop customer's only appointment is showing up and taking a number. The shop still has to tell that customer how long the wait is, and it needs a display a room full of waiting people can read without a staff member calling names.

The constraint

It's a multi-branch business, and one live queue has to serve three different audiences from the same data at once: the customer taking a ticket, the staff assigning barbers and completing services, and an unattended screen on the wall that nobody has to be looking at for it to do its job.

Architecture

A single Flask application, app.py, serves the queue board, the intake form, ticket generation, and revenue reporting across a shop's branches, with Jinja2 templates for each screen. The waiting-room display is a separate, unauthenticated route meant to run unattended on a screen in the shop; it refreshes by reloading the whole page every thirty seconds rather than holding a connection open. Revenue reporting aggregates across branches from the same completed-service records the queue itself produces.

Decisions, and what they cost

A live queue instead of a calendar

Barbershops in Ghana run on walk-ins, so TrimQ is built around a live queue instead of a calendar, printed tickets instead of bookings, and a self-refreshing waiting-room display as the primary screen rather than an admin dashboard. That's a decision about how the market actually works, not a technical preference: a calendar-first design would ask a walk-in customer for something they don't have and can't give, an appointment time.

Wait estimates are arithmetic on the queue, not a guess about the customer

Every service in the intake picker carries its own duration and price, which is what makes a wait estimate possible at all. The number shown on the queue board and printed on the ticket isn't a prediction about a particular customer; it's the sum of the service durations of everyone already ahead of them in the branch's queue.

Revenue computed on read, not maintained as a running total

The revenue report sums completed services at the moment it's requested rather than accumulating a running total column as services finish. The cost is a heavier query every time the report loads; what it buys is a number that can't drift out of step with the completed-service records it's derived from, because it's never anything other than a direct read of them.

What didn't work

One 2,312-line file, and no tests to put pressure on it

app.py is 2,312 lines and the only Python module in the project, and there is no test suite. The honest explanation is that with nothing to put structural pressure on the file, it just grew as features were added in whatever order the shop asked for them. The fix direction is known and hasn't shipped: blueprints per area, and queue and revenue logic pulled into a service layer. Moving the secret key and database URI out of the code and into the environment has shipped; the restructuring hasn't, because nothing has forced it yet.

More

Honest limits

The wait estimate sums the service durations of everyone in the queue ahead of a customer. It does not account for how many barbers are free, or how far through their current cut they are, so it is deliberately simple and it runs optimistic once the shop is full. The waiting-room display refreshes by reloading the whole page every thirty seconds; there is no websocket and no partial update. And it has never been piloted in a live shop. Everything shown here runs against a seeded demo database, so there are no production usage figures, and none are claimed.

Screenshots

1 / 5

Stack

  • Flask
  • Jinja2 templates