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(path: string): Promise { const res = await fetch(`${BASE_URL}${path}`) if (!res.ok) throw new Error(`API error: ${res.status} ${res.statusText}`) return res.json() as Promise } function mock(value: T, delayMs = 180): Promise { // 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 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 } 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 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 } 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({ queryKey: ['zones'], queryFn: () => fetchJson('/api/zones'), staleTime: STALE_5MIN, }) } export function useIndices() { return useQuery({ queryKey: ['indices'], queryFn: () => fetchJson('/api/indices'), staleTime: STALE_5MIN, }) } export function useTriggers() { return useQuery({ queryKey: ['triggers'], queryFn: () => fetchJson('/api/triggers'), staleTime: STALE_5MIN, }) } export function useBasisRisk() { return useQuery({ queryKey: ['basis-risk'], queryFn: () => fetchJson('/api/basis-risk'), staleTime: STALE_5MIN, }) } export function useNotifications() { return useQuery({ queryKey: ['notifications'], queryFn: () => fetchJson('/api/notifications'), staleTime: STALE_5MIN, }) } export function useDisbursements() { return useQuery({ queryKey: ['disbursements'], queryFn: () => fetchJson('/api/disbursements'), staleTime: STALE_5MIN, }) } export function usePipelineRuns() { return useQuery({ queryKey: ['pipeline-runs'], queryFn: () => fetchJson('/api/pipeline/runs'), staleTime: STALE_5MIN, }) } export function usePipelineStats() { return useQuery({ queryKey: ['pipeline-stats'], queryFn: () => fetchJson('/api/pipeline/stats'), staleTime: STALE_5MIN, }) } export function useEnrolled() { return useQuery({ queryKey: ['enrolled'], queryFn: () => fetchJson('/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({ queryKey: ['coverage-recommendation', payoutUsd, gender, settlement], queryFn: () => fetchJson( `/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({ queryKey: ['workers'], queryFn: () => fetchJson('/api/workers'), staleTime: STALE_5MIN, }) }