add chat system
This commit is contained in:
385
UI_V2/admin/admin_chat.py
Normal file
385
UI_V2/admin/admin_chat.py
Normal file
@@ -0,0 +1,385 @@
|
||||
import flet as ft
|
||||
from dbActions.chat import Chat
|
||||
from dbActions.users import Users
|
||||
|
||||
class AdminChat:
|
||||
def __init__(self, page: ft.Page):
|
||||
self.page = page
|
||||
self.chat_manager = Chat()
|
||||
self.user_namager = Users()
|
||||
|
||||
# Logged in user (ensure this is an admin)
|
||||
self.user = self.page.session.get("user")
|
||||
|
||||
# Access flag: if not admin, we won't build the chat UI
|
||||
self.access_denied = False
|
||||
if not self.user or self.user.get("role") != "admin":
|
||||
self.access_denied = True
|
||||
return
|
||||
|
||||
# --- Admin chat state ---
|
||||
self.active_chat_id: int | None = None
|
||||
self.active_chat_session: dict | None = None
|
||||
|
||||
# UI containers
|
||||
self.sessions_column = ft.Column(spacing=4, expand=True)
|
||||
self.messages_list = ft.ListView(expand=True, spacing=10, auto_scroll=True)
|
||||
self.input_field = ft.TextField(
|
||||
hint_text="Scrie răspunsul tău aici...",
|
||||
expand=True,
|
||||
dense=True,
|
||||
border_radius=20,
|
||||
content_padding=ft.Padding(12, 8, 12, 8),
|
||||
on_submit=self.on_send_click
|
||||
)
|
||||
self.send_button = ft.IconButton(
|
||||
icon=ft.Icons.SEND_ROUNDED,
|
||||
tooltip="Trimite",
|
||||
on_click=self.on_send_click,
|
||||
)
|
||||
|
||||
# Subscribe to PubSub for live updates from clients
|
||||
try:
|
||||
self.page.pubsub.subscribe(self.on_pubsub_message)
|
||||
except Exception as e:
|
||||
# If PubSub is not configured, admin can still see messages with manual refresh
|
||||
print(e)
|
||||
|
||||
# Cleanup old sessions (keep only chats from today in DB)
|
||||
try:
|
||||
self.chat_manager.cleanup_old_sessions()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# Preload sessions data so that when build() is called, lists are already populated
|
||||
self.load_sessions()
|
||||
|
||||
def build(self) -> ft.Control:
|
||||
"""Return main layout: left panel (sessions) + right panel (messages).
|
||||
|
||||
This allows the admin chat UI to be embedded inside another container/page.
|
||||
"""
|
||||
# If access is denied, return an informational message
|
||||
if getattr(self, "access_denied", False):
|
||||
return ft.Column(
|
||||
controls=[
|
||||
ft.Text("Acces restricționat", size=22, weight=ft.FontWeight.BOLD),
|
||||
ft.Text("Această pagină este disponibilă doar pentru administratori."),
|
||||
],
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
)
|
||||
|
||||
# Left side: list of chat sessions
|
||||
left_panel = ft.Container(
|
||||
bgcolor=ft.Colors.GREY_100,
|
||||
padding=10,
|
||||
width=280,
|
||||
content=ft.Column(
|
||||
expand=True,
|
||||
controls=[
|
||||
ft.Text("Conversații", size=18, weight=ft.FontWeight.BOLD),
|
||||
ft.Divider(),
|
||||
ft.Container(
|
||||
expand=True,
|
||||
content=ft.ListView(
|
||||
expand=True,
|
||||
spacing=4,
|
||||
controls=[self.sessions_column],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
# Right side: active conversation
|
||||
if not hasattr(self, "header_label"):
|
||||
self.header_label = ft.Text("Selectează o conversație", size=18, weight=ft.FontWeight.BOLD)
|
||||
|
||||
right_panel = ft.Container(
|
||||
expand=True,
|
||||
padding=10,
|
||||
content=ft.Column(
|
||||
expand=True,
|
||||
controls=[
|
||||
ft.Row(
|
||||
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
controls=[
|
||||
self.header_label,
|
||||
ft.Icon(ft.Icons.SUPPORT_AGENT),
|
||||
],
|
||||
),
|
||||
ft.Divider(),
|
||||
ft.Container(
|
||||
expand=True,
|
||||
content=self.messages_list,
|
||||
),
|
||||
ft.Divider(),
|
||||
ft.Row(
|
||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
controls=[self.input_field, self.send_button],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
main_row = ft.Row(
|
||||
expand=True,
|
||||
controls=[left_panel, right_panel],
|
||||
)
|
||||
|
||||
return main_row
|
||||
|
||||
def load_sessions(self):
|
||||
"""Load all chat sessions from DB and populate the left panel."""
|
||||
self.sessions_column.controls.clear()
|
||||
|
||||
try:
|
||||
sessions = self.chat_manager.get_chatsessions_with_messages()
|
||||
except Exception:
|
||||
sessions = []
|
||||
|
||||
if not sessions:
|
||||
self.sessions_column.controls.append(
|
||||
ft.Text("Nu există conversații deschise.", size=12, color=ft.Colors.GREY_600)
|
||||
)
|
||||
return
|
||||
|
||||
# Sort by last_message_at DESC (if available) or created_at
|
||||
def sort_key(session):
|
||||
return session.get("last_message_at") or session.get("created_at") or ""
|
||||
|
||||
sessions = sorted(sessions, key=sort_key, reverse=True)
|
||||
|
||||
for session in sessions:
|
||||
self.sessions_column.controls.append(self._build_session_tile(session))
|
||||
|
||||
def _build_session_tile(self, session: dict) -> ft.Container:
|
||||
"""Create a clickable row for a chat session."""
|
||||
chat_id = session.get("id")
|
||||
user_id = session.get("user_id")
|
||||
status = session.get("status", "open")
|
||||
unread_for_admin = session.get("unread_for_admin", 0)
|
||||
|
||||
title = f"Chat #{chat_id}"
|
||||
if user_id:
|
||||
user = self.user_namager.get(user_id)
|
||||
title += f" • User {user['name'].replace("~", " ")}"
|
||||
|
||||
subtitle_parts = []
|
||||
if status == "open":
|
||||
subtitle_parts.append("Deschis")
|
||||
else:
|
||||
subtitle_parts.append("Închis")
|
||||
|
||||
last_from = session.get("last_message_from")
|
||||
if last_from:
|
||||
subtitle_parts.append(f"Ultimul mesaj: {last_from}")
|
||||
|
||||
subtitle = " • ".join(subtitle_parts)
|
||||
|
||||
badge = None
|
||||
if unread_for_admin:
|
||||
badge = ft.Container(
|
||||
bgcolor=ft.Colors.RED_400,
|
||||
border_radius=20,
|
||||
padding=ft.padding.symmetric(horizontal=8, vertical=2),
|
||||
content=ft.Text("nou", size=10, color=ft.Colors.WHITE),
|
||||
)
|
||||
|
||||
bg_color = ft.Colors.BROWN_100 if unread_for_admin else ft.Colors.WHITE
|
||||
|
||||
tile = ft.Container(
|
||||
padding=8,
|
||||
border_radius=8,
|
||||
bgcolor=bg_color,
|
||||
ink=True,
|
||||
on_click=lambda e, cid=chat_id: self.on_select_session(cid),
|
||||
content=ft.Row(
|
||||
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
||||
controls=[
|
||||
ft.Column(
|
||||
spacing=2,
|
||||
controls=[
|
||||
ft.Text(title, weight=ft.FontWeight.BOLD, size=13),
|
||||
ft.Text(subtitle, size=11, color=ft.Colors.GREY_600),
|
||||
],
|
||||
),
|
||||
badge if badge else ft.Container(),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
return tile
|
||||
|
||||
def on_select_session(self, chat_id: int):
|
||||
"""Triggered when admin clicks a chat session on the left."""
|
||||
self.active_chat_id = chat_id
|
||||
|
||||
# Load session details
|
||||
try:
|
||||
session = self.chat_manager.get_chatsession(chat_id)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
session = None
|
||||
|
||||
self.active_chat_session = session
|
||||
|
||||
# Update header
|
||||
if session:
|
||||
user_id = session.get("user_id")
|
||||
label = f"Chat #{chat_id}"
|
||||
if user_id:
|
||||
user = self.user_namager.get(user_id)
|
||||
label += f" • User {user['name'].replace("~", " ")}"
|
||||
self.header_label.value = label
|
||||
else:
|
||||
self.header_label.value = f"Chat #{chat_id}"
|
||||
|
||||
# Load messages
|
||||
self.load_messages_for_active_chat()
|
||||
|
||||
# Mark messages as read for admin and update session's unread flag
|
||||
try:
|
||||
self.chat_manager.mark_messages_as_read(chat_id, "admin")
|
||||
if session:
|
||||
session["unread_for_admin"] = 0
|
||||
self.chat_manager.update_chatsession(session)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# Refresh session list to remove unread badge
|
||||
self.load_sessions()
|
||||
self.page.update()
|
||||
|
||||
def load_messages_for_active_chat(self):
|
||||
"""Load messages from DB for the currently active chat."""
|
||||
self.messages_list.controls.clear()
|
||||
|
||||
if not self.active_chat_id:
|
||||
return
|
||||
|
||||
try:
|
||||
messages = self.chat_manager.get_chatmessages_for_chat(self.active_chat_id)
|
||||
except Exception:
|
||||
messages = []
|
||||
|
||||
for msg in messages:
|
||||
sender_type = msg.get("sender_type")
|
||||
text = msg.get("text", "")
|
||||
|
||||
if sender_type == "admin":
|
||||
alignment = ft.MainAxisAlignment.END
|
||||
bubble_color = ft.Colors.BROWN_100
|
||||
else:
|
||||
alignment = ft.MainAxisAlignment.START
|
||||
bubble_color = ft.Colors.GREY_200
|
||||
|
||||
self.messages_list.controls.append(
|
||||
ft.Row(
|
||||
alignment=alignment,
|
||||
controls=[
|
||||
ft.Container(
|
||||
bgcolor=bubble_color,
|
||||
padding=8,
|
||||
border_radius=12,
|
||||
content=ft.Text(text, size=13),
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
def on_send_click(self, e):
|
||||
"""Admin sends a new message to the active chat."""
|
||||
text = self.input_field.value.strip()
|
||||
if not text or not self.active_chat_id:
|
||||
return
|
||||
|
||||
# Append admin bubble locally
|
||||
self.messages_list.controls.append(
|
||||
ft.Row(
|
||||
alignment=ft.MainAxisAlignment.END,
|
||||
controls=[
|
||||
ft.Container(
|
||||
bgcolor=ft.Colors.BROWN_100,
|
||||
padding=8,
|
||||
border_radius=12,
|
||||
content=ft.Text(text, size=13),
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
# Persist message in DB
|
||||
message = {
|
||||
"chat_id": self.active_chat_id,
|
||||
"session_token": None,
|
||||
"sender_type": "admin",
|
||||
"sender_id": self.user.get("id") if self.user else None,
|
||||
"text": text,
|
||||
}
|
||||
try:
|
||||
self.chat_manager.add_chatmessage(message)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# Update chat session metadata
|
||||
try:
|
||||
session = self.chat_manager.get_chatsession(self.active_chat_id)
|
||||
if session:
|
||||
session["last_message_from"] = "admin"
|
||||
session["unread_for_user"] = 1
|
||||
self.chat_manager.update_chatsession(session)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# Notify client side via PubSub
|
||||
try:
|
||||
self.page.pubsub.send_all(
|
||||
{
|
||||
"type": "chat_message",
|
||||
"chat_id": self.active_chat_id,
|
||||
"sender": "admin",
|
||||
"text": text,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print (e)
|
||||
|
||||
# Refresh the sessions list to reflect new last_message_from and ordering
|
||||
self.load_sessions()
|
||||
|
||||
self.input_field.value = ""
|
||||
self.page.update()
|
||||
|
||||
def on_pubsub_message(self, data):
|
||||
"""Handle PubSub messages coming from client pages."""
|
||||
if not isinstance(data, dict):
|
||||
return
|
||||
|
||||
# New message from a client
|
||||
if data.get("type") == "chat_message" and data.get("sender") == "client":
|
||||
chat_id = data.get("chat_id")
|
||||
|
||||
# If this is the currently open chat, append bubble directly
|
||||
if self.active_chat_id == chat_id:
|
||||
text = data.get("text", "")
|
||||
self.messages_list.controls.append(
|
||||
ft.Row(
|
||||
alignment=ft.MainAxisAlignment.START,
|
||||
controls=[
|
||||
ft.Container(
|
||||
bgcolor=ft.Colors.GREY_200,
|
||||
padding=8,
|
||||
border_radius=12,
|
||||
content=ft.Text(text, size=13),
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
self.page.update()
|
||||
|
||||
# In any case, refresh sessions to update unread badges
|
||||
self.load_sessions()
|
||||
self.page.update()
|
||||
Reference in New Issue
Block a user