Initial commit
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
ssww23
2026-03-10 00:55:37 +03:00
parent fc0f28d830
commit 93a655235a
155 changed files with 24768 additions and 0 deletions

View 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