climate-risk-engine / frontend /src /pages /ProgramDesigner.tsx
jtlevine's picture
Mobile responsive: hamburger sidebar + responsive grids
eaf5c00
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 (
<div className="animate-slide-up">
<div data-tour="program-title" style={{ marginBottom: '20px' }}>
<h2 className="page-title">Coverage recommendation</h2>
</div>
<LoadingSpinner message="Loading coverage data…" />
</div>
)
}
if (coverage.isError) return <ErrorState onRetry={() => 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 (
<div className="animate-slide-up">
{/* Editorial header */}
<div data-tour="program-title" style={{ marginBottom: '20px' }}>
<h2 className="page-title" style={{ maxWidth: '780px' }}>
A heat insurance program for {rec?.total_workers?.toLocaleString() ?? '—'} outdoor workers in {REGION.primaryCity}
</h2>
<p className="page-caption" style={{ maxWidth: '640px' }}>
What the tool recommends based on current heat conditions and your filters.
</p>
</div>
{/* Filter chips */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '40px',
flexWrap: 'wrap',
}}
>
<span className="eyebrow" style={{ marginRight: '4px' }}>Who</span>
{GENDERS.map((g) => (
<button
key={g.key}
onClick={() => setGender(g.key)}
className={`chip ${gender === g.key ? 'active' : ''}`}
>
{g.label}
</button>
))}
<span className="eyebrow" style={{ marginLeft: '16px', marginRight: '4px' }}>
Where
</span>
{SETTLEMENTS.map((s) => (
<button
key={s.key}
onClick={() => setSettlement(s.key)}
className={`chip ${settlement === s.key ? 'active' : ''}`}
>
{s.label}
</button>
))}
</div>
{/* Section 1 — Scope */}
<div data-tour="program-scope" style={{ marginBottom: '48px' }}>
<div className="section-header">Who's covered</div>
<div
className="grid grid-cols-1 sm:grid-cols-2 gap-6 sm:gap-8"
style={{
borderTop: '1px solid #e8e5e1',
paddingTop: '24px',
}}
>
<MetricCard
label="Workers enrolled"
value={rec?.total_workers?.toLocaleString() ?? '—'}
subtitle="outdoor workers in the program"
/>
<MetricCard
label="Neighborhoods covered"
value={zones.length}
subtitle={`of ${rec?.zones_total ?? '—'} monitored`}
/>
</div>
<p
style={{
marginTop: '12px',
fontFamily: '"Space Grotesk", system-ui, sans-serif',
fontSize: '12px',
color: '#8d909e',
}}
>
Filtered by the Who / Where selections above.
</p>
</div>
{/* Section 2 — Cost */}
<div data-tour="program-cost" style={{ marginBottom: '48px' }}>
<div className="section-header">What it costs</div>
<div
className="grid grid-cols-1 md:grid-cols-[1.4fr_1fr_1fr] gap-6 md:gap-8"
style={{
borderTop: '1px solid #e8e5e1',
paddingTop: '28px',
alignItems: 'end',
}}
>
{[
{
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) => (
<div key={m.label}>
<div
style={{
fontFamily: '"Space Grotesk", system-ui, sans-serif',
fontSize: '11px',
fontWeight: 500,
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: '#8d909e',
}}
>
{m.label}
</div>
<div
style={{
fontFamily: '"Source Serif 4", Georgia, serif',
fontSize: '44px',
lineHeight: '48px',
fontWeight: 400,
color: '#1b1e2d',
letterSpacing: '-0.01em',
fontVariantNumeric: 'tabular-nums',
marginTop: '6px',
}}
>
{m.value}
</div>
<div
style={{
fontFamily: '"Space Grotesk", system-ui, sans-serif',
fontSize: '12px',
color: '#8d909e',
marginTop: '6px',
}}
>
{m.subtitle}
</div>
</div>
))}
</div>
</div>
{/* Section 3 — Funding */}
<div className="section-header">How it's funded</div>
<p
style={{
fontFamily: '"Space Grotesk", system-ui, sans-serif',
fontSize: '12px',
color: '#8d909e',
marginTop: '-4px',
marginBottom: '16px',
maxWidth: '720px',
}}
>
Each slice of the annual program cost above goes to a different payer.
</p>
<div style={{ marginBottom: '40px', maxWidth: '720px' }}>
{/* Philanthropy row */}
<div style={{ marginBottom: '20px' }}>
<div
style={{
display: 'flex',
alignItems: 'baseline',
justifyContent: 'space-between',
marginBottom: '6px',
}}
>
<span
style={{
fontFamily: '"Space Grotesk", system-ui, sans-serif',
fontSize: '13px',
fontWeight: 500,
color: '#8a3d28',
}}
>
Philanthropy
</span>
<span
style={{
fontFamily: '"Space Grotesk", system-ui, sans-serif',
fontSize: '13px',
color: '#606373',
fontVariantNumeric: 'tabular-nums',
}}
>
{philPct}% · {formatUsd(philUsd)} · ${philPerWorker.toFixed(2)}/worker
</span>
</div>
<div style={{ position: 'relative', height: '6px', background: '#fcfaf7', border: '1px solid #e8e5e1' }}>
<div
style={{
position: 'absolute',
inset: 0,
width: `${philPct}%`,
background: '#8a3d28',
}}
/>
</div>
<input
type="range"
min={0}
max={100}
step={5}
value={philPct}
onChange={(e) => adjustSplit('phil', Number(e.target.value))}
style={{ width: '100%', marginTop: '8px', accentColor: '#8a3d28' }}
/>
</div>
{/* Insurance row */}
<div style={{ marginBottom: '20px' }}>
<div
style={{
display: 'flex',
alignItems: 'baseline',
justifyContent: 'space-between',
marginBottom: '6px',
}}
>
<span
style={{
fontFamily: '"Space Grotesk", system-ui, sans-serif',
fontSize: '13px',
fontWeight: 500,
color: '#1b1e2d',
}}
>
Insurance / Government
</span>
<span
style={{
fontFamily: '"Space Grotesk", system-ui, sans-serif',
fontSize: '13px',
color: '#606373',
fontVariantNumeric: 'tabular-nums',
}}
>
{insPct}% · {formatUsd(insUsd)} · ${insPerWorker.toFixed(2)}/worker
</span>
</div>
<div style={{ position: 'relative', height: '6px', background: '#fcfaf7', border: '1px solid #e8e5e1' }}>
<div
style={{
position: 'absolute',
inset: 0,
width: `${insPct}%`,
background: '#606373',
}}
/>
</div>
<input
type="range"
min={0}
max={100}
step={5}
value={insPct}
onChange={(e) => adjustSplit('ins', Number(e.target.value))}
style={{ width: '100%', marginTop: '8px', accentColor: '#606373' }}
/>
</div>
{/* Worker row */}
<div>
<div
style={{
display: 'flex',
alignItems: 'baseline',
justifyContent: 'space-between',
marginBottom: '6px',
}}
>
<span
style={{
fontFamily: '"Space Grotesk", system-ui, sans-serif',
fontSize: '13px',
fontWeight: 500,
color: '#8d909e',
}}
>
Worker contribution
</span>
<span
style={{
fontFamily: '"Space Grotesk", system-ui, sans-serif',
fontSize: '13px',
color: '#606373',
fontVariantNumeric: 'tabular-nums',
}}
>
{workerPct}% · {formatUsd(workerUsd)} · ${workerPerYear.toFixed(2)}/worker/yr
</span>
</div>
<div style={{ position: 'relative', height: '6px', background: '#fcfaf7', border: '1px solid #e8e5e1' }}>
<div
style={{
position: 'absolute',
inset: 0,
width: `${workerPct}%`,
background: '#8d909e',
}}
/>
</div>
<input
type="range"
min={0}
max={100}
step={5}
value={workerPct}
onChange={(e) => adjustSplit('worker', Number(e.target.value))}
style={{ width: '100%', marginTop: '8px', accentColor: '#8d909e' }}
/>
</div>
</div>
{/* Section 4 — Distribution */}
<div data-tour="program-zones" style={{ marginBottom: '40px' }}>
<div className="section-header">Where it lands</div>
<p
style={{
fontFamily: '"Space Grotesk", system-ui, sans-serif',
fontSize: '12px',
color: '#8d909e',
marginTop: '-4px',
marginBottom: '16px',
maxWidth: '720px',
}}
>
Based on 20 years of climatology, this is the expected annual flow per
neighborhood. The column totals to the Annual program cost above.
</p>
<table className="etable">
<thead>
<tr>
<th>Neighborhood</th>
<th className="num">Workers enrolled</th>
<th className="num">Trigger days / year</th>
<th className="num">Annual $ / worker</th>
<th className="num">Annual $ to neighborhood</th>
</tr>
</thead>
<tbody>
{[...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 }) => (
<tr key={z.zone_id}>
<td>
<div style={{ color: '#1b1e2d' }}>{z.zone_name}</div>
<div style={{ fontSize: '11px', color: '#8d909e' }}>
{z.settlement_type}
</div>
</td>
<td className="num">
{(z.enrolled_workers ?? 0).toLocaleString()}
</td>
<td className="num">{z.annual_trigger_days ?? 0}</td>
<td className="num">${z.annual_cost_per_worker ?? 0}</td>
<td
className="num"
style={{ fontWeight: 500, color: '#1b1e2d' }}
>
{`$${annualTotal.toLocaleString(undefined, { maximumFractionDigits: 0 })}`}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Model disclosure (collapsible) */}
<div>
<button
onClick={() => setShowModel(!showModel)}
className="text-link"
style={{ display: 'flex', alignItems: 'center', gap: '6px', color: '#606373' }}
>
{showModel ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
About this model
</button>
{showModel && (
<div
style={{
marginTop: '16px',
paddingTop: '16px',
borderTop: '1px solid #e8e5e1',
maxWidth: '640px',
}}
>
<p
style={{
fontFamily: '"Space Grotesk", system-ui, sans-serif',
fontSize: '13px',
lineHeight: 1.7,
color: '#606373',
}}
>
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.
</p>
</div>
)}
</div>
</div>
)
}