Cara Balas WA Otomatis Multi-Admin
Tutorial auto reply WhatsApp dengan multi admin. Team inbox, assignment, collaboration. Satu nomor banyak admin!
Multi-admin = Tim kerja bareng tanpa bentrok!
Dengan setup multi-admin, satu nomor WhatsApp bisa dihandle oleh banyak admin secara bersamaan dengan sistem assignment yang teratur.
Kenapa Multi-Admin?
📊 MASALAH SINGLE ADMIN:
❌ Satu orang overwhelmed
❌ Tidak bisa cuti/sakit
❌ Response time lambat
❌ No collaboration
❌ Tidak scalable
✅ DENGAN MULTI-ADMIN:
✅ Workload terdistribusi
✅ Coverage 24/7
✅ Response cepat
✅ Team collaboration
✅ ScalableArchitecture
📐 MULTI-ADMIN SETUP:
┌──────────────┐
WhatsApp ───────►│ WA Bot │
│ Server │
└──────┬───────┘
│
┌──────▼───────┐
│ Dashboard │
│ Server │
└──────┬───────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ Admin 1 │ │ Admin 2 │ │ Admin 3 │
└─────────┘ └─────────┘ └─────────┘Team Inbox Setup
Conversation Model:
javascript
const conversationSchema = {
id: String,
customerId: String, // Customer phone
customerName: String,
status: String, // 'unassigned', 'assigned', 'resolved'
assignedTo: String, // Admin ID
assignedAt: Date,
priority: String, // 'low', 'normal', 'high', 'urgent'
tags: [String],
lastMessageAt: Date,
lastMessagePreview: String,
unreadCount: Number,
createdAt: Date,
resolvedAt: Date,
metadata: Object
};
const messageSchema = {
id: String,
conversationId: String,
direction: String, // 'incoming', 'outgoing'
senderId: String, // Customer phone or Admin ID
content: String,
type: String, // 'text', 'image', 'document', etc
timestamp: Date,
status: String // 'sent', 'delivered', 'read'
};Conversation Management:
javascript
// Get atau create conversation
async function getOrCreateConversation(customerId, customerName) {
let conversation = await db.conversations.findOne({
customerId,
status: { $ne: 'resolved' }
});
if (!conversation) {
conversation = {
id: generateId(),
customerId,
customerName,
status: 'unassigned',
priority: 'normal',
tags: [],
lastMessageAt: new Date(),
unreadCount: 0,
createdAt: new Date()
};
await db.conversations.insertOne(conversation);
}
return conversation;
}
// Handle incoming message
async function handleIncomingMessage(from, message, customerName) {
const conversation = await getOrCreateConversation(from, customerName);
// Save message
await db.messages.insertOne({
id: generateId(),
conversationId: conversation.id,
direction: 'incoming',
senderId: from,
content: message.text,
type: message.type,
timestamp: new Date(),
status: 'received'
});
// Update conversation
await db.conversations.updateOne(
{ id: conversation.id },
{
$set: {
lastMessageAt: new Date(),
lastMessagePreview: message.text?.substring(0, 50)
},
$inc: { unreadCount: 1 }
}
);
// Notify assigned admin or broadcast to unassigned queue
if (conversation.assignedTo) {
await notifyAdmin(conversation.assignedTo, conversation, message);
} else {
await broadcastToAvailableAdmins(conversation, message);
}
// Auto-reply jika tidak ada admin available
if (await shouldAutoReply(conversation)) {
await sendAutoReply(from, conversation);
}
}Assignment System
Auto Assignment:
javascript
async function autoAssignConversation(conversation) {
// Get available admins
const availableAdmins = await db.admins.find({
status: 'online',
activeConversations: { $lt: 10 } // Max 10 concurrent chats
}).toArray();
if (availableAdmins.length === 0) {
return null;
}
// Round-robin or least-busy assignment
const admin = availableAdmins.reduce((prev, curr) =>
prev.activeConversations < curr.activeConversations ? prev : curr
);
// Assign
await db.conversations.updateOne(
{ id: conversation.id },
{
$set: {
status: 'assigned',
assignedTo: admin.id,
assignedAt: new Date()
}
}
);
// Update admin's active count
await db.admins.updateOne(
{ id: admin.id },
{ $inc: { activeConversations: 1 } }
);
// Notify admin
await notifyAdmin(admin.id, conversation, 'Conversation assigned to you');
return admin;
}Manual Assignment:
javascript
// API: Assign conversation to admin
app.post('/api/conversations/:id/assign', authMiddleware, async (req, res) => {
const { id } = req.params;
const { adminId } = req.body;
const conversation = await db.conversations.findOne({ id });
if (!conversation) {
return res.status(404).json({ error: 'Conversation not found' });
}
// Release dari admin sebelumnya
if (conversation.assignedTo) {
await db.admins.updateOne(
{ id: conversation.assignedTo },
{ $inc: { activeConversations: -1 } }
);
}
// Assign ke admin baru
await db.conversations.updateOne(
{ id },
{
$set: {
status: 'assigned',
assignedTo: adminId,
assignedAt: new Date()
}
}
);
await db.admins.updateOne(
{ id: adminId },
{ $inc: { activeConversations: 1 } }
);
// Notify
await notifyAdmin(adminId, conversation, 'Conversation assigned to you');
res.json({ success: true });
});
// Transfer ke admin lain
app.post('/api/conversations/:id/transfer', authMiddleware, async (req, res) => {
const { id } = req.params;
const { toAdminId, note } = req.body;
const fromAdminId = req.user.id;
// Log transfer
await db.conversationLogs.insertOne({
conversationId: id,
action: 'transfer',
from: fromAdminId,
to: toAdminId,
note,
timestamp: new Date()
});
// Update assignment
await reassignConversation(id, fromAdminId, toAdminId);
res.json({ success: true });
});Real-time Dashboard
WebSocket for Live Updates:
javascript
const { Server } = require('socket.io');
const io = new Server(server);
// Admin connects
io.on('connection', (socket) => {
const adminId = socket.handshake.auth.adminId;
// Join admin's room
socket.join(`admin:${adminId}`);
socket.join('all-admins');
console.log(`Admin ${adminId} connected`);
// Set online status
db.admins.updateOne(
{ id: adminId },
{ $set: { status: 'online', lastSeen: new Date() } }
);
// Handle disconnect
socket.on('disconnect', () => {
db.admins.updateOne(
{ id: adminId },
{ $set: { status: 'offline', lastSeen: new Date() } }
);
});
});
// Notify admin of new message
async function notifyAdmin(adminId, conversation, message) {
io.to(`admin:${adminId}`).emit('new-message', {
conversationId: conversation.id,
message
});
}
// Broadcast unassigned to all admins
async function broadcastToAvailableAdmins(conversation, message) {
io.to('all-admins').emit('new-unassigned', {
conversation,
message
});
}Dashboard Views:
javascript
// Get inbox untuk admin
app.get('/api/inbox', authMiddleware, async (req, res) => {
const adminId = req.user.id;
const { filter } = req.query;
let query = {};
switch (filter) {
case 'mine':
query = { assignedTo: adminId, status: 'assigned' };
break;
case 'unassigned':
query = { status: 'unassigned' };
break;
case 'all':
query = { status: { $ne: 'resolved' } };
break;
}
const conversations = await db.conversations
.find(query)
.sort({ lastMessageAt: -1 })
.limit(50)
.toArray();
res.json(conversations);
});
// Get conversation messages
app.get('/api/conversations/:id/messages', authMiddleware, async (req, res) => {
const { id } = req.params;
const { before, limit = 50 } = req.query;
let query = { conversationId: id };
if (before) {
query.timestamp = { $lt: new Date(before) };
}
const messages = await db.messages
.find(query)
.sort({ timestamp: -1 })
.limit(parseInt(limit))
.toArray();
// Mark as read
await db.conversations.updateOne(
{ id },
{ $set: { unreadCount: 0 } }
);
res.json(messages.reverse());
});Admin Sends Message
javascript
// Admin kirim pesan via dashboard
app.post('/api/conversations/:id/send', authMiddleware, async (req, res) => {
const { id } = req.params;
const { message, type = 'text' } = req.body;
const adminId = req.user.id;
const conversation = await db.conversations.findOne({ id });
if (!conversation) {
return res.status(404).json({ error: 'Conversation not found' });
}
// Auto-assign jika belum assigned
if (!conversation.assignedTo) {
await db.conversations.updateOne(
{ id },
{
$set: {
status: 'assigned',
assignedTo: adminId,
assignedAt: new Date()
}
}
);
}
// Send via WhatsApp
const result = await sendWhatsApp(conversation.customerId, message);
// Save message
const savedMessage = {
id: generateId(),
conversationId: id,
direction: 'outgoing',
senderId: adminId,
content: message,
type,
timestamp: new Date(),
status: 'sent',
waMessageId: result.messageId
};
await db.messages.insertOne(savedMessage);
// Update conversation
await db.conversations.updateOne(
{ id },
{
$set: {
lastMessageAt: new Date(),
lastMessagePreview: message.substring(0, 50)
}
}
);
// Broadcast to other admins viewing this conversation
io.to(`conversation:${id}`).emit('new-message', savedMessage);
res.json({ success: true, message: savedMessage });
});Quick Responses
javascript
// Saved quick responses
const quickResponses = [
{
id: 'greeting',
shortcut: '/hi',
title: 'Greeting',
content: 'Hai! Terima kasih sudah menghubungi [BRAND]. Ada yang bisa kami bantu?'
},
{
id: 'transfer',
shortcut: '/transfer',
title: 'Transfer Info',
content: 'Untuk pembayaran, bisa transfer ke:\nBCA: 1234567890\na.n. PT [BRAND]'
},
{
id: 'thanks',
shortcut: '/thanks',
title: 'Thank You',
content: 'Terima kasih sudah belanja di [BRAND]! 💕'
}
];
// Get quick responses
app.get('/api/quick-responses', authMiddleware, async (req, res) => {
const teamResponses = await db.quickResponses.find({
$or: [
{ scope: 'team' },
{ scope: 'personal', createdBy: req.user.id }
]
}).toArray();
res.json(teamResponses);
});Internal Notes
javascript
// Add internal note to conversation
app.post('/api/conversations/:id/notes', authMiddleware, async (req, res) => {
const { id } = req.params;
const { content } = req.body;
const note = {
id: generateId(),
conversationId: id,
type: 'internal_note',
content,
createdBy: req.user.id,
createdByName: req.user.name,
timestamp: new Date()
};
await db.conversationNotes.insertOne(note);
// Broadcast to team
io.to(`conversation:${id}`).emit('new-note', note);
res.json({ success: true, note });
});Performance Tracking
javascript
// Admin performance metrics
async function getAdminMetrics(adminId, startDate, endDate) {
const conversations = await db.conversations.find({
assignedTo: adminId,
createdAt: { $gte: startDate, $lte: endDate }
}).toArray();
const messages = await db.messages.find({
senderId: adminId,
timestamp: { $gte: startDate, $lte: endDate }
}).toArray();
// Calculate metrics
const resolved = conversations.filter(c => c.status === 'resolved').length;
const avgResponseTime = calculateAvgResponseTime(conversations);
return {
totalConversations: conversations.length,
resolved,
resolutionRate: (resolved / conversations.length * 100).toFixed(1) + '%',
messagesSent: messages.length,
avgResponseTime: `${avgResponseTime} menit`,
satisfaction: await getAdminCSAT(adminId, startDate, endDate)
};
}Best Practices
DO ✅
- Clear assignment rules
- Real-time sync
- Internal notes for handover
- Quick responses untuk efisiensi
- Track performance metrics
- Auto-reassign inactiveDON'T ❌
- Overlapping responses
- No handover notes
- Manual everything
- Skip metrics
- Leave unassigned too long
- No accountabilityFAQ
Berapa admin ideal per nomor WA?
Tergantung volume. Rule of thumb: 1 admin per 50-100 daily chats.
Bagaimana kalau 2 admin reply bersamaan?
Lock system - saat typing, admin lain bisa lihat. Atau assign strict.
Kesimpulan
Multi-admin = Scalable customer service!
| Single Admin | Multi-Admin |
|---|---|
| Bottleneck | Distributed |
| Slow response | Fast response |
| No coverage | 24/7 coverage |
| Not scalable | Scalable |