648 lines
26 KiB
Python
648 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, # 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('/')
|
|
|
|
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
|
|
) |