File Manager Lite
Dir:
/home/codewavebd/public_html/resources/views/admin/chats
Upload
[..]
index.blade.php (12.94 KB)
Edit
Rename
Del
Edit: index.blade.php
@extends('layouts.admin') @section('title', 'Live Chat Console') @section('page_title', 'Live Support Center') @section('content') <div class="row g-4" style="min-height: 600px;"> <!-- Thread List Column (Left) --> <div class="col-md-4 d-flex flex-column"> <div class="admin-card flex-fill d-flex flex-column p-0 overflow-hidden" style="max-height: 600px;"> <div class="p-3 border-bottom border-secondary bg-dark"> <h6 class="text-info font-heading mb-0"><i class="bi bi-chat-left-text me-2"></i>Active Conversations</h6> </div> <div class="list-group list-group-flush overflow-y-auto flex-fill" id="adminThreadsList" style="max-height: 530px;"> <div class="text-center py-5 text-muted">Loading conversations...</div> </div> </div> </div> <!-- Active Chat Box Column (Right) --> <div class="col-md-8 d-flex flex-column"> <div class="admin-card flex-fill d-flex flex-column p-0 overflow-hidden" style="max-height: 600px; min-height: 600px;"> <!-- Blank State --> <div class="d-flex flex-column align-items-center justify-content-center flex-fill text-center py-5 my-auto" id="chatBlankState"> <i class="bi bi-chat-dots text-secondary" style="font-size: 4rem;"></i> <h5 class="text-white font-heading mt-3">Select a Conversation</h5> <p class="text-muted small px-4">Choose a chat thread from the left menu to start messaging the visitor.</p> </div> <!-- Active Conversation Interface (Hidden by default) --> <div class="d-none flex-column flex-fill" id="activeChatInterface" style="height: 100%;"> <!-- Header --> <div class="p-3 border-bottom border-secondary bg-dark d-flex align-items-center justify-content-between"> <div> <h6 class="text-white mb-0" id="chatHeaderName">Visitor Name</h6> <small class="text-muted d-block mt-1"> <span id="chatHeaderPhone" class="me-3"><i class="bi bi-phone"></i> -</span> <span id="chatHeaderEmail"><i class="bi bi-envelope"></i> -</span> </small> </div> <div class="d-flex align-items-center gap-2"> <span class="badge" id="chatHeaderStatusBadge">Open</span> <button class="btn btn-sm btn-outline-danger" id="closeThreadBtn" title="Close Chat Session"> <i class="bi bi-check-lg me-1"></i> Close Thread </button> </div> </div> <!-- Chat Messages Body --> <div class="flex-fill p-4 overflow-y-auto d-flex flex-column gap-3" id="adminMessagesContainer" style="background-color: rgba(0, 0, 0, 0.2); max-height: 400px; min-height: 400px;"> <!-- Message bubbles populate here --> </div> <!-- Footer Input Area --> <div class="p-3 border-top border-secondary bg-dark" id="chatReplyFooter"> <div class="input-group"> <input type="text" id="adminMessageInput" class="form-control bg-dark border-secondary text-white py-2" placeholder="Type your reply here..." autocomplete="off"> <button class="btn btn-primary-glow px-4" type="button" id="sendAdminReplyBtn"> <i class="bi bi-send-fill me-1"></i> Send </button> </div> </div> </div> </div> </div> </div> <style> .chat-thread-item { cursor: pointer; transition: background-color 0.2s; border-bottom: 1px solid rgba(255, 255, 255, 0.05) !important; } .chat-thread-item:hover { background-color: rgba(255, 255, 255, 0.03); } .chat-thread-item.active { background-color: rgba(99, 102, 241, 0.1) !important; border-left: 3px solid var(--accent-indigo) !important; } .chat-msg-row { display: flex; width: 100%; } .chat-msg-row.visitor { justify-content: flex-start; } .chat-msg-row.admin { justify-content: flex-end; } .admin-bubble { max-width: 70%; padding: 10px 15px; border-radius: 14px; font-size: 13.5px; line-height: 1.4; word-wrap: break-word; } .chat-msg-row.visitor .admin-bubble { background-color: rgba(255, 255, 255, 0.08); color: #e2e8f0; border: 1px solid rgba(255, 255, 255, 0.05); border-bottom-left-radius: 2px; } .chat-msg-row.admin .admin-bubble { background-color: var(--accent-indigo); color: white; border-bottom-right-radius: 2px; } </style> <script> document.addEventListener('DOMContentLoaded', () => { const threadsList = document.getElementById('adminThreadsList'); const blankState = document.getElementById('chatBlankState'); const activeInterface = document.getElementById('activeChatInterface'); const messagesContainer = document.getElementById('adminMessagesContainer'); // Header elements const headerName = document.getElementById('chatHeaderName'); const headerPhone = document.getElementById('chatHeaderPhone'); const headerEmail = document.getElementById('chatHeaderEmail'); const headerStatusBadge = document.getElementById('chatHeaderStatusBadge'); // Controls const closeThreadBtn = document.getElementById('closeThreadBtn'); const replyFooter = document.getElementById('chatReplyFooter'); const replyInput = document.getElementById('adminMessageInput'); const sendReplyBtn = document.getElementById('sendAdminReplyBtn'); let activeThreadId = null; let threadsPollInterval = null; let messagesPollInterval = null; let lastMessageCount = 0; // Load Threads on Init loadThreads(); threadsPollInterval = setInterval(loadThreads, 4000); // Fetch and render thread list function loadThreads() { fetch("{{ route('admin.chats.threads') }}") .then(res => res.json()) .then(data => { if (data.success) { renderThreads(data.threads); } }); } function renderThreads(threads) { if (threads.length === 0) { threadsList.innerHTML = '<div class="text-center py-5 text-muted small">No active chat sessions.</div>'; return; } const currentActiveId = activeThreadId; threadsList.innerHTML = ''; threads.forEach(thread => { const item = document.createElement('div'); item.className = `list-group-item list-group-item-action bg-transparent p-3 chat-thread-item ${currentActiveId == thread.id ? 'active' : ''}`; item.setAttribute('data-id', thread.id); const lastMsg = thread.messages && thread.messages.length > 0 ? thread.messages[0].message : 'No messages'; const shortLastMsg = lastMsg.length > 35 ? lastMsg.substring(0, 35) + '...' : lastMsg; const badgeClass = thread.status === 'open' ? 'bg-success' : 'bg-secondary'; item.innerHTML = ` <div class="d-flex justify-content-between align-items-start"> <h6 class="text-white mb-1 font-heading small">${thread.name || 'Anonymous Visitor'}</h6> <span class="badge ${badgeClass} font-monospace" style="font-size: 9px;">${thread.status.toUpperCase()}</span> </div> <div class="text-muted small mb-1" style="font-size: 11.5px;">${thread.phone || 'No phone'}</div> <div class="text-secondary small text-truncate" style="font-size: 12px; max-width: 90%;">${shortLastMsg}</div> <div class="text-end text-muted mt-1" style="font-size: 9.5px;">${new Date(thread.updated_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</div> `; item.addEventListener('click', () => selectThread(thread.id)); threadsList.appendChild(item); }); } // Select thread to view chat function selectThread(id) { activeThreadId = id; // Highlight active item document.querySelectorAll('.chat-thread-item').forEach(item => { item.classList.remove('active'); if (item.getAttribute('data-id') == id) { item.classList.add('active'); } }); // Hide blank state and show interface blankState.classList.add('d-none'); activeInterface.classList.remove('d-none'); // Fetch messages for selected thread loadMessages(id); // Setup Polling for messages clearInterval(messagesPollInterval); messagesPollInterval = setInterval(() => loadMessages(id), 3000); } function loadMessages(id) { fetch(`/admin/chats/${id}/messages`) .then(res => res.json()) .then(data => { if (data.success) { const thread = data.thread; const messages = data.messages; // Render headers headerName.innerText = thread.name || 'Anonymous Visitor'; headerPhone.innerHTML = `<i class="bi bi-phone text-info me-1"></i> ${thread.phone || '-'}`; headerEmail.innerHTML = `<i class="bi bi-envelope text-info me-1"></i> ${thread.email || '-'}`; headerStatusBadge.innerText = thread.status.toUpperCase(); if (thread.status === 'open') { headerStatusBadge.className = 'badge bg-success'; replyFooter.classList.remove('d-none'); replyFooter.style.display = 'block'; closeThreadBtn.classList.remove('d-none'); closeThreadBtn.style.display = 'block'; } else { headerStatusBadge.className = 'badge bg-secondary'; replyFooter.classList.add('d-none'); replyFooter.style.display = 'none'; closeThreadBtn.classList.add('d-none'); closeThreadBtn.style.display = 'none'; } renderMessages(messages); } }); } function renderMessages(messages) { messagesContainer.innerHTML = ''; messages.forEach(msg => { const row = document.createElement('div'); row.className = `chat-msg-row ${msg.sender === 'visitor' ? 'visitor' : 'admin'}`; row.innerHTML = ` <div class="admin-bubble"> <div class="bubble-text"></div> <div class="text-muted text-end mt-1" style="font-size: 8.5px;"> ${new Date(msg.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} </div> </div> `; row.querySelector('.bubble-text').innerText = msg.message; messagesContainer.appendChild(row); }); // Autoscroll to bottom messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Send reply message function sendReply() { if (!activeThreadId) return; const msg = replyInput.value.trim(); if (!msg) return; replyInput.value = ''; fetch(`/admin/chats/${activeThreadId}/reply`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }, body: JSON.stringify({ message: msg }) }) .then(res => res.json()) .then(data => { if (data.success) { // Immediately refresh chat loadMessages(activeThreadId); loadThreads(); } }); } sendReplyBtn.addEventListener('click', sendReply); replyInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendReply(); }); // Close Thread Event closeThreadBtn.addEventListener('click', () => { if (!activeThreadId) return; if (confirm("Are you sure you want to close this chat thread?")) { fetch(`/admin/chats/${activeThreadId}/close`, { method: 'POST', headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' } }) .then(res => res.json()) .then(data => { if (data.success) { loadMessages(activeThreadId); loadThreads(); } }); } }); }); </script> @endsection
Simpan