Skip to content

B2B Partner Portal

A distributor’s partner portal, rebuilt so a partner can turn a purchase list into an order without a manager in the middle — on top of a pricing and stock system that was never going to be replaced.

Client
DSSL
Product
Distributor partner portal, redesign
Year
2024–2026
Role
Product Designer
Platform
Web, desktop-first

Outcome. The specification became the thing the system tracks, not the catalogue. A line keeps its source row — file, row number, original text, original quantity — from the imported spreadsheet to the placed order. What it cost: the buyer resolves every ambiguous line by hand, because nothing in the data could confirm compatibility on their behalf.

Team
Sole designer — with frontend, backend, a product manager, QA and the team lead
Product / research
Product manager on the team; no researcher — the research was mine
Duration
Roughly six months of active work, spread across a long pause
Specification review screen: forty-eight imported lines with their resolution status, with two more screens of the portal behind it
Specification review — the queue where a line’s identity is settled. Data is synthetic.

The old portal answered every question with a manager.

The distributor sells professional video-surveillance equipment to system integrators. Its partners are not shoppers: they buy from a list they already have, for a project that has a date, at prices that belong to their contract. The portal they were given behaved like a shop.

I audited its ten screens before touching anything, and the pattern held across all of them. The left rail was icons with no labels. The dashboard opened with a training banner and promotions, so a professional buyer’s workspace led with marketing. The filter panel listed dozens of technical attributes at one level, alphabetically, with no search inside it and no count of what was already selected. A product card never said whether the item was in the cart, and never said how old its stock number was.

None of that is fatal alone. Together it meant every non-trivial step ended the same way — a message to the personal manager. The manager was the only place a partner’s terms actually lived, and the portal was the thing you used before writing to them.

The real object of the redesign was the specification, not the catalogue.

The brief asked for a modern interface. The unit of work behind it — find a product, add it to a cart — was the shop’s unit, not the buyer’s.

A buyer works in specifications: ten to fifty lines for one project, written in a spreadsheet, passed around by email, corrected by hand. By the time it reaches the portal it already exists. The old flow made the partner key it in again line by line, and the moment a line was ambiguous — a code matching two products, a code the catalogue had superseded, a line whose quantity column could not be read — the only resolution available was a person.

So the object the system has to hold is not the product. It is the line: where it came from, what it originally said, and what it was resolved into. Everything downstream — pricing, warehouse planning, the order, an argument about a delivery six weeks later — needs to be able to point back at it.

Take a specification the partner already has, and carry it to a placed order without losing a single source row.

Before drawing anything, I audited what already existed.

The only hard evidence I had was the old product, so that is where I started: ten screens, one cognitive walkthrough each, four questions per step — goal, discoverability, mapping, feedback — and a severity scale that keeps “blocks the flow” apart from “looks untidy”. Where a static screen could not prove something, such as keyboard order or what happens after a click, I recorded it as not confirmed rather than counting it as a defect.

From there: a brief that keeps what is known about the old product separate from what is a target assumption, and never lets the second quietly become the first. Scenario work across the real buying shapes — a five-line reorder, a fifty-line project, a two-hundred-line import. A market pass over sixteen comparable screens, each recorded as adopt, adapt or reject with its reason, so a pattern rejected once does not come back merely because it surfaced in search again.

Then the design system, then twenty screens, then the implementation in React with a component catalogue on top of it. The last step was a synthetic run: an agent walking nine scenarios through the built prototype, thirty-five runs across three rounds, recorded as real test runs with their own events. That step is what found the things the screens were still missing.

The built prototype, on synthetic data — the same one the agent run walked through.

Open the prototype (external link, opens in a new tab)
Twenty screens as one index — every route the product has, grouped by the job it serves.
One component, every variant it is allowed to have. The matrix is checked in the catalogue, not on the screen.

Six screens were finished before I checked their structure. That order was wrong.

The market pass has two jobs — structure before the screens exist, finish after they do. I ran only the second. By the time I compared layouts against comparable products, fourteen screens were built and passing their own audit, so a structural finding on them had nowhere cheap to land. It costs a paragraph in a spec before the screen exists, and a rebuild afterwards.

The agent run then found seven defects on screens I considered done, two of them critical. The catalogue’s facet rail rendered and filtered nothing. Quick order had no paste state at all — the state a user lands in first — because in the design file it existed as a hidden layer and no one had built it. A stack of toasts could grow tall enough to swallow the button underneath it.

