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:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user