Files
roa2web-service-auto/backend/modules/telegram/bot/handlers.py
Claude Agent 30f55cf18b feat: Add A-Z filter for clients/suppliers in Telegram bot
- 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>
2026-02-21 14:34:15 +00:00

3228 lines
128 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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'
]