Two of those I wrote up wrong the first time. The toast was not mispositioned: four stacked confirmations had grown 560 px upward and the card was still catching clicks. The orders list was not unopenable: its order number was inert while the only working entry point was labelled “Pay invoice”. Fixing either first diagnosis would have fixed nothing.

Four decisions, and what each one cost.

File import and typed entry are two intakes for one specification. Exactly one screen resolves what a line is.

Why
Two intakes with two resolution mechanics produce two audit trails for the same decision, and a specification is a document people argue about later. The provenance of a line — source file, row number, original text, original quantity — has to survive to the order, or a disputed delivery has nothing to check itself against.
Cost
Quick order lost its own candidate picker. A partner who typed four lines is sent to a different screen to finish two of them, and that hand-off has to be spelled out on the button before they press it.
One ambiguous line, both candidates, and the source row it came from.

Price and availability carry two independent axes: verified, stale or not confirmed — and changed or unchanged.

Why
A price that moved is still a verified price. The first build had a single list of four statuses, which made one component both the source of truth about freshness and the record of a movement. Cart change review reads the movement; the buyer’s trust reads the freshness. One axis could not answer both.
Cost
Two axes on the two most reused components in the product, and a wider column: “not confirmed” prints those words, never a zero and never an empty cell, because a blank in a price column reads as free.
Two lines moved after the import. Each names what changed, why, and which source row it belongs to.

An ambiguous line is never resolved on the buyer’s behalf, however confident the match.

Why
Compatibility rules for this equipment are not formalised anywhere I could verify. Automating a check that does not exist moves the risk of an incompatible delivery from the system to the buyer, who finds out on site, with a crew already there.
Cost
The resolution queue is the slowest screen in the product — a median of 17.9 seconds against 0.9 to 2.8 elsewhere in the agent run. That time is the price of the guarantee, and it is paid in full by the person the guarantee protects.

Navigation does not advertise screens that do not exist. Price lists and a global document registry left the rail; documents live inside the order that produced them.

Why
Neither had a confirmed source of truth or a route behind it. A rail item that leads nowhere teaches that the rail cannot be trusted, and that lesson is more expensive than the missing item, because it is charged against every other item too.
Cost
The back-office job — find one invoice without knowing its order — has no address of its own. It comes back the day a document registry has a real source.
Four items in the rail, and every one of them has a screen behind it.

Forty-five components, and one row I refused to fork.

Twenty-four generic families and twenty-one domain components. The generic half is taken selectively from an open library and only where an approved screen needed it. The domain half — procurement tables, resolution rows, price and availability blocks, shipment groups, fulfillment plans, the order timeline — is the product’s own, because that is where its argument lives.

The most reused element is the product row, and it appears in three places doing two different jobs: choosing, in the catalogue, and reviewing, in the cart. Forking it was the obvious move and the wrong one. It carries a context property instead, and its matrix is deliberately incomplete — a browse row has no disabled state, and a commercial change cannot happen to something you have not added yet. An incomplete matrix with a reason beats a full one that invents states to fill itself.

Coverage is not checked by eye. A script walks the catalogue against the variant matrix, and a smoke run loads every story to catch the ones that compile but do not paint.

ProductRow — Cart / Browse × default · attention · disabled · added
PriceBlock — verified · stale · not confirmed × unchanged / changed
Availability — verified · stale · not confirmed × unchanged / changed
ResolutionRow — exact · ambiguous · missing · changed + parse error
FulfillmentPlan — default · selected · unavailable
EmptyState — no results · nothing yet · could not load

What got solved, and what it cost.

Solved
The path from a forty-eight-line file to a placed order runs without a manager reading it first, and every line keeps the row it came from. Three agent runs out of three reached a created order.
Sacrificed
Speed at the point of ambiguity, and a document registry of its own. Both were traded for guarantees the interface can actually keep.
Why the price was right
Both cuts protect something the system cannot verify by itself: equipment compatibility, and a document source that does not exist yet. A portal that promised either would break on the first real order, and break the trust of the person who believed it.
What changed in how I work
I run the structural check before the screens now, not after. Here I did it in the wrong order once, and a finding that would have cost a paragraph cost a rebuild instead.

Numbers from this project are covered by an NDA and are not published. There is also no baseline to publish them against — the metrics defined for it are a plan for measurement, not a claim of results. The three-of-three above is an agent run, not adoption. What is above it is the compromise, stated in full.

This case probably raised a question.

Most of it is a compromise, and compromises are worth arguing about.

Get in touch

Let's talk

If the case above answered your question — or raised one.