Files
tainagustului/UI_V2/pages/home/home.py

649 lines
26 KiB
Python

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)
ft.Text("Categori", weight=ft.FontWeight.BOLD, size=18)
],
alignment=ft.MainAxisAlignment.CENTER,
width=1000
)
self.banner = ft.Image("images/banner.png", width=1000, height=350, fit=ft.ImageFit.COVER)
self.categories_group = ft.Row([], scroll=ft.ScrollMode.ADAPTIVE, expand=True)
self.add_categories()
self.products_manager = Products()
self.products_group = ft.GridView(
spacing=10,
runs_count=5,
max_extent=200,
child_aspect_ratio=1.0,
expand=True,
width=1000
)
self.products = self.products_manager.get_all()[:20]
self.add_products(self.products)
self.searchbar = ft.TextField(
label="Cauta produsul in toate categoriile",
expand=True,
on_submit=self.on_search_btn_click
)
self.profile_placeholder = ft.Column()
self.profile_btn = ft.IconButton(
icon=ft.Icons.ACCOUNT_CIRCLE_OUTLINED,
on_click=self.on_profile_btn_click,
bgcolor=ft.Colors.BROWN,
icon_color=ft.Colors.WHITE
)
self.login_btn = ft.IconButton(
icon=ft.Icons.LOGIN,
on_click=self.on_login_btn_click,
bgcolor=ft.Colors.BROWN,
icon_color=ft.Colors.WHITE
)
if self.page.session.get("user") is not None and '@default.com' not in self.page.session.get("user")['email']:
self.profile_placeholder.controls.append(self.profile_btn)
else:
self.profile_placeholder.controls.append(self.login_btn)
self.search_for = self.page.session.get("search_for")
if self.search_for:
self.searchbar.value = self.page.session.get("search_for")
search = self.searchbar.value
buffer = []
for product in self.products:
if search.lower() in product['name'].lower():
buffer.append(product)
self.products_group.controls.clear()
self.add_products(buffer)
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 if self.page.width > 500 else 200,
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 if self.page.width > 500 else None,
scroll=ft.ScrollMode.ADAPTIVE
),
# 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('/')
def on_login_btn_click(self, e):
self.page.go('/auth')
def add_products(self, products):
for product in products:
self.new_price = ft.Text(
value=f"{round(float(product['price']) - float(product['price'])*float(product['discount'])/100,2)}",
size=14 if product['discount'] != 0 else 12,
color=ft.Colors.RED if product['discount'] != 0 else ft.Colors.BLACK,
weight=ft.FontWeight.BOLD
)
print(type(product['discount']))
self.old_price = ft.Text(
value=f"{product['price']}" if product['discount'] != 0 else '',
size=12,
color=ft.Colors.GREY,
style=ft.TextStyle(decoration=ft.TextDecoration.LINE_THROUGH),
)
card = ft.Card(
content=ft.Container(
content=ft.Column(
[
ft.Image(src=product['image'], width=170, height=100, border_radius=10, fit=ft.ImageFit.COVER),
ft.Text(product['name'], weight=ft.FontWeight.BOLD),
ft.Row(
[
ft.Text(f"Pret:", size=12),
self.old_price,
self.new_price,
ft.Text(f"lei/{product['quantity']}g", size=12),
],
alignment=ft.MainAxisAlignment.CENTER,
spacing=4
)
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER
),
ink=True,
on_click=lambda e, title=product: self.on_product_click(title),
padding=5
)
)
self.products_group.controls.append(card)
def add_categories(self):
categories = self.categories_manager.get_categories()
for category in categories:
card = ft.Card(
content=ft.Container(
content=ft.Column(
[
ft.Image(
src=category['image'],
width=100,
height = 80,
border_radius=10,
fit=ft.ImageFit.COVER),
ft.Text(category['name'])
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
alignment=ft.MainAxisAlignment.SPACE_BETWEEN
),
ink=True,
on_click=lambda e, title=category: self.on_category_click(title),
padding=5,
width=120,
height=120
)
)
self.categories_group.controls.append(card)
def on_category_click(self, cat):
name = cat['name'].replace(" ","-").lower()
self.page.session.set("category", cat)
self.page.go(f"/categorie/{name}")
def on_product_click(self, product):
self.page.session.set("product", product)
name = product['name'].replace(" ", "-").lower()
self.page.go(f'/produs/{name}')
def on_profile_btn_click(self, e):
self.page.go('/profil')
def on_cart_btn_click(self, e):
self.page.go("/pre_load_cos")
def on_search_btn_click(self, e):
search = self.searchbar.value
buffer = []
for product in self.products_manager.get_all():
if search.lower() in product['name'].lower():
buffer.append(product)
self.products_group.controls.clear()
self.add_products(buffer)
self.products_group.update()
self.searchbar.value = ''
self.searchbar.update()
def on_about_us_btn_click(self, e):
self.page.go('/about_us')
def on_terms_and_cond_btn_click(self, e):
self.page.go("/termeni_si_conditii")
def on_cancel_policy_btn_click(self, e):
self.page.go("/politica_de_anulare_comanda")
def on_confidentiality_policy_btn_click(self, e):
self.page.go('/politica_de_confidentialitate')
def on_delivery_policy_btn_click(self, e):
self.page.go("/politica_de_livrare_comanda")
def on_gdpr_btn_click(self, e):
self.page.go("/gdpr")
def build(self):
return ft.Container(
content=ft.Column(
[
ft.Row(
[
self.searchbar,
ft.IconButton(
icon=ft.Icons.SEARCH,
on_click=self.on_search_btn_click,
bgcolor=ft.Colors.BROWN,
icon_color=ft.Colors.WHITE
),
ft.VerticalDivider(),
self.profile_placeholder,
ft.IconButton(
icon=ft.Icons.SHOPPING_CART_OUTLINED,
bgcolor=ft.Colors.BROWN,
icon_color=ft.Colors.WHITE,
on_click = self.on_cart_btn_click
)
],
width=1000
),
self.banner,
ft.Column(
[
self.header,
self.categories_group,
ft.Row(
[
ft.Text("Produse Populare", size=18, weight=ft.FontWeight.BOLD),
],
alignment=ft.MainAxisAlignment.CENTER,
width=1000
),
self.products_group,
ft.Divider(height=1),
ft.Row(
[
ft.Column(
[
ft.TextButton("Despre noi", on_click=self.on_about_us_btn_click, icon=ft.Icons.INFO),
ft.TextButton("Termeni si conditii", on_click=self.on_terms_and_cond_btn_click, icon=ft.Icons.INFO),
ft.TextButton("Politica de anulare comanda",on_click=self.on_cancel_policy_btn_click, icon=ft.Icons.INFO),
ft.TextButton("Politica de confidentialitate",on_click=self.on_confidentiality_policy_btn_click, icon=ft.Icons.INFO),
ft.TextButton("Politica de livrare comanda",on_click=self.on_delivery_policy_btn_click, icon=ft.Icons.INFO),
ft.TextButton("Politica GDPR (siguranța datelor cu caracter personal)",on_click=self.on_gdpr_btn_click, icon=ft.Icons.INFO)
]
),
ft.Column(
[
ft.TextButton("TainaGustului", icon=ft.Icons.FACEBOOK)
]
)
],
alignment=ft.MainAxisAlignment.SPACE_AROUND,
vertical_alignment=ft.CrossAxisAlignment.START
)
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
expand=True,
width=1000
)
],
scroll=ft.ScrollMode.ADAPTIVE,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
),
expand=True,
padding=10,
bgcolor=ft.Colors.WHITE
)