add chat system
This commit is contained in:
@@ -1,11 +1,66 @@
|
||||
import flet as ft
|
||||
import uuid
|
||||
from dbActions.categories import Categories
|
||||
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:
|
||||
def __init__(self, page: ft.Page):
|
||||
self.page = page
|
||||
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(
|
||||
[
|
||||
#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.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):
|
||||
self.page.go('/')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user