Pārlūkot izejas kodu

feat: implement true chat protocol (sending messages via WebSocket)

unknown 2 mēneši atpakaļ
vecāks
revīzija
5f6ab948a3
3 mainītis faili ar 92 papildinājumiem un 24 dzēšanām
  1. 23 8
      backend/main.py
  2. 43 0
      backend/services/chat_manager.py
  3. 26 16
      src/components/OrderChat.vue

+ 23 - 8
backend/main.py

@@ -1,4 +1,5 @@
 import os
+import json
 # Fix matplotlib/gunicorn permission issues in server environment
 _temp_dir = os.path.join(os.getcwd(), 'temp')
 os.makedirs(_temp_dir, exist_ok=True)
@@ -151,19 +152,33 @@ async def ws_chat(websocket: WebSocket, token: str = Query(...), order_id: int =
     try:
         while True:
             data = await websocket.receive_text()
-            if data == "read":
-                if role == 'admin':
-                    db.execute_commit("UPDATE order_messages SET is_read = TRUE WHERE order_id = %s AND is_from_admin = FALSE AND is_read = FALSE", (order_id,))
-                    await global_manager.notify_admins()
-                    await global_manager.notify_order_read(order_id)
-                else:
-                    db.execute_commit("UPDATE order_messages SET is_read = TRUE WHERE order_id = %s AND is_from_admin = TRUE AND is_read = FALSE", (order_id,))
-                    await global_manager.notify_user(user_id)
+            try:
+                # Try to parse as JSON for new "true chat" protocol
+                msg_data = json.loads(data)
+                if msg_data.get("type") == "message":
+                    text = msg_data.get("text", "")
+                    await manager.handle_message(order_id, user_id, role, text)
+                elif msg_data.get("type") == "read":
+                    await _handle_read_event(order_id, user_id, role)
+            except json.JSONDecodeError:
+                # Fallback for legacy plain text messages (like "read")
+                if data == "read":
+                    await _handle_read_event(order_id, user_id, role)
     except WebSocketDisconnect:
         manager.disconnect(websocket, order_id)
     except Exception as e:
+        print(f"ERROR in ws_chat loop: {e}")
         manager.disconnect(websocket, order_id)
 
+async def _handle_read_event(order_id: int, user_id: int, role: str):
+    if role == 'admin':
+        db.execute_commit("UPDATE order_messages SET is_read = TRUE WHERE order_id = %s AND is_from_admin = FALSE AND is_read = FALSE", (order_id,))
+        await global_manager.notify_admins()
+        await global_manager.notify_order_read(order_id)
+    else:
+        db.execute_commit("UPDATE order_messages SET is_read = TRUE WHERE order_id = %s AND is_from_admin = TRUE AND is_read = FALSE", (order_id,))
+        await global_manager.notify_user(user_id)
+
 # Mount Static Files
 for sub in ["", "previews", "invoices", "reports"]:
     path = os.path.join("uploads", sub)

+ 43 - 0
backend/services/chat_manager.py

@@ -1,6 +1,7 @@
 import json
 import datetime
 import db
+import auth_utils
 from typing import Dict, List, Any
 from fastapi import WebSocket
 
@@ -60,4 +61,46 @@ class ChatConnectionManager:
         else:
             print(f"DEBUG: Attempted broadcast to Order #{order_id} but NO active connections found")
 
+    async def handle_message(self, order_id: int, user_id: int, role: str, text: str):
+        is_admin = (role == 'admin')
+        text = text.strip()
+        if not text:
+            return
+
+        # Save to DB
+        query = "INSERT INTO order_messages (order_id, user_id, is_from_admin, message) VALUES (%s, %s, %s, %s)"
+        msg_id = db.execute_commit(query, (order_id, user_id, is_admin, text))
+        
+        now = datetime.datetime.utcnow().isoformat()
+        message_payload = {
+            "id": msg_id,
+            "is_from_admin": is_admin,
+            "message": text,
+            "created_at": now,
+            "type": "message"
+        }
+        
+        # Broadcast to all in this chat
+        await self.broadcast_to_order(order_id, message_payload)
+        
+        # Global notifications (toasts, unread counts)
+        from services.global_manager import global_manager
+        if is_admin:
+            # Notify the client who owns the order
+            order = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
+            if order:
+                await global_manager.notify_user(order[0]['user_id'])
+        else:
+            # Notify admins
+            await global_manager.notify_admins()
+            await global_manager.notify_admins_new_message(order_id, text)
+            # External hooks (email, etc.)
+            try:
+                from services import event_hooks
+                event_hooks.on_message_received(order_id, user_id, text)
+            except Exception as e:
+                print(f"ERROR in chat event hooks: {e}")
+        
+        return msg_id
+
 manager = ChatConnectionManager()

+ 26 - 16
src/components/OrderChat.vue

@@ -208,22 +208,32 @@ async function handleSend() {
 
   isSending.value = true;
   try {
-    const response = await sendOrderMessage(props.orderId, text, locale.value);
-    newMessage.value = "";
-    if (textareaRef.value) {
-      textareaRef.value.style.height = "auto";
-    }
-    
-    // Add message to list only if it hasn't arrived via WS yet
-    if (!messages.value.some(m => m.id == response.id)) {
-      const myRole = authStore.user?.role === 'admin' ? 'admin' : 'user';
-      messages.value.push({
-        id: response.id,
-        message: text,
-        is_from_admin: myRole === 'admin',
-        created_at: new Date().toISOString()
-      });
-      scrollToBottom();
+    // If WS is connected, send through it for "True Chat" experience
+    if (ws && wsConnected.value && ws.readyState === WebSocket.OPEN) {
+      ws.send(JSON.stringify({ type: "message", text }));
+      newMessage.value = "";
+      if (textareaRef.value) {
+        textareaRef.value.style.height = "auto";
+      }
+    } else {
+      // Fallback to API if WS is down
+      const response = await sendOrderMessage(props.orderId, text, locale.value);
+      newMessage.value = "";
+      if (textareaRef.value) {
+        textareaRef.value.style.height = "auto";
+      }
+      
+      // Add message to list only if it hasn't arrived via WS yet
+      if (!messages.value.some(m => m.id == response.id)) {
+        const myRole = authStore.user?.role === 'admin' ? 'admin' : 'user';
+        messages.value.push({
+          id: response.id,
+          message: text,
+          is_from_admin: myRole === 'admin',
+          created_at: new Date().toISOString()
+        });
+        scrollToBottom();
+      }
     }
     
     // Start flood control cooldown