← back to Norma

app/api/drafts/[id]/route.ts

192 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';

type RouteContext = { params: Promise<{ id: string }> };

/**
 * GET /api/drafts/[id]
 * Return a single draft with its version count.
 */
export async function GET(request: NextRequest, context: RouteContext) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
  if (!orgId) {
    return NextResponse.json({ error: 'Missing org context' }, { status: 400 });
  }

  const { id } = await context.params;

  try {
    const draftResult = await query(`SELECT * FROM drafts WHERE id = $1 AND org_id = $2`, [id, orgId]);

    if (draftResult.rowCount === 0) {
      return NextResponse.json({ error: 'Draft not found' }, { status: 404 });
    }

    const versionCountResult = await query(
      `SELECT COUNT(*)::int AS version_count FROM draft_versions WHERE draft_id = $1`,
      [id]
    );

    return NextResponse.json({
      draft: {
        ...draftResult.rows[0],
        version_count: versionCountResult.rows[0].version_count,
      },
    });
  } catch (err) {
    console.error('[api/drafts/[id]] GET error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to fetch draft' }, { status: 500 });
  }
}

/**
 * PATCH /api/drafts/[id]
 * Update draft fields. Auto-increments current_version and creates a draft_version entry.
 */
export async function PATCH(request: NextRequest, context: RouteContext) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
  if (!orgId) {
    return NextResponse.json({ error: 'Missing org context' }, { status: 400 });
  }

  const { id } = await context.params;
  const client = await getClient();

  try {
    const body = await request.json();
    const allowedFields = [
      'subject', 'body_html', 'body_text', 'status', 'voice_mode',
      'email_type', 'from_name', 'from_email', 'reply_to',
    ];

    const setClauses: string[] = [];
    const values: unknown[] = [];
    let paramIndex = 1;

    for (const field of allowedFields) {
      if (field in body) {
        setClauses.push(`${field} = $${paramIndex}`);
        values.push(body[field]);
        paramIndex++;
      }
    }

    if (setClauses.length === 0) {
      return NextResponse.json({ error: 'No valid fields to update' }, { status: 400 });
    }

    await client.query('BEGIN');

    // Fetch current draft for version snapshot
    const currentDraft = await client.query(
      `SELECT * FROM drafts WHERE id = $1 AND org_id = $2`,
      [id, orgId]
    );

    if (currentDraft.rowCount === 0) {
      await client.query('ROLLBACK');
      return NextResponse.json({ error: 'Draft not found' }, { status: 404 });
    }

    const draft = currentDraft.rows[0];
    const newVersion = draft.current_version + 1;

    // Create version snapshot of the current state before updating
    await client.query(
      `INSERT INTO draft_versions (draft_id, version_number, subject, body_html, body_text, change_summary, created_by)
       VALUES ($1, $2, $3, $4, $5, $6, $7)`,
      [
        id,
        draft.current_version,
        draft.subject,
        draft.body_html,
        draft.body_text || '',
        body.change_summary || 'Auto-saved on edit', auth.username,
      ]
    );

    // Add version increment to the update
    setClauses.push(`current_version = $${paramIndex}`);
    values.push(newVersion);
    paramIndex++;

    // Execute the update
    values.push(id);
    paramIndex++;
    values.push(orgId);
    const result = await client.query(
      `UPDATE drafts SET ${setClauses.join(', ')} WHERE id = $${paramIndex - 1} AND org_id = $${paramIndex} RETURNING *`,
      values
    );

    await client.query('COMMIT');

    const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined;
    await auditLog(
      'draft.updated',
      'draft',
      id,
      { fields: Object.keys(body).filter((k) => allowedFields.includes(k)), version: newVersion },
      ip
    );

    return NextResponse.json({ draft: result.rows[0] });
  } catch (err) {
    await client.query('ROLLBACK');
    console.error('[api/drafts/[id]] PATCH error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to update draft' }, { status: 500 });
  } finally {
    client.release();
  }
}

/**
 * DELETE /api/drafts/[id]
 * Delete a draft (versions cascade via ON DELETE CASCADE).
 */
export async function DELETE(request: NextRequest, context: RouteContext) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
  if (!orgId) {
    return NextResponse.json({ error: 'Missing org context' }, { status: 400 });
  }

  const { id } = await context.params;

  try {
    const result = await query(
      `DELETE FROM drafts WHERE id = $1 AND org_id = $2 RETURNING id, subject, lane`,
      [id, orgId]
    );

    if (result.rowCount === 0) {
      return NextResponse.json({ error: 'Draft not found' }, { status: 404 });
    }

    const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined;
    await auditLog(
      'draft.deleted',
      'draft',
      id,
      { subject: result.rows[0].subject, lane: result.rows[0].lane },
      ip
    );

    return NextResponse.json({ success: true, deleted: result.rows[0] });
  } catch (err) {
    console.error('[api/drafts/[id]] DELETE error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to delete draft' }, { status: 500 });
  }
}