- Add A-Z alphabetical filter keyboard for clients and suppliers lists (same pattern as company selection, without emoji) - Increase clients/suppliers list pagination from 10 to 20 items per page - Remove emoji from company A-Z filter button for consistency - Add 6 new callback handlers: clients_alpha_menu, clients_alpha:LETTER, clients_alpha_page:PAGE:LETTER, and supplier equivalents - Dashboard service and models updates - Telegram bot: email handlers, auth, DB operations, internal API improvements - Frontend: dashboard cards updates (CashFlow, Clienti, Furnizori, Treasury) - Frontend: SolduriCompactCard and CollapsibleCard improvements - DashboardView enhancements - start.sh and run-with-restart.sh script updates - IIS web.config and service worker updates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3228 lines
128 KiB
Python
3228 lines
128 KiB
Python
"""
|
||
Telegram Bot Handlers for ROA2WEB ERP Assistant
|
||
|
||
This module implements all message and command handlers for the Telegram bot.
|
||
Handles user interactions, authentication, and routes messages to Claude Agent.
|
||
"""
|
||
|
||
import logging
|
||
from typing import Optional, Dict, Any
|
||
from io import BytesIO
|
||
|
||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||
from telegram.ext import ContextTypes
|
||
from telegram.constants import ParseMode
|
||
|
||
from backend.modules.telegram.auth.linking import (
|
||
link_telegram_account,
|
||
check_user_linked,
|
||
get_user_auth_data,
|
||
get_user_companies
|
||
)
|
||
from backend.modules.telegram.agent.session import get_session_manager
|
||
from backend.modules.telegram.db.operations import update_user_last_active, link_user_to_oracle
|
||
from backend.modules.telegram.api.client import get_backend_client
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# ============================================================================
|
||
# COMMAND HANDLERS
|
||
# ============================================================================
|
||
|
||
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle /start command.
|
||
|
||
Handles two cases:
|
||
1. /start <auth_code> - Link Telegram account to Oracle account
|
||
2. /start - Show welcome message and instructions
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
telegram_user = update.effective_user
|
||
telegram_user_id = telegram_user.id
|
||
args = context.args # Command arguments
|
||
|
||
logger.info(f"/start command from user {telegram_user_id} (@{telegram_user.username})")
|
||
|
||
# Case 1: /start <auth_code> - Link account
|
||
if args and len(args) > 0:
|
||
auth_code = args[0].upper()
|
||
logger.info(f"Attempting to link user {telegram_user_id} with code {auth_code}")
|
||
|
||
# ȘTERGE mesajul utilizatorului imediat (chat curat)
|
||
try:
|
||
await update.message.delete()
|
||
except Exception as e:
|
||
logger.warning(f"Could not delete /start message: {e}")
|
||
|
||
# Check dacă user-ul a apăsat pe "Login din Web App" înainte
|
||
web_login_msg_id = context.user_data.get('web_login_message_id')
|
||
|
||
if web_login_msg_id:
|
||
# EDITEAZĂ mesajul existent cu "Login din Web App"
|
||
try:
|
||
await context.bot.edit_message_text(
|
||
chat_id=update.effective_chat.id,
|
||
message_id=web_login_msg_id,
|
||
text="Conectare cont...",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
# IMPORTANT: NU ștergem message_id - îl păstrăm pentru editări ulterioare
|
||
except Exception as e:
|
||
logger.warning(f"Could not edit web_login message: {e}")
|
||
# Fallback: creează mesaj nou dacă editarea a eșuat
|
||
web_login_msg_id = None
|
||
|
||
# Dacă nu avem mesaj de editat, creăm unul nou
|
||
if not web_login_msg_id:
|
||
# Creează mesaj nou și salvează ID-ul în context pentru editări ulterioare
|
||
new_msg = await update.effective_chat.send_message(
|
||
"Conectare cont...",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
web_login_msg_id = new_msg.message_id
|
||
# Salvează în context pentru consistență
|
||
context.user_data['web_login_message_id'] = web_login_msg_id
|
||
|
||
# Attempt linking
|
||
result = await link_telegram_account(telegram_user, auth_code)
|
||
|
||
if result:
|
||
# Success!
|
||
username = result['username']
|
||
jwt_token = result['jwt_token']
|
||
|
||
# Get session and company info
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
company_name = company['name'] if company else None
|
||
company_cui = company.get('cui') if company else None
|
||
|
||
# Get cache status
|
||
cache_enabled = None
|
||
try:
|
||
from backend.modules.telegram.api.client import get_backend_client
|
||
client = get_backend_client()
|
||
async with client:
|
||
cache_stats = await client.get_cache_stats(jwt_token=jwt_token)
|
||
cache_enabled = cache_stats.get('user_enabled', True)
|
||
except Exception as e:
|
||
logger.warning(f"Could not get cache status in /start: {e}")
|
||
|
||
from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons
|
||
keyboard = create_main_menu(company_name, company_cui, is_authenticated=True, cache_enabled=cache_enabled)
|
||
|
||
# EDIT message to show menu with company
|
||
if company_name:
|
||
menu_text = f"{company_name}"
|
||
else:
|
||
menu_text = "Selectează o companie pentru a continua"
|
||
|
||
menu_message = pad_message_for_wide_buttons(menu_text)
|
||
|
||
# EDIT the login message (same message throughout the flow)
|
||
await context.bot.edit_message_text(
|
||
chat_id=update.effective_chat.id,
|
||
message_id=web_login_msg_id,
|
||
text=menu_message,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
logger.info(f"User {telegram_user_id} successfully linked to {username}")
|
||
|
||
else:
|
||
# Failed linking - EDIT message to show error
|
||
await context.bot.edit_message_text(
|
||
chat_id=update.effective_chat.id,
|
||
message_id=web_login_msg_id,
|
||
text="Cod invalid sau expirat\n\n"
|
||
"Generează un cod nou din aplicația web și trimite:\n"
|
||
"/start CODUL_TAU",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
logger.warning(f"Failed to link user {telegram_user_id} with code {auth_code}")
|
||
|
||
return
|
||
|
||
# Case 2: /start (no args) - Show welcome/instructions
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
|
||
if is_linked:
|
||
# FAZA 3: User is already linked - SHOW MENU
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
username = auth_data.get('username', 'utilizator') if auth_data else 'utilizator'
|
||
|
||
# Get active company
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
|
||
company_name = company['name'] if company else None
|
||
company_cui = company.get('cui') if company else None
|
||
|
||
# Create main menu
|
||
from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message
|
||
keyboard = create_main_menu(company_name, company_cui, is_authenticated=True)
|
||
menu_text = get_menu_message(company_name, company_cui)
|
||
|
||
await update.message.reply_text(
|
||
f"Bun venit, **{username}**\n\n{menu_text}",
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
else:
|
||
# User not linked - show main menu with Login button
|
||
from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons
|
||
|
||
keyboard = create_main_menu(
|
||
company_name=None,
|
||
company_cui=None,
|
||
is_authenticated=False, # Not authenticated - shows Login button
|
||
cache_enabled=None
|
||
)
|
||
|
||
welcome_text = pad_message_for_wide_buttons(
|
||
"**Bun venit la ROA2WEB Bot**\n\n"
|
||
"Pentru a incepe, te rog să te autentifici.\n\n"
|
||
"Selectează o companie pentru a continua"
|
||
)
|
||
|
||
await update.message.reply_text(
|
||
welcome_text,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in start_command: {e}", exc_info=True)
|
||
await update.message.reply_text(
|
||
"A aparut o eroare. Te rog incearca din nou mai tarziu."
|
||
)
|
||
|
||
|
||
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle /help command.
|
||
|
||
Shows comprehensive help about bot capabilities and usage.
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
|
||
logger.info(f"/help command from user {telegram_user_id}")
|
||
|
||
help_text = """
|
||
**ROA2WEB Bot - Asistent Financiar**
|
||
|
||
**Cum folosesc bot-ul?**
|
||
|
||
Dupa conectarea contului, foloseste **butoanele interactive** pentru:
|
||
|
||
**Operatiuni:**
|
||
- Selectare companie activa
|
||
- Vizualizare sold si situatie financiara
|
||
- Trezorerie (Casa, Banca)
|
||
- Sold Clienti cu detalii facturi
|
||
- Sold Furnizori cu detalii facturi
|
||
- Evolutie incasari/plati lunare
|
||
|
||
**Navigare:**
|
||
- Toate optiunile sunt accesibile prin butoane
|
||
- Apasa pe numele companiei pentru a schimba compania activa
|
||
- Foloseste butoanele "Refresh" pentru actualizare date
|
||
- Foloseste "Meniu Principal" pentru a reveni la menu
|
||
|
||
**Comenzi disponibile:**
|
||
/start - Porneste bot-ul (cu/fara cod de linking)
|
||
/menu - Afiseaza meniul principal cu butoane
|
||
/help - Acest mesaj de ajutor
|
||
/unlink - Deconecteaza contul (securitate)
|
||
|
||
**Comenzi Cache (optimizare performanta):**
|
||
/togglecache - Activeaza/Dezactiveaza cache pentru tine
|
||
/clearcache - Sterge cache pentru compania activa
|
||
/clearcache all - Sterge tot cache-ul
|
||
|
||
**Conectare cont:**
|
||
1. Loghează-te în aplicația web ROA2WEB
|
||
2. Accesează Setări → Telegram Linking
|
||
3. Generează cod (valabil 15 minute)
|
||
4. Trimite codul în Telegram: `/start CODUL_TAU`
|
||
|
||
**Securitate:**
|
||
Datele sunt protejate prin autentificare JWT.
|
||
Poti deconecta oricand cu /unlink.
|
||
"""
|
||
|
||
await update.message.reply_text(help_text, parse_mode=ParseMode.MARKDOWN)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in help_command: {e}", exc_info=True)
|
||
await update.message.reply_text(
|
||
"A aparut o eroare. Te rog incearca din nou."
|
||
)
|
||
|
||
|
||
async def clear_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle /clear command.
|
||
|
||
Clears the active company selection for the user.
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
|
||
logger.info(f"/clear command from user {telegram_user_id}")
|
||
|
||
# Clear active company from session
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
session.clear_active_company()
|
||
await session_manager.save_session(telegram_user_id)
|
||
|
||
await update.message.reply_text(
|
||
"**Companie activa stearsa**\n\n"
|
||
"Foloseste /selectcompany pentru a selecta alta companie.",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in clear_command: {e}", exc_info=True)
|
||
await update.message.reply_text(
|
||
"A apărut o eroare la ștergerea companiei active."
|
||
)
|
||
|
||
|
||
async def clearcache_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle /clearcache command.
|
||
|
||
Clears the cache for the current company or all companies.
|
||
|
||
Usage:
|
||
- /clearcache - Clear cache for current company
|
||
- /clearcache all - Clear entire cache (all companies)
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
logger.info(f"/clearcache command from user {telegram_user_id}")
|
||
|
||
# Check if user is linked
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
if not is_linked:
|
||
await update.message.reply_text(
|
||
"**Cont neconectat**\n\nFoloseste /start pentru a conecta contul.",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Get auth data
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
# Check if user wants to clear all cache
|
||
clear_all = len(context.args) > 0 and context.args[0].lower() == 'all'
|
||
|
||
client = get_backend_client()
|
||
async with client:
|
||
if clear_all:
|
||
# Clear entire cache
|
||
result = await client.client.post(
|
||
"/api/cache/invalidate",
|
||
json={},
|
||
headers=client._get_auth_headers(jwt_token)
|
||
)
|
||
if result.status_code == 200:
|
||
await update.message.reply_text(
|
||
"✅ **Cache șters complet**\n\n"
|
||
"Toate datele cached au fost șterse.",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
else:
|
||
await update.message.reply_text("❌ Eroare la ștergerea cache-ului.")
|
||
else:
|
||
# Get active company
|
||
session_manager = get_session_manager()
|
||
from backend.modules.telegram.bot.helpers import get_active_company_or_prompt
|
||
company = await get_active_company_or_prompt(update, session_manager, telegram_user_id)
|
||
|
||
if not company:
|
||
return
|
||
|
||
# Clear cache for current company
|
||
result = await client.client.post(
|
||
"/api/cache/invalidate",
|
||
json={"company_id": company['id']},
|
||
headers=client._get_auth_headers(jwt_token)
|
||
)
|
||
|
||
if result.status_code == 200:
|
||
await update.message.reply_text(
|
||
f"✅ **Cache șters pentru {company['name']}**\n\n"
|
||
"Datele vor fi reîncărcate la următoarea interogare.",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
else:
|
||
await update.message.reply_text("❌ Eroare la ștergerea cache-ului.")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in clearcache_command: {e}", exc_info=True)
|
||
await update.message.reply_text("Eroare la ștergerea cache-ului.")
|
||
|
||
|
||
async def togglecache_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle /togglecache command.
|
||
|
||
Toggles cache on/off for the current user.
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
logger.info(f"/togglecache command from user {telegram_user_id}")
|
||
|
||
# Check if user is linked
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
if not is_linked:
|
||
await update.message.reply_text(
|
||
"**Cont neconectat**\n\nFoloseste /start pentru a conecta contul.",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Get auth data
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
client = get_backend_client()
|
||
async with client:
|
||
# Get current cache stats to determine current state
|
||
stats_response = await client.client.get(
|
||
"/api/cache/stats",
|
||
headers=client._get_auth_headers(jwt_token)
|
||
)
|
||
|
||
if stats_response.status_code == 200:
|
||
stats = stats_response.json()
|
||
current_enabled = stats.get('user_cache_enabled', True)
|
||
|
||
# Toggle to opposite state
|
||
new_state = not current_enabled
|
||
|
||
toggle_response = await client.client.post(
|
||
"/api/cache/toggle-user",
|
||
json={"enabled": new_state},
|
||
headers=client._get_auth_headers(jwt_token)
|
||
)
|
||
|
||
if toggle_response.status_code == 200:
|
||
if new_state:
|
||
await update.message.reply_text(
|
||
"✅ **Cache activat**\n\n"
|
||
"Interogările tale vor folosi cache-ul pentru răspunsuri mai rapide.",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
else:
|
||
await update.message.reply_text(
|
||
"⚠️ **Cache dezactivat**\n\n"
|
||
"Interogările tale vor accesa direct baza de date Oracle.",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
else:
|
||
await update.message.reply_text("❌ Eroare la comutarea cache-ului.")
|
||
else:
|
||
await update.message.reply_text("❌ Eroare la citirea stării cache-ului.")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in togglecache_command: {e}", exc_info=True)
|
||
await update.message.reply_text("Eroare la comutarea cache-ului.")
|
||
|
||
|
||
async def companies_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle /companies command.
|
||
|
||
Shows list of companies the user has access to.
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
|
||
logger.info(f"/companies command from user {telegram_user_id}")
|
||
|
||
# Check if user is linked
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
|
||
if not is_linked:
|
||
await update.message.reply_text(
|
||
"**Cont neconectat**\n\n"
|
||
"Conecteaza-ti contul cu /start",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Get companies
|
||
companies = await get_user_companies(telegram_user_id)
|
||
|
||
if not companies:
|
||
await update.message.reply_text(
|
||
"**Nicio companie gasita**\n\n"
|
||
"Contacteaza administratorul pentru permisiuni.",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Format companies list
|
||
companies_text = f"**Companiile tale ({len(companies)}):**\n\n"
|
||
|
||
for i, comp in enumerate(companies, 1):
|
||
nume = comp.get('nume_firma', 'N/A')
|
||
comp_id = comp.get('id', 'N/A')
|
||
cui = comp.get('cui', 'N/A')
|
||
|
||
companies_text += f"{i}. **{nume}**\n"
|
||
companies_text += f" - ID: {comp_id}\n"
|
||
companies_text += f" - CUI: {cui}\n\n"
|
||
|
||
companies_text += "\nFoloseste /selectcompany pentru a selecta compania activa."
|
||
|
||
await update.message.reply_text(companies_text, parse_mode=ParseMode.MARKDOWN)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in companies_command: {e}", exc_info=True)
|
||
await update.message.reply_text(
|
||
"A aparut o eroare la incarcarea companiilor."
|
||
)
|
||
|
||
|
||
async def unlink_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle /unlink command.
|
||
|
||
Unlinks the user's Telegram account from Oracle account (security feature).
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
|
||
logger.info(f"/unlink command from user {telegram_user_id}")
|
||
|
||
# Check if linked
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
|
||
if not is_linked:
|
||
await update.message.reply_text(
|
||
"Contul tau nu este linkuit.",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Create confirmation keyboard
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("Da, deconecteaza", callback_data="unlink_confirm"),
|
||
InlineKeyboardButton("Anuleaza", callback_data="unlink_cancel")
|
||
]
|
||
]
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
|
||
await update.message.reply_text(
|
||
"**Confirmare Deconectare**\n\n"
|
||
"Esti sigur ca vrei sa deconectezi contul?\n\n"
|
||
"Accesul la date va fi oprit. Poti reconecta oricand cu un cod nou.\n\n"
|
||
"Confirma:",
|
||
reply_markup=reply_markup,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in unlink_command: {e}", exc_info=True)
|
||
await update.message.reply_text(
|
||
"A aparut o eroare."
|
||
)
|
||
|
||
|
||
async def selectcompany_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle /selectcompany [search_term] command.
|
||
|
||
Permite căutare și selectare companie cu PAGINARE (identic cu butoanele).
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
|
||
logger.info(f"/selectcompany command from user {telegram_user_id}")
|
||
|
||
# Check if user is linked
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
if not is_linked:
|
||
await update.message.reply_text(
|
||
"**Cont neconectat**\n\nFoloseste /start",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Get auth data
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
# Get search term from command arguments (optional)
|
||
search_term = " ".join(context.args) if context.args else ""
|
||
|
||
# ✅ MODIFICARE: Folosim funcția comună cu paginare
|
||
await _handle_selectcompany_view(
|
||
query_or_update=update,
|
||
telegram_user_id=telegram_user_id,
|
||
jwt_token=jwt_token,
|
||
is_callback=False,
|
||
page=0,
|
||
search_term=search_term
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in selectcompany_command: {e}", exc_info=True)
|
||
await update.message.reply_text("A aparut o eroare. Te rog incearca din nou.")
|
||
|
||
|
||
async def dashboard_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle /dashboard command - shows financial dashboard."""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
|
||
logger.info(f"/dashboard command from user {telegram_user_id}")
|
||
|
||
# Check linked
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
if not is_linked:
|
||
await update.message.reply_text(
|
||
"**Cont neconectat**\n\nFoloseste /start",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Get active company
|
||
session_manager = get_session_manager()
|
||
from backend.modules.telegram.bot.helpers import get_active_company_or_prompt
|
||
company = await get_active_company_or_prompt(update, session_manager, telegram_user_id)
|
||
|
||
if not company:
|
||
return # Prompt already sent
|
||
|
||
# Get auth data
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
# ✅ MODIFICARE: Folosim funcția comună
|
||
await _handle_sold_view(
|
||
query_or_update=update,
|
||
telegram_user_id=telegram_user_id,
|
||
company=company,
|
||
jwt_token=jwt_token,
|
||
is_callback=False
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in dashboard_command: {e}", exc_info=True)
|
||
await update.message.reply_text("Eroare la incarcarea dashboard-ului.")
|
||
|
||
|
||
async def sold_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle /sold command - alias for /dashboard."""
|
||
await dashboard_command(update, context)
|
||
|
||
|
||
async def facturi_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle /facturi [filtru] command - shows invoices list."""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
|
||
logger.info(f"/facturi command from user {telegram_user_id}")
|
||
|
||
# Check linked
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
if not is_linked:
|
||
await update.message.reply_text(
|
||
"**Cont nelinkuit**\n\nFoloseste /start.",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Get active company
|
||
session_manager = get_session_manager()
|
||
from backend.modules.telegram.bot.helpers import get_active_company_or_prompt
|
||
company = await get_active_company_or_prompt(update, session_manager, telegram_user_id)
|
||
|
||
if not company:
|
||
return
|
||
|
||
# Parse filters from args (optional: "neplatite", "platite", etc.)
|
||
filters = {}
|
||
if context.args:
|
||
status_arg = context.args[0].lower()
|
||
if status_arg in ['neplatite', 'unpaid']:
|
||
filters['status'] = 'unpaid'
|
||
elif status_arg in ['platite', 'paid']:
|
||
filters['status'] = 'paid'
|
||
|
||
# Get auth data
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
# Call API
|
||
client = get_backend_client()
|
||
async with client:
|
||
invoices = await client.search_invoices(
|
||
company_id=company['id'],
|
||
jwt_token=jwt_token,
|
||
filters=filters if filters else None
|
||
)
|
||
|
||
# Format response
|
||
from backend.modules.telegram.bot.formatters import format_invoices_response
|
||
response = format_invoices_response(invoices, company['name'])
|
||
|
||
# FAZA 3: Add action buttons
|
||
from backend.modules.telegram.bot.menus import create_action_buttons
|
||
keyboard = create_action_buttons("facturi", show_export=True)
|
||
|
||
await update.message.reply_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in facturi_command: {e}", exc_info=True)
|
||
await update.message.reply_text("Eroare la incarcarea facturilor.")
|
||
|
||
|
||
async def trezorerie_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle /trezorerie command - shows total treasury (casa + banca).
|
||
|
||
Afișează sold total trezorerie cu defalcare și butoane pentru detalii.
|
||
"""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
|
||
logger.info(f"/trezorerie command from user {telegram_user_id}")
|
||
|
||
# Check linked
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
if not is_linked:
|
||
await update.message.reply_text(
|
||
"**Cont neconectat**\n\nFoloseste /start",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Get active company
|
||
session_manager = get_session_manager()
|
||
from backend.modules.telegram.bot.helpers import get_active_company_or_prompt
|
||
company = await get_active_company_or_prompt(update, session_manager, telegram_user_id)
|
||
|
||
if not company:
|
||
return
|
||
|
||
# Get auth data
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
# ✅ MODIFICARE: Folosim treasury_breakdown_split ca în Casa/Banca
|
||
from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split
|
||
treasury_data = await get_treasury_breakdown_split(
|
||
company_id=company['id'],
|
||
jwt_token=jwt_token
|
||
)
|
||
|
||
if not treasury_data:
|
||
await update.message.reply_text("Eroare la incarcarea trezoreriei.")
|
||
return
|
||
|
||
# Extract cache metadata
|
||
cache_hit = treasury_data.get('cache_hit', False)
|
||
response_time_ms = treasury_data.get('response_time_ms', 0)
|
||
cache_source = treasury_data.get('cache_source', None)
|
||
|
||
# Format combined response (casa + banca) - rotunjit la leu (0 zecimale)
|
||
casa_total = round(treasury_data['casa']['total'])
|
||
banca_total = round(treasury_data['banca']['total'])
|
||
total_treasury = casa_total + banca_total
|
||
|
||
content = f"**Sold Total:** {total_treasury:,} RON\n\n"
|
||
content += f"**Defalcare:**\n"
|
||
content += f" - Casa: {casa_total:,} RON\n"
|
||
content += f" - Banca: {banca_total:,} RON\n\n"
|
||
content += "Foloseste butoanele pentru detalii:"
|
||
|
||
# Apply company header formatting
|
||
from backend.modules.telegram.bot.menus import format_response_with_company
|
||
text = format_response_with_company(content, company['name'])
|
||
|
||
# Add performance footer
|
||
if response_time_ms > 0:
|
||
from backend.modules.telegram.bot.formatters import add_performance_footer
|
||
text = add_performance_footer(text, cache_hit, response_time_ms, cache_source)
|
||
|
||
# Add buttons to view details
|
||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||
keyboard = InlineKeyboardMarkup([
|
||
[
|
||
InlineKeyboardButton("Detalii Casa", callback_data="menu:casa"),
|
||
InlineKeyboardButton("Detalii Banca", callback_data="menu:banca")
|
||
],
|
||
[
|
||
InlineKeyboardButton("Meniu Principal", callback_data="action:menu")
|
||
]
|
||
])
|
||
|
||
await update.message.reply_text(
|
||
text,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in trezorerie_command: {e}", exc_info=True)
|
||
await update.message.reply_text("Eroare la incarcarea trezoreriei.")
|
||
|
||
|
||
async def menu_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle /menu command - shows main menu with interactive buttons.
|
||
|
||
Displays the main menu with 6 financial options organized in a 2-column layout.
|
||
Requires user to be linked and have an active company selected.
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
logger.info(f"/menu command from user {telegram_user_id}")
|
||
|
||
# Check if user is linked
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
if not is_linked:
|
||
await update.message.reply_text(
|
||
"**Cont neconectat**\n\nFoloseste /start",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Get active company
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
|
||
# Get company data for menu
|
||
company_name = company['name'] if company else None
|
||
company_cui = company.get('cui') if company else None
|
||
|
||
# Get cache status for user
|
||
cache_enabled = None
|
||
try:
|
||
from backend.modules.telegram.api.client import get_backend_client
|
||
client = get_backend_client()
|
||
async with client:
|
||
cache_stats = await client.get_cache_stats(jwt_token=auth_data['jwt_token'])
|
||
cache_enabled = cache_stats.get('user_enabled', True)
|
||
except Exception as e:
|
||
logger.warning(f"Could not get cache status: {e}")
|
||
|
||
# Create main menu (user is authenticated if they passed the is_linked check)
|
||
from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message
|
||
keyboard = create_main_menu(company_name, company_cui, is_authenticated=True, cache_enabled=cache_enabled)
|
||
menu_text = get_menu_message(company_name, company_cui)
|
||
|
||
await update.message.reply_text(
|
||
menu_text,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in menu_command: {e}", exc_info=True)
|
||
await update.message.reply_text("Eroare la afisarea meniului.")
|
||
|
||
|
||
async def trezorerie_casa_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle /trezorerie_casa command - shows cash treasury data.
|
||
|
||
Displays treasury data for cash accounts only (Casa).
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
logger.info(f"/trezorerie_casa command from user {telegram_user_id}")
|
||
|
||
# Check linked
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
if not is_linked:
|
||
await update.message.reply_text(
|
||
"**Cont neconectat**\n\nFoloseste /start",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Get active company
|
||
session_manager = get_session_manager()
|
||
from backend.modules.telegram.bot.helpers import get_active_company_or_prompt
|
||
company = await get_active_company_or_prompt(update, session_manager, telegram_user_id)
|
||
|
||
if not company:
|
||
return # Prompt already sent
|
||
|
||
# Get auth data
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
# Get treasury breakdown split
|
||
from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split
|
||
treasury_data = await get_treasury_breakdown_split(
|
||
company_id=company['id'],
|
||
jwt_token=jwt_token
|
||
)
|
||
|
||
if not treasury_data:
|
||
await update.message.reply_text("Eroare la incarcarea trezoreriei cash.")
|
||
return
|
||
|
||
# Format response
|
||
from backend.modules.telegram.bot.formatters import format_treasury_casa_response, add_performance_footer
|
||
from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company
|
||
|
||
content = format_treasury_casa_response(treasury_data['casa'])
|
||
response = format_response_with_company(content, company['name'])
|
||
|
||
# Add performance footer if cache metadata is available
|
||
if 'cache_hit' in treasury_data and 'response_time_ms' in treasury_data:
|
||
cache_hit = treasury_data['cache_hit']
|
||
response_time_ms = treasury_data['response_time_ms']
|
||
cache_source = treasury_data.get('cache_source', None)
|
||
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
||
|
||
keyboard = create_action_buttons("casa", show_export=True)
|
||
|
||
await update.message.reply_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in trezorerie_casa_command: {e}", exc_info=True)
|
||
await update.message.reply_text("Eroare la incarcarea trezoreriei cash.")
|
||
|
||
|
||
async def trezorerie_banca_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle /trezorerie_banca command - shows bank treasury data.
|
||
|
||
Displays treasury data for bank accounts only (Banca).
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
logger.info(f"/trezorerie_banca command from user {telegram_user_id}")
|
||
|
||
# Check linked
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
if not is_linked:
|
||
await update.message.reply_text(
|
||
"**Cont neconectat**\n\nFoloseste /start",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Get active company
|
||
session_manager = get_session_manager()
|
||
from backend.modules.telegram.bot.helpers import get_active_company_or_prompt
|
||
company = await get_active_company_or_prompt(update, session_manager, telegram_user_id)
|
||
|
||
if not company:
|
||
return # Prompt already sent
|
||
|
||
# Get auth data
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
# Get treasury breakdown split
|
||
from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split
|
||
treasury_data = await get_treasury_breakdown_split(
|
||
company_id=company['id'],
|
||
jwt_token=jwt_token
|
||
)
|
||
|
||
if not treasury_data:
|
||
await update.message.reply_text("Eroare la incarcarea trezoreriei bancare.")
|
||
return
|
||
|
||
# Format response
|
||
from backend.modules.telegram.bot.formatters import format_treasury_banca_response, add_performance_footer
|
||
from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company
|
||
|
||
content = format_treasury_banca_response(treasury_data['banca'])
|
||
response = format_response_with_company(content, company['name'])
|
||
|
||
# Add performance footer if cache metadata is available
|
||
if 'cache_hit' in treasury_data and 'response_time_ms' in treasury_data:
|
||
cache_hit = treasury_data['cache_hit']
|
||
response_time_ms = treasury_data['response_time_ms']
|
||
cache_source = treasury_data.get('cache_source', None)
|
||
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
||
|
||
keyboard = create_action_buttons("banca", show_export=True)
|
||
|
||
await update.message.reply_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in trezorerie_banca_command: {e}", exc_info=True)
|
||
await update.message.reply_text("Eroare la incarcarea trezoreriei bancare.")
|
||
|
||
|
||
async def clienti_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle /clienti command - shows clients balance with maturity breakdown.
|
||
|
||
Displays total clients balance, in-term and overdue amounts, and list of clients
|
||
with interactive buttons to view details.
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
logger.info(f"/clienti command from user {telegram_user_id}")
|
||
|
||
# Check linked
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
if not is_linked:
|
||
await update.message.reply_text(
|
||
"**Cont neconectat**\n\nFoloseste /start",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Get active company
|
||
session_manager = get_session_manager()
|
||
from backend.modules.telegram.bot.helpers import get_active_company_or_prompt
|
||
company = await get_active_company_or_prompt(update, session_manager, telegram_user_id)
|
||
|
||
if not company:
|
||
return # Prompt already sent
|
||
|
||
# Get auth data
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
# Get clients with maturity data
|
||
from backend.modules.telegram.bot.helpers import get_clients_with_maturity
|
||
clients_data = await get_clients_with_maturity(
|
||
company_id=company['id'],
|
||
jwt_token=jwt_token
|
||
)
|
||
|
||
if not clients_data:
|
||
await update.message.reply_text("Eroare la incarcarea datelor clienti.")
|
||
return
|
||
|
||
# Extract cache metadata
|
||
cache_hit = clients_data.get('cache_hit', False)
|
||
response_time_ms = clients_data.get('response_time_ms', 0)
|
||
cache_source = clients_data.get('cache_source', None)
|
||
|
||
# Format response
|
||
from backend.modules.telegram.bot.formatters import format_clients_balance_response, add_performance_footer
|
||
from backend.modules.telegram.bot.menus import create_client_list_keyboard, format_response_with_company
|
||
|
||
content = format_clients_balance_response(
|
||
clients_data['clients'],
|
||
clients_data['maturity']
|
||
)
|
||
response = format_response_with_company(content, company['name'])
|
||
|
||
# Add performance footer
|
||
if response_time_ms > 0:
|
||
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
||
|
||
keyboard = create_client_list_keyboard(clients_data['clients'], page=0)
|
||
|
||
await update.message.reply_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in clienti_command: {e}", exc_info=True)
|
||
await update.message.reply_text("Eroare la incarcarea datelor clienti.")
|
||
|
||
|
||
async def furnizori_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle /furnizori command - shows suppliers balance with maturity breakdown.
|
||
|
||
Displays total suppliers balance, in-term and overdue amounts, and list of suppliers
|
||
with interactive buttons to view details.
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
logger.info(f"/furnizori command from user {telegram_user_id}")
|
||
|
||
# Check linked
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
if not is_linked:
|
||
await update.message.reply_text(
|
||
"**Cont neconectat**\n\nFoloseste /start",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Get active company
|
||
session_manager = get_session_manager()
|
||
from backend.modules.telegram.bot.helpers import get_active_company_or_prompt
|
||
company = await get_active_company_or_prompt(update, session_manager, telegram_user_id)
|
||
|
||
if not company:
|
||
return # Prompt already sent
|
||
|
||
# Get auth data
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
# Get suppliers with maturity data
|
||
from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity
|
||
suppliers_data = await get_suppliers_with_maturity(
|
||
company_id=company['id'],
|
||
jwt_token=jwt_token
|
||
)
|
||
|
||
if not suppliers_data:
|
||
await update.message.reply_text("Eroare la incarcarea datelor furnizori.")
|
||
return
|
||
|
||
# Extract cache metadata
|
||
cache_hit = suppliers_data.get('cache_hit', False)
|
||
response_time_ms = suppliers_data.get('response_time_ms', 0)
|
||
cache_source = suppliers_data.get('cache_source', None)
|
||
|
||
# Format response
|
||
from backend.modules.telegram.bot.formatters import format_suppliers_balance_response, add_performance_footer
|
||
from backend.modules.telegram.bot.menus import create_supplier_list_keyboard, format_response_with_company
|
||
|
||
content = format_suppliers_balance_response(
|
||
suppliers_data['suppliers'],
|
||
suppliers_data['maturity']
|
||
)
|
||
response = format_response_with_company(content, company['name'])
|
||
|
||
# Add performance footer
|
||
if response_time_ms > 0:
|
||
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
||
|
||
keyboard = create_supplier_list_keyboard(suppliers_data['suppliers'], page=0)
|
||
|
||
await update.message.reply_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in furnizori_command: {e}", exc_info=True)
|
||
await update.message.reply_text("Eroare la incarcarea datelor furnizori.")
|
||
|
||
|
||
async def evolutie_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle /evolutie command - shows cash flow evolution (collections/payments).
|
||
|
||
Displays performance data and monthly cash flow trends for collections and payments.
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
telegram_user_id = update.effective_user.id
|
||
logger.info(f"/evolutie command from user {telegram_user_id}")
|
||
|
||
# Check linked
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
if not is_linked:
|
||
await update.message.reply_text(
|
||
"**Cont neconectat**\n\nFoloseste /start",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Get active company
|
||
session_manager = get_session_manager()
|
||
from backend.modules.telegram.bot.helpers import get_active_company_or_prompt
|
||
company = await get_active_company_or_prompt(update, session_manager, telegram_user_id)
|
||
|
||
if not company:
|
||
return # Prompt already sent
|
||
|
||
# Get auth data
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
# Get cash flow evolution data
|
||
from backend.modules.telegram.bot.helpers import get_cashflow_evolution_data
|
||
evolution_data = await get_cashflow_evolution_data(
|
||
company_id=company['id'],
|
||
jwt_token=jwt_token
|
||
)
|
||
|
||
if not evolution_data:
|
||
await update.message.reply_text("Eroare la incarcarea datelor evolutie.")
|
||
return
|
||
|
||
# Format response
|
||
from backend.modules.telegram.bot.formatters import format_cashflow_evolution_response, add_performance_footer
|
||
from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company
|
||
|
||
content = format_cashflow_evolution_response(
|
||
evolution_data['performance'],
|
||
evolution_data['monthly']
|
||
)
|
||
response = format_response_with_company(content, company['name'])
|
||
|
||
# Add performance footer if cache metadata is available
|
||
if 'cache_hit' in evolution_data and 'response_time_ms' in evolution_data:
|
||
cache_hit = evolution_data['cache_hit']
|
||
response_time_ms = evolution_data['response_time_ms']
|
||
cache_source = evolution_data.get('cache_source', None)
|
||
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
||
|
||
keyboard = create_action_buttons("evolutie", show_export=False, show_refresh=False)
|
||
|
||
await update.message.reply_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in evolutie_command: {e}", exc_info=True)
|
||
await update.message.reply_text("Eroare la incarcarea datelor evolutie.")
|
||
|
||
|
||
# ============================================================================
|
||
# TEXT MESSAGE HANDLERS
|
||
# ============================================================================
|
||
|
||
async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle regular text messages.
|
||
|
||
Automatically detects and processes linking codes when user sends
|
||
a text that matches the code format (8 alphanumeric characters).
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
telegram_user = update.effective_user
|
||
telegram_user_id = telegram_user.id
|
||
text = update.message.text.strip()
|
||
|
||
logger.info(f"Text message from user {telegram_user_id}")
|
||
|
||
# Check if user is awaiting password for server switch
|
||
pending_server_id = context.user_data.get('pending_switch_server_id')
|
||
if pending_server_id:
|
||
# Șterge IMEDIAT mesajul cu parola (securitate)
|
||
try:
|
||
await update.message.delete()
|
||
except Exception as e:
|
||
logger.warning(f"Could not delete password message: {e}")
|
||
|
||
oracle_password = text
|
||
jwt_token = context.user_data.pop('pending_switch_jwt_token', None)
|
||
username = context.user_data.pop('pending_switch_username', None)
|
||
context.user_data.pop('pending_switch_server_id', None)
|
||
|
||
if not jwt_token or not username:
|
||
await update.effective_chat.send_message("Sesiune expirată. Încearcă din nou.")
|
||
return
|
||
|
||
await update.effective_chat.send_message("Se verifică parola și se schimbă serverul...")
|
||
|
||
client = get_backend_client()
|
||
async with client:
|
||
result = await client.switch_server(
|
||
jwt_token=jwt_token,
|
||
oracle_username=username,
|
||
new_server_id=pending_server_id,
|
||
oracle_password=oracle_password
|
||
)
|
||
|
||
if not result.get('success'):
|
||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||
await update.effective_chat.send_message(
|
||
f"❌ {result.get('message', 'Eroare la schimbarea serverului')}\n\nReîncearcă cu /menu → Schimbă server.",
|
||
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("« Meniu", callback_data="action:menu")]])
|
||
)
|
||
return
|
||
|
||
# Salvează noul JWT în SQLite
|
||
from datetime import datetime, timedelta
|
||
token_expires_at = datetime.now() + timedelta(minutes=30)
|
||
await link_user_to_oracle(
|
||
telegram_user_id=telegram_user_id,
|
||
oracle_username=result.get('username', username),
|
||
jwt_token=result['access_token'],
|
||
jwt_refresh_token=result['refresh_token'],
|
||
token_expires_at=token_expires_at
|
||
)
|
||
|
||
# Curăță compania din sesiune — aparținea serverului vechi
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
session.clear_active_company()
|
||
await session_manager.save_session(telegram_user_id)
|
||
|
||
try:
|
||
from backend.config import settings
|
||
srv = settings.get_oracle_server(pending_server_id)
|
||
srv_display = srv.name if srv else pending_server_id
|
||
except Exception:
|
||
srv_display = pending_server_id
|
||
|
||
await update.effective_chat.send_message(f"✅ Server schimbat: **{srv_display}**\nSelectează firma...", parse_mode=ParseMode.MARKDOWN)
|
||
|
||
await _handle_selectcompany_view(
|
||
query_or_update=update,
|
||
telegram_user_id=telegram_user_id,
|
||
jwt_token=result['access_token'],
|
||
is_callback=False,
|
||
page=0,
|
||
search_term=""
|
||
)
|
||
return
|
||
|
||
# Check if user is already linked
|
||
is_linked = await check_user_linked(telegram_user_id)
|
||
|
||
if is_linked:
|
||
# User is already linked - ignore text messages
|
||
# (could add natural language processing here in the future)
|
||
return
|
||
|
||
text = text.upper() # Only uppercase for linking code check
|
||
|
||
# User is NOT linked - check if text looks like a linking code
|
||
# Linking codes are exactly 8 alphanumeric characters
|
||
if len(text) == 8 and text.isalnum():
|
||
logger.info(f"Detected potential linking code: {text} from user {telegram_user_id}")
|
||
|
||
# ȘTERGE mesajul utilizatorului cu codul (chat curat)
|
||
try:
|
||
await update.message.delete()
|
||
except Exception as e:
|
||
logger.warning(f"Could not delete code message: {e}")
|
||
|
||
# Check dacă există mesaj de editat din "Login din Web App"
|
||
web_login_msg_id = context.user_data.get('web_login_message_id')
|
||
|
||
if web_login_msg_id:
|
||
# EDITEAZĂ mesajul existent cu instrucțiunile
|
||
try:
|
||
await context.bot.edit_message_text(
|
||
chat_id=update.effective_chat.id,
|
||
message_id=web_login_msg_id,
|
||
text="Conectare cont...",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
except Exception as e:
|
||
logger.warning(f"Could not edit web_login message: {e}")
|
||
# Fallback: creează mesaj nou
|
||
web_login_msg_id = None
|
||
|
||
# Dacă nu există mesaj de editat, creează unul nou
|
||
if not web_login_msg_id:
|
||
linking_msg = await update.effective_chat.send_message(
|
||
"Conectare cont...",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
web_login_msg_id = linking_msg.message_id
|
||
|
||
# Attempt linking
|
||
result = await link_telegram_account(telegram_user, text)
|
||
|
||
if result:
|
||
# Success!
|
||
username = result['username']
|
||
# Show main menu with buttons for newly linked user
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
company_name = company['name'] if company else None
|
||
company_cui = company.get('cui') if company else None
|
||
|
||
from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons
|
||
keyboard = create_main_menu(company_name, company_cui, is_authenticated=True)
|
||
|
||
# Create menu text
|
||
if company_name:
|
||
menu_text = f"{company_name}"
|
||
else:
|
||
menu_text = "Selectează o companie pentru a continua"
|
||
|
||
menu_message = pad_message_for_wide_buttons(menu_text)
|
||
|
||
# EDIT the same message to show menu (no new messages!)
|
||
await context.bot.edit_message_text(
|
||
chat_id=update.effective_chat.id,
|
||
message_id=web_login_msg_id,
|
||
text=menu_message,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
logger.info(f"User {telegram_user_id} successfully linked to {username} via direct code input")
|
||
else:
|
||
# Failed linking - EDIT the same message to show error
|
||
await context.bot.edit_message_text(
|
||
chat_id=update.effective_chat.id,
|
||
message_id=web_login_msg_id,
|
||
text="Cod invalid sau expirat\n\n"
|
||
"Generează un cod nou din aplicația web și trimite-l direct.\n\n"
|
||
"Codul expiră în 15 minute.",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
logger.warning(f"Failed to link user {telegram_user_id} with direct code: {text}")
|
||
else:
|
||
# Text doesn't look like a linking code
|
||
# Show helpful message
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("Cum obtin codul de link?", callback_data="login_help")],
|
||
[InlineKeyboardButton("Am deja cod - Linkez contul", callback_data="login_prompt")]
|
||
])
|
||
|
||
await update.message.reply_text(
|
||
"**Salut**\n\n"
|
||
"Pentru a folosi bot-ul, conecteaza contul tau ROA2WEB.\n\n"
|
||
"Codul are exact 8 caractere (exemplu: ABC12XYZ)\n\n"
|
||
"Alege o optiune:",
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in handle_text_message: {e}", exc_info=True)
|
||
await update.message.reply_text(
|
||
"A aparut o eroare. Te rog incearca din nou."
|
||
)
|
||
|
||
|
||
# ============================================================================
|
||
# CALLBACK QUERY HANDLERS (for inline buttons)
|
||
# ============================================================================
|
||
|
||
async def handle_menu_callback(query, telegram_user_id: int, callback_data: str):
|
||
"""
|
||
Handle main menu button clicks.
|
||
|
||
Callback format: menu:{action}
|
||
Actions: sold, casa, banca, clienti, furnizori, evolutie, select_company
|
||
|
||
Args:
|
||
query: CallbackQuery object
|
||
telegram_user_id: Telegram user ID
|
||
callback_data: Callback data string
|
||
"""
|
||
action = callback_data.split(":")[1]
|
||
|
||
# Get auth data
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
|
||
# If user is not authenticated and trying to access financial data, show auth required message
|
||
if auth_data is None and action != "select_company":
|
||
from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons
|
||
keyboard = create_main_menu(company_name=None, company_cui=None, is_authenticated=False)
|
||
menu_text = pad_message_for_wide_buttons(
|
||
"⚠️ **Autentificare necesară**\n\n"
|
||
"Pentru a accesa date financiare,\n"
|
||
"apasă **Login** și urmează instrucțiunile."
|
||
)
|
||
|
||
await query.edit_message_text(
|
||
menu_text,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# If action is select_company and user is not authenticated, allow it (will show empty list or error)
|
||
if action == "select_company" and auth_data is None:
|
||
await query.answer("Pentru a vedea companiile, trebuie să te autentifici mai întâi.", show_alert=True)
|
||
return
|
||
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
# Get active company
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
|
||
if not company and action != "select_company":
|
||
# Get companies and show selection directly
|
||
client = get_backend_client()
|
||
async with client:
|
||
companies = await client.get_user_companies(jwt_token=jwt_token)
|
||
|
||
if not companies:
|
||
await query.edit_message_text(
|
||
"Nu ai acces la nicio companie.\n"
|
||
"Contacteaza administratorul.",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
from backend.modules.telegram.bot.helpers import create_company_selection_keyboard_paginated
|
||
keyboard = create_company_selection_keyboard_paginated(companies, page=0)
|
||
|
||
await query.edit_message_text(
|
||
f"**Selecteaza mai intai o companie**\n\n"
|
||
f"Companiile tale ({len(companies)}):",
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Route to appropriate handler
|
||
if action == "sold":
|
||
# ✅ MODIFICARE: Folosim funcția comună
|
||
await _handle_sold_view(
|
||
query_or_update=query,
|
||
telegram_user_id=telegram_user_id,
|
||
company=company,
|
||
jwt_token=jwt_token,
|
||
is_callback=True
|
||
)
|
||
|
||
elif action == "trezorerie":
|
||
# Trezorerie unified (Casa + Banca combined)
|
||
from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split
|
||
treasury_data = await get_treasury_breakdown_split(company['id'], jwt_token)
|
||
|
||
from backend.modules.telegram.bot.formatters import format_treasury_combined_response, add_performance_footer
|
||
from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company
|
||
|
||
content = format_treasury_combined_response(treasury_data)
|
||
response = format_response_with_company(content, company['name'])
|
||
|
||
# Add performance footer if cache metadata is available
|
||
if 'cache_hit' in treasury_data and 'response_time_ms' in treasury_data:
|
||
cache_hit = treasury_data['cache_hit']
|
||
response_time_ms = treasury_data['response_time_ms']
|
||
cache_source = treasury_data.get('cache_source', None)
|
||
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
||
|
||
keyboard = create_action_buttons("trezorerie", show_export=False, show_refresh=False)
|
||
|
||
try:
|
||
await query.edit_message_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
except Exception as e:
|
||
# Ignore "Message is not modified" error
|
||
if "Message is not modified" not in str(e):
|
||
raise
|
||
|
||
elif action == "casa":
|
||
# Trezorerie casa
|
||
from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split
|
||
treasury_data = await get_treasury_breakdown_split(company['id'], jwt_token)
|
||
|
||
from backend.modules.telegram.bot.formatters import format_treasury_casa_response, add_performance_footer
|
||
from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company
|
||
|
||
content = format_treasury_casa_response(treasury_data['casa'])
|
||
response = format_response_with_company(content, company['name'])
|
||
|
||
# Add performance footer if cache metadata is available
|
||
if 'cache_hit' in treasury_data and 'response_time_ms' in treasury_data:
|
||
cache_hit = treasury_data['cache_hit']
|
||
response_time_ms = treasury_data['response_time_ms']
|
||
cache_source = treasury_data.get('cache_source', None)
|
||
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
||
|
||
keyboard = create_action_buttons("casa", show_export=False, show_refresh=False)
|
||
|
||
try:
|
||
await query.edit_message_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
except Exception as e:
|
||
# Ignore "Message is not modified" error
|
||
if "Message is not modified" not in str(e):
|
||
raise
|
||
|
||
elif action == "banca":
|
||
# Trezorerie banca
|
||
from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split
|
||
treasury_data = await get_treasury_breakdown_split(company['id'], jwt_token)
|
||
|
||
from backend.modules.telegram.bot.formatters import format_treasury_banca_response, add_performance_footer
|
||
from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company
|
||
|
||
content = format_treasury_banca_response(treasury_data['banca'])
|
||
response = format_response_with_company(content, company['name'])
|
||
|
||
# Add performance footer if cache metadata is available
|
||
if 'cache_hit' in treasury_data and 'response_time_ms' in treasury_data:
|
||
cache_hit = treasury_data['cache_hit']
|
||
response_time_ms = treasury_data['response_time_ms']
|
||
cache_source = treasury_data.get('cache_source', None)
|
||
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
||
|
||
keyboard = create_action_buttons("banca", show_export=False, show_refresh=False)
|
||
|
||
try:
|
||
await query.edit_message_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
except Exception as e:
|
||
# Ignore "Message is not modified" error
|
||
if "Message is not modified" not in str(e):
|
||
raise
|
||
|
||
elif action == "clienti":
|
||
# Sold clienți + listă cu paginare
|
||
from backend.modules.telegram.bot.helpers import get_clients_with_maturity
|
||
clients_data = await get_clients_with_maturity(company['id'], jwt_token)
|
||
|
||
from backend.modules.telegram.bot.formatters import format_clients_balance_response, add_performance_footer
|
||
from backend.modules.telegram.bot.menus import create_client_list_keyboard, format_response_with_company
|
||
|
||
content = format_clients_balance_response(
|
||
clients_data['clients'],
|
||
clients_data['maturity']
|
||
)
|
||
response = format_response_with_company(content, company['name'])
|
||
|
||
# Add performance footer if cache metadata is available
|
||
if 'cache_hit' in clients_data and 'response_time_ms' in clients_data:
|
||
cache_hit = clients_data['cache_hit']
|
||
response_time_ms = clients_data['response_time_ms']
|
||
cache_source = clients_data.get('cache_source', None)
|
||
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
||
|
||
keyboard = create_client_list_keyboard(clients_data['clients'], page=0)
|
||
|
||
await query.edit_message_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif action == "furnizori":
|
||
# Sold furnizori + listă cu paginare
|
||
from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity
|
||
suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token)
|
||
|
||
from backend.modules.telegram.bot.formatters import format_suppliers_balance_response, add_performance_footer
|
||
from backend.modules.telegram.bot.menus import create_supplier_list_keyboard, format_response_with_company
|
||
|
||
content = format_suppliers_balance_response(
|
||
suppliers_data['suppliers'],
|
||
suppliers_data['maturity']
|
||
)
|
||
response = format_response_with_company(content, company['name'])
|
||
|
||
# Add performance footer if cache metadata is available
|
||
if 'cache_hit' in suppliers_data and 'response_time_ms' in suppliers_data:
|
||
cache_hit = suppliers_data['cache_hit']
|
||
response_time_ms = suppliers_data['response_time_ms']
|
||
cache_source = suppliers_data.get('cache_source', None)
|
||
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
||
|
||
keyboard = create_supplier_list_keyboard(suppliers_data['suppliers'], page=0)
|
||
|
||
await query.edit_message_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif action == "evolutie":
|
||
# Evoluție cash flow
|
||
from backend.modules.telegram.bot.helpers import get_cashflow_evolution_data
|
||
evolution_data = await get_cashflow_evolution_data(company['id'], jwt_token)
|
||
|
||
from backend.modules.telegram.bot.formatters import format_cashflow_evolution_response, add_performance_footer
|
||
from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company
|
||
|
||
content = format_cashflow_evolution_response(
|
||
evolution_data['performance'],
|
||
evolution_data['monthly']
|
||
)
|
||
response = format_response_with_company(content, company['name'])
|
||
|
||
# Add performance footer if cache metadata is available
|
||
if 'cache_hit' in evolution_data and 'response_time_ms' in evolution_data:
|
||
cache_hit = evolution_data['cache_hit']
|
||
response_time_ms = evolution_data['response_time_ms']
|
||
cache_source = evolution_data.get('cache_source', None)
|
||
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
||
|
||
keyboard = create_action_buttons("evolutie", show_export=False, show_refresh=False)
|
||
|
||
await query.edit_message_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif action == "togglecache":
|
||
# Toggle cache pentru user
|
||
try:
|
||
client = get_backend_client()
|
||
async with client:
|
||
cache_stats = await client.get_cache_stats(jwt_token=jwt_token)
|
||
user_enabled = cache_stats.get('user_enabled', True)
|
||
|
||
# Create toggle buttons
|
||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton(
|
||
"✅ Activează" if not user_enabled else "❌ Dezactivează",
|
||
callback_data=f"cache_toggle:{'on' if not user_enabled else 'off'}"
|
||
)
|
||
],
|
||
[InlineKeyboardButton("« Înapoi la Meniu", callback_data="action:menu")]
|
||
]
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
|
||
status = "ACTIVAT" if user_enabled else "DEZACTIVAT"
|
||
message = f"**Cache Status**\n\nCurent: {status}\n\n"
|
||
|
||
if user_enabled:
|
||
message += "Vrei să dezactivezi cache-ul temporar?\nFolosește pentru teste de performanță."
|
||
else:
|
||
message += "Cache-ul este dezactivat.\nToate queries merg direct la Oracle."
|
||
|
||
await query.edit_message_text(
|
||
message,
|
||
reply_markup=reply_markup,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Toggle cache menu error: {e}", exc_info=True)
|
||
await query.answer("Eroare la obținerea status cache.", show_alert=True)
|
||
|
||
elif action == "clearcache":
|
||
# Clear cache
|
||
try:
|
||
# Create inline keyboard
|
||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("Toate companiile", callback_data="cache_clear:all"),
|
||
InlineKeyboardButton("Doar compania mea", callback_data="cache_clear:current")
|
||
],
|
||
[InlineKeyboardButton("« Înapoi la Meniu", callback_data="action:menu")]
|
||
]
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
|
||
message = "**🔄 Invalidare Cache**\n\n"
|
||
if company:
|
||
message += f"Compania curentă: {company['name']}\n\n"
|
||
message += "Alege scope:"
|
||
|
||
await query.edit_message_text(
|
||
message,
|
||
reply_markup=reply_markup,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Clear cache menu error: {e}", exc_info=True)
|
||
await query.answer("Eroare la afișarea opțiuni cache.", show_alert=True)
|
||
|
||
elif action == "select_company":
|
||
# ✅ MODIFICARE: Folosim funcția comună
|
||
await _handle_selectcompany_view(
|
||
query_or_update=query,
|
||
telegram_user_id=telegram_user_id,
|
||
jwt_token=jwt_token,
|
||
is_callback=True,
|
||
page=0,
|
||
search_term=""
|
||
)
|
||
|
||
elif action == "switch_server":
|
||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||
from shared.auth.email_server_cache import email_server_cache
|
||
from backend.modules.telegram.bot.menus import pad_message_for_wide_buttons
|
||
|
||
username = auth_data['username']
|
||
|
||
try:
|
||
servers = await email_server_cache.get_servers_for_username(username)
|
||
except Exception as e:
|
||
logger.error(f"Could not get servers for {username}: {e}")
|
||
await query.answer("Eroare la obținerea serverelor.", show_alert=True)
|
||
return
|
||
|
||
if len(servers) <= 1:
|
||
await query.answer("Ești pe singurul server disponibil.", show_alert=True)
|
||
return
|
||
|
||
# Build server selection keyboard
|
||
try:
|
||
from backend.config import settings
|
||
keyboard_rows = []
|
||
for srv_id in servers:
|
||
srv = settings.get_oracle_server(srv_id)
|
||
srv_name = srv.name if srv else srv_id
|
||
keyboard_rows.append([InlineKeyboardButton(srv_name, callback_data=f"switch_server_confirm:{srv_id}")])
|
||
except Exception:
|
||
keyboard_rows = [[InlineKeyboardButton(s, callback_data=f"switch_server_confirm:{s}")] for s in servers]
|
||
|
||
keyboard_rows.append([InlineKeyboardButton("« Înapoi", callback_data="action:menu")])
|
||
|
||
await query.edit_message_text(
|
||
pad_message_for_wide_buttons(f"Selectează serverul Oracle:\n\nUtilizator: {username}"),
|
||
reply_markup=InlineKeyboardMarkup(keyboard_rows),
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
|
||
async def handle_action_callback(query, telegram_user_id: int, callback_data: str):
|
||
"""
|
||
Handle action button clicks (Refresh, Export, Menu, Help, Login, Logout).
|
||
|
||
Callback format: action:{type}:{view}
|
||
Types: refresh, export, menu, help, login, logout
|
||
|
||
Args:
|
||
query: CallbackQuery object
|
||
telegram_user_id: Telegram user ID
|
||
callback_data: Callback data string
|
||
"""
|
||
parts = callback_data.split(":")
|
||
action_type = parts[1]
|
||
|
||
if action_type == "menu":
|
||
# Back to main menu
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
|
||
# Check if user is authenticated
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
is_authenticated = auth_data is not None
|
||
|
||
# Get cache status for user
|
||
cache_enabled = None
|
||
if is_authenticated:
|
||
try:
|
||
from backend.modules.telegram.api.client import get_backend_client
|
||
client = get_backend_client()
|
||
async with client:
|
||
cache_stats = await client.get_cache_stats(jwt_token=auth_data['jwt_token'])
|
||
cache_enabled = cache_stats.get('user_enabled', True)
|
||
except Exception as e:
|
||
logger.warning(f"Could not get cache status: {e}")
|
||
|
||
from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message
|
||
company_name = company['name'] if company else None
|
||
company_cui = company.get('cui') if company else None
|
||
keyboard = create_main_menu(company_name, company_cui, is_authenticated, cache_enabled)
|
||
menu_text = get_menu_message(company_name, company_cui)
|
||
|
||
await query.edit_message_text(
|
||
menu_text,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif action_type == "refresh":
|
||
# Refresh current view
|
||
view = parts[2] if len(parts) > 2 else "sold"
|
||
|
||
# Check if it's a detail view (client_detail:name or supplier_detail:name)
|
||
if view.startswith("client_detail:"):
|
||
entity_name = view.split(":", 1)[1] # Extract entity name
|
||
# Limit name to 40 chars for Telegram callback_data limit (64 bytes)
|
||
safe_name = entity_name[:40] if len(entity_name) > 40 else entity_name
|
||
await handle_details_callback(query, telegram_user_id, f"details:client:{safe_name}:0")
|
||
elif view.startswith("supplier_detail:"):
|
||
entity_name = view.split(":", 1)[1] # Extract entity name
|
||
# Limit name to 40 chars for Telegram callback_data limit (64 bytes)
|
||
safe_name = entity_name[:40] if len(entity_name) > 40 else entity_name
|
||
await handle_details_callback(query, telegram_user_id, f"details:supplier:{safe_name}:0")
|
||
else:
|
||
# Regular menu view refresh
|
||
await handle_menu_callback(query, telegram_user_id, f"menu:{view}")
|
||
|
||
elif action_type == "export":
|
||
# Export functionality (placeholder for now)
|
||
await query.answer("Functia de export va fi disponibila in curand", show_alert=True)
|
||
|
||
elif action_type == "help":
|
||
# Show help message above menu (edit current message)
|
||
from backend.modules.telegram.bot.menus import pad_message_for_wide_buttons, create_main_menu
|
||
|
||
# Get auth status and company info
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
is_authenticated = auth_data is not None
|
||
|
||
company_name = company['name'] if company else None
|
||
company_cui = company.get('cui') if company else None
|
||
|
||
keyboard = create_main_menu(company_name, company_cui, is_authenticated)
|
||
|
||
help_text = pad_message_for_wide_buttons(
|
||
"**Ghid Rapid**\n\n"
|
||
"**Selectare Companie** - Alege compania activă\n\n"
|
||
"**Sold Companie** - Dashboard financiar complet\n"
|
||
"**Trezorerie Casa** - Situație conturi cash\n"
|
||
"**Trezorerie Banca** - Situație conturi bancare\n"
|
||
"**Sold Clienti** - Clienți + facturi neplătite\n"
|
||
"**Sold Furnizori** - Furnizori + facturi\n"
|
||
"**Evolutie Incasari** - Trend lunar încasări\n\n"
|
||
"**Logout** - Deconectează contul\n\n"
|
||
"_Toate datele sunt în timp real din Oracle._"
|
||
)
|
||
|
||
await query.edit_message_text(
|
||
help_text,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif action_type == "logout":
|
||
# Show logout confirmation
|
||
from backend.modules.telegram.bot.menus import pad_message_for_wide_buttons
|
||
confirmation_text = pad_message_for_wide_buttons(
|
||
"**Confirmare Deconectare**\n\n"
|
||
"Ești sigur că vrei să deconectezi contul?\n\n"
|
||
"Accesul la date va fi oprit.\n"
|
||
"Poți reconecta oricând cu un cod nou."
|
||
)
|
||
|
||
keyboard = InlineKeyboardMarkup([
|
||
[
|
||
InlineKeyboardButton("Da, deconectează", callback_data="logout_confirm"),
|
||
InlineKeyboardButton("Anulează", callback_data="logout_cancel")
|
||
]
|
||
])
|
||
|
||
await query.edit_message_text(
|
||
confirmation_text,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
# NOTE: action:login is handled by email_login_handler ConversationHandler
|
||
# See app/bot/email_handlers.py for the complete email authentication flow
|
||
|
||
|
||
async def handle_details_callback(query, telegram_user_id: int, callback_data: str):
|
||
"""
|
||
Handle client/supplier detail clicks.
|
||
|
||
Callback format: details:{type}:{name}:{page}
|
||
Types: client, supplier
|
||
|
||
Args:
|
||
query: CallbackQuery object
|
||
telegram_user_id: Telegram user ID
|
||
callback_data: Callback data string
|
||
"""
|
||
parts = callback_data.split(":")
|
||
detail_type = parts[1] # client or supplier
|
||
entity_name = parts[2] # client/supplier name
|
||
page = int(parts[3]) if len(parts) > 3 else 0 # invoice page number
|
||
|
||
# Get auth data and company
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
|
||
if detail_type == "client":
|
||
# Get client details (from clients list)
|
||
# entity_name might be truncated to 40 chars, so search by startswith
|
||
from backend.modules.telegram.bot.helpers import get_clients_with_maturity
|
||
clients_data = await get_clients_with_maturity(company['id'], jwt_token)
|
||
|
||
# Find client by full or partial name match
|
||
client = next((c for c in clients_data['clients'] if c['name'].startswith(entity_name)), None)
|
||
|
||
if not client:
|
||
await query.answer("Client negăsit", show_alert=True)
|
||
return
|
||
|
||
# Use FULL client name for invoice search (not truncated)
|
||
full_client_name = client['name']
|
||
|
||
# Get client invoices by FULL name
|
||
from backend.modules.telegram.bot.helpers import get_client_invoices
|
||
invoices = await get_client_invoices(company['id'], full_client_name, jwt_token)
|
||
|
||
# Format response
|
||
from backend.modules.telegram.bot.formatters import format_client_detail_response
|
||
from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company
|
||
|
||
content = format_client_detail_response(client, invoices)
|
||
response = format_response_with_company(content, company['name'])
|
||
|
||
# Use truncated name for callback_data (to stay within 64 byte limit)
|
||
safe_name = entity_name[:40] if len(entity_name) > 40 else entity_name
|
||
keyboard = create_action_buttons(f"client_detail:{safe_name}", show_export=False, show_back=True, show_refresh=False)
|
||
|
||
await query.edit_message_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif detail_type == "supplier":
|
||
# Get supplier details (from suppliers list)
|
||
# entity_name might be truncated to 40 chars, so search by startswith
|
||
from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity
|
||
suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token)
|
||
|
||
# Find supplier by full or partial name match
|
||
supplier = next((s for s in suppliers_data['suppliers'] if s['name'].startswith(entity_name)), None)
|
||
|
||
if not supplier:
|
||
await query.answer("Furnizor negăsit", show_alert=True)
|
||
return
|
||
|
||
# Use FULL supplier name for invoice search (not truncated)
|
||
full_supplier_name = supplier['name']
|
||
|
||
# Get supplier invoices by FULL name
|
||
from backend.modules.telegram.bot.helpers import get_supplier_invoices
|
||
invoices = await get_supplier_invoices(company['id'], full_supplier_name, jwt_token)
|
||
|
||
# Format response
|
||
from backend.modules.telegram.bot.formatters import format_supplier_detail_response
|
||
from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company
|
||
|
||
content = format_supplier_detail_response(supplier, invoices)
|
||
response = format_response_with_company(content, company['name'])
|
||
|
||
# Use truncated name for callback_data (to stay within 64 byte limit)
|
||
safe_name = entity_name[:40] if len(entity_name) > 40 else entity_name
|
||
keyboard = create_action_buttons(f"supplier_detail:{safe_name}", show_export=False, show_back=True, show_refresh=False)
|
||
|
||
await query.edit_message_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
|
||
async def handle_invoice_callback(query, telegram_user_id: int, callback_data: str):
|
||
"""
|
||
Handle invoice detail clicks.
|
||
|
||
Callback format: invoice:{partner_type}:{id}
|
||
|
||
Args:
|
||
query: CallbackQuery object
|
||
telegram_user_id: Telegram user ID
|
||
callback_data: Callback data string
|
||
"""
|
||
parts = callback_data.split(":")
|
||
partner_type = parts[1] # CLIENTI or FURNIZORI
|
||
invoice_id = int(parts[2])
|
||
|
||
# Get invoice details from API (placeholder for now)
|
||
await query.answer("Detalii factura (in dezvoltare)", show_alert=True)
|
||
|
||
|
||
async def handle_navigation_back(query, telegram_user_id: int, callback_data: str):
|
||
"""
|
||
Handle back navigation.
|
||
|
||
Callback format: nav:back:{location}
|
||
Locations: menu, clienti, furnizori
|
||
|
||
Args:
|
||
query: CallbackQuery object
|
||
telegram_user_id: Telegram user ID
|
||
callback_data: Callback data string
|
||
"""
|
||
location = callback_data.split(":")[2]
|
||
|
||
if location == "menu":
|
||
# Back to main menu
|
||
await handle_action_callback(query, telegram_user_id, "action:menu")
|
||
|
||
elif location == "clienti":
|
||
# Back to clients list
|
||
await handle_menu_callback(query, telegram_user_id, "menu:clienti")
|
||
|
||
elif location == "furnizori":
|
||
# Back to suppliers list
|
||
await handle_menu_callback(query, telegram_user_id, "menu:furnizori")
|
||
|
||
|
||
async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle inline button callbacks.
|
||
|
||
Callback data formats:
|
||
- login_help - Show help on how to get link code
|
||
- login_prompt - Prompt user to enter link code
|
||
- login_back - Back to welcome message
|
||
- menu:{action} - Main menu buttons
|
||
- action:{type}:{view} - Action buttons (refresh, export, menu)
|
||
- details:{type}:{id} - Client/Supplier details
|
||
- invoice:{partner_type}:{id} - Invoice details
|
||
- nav:back:{location} - Navigation back
|
||
- select_company:{id} - Company selection (existing)
|
||
- unlink_confirm/unlink_cancel - Unlink confirmation (existing)
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context
|
||
"""
|
||
try:
|
||
query = update.callback_query
|
||
callback_data = query.data
|
||
|
||
# ========== IGNORE CALLBACKS HANDLED BY CONVERSATION HANDLER ==========
|
||
# These callbacks are managed by email_login_handler ConversationHandler
|
||
# Return immediately without answering to let ConversationHandler process them
|
||
conversation_patterns = [
|
||
'action:login', # Login button from menu
|
||
'email_login', # Email login button
|
||
'web_login_info', # Web app login info button
|
||
'cancel', # Cancel button
|
||
]
|
||
|
||
# Check exact matches
|
||
if callback_data in conversation_patterns:
|
||
logger.info(f"[BUTTON_CALLBACK] Ignoring {callback_data} - handled by ConversationHandler")
|
||
return
|
||
|
||
# Check prefix matches (e.g., resend:email@example.com)
|
||
if callback_data.startswith('resend:'):
|
||
logger.info(f"[BUTTON_CALLBACK] Ignoring {callback_data} - handled by ConversationHandler")
|
||
return
|
||
|
||
# ========== PROCESS ALL OTHER CALLBACKS ==========
|
||
await query.answer()
|
||
|
||
telegram_user_id = update.effective_user.id
|
||
|
||
logger.info(f"Button callback: {callback_data} from user {telegram_user_id}")
|
||
|
||
# ========== EXISTING CALLBACKS (preserve) ==========
|
||
|
||
# Handle pagination for company selection
|
||
if callback_data.startswith("select_company_page:"):
|
||
# Extract page number
|
||
page = int(callback_data.split(":")[1])
|
||
|
||
# Get companies
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
client = get_backend_client()
|
||
async with client:
|
||
companies = await client.get_user_companies(jwt_token=jwt_token)
|
||
|
||
# Create paginated keyboard for requested page
|
||
from backend.modules.telegram.bot.helpers import create_company_selection_keyboard_paginated
|
||
keyboard = create_company_selection_keyboard_paginated(companies, page=page)
|
||
|
||
await query.edit_message_text(
|
||
f"**Selecteaza Compania**\n\n"
|
||
f"Companiile tale ({len(companies)}):",
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif callback_data == "select_company_alpha_menu":
|
||
# Show A-Z letter filter keyboard
|
||
from backend.modules.telegram.bot.helpers import create_alpha_filter_keyboard
|
||
await query.edit_message_text(
|
||
"**Selectează litera**\n\nAlege prima literă a firmei:",
|
||
reply_markup=create_alpha_filter_keyboard(),
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif callback_data.startswith("select_company_alpha:"):
|
||
# Filter companies by starting letter and show page 0
|
||
letter = callback_data.split(":", 1)[1] # "A" or "ALL"
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
client = get_backend_client()
|
||
async with client:
|
||
all_companies = await client.get_user_companies(jwt_token=jwt_token)
|
||
|
||
if letter == "ALL":
|
||
filtered = all_companies
|
||
else:
|
||
filtered = [
|
||
c for c in all_companies
|
||
if c.get('name', c.get('nume_firma', '')).upper().startswith(letter)
|
||
]
|
||
|
||
if not filtered:
|
||
await query.answer(f"Nicio firmă cu litera {letter}.", show_alert=True)
|
||
return
|
||
|
||
from backend.modules.telegram.bot.helpers import create_company_selection_keyboard_paginated
|
||
keyboard = create_company_selection_keyboard_paginated(
|
||
filtered, page=0,
|
||
back_callback="select_company_alpha_menu",
|
||
page_callback_prefix="select_company_alpha_page",
|
||
page_callback_suffix=f":{letter}"
|
||
)
|
||
label = f"Firme cu litera {letter}" if letter != "ALL" else "Toate firmele"
|
||
await query.edit_message_text(
|
||
f"**{label}** ({len(filtered)}):",
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif callback_data.startswith("select_company_alpha_page:"):
|
||
# Paginate within an alpha-filtered company list
|
||
# Callback format: select_company_alpha_page:PAGE:LETTER
|
||
parts = callback_data.split(":")
|
||
page = int(parts[1])
|
||
letter = parts[2] # "A"–"Z" or "ALL"
|
||
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
client = get_backend_client()
|
||
async with client:
|
||
all_companies = await client.get_user_companies(jwt_token=jwt_token)
|
||
|
||
if letter == "ALL":
|
||
filtered = all_companies
|
||
else:
|
||
filtered = [
|
||
c for c in all_companies
|
||
if c.get('name', c.get('nume_firma', '')).upper().startswith(letter)
|
||
]
|
||
|
||
from backend.modules.telegram.bot.helpers import create_company_selection_keyboard_paginated
|
||
keyboard = create_company_selection_keyboard_paginated(
|
||
filtered, page=page,
|
||
back_callback="select_company_alpha_menu",
|
||
page_callback_prefix="select_company_alpha_page",
|
||
page_callback_suffix=f":{letter}"
|
||
)
|
||
label = f"Firme cu litera {letter}" if letter != "ALL" else "Toate firmele"
|
||
await query.edit_message_text(
|
||
f"**{label}** ({len(filtered)}):",
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif callback_data == "clients_alpha_menu":
|
||
# Show A-Z letter filter keyboard for clients
|
||
from backend.modules.telegram.bot.helpers import create_alpha_filter_keyboard_partner
|
||
await query.edit_message_text(
|
||
"**Selecteaza litera**\n\nAlege prima litera a clientului:",
|
||
reply_markup=create_alpha_filter_keyboard_partner("clients"),
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif callback_data.startswith("clients_alpha:"):
|
||
# Filter clients by starting letter and show page 0
|
||
letter = callback_data.split(":", 1)[1] # "A" or "ALL"
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
|
||
from backend.modules.telegram.bot.helpers import get_clients_with_maturity
|
||
clients_data = await get_clients_with_maturity(company['id'], jwt_token)
|
||
all_clients = clients_data['clients']
|
||
|
||
if letter == "ALL":
|
||
filtered = all_clients
|
||
else:
|
||
filtered = [
|
||
c for c in all_clients
|
||
if c.get('name', '').upper().startswith(letter)
|
||
]
|
||
|
||
if not filtered:
|
||
await query.answer(f"Niciun client cu litera {letter}.", show_alert=True)
|
||
return
|
||
|
||
from backend.modules.telegram.bot.menus import create_client_list_keyboard, format_response_with_company
|
||
from backend.modules.telegram.bot.formatters import format_clients_balance_response
|
||
content = format_clients_balance_response(filtered, clients_data['maturity'])
|
||
label = f"Clienti cu litera {letter}" if letter != "ALL" else "Toti clientii"
|
||
response = format_response_with_company(f"**{label}** ({len(filtered)}):\n\n{content}", company['name'])
|
||
keyboard = create_client_list_keyboard(filtered, page=0, letter=letter)
|
||
await query.edit_message_text(response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN)
|
||
|
||
elif callback_data.startswith("clients_alpha_page:"):
|
||
# Paginate within an alpha-filtered client list
|
||
# Callback format: clients_alpha_page:PAGE:LETTER
|
||
parts = callback_data.split(":")
|
||
page = int(parts[1])
|
||
letter = parts[2] # "A"–"Z" or "ALL"
|
||
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
|
||
from backend.modules.telegram.bot.helpers import get_clients_with_maturity
|
||
clients_data = await get_clients_with_maturity(company['id'], jwt_token)
|
||
all_clients = clients_data['clients']
|
||
|
||
if letter == "ALL":
|
||
filtered = all_clients
|
||
else:
|
||
filtered = [
|
||
c for c in all_clients
|
||
if c.get('name', '').upper().startswith(letter)
|
||
]
|
||
|
||
from backend.modules.telegram.bot.menus import create_client_list_keyboard, format_response_with_company
|
||
from backend.modules.telegram.bot.formatters import format_clients_balance_response
|
||
content = format_clients_balance_response(filtered, clients_data['maturity'])
|
||
label = f"Clienti cu litera {letter}" if letter != "ALL" else "Toti clientii"
|
||
response = format_response_with_company(f"**{label}** ({len(filtered)}):\n\n{content}", company['name'])
|
||
keyboard = create_client_list_keyboard(filtered, page=page, letter=letter)
|
||
await query.edit_message_text(response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN)
|
||
|
||
elif callback_data == "suppliers_alpha_menu":
|
||
# Show A-Z letter filter keyboard for suppliers
|
||
from backend.modules.telegram.bot.helpers import create_alpha_filter_keyboard_partner
|
||
await query.edit_message_text(
|
||
"**Selecteaza litera**\n\nAlege prima litera a furnizorului:",
|
||
reply_markup=create_alpha_filter_keyboard_partner("suppliers"),
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif callback_data.startswith("suppliers_alpha:"):
|
||
# Filter suppliers by starting letter and show page 0
|
||
letter = callback_data.split(":", 1)[1] # "A" or "ALL"
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
|
||
from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity
|
||
suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token)
|
||
all_suppliers = suppliers_data['suppliers']
|
||
|
||
if letter == "ALL":
|
||
filtered = all_suppliers
|
||
else:
|
||
filtered = [
|
||
s for s in all_suppliers
|
||
if s.get('name', '').upper().startswith(letter)
|
||
]
|
||
|
||
if not filtered:
|
||
await query.answer(f"Niciun furnizor cu litera {letter}.", show_alert=True)
|
||
return
|
||
|
||
from backend.modules.telegram.bot.menus import create_supplier_list_keyboard, format_response_with_company
|
||
from backend.modules.telegram.bot.formatters import format_suppliers_balance_response
|
||
content = format_suppliers_balance_response(filtered, suppliers_data['maturity'])
|
||
label = f"Furnizori cu litera {letter}" if letter != "ALL" else "Toti furnizorii"
|
||
response = format_response_with_company(f"**{label}** ({len(filtered)}):\n\n{content}", company['name'])
|
||
keyboard = create_supplier_list_keyboard(filtered, page=0, letter=letter)
|
||
await query.edit_message_text(response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN)
|
||
|
||
elif callback_data.startswith("suppliers_alpha_page:"):
|
||
# Paginate within an alpha-filtered supplier list
|
||
# Callback format: suppliers_alpha_page:PAGE:LETTER
|
||
parts = callback_data.split(":")
|
||
page = int(parts[1])
|
||
letter = parts[2] # "A"–"Z" or "ALL"
|
||
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
|
||
from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity
|
||
suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token)
|
||
all_suppliers = suppliers_data['suppliers']
|
||
|
||
if letter == "ALL":
|
||
filtered = all_suppliers
|
||
else:
|
||
filtered = [
|
||
s for s in all_suppliers
|
||
if s.get('name', '').upper().startswith(letter)
|
||
]
|
||
|
||
from backend.modules.telegram.bot.menus import create_supplier_list_keyboard, format_response_with_company
|
||
from backend.modules.telegram.bot.formatters import format_suppliers_balance_response
|
||
content = format_suppliers_balance_response(filtered, suppliers_data['maturity'])
|
||
label = f"Furnizori cu litera {letter}" if letter != "ALL" else "Toti furnizorii"
|
||
response = format_response_with_company(f"**{label}** ({len(filtered)}):\n\n{content}", company['name'])
|
||
keyboard = create_supplier_list_keyboard(filtered, page=page, letter=letter)
|
||
await query.edit_message_text(response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN)
|
||
|
||
elif callback_data.startswith("select_company:"):
|
||
# Handle company selection
|
||
company_id = int(callback_data.split(":")[1])
|
||
|
||
# Get company details
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
client = get_backend_client()
|
||
async with client:
|
||
companies = await client.get_user_companies(jwt_token=jwt_token)
|
||
|
||
# Find selected company
|
||
selected = next(
|
||
(c for c in companies if c.get('id_firma', c.get('id')) == company_id),
|
||
None
|
||
)
|
||
|
||
if selected:
|
||
# Set active company in session
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
|
||
# Extract company data with backwards compatibility
|
||
company_name = selected.get('name', selected.get('nume_firma', 'N/A'))
|
||
company_cui = selected.get('fiscal_code', selected.get('cui'))
|
||
|
||
session.set_active_company(
|
||
company_id=company_id,
|
||
company_name=company_name,
|
||
company_cui=company_cui
|
||
)
|
||
await session_manager.save_session(telegram_user_id)
|
||
|
||
# Show main menu directly (no confirmation message)
|
||
from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message
|
||
keyboard = create_main_menu(
|
||
company_name=company_name,
|
||
company_cui=company_cui
|
||
)
|
||
menu_text = get_menu_message(company_name, company_cui)
|
||
|
||
await query.edit_message_text(
|
||
menu_text,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
else:
|
||
await query.edit_message_text(
|
||
"Companie negasita sau nu ai acces la ea."
|
||
)
|
||
|
||
# ========== SWITCH SERVER CALLBACKS ==========
|
||
|
||
elif callback_data.startswith("switch_server_confirm:"):
|
||
new_server_id = callback_data.split(":", 1)[1]
|
||
telegram_user_id = update.effective_user.id
|
||
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
if not auth_data:
|
||
await query.edit_message_text("Sesiune expirată. Re-autentifică-te cu /login")
|
||
return
|
||
|
||
# Stochează serverul țintă și cere parola — servere diferite pot avea parole diferite
|
||
context.user_data['pending_switch_server_id'] = new_server_id
|
||
context.user_data['pending_switch_jwt_token'] = auth_data['jwt_token']
|
||
context.user_data['pending_switch_username'] = auth_data['username']
|
||
|
||
try:
|
||
from backend.config import settings
|
||
srv = settings.get_oracle_server(new_server_id)
|
||
srv_display = srv.name if srv else new_server_id
|
||
except Exception:
|
||
srv_display = new_server_id
|
||
|
||
from telegram import ForceReply
|
||
await query.edit_message_text(
|
||
f"🔐 **Schimbare server: {srv_display}**\n\n"
|
||
f"Introdu parola Oracle pentru acest server:\n"
|
||
f"_(Mesajul cu parola va fi șters automat)_",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
# Trimite un mesaj separat cu ForceReply pentru a forța input-ul
|
||
await context.bot.send_message(
|
||
chat_id=update.effective_chat.id,
|
||
text="Parolă:",
|
||
reply_markup=ForceReply(selective=True, input_field_placeholder="Parola Oracle...")
|
||
)
|
||
return
|
||
|
||
# ========== LOGOUT CALLBACKS ==========
|
||
|
||
elif callback_data == "logout_confirm":
|
||
# Logout user (same as unlink but shows menu after)
|
||
from backend.modules.telegram.auth.linking import unlink_user
|
||
|
||
success = await unlink_user(telegram_user_id)
|
||
|
||
if success:
|
||
# Clear session too
|
||
session_manager = get_session_manager()
|
||
await session_manager.delete_session(telegram_user_id)
|
||
|
||
# Show login menu (non-authenticated)
|
||
from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message, pad_message_for_wide_buttons
|
||
keyboard = create_main_menu(company_name=None, company_cui=None, is_authenticated=False)
|
||
menu_text = pad_message_for_wide_buttons(
|
||
"**Deconectat cu succes**\n\n"
|
||
"Contul tău a fost deconectat.\n\n"
|
||
"Pentru a te reconecta, apasă **Login**."
|
||
)
|
||
|
||
await query.edit_message_text(
|
||
menu_text,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
else:
|
||
await query.edit_message_text(
|
||
"A apărut o eroare la deconectare.\n"
|
||
"Te rog încearcă din nou."
|
||
)
|
||
|
||
elif callback_data == "logout_cancel":
|
||
# Cancel logout - return to main menu
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
|
||
# Check auth
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
is_authenticated = auth_data is not None
|
||
|
||
from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message
|
||
company_name = company['name'] if company else None
|
||
company_cui = company.get('cui') if company else None
|
||
keyboard = create_main_menu(company_name, company_cui, is_authenticated)
|
||
menu_text = get_menu_message(company_name, company_cui)
|
||
|
||
await query.edit_message_text(
|
||
menu_text,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
# ========== UNLINK CALLBACKS (LEGACY) ==========
|
||
|
||
elif callback_data == "unlink_confirm":
|
||
# Unlink user
|
||
from backend.modules.telegram.auth.linking import unlink_user
|
||
|
||
success = await unlink_user(telegram_user_id)
|
||
|
||
if success:
|
||
# Clear session too
|
||
session_manager = get_session_manager()
|
||
await session_manager.delete_session(telegram_user_id)
|
||
|
||
await query.edit_message_text(
|
||
"**Cont deconectat cu succes**\n\n"
|
||
"Datele tale au fost șterse din sistem.\n\n"
|
||
"Pentru a te reconecta, folosește `/start CODUL_TAU`",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
else:
|
||
await query.edit_message_text(
|
||
"A aparut o eroare la deconectare.\n"
|
||
"Te rog incearca din nou."
|
||
)
|
||
|
||
elif callback_data == "unlink_cancel":
|
||
await query.edit_message_text(
|
||
"Deconectare anulata.\n\n"
|
||
"Contul tau ramane linkuit."
|
||
)
|
||
|
||
# ========== LOGIN CALLBACKS ==========
|
||
|
||
elif callback_data == "login_help":
|
||
# Show detailed help on how to get link code
|
||
await query.edit_message_text(
|
||
"**Cum obțin codul de link?**\n\n"
|
||
"1. Loghează-te în aplicația web ROA2WEB\n"
|
||
"2. Mergi la: Setări → Telegram Linking\n"
|
||
"3. Apasă **Generează Cod**\n"
|
||
"4. Vei primi un cod din 8 caractere (ex: ABC12XYZ)\n"
|
||
"5. Trimite-mi comanda: `/start CODUL_TAU`\n\n"
|
||
"**Important:** Codul expiră în 15 minute.",
|
||
reply_markup=InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("Am deja cod - Linkez acum", callback_data="login_prompt")],
|
||
[InlineKeyboardButton("« Înapoi la Meniu", callback_data="action:menu")]
|
||
]),
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif callback_data == "login_prompt":
|
||
# Prompt user to enter link code directly
|
||
from telegram import ForceReply
|
||
|
||
await query.edit_message_text(
|
||
"**Conectare Cont ROA2WEB**\n\n"
|
||
"Trimite-mi codul primit din aplicatia web.\n\n"
|
||
"Poti trimite:\n"
|
||
"- Doar codul: ABC12XYZ\n"
|
||
"- Sau comanda: /start ABC12XYZ\n\n"
|
||
"Codul expira in 15 minute.",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
# Send a follow-up message with ForceReply to prompt input
|
||
await context.bot.send_message(
|
||
chat_id=telegram_user_id,
|
||
text="Scrie sau lipeste codul aici:",
|
||
reply_markup=ForceReply(
|
||
selective=True,
|
||
input_field_placeholder="ABC12XYZ"
|
||
)
|
||
)
|
||
|
||
elif callback_data == "login_back":
|
||
# Go back to welcome message
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("Cum obtin codul de link?", callback_data="login_help")],
|
||
[InlineKeyboardButton("Am deja cod - Linkez contul", callback_data="login_prompt")]
|
||
])
|
||
|
||
await query.edit_message_text(
|
||
"**Bun venit la ROA2WEB Bot!**\n\n"
|
||
"Sunt asistentul tau financiar pentru sistemul ERP ROA2WEB.\n\n"
|
||
"**Pentru a incepe, trebuie sa-ti linkezi contul Telegram cu contul tau ROA2WEB.**\n\n"
|
||
"Alege o optiune:",
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
# ========== NEW CALLBACKS (FAZA 4) ==========
|
||
|
||
# NIVEL 1: Main Menu Buttons
|
||
elif callback_data.startswith("menu:"):
|
||
await handle_menu_callback(query, telegram_user_id, callback_data)
|
||
|
||
# Action Buttons
|
||
elif callback_data.startswith("action:"):
|
||
await handle_action_callback(query, telegram_user_id, callback_data)
|
||
|
||
# NIVEL 2: Client/Supplier Details
|
||
elif callback_data.startswith("details:"):
|
||
await handle_details_callback(query, telegram_user_id, callback_data)
|
||
|
||
# NIVEL 3: Invoice Details
|
||
elif callback_data.startswith("invoice:"):
|
||
await handle_invoice_callback(query, telegram_user_id, callback_data)
|
||
|
||
# Navigation Back
|
||
elif callback_data.startswith("nav:back:"):
|
||
await handle_navigation_back(query, telegram_user_id, callback_data)
|
||
|
||
# ========== CACHE CALLBACKS (FAZA 6) ==========
|
||
|
||
elif callback_data.startswith("cache_toggle:"):
|
||
# Handle cache toggle button
|
||
action = callback_data.split(":")[1]
|
||
enabled = action == "on"
|
||
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
try:
|
||
client = get_backend_client()
|
||
async with client:
|
||
await client.toggle_user_cache(jwt_token=jwt_token, enabled=enabled)
|
||
|
||
status = "activat" if enabled else "dezactivat"
|
||
message = f"✅ **Cache {status}** pentru tine.\n\n"
|
||
|
||
if enabled:
|
||
message += "Queries vor fi servite din cache când e posibil."
|
||
else:
|
||
message += "Toate queries vor merge direct la Oracle.\nFolosește /togglecache din nou pentru reactivare."
|
||
|
||
# Add back button
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("« Înapoi la Meniu", callback_data="action:menu")]
|
||
])
|
||
|
||
await query.edit_message_text(
|
||
message,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Toggle cache callback error: {e}", exc_info=True)
|
||
await query.answer("❌ Eroare la modificarea setării cache.", show_alert=True)
|
||
|
||
elif callback_data.startswith("cache_clear:"):
|
||
# Handle clear cache button
|
||
scope = callback_data.split(":")[1]
|
||
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
try:
|
||
client = get_backend_client()
|
||
|
||
if scope == "all":
|
||
# Clear all cache
|
||
async with client:
|
||
await client.invalidate_cache(jwt_token=jwt_token, company_id=None)
|
||
message = "✅ Cache invalidat pentru **toate companiile**.\n\nDatele vor fi refreshate la următoarea interogare."
|
||
|
||
elif scope == "current":
|
||
# Clear only current company
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
|
||
if not company:
|
||
await query.answer("Nu ai o companie selectată.", show_alert=True)
|
||
return
|
||
|
||
async with client:
|
||
await client.invalidate_cache(jwt_token=jwt_token, company_id=company['id'])
|
||
message = f"✅ Cache invalidat pentru **{company['name']}**.\n\nDatele vor fi refreshate la următoarea interogare."
|
||
|
||
# Add back button
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("« Înapoi la Meniu", callback_data="action:menu")]
|
||
])
|
||
|
||
await query.edit_message_text(
|
||
message,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Clear cache callback error: {e}", exc_info=True)
|
||
await query.answer("❌ Eroare la ștergerea cache-ului.", show_alert=True)
|
||
|
||
# ========== PAGINATION CALLBACKS ==========
|
||
|
||
elif callback_data.startswith("clients_page:"):
|
||
# Handle clients pagination
|
||
page = int(callback_data.split(":")[1])
|
||
|
||
# Get auth data and company
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
|
||
# Get clients with maturity
|
||
from backend.modules.telegram.bot.helpers import get_clients_with_maturity
|
||
clients_data = await get_clients_with_maturity(company['id'], jwt_token)
|
||
|
||
from backend.modules.telegram.bot.formatters import format_clients_balance_response
|
||
from backend.modules.telegram.bot.menus import create_client_list_keyboard, format_response_with_company
|
||
|
||
content = format_clients_balance_response(
|
||
clients_data['clients'],
|
||
clients_data['maturity']
|
||
)
|
||
response = format_response_with_company(content, company['name'])
|
||
keyboard = create_client_list_keyboard(clients_data['clients'], page=page)
|
||
|
||
await query.edit_message_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif callback_data.startswith("suppliers_page:"):
|
||
# Handle suppliers pagination
|
||
page = int(callback_data.split(":")[1])
|
||
|
||
# Get auth data and company
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
|
||
# Get suppliers with maturity
|
||
from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity
|
||
suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token)
|
||
|
||
from backend.modules.telegram.bot.formatters import format_suppliers_balance_response
|
||
from backend.modules.telegram.bot.menus import create_supplier_list_keyboard, format_response_with_company
|
||
|
||
content = format_suppliers_balance_response(
|
||
suppliers_data['suppliers'],
|
||
suppliers_data['maturity']
|
||
)
|
||
response = format_response_with_company(content, company['name'])
|
||
keyboard = create_supplier_list_keyboard(suppliers_data['suppliers'], page=page)
|
||
|
||
await query.edit_message_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif callback_data.startswith("invoices_page:"):
|
||
# Handle invoices pagination
|
||
# Format: invoices_page:PARTNER_TYPE:PARTNER_NAME:PAGE
|
||
parts = callback_data.split(":")
|
||
partner_type = parts[1] # CLIENTI or FURNIZORI
|
||
partner_name = parts[2]
|
||
page = int(parts[3])
|
||
|
||
# Get auth data and company
|
||
auth_data = await get_user_auth_data(telegram_user_id)
|
||
jwt_token = auth_data['jwt_token']
|
||
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
company = session.get_active_company()
|
||
|
||
# Get invoices for this partner
|
||
if partner_type == "CLIENTI":
|
||
from backend.modules.telegram.bot.helpers import get_client_invoices, get_clients_with_maturity
|
||
invoices = await get_client_invoices(company['id'], partner_name, jwt_token)
|
||
|
||
# Get client details
|
||
clients_data = await get_clients_with_maturity(company['id'], jwt_token)
|
||
partner = next((c for c in clients_data['clients'] if c['name'] == partner_name), None)
|
||
|
||
from backend.modules.telegram.bot.formatters import format_client_detail_response
|
||
content = format_client_detail_response(partner, invoices)
|
||
else:
|
||
from backend.modules.telegram.bot.helpers import get_supplier_invoices, get_suppliers_with_maturity
|
||
invoices = await get_supplier_invoices(company['id'], partner_name, jwt_token)
|
||
|
||
# Get supplier details
|
||
suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token)
|
||
partner = next((s for s in suppliers_data['suppliers'] if s['name'] == partner_name), None)
|
||
|
||
from backend.modules.telegram.bot.formatters import format_supplier_detail_response
|
||
content = format_supplier_detail_response(partner, invoices)
|
||
|
||
from backend.modules.telegram.bot.menus import create_invoice_list_keyboard, format_response_with_company
|
||
response = format_response_with_company(content, company['name'])
|
||
keyboard = create_invoice_list_keyboard(invoices, partner_type, partner_name, page=page)
|
||
|
||
await query.edit_message_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
elif callback_data == "noop":
|
||
# No operation - just acknowledge
|
||
pass
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in button_callback: {e}", exc_info=True)
|
||
|
||
|
||
# ============================================================================
|
||
# ERROR HANDLER
|
||
# ============================================================================
|
||
|
||
async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Handle errors in bot operations.
|
||
|
||
Args:
|
||
update: Telegram update object
|
||
context: Telegram context with error
|
||
"""
|
||
logger.error(f"Update {update} caused error {context.error}", exc_info=context.error)
|
||
|
||
# Try to send error message to user
|
||
try:
|
||
if update and update.effective_message:
|
||
await update.effective_message.reply_text(
|
||
"**A aparut o eroare tehnica**\n\n"
|
||
"Te rog incearca din nou sau contacteaza support.\n\n"
|
||
"Daca problema persista, foloseste /clear pentru a reseta conversatia.",
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Failed to send error message to user: {e}")
|
||
|
||
|
||
# ============================================================================
|
||
# COMMON HANDLER FUNCTIONS (pentru consistență comenzi/butoane)
|
||
# ============================================================================
|
||
|
||
async def _handle_expired_auth(query_or_update, telegram_user_id: int, auth_data: Optional[Dict[str, Any]]) -> bool:
|
||
"""
|
||
Check if auth_data is None (token expired/refresh failed) and send user-friendly message.
|
||
|
||
Args:
|
||
query_or_update: Update (command) or CallbackQuery (button)
|
||
telegram_user_id: Telegram user ID
|
||
auth_data: Authentication data from get_user_auth_data()
|
||
|
||
Returns:
|
||
True if auth is expired (stop execution), False if auth is valid (continue)
|
||
"""
|
||
if auth_data is None:
|
||
logger.warning(f"Auth expired for user {telegram_user_id}, sending re-authentication message")
|
||
|
||
# Create re-authentication message
|
||
message = (
|
||
"⚠️ **Sesiunea ta a expirat**\n\n"
|
||
"Token-ul de autentificare a expirat și nu a putut fi reînnoit automat.\n\n"
|
||
"**Pentru a continua:**\n"
|
||
"1. Accesează aplicația web ROA2WEB\n"
|
||
"2. Loginează-te cu contul tău Oracle\n"
|
||
"3. Generează un nou cod de link pentru Telegram\n"
|
||
"4. Trimite comanda `/start CODUL_TAU`\n\n"
|
||
"_Sau folosește `/unlink` pentru a deconecta contul actual._"
|
||
)
|
||
|
||
# Send message based on source type
|
||
from telegram import CallbackQuery
|
||
if isinstance(query_or_update, CallbackQuery):
|
||
# It's a button callback - transform menu to Login menu
|
||
await query_or_update.answer("Sesiunea a expirat. Te rog să te reconectezi.", show_alert=True)
|
||
|
||
# Transform the current message (menu) to Login menu
|
||
from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons
|
||
keyboard = create_main_menu(company_name=None, company_cui=None, is_authenticated=False)
|
||
menu_text = pad_message_for_wide_buttons(
|
||
"⚠️ **Sesiunea a expirat**\n\n"
|
||
"Pentru a continua, apasă **Login**\n"
|
||
"și urmează instrucțiunile."
|
||
)
|
||
|
||
await query_or_update.edit_message_text(
|
||
menu_text,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
else:
|
||
# It's a command (Update) - just send message
|
||
await query_or_update.message.reply_text(message, parse_mode=ParseMode.MARKDOWN)
|
||
|
||
return True # Stop execution
|
||
|
||
return False # Continue execution
|
||
|
||
|
||
async def _handle_sold_view(
|
||
query_or_update,
|
||
telegram_user_id: int,
|
||
company: Dict[str, Any],
|
||
jwt_token: str,
|
||
is_callback: bool = False
|
||
):
|
||
"""
|
||
Common handler pentru sold view (dashboard).
|
||
|
||
Folosit de:
|
||
- Comanda /dashboard
|
||
- Comanda /sold
|
||
- Butonul menu:sold
|
||
|
||
Args:
|
||
query_or_update: Query (callback) sau Update (command)
|
||
telegram_user_id: Telegram user ID
|
||
company: Dict cu id, name, cui
|
||
jwt_token: JWT token
|
||
is_callback: True dacă e apelat din callback, False dacă e command
|
||
"""
|
||
try:
|
||
client = get_backend_client()
|
||
async with client:
|
||
data = await client.get_dashboard_data(
|
||
company_id=company['id'],
|
||
jwt_token=jwt_token
|
||
)
|
||
|
||
if not data:
|
||
error_msg = "Eroare la incarcarea dashboard-ului."
|
||
if is_callback:
|
||
await query_or_update.edit_message_text(error_msg)
|
||
else:
|
||
await query_or_update.message.reply_text(error_msg)
|
||
return
|
||
|
||
from backend.modules.telegram.bot.formatters import format_dashboard_response, add_performance_footer
|
||
from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company
|
||
|
||
# Extract cache metadata
|
||
cache_hit = data.get('cache_hit', False)
|
||
response_time_ms = data.get('response_time_ms', 0)
|
||
cache_source = data.get('cache_source', None)
|
||
|
||
content = format_dashboard_response(data)
|
||
response = format_response_with_company(content, company['name'])
|
||
|
||
# Add performance footer
|
||
if response_time_ms > 0:
|
||
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
||
keyboard = create_action_buttons("sold", show_export=False, show_refresh=False)
|
||
|
||
if is_callback:
|
||
await query_or_update.edit_message_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
else:
|
||
await query_or_update.message.reply_text(
|
||
response,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in _handle_sold_view: {e}", exc_info=True)
|
||
error_msg = "Eroare la incarcarea dashboard-ului."
|
||
if is_callback:
|
||
await query_or_update.answer(error_msg, show_alert=True)
|
||
else:
|
||
await query_or_update.message.reply_text(error_msg)
|
||
|
||
|
||
async def _handle_selectcompany_view(
|
||
query_or_update,
|
||
telegram_user_id: int,
|
||
jwt_token: str,
|
||
is_callback: bool = False,
|
||
page: int = 0,
|
||
search_term: str = ""
|
||
):
|
||
"""
|
||
Common handler pentru company selection cu paginare.
|
||
|
||
Folosit de:
|
||
- Comanda /selectcompany
|
||
- Butonul menu:select_company
|
||
- Callback-urile de paginare (select_company_page:N)
|
||
|
||
Args:
|
||
query_or_update: Query (callback) sau Update (command)
|
||
telegram_user_id: Telegram user ID
|
||
jwt_token: JWT token
|
||
is_callback: True dacă e apelat din callback, False dacă e command
|
||
page: Numărul paginii (0-indexed)
|
||
search_term: Termen de căutare (opțional)
|
||
"""
|
||
try:
|
||
client = get_backend_client()
|
||
async with client:
|
||
companies = await client.get_user_companies(jwt_token=jwt_token)
|
||
|
||
# Apply search filter if provided
|
||
if search_term:
|
||
from backend.modules.telegram.bot.helpers import search_companies_by_name
|
||
companies = await search_companies_by_name(search_term, jwt_token)
|
||
|
||
if not companies:
|
||
error_msg = f"Nu am gasit companii care contin '{search_term}'.\n\n" \
|
||
"Incearca alt termen sau /selectcompany pentru lista completa."
|
||
if is_callback:
|
||
await query_or_update.answer(error_msg, show_alert=True)
|
||
else:
|
||
await query_or_update.message.reply_text(error_msg)
|
||
return
|
||
|
||
if not companies:
|
||
error_msg = "Nu ai acces la nicio companie.\nContacteaza administratorul."
|
||
if is_callback:
|
||
await query_or_update.edit_message_text(
|
||
error_msg,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
else:
|
||
await query_or_update.message.reply_text(
|
||
error_msg,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
# Auto-selectează dacă există exact o singură firmă
|
||
if len(companies) == 1:
|
||
selected = companies[0]
|
||
company_id = selected.get('id_firma', selected.get('id'))
|
||
company_name = selected.get('name', selected.get('nume_firma', 'N/A'))
|
||
company_cui = selected.get('fiscal_code', selected.get('cui'))
|
||
|
||
session_manager = get_session_manager()
|
||
session = await session_manager.get_or_create_session(telegram_user_id)
|
||
session.set_active_company(
|
||
company_id=company_id,
|
||
company_name=company_name,
|
||
company_cui=company_cui
|
||
)
|
||
await session_manager.save_session(telegram_user_id)
|
||
|
||
from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message
|
||
keyboard = create_main_menu(company_name=company_name, company_cui=company_cui)
|
||
menu_text = f"✅ Firmă selectată automat: **{company_name}**\n\n" + get_menu_message(company_name, company_cui)
|
||
|
||
if is_callback:
|
||
await query_or_update.edit_message_text(
|
||
menu_text,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
else:
|
||
await query_or_update.message.reply_text(
|
||
menu_text,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
return
|
||
|
||
from backend.modules.telegram.bot.helpers import create_company_selection_keyboard_paginated
|
||
keyboard = create_company_selection_keyboard_paginated(companies, page=page)
|
||
|
||
message = f"**Selecteaza Compania**\n\n"
|
||
if search_term:
|
||
message += f"Rezultate '{search_term}' ({len(companies)}):"
|
||
else:
|
||
message += f"Companiile tale ({len(companies)}):"
|
||
|
||
if is_callback:
|
||
await query_or_update.edit_message_text(
|
||
message,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
else:
|
||
await query_or_update.message.reply_text(
|
||
message,
|
||
reply_markup=keyboard,
|
||
parse_mode=ParseMode.MARKDOWN
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in _handle_selectcompany_view: {e}", exc_info=True)
|
||
error_msg = "A aparut o eroare. Te rog incearca din nou."
|
||
if is_callback:
|
||
await query_or_update.answer(error_msg, show_alert=True)
|
||
else:
|
||
await query_or_update.message.reply_text(error_msg)
|
||
|
||
|
||
# Export all handlers
|
||
__all__ = [
|
||
'start_command',
|
||
'help_command',
|
||
'clear_command',
|
||
'companies_command',
|
||
'unlink_command',
|
||
'selectcompany_command',
|
||
'dashboard_command',
|
||
'sold_command',
|
||
'facturi_command',
|
||
'trezorerie_command',
|
||
# FAZA 3: New command handlers with button interface
|
||
'menu_command',
|
||
'trezorerie_casa_command',
|
||
'trezorerie_banca_command',
|
||
'clienti_command',
|
||
'furnizori_command',
|
||
'evolutie_command',
|
||
# Text message handlers
|
||
'handle_text_message',
|
||
# FAZA 4: Callback helper functions
|
||
'handle_menu_callback',
|
||
'handle_action_callback',
|
||
'handle_details_callback',
|
||
'handle_invoice_callback',
|
||
'handle_navigation_back',
|
||
# Callback and error handlers
|
||
'button_callback',
|
||
'error_handler',
|
||
# Common handler functions
|
||
'_handle_sold_view',
|
||
'_handle_selectcompany_view'
|
||
]
|