Skip to content

← Back to the project on the home page

Iyabo Hair

Live · Private access

Iyabo Hair

An operations tool for a wig business that runs on couriers and mobile money.

Try the demo

Client
Iyabo Hair, Dakar
Role
Sole designer and engineer
Year
2026
Iyabo Hair

One order, three ledgers

Iyabo HairIyabo HairLiveSimulation · fictional data
What is real?

Simulation built on the code of Iyabo Hair, fictional data.

Real code
The rules for what is owed, paid and remaining, for what a courier holds and hands over, and for how a delivery status moves the product come from Iyabo Hair's code, consolidated and checked against the original calculation. The statuses and labels are the application's.
Fictional data
The twenty orders, the three couriers, the products and the prices are invented. No client and no phone number.
Added illustration
The “What if it were a single record?” switch is an explanation added by this site. It is not a feature of the application. The screen layout is also recomposed for the demo.
Iyabo Hair

Iyabo Hair

The demo loads only when you ask for it.

  1. 01

    The problem

    Iyabo Hair sells wigs across Dakar. A wig can leave the shelf on Monday, be delivered on Wednesday and be paid for on Friday, partly, and sometimes to the courier instead of the shop. A single order status cannot tell those three stories apart.

  2. 02

    Three ledgers

    Every order carries three separate records: where the product is, how the delivery stands, and what has been paid. Changing the delivery status moves the product physically (with the courier, delivered, returned) but never touches the money. Recording a payment never changes a status.

    Under the hood

    The order has a status for the product, a status for the delivery, and no money field: money is worked out from typed payments.

    File : Iyabo Hair · prisma/schema.prisma · lines 191 to 201

      // Les TROIS suivis indépendants  productStatus  ProductStatus  @default(EN_STOCK)  deliveryStatus DeliveryStatus @default(A_PREPARER)  // Le suivi ARGENT est calculé à partir des Payment liés :  // resteAPayer perruque = quantity * unitPrice - somme(Payment PERRUQUE)  // resteAPayer livraison = deliveryFee - somme(Payment LIVRAISON)   // Qui est prévu pour encaisser ce qui n'est pas encore payé  // (détermine ce que le livreur doit récupérer à la livraison)  perruqueEncaisseur  PlannedCollector @default(LIVREUR)  livraisonEncaisseur PlannedCollector @default(LIVREUR)

    Real code from the project, copied and anonymised.

  3. 03

    Every franc has a place

    A payment is either for the wig or for the delivery, never both, and it records who physically received it. That is how the app can answer where each franc is. A delivery cannot be closed with a silent unpaid balance: the courier confirms what was collected and how, or reports a problem.

    Under the hood

    Before a delivery is closed, the remaining amount of each track is worked out and any unpaid balance blocks it.

    File : Iyabo Hair · app/livreur/livraisons/[id]/actions.ts · lines 106 to 132

      const payeP = order.payments    .filter(function (p) {      return p.type === "PERRUQUE";    })    .reduce(function (sum, p) {      return sum + p.amount;    }, 0);  const payeL = order.payments    .filter(function (p) {      return p.type === "LIVRAISON";    })    .reduce(function (sum, p) {      return sum + p.amount;    }, 0);   const restePerruque = order.quantity * order.unitPrice - payeP;  const resteLivraison = order.deliveryFee - payeL;   // Cohérence : si un montant restait dû et que le livreur ne  // l'a pas reçu, il doit passer par "Problème" plutôt que  // valider une livraison avec un impayé silencieux  if (restePerruque > 0 && perruqueRecue !== "OUI") {    return "La perruque n'est pas payée (" + restePerruque.toLocaleString("fr-FR") + " F). Encaisse-la ou signale un problème.";  }  if (resteLivraison > 0 && livraisonRecue !== "OUI") {    return "La livraison n'est pas payée (" + resteLivraison.toLocaleString("fr-FR") + " F). Encaisse-la ou signale un problème.";  }

    Real code from the project, copied and anonymised.

  4. 04

    A prepaid fee is not owed yet

    When a customer pays the delivery fee in advance, the shop holds that money, but it only becomes the courier's once the order is delivered. A failed or postponed delivery leaves it pending. A courier's cash handover is the money they hold minus the fees now owed to them.

    Under the hood

    The fees collected by the shop count as owed to the courier only on delivered orders; the others stay pending.

    File : Iyabo Hair · app/livreur/caisse/page.tsx · lines 39 to 61

      const livraisonsAcquises = await prisma.payment.findMany({    where: {      type: "LIVRAISON",      remiseId: null,      collectedBy: { role: "ADMIN" },      order: { livreurId: livreurId, deliveryStatus: "LIVREE" },    },  });  const totalAcquises = livraisonsAcquises.reduce(function (sum, p) {    return sum + p.amount;  }, 0);   const livraisonsEnAttente = await prisma.payment.findMany({    where: {      type: "LIVRAISON",      remiseId: null,      collectedBy: { role: "ADMIN" },      order: { livreurId: livreurId, deliveryStatus: { not: "LIVREE" } },    },  });  const totalEnAttente = livraisonsEnAttente.reduce(function (sum, p) {    return sum + p.amount;  }, 0);

    Real code from the project, copied and anonymised.

  5. 05

    Try it yourself

    Open an order, change its status, record a payment, deliver it as the courier, then hand over a courier's cash and validate it as the shop. Switch on the question about a single record to see what one status would hide. That block is an illustration added by this site, not a feature of the application.

