2026 04 08 Pricing Benchmark
Pricing Benchmark Implementation Plan
Section titled “Pricing Benchmark Implementation Plan”For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a Pricing Benchmark tab to the forecasting app that shows oncology drug WAC prices with filterable table and WAC-to-Net discount calculator.
Architecture: Refactor the boolean-based view switching (compareActive) to a union type (activeView: "editor" | "comparison" | "pricing"), then build the Pricing view as a peer of the Comparison view — same layout pattern, new sections/Pricing/ directory. Data fetched from GET /api/forecasting_pricings via useQuery.
Tech Stack: React 19, TypeScript, Jotai (read-only for indication), TanStack Query v5, Radix Switch, Tailwind CSS, lucide-react icons
Spec: docs/superpowers/specs/2026-04-08-pricing-benchmark-design.md
Pre-requisite: API Changes
Section titled “Pre-requisite: API Changes”The API work (rename statistics → forecasting_statistics, create forecasting_pricings table, drop legacy schema) is handled in a separate Claude Code session in ../bioloupe-data-gov. See Section 5 of the spec for the full prompt.
The frontend tasks below can begin immediately — Tasks 1-5 are pure refactoring and UI scaffolding that don’t depend on the API being ready. Tasks 6-7 wire the API data and should be done after the API work is complete.
Task 1: Refactor compareActive → activeView in ScenarioProvider
Section titled “Task 1: Refactor compareActive → activeView in ScenarioProvider”Files:
- Modify:
src/features/reports/forecasting/scenarios/ScenarioProvider.tsx
This is the foundational refactor — all subsequent tasks depend on this.
- Step 1: Update the
ScenarioContextValueinterface
In src/features/reports/forecasting/scenarios/ScenarioProvider.tsx, replace the compareActive and setCompareActive fields (lines 124-125) with the new union type:
// Add this type above the interface (around line 99, after the existing helpers)type ActiveView = "editor" | "comparison" | "pricing";In the ScenarioContextValue interface, replace:
compareActive: boolean; setCompareActive: (active: boolean) => void;with:
activeView: ActiveView; setActiveView: (view: ActiveView) => void;- Step 2: Update the state declaration
Replace line 187:
const [compareActive, setCompareActive] = useState(false);with:
const [activeView, setActiveView] = useState<ActiveView>("editor");- Step 3: Update the auto-exit useEffect
Replace the compareActive check in the useEffect (lines 193-204). The effect cleans selectedForComparison and auto-exits comparison when tabs drop below 2:
useEffect(() => { const currentIds = new Set(tabsState.map((t) => t.meta.id)); setSelectedForComparison((prev) => { const cleaned = prev.filter((id) => currentIds.has(id)); const newIds = tabsState.filter((t) => !prev.includes(t.meta.id)).map((t) => t.meta.id); const updated = [...cleaned, ...newIds]; if (updated.length < 2 && activeView === "comparison") { setActiveView("editor"); } return updated; });}, [tabsState, activeView]);- Step 4: Update the context value memo
In the useMemo value object (around lines 640-641), replace:
compareActive, setCompareActive,with:
activeView, setActiveView,In the dependency array (around lines 666-667), replace:
compareActive,with:
activeView,- Step 5: Export the
ActiveViewtype
Add to the useScenario export area so consumers can import the type. In src/features/reports/forecasting/scenarios/ScenarioProvider.tsx, make sure ActiveView is exported:
export type { ActiveView };- Step 6: Update the scenarios barrel export
In src/features/reports/forecasting/scenarios/index.ts, add the type export:
export type { ActiveView } from "./ScenarioProvider";- Step 7: Verify no TypeScript errors
Run: pnpm tsc --noEmit 2>&1 | head -40
Expected: Errors in files that still reference compareActive — that’s expected and fixed in Task 2.
- Step 8: Commit
git add src/features/reports/forecasting/scenarios/ScenarioProvider.tsx src/features/reports/forecasting/scenarios/index.tsgit commit -m "refactor(scenarios): replace compareActive boolean with activeView union type"Task 2: Update all compareActive consumers
Section titled “Task 2: Update all compareActive consumers”Files:
-
Modify:
src/features/reports/forecasting/Forecasting.tsx -
Modify:
src/features/reports/forecasting/scenarios/ScenarioTabBar.tsx -
Modify:
src/features/reports/forecasting/sections/Comparison/ComparisonFilterRow.tsx -
Step 1: Update
ForecastingContentin Forecasting.tsx
At line 575, change the destructuring:
const { activeTabId, activeStore, compareActive } = useScenario();to:
const { activeTabId, activeStore, activeView } = useScenario();Replace the branching at lines 578-584:
if (compareActive) {with:
if (activeView === "comparison") {No other changes needed in this file yet — the pricing branch is added in Task 5.
- Step 2: Update
ScenarioTabBar
In src/features/reports/forecasting/scenarios/ScenarioTabBar.tsx, update the destructuring (lines 38-48):
Replace:
compareActive, setCompareActive,with:
activeView, setActiveView,Update the ScenarioTab isActive prop (line 78):
isActive={activeView === "editor" && tab.meta.id === activeTabId}Update the onSwitch callback (lines 81-84):
onSwitch={() => { setActiveView("editor"); switchScenario(tab.meta.id);}}Update the CompareTab props (lines 94-98):
<CompareTab isActive={activeView === "comparison"} disabled={tabs.length < 2} onClick={() => setActiveView("comparison")}/>- Step 3: Update
CompareTab(no changes needed)
CompareTab.tsx receives isActive, disabled, onClick as props — it doesn’t reference compareActive directly. No changes needed. Verify by reading the file.
- Step 4: Update
ComparisonFilterRow
In src/features/reports/forecasting/sections/Comparison/ComparisonFilterRow.tsx, update the destructuring (line 15):
Replace:
const { tabs, selectedForComparison, setSelectedForComparison, setCompareActive } = useScenario();with:
const { tabs, selectedForComparison, setSelectedForComparison, setActiveView } = useScenario();Update the back button onClick (line 45):
onClick={() => setActiveView("editor")}- Step 5: Search for any remaining
compareActivereferences
Run: grep -r "compareActive\|setCompareActive" --include="*.ts" --include="*.tsx" src/
Expected: Zero matches. If any remain, update them following the same pattern.
- Step 6: Verify TypeScript compiles
Run: pnpm tsc --noEmit
Expected: No errors.
- Step 7: Run lint
Run: pnpm lint
Expected: No new violations.
- Step 8: Commit
git add src/features/reports/forecasting/Forecasting.tsx src/features/reports/forecasting/scenarios/ScenarioTabBar.tsx src/features/reports/forecasting/sections/Comparison/ComparisonFilterRow.tsxgit commit -m "refactor: migrate all compareActive consumers to activeView"Task 3: Add PricingTab component and wire into ScenarioTabBar
Section titled “Task 3: Add PricingTab component and wire into ScenarioTabBar”Files:
-
Create:
src/features/reports/forecasting/scenarios/PricingTab.tsx -
Modify:
src/features/reports/forecasting/scenarios/ScenarioTabBar.tsx -
Modify:
src/features/reports/forecasting/scenarios/index.ts -
Step 1: Create
PricingTabcomponent
Create src/features/reports/forecasting/scenarios/PricingTab.tsx:
import { DollarSign } from "lucide-react";import { cn } from "@/lib/utils";
interface PricingTabProps { isActive: boolean; onClick: () => void;}
export function PricingTab({ isActive, onClick }: PricingTabProps) { return ( <button type="button" role="tab" aria-selected={isActive} onClick={onClick} className={cn( "flex items-center gap-1.5 px-4 py-2 text-sm border-b-2 transition-colors whitespace-nowrap", isActive ? "border-primary bg-background font-semibold text-foreground" : "border-transparent text-muted-foreground hover:text-foreground hover:border-border/40 hover:bg-muted/50", )} > <DollarSign className={cn("h-4 w-4", !isActive && "text-primary/60")} /> Pricing </button> );}- Step 2: Add
PricingTabtoScenarioTabBar
In src/features/reports/forecasting/scenarios/ScenarioTabBar.tsx, add the import:
import { PricingTab } from "./PricingTab";After the CompareTab block (after line 98), add a divider and the PricingTab:
{/* Pricing divider + tab */} <div className="h-6 border-l border-border mx-1" /> <PricingTab isActive={activeView === "pricing"} onClick={() => setActiveView("pricing")} />- Step 3: Update barrel export
In src/features/reports/forecasting/scenarios/index.ts, add:
export { PricingTab } from "./PricingTab";- Step 4: Verify TypeScript compiles
Run: pnpm tsc --noEmit
Expected: No errors.
- Step 5: Run lint
Run: pnpm lint
Expected: No new violations.
- Step 6: Commit
git add src/features/reports/forecasting/scenarios/PricingTab.tsx src/features/reports/forecasting/scenarios/ScenarioTabBar.tsx src/features/reports/forecasting/scenarios/index.tsgit commit -m "feat(pricing): add PricingTab component to ScenarioTabBar"Task 4: Create pricing-utils.ts and section-config update
Section titled “Task 4: Create pricing-utils.ts and section-config update”Files:
-
Create:
src/features/reports/forecasting/sections/Pricing/pricing-utils.ts -
Modify:
src/features/reports/forecasting/section-config.ts -
Step 1: Create
pricing-utils.ts
Create src/features/reports/forecasting/sections/Pricing/pricing-utils.ts:
/** * Pricing Benchmark Utilities * * Pure helper functions for the pricing table: formatting, normalization, filtering. */
/** Split "{PD-1,VEGFR}" → ["PD-1", "VEGFR"] */export function targetNormalize(value: string | undefined): string[] { if (!value) return []; return value.replace(/[{}]/g, "").split(",").map((s) => s.trim()).filter(Boolean);}
/** Format number with locale thousand separators, truncated to integer */export function toThousandSeparator(value: number | string | undefined): string { const num = typeof value === "string" ? Number(value) : value; if (num === undefined || num === null || Number.isNaN(num)) return ""; return Math.trunc(num).toLocaleString();}
/** Compute discounted monthly net price */export function computeNetPrice(wacPrice: number, discountPercent: number): number { return Math.round(wacPrice * (1 - discountPercent / 100));}
export interface PricingEntry { id: number; brand_name: string; indication: string; monthly_wac_price: string; target: string | null; technology: string | null; date_approved: string | null; disease: string | null; disease_id: number | null; source: Array<{ name: string; url?: string | null; sampleSize?: number | null; comment?: string | null }> | null;}
export interface PricingFilters { brand: string; indication: string; target: string; technology: string;}
/** Client-side filter pipeline */export function filterPricingData(data: PricingEntry[], filters: PricingFilters): PricingEntry[] { return data.filter((item) => { if (filters.brand && !item.brand_name.toLowerCase().includes(filters.brand.toLowerCase())) return false; if (filters.indication && !item.indication.toLowerCase().includes(filters.indication.toLowerCase())) return false; if (filters.target && !item.target?.toLowerCase().includes(filters.target.toLowerCase())) return false; if (filters.technology && !item.technology?.toLowerCase().includes(filters.technology.toLowerCase())) return false; return true; });}
/** Sort by date_approved descending (newest first), nulls last */export function sortByDateDesc(data: PricingEntry[]): PricingEntry[] { return data.slice().sort((a, b) => { if (!a.date_approved) return 1; if (!b.date_approved) return -1; return new Date(b.date_approved).getTime() - new Date(a.date_approved).getTime(); });}- Step 2: Add
PRICING_SECTIONSto section-config
In src/features/reports/forecasting/section-config.ts, add after the COMPARISON_SECTIONS block (after line 61):
export const PRICING_SECTIONS: SectionConfig[] = [ { id: "section-pricing", label: "Pricing Benchmark", },] as const;- Step 3: Verify TypeScript compiles
Run: pnpm tsc --noEmit
Expected: No errors.
- Step 4: Commit
git add src/features/reports/forecasting/sections/Pricing/pricing-utils.ts src/features/reports/forecasting/section-config.tsgit commit -m "feat(pricing): add pricing utilities and section config"Task 5: Create PricingFilterRow component
Section titled “Task 5: Create PricingFilterRow component”Files:
-
Create:
src/features/reports/forecasting/sections/Pricing/PricingFilterRow.tsx -
Step 1: Create
PricingFilterRow
Create src/features/reports/forecasting/sections/Pricing/PricingFilterRow.tsx:
import { ArrowLeft } from "lucide-react";import { InfoButton } from "@/components/ui/info-button";import { Switch } from "@/components/ui/switch";import { useScenario } from "../../scenarios";
const DISCOUNT_SOURCES = [ { name: "Medicaid 23.1% Minimum Rebate", url: "https://www.medicaid.gov/medicaid/prescription-drugs/medicaid-drug-rebate-program/unit-rebate-amount-calculation", }, { name: "IQVIA Global Oncology Trends 2023", url: "https://www.iqvia.com/insights/the-iqvia-institute/reports-and-publications/reports/global-oncology-trends-2023", }, { name: "FTC PBMs & Drug Pricing 2023", url: "https://www.ftc.gov/news-events/news/press-releases/2024/07/pharmacy-benefit-managers-powerful-middlemen", }, { name: "DrugChannels.net", url: "https://www.drugchannels.net/2024/07/pbm-power-gross-to-net-bubble-reached.html", },];
interface PricingFilterRowProps { diseaseFilter: string; discountEnabled: boolean; onToggleDiscount: (enabled: boolean) => void;}
export function PricingFilterRow({ diseaseFilter, discountEnabled, onToggleDiscount,}: PricingFilterRowProps) { const { setActiveView } = useScenario();
return ( <div className="flex items-center gap-3 flex-wrap mt-3"> {/* Back button */} <button type="button" onClick={() => setActiveView("editor")} className="flex items-center gap-1.5 px-3.5 py-1.5 rounded-md bg-primary text-primary-foreground text-sm font-medium shadow-sm hover:bg-primary/90 transition-colors" > <ArrowLeft className="h-4 w-4" /> Back to editor </button>
{/* Divider */} <div className="h-6 border-l border-border" />
{/* Indication chip */} <span className="text-xs text-muted-foreground/70">Indication:</span> <span className="inline-flex items-center gap-1.5 py-1 pl-2 pr-3 rounded-full border-[1.5px] border-primary text-primary text-xs font-medium"> <span className="w-1.5 h-1.5 rounded-full bg-primary shrink-0" /> {diseaseFilter || "All"} </span>
{/* Discount toggle — right aligned */} <div className="flex items-center gap-2 ml-auto px-3 py-1.5 rounded-lg bg-muted border border-border"> <label htmlFor="discount-toggle" className={`text-xs font-medium ${discountEnabled ? "text-emerald-700" : "text-muted-foreground"}`} > WAC-to-Net Discount </label> <InfoButton sources={DISCOUNT_SOURCES}> <p> For recently approved oncology drugs, the typical discount off WAC ranges from 20% to 35%, depending on payer mix, administration setting, and competition. </p> <p className="mt-2"> Use ~20% for novel, first-in-class drugs with limited exposure to 340B, Medicaid, and PBM rebates. </p> <p className="mt-2"> Use ~35% if the drug is administered in hospital/clinic settings, covered under Medicare Part D, or faces competitive pressure. </p> </InfoButton> <Switch id="discount-toggle" checked={discountEnabled} onCheckedChange={onToggleDiscount} /> </div> </div> );}- Step 2: Verify TypeScript compiles
Run: pnpm tsc --noEmit
Expected: No errors.
- Step 3: Commit
git add src/features/reports/forecasting/sections/Pricing/PricingFilterRow.tsxgit commit -m "feat(pricing): add PricingFilterRow with discount toggle and InfoButton"Task 6: Create PricingTable component
Section titled “Task 6: Create PricingTable component”Files:
-
Create:
src/features/reports/forecasting/sections/Pricing/PricingTable.tsx -
Step 1: Create
PricingTable
Create src/features/reports/forecasting/sections/Pricing/PricingTable.tsx:
import { useMemo, useState } from "react";import type { ReactElement } from "react";import { computeNetPrice, filterPricingData, sortByDateDesc, targetNormalize, toThousandSeparator,} from "./pricing-utils";import type { PricingEntry, PricingFilters } from "./pricing-utils";
interface PricingTableProps { data: PricingEntry[]; discountEnabled: boolean; showAll: boolean; onToggleShowAll: (checked: boolean) => void;}
export function PricingTable({ data, discountEnabled, showAll, onToggleShowAll,}: PricingTableProps): ReactElement { const [filters, setFilters] = useState<PricingFilters>({ brand: "", indication: "", target: "", technology: "", }); const [discounts, setDiscounts] = useState<Record<string, string>>({});
const filteredData = useMemo(() => { return sortByDateDesc(filterPricingData(data, filters)); }, [data, filters]);
const updateFilter = (key: keyof PricingFilters, value: string) => { setFilters((prev) => ({ ...prev, [key]: value })); };
const handleDiscountChange = (key: string, value: string) => { setDiscounts((prev) => ({ ...prev, [key]: value })); };
const formatDate = (dateStr: string | null): string => { if (!dateStr) return "—"; const d = new Date(dateStr); if (Number.isNaN(d.getTime())) return "—"; return `${String(d.getMonth() + 1).padStart(2, "0")}/${d.getFullYear()}`; };
return ( <div> {/* Search filters */} <div className="grid grid-cols-4 gap-3 p-4 border-b border-border"> <input type="text" placeholder="Filter by brand name..." className="px-3 py-1.5 text-xs border border-border rounded-md bg-muted/30 outline-none focus:border-ring focus:ring-2 focus:ring-ring/10" value={filters.brand} onChange={(e) => updateFilter("brand", e.target.value)} /> <input type="text" placeholder="Filter by indication..." className="px-3 py-1.5 text-xs border border-border rounded-md bg-muted/30 outline-none focus:border-ring focus:ring-2 focus:ring-ring/10" value={filters.indication} onChange={(e) => updateFilter("indication", e.target.value)} /> <input type="text" placeholder="Filter by target..." className="px-3 py-1.5 text-xs border border-border rounded-md bg-muted/30 outline-none focus:border-ring focus:ring-2 focus:ring-ring/10" value={filters.target} onChange={(e) => updateFilter("target", e.target.value)} /> <input type="text" placeholder="Filter by technology..." className="px-3 py-1.5 text-xs border border-border rounded-md bg-muted/30 outline-none focus:border-ring focus:ring-2 focus:ring-ring/10" value={filters.technology} onChange={(e) => updateFilter("technology", e.target.value)} /> </div>
{/* Table */} <div className="overflow-x-auto"> <table className="w-full text-sm"> <thead> <tr className="bg-muted/50"> <th className="text-left px-4 py-2.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground"> Brand Name </th> <th className="text-left px-4 py-2.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground"> Indication </th> <th className="text-right px-4 py-2.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground"> Monthly WAC ($) </th> <th className="text-left px-4 py-2.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground"> Target </th> <th className="text-left px-4 py-2.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground"> Technology </th> <th className="text-center px-4 py-2.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground"> Date Approved </th> {discountEnabled && ( <> <th className="text-center px-4 py-2.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground bg-amber-50"> Discount % </th> <th className="text-right px-4 py-2.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground bg-amber-50"> Monthly Net ($) </th> </> )} </tr> </thead> <tbody> {filteredData.map((item) => { const key = `${item.brand_name}/${item.indication}`; const discountStr = discounts[key] ?? ""; const discountNum = Number.parseFloat(discountStr) || 0; const wac = Number(item.monthly_wac_price) || 0;
return ( <tr key={item.id} className="border-b border-border/30 hover:bg-muted/20"> <td className="px-4 py-2.5 font-semibold">{item.brand_name}</td> <td className="px-4 py-2.5">{item.indication}</td> <td className="px-4 py-2.5 text-right font-mono font-medium"> ${toThousandSeparator(item.monthly_wac_price)} </td> <td className="px-4 py-2.5"> {targetNormalize(item.target ?? undefined).map((t) => ( <span key={t} className="inline-block px-2 py-0.5 mr-1 mb-0.5 rounded bg-violet-50 text-violet-500 text-[11px] font-medium" > {t} </span> ))} </td> <td className="px-4 py-2.5"> {item.technology && ( <span className="inline-block px-2 py-0.5 rounded bg-emerald-50 text-emerald-600 text-[11px] font-medium"> {item.technology} </span> )} </td> <td className="px-4 py-2.5 text-center text-muted-foreground text-xs"> {formatDate(item.date_approved)} </td> {discountEnabled && ( <> <td className="px-4 py-2.5 text-center"> <input type="number" min="0" max="100" placeholder="—" className="w-14 px-2 py-1 text-xs text-center border-[1.5px] border-amber-400 rounded bg-amber-50 outline-none focus:border-amber-500 focus:ring-2 focus:ring-amber-500/15 font-medium" value={discountStr} onChange={(e) => handleDiscountChange(key, e.target.value)} /> </td> <td className="px-4 py-2.5 text-right font-mono font-semibold"> {discountNum > 0 ? ( <span className="text-emerald-700"> ${toThousandSeparator(computeNetPrice(wac, discountNum))} </span> ) : ( <span className="text-muted-foreground/40">—</span> )} </td> </> )} </tr> ); })} {filteredData.length === 0 && ( <tr> <td colSpan={discountEnabled ? 8 : 6} className="px-4 py-8 text-center text-muted-foreground text-sm" > No drugs found matching the current filters. </td> </tr> )} </tbody> </table> </div>
{/* Show all toggle */} <div className="flex items-center gap-2 px-4 py-2.5 border-t border-border"> <input type="checkbox" id="showAllDrugs" checked={showAll} onChange={(e) => onToggleShowAll(e.target.checked)} className="accent-primary w-3.5 h-3.5" /> <label htmlFor="showAllDrugs" className="text-xs text-muted-foreground"> Show all drugs (across all indications) </label> </div> </div> );}- Step 2: Verify TypeScript compiles
Run: pnpm tsc --noEmit
Expected: No errors.
- Step 3: Commit
git add src/features/reports/forecasting/sections/Pricing/PricingTable.tsxgit commit -m "feat(pricing): add PricingTable component with filters and discount columns"Task 7: Create PricingLayout, barrel export, and wire into ForecastingContent
Section titled “Task 7: Create PricingLayout, barrel export, and wire into ForecastingContent”Files:
-
Create:
src/features/reports/forecasting/sections/Pricing/PricingLayout.tsx -
Create:
src/features/reports/forecasting/sections/Pricing/index.ts -
Modify:
src/features/reports/forecasting/Forecasting.tsx -
Step 1: Create
PricingLayout
Create src/features/reports/forecasting/sections/Pricing/PricingLayout.tsx:
import { useAtomValue } from "jotai";import { useState } from "react";import type { ReactElement } from "react";import { useQuery } from "@tanstack/react-query";import { Footer } from "@/components/ui/footer";import { Header } from "@/components/ui/header";import { InfoButton } from "@/components/ui/info-button";import { SectionCard } from "@/components/ui/section-card";import { SectionNav } from "@/components/ui/section-nav";import { Api } from "@/lib/api";import { cn } from "@/lib/utils";import { indicationAtom } from "../../atoms";import { useStickyScroll } from "../../hooks/useStickyScroll";import { ScenarioTabBar } from "../../scenarios";import { PRICING_SECTIONS } from "../../section-config";import { PricingFilterRow } from "./PricingFilterRow";import { PricingTable } from "./PricingTable";import type { PricingEntry } from "./pricing-utils";
interface PricingApiResponse { data: PricingEntry[]; meta: { total_count: number };}
async function fetchPricingData(disease: string | null): Promise<PricingEntry[]> { const params = disease ? `?disease=${encodeURIComponent(disease)}` : ""; const response = await Api.get(`/api/forecasting_pricings${params}`); const json: PricingApiResponse = await response.json(); return json.data;}
export function PricingLayout(): ReactElement { const currentIndication = useAtomValue(indicationAtom); const isSticky = useStickyScroll();
const [diseaseFilter] = useState<string>(currentIndication ?? ""); const [showAll, setShowAll] = useState(false); const [discountEnabled, setDiscountEnabled] = useState(false);
const queryDisease = showAll ? null : diseaseFilter;
const { data: pricingData = [], isLoading, error } = useQuery({ queryKey: ["forecasting-pricings", queryDisease], queryFn: () => fetchPricingData(queryDisease), });
const sectionTooltip = ( <InfoButton> Pricing data derived from the Texas DSHS Prescription Drug Price Disclosure Program (WAC + Price Increase Reports). Per-milligram costs were calculated using FDA-listed active ingredients, then converted to approximate monthly prices based on standard-of-care dosing regimens. </InfoButton> );
return ( <div className="min-h-screen bg-muted/30 flex flex-col"> <Header /> <SectionNav sections={PRICING_SECTIONS} />
<main className="w-[90%] lg:w-[75%] mx-auto pt-8 pb-8 flex flex-col gap-4 flex-grow"> {/* Sticky bar */} <div className={cn( "sticky top-[72px] z-20 -mt-2 px-6 py-3 transition-all duration-300", isSticky ? "bg-muted border-border shadow-lg rounded-none backdrop-blur-md" : "bg-background/95 border shadow-sm rounded-xl backdrop-blur-sm", )} > <ScenarioTabBar /> <PricingFilterRow diseaseFilter={diseaseFilter} discountEnabled={discountEnabled} onToggleDiscount={setDiscountEnabled} /> </div>
{/* Content */} <SectionCard id="section-pricing" title="Drug Pricing Benchmark" titleTooltip={sectionTooltip} headerMeta={ <span className="text-xs font-medium text-muted-foreground bg-muted px-2 py-0.5 rounded-full"> {pricingData.length} drugs </span> } > {isLoading ? ( <div className="flex items-center justify-center py-16 text-sm text-muted-foreground"> Loading pricing data... </div> ) : error ? ( <div className="flex items-center justify-center py-16 text-sm text-destructive"> Failed to load pricing data. Please try again. </div> ) : ( <PricingTable data={pricingData} discountEnabled={discountEnabled} showAll={showAll} onToggleShowAll={setShowAll} /> )} </SectionCard> </main>
<Footer /> </div> );}- Step 2: Create barrel export
Create src/features/reports/forecasting/sections/Pricing/index.ts:
export { PricingFilterRow } from "./PricingFilterRow";export { PricingLayout } from "./PricingLayout";- Step 3: Wire
PricingLayoutintoForecastingContent
In src/features/reports/forecasting/Forecasting.tsx:
Add the import (after the Comparison imports, around line 59):
import { PricingLayout } from "./sections/Pricing";In ForecastingContent (around line 578), add the pricing branch before the comparison branch:
if (activeView === "pricing") { return ( <Provider store={activeStore}> <PricingLayout /> </Provider> );}
if (activeView === "comparison") {- Step 4: Verify TypeScript compiles
Run: pnpm tsc --noEmit
Expected: No errors.
- Step 5: Run lint
Run: pnpm lint
Expected: No new violations. Fix any issues (e.g., unused imports).
- Step 6: Build verification
Run: pnpm build
Expected: Build succeeds.
- Step 7: Commit
git add src/features/reports/forecasting/sections/Pricing/ src/features/reports/forecasting/Forecasting.tsx src/features/reports/forecasting/section-config.tsgit commit -m "feat(pricing): add PricingLayout and wire into ForecastingContent"Task 8: Update frontend API path for statistics rename
Section titled “Task 8: Update frontend API path for statistics rename”Files:
- Modify:
src/hooks/useStatistics.ts
Note: This task should be done AFTER the API session completes the
statistics→forecasting_statisticsrename. Until then, skip this task.
- Step 1: Update the API endpoint
In src/hooks/useStatistics.ts, line 45, change:
const response = await Api.get("/api/statistics");to:
const response = await Api.get("/api/forecasting_statistics");- Step 2: Update the query key
In src/hooks/useStatistics.ts, line 72, change:
export const STATISTICS_QUERY_KEY = ["statistics"] as const;to:
export const STATISTICS_QUERY_KEY = ["forecasting-statistics"] as const;- Step 3: Search for any other references
Run: grep -r '"/api/statistics"' --include="*.ts" --include="*.tsx" src/
Expected: Zero matches.
- Step 4: Verify TypeScript compiles and build succeeds
Run: pnpm tsc --noEmit && pnpm build
Expected: No errors.
- Step 5: Commit
git add src/hooks/useStatistics.tsgit commit -m "feat: update statistics API path to /api/forecasting_statistics"Task 9: Final verification
Section titled “Task 9: Final verification”- Step 1: Run full lint
Run: pnpm lint
Expected: No violations.
- Step 2: Run full build
Run: pnpm build
Expected: Build succeeds with no warnings.
- Step 3: Visual smoke test
Start the dev server: pnpm dev
Verify:
- Load any model — scenario tabs appear
- Compare tab still works (click → comparison view → “Back to editor” → back)
- Pricing tab appears next to Compare with dollar-sign icon
- Click Pricing → pricing view loads with:
- “Back to editor” button works
- Indication chip shows the current disease
- Discount toggle works (shows/hides columns)
- InfoButton next to toggle shows discount guidance + sources
- Section header has InfoButton with data source description
- Table loads data (requires API to be running)
- Text filters work
- “Show all drugs” checkbox re-fetches without disease filter
- Discount inputs compute net prices correctly
- Step 4: Commit any final fixes
If any issues found during smoke testing, fix and commit.