287 lines
9.1 KiB
TypeScript
287 lines
9.1 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { getDatabase, Notification } from '@/lib/db';
|
|
import { requireRole } from '@/lib/auth';
|
|
|
|
/**
|
|
* GET /api/notifications - Get notifications for a specific recipient
|
|
* Query params: recipient, unread_only, type, limit, offset
|
|
*/
|
|
export async function GET(request: NextRequest) {
|
|
const auth = requireRole(request, 'viewer')
|
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
|
|
|
try {
|
|
const db = getDatabase();
|
|
const { searchParams } = new URL(request.url);
|
|
|
|
// Parse query parameters
|
|
const recipient = searchParams.get('recipient');
|
|
const unread_only = searchParams.get('unread_only') === 'true';
|
|
const type = searchParams.get('type');
|
|
const limit = parseInt(searchParams.get('limit') || '50');
|
|
const offset = parseInt(searchParams.get('offset') || '0');
|
|
|
|
if (!recipient) {
|
|
return NextResponse.json({ error: 'Recipient is required' }, { status: 400 });
|
|
}
|
|
|
|
// Build dynamic query
|
|
let query = 'SELECT * FROM notifications WHERE recipient = ?';
|
|
const params: any[] = [recipient];
|
|
|
|
if (unread_only) {
|
|
query += ' AND read_at IS NULL';
|
|
}
|
|
|
|
if (type) {
|
|
query += ' AND type = ?';
|
|
params.push(type);
|
|
}
|
|
|
|
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
|
params.push(limit, offset);
|
|
|
|
const stmt = db.prepare(query);
|
|
const notifications = stmt.all(...params) as Notification[];
|
|
|
|
// Prepare source detail statements once (avoids N+1)
|
|
const taskDetailStmt = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?');
|
|
const commentDetailStmt = db.prepare(`
|
|
SELECT c.id, c.content, c.task_id, t.title as task_title
|
|
FROM comments c
|
|
LEFT JOIN tasks t ON c.task_id = t.id
|
|
WHERE c.id = ?
|
|
`);
|
|
const agentDetailStmt = db.prepare('SELECT id, name, role, status FROM agents WHERE id = ?');
|
|
|
|
// Enhance notifications with related entity data
|
|
const enhancedNotifications = notifications.map(notification => {
|
|
let sourceDetails = null;
|
|
|
|
try {
|
|
if (notification.source_type && notification.source_id) {
|
|
switch (notification.source_type) {
|
|
case 'task': {
|
|
const task = taskDetailStmt.get(notification.source_id) as any;
|
|
if (task) {
|
|
sourceDetails = { type: 'task', ...task };
|
|
}
|
|
break;
|
|
}
|
|
case 'comment': {
|
|
const comment = commentDetailStmt.get(notification.source_id) as any;
|
|
if (comment) {
|
|
sourceDetails = {
|
|
type: 'comment',
|
|
...comment,
|
|
content_preview: comment.content?.substring(0, 100) || ''
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
case 'agent': {
|
|
const agent = agentDetailStmt.get(notification.source_id) as any;
|
|
if (agent) {
|
|
sourceDetails = { type: 'agent', ...agent };
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Failed to fetch source details for notification ${notification.id}:`, error);
|
|
}
|
|
|
|
return {
|
|
...notification,
|
|
source: sourceDetails
|
|
};
|
|
});
|
|
|
|
// Get unread count for this recipient
|
|
const unreadCount = db.prepare(`
|
|
SELECT COUNT(*) as count
|
|
FROM notifications
|
|
WHERE recipient = ? AND read_at IS NULL
|
|
`).get(recipient) as { count: number };
|
|
|
|
// Get total count for pagination
|
|
let countQuery = 'SELECT COUNT(*) as total FROM notifications WHERE recipient = ?';
|
|
const countParams: any[] = [recipient];
|
|
if (unread_only) {
|
|
countQuery += ' AND read_at IS NULL';
|
|
}
|
|
if (type) {
|
|
countQuery += ' AND type = ?';
|
|
countParams.push(type);
|
|
}
|
|
const countRow = db.prepare(countQuery).get(...countParams) as { total: number };
|
|
|
|
return NextResponse.json({
|
|
notifications: enhancedNotifications,
|
|
total: countRow.total,
|
|
page: Math.floor(offset / limit) + 1,
|
|
limit,
|
|
unreadCount: unreadCount.count
|
|
});
|
|
} catch (error) {
|
|
console.error('GET /api/notifications error:', error);
|
|
return NextResponse.json({ error: 'Failed to fetch notifications' }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PUT /api/notifications - Mark notifications as read
|
|
* Body: { ids: number[] } or { recipient: string } (mark all as read)
|
|
*/
|
|
export async function PUT(request: NextRequest) {
|
|
const auth = requireRole(request, 'operator');
|
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
|
|
|
try {
|
|
const db = getDatabase();
|
|
const body = await request.json();
|
|
const { ids, recipient, markAllRead } = body;
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
if (markAllRead && recipient) {
|
|
// Mark all notifications as read for this recipient
|
|
const stmt = db.prepare(`
|
|
UPDATE notifications
|
|
SET read_at = ?
|
|
WHERE recipient = ? AND read_at IS NULL
|
|
`);
|
|
|
|
const result = stmt.run(now, recipient);
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
markedAsRead: result.changes
|
|
});
|
|
} else if (ids && Array.isArray(ids)) {
|
|
// Mark specific notifications as read
|
|
const placeholders = ids.map(() => '?').join(',');
|
|
const stmt = db.prepare(`
|
|
UPDATE notifications
|
|
SET read_at = ?
|
|
WHERE id IN (${placeholders}) AND read_at IS NULL
|
|
`);
|
|
|
|
const result = stmt.run(now, ...ids);
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
markedAsRead: result.changes
|
|
});
|
|
} else {
|
|
return NextResponse.json({
|
|
error: 'Either provide ids array or recipient with markAllRead=true'
|
|
}, { status: 400 });
|
|
}
|
|
} catch (error) {
|
|
console.error('PUT /api/notifications error:', error);
|
|
return NextResponse.json({ error: 'Failed to update notifications' }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DELETE /api/notifications - Delete notifications
|
|
* Body: { ids: number[] } or { recipient: string, olderThan: number }
|
|
*/
|
|
export async function DELETE(request: NextRequest) {
|
|
const auth = requireRole(request, 'admin');
|
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
|
|
|
try {
|
|
const db = getDatabase();
|
|
const body = await request.json();
|
|
const { ids, recipient, olderThan } = body;
|
|
|
|
if (ids && Array.isArray(ids)) {
|
|
// Delete specific notifications
|
|
const placeholders = ids.map(() => '?').join(',');
|
|
const stmt = db.prepare(`
|
|
DELETE FROM notifications
|
|
WHERE id IN (${placeholders})
|
|
`);
|
|
|
|
const result = stmt.run(...ids);
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
deleted: result.changes
|
|
});
|
|
} else if (recipient && olderThan) {
|
|
// Delete old notifications for recipient
|
|
const stmt = db.prepare(`
|
|
DELETE FROM notifications
|
|
WHERE recipient = ? AND created_at < ?
|
|
`);
|
|
|
|
const result = stmt.run(recipient, olderThan);
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
deleted: result.changes
|
|
});
|
|
} else {
|
|
return NextResponse.json({
|
|
error: 'Either provide ids array or recipient with olderThan timestamp'
|
|
}, { status: 400 });
|
|
}
|
|
} catch (error) {
|
|
console.error('DELETE /api/notifications error:', error);
|
|
return NextResponse.json({ error: 'Failed to delete notifications' }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/notifications/mark-delivered - Mark notifications as delivered to agent
|
|
* Body: { agent: string }
|
|
*/
|
|
export async function POST(request: NextRequest) {
|
|
const auth = requireRole(request, 'operator');
|
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
|
|
|
try {
|
|
const db = getDatabase();
|
|
const body = await request.json();
|
|
const { agent, action } = body;
|
|
|
|
if (action === 'mark-delivered') {
|
|
if (!agent) {
|
|
return NextResponse.json({ error: 'Agent name is required' }, { status: 400 });
|
|
}
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
// Mark undelivered notifications as delivered
|
|
const stmt = db.prepare(`
|
|
UPDATE notifications
|
|
SET delivered_at = ?
|
|
WHERE recipient = ? AND delivered_at IS NULL
|
|
`);
|
|
|
|
const result = stmt.run(now, agent);
|
|
|
|
// Get the notifications that were just marked as delivered
|
|
const deliveredNotifications = db.prepare(`
|
|
SELECT * FROM notifications
|
|
WHERE recipient = ? AND delivered_at = ?
|
|
ORDER BY created_at DESC
|
|
`).all(agent, now) as Notification[];
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
delivered: result.changes,
|
|
notifications: deliveredNotifications
|
|
});
|
|
} else {
|
|
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
|
}
|
|
} catch (error) {
|
|
console.error('POST /api/notifications error:', error);
|
|
return NextResponse.json({ error: 'Failed to process notification action' }, { status: 500 });
|
|
}
|
|
} |