Cara Balas WA Otomatis Multi-Admin

Tutorial auto reply WhatsApp dengan multi admin. Team inbox, assignment, collaboration. Satu nomor banyak admin!

Cara Balas WA Otomatis Multi-Admin
Cara Balas WA Otomatis Multi-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
✅ Scalable

Architecture

📐 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 inactive

DON'T ❌

- Overlapping responses
- No handover notes
- Manual everything
- Skip metrics
- Leave unassigned too long
- No accountability

FAQ

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 AdminMulti-Admin
BottleneckDistributed
Slow responseFast response
No coverage24/7 coverage
Not scalableScalable

Setup Multi-Admin →


Artikel Terkait