← back to Norma

app/api/onboard/route.ts

102 lines

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';

// ─── GET /api/onboard ─────────────────────────────────────────────────────────
// Query params: status=discovered|contacted|onboarded|rejected, page=1, limit=20, sort=match_score

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

  try {
    const { searchParams } = new URL(request.url);
    const status = searchParams.get('status') ?? '';
    const page = Math.max(1, parseInt(searchParams.get('page') ?? '1', 10) || 1);
    const limit = Math.min(100, Math.max(1, parseInt(searchParams.get('limit') ?? '20', 10) || 20));
    const offset = (page - 1) * limit;

    const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
    const VALID_STATUSES = ['discovered', 'contacted', 'onboarded', 'rejected'];
    const useStatusFilter = status && VALID_STATUSES.includes(status);

    const conditions: string[] = ['TRUE'];
    const params: unknown[] = [];
    let pIdx = 1;

    if (orgId) {
      conditions.push(`od.org_id = $${pIdx}`);
      params.push(orgId);
      pIdx++;
    }
    if (useStatusFilter) {
      conditions.push(`od.status = $${pIdx}`);
      params.push(status);
      pIdx++;
    }

    const whereClause = `WHERE ${conditions.join(' AND ')}`;
    const limitIdx = pIdx;
    const offsetIdx = pIdx + 1;
    params.push(limit, offset);

    const { rows } = await query<{
      id: string;
      org_id: string | null;
      org_name: string;
      trigger_label: string | null;
      match_score: number | null;
      status: string;
      ntee_code: string | null;
      state: string | null;
      recommended_actions: unknown;
      notes: string | null;
      created_at: string;
      updated_at: string | null;
    }>(
      `SELECT
         od.id,
         od.org_id,
         COALESCE(o.name, 'Unknown Org') AS org_name,
         od.trigger_label,
         od.match_score,
         od.status,
         o.ntee_code,
         o.state,
         od.recommended_actions,
         od.notes,
         od.created_at,
         od.updated_at
       FROM onboard_discoveries od
       LEFT JOIN organizations o ON o.id = od.org_id
       ${whereClause}
       ORDER BY od.match_score DESC NULLS LAST, od.created_at DESC
       LIMIT $${limitIdx} OFFSET $${offsetIdx}`,
      params,
    );

    const countParams: unknown[] = [];
    const countConditions: string[] = ['TRUE'];
    if (orgId) { countConditions.push(`od.org_id = $${countParams.length + 1}`); countParams.push(orgId); }
    if (useStatusFilter) { countConditions.push(`od.status = $${countParams.length + 1}`); countParams.push(status); }
    const { rows: countRows } = await query<{ count: string }>(
      `SELECT COUNT(*) AS count
       FROM onboard_discoveries od
       WHERE ${countConditions.join(' AND ')}`,
      countParams,
    );

    return NextResponse.json({
      items: rows,
      total: parseInt(countRows[0]?.count ?? '0', 10),
      page,
      limit,
      status_filter: status || null,
    });
  } catch (err: unknown) {
    console.error('[api/onboard] Error:', (err as Error).message);
    return NextResponse.json({ error: (err as Error).message }, { status: 500 });
  }
}