An order portal for 1099 sales reps, on a spreadsheet backend.

Dax Distro sold wholesale flower through independent reps who could not be given a login to the company's order system. Orders came in by text and got retyped. Prices depended on a volume tier that changed with the size of the whole order, so every quote was a lookup, and every commission was arithmetic someone did later. I built a portal where a rep signs in, picks one of their own customers, adds lines from live stock, and watches the tier, the price, and their commission settle as they type. Submit writes the order to the Google Sheet the office already ran on.

Dax Distro · Feb–Apr 2026 · Google Apps Script, then PHP · Google Sheets as the database · retired with the company · ← all work

The problem

Reps in the field, orders arriving as texts and calls, the office retyping them into the wholesale system. A price sheet with five product grades and five volume tiers, where the tier came from the total weight of the order, so a rep quoting one line at a time got it wrong. Commissions worked out by hand at month end. Nobody in the field could see what was actually in stock.

What I built

A web portal with a Google sign-in limited to company accounts. A rep sees only the customers they brought in, builds an order from inventory grouped by grade with stock levels showing, and the app works out the tier from the order's total weight, prices each line, and shows the commission. Submitting writes the order to the sheet, takes the stock down, and emails the office. Admins see every order, move it through its statuses, and run inventory.

What changed

Pricing arguments ended, because the same matrix priced everyone and an override was allowed but logged. Out-of-stock product disappeared from the form instead of being sold. Reps got their own numbers on demand: sales, commission, top customers. The first version was live the day it was started. Six weeks later, after the reps had lived in it, I rewrote it on a faster stack without changing what they saw.

What the rep saw

One page, top to bottom: the customer, the lines, the money. The customer card searches the rep's own book by name, contact, or phone; a new customer is a short form with duplicate detection on phone and email, and a billing address that defaults to the shipping one. The product list is grouped by grade and shows the stock left. Here three lines add up to twelve pounds, which lands the whole order in the Gold tier, and every line is priced at Gold.

Screenshot of the order portal's New Order page. A purple header with the Dax Distro logo and the signed-in rep's name. A Customer card shows the selected customer, Northside Smoke and Vape, with contact, phone, email, and city, plus Edit and Change buttons. An Order Items card lists three lines: Blue Meridian 4 lbs, Sugar Pine 3 lbs, Copper Haze 5 lbs, each marked Gold tier with a price per pound, commission per pound, and line total. A totals bar reads 12 lbs (Gold), order total $9,150.00, your commission $415.00.
The order builder. The tier is computed from all three lines together, and the commission column is the rep's own number, not the company's.

Below the lines: notes, a shipping charge typed in from the carrier's estimate, and a payment method. ACH is the default; a card adds a two-percent fee to the subtotal plus shipping. Commission is figured on product only, never on shipping or fees, and the summary says so in numbers rather than in fine print.

Rules the app enforced, so nobody had to argue about them

After submit

An order gets an ID, one row per line in the sheet, an email to the office with the full order laid out, and a place in the rep's Current Orders list. From there it walks Pending, Approved, Processing, Shipped, Completed, and only an admin can move it. A rep sees a badge; an admin sees a dropdown. Clicking a row opens the lines with the tier each one was priced at, and the totals, fees, and commission underneath.

Screenshot of the Current Orders page. A table of four orders with ID, date, customer, item count, total, and a status badge: Pending, Approved, Processing, Shipped. The first order is expanded to show its line items, Night Harbor 2 lbs and Lemon Lantern 6 lbs at Silver tier, with price, line total, and commission per line, then total weight 8 lbs, subtotal $6,700, shipping $60, payment Credit Card, CC fee $135.20, grand total $6,895.20, and commission $330.
A rep's order list with one order opened. The Silver tier on both lines came from the order's eight pounds, not from either line alone.

The Reports view gives a rep the same four numbers the office would otherwise be asked for, over any date range: total sales, total commission, order count, and average order, with cancelled orders excluded and counted separately, plus a top-ten of their customers. An admin sees the same page across any rep or all of them, and can download the commission run as a spreadsheet.

Screenshot of the Reports page: date preset buttons with All Time selected, a from and to date range, and a Download CSV button; five stat tiles reading total sales $37,825, total commission $1,890, five total orders, one cancelled, average order size $7,565; and a Top Customers by Sales table of five shops.
Reports, from the rep's chair. The commission tile is the number they used to have to ask for.

Where the data lived

