Skip to content

2026 04 08 Pricing Benchmark

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


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 compareActiveactiveView 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 ScenarioContextValue interface

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 ActiveView type

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
Terminal window
git add src/features/reports/forecasting/scenarios/ScenarioProvider.tsx src/features/reports/forecasting/scenarios/index.ts
git 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 ForecastingContent in 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 compareActive references

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
Terminal window
git add src/features/reports/forecasting/Forecasting.tsx src/features/reports/forecasting/scenarios/ScenarioTabBar.tsx src/features/reports/forecasting/sections/Comparison/ComparisonFilterRow.tsx
git 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 PricingTab component

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 PricingTab to ScenarioTabBar

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
Terminal window
git add src/features/reports/forecasting/scenarios/PricingTab.tsx src/features/reports/forecasting/scenarios/ScenarioTabBar.tsx src/features/reports/forecasting/scenarios/index.ts
git 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_SECTIONS to 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
Terminal window
git add src/features/reports/forecasting/sections/Pricing/pricing-utils.ts src/features/reports/forecasting/section-config.ts
git commit -m "feat(pricing): add pricing utilities and section config"

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
Terminal window
git add src/features/reports/forecasting/sections/Pricing/PricingFilterRow.tsx
git commit -m "feat(pricing): add PricingFilterRow with discount toggle and InfoButton"

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
Terminal window
git add src/features/reports/forecasting/sections/Pricing/PricingTable.tsx
git 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 PricingLayout into ForecastingContent

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
Terminal window
git add src/features/reports/forecasting/sections/Pricing/ src/features/reports/forecasting/Forecasting.tsx src/features/reports/forecasting/section-config.ts
git 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 statisticsforecasting_statistics rename. 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
Terminal window
git add src/hooks/useStatistics.ts
git commit -m "feat: update statistics API path to /api/forecasting_statistics"

  • 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:

  1. Load any model — scenario tabs appear
  2. Compare tab still works (click → comparison view → “Back to editor” → back)
  3. Pricing tab appears next to Compare with dollar-sign icon
  4. 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
  5. Table loads data (requires API to be running)
  6. Text filters work
  7. “Show all drugs” checkbox re-fetches without disease filter
  8. Discount inputs compute net prices correctly
  • Step 4: Commit any final fixes

If any issues found during smoke testing, fix and commit.