This commit is contained in:
178
app/Http/Controllers/Admin/ChatController.php
Normal file
178
app/Http/Controllers/Admin/ChatController.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ChatConversation;
|
||||
use App\Models\ChatMessage;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ChatController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$conversations = ChatConversation::query()
|
||||
->with('user', 'latestMessage')
|
||||
->withCount([
|
||||
'messages as unread_count' => fn ($query) => $query
|
||||
->where('sender', 'customer')
|
||||
->where('is_read', false),
|
||||
])
|
||||
->orderByDesc('last_message_at')
|
||||
->orderByDesc('id')
|
||||
->paginate(30);
|
||||
|
||||
$selectedConversation = $this->resolveSelectedConversation($request, $conversations);
|
||||
$initialMessages = collect();
|
||||
|
||||
if ($selectedConversation) {
|
||||
$this->markCustomerMessagesAsRead($selectedConversation);
|
||||
$initialMessages = $this->conversationMessages($selectedConversation);
|
||||
}
|
||||
|
||||
return view('admin.chats.index', [
|
||||
'conversations' => $conversations,
|
||||
'selectedConversation' => $selectedConversation,
|
||||
'initialMessages' => $initialMessages->map(fn (ChatMessage $message) => $this->messagePayload($message))->values(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function messages(ChatConversation $conversation): JsonResponse
|
||||
{
|
||||
$this->markCustomerMessagesAsRead($conversation);
|
||||
|
||||
return response()->json([
|
||||
'conversation' => $this->conversationPayload($conversation->loadMissing('user')),
|
||||
'messages' => $this->conversationMessages($conversation)
|
||||
->map(fn (ChatMessage $message) => $this->messagePayload($message))
|
||||
->values(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function storeMessage(Request $request, ChatConversation $conversation): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'message' => ['required', 'string', 'max:2000'],
|
||||
]);
|
||||
|
||||
$messageText = $this->sanitizeMessage((string) $validated['message']);
|
||||
if ($messageText === '') {
|
||||
throw ValidationException::withMessages([
|
||||
'message' => 'Сообщение содержит недопустимые символы.',
|
||||
]);
|
||||
}
|
||||
|
||||
$message = $conversation->messages()->create([
|
||||
'sender' => 'admin',
|
||||
'body' => $messageText,
|
||||
'is_read' => false,
|
||||
]);
|
||||
|
||||
$conversation->update([
|
||||
'status' => 'open',
|
||||
'last_message_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => $this->messagePayload($message),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateStatus(Request $request, ChatConversation $conversation): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'status' => ['required', 'string', Rule::in([
|
||||
ChatConversation::STATUS_OPEN,
|
||||
ChatConversation::STATUS_CLOSED,
|
||||
])],
|
||||
]);
|
||||
|
||||
$conversation->update([
|
||||
'status' => $validated['status'],
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.chats.index', ['conversation' => $conversation->id])
|
||||
->with('status', $conversation->isClosed() ? 'Чат закрыт.' : 'Чат открыт.');
|
||||
}
|
||||
|
||||
public function destroy(ChatConversation $conversation): RedirectResponse
|
||||
{
|
||||
$conversation->delete();
|
||||
|
||||
return redirect()
|
||||
->route('admin.chats.index')
|
||||
->with('status', 'Чат удален.');
|
||||
}
|
||||
|
||||
private function resolveSelectedConversation(Request $request, $conversations): ?ChatConversation
|
||||
{
|
||||
$selectedId = $request->integer('conversation');
|
||||
if ($selectedId > 0) {
|
||||
return ChatConversation::query()->with('user')->find($selectedId);
|
||||
}
|
||||
|
||||
$firstConversation = $conversations->first();
|
||||
if (!$firstConversation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ChatConversation::query()->with('user')->find($firstConversation->id);
|
||||
}
|
||||
|
||||
private function markCustomerMessagesAsRead(ChatConversation $conversation): void
|
||||
{
|
||||
$conversation->messages()
|
||||
->where('sender', 'customer')
|
||||
->where('is_read', false)
|
||||
->update(['is_read' => true]);
|
||||
}
|
||||
|
||||
private function conversationMessages(ChatConversation $conversation)
|
||||
{
|
||||
return $conversation->messages()
|
||||
->orderBy('id')
|
||||
->limit(200)
|
||||
->get();
|
||||
}
|
||||
|
||||
private function messagePayload(ChatMessage $message): array
|
||||
{
|
||||
return [
|
||||
'id' => $message->id,
|
||||
'sender' => $message->sender,
|
||||
'body' => $message->body,
|
||||
'created_at' => $message->created_at?->toIso8601String(),
|
||||
'time' => $message->created_at?->format('H:i'),
|
||||
];
|
||||
}
|
||||
|
||||
private function conversationPayload(ChatConversation $conversation): array
|
||||
{
|
||||
$preview = $conversation->latestMessage?->body;
|
||||
|
||||
return [
|
||||
'id' => $conversation->id,
|
||||
'title' => $conversation->display_name,
|
||||
'subtitle' => $conversation->user?->email ?: ('Токен: ' . Str::limit($conversation->visitor_token, 14, '...')),
|
||||
'preview' => $preview ? Str::limit($preview, 80) : 'Нет сообщений',
|
||||
'status' => $conversation->status,
|
||||
'is_closed' => $conversation->isClosed(),
|
||||
];
|
||||
}
|
||||
|
||||
private function sanitizeMessage(string $value): string
|
||||
{
|
||||
$text = strip_tags($value);
|
||||
$text = str_replace(["\r\n", "\r"], "\n", $text);
|
||||
$text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $text) ?? $text;
|
||||
|
||||
return trim(Str::limit($text, 2000, ''));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user