chat support
This commit is contained in:
@@ -1,12 +1,141 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Dashboard')
|
||||
@section('page-title', 'Chat Support')
|
||||
|
||||
@section('content')
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h4>Welcome to the Admin chat</h4>
|
||||
<p>Here you can manage all system modules.</p>
|
||||
</div>
|
||||
|
||||
<div class="container py-4">
|
||||
|
||||
<h2 class="mb-4 fw-bold">Customer Support Chat</h2>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
|
||||
@if($tickets->count() === 0)
|
||||
<div class="p-4 text-center text-muted">
|
||||
<h5>No customer chats yet.</h5>
|
||||
</div>
|
||||
@else
|
||||
<ul class="list-group list-group-flush">
|
||||
|
||||
@foreach($tickets as $ticket)
|
||||
@php
|
||||
// Get last message
|
||||
$lastMsg = $ticket->messages()->latest()->first();
|
||||
@endphp
|
||||
|
||||
<li class="list-group-item py-3">
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
|
||||
<!-- LEFT -->
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
|
||||
<!-- Avatar -->
|
||||
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center"
|
||||
style="width: 45px; height: 45px; font-size: 18px;">
|
||||
{{ strtoupper(substr($ticket->user->customer_name ?? $ticket->user->name, 0, 1)) }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- Name + unread badge -->
|
||||
<h6 class="mb-1 fw-semibold">
|
||||
{{ $ticket->user->customer_name ?? $ticket->user->name }}
|
||||
|
||||
<span
|
||||
id="badge-{{ $ticket->id }}"
|
||||
class="badge bg-danger ms-2 {{ $ticket->unread_count == 0 ? 'd-none' : '' }}">
|
||||
{{ $ticket->unread_count }}
|
||||
</span>
|
||||
</h6>
|
||||
|
||||
<!-- Last message -->
|
||||
<small class="text-muted">
|
||||
@if($lastMsg)
|
||||
@if($lastMsg->message)
|
||||
{{ Str::limit($lastMsg->message, 35) }}
|
||||
@elseif(Str::startsWith($lastMsg->file_type, 'image'))
|
||||
📷 Image
|
||||
@else
|
||||
📎 Attachment
|
||||
@endif
|
||||
@else
|
||||
<i>No messages yet</i>
|
||||
@endif
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- RIGHT -->
|
||||
<div class="text-end">
|
||||
<span class="badge {{ $ticket->status === 'open' ? 'bg-success' : 'bg-danger' }}">
|
||||
{{ ucfirst($ticket->status) }}
|
||||
</span>
|
||||
|
||||
<a href="{{ route('admin.chat.open', $ticket->id) }}"
|
||||
class="btn btn-sm btn-primary ms-2">
|
||||
Open Chat →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</li>
|
||||
|
||||
@endforeach
|
||||
|
||||
</ul>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@section('scripts')
|
||||
<script>
|
||||
// -------------------------------
|
||||
// WAIT FOR ECHO READY (DEFINE IT)
|
||||
// -------------------------------
|
||||
function waitForEcho(callback, retries = 40) {
|
||||
if (window.Echo) {
|
||||
console.log('%c[ECHO] Ready (Admin List)', 'color: green; font-weight: bold;');
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (retries <= 0) {
|
||||
console.error('[ECHO] Failed to initialize');
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => waitForEcho(callback, retries - 1), 200);
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// LISTEN FOR REALTIME MESSAGES
|
||||
// -------------------------------
|
||||
waitForEcho(() => {
|
||||
console.log('[ADMIN LIST] Listening for new messages...');
|
||||
|
||||
window.Echo.private('admin.chat')
|
||||
.listen('.NewChatMessage', (event) => {
|
||||
|
||||
// only USER → ADMIN messages
|
||||
if (event.sender_type !== 'App\\Models\\User') return;
|
||||
|
||||
const badge = document.getElementById(`badge-${event.ticket_id}`);
|
||||
if (!badge) return;
|
||||
|
||||
let count = parseInt(badge.innerText || 0);
|
||||
badge.innerText = count + 1;
|
||||
badge.classList.remove('d-none');
|
||||
|
||||
console.log('[ADMIN LIST] Badge updated for ticket', event.ticket_id);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
|
||||
|
||||
@endsection
|
||||
|
||||
217
resources/views/admin/chat_window.blade.php
Normal file
217
resources/views/admin/chat_window.blade.php
Normal file
@@ -0,0 +1,217 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Chat With ' . ($ticket->user->customer_name ?? $ticket->user->name))
|
||||
|
||||
@section('content')
|
||||
|
||||
<style>
|
||||
.chat-box {
|
||||
height: 70vh;
|
||||
overflow-y: auto;
|
||||
background: #f5f6fa;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
.message {
|
||||
max-width: 65%;
|
||||
padding: 10px 14px;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.message.admin {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.message.user {
|
||||
background: #ffffff;
|
||||
border: 1px solid #ddd;
|
||||
margin-right: auto;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.chat-input {
|
||||
position: fixed;
|
||||
bottom: 15px;
|
||||
left: 250px;
|
||||
right: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container py-4">
|
||||
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h4 class="fw-bold mb-0">
|
||||
Chat With: {{ $ticket->user->customer_name ?? $ticket->user->name }}
|
||||
</h4>
|
||||
<span class="badge ms-3 {{ $ticket->status === 'open' ? 'bg-success' : 'bg-danger' }}">
|
||||
{{ ucfirst($ticket->status) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="chatBox" class="chat-box border shadow-sm">
|
||||
|
||||
@foreach($messages as $msg)
|
||||
<div class="message {{ $msg->sender_type === 'App\\Models\\Admin' ? 'admin' : 'user' }}">
|
||||
|
||||
{{-- TEXT --}}
|
||||
@if($msg->message)
|
||||
<div>{{ $msg->message }}</div>
|
||||
@endif
|
||||
|
||||
{{-- FILE --}}
|
||||
@if($msg->file_path)
|
||||
<div class="mt-2">
|
||||
@php $isImage = Str::startsWith($msg->file_type, 'image'); @endphp
|
||||
|
||||
@if($isImage)
|
||||
<img src="{{ asset('storage/'.$msg->file_path) }}" style="max-width:150px;" class="rounded">
|
||||
@else
|
||||
<a href="{{ asset('storage/'.$msg->file_path) }}" target="_blank">📎 View Attachment</a>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<small class="text-muted d-block mt-1">
|
||||
{{ $msg->created_at->format('d M h:i A') }}
|
||||
</small>
|
||||
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
|
||||
<div class="chat-input">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body d-flex align-items-center gap-2">
|
||||
<input type="text" id="messageInput" class="form-control" placeholder="Type your message...">
|
||||
<input type="file" id="fileInput" class="form-control" style="max-width:200px;">
|
||||
<button class="btn btn-primary" id="sendBtn">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
|
||||
@section('scripts')
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
// ✅ Make current admin ID available to JS
|
||||
const CURRENT_ADMIN_ID = {{ auth('admin')->id() }};
|
||||
|
||||
console.log("CHAT WINDOW: script loaded");
|
||||
|
||||
// -------------------------------
|
||||
// WAIT FOR ECHO READY
|
||||
// -------------------------------
|
||||
function waitForEcho(callback, retries = 40) {
|
||||
if (window.Echo) {
|
||||
console.log("%c[ECHO] Ready!", "color: green; font-weight: bold;", window.Echo);
|
||||
return callback();
|
||||
}
|
||||
|
||||
console.warn("[ECHO] Not ready. Retrying...");
|
||||
if (retries <= 0) {
|
||||
console.error("[ECHO] FAILED to initialize after retry limit");
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => waitForEcho(callback, retries - 1), 200);
|
||||
}
|
||||
|
||||
// Scroll chat down
|
||||
function scrollToBottom() {
|
||||
const el = document.getElementById("chatBox");
|
||||
if (el) el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
scrollToBottom();
|
||||
|
||||
// -------------------------------
|
||||
// SEND MESSAGE (WORKING PART FROM SCRIPT #1)
|
||||
// -------------------------------
|
||||
document.getElementById("sendBtn").addEventListener("click", function () {
|
||||
console.log("[SEND] Attempting to send message...");
|
||||
|
||||
let msg = document.getElementById("messageInput").value;
|
||||
let file = document.getElementById("fileInput").files[0];
|
||||
|
||||
if (!msg.trim() && !file) {
|
||||
alert("Please type something or upload a file.");
|
||||
return;
|
||||
}
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append("message", msg);
|
||||
if (file) formData.append("file", file);
|
||||
|
||||
fetch("{{ route('admin.chat.send', $ticket->id) }}", {
|
||||
method: "POST",
|
||||
headers: { "X-CSRF-TOKEN": "{{ csrf_token() }}" },
|
||||
body: formData
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then((response) => {
|
||||
console.log("[SEND] Message sent:", response);
|
||||
document.getElementById("messageInput").value = "";
|
||||
document.getElementById("fileInput").value = "";
|
||||
})
|
||||
.catch(err => console.error("[SEND] Error:", err));
|
||||
});
|
||||
|
||||
// -------------------------------
|
||||
// LISTEN FOR REALTIME MESSAGE (WORKING PART FROM SCRIPT #2)
|
||||
// ----------------------------
|
||||
|
||||
waitForEcho(() => {
|
||||
const ticketId = "{{ $ticket->id }}";
|
||||
|
||||
console.log("[ECHO] Subscribing to PRIVATE channel:", `ticket.${ticketId}`);
|
||||
|
||||
window.Echo.private(`ticket.${ticketId}`)
|
||||
.listen(".NewChatMessage", (event) => {
|
||||
|
||||
console.log("%c[REALTIME RECEIVED]", "color: blue; font-weight: bold;", event);
|
||||
|
||||
const msg = event; // ✅ flat payload
|
||||
|
||||
// ✅ CHECK IF THIS MESSAGE IS SENT BY CURRENT ADMIN
|
||||
const isMine =
|
||||
msg.sender_type === 'App\\Models\\Admin' &&
|
||||
msg.sender_id === CURRENT_ADMIN_ID;
|
||||
|
||||
let html = `
|
||||
<div class="message ${isMine ? 'admin' : 'user'}">
|
||||
${msg.message ?? ''}
|
||||
`;
|
||||
|
||||
if (msg.file_url) {
|
||||
if (msg.file_type?.startsWith("image")) {
|
||||
html += `<img src="${msg.file_url}" class="rounded mt-2" style="max-width:150px;">`;
|
||||
} else {
|
||||
html += `<a href="${msg.file_url}" target="_blank" class="mt-2 d-block">📎 View File</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += `
|
||||
<small class="text-muted d-block mt-1">Just now</small>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document
|
||||
.getElementById("chatBox")
|
||||
.insertAdjacentHTML("beforeend", html);
|
||||
|
||||
scrollToBottom();
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@@ -1,7 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Admin Panel</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet" />
|
||||
@@ -220,6 +222,7 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="sidebar">
|
||||
@@ -346,6 +349,8 @@
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
@vite(['resources/js/app.js'])
|
||||
@yield('scripts') <!-- ⭐ REQUIRED FOR CHAT TO WORK -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const headerToggle = document.getElementById('headerToggle');
|
||||
|
||||
Reference in New Issue
Block a user