← back to Norma

app/api/sessions/route.ts

153 lines

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

/**
 * GET /api/sessions
 * List sessions ordered by session_date DESC, with nested drafts.
 * Supports pagination: ?limit=50&offset=0
 */
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 orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;

    let limit = parseInt(searchParams.get('limit') || '50', 10);
    if (isNaN(limit) || limit < 1) limit = 50;
    if (limit > 200) limit = 200;

    let offset = parseInt(searchParams.get('offset') || '0', 10);
    if (isNaN(offset) || offset < 0) offset = 0;

    const orgWhere = orgId ? 'WHERE s.org_id = $3' : '';
    const orgCountWhere = orgId ? 'WHERE org_id = $1' : '';
    const countParams = orgId ? [orgId] : [];

    const countResult = await query(
      `SELECT COUNT(*)::int AS total FROM sessions ${orgCountWhere}`,
      countParams
    );
    const total = countResult.rows[0]?.total ?? 0;

    const queryParams: unknown[] = [limit, offset];
    if (orgId) queryParams.push(orgId);

    const sessionsResult = await query(
      `SELECT s.*,
              COALESCE(
                json_agg(
                  json_build_object(
                    'id', d.id,
                    'lane', d.lane,
                    'lane_label', d.lane_label,
                    'subject', d.subject,
                    'body_html', COALESCE(d.body_html, ''),
                    'body_text', COALESCE(d.body_text, ''),
                    'email_type', d.email_type,
                    'voice_mode', d.voice_mode,
                    'status', d.status,
                    'current_version', d.current_version,
                    'created_at', d.created_at,
                    'updated_at', d.updated_at
                  ) ORDER BY d.lane
                ) FILTER (WHERE d.id IS NOT NULL),
                '[]'::json
              ) AS drafts
       FROM sessions s
       LEFT JOIN drafts d ON d.session_id = s.id
       ${orgWhere}
       GROUP BY s.id
       ORDER BY s.session_date DESC
       LIMIT $1 OFFSET $2`,
      queryParams
    );

    return NextResponse.json({ sessions: sessionsResult.rows, total, limit, offset });
  } catch (err) {
    console.error('[api/sessions] GET error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to fetch sessions' }, { status: 500 });
  }
}

/**
 * POST /api/sessions
 * Create a new "Morning Batch" session with 3 empty lane drafts (A/B/C).
 */
export async function POST(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
  const client = await getClient();

  try {
    await client.query('BEGIN');

    // Create the session
    const today = new Date().toISOString().split('T')[0];
    const title = `Morning Batch - ${new Date().toLocaleDateString('en-US', {
      weekday: 'long',
      year: 'numeric',
      month: 'long',
      day: 'numeric',
    })}`;

    const sessionResult = await client.query(
      `INSERT INTO sessions (title, session_date, status, created_by, org_id)
       VALUES ($1, $2, 'draft', $3, $4)
       RETURNING *`,
      [title, today, auth.username, orgId || null]
    );

    const session = sessionResult.rows[0];

    // Create 3 lane drafts
    const lanes = [
      { lane: 'A', lane_label: 'Action', email_type: 'action' },
      { lane: 'B', lane_label: 'Policy', email_type: 'policy' },
      { lane: 'C', lane_label: 'Resources', email_type: 'resources' },
    ];

    const drafts = [];
    for (const l of lanes) {
      const draftResult = await client.query(
        `INSERT INTO drafts (session_id, lane, lane_label, subject, body_html, body_text, email_type, status, created_by, org_id)
         VALUES ($1, $2, $3, $4, $5, $6, $7, 'draft', $8, $9)
         RETURNING *`,
        [
          session.id,
          l.lane,
          l.lane_label,
          `[${l.lane_label}] `,
          '',
          '',
          l.email_type, auth.username,
          orgId || null,
        ]
      );
      drafts.push(draftResult.rows[0]);
    }

    await client.query('COMMIT');

    const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined;
    await auditLog('session.created', 'session', session.id, { title, lanes: lanes.map((l) => l.lane) }, ip);

    return NextResponse.json(
      { session: { ...session, drafts } },
      { status: 201 }
    );
  } catch (err) {
    await client.query('ROLLBACK');
    console.error('[api/sessions] POST error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to create session' }, { status: 500 });
  } finally {
    client.release();
  }
}