← back to Norma
app/api/email-sends/route.ts
145 lines
/**
* GET /api/email-sends — List email campaigns, optionally filtered by type/status
* POST /api/email-sends — Create a new email campaign record
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
import { query } from '@/lib/db';
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
const url = new URL(request.url);
const emailType = url.searchParams.get('type');
const status = url.searchParams.get('status');
const limit = Math.min(parseInt(url.searchParams.get('limit') || '50', 10), 200);
try {
const conditions: string[] = [];
const params: unknown[] = [];
let paramIdx = 1;
if (orgId) {
conditions.push(`org_id = $${paramIdx++}`);
params.push(orgId);
}
if (emailType) {
conditions.push(`email_type = $${paramIdx++}`);
params.push(emailType);
}
if (status) {
conditions.push(`status = $${paramIdx++}`);
params.push(status);
}
const where = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
// Get campaigns
const campaignsRes = await query(
`SELECT id, email_type, subject, body_html, body_text, target_audience,
status, tone, sent_at, sent_count, open_count, click_count,
ai_generated, target_name, target_email, created_at, updated_at
FROM email_campaigns
${where}
ORDER BY created_at DESC
LIMIT $${paramIdx}`,
[...params, limit],
);
// Get counts by type
const typeConditions: string[] = [];
const typeParams: unknown[] = [];
let typeIdx = 1;
if (orgId) {
typeConditions.push(`org_id = $${typeIdx++}`);
typeParams.push(orgId);
}
const typeWhere = typeConditions.length > 0 ? 'WHERE ' + typeConditions.join(' AND ') : '';
const countsRes = await query(
`SELECT email_type, COUNT(*)::int as count
FROM email_campaigns
${typeWhere}
GROUP BY email_type`,
typeParams,
);
const typeCounts: Record<string, number> = {};
for (const row of countsRes.rows) {
typeCounts[row.email_type || 'general'] = row.count;
}
return NextResponse.json({
campaigns: campaignsRes.rows,
typeCounts,
total: campaignsRes.rows.length,
});
} catch (err) {
console.error('[api/email-sends] GET error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to load email campaigns' }, { status: 500 });
}
}
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;
try {
const body = await request.json();
const {
email_type = 'general',
subject,
body_html,
body_text,
target_audience = [],
status = 'draft',
tone = 'formal',
target_name,
target_email,
} = body;
if (!subject || typeof subject !== 'string' || !subject.trim()) {
return NextResponse.json({ error: 'subject is required' }, { status: 400 });
}
if (!body_html || typeof body_html !== 'string') {
return NextResponse.json({ error: 'body_html is required' }, { status: 400 });
}
const res = await query(
`INSERT INTO email_campaigns
(email_type, subject, body_html, body_text, target_audience, status, tone,
target_name, target_email, org_id, ai_generated, sent_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING *`,
[
email_type,
subject.trim(),
body_html,
body_text || '',
target_audience,
status,
tone,
target_name || null,
target_email || null,
orgId || null,
true,
status === 'sent' ? new Date().toISOString() : null,
],
);
return NextResponse.json({ campaign: res.rows[0] }, { status: 201 });
} catch (err) {
console.error('[api/email-sends] POST error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to create email campaign' }, { status: 500 });
}
}