Implement unified Telegram bot interface with Login/Logout and fix callback_data limits
**Interface improvements:** - Add persistent Login/Logout buttons to main menu - Help button now updates text above menu (not separate message) - Expired token automatically transforms menu to Login state - Consolidate linking success messages (single welcome + menu) **Fix callback_data length limits (Telegram 64-byte limit):** - Truncate client/supplier names to 40 chars in callback_data - Use full names for API calls but truncated for buttons - Fix pagination buttons for long partner names (30 chars limit) - Search entities by prefix match to handle truncated names **Treasury improvements:** - Show ALL bank/cash accounts (removed 5-item limit) - Remove unnecessary Refresh/Export buttons from treasury views - Handle "Message is not modified" error gracefully **Bug fixes:** - Fix Markdown parsing errors (replace <cod> with `CODUL_TAU`) - Fix silent errors when token expires (show user-friendly message) - Fix Button_data_invalid errors on pagination and details 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -124,13 +124,10 @@ def format_treasury_casa_response(data: Dict[str, Any], company_name: str = None
|
||||
casa_accounts = data.get('accounts', [])
|
||||
if casa_accounts:
|
||||
text += "**Conturi de Casa:**\n"
|
||||
for acc in casa_accounts[:5]: # Max 5
|
||||
for acc in casa_accounts: # Show all accounts
|
||||
name = acc.get('name', 'N/A')
|
||||
balance = round(acc.get('balance', 0))
|
||||
text += f" - {name}: {balance:,} RON\n"
|
||||
|
||||
if len(casa_accounts) > 5:
|
||||
text += f" ... si inca {len(casa_accounts) - 5} conturi"
|
||||
else:
|
||||
text += "Nu exista conturi de casa configurate."
|
||||
|
||||
@@ -162,13 +159,10 @@ def format_treasury_banca_response(data: Dict[str, Any], company_name: str = Non
|
||||
bank_accounts = data.get('accounts', [])
|
||||
if bank_accounts:
|
||||
text += "**Conturi Bancare:**\n"
|
||||
for acc in bank_accounts[:5]: # Max 5
|
||||
for acc in bank_accounts: # Show all accounts
|
||||
name = acc.get('name', 'N/A')
|
||||
balance = round(acc.get('balance', 0))
|
||||
text += f" - {name}: {balance:,} RON\n"
|
||||
|
||||
if len(bank_accounts) > 5:
|
||||
text += f" ... si inca {len(bank_accounts) - 5} conturi"
|
||||
else:
|
||||
text += "Nu exista conturi bancare configurate."
|
||||
|
||||
|
||||
@@ -69,24 +69,6 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if result:
|
||||
# Success!
|
||||
username = result['username']
|
||||
companies = result.get('companies', [])
|
||||
|
||||
companies_text = ""
|
||||
if companies:
|
||||
companies_text = "\n\n**Companiile tale:**\n"
|
||||
for comp_id in companies[:3]: # Show first 3 (companies are IDs as strings)
|
||||
companies_text += f"- Companie ID: {comp_id}\n"
|
||||
|
||||
if len(companies) > 3:
|
||||
companies_text += f"- ... si inca {len(companies) - 3} companii\n"
|
||||
|
||||
await update.message.reply_text(
|
||||
f"**Cont conectat cu succes**\n\n"
|
||||
f"Bun venit, **{username}**\n"
|
||||
f"{companies_text}\n"
|
||||
f"Foloseste meniul sau /help pentru comenzi.",
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
|
||||
# Show main menu with buttons for newly linked user
|
||||
session_manager = get_session_manager()
|
||||
@@ -95,12 +77,27 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
company_name = company['name'] if company else None
|
||||
company_cui = company.get('cui') if company else None
|
||||
|
||||
from app.bot.menus import create_main_menu, get_menu_message
|
||||
keyboard = create_main_menu(company_name, company_cui)
|
||||
menu_text = get_menu_message(company_name, company_cui)
|
||||
from app.bot.menus import create_main_menu, pad_message_for_wide_buttons
|
||||
keyboard = create_main_menu(company_name, company_cui, is_authenticated=True)
|
||||
|
||||
# Single welcome message with menu
|
||||
if company_name:
|
||||
welcome_text = (
|
||||
f"**Cont conectat cu succes**\n\n"
|
||||
f"Bun venit, **{username}**!\n\n"
|
||||
f"{company_name}"
|
||||
)
|
||||
else:
|
||||
welcome_text = (
|
||||
f"**Cont conectat cu succes**\n\n"
|
||||
f"Bun venit, **{username}**!\n\n"
|
||||
f"Selectează o companie pentru a continua"
|
||||
)
|
||||
|
||||
welcome_message = pad_message_for_wide_buttons(welcome_text)
|
||||
|
||||
await update.message.reply_text(
|
||||
menu_text,
|
||||
welcome_message,
|
||||
reply_markup=keyboard,
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
@@ -111,9 +108,9 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
# Failed linking
|
||||
await update.message.reply_text(
|
||||
"**Cod invalid sau expirat**\n\n"
|
||||
"Genereaza un cod nou din aplicatia web si trimite:\n"
|
||||
"/start <cod>\n\n"
|
||||
"Codul expira in 15 minute.",
|
||||
"Generează un cod nou din aplicația web și trimite:\n"
|
||||
"`/start CODUL_TAU`\n\n"
|
||||
"Codul expiră în 15 minute.",
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
|
||||
@@ -135,11 +132,12 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
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 app.bot.menus import create_main_menu, get_menu_message
|
||||
keyboard = create_main_menu(company_name)
|
||||
menu_text = get_menu_message(company_name)
|
||||
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}",
|
||||
@@ -212,10 +210,10 @@ Dupa conectarea contului, foloseste **butoanele interactive** pentru:
|
||||
/unlink - Deconecteaza contul (securitate)
|
||||
|
||||
**Conectare cont:**
|
||||
1. Logheaza-te in aplicatia web ROA2WEB
|
||||
2. Acceseaza Setari > Telegram Linking
|
||||
3. Genereaza cod (valabil 15 minute)
|
||||
4. Trimite codul in Telegram: /start <cod>
|
||||
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.
|
||||
@@ -647,9 +645,9 @@ async def menu_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
company_name = company['name'] if company else None
|
||||
company_cui = company.get('cui') if company else None
|
||||
|
||||
# Create main menu
|
||||
# Create main menu (user is authenticated if they passed the is_linked check)
|
||||
from app.bot.menus import create_main_menu, get_menu_message
|
||||
keyboard = create_main_menu(company_name, company_cui)
|
||||
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(
|
||||
@@ -1049,25 +1047,6 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
if result:
|
||||
# Success!
|
||||
username = result['username']
|
||||
companies = result.get('companies', [])
|
||||
|
||||
companies_text = ""
|
||||
if companies:
|
||||
companies_text = "\n\n**Companiile tale:**\n"
|
||||
for comp_id in companies[:3]: # Show first 3
|
||||
companies_text += f"- Companie ID: {comp_id}\n"
|
||||
|
||||
if len(companies) > 3:
|
||||
companies_text += f"- ... si inca {len(companies) - 3} companii\n"
|
||||
|
||||
await update.message.reply_text(
|
||||
f"**Cont conectat cu succes**\n\n"
|
||||
f"Bun venit, **{username}**\n"
|
||||
f"{companies_text}\n"
|
||||
f"Foloseste meniul sau /help pentru comenzi.",
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
|
||||
# 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)
|
||||
@@ -1075,12 +1054,27 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
company_name = company['name'] if company else None
|
||||
company_cui = company.get('cui') if company else None
|
||||
|
||||
from app.bot.menus import create_main_menu, get_menu_message
|
||||
keyboard = create_main_menu(company_name, company_cui)
|
||||
menu_text = get_menu_message(company_name, company_cui)
|
||||
from app.bot.menus import create_main_menu, pad_message_for_wide_buttons
|
||||
keyboard = create_main_menu(company_name, company_cui, is_authenticated=True)
|
||||
|
||||
# Single welcome message with menu
|
||||
if company_name:
|
||||
welcome_text = (
|
||||
f"**Cont conectat cu succes**\n\n"
|
||||
f"Bun venit, **{username}**!\n\n"
|
||||
f"{company_name}"
|
||||
)
|
||||
else:
|
||||
welcome_text = (
|
||||
f"**Cont conectat cu succes**\n\n"
|
||||
f"Bun venit, **{username}**!\n\n"
|
||||
f"Selectează o companie pentru a continua"
|
||||
)
|
||||
|
||||
welcome_message = pad_message_for_wide_buttons(welcome_text)
|
||||
|
||||
await update.message.reply_text(
|
||||
menu_text,
|
||||
welcome_message,
|
||||
reply_markup=keyboard,
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
@@ -1140,6 +1134,29 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str)
|
||||
|
||||
# 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 app.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
|
||||
@@ -1193,13 +1210,18 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str)
|
||||
|
||||
content = format_treasury_casa_response(treasury_data['casa'])
|
||||
response = format_response_with_company(content, company['name'])
|
||||
keyboard = create_action_buttons("casa", show_export=True)
|
||||
keyboard = create_action_buttons("casa", show_export=False, show_refresh=False)
|
||||
|
||||
await query.edit_message_text(
|
||||
response,
|
||||
reply_markup=keyboard,
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
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
|
||||
@@ -1211,13 +1233,18 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str)
|
||||
|
||||
content = format_treasury_banca_response(treasury_data['banca'])
|
||||
response = format_response_with_company(content, company['name'])
|
||||
keyboard = create_action_buttons("banca", show_export=True)
|
||||
keyboard = create_action_buttons("banca", show_export=False, show_refresh=False)
|
||||
|
||||
await query.edit_message_text(
|
||||
response,
|
||||
reply_markup=keyboard,
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
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
|
||||
@@ -1296,10 +1323,10 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str)
|
||||
|
||||
async def handle_action_callback(query, telegram_user_id: int, callback_data: str):
|
||||
"""
|
||||
Handle action button clicks (Refresh, Export, Menu).
|
||||
Handle action button clicks (Refresh, Export, Menu, Help, Login, Logout).
|
||||
|
||||
Callback format: action:{type}:{view}
|
||||
Types: refresh, export, menu
|
||||
Types: refresh, export, menu, help, login, logout
|
||||
|
||||
Args:
|
||||
query: CallbackQuery object
|
||||
@@ -1315,10 +1342,14 @@ async def handle_action_callback(query, telegram_user_id: int, callback_data: st
|
||||
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
|
||||
|
||||
from app.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)
|
||||
keyboard = create_main_menu(company_name, company_cui, is_authenticated)
|
||||
menu_text = get_menu_message(company_name, company_cui)
|
||||
|
||||
await query.edit_message_text(
|
||||
@@ -1334,10 +1365,14 @@ async def handle_action_callback(query, telegram_user_id: int, callback_data: st
|
||||
# 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
|
||||
await handle_details_callback(query, telegram_user_id, f"details:client:{entity_name}:0")
|
||||
# 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
|
||||
await handle_details_callback(query, telegram_user_id, f"details:supplier:{entity_name}:0")
|
||||
# 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}")
|
||||
@@ -1346,6 +1381,101 @@ async def handle_action_callback(query, telegram_user_id: int, callback_data: st
|
||||
# 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 app.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 app.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
|
||||
)
|
||||
|
||||
elif action_type == "login":
|
||||
# Prompt user to enter link code directly (same as login_prompt functionality)
|
||||
from telegram import ForceReply
|
||||
from app.bot.menus import pad_message_for_wide_buttons
|
||||
|
||||
# Edit the current message with instructions
|
||||
login_text = pad_message_for_wide_buttons(
|
||||
"**Conectare Cont ROA2WEB**\n\n"
|
||||
"Trimite-mi codul primit din aplicația web.\n\n"
|
||||
"Poți trimite:\n"
|
||||
"• Doar codul: ABC12XYZ\n"
|
||||
"• Sau comanda: /start ABC12XYZ\n\n"
|
||||
"Codul expiră în 15 minute."
|
||||
)
|
||||
|
||||
# Buttons for help or cancel
|
||||
keyboard = InlineKeyboardMarkup([
|
||||
[InlineKeyboardButton("Cum obțin codul?", callback_data="login_help")],
|
||||
[InlineKeyboardButton("« Anulează", callback_data="action:menu")]
|
||||
])
|
||||
|
||||
await query.edit_message_text(
|
||||
login_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
|
||||
# Send a follow-up message with ForceReply to prompt input
|
||||
await query.message.reply_text(
|
||||
"Scrie sau lipește codul aici:",
|
||||
reply_markup=ForceReply(
|
||||
selective=True,
|
||||
input_field_placeholder="ABC12XYZ"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def handle_details_callback(query, telegram_user_id: int, callback_data: str):
|
||||
"""
|
||||
@@ -1373,26 +1503,35 @@ async def handle_details_callback(query, telegram_user_id: int, callback_data: s
|
||||
company = session.get_active_company()
|
||||
|
||||
if detail_type == "client":
|
||||
# Get client invoices by name
|
||||
from app.bot.helpers import get_client_invoices
|
||||
invoices = await get_client_invoices(company['id'], entity_name, jwt_token)
|
||||
|
||||
# Get client details (from clients list)
|
||||
# entity_name might be truncated to 40 chars, so search by startswith
|
||||
from app.bot.helpers import get_clients_with_maturity
|
||||
clients_data = await get_clients_with_maturity(company['id'], jwt_token)
|
||||
client = next((c for c in clients_data['clients'] if c['name'] == entity_name), None)
|
||||
|
||||
# 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 negasit", show_alert=True)
|
||||
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 app.bot.helpers import get_client_invoices
|
||||
invoices = await get_client_invoices(company['id'], full_client_name, jwt_token)
|
||||
|
||||
# Format response
|
||||
from app.bot.formatters import format_client_detail_response
|
||||
from app.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'])
|
||||
keyboard = create_action_buttons(f"client_detail:{entity_name}", show_export=False, show_back=True)
|
||||
|
||||
# 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,
|
||||
@@ -1401,26 +1540,35 @@ async def handle_details_callback(query, telegram_user_id: int, callback_data: s
|
||||
)
|
||||
|
||||
elif detail_type == "supplier":
|
||||
# Get supplier invoices by name
|
||||
from app.bot.helpers import get_supplier_invoices
|
||||
invoices = await get_supplier_invoices(company['id'], entity_name, jwt_token)
|
||||
|
||||
# Get supplier details (from suppliers list)
|
||||
# entity_name might be truncated to 40 chars, so search by startswith
|
||||
from app.bot.helpers import get_suppliers_with_maturity
|
||||
suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token)
|
||||
supplier = next((s for s in suppliers_data['suppliers'] if s['name'] == entity_name), None)
|
||||
|
||||
# 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 negasit", show_alert=True)
|
||||
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 app.bot.helpers import get_supplier_invoices
|
||||
invoices = await get_supplier_invoices(company['id'], full_supplier_name, jwt_token)
|
||||
|
||||
# Format response
|
||||
from app.bot.formatters import format_supplier_detail_response
|
||||
from app.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'])
|
||||
keyboard = create_action_buttons(f"supplier_detail:{entity_name}", show_export=False, show_back=True)
|
||||
|
||||
# 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,
|
||||
@@ -1582,6 +1730,63 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"Companie negasita sau nu ai acces la ea."
|
||||
)
|
||||
|
||||
# ========== LOGOUT CALLBACKS ==========
|
||||
|
||||
elif callback_data == "logout_confirm":
|
||||
# Logout user (same as unlink but shows menu after)
|
||||
from app.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 app.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 app.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 app.auth.linking import unlink_user
|
||||
@@ -1595,8 +1800,8 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
|
||||
await query.edit_message_text(
|
||||
"**Cont deconectat cu succes**\n\n"
|
||||
"Datele tale au fost sterse din sistem.\n\n"
|
||||
"Pentru a te reconecta, foloseste /start <cod>",
|
||||
"Datele tale au fost șterse din sistem.\n\n"
|
||||
"Pentru a te reconecta, folosește `/start CODUL_TAU`",
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
else:
|
||||
@@ -1616,16 +1821,16 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
elif callback_data == "login_help":
|
||||
# Show detailed help on how to get link code
|
||||
await query.edit_message_text(
|
||||
"**Cum obtin codul de link?**\n\n"
|
||||
"1. Logheaza-te in aplicatia web ROA2WEB\n"
|
||||
"2. Mergi la: Setari -> Telegram Linking\n"
|
||||
"3. Apasa \"Genereaza Cod\"\n"
|
||||
"**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 expira in 15 minute.",
|
||||
"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("« Inapoi la Bun Venit", callback_data="login_back")]
|
||||
[InlineKeyboardButton("« Înapoi la Meniu", callback_data="action:menu")]
|
||||
]),
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
@@ -1845,6 +2050,62 @@ async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
# 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 app.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,
|
||||
|
||||
@@ -187,7 +187,8 @@ def get_menu_message(
|
||||
|
||||
def create_main_menu(
|
||||
company_name: Optional[str] = None,
|
||||
company_cui: Optional[str] = None
|
||||
company_cui: Optional[str] = None,
|
||||
is_authenticated: bool = True
|
||||
) -> InlineKeyboardMarkup:
|
||||
"""
|
||||
Create main menu keyboard (Level 1) with financial options.
|
||||
@@ -197,6 +198,7 @@ def create_main_menu(
|
||||
Args:
|
||||
company_name: Active company name, or None if no company selected
|
||||
company_cui: Company fiscal code (CUI), or None
|
||||
is_authenticated: Whether user is authenticated (affects Login/Logout button)
|
||||
|
||||
Returns:
|
||||
InlineKeyboardMarkup with main menu buttons
|
||||
@@ -240,48 +242,52 @@ def create_main_menu(
|
||||
]
|
||||
])
|
||||
|
||||
# Row 5: Help button (full width)
|
||||
keyboard.append([
|
||||
InlineKeyboardButton("Help", callback_data="action:help")
|
||||
])
|
||||
# Row 5: Help/Logout buttons (authenticated) or Login button (non-authenticated)
|
||||
if is_authenticated:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton("Help", callback_data="action:help"),
|
||||
InlineKeyboardButton("Logout", callback_data="action:logout")
|
||||
])
|
||||
else:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton("Login", callback_data="action:login")
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(keyboard)
|
||||
|
||||
|
||||
def create_action_buttons(current_view: str, show_export: bool = True, show_back: bool = False) -> InlineKeyboardMarkup:
|
||||
def create_action_buttons(current_view: str, show_export: bool = True, show_back: bool = False, show_refresh: bool = True) -> InlineKeyboardMarkup:
|
||||
"""
|
||||
Create action buttons for responses (Refresh, Export, Back, Menu).
|
||||
|
||||
Layout (buttons made wide by message text padding):
|
||||
[Refresh] [Export] (if show_export=True)
|
||||
[Refresh] [Export] (if show_refresh=True and show_export=True)
|
||||
[Refresh] (if show_refresh=True and show_export=False)
|
||||
[Înapoi] (if show_back=True, full width)
|
||||
[Menu] (full width)
|
||||
|
||||
Or:
|
||||
[Refresh] (if show_export=False)
|
||||
[Înapoi] (if show_back=True, full width)
|
||||
[Menu] (full width)
|
||||
[Menu] (full width, always shown)
|
||||
|
||||
Args:
|
||||
current_view: View identifier for refresh callback (e.g., "sold", "clienti")
|
||||
show_export: Whether to show Export button
|
||||
show_back: Whether to show Back button to list
|
||||
show_refresh: Whether to show Refresh button
|
||||
|
||||
Returns:
|
||||
InlineKeyboardMarkup with action buttons
|
||||
"""
|
||||
keyboard = []
|
||||
|
||||
# Row 1: Refresh and optionally Export
|
||||
if show_export:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton("Refresh", callback_data=f"action:refresh:{current_view}"),
|
||||
InlineKeyboardButton("Export", callback_data=f"action:export:{current_view}")
|
||||
])
|
||||
else:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton("Refresh", callback_data=f"action:refresh:{current_view}")
|
||||
])
|
||||
# Row 1: Refresh and optionally Export (only if show_refresh is True)
|
||||
if show_refresh:
|
||||
if show_export:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton("Refresh", callback_data=f"action:refresh:{current_view}"),
|
||||
InlineKeyboardButton("Export", callback_data=f"action:export:{current_view}")
|
||||
])
|
||||
else:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton("Refresh", callback_data=f"action:refresh:{current_view}")
|
||||
])
|
||||
|
||||
# Row 2: Back to List (if show_back is True)
|
||||
if show_back:
|
||||
@@ -344,10 +350,15 @@ def create_client_list_keyboard(clients: List[Dict], max_items: int = 10, page:
|
||||
balance_str = f"{balance:,.0f}" if balance else "0"
|
||||
|
||||
button_text = f"{client_name} - {balance_str} RON"
|
||||
|
||||
# Limit callback_data to 64 bytes (Telegram limit)
|
||||
# Use only first 40 chars of name to stay within limit
|
||||
safe_name = client_name[:40] if len(client_name) > 40 else client_name
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
button_text,
|
||||
callback_data=f"details:client:{client_name}:0" # name:page
|
||||
callback_data=f"details:client:{safe_name}:0" # name:page
|
||||
)
|
||||
])
|
||||
|
||||
@@ -417,10 +428,15 @@ def create_supplier_list_keyboard(suppliers: List[Dict], max_items: int = 10, pa
|
||||
balance_str = f"{balance:,.0f}" if balance else "0"
|
||||
|
||||
button_text = f"{supplier_name} - {balance_str} RON"
|
||||
|
||||
# Limit callback_data to 64 bytes (Telegram limit)
|
||||
# Use only first 40 chars of name to stay within limit
|
||||
safe_name = supplier_name[:40] if len(supplier_name) > 40 else supplier_name
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
button_text,
|
||||
callback_data=f"details:supplier:{supplier_name}:0" # name:page
|
||||
callback_data=f"details:supplier:{safe_name}:0" # name:page
|
||||
)
|
||||
])
|
||||
|
||||
@@ -480,6 +496,9 @@ def create_invoice_list_keyboard(
|
||||
"""
|
||||
keyboard = []
|
||||
|
||||
# Limit partner_name to 30 chars for Telegram callback_data limit (64 bytes)
|
||||
safe_partner_name = partner_name[:30] if len(partner_name) > 30 else partner_name
|
||||
|
||||
# Calculate pagination
|
||||
total_invoices = len(invoices)
|
||||
total_pages = (total_invoices + max_items - 1) // max_items # Ceiling division
|
||||
@@ -517,7 +536,7 @@ def create_invoice_list_keyboard(
|
||||
# Previous button
|
||||
if page > 0:
|
||||
nav_buttons.append(
|
||||
InlineKeyboardButton("< Anterior", callback_data=f"invoices_page:{partner_type}:{partner_name}:{page-1}")
|
||||
InlineKeyboardButton("< Anterior", callback_data=f"invoices_page:{partner_type}:{safe_partner_name}:{page-1}")
|
||||
)
|
||||
|
||||
# Page indicator (non-clickable)
|
||||
@@ -528,7 +547,7 @@ def create_invoice_list_keyboard(
|
||||
# Next button
|
||||
if page < total_pages - 1:
|
||||
nav_buttons.append(
|
||||
InlineKeyboardButton("Următor >", callback_data=f"invoices_page:{partner_type}:{partner_name}:{page+1}")
|
||||
InlineKeyboardButton("Următor >", callback_data=f"invoices_page:{partner_type}:{safe_partner_name}:{page+1}")
|
||||
)
|
||||
|
||||
keyboard.append(nav_buttons)
|
||||
|
||||
Reference in New Issue
Block a user