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
166 lines
5.5 KiB
TypeScript
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;
|