11# frozen_string_literal: true
22
33module Tenants
4- # Контроллер для просмотра чатов tenant'а
4+ # Контроллер для просмотра и управления чатами tenant'а
55 #
66 # Показывает список чатов с пагинацией и сортировкой,
7- # а также историю переписки выбранного чата.
7+ # историю переписки выбранного чата, а также позволяет
8+ # менеджерам перехватывать диалоги и отправлять сообщения.
89 class ChatsController < ApplicationController
10+ include ErrorLogger
11+
912 PER_PAGE = 20
1013
1114 # GET /chats
@@ -22,6 +25,59 @@ def show
2225 @chat = load_chat_with_messages ( params [ :id ] )
2326 end
2427
28+ # POST /chats/:id/takeover
29+ # Перехват диалога менеджером
30+ def takeover
31+ @chat = find_chat
32+ ChatTakeoverService . new ( @chat ) . takeover! ( current_user )
33+
34+ respond_to do |format |
35+ format . turbo_stream
36+ format . html { redirect_to tenant_chat_path ( @chat ) , notice : t ( '.success' ) }
37+ end
38+ rescue ChatTakeoverService ::AlreadyTakenError => e
39+ respond_with_error ( t ( '.already_taken' ) )
40+ rescue ChatTakeoverService ::UnauthorizedError => e
41+ respond_with_error ( t ( '.unauthorized' ) )
42+ end
43+
44+ # POST /chats/:id/release
45+ # Возврат диалога боту
46+ def release
47+ @chat = find_chat
48+ ChatTakeoverService . new ( @chat ) . release! ( user : current_user )
49+
50+ respond_to do |format |
51+ format . turbo_stream
52+ format . html { redirect_to tenant_chat_path ( @chat ) , notice : t ( '.success' ) }
53+ end
54+ rescue ChatTakeoverService ::NotTakenError => e
55+ respond_with_error ( t ( '.not_taken' ) )
56+ rescue ChatTakeoverService ::UnauthorizedError => e
57+ respond_with_error ( t ( '.unauthorized' ) )
58+ end
59+
60+ # POST /chats/:id/send_message
61+ # Отправка сообщения менеджером
62+ def send_message
63+ @chat = find_chat
64+
65+ return respond_with_error ( t ( '.empty_message' ) ) if params [ :text ] . blank?
66+
67+ @message = ManagerMessageService . new ( @chat ) . send! ( current_user , params [ :text ] )
68+
69+ respond_to do |format |
70+ format . turbo_stream
71+ format . html { redirect_to tenant_chat_path ( @chat ) }
72+ end
73+ rescue ManagerMessageService ::NotInManagerModeError => e
74+ respond_with_error ( t ( '.not_in_manager_mode' ) )
75+ rescue ManagerMessageService ::NotTakenByUserError => e
76+ respond_with_error ( t ( '.not_taken_by_user' ) )
77+ rescue ManagerMessageService ::RateLimitExceededError => e
78+ respond_with_error ( t ( '.rate_limit_exceeded' ) )
79+ end
80+
2581 private
2682
2783 # Загружает чат со всеми сообщениями (с лимитом для производительности)
@@ -35,9 +91,10 @@ def load_chat_with_messages(chat_id)
3591
3692 # Загружаем сообщения с лимитом для производительности
3793 # (200 сообщений ≈ 120KB HTML, 2000 DOM nodes)
94+ # Используем id как tiebreaker при одинаковом created_at
3895 messages = chat . messages
3996 . includes ( :tool_calls )
40- . order ( created_at : :desc )
97+ . order ( created_at : :desc , id : :desc )
4198 . limit ( ApplicationConfig . max_chat_messages_display )
4299 . reverse
43100
@@ -63,11 +120,12 @@ def fetch_chats
63120 def preload_last_messages ( chats )
64121 return if chats . empty?
65122
66- # Get last message for each chat in single query using window function
123+ # Get last message for each chat in single query using DISTINCT ON
124+ # Order by id DESC as tiebreaker when created_at is the same
67125 last_messages = Message
68126 . where ( chat_id : chats . map ( &:id ) )
69127 . select ( 'DISTINCT ON (chat_id) *' )
70- . order ( 'chat_id, created_at DESC' )
128+ . order ( 'chat_id, created_at DESC, id DESC ' )
71129 . index_by ( &:chat_id )
72130
73131 # Assign to association cache
@@ -79,5 +137,27 @@ def preload_last_messages(chats)
79137 def sort_column
80138 %w[ last_message_at created_at ] . include? ( params [ :sort ] ) ? params [ :sort ] : 'last_message_at'
81139 end
140+
141+ # Находит чат для takeover/release/send_message actions
142+ def find_chat
143+ current_tenant . chats
144+ . with_client_details
145+ . includes ( messages : :tool_calls )
146+ . find ( params [ :id ] )
147+ end
148+
149+ # Отвечает с ошибкой
150+ def respond_with_error ( message )
151+ respond_to do |format |
152+ format . turbo_stream do
153+ render turbo_stream : turbo_stream . replace (
154+ 'flash' ,
155+ partial : 'tenants/shared/flash' ,
156+ locals : { message : message , type : :error }
157+ )
158+ end
159+ format . html { redirect_to tenant_chat_path ( @chat ) , alert : message }
160+ end
161+ end
82162 end
83163end
0 commit comments