[Interview]
Traced to a specific person and date. Highest authority. Overrides everything else when it conflicts.
Replaced a legacy desktop inventory tool and a disconnected ordering system with one platform, designed as a small set of rules that hold across 40+ screens instead of screen-by-screen decisions.
About DEF Beauty Supply
DEF Beauty Supply is a B2B beauty wholesaler serving professional salons across Italy. The business ran on a legacy desktop inventory tool plus a disconnected WeChat ordering system: fifteen people, no shared source of truth. I designed and built its replacement, an ERP that treats sales, warehouse, finance, and procurement as one system instead of four.
Role
Research, Product Design & Full-stack Engineering
Solo, including interviews, system design, and implementation.
Team
Solo, with an AI coding agent (Claude Code) as implementation partner.
Scope
Platform-wide: dashboards, sales, procurement, inventory, customers, finance
Company
DEF Beauty Supply
Year
2026
01 / Context
DEF Beauty Supply sells professional nail, hair, and beauty products wholesale to salons across Italy, through a legacy desktop inventory tool and a WeChat mini-program that never talked to each other. Neither system knew what the other believed was true.
The gap between 8 real jobs and 5 permission roles is real; I kept it visible instead of inventing roles nobody asked for.
02 / Before
Storefront, sales, inventory, procurement, receiving, finance: six parts feeding each other. None of that was one system. Every arrow below was, in practice, a person re-typing what another screen already knew.
The business this had to serve, drawn as one system. It never was one system: see the actual purchasing screen, dragged against its replacement, in chapter 05.
03 / Research System
I interviewed sales, warehouse, finance, procurement, and the owner, one person per role. Instead of writing a research summary and moving on, I built the findings into a single living file (UX.md) that every design decision, mine or the AI agent's, has to cite. Every line carries a source tag:
[Interview]
Traced to a specific person and date. Highest authority. Overrides everything else when it conflicts.
[Code]
Verifiable directly in the repository right now.
[Decision]
Already settled and written down, not open for silent reinterpretation.
[Assumption]
Not yet validated. The agent is told not to treat it as fact.
Why this matters
04 / One Skeleton, Five Roles
Every dashboard keeps the same shell: KPI tiles, a trend chart, a “tasks to handle” panel. What changes is which numbers earn a tile, sourced from what each role told me they actually check. The legacy backend already had a page called “Dashboard” — same numbers for everyone, owner or warehouse hand. Drag to compare it against the Admin view it became.


Before: one Dashboard page for every login, mixing task types that belonged to different jobs. After: the same shell, reweighted per role — this is the Admin view, one of four.
1 of 4
Two pieces of that shell, running here instead of as screenshots: the trend chart and the customer-type mix, on the same component code and demo data as the Admin view.
Last 30 days net sales, refunds deducted
The rule, not the screen
05 / Rules Over Screens
A purchase order carries seven statuses in the database. The interface shows exactly four. I asked the procurement lead whether a middle state like “sent to supplier, awaiting confirmation” was worth adding. The answer: no, just upload the confirmation when it comes back.
Seven database statuses, two of them legacy. Four buckets a person actually reasons in.
Negotiating confirmed price against list price and document discount didn't exist as its own screen before — the legacy system had one moment for a purchase: receiving.

Products group by SPU, collapsed by default. Pricing panel on the right tracks document-level discount and shipping threshold, both of which change the per-line math.

Expanding a group reveals SKU-level rows. Estimated quantity and price sit next to editable confirmed fields, the pattern used everywhere OCR fills a value: the system proposes, the person on the counter decides. Column widths are fixed so expanding a row never reflows the ones a person already checked.
A product can't get a selling price until its real cost is known. Italian suppliers stack discounts like “50%+50%+7,69”, and each tier applies to what the previous one left, not to the list price. This calculator gives the real unit cost before ordering, and the same number once the supplier confirms.
Real purchase cost per unit
Incl. IVA 22% · estimate before ordering, confirmed after upload
If the tiers were added
50% + 50% + 7.69% = 107.69% off
−€1.5380
Negative cost
Chained, as suppliers mean it
0.5 × 0.5 × 0.9231 = 0.230775
€4.6155
End on the number pricing needs
The chain resolves to one figure, the tax-inclusive cost per unit. That is what gets saved as the product’s purchase cost and what the selling price is marked up from.
Same math before and after the order
Before ordering, a buyer enters a supplier’s discount terms to estimate what one item will really cost. After the confirmation is uploaded, the confirmed price is computed the same way, so the estimate and the final price never disagree on method.
Accept the text exactly as printed
“7,69” is 7.69 and “1.250,00” is 1250. The parser reads whichever separator comes last as the decimal and ignores prefixes like “sc.”, so nothing has to be retyped.
Receiving is where the two systems overlap: both record what physically came in. Drag to compare the same task, years apart.


Before: one flat table, a generic supplier label, a receiving date and warehouse, no distinction between what was ordered and what arrived. After: ordered and received quantities sit side by side per SKU, with the actual discrepancy (“short on Classic bonder 8ml”) recorded in place, not on a separate exceptions screen.
06 / One Rule, One Exception
Every action that touches stock or money is irreversible once confirmed. The default rule: state the consequence, then ask — not “Are you sure?”
Confirm stocktake — dialog copy
About to: · Adjust stock for 12 SKUs · Record €340.50 in losses (posts to finance) · This cannot be undone
Then one interview broke the default on purpose: a customer finishes checking out, remembers one more item, while the next customer waits. A confirmation dialog here is not caution, it is a tax on the line. So hold/resume uses an instant toast instead:
Held: Zhang San, 5 items
Hold and resume aren't new — the legacy system had the same buttons in the same corner. What changed is context per row: packaging tier, live stock, oversell warning, gift flag, all inline at the moment of typing a quantity.