What you can do here

  • Change the delivery status of twenty made-up orders
  • Record a payment and see what is still owed
  • Close a delivery as the courier
  • Hand over cash and validate it as the manager
  • See the product, delivery and money ledgers update separately

Stack

Next.js 16 · TypeScript · Tailwind CSS v4 · Prisma 7 · Neon Postgres · NextAuth v5 · Vercel

Context

Iyabo Hair sells wigs across Dakar. Orders arrive through social media, stock moves through a small team, and couriers deliver on motorbikes and get paid in cash or mobile money. Before this app, all of it lived in notebooks and chat threads. Nobody could answer two basic questions at the same time: what do we actually have, and who currently owes whom.

The problem

The owner did not need a shop. She needed to stop losing money in the gap between three things that move at different speeds: the product, the delivery, and the cash. A wig can leave the shelf on Monday, be delivered on Wednesday, and be paid for on Friday, sometimes partially, sometimes to the courier rather than the business. Any system that treats those as one event will quietly produce wrong numbers, and wrong numbers in this business are somebody's salary.

Constraints

  • Staff work entirely from mid-range Android phones, often on unstable mobile data
  • No point of sale, no card terminal, no accounting software to integrate with
  • Payments arrive through Wave and Orange Money, or in cash to a courier
  • Couriers are not employees and their pay is calculated per delivery
  • The team had never used an internal tool before, so anything confusing would be abandoned within a week

What I built

  • Order entry with stock deduction and per-item margin
  • Delivery assignment, status tracking, and courier settlement
  • Cash ledger reconciling mobile money, cash, and outstanding balances
  • Role-based access for the owner, staff administrators, and couriers
  • Scannable Wave and Orange Money payment codes attached to each order
  • Installable progressive web app, so staff open it from the home screen like any other app

Key decisions

  1. 01

    Three ledgers that never write to each other

    The obvious design is one order record that updates stock, delivery, and cash together. I refused it. Product, delivery, and money are tracked as three independent flows, and no action in one automatically mutates another. Marking a delivery complete does not settle the money. Recording a payment does not close the delivery. It costs a few extra taps, but it means a mistake stays local instead of silently corrupting the other two. In a business where the numbers decide who gets paid, a wrong figure is far more expensive than an extra tap.

  2. 02

    Delivery fees are never the company's money

    The delivery fee belongs to the courier and the wig price belongs to the business. The two are separated at the point of entry and never merged anywhere in the system, including in reports. This sounds trivial until a customer pays one lump sum through a single mobile money transfer. Modelling it as one amount to be split later is how businesses end up spending their couriers' wages without noticing, then arguing about it at the end of the month.

  3. 03

    Prepaid deliveries do not count until they happen

    When a customer pays in advance, the courier's fee is recorded but only counts toward what they are owed once the delivery is actually completed. The alternative, crediting on payment, makes the courier balance look right on the dashboard while being wrong in reality, and reverses badly on returns and failed deliveries. This one rule removed an entire category of end-of-week disputes.

Results

  • Replaced notebooks and chat threads with a single source of truth
  • Installed on staff phones as a home screen app, no store distribution needed