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>
This commit is contained in:
Claude Agent
2026-02-21 14:34:15 +00:00
parent 1366dbc11c
commit 30f55cf18b
28 changed files with 1671 additions and 520 deletions

View File

@@ -20,7 +20,7 @@ from backend.modules.telegram.auth.linking import (
get_user_companies
)
from backend.modules.telegram.agent.session import get_session_manager
from backend.modules.telegram.db.operations import update_user_last_active
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__)
@@ -1279,9 +1279,82 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
try:
telegram_user = update.effective_user
telegram_user_id = telegram_user.id
text = update.message.text.strip().upper()
text = update.message.text.strip()
logger.info(f"Text message from user {telegram_user_id}: {text}")
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)
@@ -1291,6 +1364,8 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
# (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():
@@ -1740,6 +1815,43 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str)
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):
"""
@@ -2099,6 +2211,241 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
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])
@@ -2151,6 +2498,44 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
"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":
@@ -2741,6 +3126,40 @@ async def _handle_selectcompany_view(
)
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)