vendor-funnel-customersrun.md00_intake/stub.mdThe funnel section (funnel-section.tsx)
shows the revenue funnel (lead → activated → BRD → in-delivery → delivered → CSAT, with counts +
post-BRD revenue + conversion % + avg days), revenue-lost-by-stage, a lead-to-BRD conversion card,
and the top-customers table. The existing vendorService.getLeadFunnel + getTopCustomers already
supply the funnel counts and a partial top-customers list (the page adapts them, defaulting
revenueAwaitingCSAT: 0, status: "stable", and revWeighted CSAT to the raw CSAT). The revenue overlay,
revenue-lost, lead-to-BRD and the missing customer columns are still mock.
getLeadFunnel to overlay post-BRD revenue, conversion % and avg days per stage
on the existing counts (reusing feature 2's revenue), and add revenue-lost-by-stage (count + revenuegetTopCustomers: real revenue-weighted CSAT, revenue awaiting CSAT, and an
at-risk vs stable status (from feature 6's risk flags) — removing the page's hard-coded adapter
defaults.vendorData.funnel / topCustomers fallbacks on the page.vendorData fallbacks for this section.getLeadFunnel / getTopCustomers, don't fork.
Confirm the canonical funnel stages against the status graph (the dashboards mock and the existing
service may differ — reconcile to one).touches: packages/services/src/server (extend vendorService funnel + top-customers),
reads packages/services/src/db/models/{lead,invoice,csat,status-history}.ts,
apps/web/app/(app)/vendor/dashboard/page.tsx (drop adapter defaults + fallbacks),
apps/web/components/dashboard/vendor/{funnel-section,funnel-chart-card,top-customers-table}.tsx.01_define/output/spec.mdgetLeadFunnel + getTopCustomers, add revenue-lost + lead-to-BRD); apps/web/app/(app)/vendor/dashboard/{page.tsx,empty-data.ts}; apps/web/components/dashboard/vendor/types.ts; reads packages/services/src/db/models/{lead,invoice,csat,status-history,quote}.tsThe vendor dashboard's "Work entering service" section — the revenue funnel chart, the
lead-to-BRD conversion card, the revenue-lost-by-stage table and the top-customers table — is the
last section of the vendor-dashboard-data epic still running on placeholder data. Today
vendorService.getLeadFunnel supplies only stage counts and getTopCustomers supplies a
partial list; the page's adaptTopCustomer hard-codes revWeightedCSAT to the raw CSAT,
revenueAwaitingCSAT: 0 and status: "stable", and the post-BRD revenue overlay, the
revenue-lost-by-stage table and the lead-to-BRD conversion card fall back to the zeroed
emptyVendorData. That leaves the vendor looking at a funnel that shows volume but not the money
moving through it, and a top-customers table whose risk and CSAT-by-revenue columns are fabricated.
Completing this section finishes the epic's mandate — every vendor-dashboard section driven by real, per-tenant data over the selected window — which advances the Build the Bridge initiative's Q2 2026 Objective 1 (Establish Product-Market Fit with Vendor Partners): vendors, the paying subscriber org, can only judge product fit if the conversion-and-revenue view they care about ("conversion rate at every stage of the funnel", time-in-stage, CSAT) reflects their real book of business.
Extend the existing tenant- and period-scoped vendorService (do not fork it) so the funnel
section renders entirely from real data, then drop the page's adapter defaults and emptyVendorData
fallbacks for this section.
getLeadFunnel). Keep the real per-stage counts, and overlay, per
stage from the BRD/scoped stage onward: post-BRD revenue, conversion % (share of the prior
stage that reached this stage) and avg days in stage. Post-BRD revenue reuses feature 2's
per-customer revenue (getRevenueRetention → perCustomerRevenue) / accepted-quote totals — it is
not a new revenue definition; avg days-in-stage reuses the lead status-history dwell approach the
revenue-flow engine (getRevenueFlow) already uses. Pre-BRD stages carry count only (matching the
chart's "volume pre-BRD / revenue post-BRD" legend).getTopCustomers). Add real revenue-weighted CSAT, revenue
awaiting CSAT (windowed paid revenue from customers with no CSAT in the window) and an
at-risk vs stable status per customer row, removing the page's hard-coded adapter defaults.apps/web/app/(app)/vendor/dashboard/page.tsx passes the extended service
output straight through to FunnelSection; drop adaptTopCustomer's hard-coded
revWeightedCSAT / revenueAwaitingCSAT / status and the ?? emptyVendorData.{funnel, revenueLostByStage, leadToBRD, topCustomers} fallbacks for this section (the empty-data zero-state
stays only as the failed-read fallback, consistent with the sibling sections). Widen
VendorData.funnel in components/dashboard/vendor/types.ts to carry the optional per-stage
revenue the chart already reads.statusService.getByWorkflow("lead"), as getLeadFunnel already does) — not the old dashboards
mock labels. Where the mock and the status graph disagree, the status graph wins; "BRD" is
resolved to the canonical scoped/BRD status and is the boundary at which the revenue overlay begins.vendor-revenue-risk, feature 6) has not landed, so a customer row's at-risk flag reuses the
same heuristic risk-signal set the already-shipped customer-health section uses —
lowCSAT (revenue-weighted/normalised CSAT below the shared LOW_CSAT_CUTOFF of 4.0) or an
ageing open project (> 30 days). This keeps the funnel's status column and the customer-health
section consistent today; when feature 6 lands it owns the flags and both converge on it (no
parallel risk definition is introduced here).emptyVendorData fallback for this block on a successful read).page.tsx no longer hard-codes revWeightedCSAT,
revenueAwaitingCSAT or status in adaptTopCustomer.emptyVendorData.{funnel, revenueLostByStage, leadToBRD, topCustomers} except as the explicit failed-read zero-state (matching the sibling
sections' error handling).vendor-revenue-risk) — this feature only
consumes a heuristic at-risk signal; it does not build the revenue-risk section or its flags.funnel-section.tsx, funnel-chart-card.tsx and
top-customers-table.tsx already render the richer shapes; this run wires data to them, not redesign.apps/dashboards — that app stays the dummy source-of-truth.vendor-revenue-risk lands, the at-risk flag should switch from the shared
heuristic to feature 6's renewal-risk flags — that is the documented follow-up, not a blocker for
this run, since both are defined to agree.)02_build/output/notes.mdpackages/services/src/db/services/vendor/index.ts — extended the funnel
service (no fork):getLeadFunnel now returns VendorFunnel ({ funnel, revenueLostByStage, leadToBRD }) instead of { funnel }. Funnel rows keep the snapshot
per-stage counts (non-rejected, by current status) and, from the BRD
stage (quotation_process) onward, overlay open post-BRD revenue
(accepted-quote total → latest quote → 0, the same definition the
revenue-flow strip uses), conversion % (reached-this ÷ reached-prior over
the canonical forward path) and avg days-in-stage (status-history dwell,
re-entries summed). Revenue-lost-by-stage: leads that stalled before
delivery (rejected / qualified-out) attributed to their furthest stage, with
revenue lost as a share of revenue that reached the stage. Lead-to-BRD
card: conversion rate, revenue created at BRD and avg lead→scoped days,
each with a change vs the equal-length prior window.loadFunnelWindow (windowed leads + open revenue +
status timeline), funnelProgress (furthest forward-path stage + first BRD
transition) and leadToBrdStats (the three lead→BRD scalars, reused for the
prior window).getTopCustomers now returns real revWeightedCSAT, revenueAwaitingCSAT
(paid revenue from customers with no CSAT response) and an at-risk / stable
status (low-CSAT < shared 4.0 cutoff or ageing open project > 30d — the
same heuristic, and the same ageing query shape, the customer-health section
uses, so the two sections' risk view agrees).LEAD_FUNNEL_PATH, BRD_STAGE, QUALIFIED_OUT_STATUS,
AGEING_DAYS) + new exported types (VendorRevenueLostRow,
VendorLeadToBRD, VendorFunnel); VendorFunnelRow / VendorTopCustomer
gained the overlay / risk fields.packages/services/src/db/services/index.ts — re-export the three new
funnel types.apps/web/app/(app)/vendor/dashboard/page.tsx — adaptTopCustomer no
longer hard-codes revWeightedCSAT / revenueAwaitingCSAT / status (reads
them from the service); the funnel section's revenueLostByStage and
leadToBRD are now wired from the extended getLeadFunnel result, with the
emptyVendorData zero-state kept only as the failed-read fallback.apps/web/components/dashboard/vendor/types.ts — widened VendorData.funnel
to carry the optional revenue / conversionPct / avgDays overlay.apps/web/components/dashboard/vendor/funnel-chart-card.tsx — FunnelRow
carries the overlay fields and the chart tooltip surfaces post-BRD revenue,
conversion % and avg days when present.quotation_process
onward, surfaced in the chart tooltip.page.tsx no longer hard-codes those three fields.emptyVendorData.{funnel, revenueLostByStage, leadToBRD, topCustomers} except as the explicit failed-read zero-state.pnpm --filter @sustentus/services typecheck — clean.pnpm --filter web typecheck — clean (after building @sustentus/services so
the web app resolves the updated package types).pending → backlog → quotation_process → awaiting_confirmation → work_in_progress → delivered → survey_sent → completed); quotation_process
("Bid Process Started") is treated as the BRD/scoping boundary, matching the
revenue-flow strip's first revenue-bearing stage. qualified_out is off-path
(a drop-off), so it never carries an overlay.revWeightedCSAT equals the customer's own normalised CSAT — at a
single account there is nothing to weight across; the portfolio-level
revenue-weighted CSAT remains in the customer-health section. csat/spend
lookups in getTopCustomers stay lifetime (as shipped in feature 1); the
window still selects which customers appear. The at-risk flag shares the
customer-health definition (low-CSAT-or-ageing).vendor-revenue-risk lands, the
at-risk flag should switch from this shared heuristic to its renewal-risk flags.03_release/output/changelog.mdEntry: apps/help/app/changelog/2026-06-25-vendor-funnel-customers/page.mdx
title: See your revenue funnel and top customers with real numbers personas: [vendor]
The "work entering service" section of your vendor dashboard now runs on real data instead of placeholders. For the date range you choose, the revenue funnel shows how many leads sit at each stage and — from scoping onward — the revenue, conversion rate and average time in each stage. Alongside it, a lead-to-scoping card and a revenue-lost-before-go-live table show where value is created and where it leaks. The top-customers table now shows each account's revenue-weighted satisfaction, revenue still awaiting a satisfaction score, and whether the account is at risk or stable.
03_release/output/investor-update.mdWho it's for: Vendor partners — the paying tenant What shipped: The vendor dashboard's revenue funnel, lead-to-scoping card, revenue-lost table and top-customers table now run on real per-tenant data over any date range. Why it matters: It completes the vendor revenue cockpit — every section now real — advancing Build the Bridge, Q2 2026 Objective 1: establish product-market fit with vendor partners.
Dig deeper: <merged-PR URL> · <changelog entry URL>