changemaker.lite/api/src/modules/sms/conversations/sms-conversations.routes.ts
bunker-admin aaba7df97d Add new conversation feature to SMS module
Enable starting ad-hoc SMS conversations from the conversations page
by searching contacts across SMS lists, CRM, and existing threads,
then composing and sending a first message.

Bunker Admin
2026-02-28 16:55:24 -07:00

166 lines
5.5 KiB
TypeScript

import { Router } from 'express';
import { authenticate } from '../../../middleware/auth.middleware';
import { requireRole } from '../../../middleware/rbac.middleware';
import { smsConversationsService } from './sms-conversations.service';
const router = Router();
router.use(authenticate, requireRole('SUPER_ADMIN', 'INFLUENCE_ADMIN'));
// GET /api/sms/conversations — list conversations
router.get('/', async (req, res, next) => {
try {
const page = Math.max(1, Number(req.query.page) || 1);
const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 50));
const result = await smsConversationsService.findAll({
page,
limit,
search: req.query.search as string | undefined,
status: req.query.status as string | undefined,
campaignId: req.query.campaignId as string | undefined,
unreadOnly: req.query.unreadOnly === 'true',
});
res.json(result);
} catch (err) { next(err); }
});
// GET /api/sms/conversations/contact-search — search contacts for new conversation
router.get('/contact-search', async (req, res, next) => {
try {
const q = (req.query.q as string || '').trim();
if (q.length < 2) {
res.json({ results: [] });
return;
}
const results = await smsConversationsService.searchContacts(q);
res.json({ results });
} catch (err) { next(err); }
});
// POST /api/sms/conversations — start new conversation
router.post('/', async (req, res, next) => {
try {
const { phone, message, contactName, contactId } = req.body as {
phone?: string;
message?: string;
contactName?: string;
contactId?: string;
};
if (!phone || typeof phone !== 'string' || phone.replace(/\D/g, '').length < 7) {
res.status(400).json({ error: 'Valid phone number is required (min 7 digits)' });
return;
}
if (!message || typeof message !== 'string' || message.trim().length === 0) {
res.status(400).json({ error: 'Message is required' });
return;
}
if (message.length > 1600) {
res.status(400).json({ error: 'Message cannot exceed 1600 characters' });
return;
}
const conversation = await smsConversationsService.startConversation({
phone,
message: message.trim(),
contactName,
contactId,
});
res.status(201).json(conversation);
} catch (err: any) {
if (err.statusCode === 409) {
res.status(409).json({ error: err.message });
return;
}
if (err.statusCode === 400) {
res.status(400).json({ error: err.message });
return;
}
next(err);
}
});
// GET /api/sms/conversations/stats — conversation stats
router.get('/stats', async (_req, res, next) => {
try {
const stats = await smsConversationsService.getStats();
res.json(stats);
} catch (err) { next(err); }
});
// GET /api/sms/conversations/:id — single conversation with messages
router.get('/:id', async (req, res, next) => {
try {
const conversation = await smsConversationsService.findById(req.params.id as string);
if (!conversation) { res.status(404).json({ error: 'Conversation not found' }); return; }
res.json(conversation);
} catch (err) { next(err); }
});
// POST /api/sms/conversations/:id/read — mark conversation as read
router.post('/:id/read', async (req, res, next) => {
try {
await smsConversationsService.markRead(req.params.id as string);
res.json({ success: true });
} catch (err) { next(err); }
});
// PUT /api/sms/conversations/:id/notes — update notes
router.put('/:id/notes', async (req, res, next) => {
try {
const { notes } = req.body as { notes: string };
const conversation = await smsConversationsService.updateNotes(req.params.id as string, notes || '');
res.json(conversation);
} catch (err) { next(err); }
});
// PUT /api/sms/conversations/:id/tags — update tags
router.put('/:id/tags', async (req, res, next) => {
try {
const { tags } = req.body as { tags: string[] };
const conversation = await smsConversationsService.updateTags(req.params.id as string, tags || []);
res.json(conversation);
} catch (err) { next(err); }
});
// POST /api/sms/conversations/:id/reply — send reply
router.post('/:id/reply', async (req, res, next) => {
try {
const { message } = req.body as { message: string };
if (!message || typeof message !== 'string') {
res.status(400).json({ error: 'Message is required' });
return;
}
const smsMessage = await smsConversationsService.reply(req.params.id as string, message);
res.json(smsMessage);
} catch (err) { next(err); }
});
// --- Bulk Actions ---
// POST /api/sms/conversations/bulk-read — mark multiple conversations as read
router.post('/bulk-read', async (req, res, next) => {
try {
const { ids } = req.body as { ids: string[] };
if (!Array.isArray(ids) || ids.length === 0) {
res.status(400).json({ error: 'ids array is required' });
return;
}
const result = await smsConversationsService.bulkMarkRead(ids);
res.json(result);
} catch (err) { next(err); }
});
// POST /api/sms/conversations/bulk-close — close multiple conversations
router.post('/bulk-close', async (req, res, next) => {
try {
const { ids } = req.body as { ids: string[] };
if (!Array.isArray(ids) || ids.length === 0) {
res.status(400).json({ error: 'ids array is required' });
return;
}
const result = await smsConversationsService.bulkClose(ids);
res.json(result);
} catch (err) { next(err); }
});
export const smsConversationsRouter = router;