← back to Norma

app/api/globe/countries/route.ts

162 lines

/**
 * GET /api/globe/countries
 * Returns all country profiles with activity scores for globe dot placement.
 *
 * Response:
 *   { countries: CountryRow[] }
 *
 * CountryRow:
 *   code, name, region, lat, lng, value, article_count, org_count,
 *   sentiment_avg, todays_headlines
 */

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { COUNTRY_CENTROIDS } from '@/components/globe/globe-utils';

export async function GET(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  try {
    // Try to query country_profiles table if it exists
    let rows: Array<Record<string, unknown>> = [];

    try {
      const result = await query(`
        SELECT
          cp.country_code,
          cp.country_name,
          cp.region,
          cp.latitude,
          cp.longitude,
          cp.article_count,
          cp.org_count,
          cp.sentiment_avg,
          COALESCE(
            (SELECT COUNT(*)::int
             FROM newspaper_frontpages np
             WHERE np.country = cp.country_code
               AND np.scraped_date = CURRENT_DATE),
            0
          ) AS todays_headlines
        FROM country_profiles cp
        ORDER BY cp.article_count DESC
        LIMIT 200
      `);
      rows = result.rows;
    } catch {
      // country_profiles table may not exist yet — build from other tables
    }

    // If we got rows from country_profiles, return them enriched
    if (rows.length > 0) {
      const countries = rows.map(r => {
        const code = String(r.country_code || '');
        const centroid = COUNTRY_CENTROIDS[code] || [0, 0];
        const lat = typeof r.latitude === 'number' ? r.latitude : centroid[0];
        const lng = typeof r.longitude === 'number' ? r.longitude : centroid[1];

        return {
          code,
          name: String(r.country_name || code),
          region: r.region ? String(r.region) : null,
          lat,
          lng,
          article_count: Number(r.article_count) || 0,
          org_count: Number(r.org_count) || 0,
          sentiment_avg: r.sentiment_avg != null ? Number(r.sentiment_avg) : null,
          todays_headlines: Number(r.todays_headlines) || 0,
          value: (Number(r.article_count) || 0) + (Number(r.org_count) || 0),
        };
      });

      return NextResponse.json({ countries });
    }

    // Fallback: build from organizations table country field + statements
    const orgResult = await query(`
      SELECT
        UPPER(TRIM(o.country)) AS country_code,
        COUNT(DISTINCT o.id)::int AS org_count
      FROM organizations o
      WHERE o.country IS NOT NULL AND o.country != ''
      GROUP BY UPPER(TRIM(o.country))
      HAVING COUNT(DISTINCT o.id) > 0
      ORDER BY org_count DESC
      LIMIT 100
    `).catch(() => ({ rows: [] as Array<Record<string, unknown>> }));

    // Build country list from orgs + centroids
    const countryMap = new Map<string, {
      code: string;
      org_count: number;
      article_count: number;
      sentiment_avg: number | null;
    }>();

    for (const r of orgResult.rows) {
      const code = String(r.country_code || '');
      if (!code || !COUNTRY_CENTROIDS[code]) continue;
      countryMap.set(code, {
        code,
        org_count: Number(r.org_count) || 0,
        article_count: 0,
        sentiment_avg: null,
      });
    }

    // Merge statement country data if available
    await query(`
      SELECT
        UPPER(TRIM(country)) AS country_code,
        COUNT(*)::int AS statement_count,
        AVG(sentiment_score)::float AS sentiment_avg
      FROM nonprofit_statements
      WHERE country IS NOT NULL AND country != ''
      GROUP BY UPPER(TRIM(country))
    `).then(res => {
      for (const r of res.rows) {
        const code = String(r.country_code || '');
        if (!code) continue;
        const existing = countryMap.get(code);
        if (existing) {
          existing.article_count = Number(r.statement_count) || 0;
          existing.sentiment_avg = r.sentiment_avg != null ? Number(r.sentiment_avg) : null;
        } else if (COUNTRY_CENTROIDS[code]) {
          countryMap.set(code, {
            code,
            org_count: 0,
            article_count: Number(r.statement_count) || 0,
            sentiment_avg: r.sentiment_avg != null ? Number(r.sentiment_avg) : null,
          });
        }
      }
    }).catch(() => { /* statements table may not exist */ });

    // Build response from centroid map — always include all known centroids
    // with at least a stub entry so the globe always has data
    const countries = Object.entries(COUNTRY_CENTROIDS).map(([code, [lat, lng]]) => {
      const data = countryMap.get(code);
      return {
        code,
        name: code, // will be enriched by name lookup if available
        region: null,
        lat,
        lng,
        article_count: data?.article_count || 0,
        org_count: data?.org_count || 0,
        sentiment_avg: data?.sentiment_avg ?? null,
        todays_headlines: 0,
        value: (data?.article_count || 0) + (data?.org_count || 0),
      };
    }).filter(c => c.value > 0 || c.code === 'US');

    return NextResponse.json({ countries });
  } catch (err) {
    console.error('[api/globe/countries] error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to fetch country data' }, { status: 500 });
  }
}