import { useState } from 'react' import { ChevronDown, ChevronRight } from 'lucide-react' import { LoadingSpinner, ErrorState } from '../components/LoadingState' import { useCoverageRecommendation } from '../lib/api' import MetricCard from '../components/MetricCard' import { REGION } from '../regionConfig' function formatUsd(n: number | undefined | null): string { if (n == null) return '—' if (n >= 1_000_000) return '$' + (n / 1_000_000).toFixed(1) + 'M' if (n >= 1_000) return '$' + (n / 1_000).toFixed(0) + 'K' return '$' + n.toLocaleString(undefined, { maximumFractionDigits: 0 }) } const GENDERS = [ { key: 'all', label: 'All' }, { key: 'women', label: 'Women' }, { key: 'men', label: 'Men' }, ] const SETTLEMENTS = [ { key: 'all', label: 'All zones' }, { key: 'informal', label: 'Informal' }, { key: 'mixed', label: 'Mixed' }, { key: 'formal', label: 'Formal' }, ] export default function ProgramDesigner() { const [payoutUsd] = useState(10) const [showModel, setShowModel] = useState(false) const [gender, setGender] = useState('all') const [settlement, setSettlement] = useState('all') const [workerPct, setWorkerPct] = useState(15) const [philPct, setPhilPct] = useState(45) const [insPct, setInsPct] = useState(40) function adjustSplit(changed: 'worker' | 'phil' | 'ins', newVal: number) { const val = Math.max(0, Math.min(100, newVal)) if (changed === 'phil') { const remaining = 100 - val const otherTotal = insPct + workerPct || 1 setPhilPct(val) setInsPct(Math.round((remaining * insPct) / otherTotal)) setWorkerPct(100 - val - Math.round((remaining * insPct) / otherTotal)) } else if (changed === 'ins') { const remaining = 100 - val const otherTotal = philPct + workerPct || 1 setInsPct(val) setPhilPct(Math.round((remaining * philPct) / otherTotal)) setWorkerPct(100 - val - Math.round((remaining * philPct) / otherTotal)) } else { const remaining = 100 - val const otherTotal = philPct + insPct || 1 setWorkerPct(val) setPhilPct(Math.round((remaining * philPct) / otherTotal)) setInsPct(100 - val - Math.round((remaining * philPct) / otherTotal)) } } const coverage = useCoverageRecommendation(payoutUsd, `${gender}|${settlement}`) if (coverage.isLoading) { return (

Coverage recommendation

) } if (coverage.isError) return coverage.refetch()} /> const rec = coverage.data?.recommendation const zones = coverage.data?.zones ?? [] const annual = rec?.annual_cost_total ?? 0 const annualPerWorker = rec?.annual_cost_per_worker ?? 0 const philUsd = (annual * philPct) / 100 const insUsd = (annual * insPct) / 100 const workerUsd = (annual * workerPct) / 100 const philPerWorker = (annualPerWorker * philPct) / 100 const insPerWorker = (annualPerWorker * insPct) / 100 const workerPerYear = (annualPerWorker * workerPct) / 100 return (
{/* Editorial header */}

A heat insurance program for {rec?.total_workers?.toLocaleString() ?? '—'} outdoor workers in {REGION.primaryCity}

What the tool recommends based on current heat conditions and your filters.

{/* Filter chips */}
Who {GENDERS.map((g) => ( ))} Where {SETTLEMENTS.map((s) => ( ))}
{/* Section 1 — Scope */}
Who's covered

Filtered by the Who / Where selections above.

{/* Section 2 — Cost */}
What it costs
{[ { label: 'Annual program cost', value: formatUsd(rec?.annual_cost_total), subtitle: `to protect ${rec?.total_workers?.toLocaleString() ?? '—'} workers`, }, { label: 'Cost per worker', value: `$${rec?.annual_cost_per_worker ?? '—'}`, subtitle: 'per year', }, { label: '3-year total', value: formatUsd((rec?.annual_cost_total ?? 0) * 3), subtitle: 'full pilot horizon', }, ].map((m) => (
{m.label}
{m.value}
{m.subtitle}
))}
{/* Section 3 — Funding */}
How it's funded

Each slice of the annual program cost above goes to a different payer.

{/* Philanthropy row */}
Philanthropy {philPct}% · {formatUsd(philUsd)} · ${philPerWorker.toFixed(2)}/worker
adjustSplit('phil', Number(e.target.value))} style={{ width: '100%', marginTop: '8px', accentColor: '#8a3d28' }} />
{/* Insurance row */}
Insurance / Government {insPct}% · {formatUsd(insUsd)} · ${insPerWorker.toFixed(2)}/worker
adjustSplit('ins', Number(e.target.value))} style={{ width: '100%', marginTop: '8px', accentColor: '#606373' }} />
{/* Worker row */}
Worker contribution {workerPct}% · {formatUsd(workerUsd)} · ${workerPerYear.toFixed(2)}/worker/yr
adjustSplit('worker', Number(e.target.value))} style={{ width: '100%', marginTop: '8px', accentColor: '#8d909e' }} />
{/* Section 4 — Distribution */}
Where it lands

Based on 20 years of climatology, this is the expected annual flow per neighborhood. The column totals to the Annual program cost above.

{[...zones] .map((z) => ({ z, annualTotal: (z.annual_cost_per_worker ?? 0) * (z.enrolled_workers ?? 0), })) .sort((a, b) => b.annualTotal - a.annualTotal) .map(({ z, annualTotal }) => ( ))}
Neighborhood Workers enrolled Trigger days / year Annual $ / worker Annual $ to neighborhood
{z.zone_name}
{z.settlement_type}
{(z.enrolled_workers ?? 0).toLocaleString()} {z.annual_trigger_days ?? 0} ${z.annual_cost_per_worker ?? 0} {`$${annualTotal.toLocaleString(undefined, { maximumFractionDigits: 0 })}`}
{/* Model disclosure (collapsible) */}
{showModel && (

Empirical burn analysis — the same approach operational parametric insurers (ARC, CCRIF, SEWA pilot) use. Twenty years of ERA5-Land reanalysis data are replayed through the trigger rules, with zone-specific urban heat island correction, to count how often each tier would have paid out historically. Expected annual loss × loading factors (basis risk, admin, contingency) gives the loaded premium, then the SEWA funding split allocates cost across workers, philanthropy, and insurer.

)}
) }