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 None, # chat messages area height #expand = True if self.page.width < 500 else None, 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 )