The office already ran on one Google Sheet, so the portal used that sheet as its database instead of replacing it. Customers, orders, inventory, the stock ledger, the rep roster, and the form settings are each a tab. The office kept working in the spreadsheet they knew, and the portal put rules in front of it. Around that core sat the connections a distributor actually needs: the website's stock, the accounting system, the product photos, and a nightly backup.

THE REP THE RULES THE DATABASE THE REST OF THE BUSINESS A rep's browser one page, no install Google sign-in, company accounts only orders · customers · reports admins: inventory The API PHP on the web host checks the session recomputes every price locks stock while it changes caches reads for 60 seconds the browser suggests, this decides OC Backend · one Google Sheet Customers · Orders Inventory · InventoryLog AuthorizedReps · Settings a tab each, headers as columns the rep roster is a tab too: add a row, the rep can sign in the office keeps working here JSON Sheets API Email to the office one message per order, one per cancellation WooCommerce (the website) stock pushed out on every order web orders come back by webhook new product → a product page QuickBooks Online new customer or item → created there Google Drive product photo: pending → in stock → the product page, with an AI-written description via n8n nightly copy of the sheet · last 30 kept a failed sync is logged and retried every 15 minutes, so a slow partner never blocks a rep's order

Left to right: the rep, the rules, the spreadsheet, and everything the spreadsheet had to stay in step with. The sheet is the one thing in the picture the office could open and read without me.

The admin side

Admins got an Inventory page the reps never saw: receive a delivery, correct a count with a required note, edit a product, and read the ledger of every change underneath. Adding a product did more than add a row. It created the product page on the company website, pulled the product's photo from a Drive folder and attached it, and asked an n8n workflow to write the description.

The photo side was borrowed. Dax was a spin-off of Elevated, and I had already built the photo editor there, so its Drive folders and its cleaned-up photos were the raw material this portal reached for. What Dax gave back was the other half of the loop. Wiring this portal into WordPress and WooCommerce, stock both ways and a product page per new product, showed me the photo editor could go a lot further than a folder of finished images. When I went back to Elevated, I took that WooCommerce work with me and closed the loop there: the photo editor and the Live Ref sheet now feed the Elevated site and build every product page themselves.

Screenshot of the admin Inventory Management page. A product table with rating, product name, type, potency, a color-coded stock badge, a checkmark or cross for whether the website has a photo, and action buttons per row. Below it, a Recent Activity ledger: date, product, change type (Order, Received, Adjustment, Cancellation), the change amount in green or red, previous stock to new stock, who made the change, and a note such as an order ID, a delivery invoice, or 'damaged in storage'.
Inventory with its ledger. Every number in the Stock column has a history underneath it, and a cancellation shows up as stock coming back.

Two builds in six weeks

The first version was a Google Apps Script web app: one HTML file and one script, embedded in the company's WordPress site, live the same day it was started. Sign-in was email plus a PIN, because Google's own sign-in misbehaved inside that sandbox when a rep had two Google accounts in the browser. It worked, and the reps used it for two weeks. Its one real flaw was the platform: Apps Script wakes up cold, and a rep tapping the link on a phone waited five to fifteen seconds for the page.

So I rewrote the backend in PHP on the same host as the company website. The two dozen script functions became service classes behind a small API, the page's calls became ordinary fetches, sessions became signed tokens, sign-in became Google's, restricted to company accounts, and a memory cache kept the common reads under two hundred milliseconds. The page the reps saw did not change. The old version stayed deployed for a month in case of rollback, and was never needed.

Where it stands

Dax Distro wound down in July 2026 and the portal was retired with it. The code, the docs, and the runbooks are archived. Everything on this page comes from a copy of the portal running on my own machine against made-up customers, made-up products, made-up prices, and made-up orders. No real rep, customer, or price appears here.

The reps had a second tool from the same period: a lead-tracking app on their phones, backed by another Google Sheet.

Stack

PHP 8.4 · Slim 4 Google Sheets API Google sign-in JWT sessions Memcached Gmail SMTP WooCommerce REST + webhooks QuickBooks Online API Google Drive API n8n Cloudways Google Apps Script (v1)

A spreadsheet as the database is a choice people apologize for. Here it was the point: the office could read it, fix it, and back it up without a developer, and the portal's job was to keep it honest.

Built in conversation

Same method as everything on this site. The pricing engine, the rep-scoping, the two-way stock sync, and the PHP rewrite all came out of dialogue with Claude, one feature at a time, each one tested with the reps before the next. The rewrite from Apps Script to PHP took days rather than weeks because the first version was already the specification.

The philosophy, or what I'm building right now.