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 (
What the tool recommends based on current heat conditions and your filters.
Filtered by the Who / Where selections above.
Each slice of the annual program cost above goes to a different payer.
Based on 20 years of climatology, this is the expected annual flow per neighborhood. The column totals to the Annual program cost above.
| 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 })}`} |
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.