Same page, same hold/resume buttons. Packaging tier, live stock, and tax breakdown are the new additions per row.
Logged, not hidden
07 / Two Cadences
The disagreement sits at the last step of an order's life: when does money become an invoice?
Sales wanted
Invoice the moment an order closes, then strip out anything that turns out not to be invoiceable during review.
Finance needed
B2B customers settle in batches over time. An invoice only makes sense once payment has actually come in, sometimes covering several orders at once.
Neither side was wrong — two real cadences in the same business. The system holds both timelines instead of forcing one workflow to be “correct” and the other a workaround: a sales order tracks shipment and payment as two independent tracks, not one combined status.
A shipped order can still be unpaid. A paid order can still be waiting on a partial shipment. Invoicing reads the payment track, not the shipment track — which is why it can't just fire when an order closes.
08 / One Order, End to End
Two of the legacy steps weren't features, they were notes fields standing in for features — and one didn't exist at all.
Before: adding products took two separate mechanisms (old-item picker, new-item picker), and payment, shipping, and invoicing were all the same move — write it in a notes field, or nowhere. After: one search adds anything, and the three money-and-goods steps each have a real status the rest of the system can read.
Four screens along that flow, from the sales counter to finance — captured from the production code, with English demo data.

Building the order: one search box adds anything, but each line keeps its own packaging unit — plain text when a SKU has one unit, a dropdown when it sells by box or carton. There's no separate “add gift” flow: any line becomes a gift by toggling its own row, and drops out of the total.

Saving the order asks two separate questions. Was it paid, and how? Did the customer take goods with them? A partial hand-off isn't a status someone picks — it's simply picking up fewer than were ordered, line by line.

The same order afterwards: shipment is partial — one pickup done, a courier delivery still in draft, two bottles not yet allocated — while payment is already complete. Invoice status and payment status sit in separate boxes, so neither is inferred from shipping.

Where the payment track turns into an invoice: finance picks lines out of paid orders and sets the amounts, instead of editing a document generated when the order closed. The gift line is listed but carries no amount; shipping becomes its own line.
09 / How I Worked
I made the calls this case study describes — the status model, the confirmation policy, the dashboard skeleton, the invoicing compromise — and wrote them down before any code existed. Claude Code built against that written record, not against a conversation.
01
Spec
What the screen must do, which states it passes through, which existing rule it has to match.
02
Agent builds
Claude Code implements the React/TypeScript frontend and Supabase backend against the spec.
03
I review
Against the same spec and UX.md, not against memory of what I asked for.
10 / Honest Status
| Module | Status | Note |
|---|---|---|
| Phase 0 · Foundation | Shipped | Supabase, Vite, and Workers scaffolding |
| Phase 1 · Auth | Shipped | Login, JWT roles, route guards, dashboard |
| Phase 2 · Products | Shipped | Categories, products, SKUs, multi-tier packaging |
| Phase 3 · Procurement | Shipped | Suppliers, requests, orders, receiving |
| Phase 4 · Sales | Shipped | Order creation, shipping, returns, exchanges |
| Phase 5 · Customers | Shipped | Accounts, fund ledger, tiers |
| Phase 6 · Finance | Shipped | Invoicing, payment ledger, reconciliation; Fatture in Cloud sync not yet connected |
How correctness is checked
57.38 × 1.22 × 5) kept permanently as a test. Most runs against a mock backend; a gated subset writes to the real database. Not yet wired into the deploy pipeline, so a bad merge can still ship without failing a check.Accessibility, audited and fixed
I ran axe-core across the live code — dashboards, sales, finance, procurement, inventory, customers, settings, and the two money dialogs. Most failures came from a few shared pieces, so the fixes were systemic rather than page by page: the brand blue moved from #197afa to #1366d6, status text got its own darker tokens, every select and number input now has an accessible name, the open-pages strip became a keyboard-operable tab list, and order details use valid description lists. The 7 remaining are 16px stepper arrows beside a full-size number input, covered by WCAG 2.5.8's equivalent-control exception.
What's next
WeChat storefront cutover
Still runs on a third-party SaaS backend, not this platform's own data. The plan connects it through a thin API layer, paired with a visual agent already generating product cover and detail shots.
Fatture in Cloud connection
Invoices generated here don't reach Italian e-invoicing yet. Wiring up that API is the remaining piece of Finance.
Known limits
Interview coverage
One person per role. Enough to expose mechanics, not enough to prove it generalizes across all fifteen staff.
Confirm-dialog migration
Tracked in chapter 06, not finished.
A separate, deeper case study covers the AI procurement agent built on top of this same platform: Procurement Agent for DEF Beauty Supply. This page is about the platform underneath it.
11 / Recap
01
One skeleton, reweighted
Four dashboards share a shell; what changes is which numbers earn a tile, sourced from what each role told me they check.
02
Collapse to what people reason in
Seven backend statuses become four buckets, because an interview confirmed the extra states have no owner.
03
Confirm by default, except where a queue is real
One consistent policy, one logged exception, and the remaining debt tracked instead of hidden.
04
Two tracks, not one status
Shipment and payment move independently, so sales can hand goods over and finance can invoice on its own cadence.