This commit is contained in:
205
resources/views/admin/chats/index.blade.php
Normal file
205
resources/views/admin/chats/index.blade.php
Normal file
@@ -0,0 +1,205 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('meta_title', 'Чаты с клиентами')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Чаты', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Чаты с клиентами</h2>
|
||||
<p>Отвечайте клиентам прямо из админки.</p>
|
||||
</div>
|
||||
|
||||
@if ($conversations->isEmpty())
|
||||
<div class="pc-card">
|
||||
<p>Пока нет сообщений от клиентов.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="pc-admin-chat-layout">
|
||||
<aside class="pc-card pc-admin-chat-list">
|
||||
@foreach ($conversations as $conversation)
|
||||
@php
|
||||
$isActive = $selectedConversation && $selectedConversation->id === $conversation->id;
|
||||
$subtitle = $conversation->user?->email ?: ('Токен: ' . \Illuminate\Support\Str::limit($conversation->visitor_token, 14, '...'));
|
||||
$preview = $conversation->latestMessage?->body ? \Illuminate\Support\Str::limit($conversation->latestMessage->body, 80) : 'Нет сообщений';
|
||||
@endphp
|
||||
<a class="pc-admin-chat-item {{ $isActive ? 'is-active' : '' }}" href="{{ route('admin.chats.index', ['conversation' => $conversation->id]) }}">
|
||||
<div class="pc-admin-chat-item-top">
|
||||
<strong>{{ $conversation->display_name }}</strong>
|
||||
@if ($conversation->unread_count > 0)
|
||||
<span class="pc-admin-chat-badge">{{ $conversation->unread_count }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<span class="pc-muted">{{ $subtitle }}</span>
|
||||
@if ($conversation->status === \App\Models\ChatConversation::STATUS_CLOSED)
|
||||
<span class="pc-muted">Чат закрыт</span>
|
||||
@endif
|
||||
<span class="pc-muted">{{ $preview }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</aside>
|
||||
|
||||
<div class="pc-card pc-admin-chat-panel">
|
||||
@if ($selectedConversation)
|
||||
@php
|
||||
$isClosed = $selectedConversation->status === \App\Models\ChatConversation::STATUS_CLOSED;
|
||||
@endphp
|
||||
<div
|
||||
id="pc-admin-chat"
|
||||
data-fetch-url="{{ route('admin.chats.messages', $selectedConversation) }}"
|
||||
data-send-url="{{ route('admin.chats.messages.store', $selectedConversation) }}"
|
||||
data-csrf="{{ csrf_token() }}"
|
||||
>
|
||||
<div class="pc-admin-chat-head">
|
||||
<div>
|
||||
<h3>{{ $selectedConversation->display_name }}</h3>
|
||||
<p class="pc-muted">{{ $selectedConversation->user?->email ?: 'Гость' }}</p>
|
||||
<p class="pc-muted">{{ $isClosed ? 'Чат закрыт' : 'Чат открыт' }}</p>
|
||||
</div>
|
||||
<div class="pc-admin-chat-actions">
|
||||
<form method="post" action="{{ route('admin.chats.status', $selectedConversation) }}">
|
||||
@csrf
|
||||
@method('patch')
|
||||
<input type="hidden" name="status" value="{{ $isClosed ? \App\Models\ChatConversation::STATUS_OPEN : \App\Models\ChatConversation::STATUS_CLOSED }}">
|
||||
<button class="pc-btn ghost" type="submit">{{ $isClosed ? 'Открыть чат' : 'Закрыть чат' }}</button>
|
||||
</form>
|
||||
<form method="post" action="{{ route('admin.chats.destroy', $selectedConversation) }}" onsubmit="return confirm('Удалить чат и все сообщения?')">
|
||||
@csrf
|
||||
@method('delete')
|
||||
<button class="pc-btn ghost" type="submit">Удалить чат</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pc-admin-chat-messages" id="pc-admin-chat-messages"></div>
|
||||
|
||||
<form class="pc-admin-chat-form" id="pc-admin-chat-form">
|
||||
<textarea name="message" rows="3" maxlength="2000" placeholder="{{ $isClosed ? 'Чат закрыт. Сначала откройте его.' : 'Введите ответ клиенту...' }}" required @disabled($isClosed)></textarea>
|
||||
<button class="pc-btn primary" type="submit" @disabled($isClosed)>{{ $isClosed ? 'Чат закрыт' : 'Отправить' }}</button>
|
||||
</form>
|
||||
</div>
|
||||
@else
|
||||
<p>Выберите чат слева.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pc-pagination">
|
||||
{{ $conversations->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
@endsection
|
||||
|
||||
@if ($selectedConversation)
|
||||
@push('scripts')
|
||||
<script>
|
||||
(() => {
|
||||
const root = document.getElementById('pc-admin-chat');
|
||||
const list = document.getElementById('pc-admin-chat-messages');
|
||||
const form = document.getElementById('pc-admin-chat-form');
|
||||
if (!root || !list || !form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchUrl = root.dataset.fetchUrl;
|
||||
const sendUrl = root.dataset.sendUrl;
|
||||
const csrf = root.dataset.csrf;
|
||||
const initialMessages = @json($initialMessages);
|
||||
|
||||
const renderMessages = (messages) => {
|
||||
if (!Array.isArray(messages)) {
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = '';
|
||||
messages.forEach((message) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = `pc-admin-chat-message ${message.sender === 'admin' ? 'is-admin' : 'is-customer'}`;
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'pc-admin-chat-bubble';
|
||||
bubble.textContent = message.body || '';
|
||||
|
||||
const meta = document.createElement('span');
|
||||
meta.className = 'pc-muted';
|
||||
meta.textContent = message.time || '';
|
||||
|
||||
item.appendChild(bubble);
|
||||
item.appendChild(meta);
|
||||
list.appendChild(item);
|
||||
});
|
||||
|
||||
list.scrollTop = list.scrollHeight;
|
||||
};
|
||||
|
||||
const fetchMessages = async () => {
|
||||
try {
|
||||
const response = await fetch(fetchUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
renderMessages(payload.messages || []);
|
||||
} catch (error) {
|
||||
// Ignore temporary connection issues and continue polling.
|
||||
}
|
||||
};
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const textarea = form.querySelector('textarea[name="message"]');
|
||||
const button = form.querySelector('button[type="submit"]');
|
||||
if (!textarea || !button) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = textarea.value.trim();
|
||||
if (message === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
button.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(sendUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': csrf,
|
||||
},
|
||||
body: JSON.stringify({ message }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
textarea.value = '';
|
||||
await fetchMessages();
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
renderMessages(initialMessages);
|
||||
fetchMessages();
|
||||
window.setInterval(fetchMessages, 4000);
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
@endif
|
||||
Reference in New Issue
Block a user