File size: 11,721 Bytes
5c33366 f02f32b e2d3383 273b642 e2d3383 273b642 f02f32b f811c9b 273b642 e2d3383 273b642 e2d3383 273b642 e2d3383 c7d99b0 e2d3383 273b642 e2d3383 273b642 e2d3383 273b642 e2d3383 273b642 e2d3383 273b642 e2d3383 273b642 e2d3383 f811c9b e2d3383 f811c9b e2d3383 f811c9b e2d3383 f811c9b e2d3383 f811c9b e2d3383 1df3b8c f02f32b 1df3b8c f811c9b 1df3b8c e2d3383 f811c9b e2d3383 f811c9b e2d3383 f811c9b e2d3383 d73c30b b8d2182 d73c30b 273b642 b8d2182 d73c30b b8d2182 273b642 d73c30b b8d2182 f02f32b b8d2182 d73c30b b8d2182 f02f32b d73c30b 273b642 d73c30b d030f97 d73c30b d030f97 273b642 f811c9b 273b642 d73c30b 1b15bc9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 | import { useQuery } from '@tanstack/react-query'
import type { DisbursementsResponse } from '../types'
// ββ Fixture-backed data layer βββββββββββββββββββββββββββββββββββββββββββββ
// The portfolio copy of this dashboard runs entirely off in-memory fixtures
// so the dashboard renders a fully-populated experience without a Neon
// database or deployed Vercel functions. Hook names, types, and return
// shapes are preserved verbatim so the pages are unaware of the swap.
// Fixtures are imported lazily inside each queryFn so TypeScript still
// type-checks the api.ts interface exports even if mockData.ts moves.
const BASE_URL = import.meta.env.VITE_API_URL ?? ''
async function fetchJson<T>(path: string): Promise<T> {
const res = await fetch(`${BASE_URL}${path}`)
if (!res.ok) throw new Error(`API error: ${res.status} ${res.statusText}`)
return res.json() as Promise<T>
}
function mock<T>(value: T, delayMs = 180): Promise<T> {
// Short delay so React Query still flashes a loading state on first paint.
return new Promise((resolve) => setTimeout(() => resolve(value), delayMs))
}
// ββ Types (matched to actual API response field names) ββββββββββββββββββ
// Field names are kept verbatim-aligned with the Neon serverless handlers
// in `api/*.ts` so the porting chat can swap `mock(...)` β `fetch(...)`
// without touching page components. Any field in these interfaces is
// guaranteed to be returned by the corresponding handler; fields the
// backend doesn't return are omitted even if the mock could compute them.
export interface Zone {
zone_id: string
name: string
city: string
country: string
latitude: number
longitude: number
elevation_m: number
settlement_type: string
worker_population_est: number
outdoor_exposure_pct: number
heat_vulnerability: string
risk_level: string
corrected_temp_c: number
current_wbgt_c: number
current_heat_index_c: number
max_temp_c: number
max_wbgt_c: number
consecutive_hot_days: number
trigger_probability_7d: number
prediction_confidence: number
model_tier: string
// Zone-specific trigger thresholds from the zone_thresholds table (populated
// by the pipeline when THRESHOLD_MODE=zone_specific). Null for zones without
// calibrated thresholds β consumers fall back to city-wide defaults (35.1/38.8).
alert_threshold_c?: number | null
payout_threshold_c?: number | null
threshold_uhi_model?: string | null
threshold_mode?: string | null
}
export interface ZonesResponse {
zones: Zone[]
total: number
cities: string[]
}
export interface DailyHeat {
date: string
temp_c: number
grid_temp_c: number
wbgt_c: number
}
export interface IndexData {
zone_id: string
zone_name: string
city: string
temp_current: number
wbgt_current: number
risk_level: string
daily_history: DailyHeat[]
}
export interface IndicesResponse {
indices: IndexData[]
total: number
}
export interface Trigger {
zone_id: string
zone_name: string
city: string
trigger_level: string
triggered_at: string
heat_risk_score: number
max_temp_c: number
max_wbgt_c: number
consecutive_days: number
settlement_type: string
payout_per_worker_usd: number
}
export interface TriggersResponse {
triggers: Trigger[]
total: number
}
export interface BasisRisk {
zone_id: string
zone_name: string
city: string
settlement_type: string
false_positive_rate: number
false_negative_rate: number
correlation: number
mae: number
accuracy_by_tier: Record<string, number>
recommendations: unknown
}
export interface BasisRiskResponse {
basis_risk: BasisRisk[]
total: number
}
export interface Notification {
id: string
zone_id: string
zone_name: string
city: string
trigger_level: string
channel: string
language: string
recipient_count: number
message_preview: string
status: string
delivered_at: string
}
export interface NotificationsResponse {
notifications: Notification[]
by_language: Record<string, number>
}
export interface PipelineStep {
step: string
status: string
duration_s: number
}
export interface PipelineRun {
run_id: string
started_at: string
ended_at: string
status: string
duration_s: number
zones_processed: number
triggers_found: number
notifications_sent: number
total_cost_usd: number
steps: PipelineStep[]
}
export interface PipelineRunsResponse {
runs: PipelineRun[]
total: number
}
export interface PipelineStats {
total_runs: number
successful_runs: number
success_rate: number
zones_monitored: number
cities: number
active_triggers: number
total_enrolled: number
total_cost_usd: number
avg_cost_per_run_usd: number
last_run: string | null
data_sources: string[]
}
export interface EnrolledResponse {
by_zone: { zone_id: string; enrolled: number; worker_population: number }[]
total_enrolled: number
}
export interface CalibrateParams {
temp_threshold: number
consecutive_days: number
wbgt_threshold: number
payout_usd: number
budget_usd: number
worker_contribution_usd: number
}
export interface CalibrateZoneResult {
zone_id: string
zone_name: string
city: string
settlement_type: string
heat_vulnerability: string
enrolled_workers: number
days_above_temp: number
days_above_wbgt: number
consecutive_days_temp: number
consecutive_days_wbgt: number
trigger_events: number
events_per_year: number
annual_payout_per_worker: number
annual_payout_total: number
basis_risk_score: number
triggered: boolean
actuarial_cost_per_worker: number
cost_breakdown: Record<string, any>
allocated_budget: number
workers_covered: number
coverage_pct: number
priority_rank: number
}
export interface CalibrateSummary {
total_zones: number
zones_triggered: number
total_trigger_days: number
avg_events_per_year: number
total_annual_cost: number
avg_cost_per_worker: number
total_enrolled: number
avg_basis_risk: number
}
export interface CalibrateAllocation {
budget_usd: number
worker_contribution_usd: number
workers_covered: number
overall_coverage_pct: number
zones_fully_funded: number
zones_partially_funded: number
zones_unfunded: number
stretch_analysis: Record<string, any>
}
export interface CalibrateResponse {
zones: CalibrateZoneResult[]
summary: CalibrateSummary
allocation: CalibrateAllocation
thresholds: CalibrateParams
}
// ββ Query hooks ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
import {
ZONES_RESPONSE,
INDICES_RESPONSE,
TRIGGERS_RESPONSE,
BASIS_RISK_RESPONSE,
NOTIFICATIONS_RESPONSE,
DISBURSEMENTS_RESPONSE,
PIPELINE_RUNS_RESPONSE,
PIPELINE_STATS_RESPONSE,
ENROLLED_RESPONSE,
buildCoverage,
} from './mockData'
const STALE_5MIN = 5 * 60 * 1000
export function useZones() {
return useQuery<ZonesResponse>({
queryKey: ['zones'],
queryFn: () => fetchJson<ZonesResponse>('/api/zones'),
staleTime: STALE_5MIN,
})
}
export function useIndices() {
return useQuery<IndicesResponse>({
queryKey: ['indices'],
queryFn: () => fetchJson<IndicesResponse>('/api/indices'),
staleTime: STALE_5MIN,
})
}
export function useTriggers() {
return useQuery<TriggersResponse>({
queryKey: ['triggers'],
queryFn: () => fetchJson<TriggersResponse>('/api/triggers'),
staleTime: STALE_5MIN,
})
}
export function useBasisRisk() {
return useQuery<BasisRiskResponse>({
queryKey: ['basis-risk'],
queryFn: () => fetchJson<BasisRiskResponse>('/api/basis-risk'),
staleTime: STALE_5MIN,
})
}
export function useNotifications() {
return useQuery<NotificationsResponse>({
queryKey: ['notifications'],
queryFn: () => fetchJson<NotificationsResponse>('/api/notifications'),
staleTime: STALE_5MIN,
})
}
export function useDisbursements() {
return useQuery<DisbursementsResponse>({
queryKey: ['disbursements'],
queryFn: () => fetchJson<DisbursementsResponse>('/api/disbursements'),
staleTime: STALE_5MIN,
})
}
export function usePipelineRuns() {
return useQuery<PipelineRunsResponse>({
queryKey: ['pipeline-runs'],
queryFn: () => fetchJson<PipelineRunsResponse>('/api/pipeline/runs'),
staleTime: STALE_5MIN,
})
}
export function usePipelineStats() {
return useQuery<PipelineStats>({
queryKey: ['pipeline-stats'],
queryFn: () => fetchJson<PipelineStats>('/api/pipeline/stats'),
staleTime: STALE_5MIN,
})
}
export function useEnrolled() {
return useQuery<EnrolledResponse>({
queryKey: ['enrolled'],
queryFn: () => fetchJson<EnrolledResponse>('/api/enrolled-workers'),
staleTime: STALE_5MIN,
})
}
// ββ Coverage recommendation ββ
export interface CoverageZone {
zone_id: string
zone_name: string
city: string
settlement_type: string
heat_vulnerability: string
urgency: string
current_temp_c: number
current_wbgt_c: number
trigger_probability_7d: number
triggers_this_week: boolean
risk_level: string
outdoor_exposure_pct: number
enrolled_workers: number
// Per-worker this week
weekly_cost_per_worker: number
alert_payout: number
insurance_payout: number
total_payout_this_week: number
worker_contribution: number
philanthropy_share: number
insurer_premium: number
neural_model: boolean
learned_frequency: number | null
payout_factor: number | null
// Annual estimates
annual_cost_per_worker: number
annual_worker_share: number
annual_philanthropy_share: number
annual_insurer_share: number
annual_trigger_days: number
}
export interface CoverageRecommendation {
weekly_cost_per_worker: number
weekly_cost_total: number
annual_cost_total: number
annual_cost_per_worker: number
total_workers: number
zones_triggering: number
zones_total: number
model_type: string
scenario: string
payout_threshold_wbgt: number
}
export interface CoverageCostSummary {
total_to_workers_pct: number
total_admin_pct: number
total_basis_risk_pct: number
worker_contribution_pct: number
philanthropy_pct: number
insurer_pct: number
}
export interface CoverageResponse {
recommendation: CoverageRecommendation
zones: CoverageZone[]
cost_summary: CoverageCostSummary
}
export function useCoverageRecommendation(payoutUsd: number = 10, filters: string = 'all|all') {
const [gender, settlement] = filters.split('|')
return useQuery<CoverageResponse>({
queryKey: ['coverage-recommendation', payoutUsd, gender, settlement],
queryFn: () =>
fetchJson<CoverageResponse>(
`/api/coverage-recommendation?payout_usd=${payoutUsd}&gender=${gender || 'all'}&settlement=${settlement || 'all'}`
),
staleTime: STALE_5MIN,
})
}
// ββ Pilot worker personas (DPI-style) βββββββββββββββββββββββββββββββββββ
export interface Worker {
worker_id: string
name: string
name_swahili: string | null
nida_id: string | null
phone: string
zone_id: string
zone_name: string
settlement_type: string
occupation: string
age: number | null
years_outdoor: number | null
household_size: number | null
mobile_money: string | null
tasaf_enrolled: boolean
enrolled_at: string
}
export interface WorkersResponse {
workers: Worker[]
total: number
}
export function useWorkers() {
return useQuery<WorkersResponse>({
queryKey: ['workers'],
queryFn: () => fetchJson<WorkersResponse>('/api/workers'),
staleTime: STALE_5MIN,
})
}
|