← back to Norma

app/api/clickup/route.ts

114 lines

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

const CLICKUP_BASE = 'https://api.clickup.com/api/v2';

function getApiKey(): string | null {
  return process.env.CLICKUP_API_KEY || null;
}

async function clickupFetch(path: string, options?: RequestInit) {
  const apiKey = getApiKey();
  if (!apiKey) throw new Error('CLICKUP_API_KEY not configured');
  const res = await fetch(`${CLICKUP_BASE}${path}`, {
    ...options,
    headers: {
      Authorization: apiKey,
      'Content-Type': 'application/json',
      ...options?.headers,
    },
  });
  if (!res.ok) throw new Error(`ClickUp API ${res.status}: ${await res.text()}`);
  return res.json();
}

/**
 * GET /api/clickup?action=spaces|tasks|lists
 * Fetch ClickUp data.
 */
export async function GET(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const { searchParams } = new URL(request.url);
  const action = searchParams.get('action') || 'spaces';

  try {
    if (action === 'teams') {
      const data = await clickupFetch('/team');
      return NextResponse.json({ teams: data.teams || [] });
    }

    if (action === 'spaces') {
      const teamData = await clickupFetch('/team');
      const teamId = teamData.teams?.[0]?.id;
      if (!teamId) return NextResponse.json({ spaces: [] });
      const data = await clickupFetch(`/team/${teamId}/space`);
      return NextResponse.json({ spaces: data.spaces || [] });
    }

    if (action === 'lists') {
      const spaceId = searchParams.get('space_id');
      if (!spaceId) return NextResponse.json({ error: 'space_id required' }, { status: 400 });
      const data = await clickupFetch(`/space/${spaceId}/list`);
      return NextResponse.json({ lists: data.lists || [] });
    }

    if (action === 'tasks') {
      const listId = searchParams.get('list_id');
      if (!listId) return NextResponse.json({ error: 'list_id required' }, { status: 400 });
      const data = await clickupFetch(`/list/${listId}/task?limit=50`);
      return NextResponse.json({ tasks: data.tasks || [] });
    }

    return NextResponse.json({ error: `Unknown action: ${action}` }, { status: 400 });
  } catch (err) {
    return NextResponse.json({ error: (err as Error).message }, { status: 500 });
  }
}

/**
 * POST /api/clickup
 * Create a ClickUp task from Norma (petition, grant, campaign).
 */
export async function POST(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const body = await request.json();
  const { list_id, name, description, priority, due_date, tags, assignees, source_type, source_id } = body;

  if (!list_id || !name) {
    return NextResponse.json({ error: 'list_id and name required' }, { status: 400 });
  }

  try {
    const taskPayload: Record<string, unknown> = {
      name,
      description: description || '',
      priority: priority || 3,
      tags: tags || [],
    };
    if (due_date) taskPayload.due_date = new Date(due_date).getTime();
    if (assignees) taskPayload.assignees = assignees;

    const task = await clickupFetch(`/list/${list_id}/task`, {
      method: 'POST',
      body: JSON.stringify(taskPayload),
    });

    // Log the integration in DB
    await query(
      `INSERT INTO connected_services_log (service_id, action, source_type, source_id, external_id, metadata)
       VALUES ('clickup', 'create_task', $1, $2, $3, $4)
       ON CONFLICT DO NOTHING`,
      [source_type || 'manual', source_id || null, task.id, JSON.stringify({ name, list_id })]
    ).catch(() => {}); // table may not exist yet, that's ok

    return NextResponse.json({ task });
  } catch (err) {
    return NextResponse.json({ error: (err as Error).message }, { status: 500 });
  }
}