The Router keeps everything in SharePoint lists, so Power BI connects
natively — no exports, no gateway, no backend. This page is the build
recipe for whoever owns Power BI at your agency; the first dashboard is
about an hour of work.
CostValue number column on the BillTracker — the numeric twin of the human-typed EstimatedCost text ("$1.2M", "(50,000) savings"). Harvest writes it automatically; **Load fiscal rollup** backfills it for hand-typed rows. Run one rollup load before your first refresh so the column is populated.https://tenant.sharepoint.com/sites/Legislative) → 2.0 implementation.BillTracker), audit (LegislativeAudit), and routing (LegislativeRoutingMatrix).Title (bill), Division, Status, DueDate, FiscalYear, EstimatedCost, CostValue, ImpactSeverity, Created, ModifiedTitle (bill), Status, Divisions, Created, PublishedBy, SourceSubjectTitle (code), DivisionName, EmailsDueDate/Created/Modified → Date/Time, CostValue → Decimal.Add a custom column to BillTracker if you see blank CostValue on rows
with text costs:
= let t = Text.Lower(Text.Trim([EstimatedCost] ?? "")),
neg = Text.StartsWith(t, "(") or Text.StartsWith(t, "-"),
clean = Text.Select(t, {"0".."9", ".", "k", "m", "b"}),
num = try Number.From(Text.Select(clean, {"0".."9", "."})) otherwise null,
mult = if Text.Contains(clean, "b") then 1000000000
else if Text.Contains(clean, "m") then 1000000
else if Text.Contains(clean, "k") then 1000 else 1
in if num = null then null else (if neg then -1 else 1) * num * mult
BillTracker[Division] → LegislativeRoutingMatrix[Title] (many-to-one) so visuals can show full division names.BillTracker[Title] → LegislativeAudit[Title] (many-to-many is fine here; the audit adds publish timestamps and who published).
Total Estimated Cost = SUM ( BillTracker[CostValue] )
Outstanding = CALCULATE ( COUNTROWS ( BillTracker ),
BillTracker[Status] IN { "Pending review", "In review" } )
Overdue = CALCULATE ( COUNTROWS ( BillTracker ),
BillTracker[Status] IN { "Pending review", "In review" },
BillTracker[DueDate] < NOW () )
Avg Response Hours = AVERAGEX (
FILTER ( BillTracker, BillTracker[Status] = "Commented" ),
DATEDIFF ( BillTracker[Created], BillTracker[Modified], HOUR ) )
Awaiting Estimates = CALCULATE ( COUNTROWS ( BillTracker ),
ISBLANK ( BillTracker[CostValue] ),
NOT BillTracker[Status] IN { "No comment needed" } )
Avg Response Hours uses row Created→Modified as a proxy for
post-to-comment time; it reads slightly high if rows are edited after
commenting, which is acceptable for a trend line.
FiscalYear and Division; matrix: FiscalYear × ImpactSeverity with Awaiting Estimates alongside so "TBD" rows stay visible, never silently zero.Publish to your workspace → schedule refresh (SharePoint credentials =
your org account; daily is plenty, hourly during session) → in the
legislative Teams channel, + Add a tab → Power BI and pick the
report. Divisions then see the scorecard right next to the BillTracker
tab where they mark their status.
Privacy note: the dashboard reads the same lists your team already
has access to, under the viewer's own permissions. Nothing new is
collected and nothing leaves your tenant.