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, '')); } }