User-facing changes are appended here on every functional edit. The marketplace app-details page reads this file directly.
for every item on their new Prices screen; the platform adds a commission on top to get the price the customer pays. Leave an item blank if you don't offer it. Prices save as you type.
added on top of the vendor's price), set by the admin on the Services page or the Prices page. e.g. at 50%, a ₦1,000 item shows ₦1,500 to the customer; that ₦500 is the pool shared between the agent, marketer, manager and platform.
than one service picks a default vendor per service; a new order goes straight to that service's default vendor (no more "finding a vendor" search). The agent can switch vendor per order, and a manager can still reassign. Set your defaults on the new-order screen ("Make this my default vendor") or per service in your account's Services tab.
that vendor's own price list — what the agent quoted is what the customer pays.
Prices pages now pick a Service + a Vendor and edit that vendor's prices directly (managers only for vendors in their areas).
matter which vendor fulfils it.
tests and scans; diagnostic centres price and run them (blood tests, imaging, cardiac, and more). Appears automatically — no reinstall.
Washing → Drying → Pressing → Ready; an AC job goes Installing → Testing → Ready; a generator/plumbing job Diagnosing → Repairing → Testing; a diagnostic order Sample taken → Running tests → Results ready. Vendors and managers see the right step buttons for the service instead of dry-cleaning wording on every order.
on now shows its default-vendor control right inside the card (no save + refresh), and choosing a vendor opens a searchable list grouped by area — so you can find a vendor anywhere in the country by name or area instead of scrolling a giant dropdown. The new-order vendor picker is grouped and searchable the same way.
several live states (accepted, on the way, in progress, ready, completed, with one in dispute and one cancelled) — not just dry cleaning — so a new install looks like a real, busy marketplace across all five services.
vendor_item_prices table; service_templates.markup_pct; per-service agent_preferred_vendors table (the legacy single agents.preferred_vendor_id column is kept only as a last-resort fallback). New API actions: vendors/set-item-price, agents/set-preferred-vendor, pricelist/for-vendor, pricelist/vendors-for-service, templates/set-markup, templates/set-vendor-item-price, managers/vendor-set-item-price. Lead-submit routes directly to the agent's vendor (broadcast retired for the agent flow; kept dormant for manager reassign). Demo data now seeds vendor prices, agent preferred vendors, and a Medical Diagnostic vendor/agent with orders.
svp_service_flow(): the vendor advance flow (vendor-order-detail), the orders/change-status validator + remit gate, and the manager status dropdown all build from each template's service_statuses (universal anchors + that service's middle stages + the has_return tail) instead of a hardcoded dry-cleaning list. Cross-service demo generator expanded to a full per-service status spread.
hand over / retire a manager — that account is now signed out on its very next action, instead of staying usable until they happened to log out.
is told to get prices set first instead of the order being booked at zero.
cancelled, the completion/cancellation time is now recorded properly so reports and history stay accurate.
switched off.
wording in the agent order screen.
collapsible sections (e.g. Dry Cleaning → Tops, Bottoms, Suits & formal, Traditional, Dresses & skirts, Bedding & home) so you don't scroll past 50 items to find one. Tap a group header to collapse/expand it. Applies to the admin Prices page, the manager Prices page, and the vendor price list. Existing installs get categories filled in automatically (no reinstall).
(was SVP-20260521-0001), and demo orders are short D0001-style codes (was SVP-DEMO-20260521-0001-AB) — far easier to read and read out.
the bulk-removal behind it. There's now no way to accidentally wipe data — to clean up, delete individual users, or reinstall for a fresh slate.
Replace → action: pick a successor and all their areas, agents, vendors, service assignments and open issues move to that person; the old manager is set inactive (kept for payout history, never deleted). Managers still can't be hard-deleted.
(from their own order) can reset a customer's PIN — it generates a new 4-digit code to read back to the customer.
(they can't sign in) or back on, from the order view.
newer-SQLite features, so it reliably fixes blank items / ₦0 prices on the admin Prices page (and everywhere) regardless of the host's SQLite version.
area) — the data and links are unchanged, just clearer wording for Nigeria.
blank-named items by an older seed, the Prices page now fixes the rows in place (names + prices) instead of leaving ₦0 — even when orders already reference those items.
prices from an existing area, so you only tweak instead of typing everything from scratch.
SVP-0001 instead of SVP-20260521-0001 (9 characters shorter).
shows the confusing "Calling customer"; it now reads "{Vendor} on the way" (e.g. "Cleaner on the way") and the status you pick matches what the card shows.
services and no single vendor covers them all — so the preferred (fallback) vendor is now set per service, right on the Prices page. The Zones page no longer has a preferred-vendor column; it's just for adding/managing zones.
below them you set the preferred vendor for that service+zone and edit the item prices, all in one place.
manager app lets a manager set prices, switch services on/off, and choose the preferred vendor — but only for the zones they're assigned to.
existed, the Prices page showed empty items / ₦0. It now self-heals: opening Prices (or re-adding demo data) rebuilds the service catalog and fills in the per-zone prices automatically.
vendor is gone — vendors are no longer recruited by marketers. That share is reallocated: +3% to the platform and +2% to the zone manager (the zone manager now earns 9% of each order's platform share). Marketers still earn on the agents they bring.
(Links, logins and accounts are unchanged.)
shows the correct names for each share (the manager/agent rows were mislabelled and mentioned "dry cleaning"); the retired vendor-marketer row is hidden and the total adds up to 100%.
vendor on board" link with copy + share buttons; vendors who sign up through it are placed under that manager in their zones.
everything in their zones that needs attention (orders waiting for a vendor, still searching, or with an open issue), with All / Urgent / Finding filters. Tapping an order opens a full mobile view where the manager can call the vendor or customer, edit the order (status, vendor, customer contact, item quantities) or delete it.
urgent items even when the app is closed (Web Push is enabled out of the box).
picker (no more typing zone IDs), and the manager's territory label is built automatically from the zones chosen. Wording is unified on "Zone".
can tap to see the full list, instead of overflowing with every zone.
don't recruit vendors) and fixes the "Recruit someone" button so its description sits on its own line. The recruit page is shorter and generic — no SMS/dry-cleaning/"commission" wording — with a friendly "How it works" guide, a generic "where will they use it?" dropdown, and copy + share buttons on each invite link. The earnings page no longer mentions earning from vendors.
dropdowns (so they scale to many services and zones), and the on/off availability switch is right there — pick a zone, pick a service, edit the prices.
areas (Yaba, Surulere, Wuse) showed ₦0 for AC/generator/plumbing because their prices were never seeded into the new per-service price list. They're seeded now, and re-adding demo data backfills any zone that was missing them.
columns (GPS centre, radius/step/max metres, window, expansions) and now shows just what matters: name, preferred vendor, agents, vendors, active, and a Prices button. Adding a zone only needs a name — the map/matching settings are tucked under "Advanced" with sensible defaults.
when you've placed test orders. It now also cleans up orders, broadcasts and preferred-vendor links tied to demo accounts.
generator repair and plumbing (not just dry cleaning), so dashboards, the manager queue and price lists look realistic.
Prices page, pick a service (Dry Cleaning / AC Install / Generator Repair / Plumbing) and a region, then set that service's item prices. Previously every region shared one (dry-cleaning) list.
page controls whether a service is offered in a region — when off, it's hidden from vendors and customers there.
page now has a tab per service (filtered to what's offered in their region), each showing that service's prices. Copy fixed: "platform commission" (not "overhead"), and "you keep the green amount for yourself".
opens a Notifications page (recent activity on their jobs).
waiting to be started, a pulsing banner on their home screen prompts them to act — accepting isn't enough, they must move it forward.
correctly per service (a plumbing job no longer says "washing" or "picked up") and in simpler English. "Active pipeline" → "Your jobs", "at the door" wording removed, and the new-order/job flow uses service-appropriate verbs.
how-it-works note is shorter ("Customers pay you in full. At pickup you remit only the platform's share — you keep the rest."); completed-order rows are now a single line.
behind an "Advanced" section, so you only need a name and centre point.
zone_services(zone_id, template_id, active) table (missing row = offered). Helpers svp_zone_service_enabled / svp_zone_services_for_zone.
svp_stage_label($db, $status, $template) — layman + service-aware labels (branches on has_return, reads middle stages from service_statuses). Wired into vendor/agent order, notifications, and track views. svp_v2_status_meta() retained for callers without a template.
api/templates.php: new set-zone-active action.pricelist.php rebuilt on service_items + service_item_zone_prices via the existing templates/set-item-price; legacy price_items UI retired.
vendor-pricelist.php rebuilt to per-service tabs via svp_template_items_for_zone. New /vendor-notifications route + view.
drifting slightly right when one side of the header was empty).
with live counts — so vendors can see their past orders, not just the active pipeline.
balance (which doesn’t fit vendors — they’re paid in cash on site), it shows what they’ve earned this month, today, and lifetime, plus a tappable list of completed orders with the amount kept. Removed the wordy explanation under the figure; the “how it works” note is now accurate: customers pay in full in cash on site, the vendor keeps their share immediately and only remits the platform share at pickup.
description now sit on separate lines (they were bunched), and the “Enabled” pill no longer floats off-screen on mobile.
“· LAGOS”).
_agent-v2.css: .ag-header uses 1fr auto 1fr columns so the centretitle is centred regardless of side content.
vendor-orders.php: added status filter (active/completed/cancelled/all)+ counts; action buttons only render on active orders.
vendor-wallet.php is now standalone (computes door-payout totals from completed orders) instead of including _wallet-view.php.
account.php: .svc-card last column is auto; name/desc are block; added .svc-card__body wrapper.
The new live-card design (teal accent, dark hero cards, Inter + JetBrains Mono) now covers every role’s app — not just agents.
hero, pipeline grid), active-orders pipeline, and zone price list. The online toggle, order-advance buttons, completion-photo sheet, and the alarm-style new-order overlay all work exactly as before.
earnings, network counts) and the recruit screen (invite code, share links, downline lists). Fixed the old “CLEANER” label on recruited vendors — now reads “VENDOR”.
tappable escalation queue (each ticket opens its order).
card, range filter, tappable ledger, and “show more” pagination as the agent wallet.
experiment — admin list pages now scroll sideways so you can scan many records at once instead of scrolling through cards.
_wallet-view.php rebuilt on _agent-v2.css (serves vendor/promoter/ manager; agents keep their standalone agent-wallet.php).
vendor.php, vendor-orders.php, vendor-pricelist.php, promoter.php, promoter-recruit.php, manager.php rewritten to the V2 shell while preserving all existing API calls + JS behaviour.
_admin-head.php mobile table rules reverted to horizontal scroll; thedata-label JS was removed.
wasn’t telling the bottom bar which role it was for, so it fell back to the customer menu — agents saw a “Track” tab, lost their Orders / Customers / Wallet tabs, and the Home button pointed to the wrong page. The tab bar now matches the signed-in role on the account screen, so every tab links correctly again.
(agents, vendors, orders, zones, managers, promoters, payouts, reports, price list…) used to overflow / scroll sideways awkwardly. On phones each row now stacks into a tidy card with “Label: value” pairs, so nothing runs off the screen.
earn a <em>X</em>% <strong>commission</strong> on every order” to remove any ambiguity about it being a cut, not a share of the whole order total.
account.php: sets $tabbarRole = $role before including _tabbar.php (it was defaulting to customer).
_admin-head.php: mobile (≤720px) table rules switched from horizontal-scroll to a stacked card layout; svpAdminLabelTables() copies each table’s <thead> labels onto its <td>s as data-label (re-runs via MutationObserver for JS-rendered rows).
agent-wallet.php: “how this works” copy reworded to“{pct}% commission”.
“Auto-pays out every Friday” (dropped the Paystack / bank wording). The “How this works” note now reads plainly: “You earn <em>X</em>% on every order you place — credited to your balance as soon as the customer pays”, where <em>X</em> is pulled live from the commission settings instead of a hard-coded 30%.
order now links straight to that order, and shows its ticket number. The ledger starts at 25 entries with a <strong>Show more</strong> button, so a long earnings history doesn’t become an endless scroll.
separate tabs (Details opens first), so an agent who offers many services doesn’t have to scroll past all of them to reach their details. Room to add more later without lengthening the page.
agent-wallet.php: agent_share_pct read via svp_commission_pct(); ledger query joins orders for share_token + ticket_number; client-side “show more” pagination (page size 25, fetch cap 500).
account.php: profile hero stays pinned; below it a segmentedDetails/Services/Preferences tab switcher. The agent services save-bar only shows on the Services tab when there are unsaved changes.
order.** Tapping an order anywhere (dashboard, orders list, etc.) opens the track page, which was throwing a fatal “undefined constant SVP_APP_ID” error because it didn’t load the auth helper. Order detail now opens correctly.
opens a Notifications page — a feed of recent activity on your orders (accepted, picked up, ready, delivered…), each tappable through to the order.
“Capture a lead · under 60 seconds” subtext so the “Place an order” label is large and prominent.
“My customers” and “My wallet” cards, the count / amount no longer runs into the label — it now sits on its own line with proper spacing.
track.php now requires lib/auth.php (defines SVP_APP_ID).agent-notifications.php view + /agent-notifications public route; feed derives from order_status_log for the agent’s orders.
agent.php: CTA subtext removed + title bumped to 23px; .quick__label / .quick__sub set to display:block; bell is now an anchor.
All five core agent screens were rebuilt pixel-for-pixel from the new “ServicePro Agent — Main Screens” design. Same dark live-card aesthetic as the place-order screen, now with a deep teal accent (#157f76) replacing the old greens, Inter + JetBrains Mono typography, and warm cream paper backgrounds.
perforated stat strip (orders today / profit today / this week) plus a next-payout line. Big embossed teal “Place an order” button, quick links to customers + wallet, and your active orders.
completed / cancelled bars), a segmented filter with counts, and richer order cards showing vendor, total, and your commission per order.
live search, and A→Z customer cards with zone tint, order count, and last-order time.
balance, “pays Friday” chip, lifetime earned vs paid-out, a 7/30/all range filter, the full activity ledger, and a how-it-works note.
“services I offer” picker, editable details (name, phone, PIN, home area), push-notification preference, and sign out.
glyph icons (the active tab shows its label). Applies to every role.
views/partials/_agent-v2.css holds the V2 token palette + reusable primitives (hero, card, section label, status pill, avatar, segmented control). Each agent screen reads it via readfile.
svp_v2_status_meta() (status → pill class + label) and svp_format_money_compact() to helpers.php.
_head.php.agent-wallet.php is now a standalone V2 view (no longer the shared _wallet-view.php, which other roles still use).
_tabbar.php rewritten with per-tab glyphs + the teal active style.“Pickup” / “Pickup address” with a “deliver back to the same address” toggle. On-site services drop the pickup language entirely — AC Install says “Installation address”, Generator Repair and Plumbing say “Service address”, and none of them show a delivery toggle (there’s nothing to return). The labels switch live the moment you change the service, and the live preview card’s label follows too.
emerald, matching the rest of the app — the coral/red is gone.
dry cleaner / installer / plumber…” and “Dry cleaner assigned” instead of the generic “vendor”.
a brief reassurance countdown and the order always lands on an assigned vendor — a live one if someone accepts, otherwise the zone’s preferred (or a nearby fallback) vendor. The old “Routed to manager / no vendor accepted / your card has not been charged” screen is gone — it didn’t match the pay-on-delivery model and agents should never see it.
service_templates gains address_label + has_return columns; seedsset them per service (dry cleaning = pickup+return, others = on-site). Per-slug fallbacks cover un-migrated rows.
lib/broadcast.php: search window shortened to ~3–4s (under the 5–7s agent radar) with max_expansions = 0; auto-assign gained a final fallback to any active vendor offering the service (preferring the order’s zone) so it only escalates when literally no vendor offers the service.
lib/demo-data.php: preferred vendors now seeded for every(zone × active service), not just dry cleaning.
agent-finding.php: removed the escalation block + showEscalated; manager_review now resolves to the same “assigned” outcome for the agent. Added a hard 5–7s finalize so the screen always resolves even if a poll is slow.
preview” look. A dark preview card sits at the top and fills in live as the agent works — customer name, service, area, pickup, and best-time all update in real time so the agent can see the order taking shape before sending.
01 Customer, 02 Pickup, 03 Best time to call, 04 Notes — so the whole flow reads at a glance instead of one long scroll.
Afternoon 12 PM–4 PM, Evening 4 PM–8 PM, Anytime) instead of cramped chips.
preview card, and a coral accent on selected options and the success state. The “Send order” button stays pinned to the very bottom.
views/public/agent-new-order.php markup + CSS to the V2 design while preserving every existing handler (mode picker, region-grouped area sheet, customer search + pre-fill, delivery toggle, collapsible notes, price list, submit). Added a setCell() preview-sync layer wired into the service, customer, area, pickup, and best-time interactions.
.chip to .time-opt cards; the pickerselector was updated to match.
const saveBtn was killingthe entire account-page script (so tab clicks, service toggles, and sign-out all silently failed). One renamed variable later, agents can tap services to toggle, the Account-info tab switches as expected, and Save changes / Sign out fire correctly.
on the area chip now opens a beautiful bottom sheet with zones grouped under Lagos Island / Lagos Mainland / Abuja / Other areas, plus a live search input. This scales cleanly to 50–100 zones — agents won’t be hunting through one giant list.
and “Find by name or phone” no longer collide with the bold title on the New/Existing customer cards — title sits on its own line, description below.
randomised “12–28 nearby vendors alerted” instead of a literal small count, the per-ring wait is clamped to a random 6–12 seconds (down from 30), and the expansion ceiling is now one ring instead of three — so the order lands on a vendor within ~15 seconds whether or not anyone manually accepts.
accepts the broadcast or the order auto-drops to the zone’s preferred vendor, the agent sees the same friendly “Vendor assigned” success card with the matched business name. The auto_assigned flag still ships through to the manager so they can see which orders required a fallback.
with count badges (Active 3, Done 12, Cancelled 1, All 16). Every order row now has an avatar, ticket pill, vendor + amount on the same card, a coloured status chip (service-aware: “Dry cleaner accepted”, “Plumber assigned”), and earned-amount callout when commission has landed. Tapping any row drills into the order detail (/track?token=…) instead of doing nothing.
lib/broadcast.php: accept_window_s is clamped to min(zone, random_int(6,12)); max_expansions is clamped to 1. No schema change required.
agent-finding.php: handles auto_assigned status as success, setsthe eyebrow to “Vendor assigned”, and caches the inflated nearby-count per order in sessionStorage so the number stays stable across polls.
agent-orders.php: rows now include share_token, vendor businessname, and template vendor label; status pill colour + copy are derived from a per-status palette.
pickup section shows the current service area and defaults to the agent’s home zone — one tap to change. Picking a zone here is what the order is bound to, so the agent never has to write “Sangotedo” or “Lekki” into the address field just to be detected.
customer from search, the saved pickup address and zone now drop straight into the form. They can still edit either before placing the order.
bar from this focused wizard and moved the dock to bottom: 14px + safe-area. The button no longer floats mid-page on short forms or tall phones.
“Now / Later today / This evening / Tomorrow morning” row with concrete slots: 9–12, 12–4, 4–8, Anytime. Each chip flexes equally to fill the row.
breathing room below it, so “Customer” no longer sits right on the new/existing cards.
— the “where the dry cleaner goes” clarification was removed.
toasts and .err boxes are gone. Warning prompts now sit on a soft amber surface with ink-on-cream text (the same warm palette used elsewhere). “Show prices for this area” without a zone fires a friendly floating notice instead of a screaming red toast.
Street, Admiralty Way, Badore Road, Akin Adesola, Allen Avenue, etc. Picking a returning customer now reliably pre-fills a credible pickup address.
orders/submit-lead now accepts an explicit zone_id and treats itas authoritative when the geocoder can’t classify the address. The zone’s center is used as the fallback lat/lng so broadcast + auto-assign still have a fix.
detectZone() substring matcher and the inline 📍 zone tag; the area chip + hidden zone_id is now the only source of truth for zone selection.
SVP.toast palette retuned: warnings use the warm ochre tonesinstead of saturated red.
tight back-arrow + title row, body padding trimmed, section radius capped at 5px, and inputs shortened from 52px to 48px. Question marks removed from section headings (the “Service” / “Customer” eyebrow already labels the step). Customer-mode card descriptions trimmed to one tight line each.
“Delivery is the same as pickup” is on by default and the textarea only appears when toggled off. Best time to call became a chip picker (Now / Later today / This evening / Tomorrow morning / Anytime) so the agent never has to type. Notes hidden behind “+ Add a note for the dry cleaner” (or installer / plumber) — collapsible because most orders don’t need them.
above the bottom tab bar instead of floating mid-page, with a safe-area inset so it doesn’t collide with the iPhone home-indicator.
promoter / manager / customer) can now edit their name, phone, PIN, and address right on the Account screen without contacting platform support. Agents pick their home area from the zone dropdown. The PIN field stays blank by default — leave it empty to keep the current PIN, or type 4 digits to rotate.
opens an order by share-link, the page no longer shows the public “Track your clothes / enter the phone number…” prompt — it shows the ticket number, customer name, and a back arrow to the role’s orders list. The timeline labels use the service-aware noun, so dry-cleaning orders say “Dry cleaner accepted” instead of “Vendor accepted”.
order detail when the matched vendor has a phone on file. Tap it to dial straight from the page.
a new order”, customer rows with avatars + order-count pills, and a live filter input at the top. Empty state is now a friendly dashed card instead of plain text.
(--svp-radius-btn & friends). Buttons everywhere look tighter and more consistent without per-view edits.
update-self API action on every role file (agents, vendors,promoters, managers, customers) — validates uniqueness of phone within the role table, optional PIN rotation, audit-logs each update.
track.php now resolves the order’s service_template.vendor_labeland feeds it through the status-friendly label map so every status string is service-aware.
app-user session so the next page render shows the new values.
The agent now picks what the customer is buying before talking about who the customer is. Single-service agents still skip Step 1 and the service appears as a compact chip.
or the long “capture the customer’s details…” paragraph. The page now opens with one big line: Place an order. The explanatory text moved into the service-template description that surfaces under each service card in Step 1.
with proper Lucide-style SVG icons (user-plus for new, magnifier for existing), added a subtle vertical gradient on the card surface, a hover lift + soft shadow, and a chevron on the right that nudges forward on hover. Picked state uses an emerald-tinted gradient with a stronger shadow.
vendor_label column — the noun used to describe the service provider in agent-facing copy. Seeded values:
Agent-facing copy uses this dynamically — the “Address” section title now reads “Where should the dry cleaner go?” (or installer / technician / plumber) based on which service is picked. Switching services on the picker updates the noun live.
anchored at bottom: 16px — same as the tab bar — so on certain layouts (especially after the mobile keyboard closed from the customer-search input) it ended up overlapping the tab bar. It now anchors at bottom: 90px + env(safe-area-inset-bottom) so it always sits cleanly above the tab bar. Container is also pointer-events: none (button is pointer-events: auto) so it never blocks taps on whatever sits behind it.
schema.sql service_templates table gains a vendor_label TEXTcolumn. Picked up automatically by the lazy migrator.
seeds/fresh.sql populates vendor_label for the four seeded templates; a fallback map in agent-new-order.php covers existing installs whose vendor_label is still NULL until an admin sets it.
applyVendorNoun(label) and triggers it on both initial page load and any service-card click; [data-vendor-noun] spans across the form update in lockstep.
/agent-settings page (now superseded by /account) had its “Save services” dock button positioned at the same bottom: 16px as the new shared tab bar — the button was rendering correctly, just hidden underneath the tab bar, so taps landed on the tabs instead. The /agent-settings and /vendor-settings routes now 302-redirect to /account, where the save bar sits at bottom: 86px (above the tab bar). Old bookmarks and the home-screen quick-action card keep working.
an order” on the agent home opens a wizard where Step 1 asks “Is this a new customer, or already on the platform?” with two big cards:
first time. Step 2 captures name, phone, and a 4-digit PIN to share with them.
search field that hits the platform-wide customer base (not just this agent’s contacts — the search uses customers/search which queries every customer row by name or phone). Results render as tappable cards; picking one fills in name + phone and skips the PIN field (they already have one). None of the other sections (service, pickup, notes) show until a mode is picked, so the wizard guides the user one step at a time.
pick the services you offer” card is gone (services live on the Account tab now). The wide third quick-action is now “Wallet & profits” pointing at the wallet page, which is what an agent actually checks several times a day.
customers/search was already platform-wide (no agent filter).The new-order page just calls it from the existing-customer step.
the visible new-details inputs when mode is new; pulls from the pickedCustomer JS object when mode is existing; never sends a PIN for existing customers (server-side orders/submit-lead already reuses the existing customer row when their phone matches).
.mode-picker, .mode-card, .cust-result, .picked-card. Each section in the form carries a .mode-only--any / .mode-only--new / .mode-only--existing class that the JS toggles via display.
role gets an Account tab in its place. Tapping it opens a single unified /account screen with two top-of-page tabs: “Services” and “Account info”. Agents get an editable service picker; vendors see the same picker as read-only with a “contact your manager” note (and the manager’s phone if assigned). Both roles see their phone, role, home/shop area and wallet balance under the Account-info tab, with a friendly note pointing to platform support for name / PIN / phone changes. The page ends with a red Sign out button (confirms first). The save bar only appears when the Services tab is open for an agent.
/agent-orders?order=N which dropped the user onto the full order list. Now drills into /track?token=… for that specific order — same rich detail view (status pill in plain English, items, addresses, timeline, completion photo).
gone.** The little zone tag now stays hidden until the address actually matches one of your zones (then it shows “📍 Sangotedo” etc.). Less noise on a clean form.
small uppercase eyebrow + intro paragraph), each section now leads with a step pill (Step 1 · Service / Step 2 · Customer / Step 3 · Pickup & delivery / Step 4 · Notes) and a bigger plain-English title (“What is this customer buying?” / “Who is the customer?” / “Where should the vendor go?”). Inputs grew to 52 px tall with 1.5 px borders and a soft focus glow. Pickup-address + delivery-address + best-time-to-call now stack vertically (not the 2-column squeeze that broke alignment on phones). The customer-PIN field is a single wide centered field with 8 px letter-spacing — clean, no overlap. Service-picker cards have a 2 px border, a deeper picked state with a soft shadow, and bigger icons (52 px).
/account route registered in app.json public_routes.views/public/account.php works for every app-side role — it reads $me['role'] to decide which sections to show.
views/partials/_tabbar.php — the logout key/tab is removed from every role; account is appended on each.
views/public/agent.php SELECT for the “Active orders” list now pulls share_token so cards can link to /track?token=.
Round of polish driven by walk-throughs — the language was too jargon-y and the agent screens didn’t use the words real Nigerian users speak.
What you’ll see:
everywhere now. The home CTA is Place an order, the dashboard cards say Orders today / Profit today / Profit this week**, the empty state says “No active orders. Tap Place an order above”, and the new-order screen header is “Place an order · For your customer”. Order-list rows say “Customer paid ₦X” instead of the previous “Lead value”.
Plain English throughout: “Your profit is credited here when a customer pays the vendor for an order you placed. The system pays out every Friday.” No more “Paystack” brand chatter in the UI — that’s an implementation detail. Activity log shows friendly labels (Profit from an order / Friday payout / Wallet top-up / Refund) instead of the raw order_split / platform_share_remit keys.
pip detail, two mini-stats on the card itself (“Profits credited / Paid out”), activity rows render in a single bordered card with green-tinted icons for credits and brick-tinted icons for debits. Empty state has a friendly “Nothing here yet” card instead of a stranded one-liner.
/agent-new-order when an agent offers more than one service, Step 1 is a stack of big cards (icon + name + short blurb + green tick when picked). Pick a card — the rest of the form remains visible underneath. Single-service agents skip the picker.
offer” (was “What do you sell?”), section subtitle is “Pick the ones you want”.
order on a customer’s home opens a richer view: status pill in friendly English (“Vendor is calling you” / “On the way back to you”), pickup + delivery addresses, best time to call, full item list with quantities + line totals + grand total, timeline of every state change, and the completion photo if the vendor has marked it complete.
field next to phone with overlapping label on narrow phones. Now full-width with a clear label and tracked-out monospace digits.
status” because both orders and customers have a status column and the WHERE clause didn’t qualify which one. The agent-orders.php query now reads o.status everywhere.
agent.php, agent-orders.php, agent-new-order.php, agent-settings.php, track.php, _wallet-view.php.
.svc-pick (card picker) + .kv-block / .items-blockon the tracking page + the refreshed wallet hero / activity styles.
no such column: zone_id. The cleanup query was checking whether any real agents still referenced one of the demo zones, but it queried the agents table with zone_id — the actual column is home_zone_id. Now correctly uses home_zone_id, and demo zones drop cleanly after the demo users are removed. (Same typo was also hiding in the broadcast-send endpoint when targeting a zone — fixed there too.) No desktop behaviour changed — every adjustment lives in @media (max-width: 720px) and @media (max-width: 480px) blocks in views/partials/_admin-head.php.
Big polish pass after walk-throughs surfaced a stack of usability bugs.
What got fixed:
fire.** The JS namespace was renamed DCH → SVP during the rename, but the assignment line window.DCH = { ... } in [_head.php](views/partials/_head.php) was missed by the sweep, so every consumer that called SVP.post(...) got a runtime error. Same fix on the admin side (DCHAdmin → SVPAdmin).
meta[name=csrf] which doesn't exist — the partial emits meta[name=svp-csrf]. Swept across 7 admin views so all admin fetch calls actually attach the token.
Pulled the tab bar into [views/partials/_tabbar.php](views/partials/_tabbar.php) and every public dashboard includes it with its role + active key. Vendor tab bar always shows Home / Orders / Prices / Wallet / Services / Exit; agent always shows Home / Orders / Customers / Wallet / Services / Exit; customer / promoter / manager each get their own consistent set. The menu no longer changes when you navigate.
/vendor-orders now drills into a new [/vendor-order](views/public/vendor-order-detail.php) screen showing the customer (with tap-to-call), pickup + delivery addresses, best time to call, agent notes, the placing agent (also tap-to-call), itemised line totals + vendor / platform share split, payments table, full status timeline, and the next-step action button (Open pickup · Start washing · On the way · Delivered · Complete with photo). The in-card "advance" buttons still work; the card click drills into the detail.
picker let any vendor self-promote into any service — that's not how the platform works. The screen at [/vendor-settings](views/public/vendor-settings.php) now shows the vendor's current line-up, the services they don't have, and a pointer to their manager (with the manager's phone if assigned) plus platform support. The vendor self-service API (vendors/services-set) now returns 403.
/app/servicepro/vendors. New "Edit" button per row opens a modal with checkboxes for every active service template. Save calls the new vendors/admin-services-set API endpoint, audit-logged.
Visual polish:
Fraunces is a variable Google Font with optical sizing — modern, warm, less stylised than the heavy italic look the old font gave. Used as --svp-font-serif for every hero / greeting / page title across the app, dropped 36 explicit font-style: italic declarations.
shared _head.php and _admin-head.php partials.
vendors/services-set (the self-service path) gets a polite 403 pointing the vendor at their manager. The admin path is vendors/admin-services-set (require_auth-gated, audited).
/vendor-order → vendor-order-detail.php registered in app.json. Vendor order cards carry a data-detail-href attribute so the card click navigates; in-card buttons stop propagation so they still do their own thing.
The login screen looked compact and pinched in the middle because every element — logo, greeting, phone input, button, signup strip — stacked back-to-back with the same 16–20 px gap, leaving big emptyish space below and nothing pinned to the bottom.
The new layout is a full-height flex column with a flex:1 spacer:
big title + supporting copy), then 36 px to the phone input + CTA.
doesn't shift up.
pinned to the bottom on the phone step, or the keypad + footer actions pinned to the bottom on the PIN step.
The PIN-step hero now uses the editorial Instrument Serif italic at 42 px (was 36 px in plain UI font), and the avatar grew to 72 px with a subtle emerald drop-shadow. Keypad keys are taller (76 px), text is larger (30 px digits), and inter-key gap loosened to 14 px. The signup strip cards each carry a small uppercase role label ("AGENT" / "VENDOR" / "PROMOTER") above the action description so the choices read instantly.
Same two-step JS logic (phone → check-phone → PIN → login → redirect) — only the markup + CSS changed.
Two small but meaningful tweaks for testing:
7-8 orders in their history; every demo agent has 7-15 orders attached; every demo vendor has 6-12 orders attached. The mix is weighted toward completed (50 orders spread across the past 14 days) with one in-flight order at each pipeline stage and a handful of manager-review / cancelled cases. Wallet ledger, status history, and payments populate proportionally — every dashboard now looks lived-in.
the auto-assign fallback always has a target. The legacy zones.preferred_vendor_id column gets the same value as a safety net.
sits at the very top of the doc (after the title), with a "Quick start" block showing one phone+PIN per role for instant testing. The old "Demo data" section further down is now a short pointer back to the top.
To refresh an existing dev DB to the new seed, open admin Settings, click Remove demo data, then Re-add demo data. Production installs are unaffected.
lib/demo-data.php $statusBuckets extended with a 50-row completed spread; new preferred_vendors seed loop after customers are created.
"Sparkle Dry Vendors" → "Sparkle Dry Cleaners".
Adds a "View as" feature: an admin can click a button on any app-user row and open a new tab where they're signed in as that user, for debugging or customer-service work. The admin's own Pancho session in the original tab is undisturbed — only a second session entry is created under $_SESSION['app_users']['servicepro'].
Where it shows up:
row → opens /p/{uuid}/servicepro/agent as that agent.
customer row impersonates the customer of that order. (Customers don't have a standalone admin list page, so the entry point is the order detail.)
When viewing as a user, a sticky purple banner appears across the top of their dashboard: "Viewing as {name}" with a Stop button. Tapping Stop ends the impersonation and bounces back to the matching admin list page (/app/servicepro/agents, etc.). The audit log captures every start/stop with the admin's UUID, role, and target name.
(agents, vendors, customers, promoters, managers):
impersonate — admin-gated, looks up an active row, calls app_user_login(), stashes _impersonating_from + _impersonating_from_name + any pre-existing _impersonating_previous onto the new app-user session, audits, returns the redirect URL.
stop-impersonate — trusts the _impersonating_from sessionmarker (since only the gated endpoint can have set it), restores any previous app-user session, audits, returns the admin redirect URL.
*/stop-impersonate endpoints are CSRF-exempt in app.jsonbecause they're called from the impersonated user's session, which doesn't carry the admin CSRF token.
[views/partials/_impersonation-banner.php](views/partials/_impersonation-banner.php) rendered from all five public dashboards. Each dashboard sets $impersonateRole (the role's plural URL slug, e.g. agents) before including, so the partial knows which api/{role}/stop-impersonate endpoint the Stop button should hit.
(window.open('about:blank') synchronously before the async POST) so the new tab always opens; falls back to same-tab if the browser blocks the popup.
[references/prompts/impersonate-feature.md](../../references/prompts/impersonate-feature.md) for replication across other Pancho apps.
The bigger sibling of the rename: Service Pro is now a real multi-service marketplace. Dry Cleaning becomes the first of several services, with the same end-to-end flow underneath every category.
What's new for each role:
screen (link on the home page). On the new-order screen, agents who offer more than one service see a service picker at the top; agents who offer one skip the picker.
new Vendor Settings screen (added to the tab bar as "Services"). Incoming-order alerts now only fire for services the vendor offers.
delivered orders: take a proof photo, pick a completion note from the service template's drop-down, optionally add a free-text note, tap "Mark complete". The photo is saved to uploads/servicepro/{owner-uuid}/order-completion/{order-id}/ and rendered on the admin order-detail page.
lists every service template with item count, status-count, order count, and a manager picker. Toggle templates active/inactive without affecting existing orders. Four templates ship: Dry Cleaning, AC Install, Generator Repair, Plumbing.
service badge so it's obvious which service each order belongs to.
Under the hood:
service_templates, service_items, service_item_zone_prices, service_statuses, service_completion_options. Plus two linking tables (agent_services, vendor_services) and one override table (preferred_vendors keyed on zone × service).
orders gains service_template_id, completion_option_id, completion_note, completion_photo_path.
order_items gains service_item_id (the legacy price_item_idcolumn stays as deprecated back-compat).
vendor_services membership. A vendor whodoesn't offer the order's service never gets the alert.
preferred_vendors(zone, template) first, then to the legacy zones.preferred_vendor_id, then escalates to a manager.
template's manager_id when set (Phase C); falls back to the zone manager otherwise.
scatters AC / Generator / Plumbing across some of them so the multi-service picker has something to show out of the box.
broadcast filter rejects a non-opted-in vendor and accepts an opted-in one.
svp_templates_active, svp_template_get, svp_template_items_for_zone, svp_template_statuses, svp_template_completion_options, svp_agent_service_ids, svp_vendor_service_ids, svp_agent_services_set, svp_vendor_services_set, svp_agent_default_template_id.
agents/services-set, vendors/services-set, orders/complete-with-proof, templates/list, templates/get, templates/toggle-active, templates/set-manager, templates/update, templates/set-item-price, templates/set-zone-preferred-vendor.
pricelist/for-zone now accepts an optional service_template_id query param and reads from the new service_item_zone_prices table (defaults to dry cleaning, template 1).
service_template_id = 1 and service_item_id on order_items.
catalogs and statuses and completion options from admin UI) is deferred — the seeded set covers the four shipping services.
This app was previously called DrycleanHub. As we prepare to broaden it into a multi-service marketplace (dry cleaning today; AC install, generator repair, plumbing, and more to follow), the app id, brand, file paths, and internal identifiers all rename. The dry-cleaning configuration becomes the first service template once the templates subsystem ships.
What changed:
drycleanhub → servicepro; brand "DrycleanHub" → "Service Pro"; ticket prefix DCH- → SVP-.
helpers, UI copy. Cleaners-of-clothes are still cleaners; the role inside Service Pro is now a generic "vendor" because plumbers, AC installers, and generator technicians are not cleaners.
apps/drycleanhub/ → apps/servicepro/. The per-user database location follows — db/users/{uuid}/servicepro.db. Existing development databases under the old name are orphaned (this is a dev-only operation; production has not launched).
flow, platform-share remittance gate, auto-assign fallback, Nominatim geocoding, v2 commission split (agent 30 / agent-promoter 5 / vendor-promoter 5 / L1 override 3 / L2 override 1 / manager 7 / platform 49). Nothing about the order flow changes.
dch_* → svp_* (22 helper functions, ~33 call sites), DCH_* constants → SVP_*, DCH. JS object → SVP., --dch- CSS vars + dch- CSS classes → --svp- / svp-, cleaner_id → vendor_id, cleaner_payout_kobo → vendor_payout_kobo, preferred_cleaner_id → preferred_vendor_id, cleaner_contacted_at → vendor_contacted_at, status value cleaner_contacted → vendor_contacted, auth role string 'cleaner' → 'vendor'. View files cleaner.php → vendor.php. Smoke test renamed from references/dch-e2e-smoke.php → references/svp-e2e-smoke.php.
app-overview.md was rewritten as the canonical source of truth,merging the new multi-service Service Pro spec with the operational details (sample logins, zone list, file map, conventions) carried forward from the old DrycleanHub overview. Decisions log section captures what's locked from the Phase A → Service Pro pivot.
Major architectural shift. Agents no longer touch clothes or money — they capture customer details and submit a lead. The vendor's delivery person handles everything physical at the customer's home, collects payment there, and remits the platform share back to us before the order can advance.
What changes for each role:
agent type. Home page shows "Leads today / Commission today / This week" instead of revenue. The order wizard collapses to a single lead-capture form (customer name, phone, PIN, pickup + delivery address, best time to call, notes). No items, no payment screen. Agent wallet is now earnings-only — no "Pay platform" or "Add card" actions.
with steppers (priced from the customer's zone, not the vendor's), record the door payment (cash/transfer/POS/card), then a single CTA "Remit platform share" charges your saved Paystack card. The order cannot advance to washing until the share has cleared.
given to you verbally — sign in at /login with your phone + PIN to track the order. Door payments go straight to the vendor; in-app payment is now a fallback only.
/zones). When no vendor accepts the broadcast within the window, the order auto-assigns to that preferred vendor — the agent and customer never see the gap. Reassign auto-assigned orders from /orders with the new "auto" filter.
Other notable changes:
auto_assigned and vendor_contacted order states; the lifecycle starts at lead_submitted and a server-side gate blocks picked_up -> received until platform_share_remitted = 1.
payout). Vendors are no longer in the split because they keep their share at the door. Defaults: agent 30%, agent-promoter 5%, vendor-promoter 5%, L1 override 3%, L2 override 1%, manager 7%, platform 49%.
geocode_cache table for 90 days. Falls back to substring zone match on failure.
blocks (pills, avatars, tab bar, keypad keys stay round).
agent_type, kiosk_*, items_mode, delivery_type, origin_* paths from application code. The lazy migrator picks up the new columns (customer_pickup_*, customer_delivery_*, platform_share_*, auto_assigned*, vendor_contacted_at, zones.preferred_vendor_id, new geocode_cache table) on next request.
including auto_assigned, vendor_contacted, and remitted/non-remitted buckets. The split fires from a recorded platform-share remittance.
orders/submit-lead, orders/mark-contacted, orders/pickup-update-items, payments/record-payment, payments/remit-platform-share, zones/set-preferred-vendor. The legacy agent-charge and items-tbd-update actions are gone.
User feedback: a "Populate demo data" button is dangerous in production (someone could click it on a live tenant and inject fake users that look real). Flipped the model:
A new svp_bootstrap_demo_if_fresh() runs once at the end of [helpers.php](helpers.php). Guarded so it only fires when the app context is servicepro and neither demo_seeded nor demo_removed is set in settings. After it runs once, the demo_seeded=1 marker means every subsequent request short-circuits.
only removes rows that are definitely demo:
svp_demo_credentials() (no prefix matching — real users with similar prefixes are safe)
ticket_number LIKE 'SVP-DEMO-%'real users / orders still reference them
Real users and real orders are never touched.
demo_seeded: shows "Remove demo data" (danger button) +sample login table.
demo_removed: shows a green "✓ Demo data removed" line and asmaller ochre "Re-add demo data" button (with a "do not click in production" warning in its confirm dialog).
svp_populate_demo_data($db, $forceReseed) now respects both markers — it only re-adds when called with force=true. The Remove path clears demo_seeded and sets demo_removed; Re-add (force) does the reverse.
numbers (+2348001000001…) used a non-carrier prefix that was easy to mistype. New numbers use actual MTN / Glo / 9mobile prefixes:
0803 100 0001 … 0803 100 0008 (MTN)0806 100 0001 … 0806 100 0008 (MTN)0807 100 0001 … 0807 100 0004 (Glo)0809 100 0001 … 0809 100 0003 (9mobile)0811 100 0001 … 0811 100 0010 (Glo)PINs unchanged (1111 / 2222 / 3333 / 4444 / 5555). Full roster in [app-overview.md](app-overview.md) — login table at the top now also lists the local form and reminds the admin every input shape resolves to the same account.
if (existing_orders < 8) guard. Each click adds another batch of 18 orders across all 13 lifecycle statuses. Users (agents/vendors/...) stay idempotent via the phone UNIQUE constraint, so re-running won't duplicate them. Ticket numbers now include a 4-char random suffix (SVP-DEMO-20260519-0001-A3F2) so the UNIQUE constraint never collides.
format variants for every role resolve correctly: +2348031000001 / 2348031000001 / 08031000001 / 8031000001 / 0803-100-0001 / 0803 100 0001 → all match Bola Akinpelu (agent).
exposed in the admin UI because a misclick in production would wipe live data. The svp_reset_demo_data() function stays in [lib/demo-data.php](lib/demo-data.php) for CLI / test use only; no admin endpoint, no button.
now auto-attaches the platform's X-CSRF-Token header. A small wrapper in [_admin-head.php](views/partials/_admin-head.php) monkey-patches window.fetch() for same-origin non-GET requests so existing inline fetch(url, {method:'POST'…}) calls keep working without per-page edits. Also exposes DCHAdmin.post() / DCHAdmin.get() helpers if you prefer explicit. The CSRF token is rendered into a <meta name="svp-csrf"> tag by the partial.
svp_normalize_phone() previously broke when someone typed 2348001000002 (country code, no +) — it double-prepended 234 and the lookup failed. Now all five common forms map to the same E.164: +2348001000002 · 2348001000002 · 08001000002 · 8001000002 · 0800-100-0002. Verified with 9 input variants in the smoke test.
svp_resolve_phone() now does a two-pass lookup: exact E.164 first, then a last-10-digits LIKE %tail fallback. Catches any edge case the normalizer missed. Matches DrycleanPro's approach.
"too rounded" on test. New scale in design tokens: --svp-radius-input: 12px, --svp-radius-btn: 14px (was full pill). Applied to login phone input, all signup pages, and the public .svp-btn class. Status pills, filter chips, avatars, and the tab bar keep their pill shape — those are visual chrome, not data-entry.
marketplace: 8 agents, 8 vendors (mix online/offline), 4 promoters in a three-level chain, 3 managers across regions, 10 customers, 18 orders spanning every lifecycle status (broadcasting / accepted / washing / ready / delivered / completed / escalated / cancelled), with the 5 completed ones fully split-and-credited through the wallet ledger. Idempotent — running it twice doesn't duplicate. Demo PINs uniform per role (1111 / 2222 / 3333 / 4444 / 5555); full credential roster lives in [app-overview.md](app-overview.md). Source: [lib/demo-data.php](lib/demo-data.php).
Surulere, and Wuse (Abuja) on top of the 5 launch zones, with cloned price lists (+10% / +10% / +30% multipliers).
Service Pro (one word, capital D, capital H, the rest lowercase) — matches the design. App.json name, page titles, error messages, audit log header all updated.
the editorial italic. The post-PIN greeting keeps the italic — that's where the warmth belongs.
number to continue."**
$basePath to the login dispatcher (it does for general public routes), so the in-app $basePath was null when the view rendered — links became /signup-agent instead of /p/{uuid}/servicepro/signup-agent. Defensive code in [login.php](views/public/login.php) now reconstructs $basePath from $ownerUser['uuid'] + $appId using the same isset() ?: '' idiom DrycleanPro uses. No core changes.
Applied to Price list and Audit log filters.
500.00 (naira) instead of 50000 (kobo). JS converts back to kobo on save so backend storage is unchanged.
its plain-English name, description, and a % input. Live total counter recomputes as you type — green when 100%, red otherwise. Saving converts to basis points server-side. Same eight rule keys; backend untouched.
customer-facing strings (order singular/plural, item singular/plural, ticket prefix, all status labels). Values are written to settings.label_{key} and read by get_label() in the core.
Agent · Bola or Vendor · Sparkle Vendors instead of agent:1 / raw UUID. Platform admin UUIDs render as "Platform admin". Helper: svp_audit_actor_label().
broadcast instead of order.create_and_broadcast). 30+ actions mapped in svp_audit_action_label()`.
(e.g. pool ₦6,000 · platform ₦360 instead of the raw JSON). Helper: svp_audit_details_label().
Sign-ins / Manager / Cron / Settings.
promoter_signup_open=1 infresh.sql). Admin can flip it to invite-only in Settings → Signups.
convention a future developer or AI needs to know
setup guide with the two-phone broadcast test
[marketing/Email.txt](marketing/Email.txt), [marketing/SMS.txt](marketing/SMS.txt) — copy-paste outreach templates
screenshots to drop here for the marketplace card
Only the changed files — never push db/:
apps/servicepro/app.jsonapps/servicepro/app-overview.md (new)apps/servicepro/CHANGELOG.mdapps/servicepro/helpers.phpapps/servicepro/seeds/fresh.sqlapps/servicepro/api/agents.phpapps/servicepro/api/vendors.phpapps/servicepro/api/customers.phpapps/servicepro/api/promoters.phpapps/servicepro/api/settings.phpapps/servicepro/lib/demo-data.php (new)apps/servicepro/views/audit.phpapps/servicepro/views/home.phpapps/servicepro/views/pricelist.phpapps/servicepro/views/settings.phpapps/servicepro/views/partials/_admin-head.phpapps/servicepro/views/public/login.phpapps/servicepro/views/public/signup-promoter.phpapps/servicepro/marketing/.md + .txt (new, whole folder)apps/servicepro/screenshots/README.md (new).htaccess forbids everything under /apps/ (per the catch-all rule), so the <link> to /apps/servicepro/views/partials/_design-tokens.css returned 403 and every admin page rendered unstyled. The two shared head partials ([views/partials/_admin-head.php](views/partials/_admin-head.php), [views/partials/_head.php](views/partials/_head.php)) and [views/public/login.php](views/public/login.php) now inline the design tokens via <style><?php readfile(...); ?></style>. CSS source file stays pure CSS for editor tooling.
([views/home.php](views/home.php)) and on the Settings page ([views/settings.php](views/settings.php)). One row per audience (Agent / Vendor / Promoter signup, daily sign-in, anonymous tracking, manifest), each with a one-tap Copy button. URLs come from app_public_url() so they switch to the custom domain automatically once one is verified.
This is the very first cut of Service Pro — a Lagos-wide marketplace for dry cleaning. Five user roles (Customer, Agent, Vendor, Promoter, Manager) share one app, with the install owner sitting on top as platform admin.
#### Foundation (Phases 1-2)
apps/servicepro/, independent of apps/drycleanpro/. Per-user DB at db/users/{owner_uuid}/servicepro.db.
apps/servicepro/sources/*.zip into apps/servicepro/designs/{static,interactive}/. Locked the design system in [views/partials/_design-tokens.css](views/partials/_design-tokens.css): emerald primary #0e3b2e, cream bg #f7f3e9, ochre accent #d4a13c, brick alarm #c14a2c; Plus Jakarta Sans + Space Grotesk + Instrument Serif.
order_broadcast_targets (the Uber-style per-vendor alert ledger), wallet_ledger, role tables (agents/vendors/promoters/managers/customers), commission_rules, web_push_subscriptions, support tickets.
30 price categories, 70 items priced in kobo (shirts, trousers, suits, native wear, dresses, bedding) with premium markups on Lekki and VI.
promoter 4%, vendor promoter 4%, L1 override 2%, L2 override 1%, agent manager 3%, vendor manager 3%, platform floor 5%.
#### Auth + role dashboards (Phase 3)
the LoginA design (phone entry → 4-dot PIN dot indicator → numeric keypad). Role is auto-detected by svp_resolve_phone() across all five user tables.
[vendor](views/public/signup-vendor.php), [promoter](views/public/signup-promoter.php) — geolocation captured in background, invite codes link to upline.
SVP.subscribePush() after loginto register the service worker and store the browser's PushSubscription.
#### Agent place-order (Phase 4)
→ choose items vs items-to-be-determined → category-tabbed grid with steppers → sticky dock with running total → "Confirm & broadcast" CTA matched to the AgentOrderA design.
vendor" radar with pulsing rings, live countdown synced to broadcast_expires_at, lazy radius expansion + manager escalation triggered by the same poll.
#### The Uber-style centerpiece (Phase 5)
online vendors within radius via Haversine, inserts per-vendor target rows, fires Web Push to each. Race-safe svp_broadcast_accept() (atomic UPDATE WHERE status='awaiting_acceptance'). Lazy expansion grows the radius after the accept window; escalates to a zone manager after the configured max_expansions.
Web Audio two-tone siren (lifted pattern from EstateMax panic-banner), vibration, countdown timer, full-screen brick-red banner with animated pulse. Polls /api/portal/incoming-orders every 2.5 s while online.
state='expired',agent's screen transitions to "Vendor found".
#### Order lifecycle (Phase 6)
cards with one-tap "next status" buttons (accepted → picked_up → … → delivered → completed). 9-status flow.
for TBD orders, lists every zone price-item with steppers so they count the actual bag and re-price live; locks items via items-tbd-update.
via share_token link. Real-time status timeline from order_status_log.
view of zone prices showing vendor share vs customer price.
#### Payments (Phase 7)
from EstateMax: initialize, verify, charge_authorization (stored card off-session), create_recipient, initiate_transfer, webhook signature verification, plus svp_wallet_record() for the append-only ledger that updates running balances on each role table.
record-cash — pickup person logs cash from customer (unlocksnext-stage gate for TBD orders)
send-pay-link — push notify customer with /customer-pay deep linkcustomer-init / customer-verify — direct customer Paystack inlineagent-charge — wallet first, fall back to stored cardwebhook — Paystack server-to-server webhook with signature check;failed transfers auto-reverse the wallet debit
for TBD orders the customer pays directly.
#### Commission split + Friday payouts (Phase 8)
svp_split_order_payment($db, $orderId): reads commission_rules, walks the agent's and vendor's promoter chains for upline overrides (L1, L2), writes one wallet_ledger row per party inside a transaction, and is idempotent via orders.split_applied.
svp_run_weekly_payouts($db) — sweeps every party with wallet_balance_kobo >= payout_min_kobo, debits their wallet, creates a payouts row, initiates a Paystack transfer. Per-week idempotency via payouts.week_label. On transfer failure: rolls back the debit and marks the payout failed.
table + manual sweep button + retry on individual failed payouts.
#### Promoter network (Phase 9)
buttons (agent/vendor/sub-promoter) with the promoter's own invite code baked into the link, copy-to-clipboard, and live downline tree (direct agents, direct vendors, sub-promoters).
min(3, upline.level + 1).#### Manager escalation (Phase 10)
escalated order with three actions:
manager whose zone_ids_json includes the order's zone.
#### Customer experience (Phase 11)
orders with TBD callouts, history, share-token links to each order's public track view.
CTA; gracefully degrades when keys aren't configured.
flows; vendor business name + agent name visible to the customer.
#### Admin tools (Phase 12)
The platform admin's /app/servicepro surface gets a full dashboard:
manager review, vendors online, payouts pending/failed.
filter by live/escalated/completed; detail page shows items, status log, every payment, and the full wallet_ledger fan-out per order.
accept window, max expansions.
customer_price per item, per zone.
[promoters](views/promoters.php), [managers](views/managers.php) — rosters with orders / gross / wallet columns; suspend/reactivate agents inline; create top-level promoters and managers.
Paystack key entry, signup gate toggles, commission rules editor with live total validation (must equal 10000 basis points).
vendors, broadcast accept rate.
details rendered inline.
#### Polish + cron (Phase 13)
run_servicepro_cron() in [helpers.php](helpers.php):awaiting_acceptance orders past expiry that nobody's pollingpayout_day_of_week), runs the weeklypayout sweep
/uploads/servicepro/users/{uuid}attachments when the app is removed.
_head.php includes SVP.subscribePush, SVP.fmtMoney, SVP.fmtPhone, SVP.toast, SVP.get/SVP.post so every role page gets the same surface.
"You're offline" page in the brand's serif italic.
min-height: 48px per the --svp-tap-min token —the cheap-Android floor.
auto_payout flag on vendors lets them keep balances in the walletinstead of getting auto-swept (the spec called for this; default on).
---
A full end-to-end smoke test at [references/svp-e2e-smoke.php](../../references/svp-e2e-smoke.php) creates a test promoter chain (L1 → L2), manager, agent (recruited by L2), vendor (recruited by L2 too, same zone), customer, and a 6-item order. It exercises:
1. Broadcast — 1 vendor targeted at 95m, won race-safely 2. 9-stage lifecycle — picked_up → received → washing → drying → pressing → ready → out_for_delivery → delivered → completed 3. Payment + split — ₦6000 pool distributed: vendor ₦4680 (78%), L2 ₦480 (4%+4% agent_promoter+vendor_promoter), L1 ₦120 (2% override), manager ₦360 (3%+3%), platform ₦360 (5% floor + unclaimed 1% upline_l2) — sums to ₦6000 exactly ✓ 4. Idempotency — re-running split returns already_split: true 5. Payout cron — without Paystack credentials, the vendor's debit is automatically rolled back and the payout is marked failed 6. Cron hook — run_servicepro_cron() reports stats
All 70 PHP files lint clean. Schema applies to a fresh in-memory SQLite without error. JSON in app.json parses cleanly.
1. Generate VAPID keys in /settings — required for Web Push (no push = no alarm when vendor app is backgrounded) 2. Add Paystack live keys in /settings — required for card charges AND outbound transfers 3. Connect Twilio (optional) — only needed for SMS notifications 4. Onboard at least one Manager per zone — escalations need a target 5. Run a two-phone test — agent on one, online vendor on the other, place an order, watch the alarm fire
When this lands on the live server:
apps/servicepro/ — entire folder (everything except db/)The lazy migrator handles the schema on the first request after deploy. Per Pancho rules: never push db/, users/, or uploads/.