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()
|
||||||
@@ -7,10 +7,20 @@ from admin.clients import Clients
|
|||||||
from admin.fidelity_cards import FidelityCards
|
from admin.fidelity_cards import FidelityCards
|
||||||
from admin.settings import Settings
|
from admin.settings import Settings
|
||||||
from admin.inventory.inventory import Inventory
|
from admin.inventory.inventory import Inventory
|
||||||
|
from admin.admin_chat import AdminChat
|
||||||
|
|
||||||
class Dashboard:
|
class Dashboard:
|
||||||
def __init__(self, page: ft.Page):
|
def __init__(self, page: ft.Page):
|
||||||
self.page = page
|
self.page = page
|
||||||
|
# Hide client floating chat button when in admin dashboard
|
||||||
|
# Home/landing page is responsible for re-creating it when needed.
|
||||||
|
try:
|
||||||
|
# If a floating_action_button exists (client chat), remove it for admin views
|
||||||
|
if getattr(self.page, "floating_action_button", None) is not None:
|
||||||
|
self.page.floating_action_button = None
|
||||||
|
except Exception as e:
|
||||||
|
# If anything goes wrong, fail silently; admin UI will still work.
|
||||||
|
print(e)
|
||||||
self.category = Category(self.page)
|
self.category = Category(self.page)
|
||||||
self.placeholder = ft.Container(
|
self.placeholder = ft.Container(
|
||||||
content=self.category.build(),
|
content=self.category.build(),
|
||||||
@@ -56,9 +66,9 @@ class Dashboard:
|
|||||||
label_content=ft.Text("Banner"),
|
label_content=ft.Text("Banner"),
|
||||||
),
|
),
|
||||||
ft.NavigationRailDestination(
|
ft.NavigationRailDestination(
|
||||||
icon=ft.Icons.CARD_GIFTCARD_OUTLINED,
|
icon=ft.Icons.CHAT_OUTLINED,
|
||||||
selected_icon=ft.Icon(ft.Icons.CARD_GIFTCARD),
|
selected_icon=ft.Icon(ft.Icons.CHAT),
|
||||||
label_content=ft.Text("Card de\nfidelitate"),
|
label_content=ft.Text("Chat"),
|
||||||
),
|
),
|
||||||
ft.NavigationRailDestination(
|
ft.NavigationRailDestination(
|
||||||
icon=ft.Icons.INVENTORY_2_OUTLINED,
|
icon=ft.Icons.INVENTORY_2_OUTLINED,
|
||||||
@@ -105,9 +115,13 @@ class Dashboard:
|
|||||||
self.placeholder.content = self.banner.build()
|
self.placeholder.content = self.banner.build()
|
||||||
self.placeholder.update()
|
self.placeholder.update()
|
||||||
case 6:
|
case 6:
|
||||||
self.fidelity_cards = FidelityCards(self.page)
|
# self.fidelity_cards = FidelityCards(self.page)
|
||||||
self.placeholder.content = self.fidelity_cards.build()
|
# self.placeholder.content = self.fidelity_cards.build()
|
||||||
|
# self.placeholder.update()
|
||||||
|
self.chat = AdminChat(self.page)
|
||||||
|
self.placeholder.content = self.chat.build()
|
||||||
self.placeholder.update()
|
self.placeholder.update()
|
||||||
|
|
||||||
case 7:
|
case 7:
|
||||||
self.inventory = Inventory(self.page, self)
|
self.inventory = Inventory(self.page, self)
|
||||||
self.placeholder.content = self.inventory.build()
|
self.placeholder.content = self.inventory.build()
|
||||||
|
|||||||
352
UI_V2/dbActions/chat.py
Normal file
352
UI_V2/dbActions/chat.py
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
import sqlite3
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class Chat:
|
||||||
|
def __init__(self, db_path="instance/app_database.db"):
|
||||||
|
self.db_path = db_path
|
||||||
|
self._create_chatsessions_table()
|
||||||
|
self._create_chatmessages_table()
|
||||||
|
|
||||||
|
def _create_chatsessions_table(self):
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS chatsessions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER,
|
||||||
|
session_token TEXT,
|
||||||
|
status TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_message_at DATETIME ,
|
||||||
|
last_message_from TEXT,
|
||||||
|
unread_for_admin INTEGER,
|
||||||
|
unread_for_user INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def _create_chatmessages_table(self):
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS chatmessages (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
chat_id INTEGER,
|
||||||
|
session_token TEXT,
|
||||||
|
sender_type TEXT,
|
||||||
|
sender_id INTEGER,
|
||||||
|
text TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
read_at DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def add_chatsession(self, chatsession):
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO chatsessions (user_id, session_token, status, last_message_at, last_message_from, unread_for_admin, unread_for_user)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""", (
|
||||||
|
chatsession['user_id'],
|
||||||
|
chatsession['session_token'],
|
||||||
|
chatsession['status'],
|
||||||
|
chatsession['last_message_at'],
|
||||||
|
chatsession['last_message_from'],
|
||||||
|
chatsession['unread_for_admin'],
|
||||||
|
chatsession['unread_for_user']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_chatsession(self, chat_id: int):
|
||||||
|
"""Return a single chat session by its primary key id."""
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM chatsessions
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(chat_id,),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
return {
|
||||||
|
'id': row[0],
|
||||||
|
'user_id': row[1],
|
||||||
|
'session_token': row[2],
|
||||||
|
'status': row[3],
|
||||||
|
'created_at': row[4],
|
||||||
|
'last_message_at': row[5],
|
||||||
|
'last_message_from': row[6],
|
||||||
|
'unread_for_admin': row[7],
|
||||||
|
'unread_for_user': row[8],
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_chatsession_by_token(self, session_token: str) -> Optional[dict]:
|
||||||
|
"""Return the chat session associated with a given browser / user session token."""
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM chatsessions
|
||||||
|
WHERE session_token = ?
|
||||||
|
""",
|
||||||
|
(session_token,),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
return {
|
||||||
|
'id': row[0],
|
||||||
|
'user_id': row[1],
|
||||||
|
'session_token': row[2],
|
||||||
|
'status': row[3],
|
||||||
|
'created_at': row[4],
|
||||||
|
'last_message_at': row[5],
|
||||||
|
'last_message_from': row[6],
|
||||||
|
'unread_for_admin': row[7],
|
||||||
|
'unread_for_user': row[8],
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_all_chatsessions(self):
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM chatsessions
|
||||||
|
""")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
if rows:
|
||||||
|
buffer = []
|
||||||
|
for row in rows:
|
||||||
|
buffer.append(
|
||||||
|
{
|
||||||
|
'id': row[0],
|
||||||
|
'user_id': row[1],
|
||||||
|
'session_token': row[2],
|
||||||
|
'status': row[3],
|
||||||
|
'created_at' : row[4],
|
||||||
|
'last_message_at': row[5],
|
||||||
|
'last_message_from': row[6],
|
||||||
|
'unread_for_admin': row[7],
|
||||||
|
'unread_for_user': row[8],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return buffer
|
||||||
|
return []
|
||||||
|
|
||||||
|
def update_chatsession(self, chatsession):
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE chatsessions SET
|
||||||
|
user_id = ?,
|
||||||
|
status = ?,
|
||||||
|
last_message_at = ?,
|
||||||
|
last_message_from = ?,
|
||||||
|
unread_for_admin = ?,
|
||||||
|
unread_for_user = ?
|
||||||
|
WHERE id = ?
|
||||||
|
''', (
|
||||||
|
chatsession['user_id'],
|
||||||
|
chatsession['status'],
|
||||||
|
chatsession['last_message_at'],
|
||||||
|
chatsession['last_message_from'],
|
||||||
|
chatsession['unread_for_admin'],
|
||||||
|
chatsession['unread_for_user'],
|
||||||
|
chatsession['id']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def delete(self, id):
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
DELETE FROM chatsessions WHERE id=?;
|
||||||
|
''', (id,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def add_chatmessage(self, message: dict) -> bool:
|
||||||
|
"""Insert a new chat message.
|
||||||
|
|
||||||
|
Expected keys in message dict:
|
||||||
|
- chat_id (int)
|
||||||
|
- session_token (str)
|
||||||
|
- sender_type (str: 'client' or 'admin')
|
||||||
|
- sender_id (int or None)
|
||||||
|
- text (str)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO chatmessages (chat_id, session_token, sender_type, sender_id, text)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
message['chat_id'],
|
||||||
|
message.get('session_token'),
|
||||||
|
message['sender_type'],
|
||||||
|
message.get('sender_id'),
|
||||||
|
message['text'],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
# Refresh last_message_at on the corresponding chat session
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
UPDATE chatsessions
|
||||||
|
SET last_message_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(message["chat_id"],),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_chatmessages_for_chat(self, chat_id: int):
|
||||||
|
"""Return all messages for a given chat, ordered by creation time."""
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, chat_id, session_token, sender_type, sender_id, text, created_at, read_at
|
||||||
|
FROM chatmessages
|
||||||
|
WHERE chat_id = ?
|
||||||
|
ORDER BY created_at ASC, id ASC
|
||||||
|
""",
|
||||||
|
(chat_id,),
|
||||||
|
)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
messages = []
|
||||||
|
for row in rows:
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
'id': row[0],
|
||||||
|
'chat_id': row[1],
|
||||||
|
'session_token': row[2],
|
||||||
|
'sender_type': row[3],
|
||||||
|
'sender_id': row[4],
|
||||||
|
'text': row[5],
|
||||||
|
'created_at': row[6],
|
||||||
|
'read_at': row[7],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return messages
|
||||||
|
|
||||||
|
def mark_messages_as_read(self, chat_id: int, reader_type: str):
|
||||||
|
"""Mark messages in a chat as read for a given reader.
|
||||||
|
|
||||||
|
Typically called with reader_type='admin' or 'client'.
|
||||||
|
We mark as read all messages where sender_type != reader_type and read_at IS NULL.
|
||||||
|
"""
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
UPDATE chatmessages
|
||||||
|
SET read_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE chat_id = ? AND sender_type != ? AND read_at IS NULL
|
||||||
|
""",
|
||||||
|
(chat_id, reader_type),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def delete_chatmessages_for_chat(self, chat_id: int):
|
||||||
|
"""Delete all messages for a given chat (e.g. when cleaning up a closed session)."""
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM chatmessages
|
||||||
|
WHERE chat_id = ?
|
||||||
|
""",
|
||||||
|
(chat_id,),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def cleanup_old_sessions(self):
|
||||||
|
"""Delete chat sessions and their messages older than today.
|
||||||
|
|
||||||
|
Any session whose effective date (last_message_at if set, otherwise created_at)
|
||||||
|
is strictly before today's date will be removed together with its messages.
|
||||||
|
"""
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# Identify sessions to delete: older than today based on last_message_at or created_at
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT id FROM chatsessions
|
||||||
|
WHERE DATE(COALESCE(last_message_at, created_at)) < DATE('now','localtime')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
if not rows:
|
||||||
|
return
|
||||||
|
|
||||||
|
old_ids = [row[0] for row in rows]
|
||||||
|
|
||||||
|
# Delete messages for these sessions
|
||||||
|
cursor.execute(
|
||||||
|
f"""
|
||||||
|
DELETE FROM chatmessages
|
||||||
|
WHERE chat_id IN ({','.join(['?'] * len(old_ids))})
|
||||||
|
""",
|
||||||
|
old_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete the sessions themselves
|
||||||
|
cursor.execute(
|
||||||
|
f"""
|
||||||
|
DELETE FROM chatsessions
|
||||||
|
WHERE id IN ({','.join(['?'] * len(old_ids))})
|
||||||
|
""",
|
||||||
|
old_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def get_chatsessions_with_messages(self):
|
||||||
|
"""Return only chat sessions that have at least one message."""
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT cs.id, cs.user_id, cs.session_token, cs.status,
|
||||||
|
cs.created_at, cs.last_message_at, cs.last_message_from,
|
||||||
|
cs.unread_for_admin, cs.unread_for_user
|
||||||
|
FROM chatsessions cs
|
||||||
|
WHERE cs.id IN (SELECT DISTINCT chat_id FROM chatmessages)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
sessions = []
|
||||||
|
for row in rows:
|
||||||
|
sessions.append(
|
||||||
|
{
|
||||||
|
"id": row[0],
|
||||||
|
"user_id": row[1],
|
||||||
|
"session_token": row[2],
|
||||||
|
"status": row[3],
|
||||||
|
"created_at": row[4],
|
||||||
|
"last_message_at": row[5],
|
||||||
|
"last_message_from": row[6],
|
||||||
|
"unread_for_admin": row[7],
|
||||||
|
"unread_for_user": row[8],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return sessions
|
||||||
@@ -119,6 +119,29 @@ class Users:
|
|||||||
}
|
}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_user_by_role(self, role: str) -> Optional[dict]:
|
||||||
|
"""Retrieve user details by username."""
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM users
|
||||||
|
WHERE role = ?
|
||||||
|
""", (role,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
return {
|
||||||
|
"id": row[0],
|
||||||
|
"email": row[1],
|
||||||
|
"token": row[3],
|
||||||
|
"name":row[4],
|
||||||
|
"phone": row[5],
|
||||||
|
"address": row[6],
|
||||||
|
"created_at": row[7],
|
||||||
|
"status": row[8],
|
||||||
|
"role":row[9]
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
def get(self, id: int) -> Optional[dict]:
|
def get(self, id: int) -> Optional[dict]:
|
||||||
"""Retrieve user details by username."""
|
"""Retrieve user details by username."""
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
|||||||
53
UI_V2/helpers/notifications.py
Normal file
53
UI_V2/helpers/notifications.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import flet as ft
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def send_browser_notification(page: ft.Page, title: str, body: str) -> None:
|
||||||
|
"""Broadcast a browser notification event to all connected sessions.
|
||||||
|
|
||||||
|
This is typically called from the client (shop) side when a new chat
|
||||||
|
is opened or the first message is sent. The actual visual notification
|
||||||
|
will be displayed only on admin pages that handle the event.
|
||||||
|
"""
|
||||||
|
if page is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
payload: dict[str, Any] = {
|
||||||
|
"type": "browser_notification",
|
||||||
|
"title": title,
|
||||||
|
"body": body,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use PubSub so that admin sessions can react and show a snackbar/toast.
|
||||||
|
try:
|
||||||
|
# send_all so that any admin pages, and potentially this page, can receive it
|
||||||
|
page.pubsub.send_all(payload)
|
||||||
|
except Exception:
|
||||||
|
# If PubSub is not available, we simply do nothing; email still covers notification.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def show_local_browser_notification(page: ft.Page, title: str, body: str) -> None:
|
||||||
|
"""Show a visual notification (snackbar) on the current page for admin users.
|
||||||
|
|
||||||
|
This is intended to be called from admin pages when a `browser_notification`
|
||||||
|
event is received via PubSub. It will silently do nothing for non-admin users.
|
||||||
|
"""
|
||||||
|
if page is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
user = page.session.get("user") if hasattr(page, "session") else None
|
||||||
|
if not user or user.get("role") != "admin":
|
||||||
|
# Only admins see these notifications in the browser
|
||||||
|
return
|
||||||
|
|
||||||
|
message = f"{title}: {body}" if title else body
|
||||||
|
|
||||||
|
page.snack_bar = ft.SnackBar(
|
||||||
|
content=ft.Text(message),
|
||||||
|
open=True,
|
||||||
|
action="OK",
|
||||||
|
)
|
||||||
|
page.update()
|
||||||
@@ -1,11 +1,66 @@
|
|||||||
import flet as ft
|
import flet as ft
|
||||||
|
import uuid
|
||||||
from dbActions.categories import Categories
|
from dbActions.categories import Categories
|
||||||
from dbActions.products import Products
|
from dbActions.products import Products
|
||||||
|
from dbActions.chat import Chat
|
||||||
|
from helpers.emails import send_gmail
|
||||||
|
from helpers.notifications import send_browser_notification
|
||||||
|
from dbActions.users import Users
|
||||||
|
|
||||||
class Home:
|
class Home:
|
||||||
def __init__(self, page: ft.Page):
|
def __init__(self, page: ft.Page):
|
||||||
self.page = page
|
self.page = page
|
||||||
self.categories_manager = Categories()
|
self.categories_manager = Categories()
|
||||||
|
self.chat_manager = Chat()
|
||||||
|
# --- Chat session setup ---
|
||||||
|
# Logged-in user (if any)
|
||||||
|
self.user = self.page.session.get("user")
|
||||||
|
print(self.user)
|
||||||
|
|
||||||
|
self.users_manager = Users()
|
||||||
|
self.admin = self.users_manager.get_user_by_role("admin")
|
||||||
|
|
||||||
|
# Persistent token per browser session, used to associate a chat session
|
||||||
|
self.chat_session_token = self.page.session.get("chat_session_token")
|
||||||
|
if not self.chat_session_token:
|
||||||
|
self.chat_session_token = uuid.uuid4().hex
|
||||||
|
self.page.session.set("chat_session_token", self.chat_session_token)
|
||||||
|
|
||||||
|
# Get or create chat session in DB
|
||||||
|
chat_session = self.chat_manager.get_chatsession_by_token(self.chat_session_token)
|
||||||
|
if chat_session is None:
|
||||||
|
chat_data = {
|
||||||
|
"user_id": self.user["id"] if self.user else None,
|
||||||
|
"session_token": self.chat_session_token,
|
||||||
|
"status": "open",
|
||||||
|
"last_message_at": None,
|
||||||
|
"last_message_from": None,
|
||||||
|
"unread_for_admin": 0,
|
||||||
|
"unread_for_user": 0,
|
||||||
|
}
|
||||||
|
self.chat_manager.add_chatsession(chat_data)
|
||||||
|
chat_session = self.chat_manager.get_chatsession_by_token(self.chat_session_token)
|
||||||
|
|
||||||
|
# If we have a logged-in user, always make sure the chat session is linked to that user
|
||||||
|
if self.user and chat_session is not None:
|
||||||
|
current_user_id = chat_session.get("user_id")
|
||||||
|
new_user_id = self.user.get("id")
|
||||||
|
# Only update if different or missing to avoid unnecessary writes
|
||||||
|
if current_user_id is None or current_user_id != new_user_id:
|
||||||
|
chat_session["user_id"] = new_user_id
|
||||||
|
try:
|
||||||
|
self.chat_manager.update_chatsession(chat_session)
|
||||||
|
except Exception as e:
|
||||||
|
print("Unable to update chatsession user_id:", e)
|
||||||
|
|
||||||
|
self.chat_id = chat_session["id"]
|
||||||
|
|
||||||
|
# Subscribe to PubSub to receive admin messages for this chat
|
||||||
|
try:
|
||||||
|
self.page.pubsub.subscribe(self.on_pubsub_message)
|
||||||
|
except Exception:
|
||||||
|
# If PubSub is not configured, just ignore; chat will still work locally.
|
||||||
|
pass
|
||||||
self.header = ft.Row(
|
self.header = ft.Row(
|
||||||
[
|
[
|
||||||
#ft.Button("Acasa", icon=ft.Icons.HOME, on_click=self.on_acasa_btn_click)
|
#ft.Button("Acasa", icon=ft.Icons.HOME, on_click=self.on_acasa_btn_click)
|
||||||
@@ -66,6 +121,339 @@ class Home:
|
|||||||
self.page.session.set("search_for", None)
|
self.page.session.set("search_for", None)
|
||||||
self.searchbar.value = ''
|
self.searchbar.value = ''
|
||||||
|
|
||||||
|
page.floating_action_button = ft.FloatingActionButton(
|
||||||
|
icon=ft.Icons.CHAT,
|
||||||
|
tooltip="Ai nevoie de ajutor?",
|
||||||
|
on_click=self.open_chat,
|
||||||
|
bgcolor=ft.Colors.BROWN_50,
|
||||||
|
badge=ft.Badge(bgcolor=ft.Colors.GREEN, small_size=10)
|
||||||
|
)
|
||||||
|
page.floating_action_button_location = ft.FloatingActionButtonLocation.END_FLOAT
|
||||||
|
|
||||||
|
# Flag: does this chat already have any messages persisted in DB?
|
||||||
|
self.chat_has_messages = False
|
||||||
|
|
||||||
|
# Messages list (scrollable)
|
||||||
|
self.messages_list = ft.ListView(
|
||||||
|
expand=True,
|
||||||
|
spacing=10,
|
||||||
|
auto_scroll=True,
|
||||||
|
padding=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load existing chat history from DB or show a default welcome message
|
||||||
|
self.load_chat_history()
|
||||||
|
|
||||||
|
# Input field
|
||||||
|
self.chat_input = ft.TextField(
|
||||||
|
hint_text="Scrie mesajul tău aici...",
|
||||||
|
border_radius=20,
|
||||||
|
dense=True,
|
||||||
|
content_padding=ft.Padding(12, 8, 12, 8),
|
||||||
|
expand=True,
|
||||||
|
on_submit=self.on_send_click
|
||||||
|
)
|
||||||
|
|
||||||
|
self.send_button = ft.IconButton(
|
||||||
|
icon=ft.Icons.SEND_ROUNDED,
|
||||||
|
tooltip="Trimite",
|
||||||
|
on_click=self.on_send_click,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.chat_header = ft.Row(
|
||||||
|
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
||||||
|
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||||
|
controls=[
|
||||||
|
ft.Row(
|
||||||
|
spacing=10,
|
||||||
|
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||||
|
controls=[
|
||||||
|
ft.CircleAvatar(
|
||||||
|
content=ft.Icon(ft.Icons.SUPPORT_AGENT),
|
||||||
|
bgcolor=ft.Colors.BROWN_100,
|
||||||
|
radius=18,
|
||||||
|
),
|
||||||
|
ft.Column(
|
||||||
|
spacing=0,
|
||||||
|
controls=[
|
||||||
|
ft.Text("Suport online", weight="bold", size=15),
|
||||||
|
ft.Text("De obicei răspundem în câteva minute", size=11, color=ft.Colors.GREY_600),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ft.IconButton(
|
||||||
|
icon=ft.Icons.CLOSE,
|
||||||
|
tooltip="Închide chat-ul",
|
||||||
|
on_click=self.close_chat,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Whole chat panel inside BottomSheet
|
||||||
|
self.chat_content = ft.Container(
|
||||||
|
# This keeps it nice on desktop (not full width) but still responsive
|
||||||
|
content=ft.Column(
|
||||||
|
expand=True,
|
||||||
|
controls=[
|
||||||
|
self.chat_header,
|
||||||
|
ft.Divider(),
|
||||||
|
ft.Container(
|
||||||
|
height=280, # chat messages area height
|
||||||
|
content=self.messages_list,
|
||||||
|
),
|
||||||
|
ft.Divider(),
|
||||||
|
ft.Row(
|
||||||
|
controls=[self.chat_input, self.send_button],
|
||||||
|
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
padding=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.chat_sheet = ft.BottomSheet(
|
||||||
|
ft.Column(
|
||||||
|
controls=[
|
||||||
|
ft.Text(),
|
||||||
|
ft.Row(
|
||||||
|
controls=[
|
||||||
|
ft.Container(
|
||||||
|
content=self.chat_content,
|
||||||
|
bgcolor=ft.Colors.WHITE,
|
||||||
|
border_radius=16,
|
||||||
|
shadow=ft.BoxShadow(
|
||||||
|
blur_radius=18,
|
||||||
|
spread_radius=0,
|
||||||
|
color=ft.Colors.with_opacity(0.25, ft.Colors.BLACK),
|
||||||
|
),
|
||||||
|
# Max width for desktop; on mobile it will just fill the width
|
||||||
|
width=350,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
alignment=ft.MainAxisAlignment.CENTER,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
width=400,
|
||||||
|
height=500
|
||||||
|
),
|
||||||
|
# Optional: set this to True if you want click-outside to close
|
||||||
|
enable_drag=True,
|
||||||
|
is_scroll_controlled=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Header close action
|
||||||
|
def close_chat(self, e):
|
||||||
|
self.page.close(self.chat_sheet)
|
||||||
|
|
||||||
|
def load_chat_history(self):
|
||||||
|
"""Load messages from DB for this chat and render them in the list view.
|
||||||
|
|
||||||
|
If there is no history, show a default welcome message.
|
||||||
|
"""
|
||||||
|
# Clear any existing controls, just to be safe
|
||||||
|
self.messages_list.controls.clear()
|
||||||
|
|
||||||
|
history = []
|
||||||
|
try:
|
||||||
|
history = self.chat_manager.get_chatmessages_for_chat(self.chat_id)
|
||||||
|
except Exception:
|
||||||
|
history = []
|
||||||
|
|
||||||
|
# Track if this chat already has any messages
|
||||||
|
self.chat_has_messages = bool(history)
|
||||||
|
|
||||||
|
if not history:
|
||||||
|
# Pre-fill with a welcome message (design only)
|
||||||
|
self.messages_list.controls.append(
|
||||||
|
ft.Container(
|
||||||
|
bgcolor=ft.Colors.GREY_200,
|
||||||
|
padding=8,
|
||||||
|
border_radius=12,
|
||||||
|
content=ft.Text(
|
||||||
|
"Bună! 👋 Cu ce te pot ajuta azi?",
|
||||||
|
size=13,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
for msg in history:
|
||||||
|
if msg.get("sender_type") == "client":
|
||||||
|
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(msg.get("text", ""), size=13),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_send_click(self, e):
|
||||||
|
text = self.chat_input.value.strip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Determine if this is the first message in this chat
|
||||||
|
is_first_message = not getattr(self, "chat_has_messages", False)
|
||||||
|
|
||||||
|
# Append user 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.chat_id,
|
||||||
|
"session_token": self.chat_session_token,
|
||||||
|
"sender_type": "client",
|
||||||
|
"sender_id": self.user["id"] if self.user else None,
|
||||||
|
"text": text,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
self.chat_manager.add_chatmessage(message)
|
||||||
|
except Exception:
|
||||||
|
# If DB fails, ignore for now but still show message locally
|
||||||
|
pass
|
||||||
|
|
||||||
|
# From this point on, the chat definitely has at least one message
|
||||||
|
self.chat_has_messages = True
|
||||||
|
|
||||||
|
# If this was the first message, trigger notifications (email + browser) for ADMIN
|
||||||
|
if is_first_message:
|
||||||
|
# 1) Email to admin, if we have a usable email address
|
||||||
|
try:
|
||||||
|
to_email = None
|
||||||
|
if getattr(self, "admin", None):
|
||||||
|
# Depending on how get_user_by_role works, self.admin may be a dict or a list
|
||||||
|
admin_obj = self.admin
|
||||||
|
if isinstance(admin_obj, dict):
|
||||||
|
to_email = admin_obj.get("email")
|
||||||
|
elif isinstance(admin_obj, (list, tuple)) and admin_obj:
|
||||||
|
# Take the first admin record if a list is returned
|
||||||
|
first_admin = admin_obj[0]
|
||||||
|
if isinstance(first_admin, dict):
|
||||||
|
to_email = first_admin.get("email")
|
||||||
|
|
||||||
|
if to_email and "@" in to_email:
|
||||||
|
subject = "[Taina Gustului] Chat nou de la client"
|
||||||
|
client_info = "un vizitator neautentificat"
|
||||||
|
if self.user:
|
||||||
|
client_email = self.user.get("email")
|
||||||
|
client_name = self.user.get("name") or self.user.get("full_name")
|
||||||
|
if client_email:
|
||||||
|
client_info = f"clientul cu email {client_email}"
|
||||||
|
elif client_name:
|
||||||
|
client_info = f"clientul {client_name}"
|
||||||
|
|
||||||
|
body = (
|
||||||
|
"Bună,\n\n"
|
||||||
|
f"{client_info} a început o discuție pe chat în magazinul online Taina Gustului.\n"
|
||||||
|
"Poți răspunde direct din panoul de administrare, în secțiunea de chat.\n\n"
|
||||||
|
"Acest mesaj este generat automat."
|
||||||
|
)
|
||||||
|
send_gmail(to_email, subject, body)
|
||||||
|
except Exception as ex:
|
||||||
|
print("Unable to send chat email notification to admin:", ex)
|
||||||
|
|
||||||
|
# 2) Browser notification (intended for admin side)
|
||||||
|
# This simply delegates to helpers.notifications.send_browser_notification,
|
||||||
|
# which you can implement to show notifications in the admin UI.
|
||||||
|
try:
|
||||||
|
title = "Chat nou de la client"
|
||||||
|
client_info = None
|
||||||
|
if self.user:
|
||||||
|
client_info = self.user.get("email") or self.user.get("name")
|
||||||
|
|
||||||
|
body = (
|
||||||
|
"Un client a început o discuție pe chat."
|
||||||
|
if not client_info
|
||||||
|
else f"{client_info} a început o discuție pe chat."
|
||||||
|
)
|
||||||
|
send_browser_notification(self.page, title, body)
|
||||||
|
except Exception as ex:
|
||||||
|
print("Unable to send browser notification for admin:", ex)
|
||||||
|
|
||||||
|
# Update chat session metadata
|
||||||
|
try:
|
||||||
|
chatsession = self.chat_manager.get_chatsession(self.chat_id)
|
||||||
|
if chatsession:
|
||||||
|
chatsession["last_message_from"] = "client"
|
||||||
|
chatsession["unread_for_admin"] = 1
|
||||||
|
# do not change unread_for_user here
|
||||||
|
self.chat_manager.update_chatsession(chatsession)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Notify other sessions (e.g., admin console) via PubSub
|
||||||
|
try:
|
||||||
|
self.page.pubsub.send_others(
|
||||||
|
{
|
||||||
|
"type": "chat_message",
|
||||||
|
"chat_id": self.chat_id,
|
||||||
|
"sender": "client",
|
||||||
|
"text": text,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# PubSub might not be available; safe to ignore
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.chat_input.value = ""
|
||||||
|
# Keep the focus in the chat input so the user can continue typing
|
||||||
|
self.chat_input.focus()
|
||||||
|
self.chat_input.update()
|
||||||
|
self.page.update()
|
||||||
|
|
||||||
|
def on_pubsub_message(self, data):
|
||||||
|
"""Handle incoming PubSub messages (e.g., from admin)."""
|
||||||
|
# Only process dictionary payloads
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Handle incoming chat messages for this chat
|
||||||
|
if data.get("type") == "chat_message" and data.get("chat_id") == self.chat_id:
|
||||||
|
if data.get("sender") == "admin":
|
||||||
|
# Show admin message as a bubble on the left
|
||||||
|
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(data.get("text", ""), size=13),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.page.update()
|
||||||
|
|
||||||
|
def open_chat(self, e):
|
||||||
|
self.page.open(self.chat_sheet)
|
||||||
|
|
||||||
def on_acasa_btn_click(self, e):
|
def on_acasa_btn_click(self, e):
|
||||||
self.page.go('/')
|
self.page.go('/')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user