- 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>
643 lines
22 KiB
Python
643 lines
22 KiB
Python
"""
|
|
Menu builders for Telegram bot inline keyboards.
|
|
|
|
This module provides functions to create InlineKeyboardMarkup objects
|
|
for different menu levels and navigation patterns in the bot.
|
|
|
|
NOTE: All button texts are plain text WITHOUT emojis/icons as per requirements.
|
|
|
|
BUTTON WIDTH: Inline keyboard width is determined by the message text width.
|
|
To make buttons wider, we pad message text with invisible characters.
|
|
"""
|
|
|
|
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
|
from typing import List, Dict, Optional
|
|
from datetime import datetime
|
|
|
|
# ============================================================================
|
|
# IMPORTANT: BUTTON WIDTH CONFIGURATION
|
|
# ============================================================================
|
|
# Inline keyboard button width is determined by MESSAGE TEXT WIDTH!
|
|
# DO NOT REMOVE PADDING - it makes buttons wide like BotFather!
|
|
# ============================================================================
|
|
|
|
# Zero-Width Joiner character - invisible but prevents Telegram from trimming spaces
|
|
# This character has ZERO width (invisible) but prevents space trimming
|
|
ZERO_WIDTH_JOINER = '\u200D'
|
|
|
|
# Target character count per line to make buttons VERY WIDE
|
|
# Higher value = wider buttons (BotFather uses ~45-50 chars)
|
|
# DO NOT DECREASE THIS VALUE - buttons will become narrow!
|
|
TARGET_WIDTH = 50 # Increased from 40 to make buttons WIDER
|
|
|
|
# Enable/disable padding globally (useful for testing)
|
|
# KEEP THIS TRUE - disabling makes buttons narrow!
|
|
ENABLE_BUTTON_PADDING = True
|
|
|
|
|
|
def _get_current_month_ro() -> str:
|
|
"""Get current month name in Romanian."""
|
|
months_ro = {
|
|
1: "Ianuarie", 2: "Februarie", 3: "Martie", 4: "Aprilie",
|
|
5: "Mai", 6: "Iunie", 7: "Iulie", 8: "August",
|
|
9: "Septembrie", 10: "Octombrie", 11: "Noiembrie", 12: "Decembrie"
|
|
}
|
|
now = datetime.now()
|
|
return f"{months_ro[now.month]} {now.year}"
|
|
|
|
|
|
def _pad_line_for_wide_buttons(text: str, target_width: int = TARGET_WIDTH) -> str:
|
|
"""
|
|
Pad a single line of text with invisible characters to make inline buttons wider.
|
|
|
|
⚠️ CRITICAL: DO NOT REMOVE THIS FUNCTION - it makes buttons wide!
|
|
The width of InlineKeyboardMarkup buttons is determined by the message text width.
|
|
By padding text with spaces + zero-width joiner, we force wider buttons.
|
|
|
|
How it works:
|
|
1. Calculate how many characters needed to reach target_width
|
|
2. Add spaces + Zero-Width Joiner (invisible character)
|
|
3. Result: wider message = wider buttons (like BotFather)
|
|
|
|
Args:
|
|
text: The text line to pad
|
|
target_width: Target character count (default 50 for VERY WIDE buttons)
|
|
|
|
Returns:
|
|
Padded text with invisible characters (user sees normal text, Telegram sees wider text)
|
|
"""
|
|
current_length = len(text)
|
|
if current_length >= target_width:
|
|
return text
|
|
|
|
# ⚠️ DO NOT REMOVE: Add spaces + zero-width joiner at the end
|
|
# This makes buttons WIDE without changing visible text!
|
|
padding_needed = target_width - current_length
|
|
padding = ' ' * padding_needed + ZERO_WIDTH_JOINER
|
|
|
|
return text + padding
|
|
|
|
|
|
def pad_message_for_wide_buttons(message: str, target_width: int = TARGET_WIDTH, force: bool = False) -> str:
|
|
"""
|
|
Pad all lines in a message to make inline keyboard buttons wider.
|
|
|
|
⚠️ CRITICAL: DO NOT REMOVE THIS FUNCTION - it makes buttons wide!
|
|
This is the MAIN function that applies padding to ALL messages with keyboards.
|
|
|
|
Why we need this:
|
|
- Telegram determines button width based on MESSAGE TEXT width
|
|
- Short messages = narrow buttons
|
|
- Wide messages (with invisible padding) = WIDE buttons like BotFather
|
|
|
|
Args:
|
|
message: Multi-line message text
|
|
target_width: Target character count per line (default 50)
|
|
force: Force padding even if ENABLE_BUTTON_PADDING is False
|
|
|
|
Returns:
|
|
Message with all lines padded (if enabled or forced)
|
|
"""
|
|
# ⚠️ DO NOT REMOVE: Check if padding is enabled
|
|
if not ENABLE_BUTTON_PADDING and not force:
|
|
return message
|
|
|
|
# ⚠️ DO NOT REMOVE: Apply padding to each line
|
|
lines = message.split('\n')
|
|
padded_lines = [_pad_line_for_wide_buttons(line, target_width) for line in lines]
|
|
return '\n'.join(padded_lines)
|
|
|
|
|
|
def format_response_with_company(
|
|
content: str,
|
|
company_name: Optional[str] = None,
|
|
apply_padding: bool = True
|
|
) -> str:
|
|
"""
|
|
Format a response with company name at the top (simplified format).
|
|
|
|
⚠️ IMPORTANT: Applies padding by default to make buttons WIDE!
|
|
|
|
Format:
|
|
Company Name
|
|
|
|
[Content]
|
|
|
|
Args:
|
|
content: The main content text
|
|
company_name: Company name to show at top (if None, just returns content)
|
|
apply_padding: Whether to apply invisible padding for wider buttons (default TRUE)
|
|
|
|
Returns:
|
|
Formatted response with company name header AND padding for wide buttons
|
|
"""
|
|
if company_name:
|
|
message = f"{company_name}\n\n{content}"
|
|
else:
|
|
message = content
|
|
|
|
# ⚠️ DO NOT REMOVE: Apply padding to make inline keyboard buttons WIDE!
|
|
# Without this, buttons become narrow like before
|
|
if apply_padding:
|
|
message = pad_message_for_wide_buttons(message)
|
|
|
|
return message
|
|
|
|
|
|
def get_menu_message(
|
|
company_name: Optional[str] = None,
|
|
company_cui: Optional[str] = None,
|
|
apply_padding: bool = True
|
|
) -> str:
|
|
"""
|
|
Get the menu message text with company details (simplified format).
|
|
|
|
⚠️ IMPORTANT: Applies padding by default to make menu buttons WIDE!
|
|
|
|
Format without labels - just values:
|
|
- Line 1: Company name
|
|
- Line 2: CUI
|
|
- Line 3: Accounting month
|
|
|
|
Args:
|
|
company_name: Active company name
|
|
company_cui: Company fiscal code (CUI)
|
|
apply_padding: Whether to apply invisible padding for wider buttons (default TRUE)
|
|
|
|
Returns:
|
|
Formatted message text for menu WITH padding for wide buttons
|
|
"""
|
|
if company_name:
|
|
# Simplified format: just values, no labels
|
|
message = f"{company_name}\n"
|
|
if company_cui:
|
|
message += f"{company_cui}\n"
|
|
message += f"{_get_current_month_ro()}"
|
|
else:
|
|
# No company selected - just prompt
|
|
message = "Selectează o companie pentru a continua"
|
|
|
|
# ⚠️ DO NOT REMOVE: Apply padding to make inline keyboard buttons WIDE!
|
|
# This makes buttons look like BotFather (wide, not narrow)
|
|
if apply_padding:
|
|
message = pad_message_for_wide_buttons(message)
|
|
|
|
return message
|
|
|
|
|
|
def create_main_menu(
|
|
company_name: Optional[str] = None,
|
|
company_cui: Optional[str] = None,
|
|
is_authenticated: bool = True,
|
|
cache_enabled: Optional[bool] = None
|
|
) -> InlineKeyboardMarkup:
|
|
"""
|
|
Create main menu keyboard (Level 1) with financial options.
|
|
|
|
Layout: Full-width buttons with company selection at top
|
|
|
|
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)
|
|
cache_enabled: Cache state for user (True=ON, False=OFF, None=unknown)
|
|
|
|
Returns:
|
|
InlineKeyboardMarkup with main menu buttons
|
|
"""
|
|
keyboard = []
|
|
|
|
# Only show financial menu if authenticated
|
|
if is_authenticated:
|
|
# Row 1: Company selection (full width, single line - InlineKeyboardButton doesn't support multiline)
|
|
if company_name:
|
|
# Short company name for button (CUI and month will be shown in message text)
|
|
# Truncate long names to fit in button
|
|
max_length = 35
|
|
display_name = company_name if len(company_name) <= max_length else company_name[:max_length-3] + "..."
|
|
|
|
keyboard.append([
|
|
InlineKeyboardButton(
|
|
f"{display_name}",
|
|
callback_data="menu:select_company"
|
|
)
|
|
])
|
|
else:
|
|
keyboard.append([
|
|
InlineKeyboardButton(
|
|
"Selectare Companie",
|
|
callback_data="menu:select_company"
|
|
)
|
|
])
|
|
|
|
# Rows 2-4: Financial options (compact layout with unified Trezorerie button)
|
|
keyboard.extend([
|
|
[
|
|
InlineKeyboardButton("Sold Companie", callback_data="menu:sold"),
|
|
InlineKeyboardButton("Trezorerie", callback_data="menu:trezorerie")
|
|
],
|
|
[
|
|
InlineKeyboardButton("Sold Clienti", callback_data="menu:clienti"),
|
|
InlineKeyboardButton("Sold Furnizori", callback_data="menu:furnizori")
|
|
],
|
|
[
|
|
InlineKeyboardButton("Evolutie Incasari", callback_data="menu:evolutie")
|
|
]
|
|
])
|
|
|
|
# Row 5: Cache options (2 buttons per row, only if authenticated)
|
|
if is_authenticated:
|
|
# Dynamic cache toggle button showing current state
|
|
if cache_enabled is None:
|
|
cache_button_text = "Toggle Cache"
|
|
elif cache_enabled:
|
|
cache_button_text = "Cache: ON"
|
|
else:
|
|
cache_button_text = "Cache: OFF"
|
|
|
|
keyboard.append([
|
|
InlineKeyboardButton(cache_button_text, callback_data="menu:togglecache"),
|
|
InlineKeyboardButton("Clear Cache", callback_data="menu:clearcache")
|
|
])
|
|
|
|
# Row 6: Switch Server button (authenticated only)
|
|
if is_authenticated:
|
|
keyboard.append([
|
|
InlineKeyboardButton("Schimba Server", callback_data="menu:switch_server"),
|
|
])
|
|
|
|
# Row 7: 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, 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_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, 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 (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:
|
|
# Determine back callback based on current view
|
|
# ✅ FIX: Handle detail views (client_detail:name, supplier_detail:name)
|
|
if current_view.startswith("client_detail:"):
|
|
back_callback = "menu:clienti" # Back to client list
|
|
elif current_view.startswith("supplier_detail:"):
|
|
back_callback = "menu:furnizori" # Back to supplier list
|
|
elif current_view == "clienti":
|
|
back_callback = "clients_page:0" # Match handlers.py:1689
|
|
elif current_view == "furnizori":
|
|
back_callback = "suppliers_page:0" # Match handlers.py:1721
|
|
else:
|
|
back_callback = "action:menu" # Fallback to menu
|
|
|
|
keyboard.append([
|
|
InlineKeyboardButton("« Înapoi", callback_data=back_callback)
|
|
])
|
|
|
|
# Row 3: Back to Menu (full width)
|
|
keyboard.append([
|
|
InlineKeyboardButton("Meniu Principal", callback_data="action:menu")
|
|
])
|
|
|
|
return InlineKeyboardMarkup(keyboard)
|
|
|
|
|
|
def create_client_list_keyboard(clients: List[Dict], max_items: int = 20, page: int = 0, letter: str = None) -> InlineKeyboardMarkup:
|
|
"""
|
|
Create client list keyboard (Level 2) with client buttons and pagination.
|
|
|
|
Layout: 1 column for clients, pagination controls, 2 columns for navigation
|
|
|
|
Args:
|
|
clients: List of client dicts with keys: id, name, balance
|
|
max_items: Maximum number of clients per page (default: 10)
|
|
page: Current page number (0-indexed)
|
|
letter: Optional letter filter (e.g. "A", "ALL") - when set, uses alpha pagination
|
|
|
|
Returns:
|
|
InlineKeyboardMarkup with client list buttons and pagination
|
|
"""
|
|
keyboard = []
|
|
|
|
# Sort clients alphabetically by name
|
|
sorted_clients = sorted(clients, key=lambda x: x.get('name', '').lower())
|
|
|
|
# Calculate pagination
|
|
total_clients = len(sorted_clients)
|
|
total_pages = (total_clients + max_items - 1) // max_items # Ceiling division
|
|
start_idx = page * max_items
|
|
end_idx = min(start_idx + max_items, total_clients)
|
|
|
|
# Display clients for current page
|
|
display_clients = sorted_clients[start_idx:end_idx]
|
|
|
|
# Add client buttons (1 per row)
|
|
for client in display_clients:
|
|
client_name = client.get('name', 'N/A')
|
|
balance = client.get('balance', 0)
|
|
|
|
# Format balance with thousands separator
|
|
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:{safe_name}:0" # name:page
|
|
)
|
|
])
|
|
|
|
# Pagination controls (only if more than one page)
|
|
if total_pages > 1:
|
|
nav_buttons = []
|
|
|
|
# Choose pagination callback based on whether letter filter is active
|
|
if letter:
|
|
prev_cb = f"clients_alpha_page:{page-1}:{letter}"
|
|
next_cb = f"clients_alpha_page:{page+1}:{letter}"
|
|
else:
|
|
prev_cb = f"clients_page:{page-1}"
|
|
next_cb = f"clients_page:{page+1}"
|
|
|
|
# Previous button
|
|
if page > 0:
|
|
nav_buttons.append(
|
|
InlineKeyboardButton("< Anterior", callback_data=prev_cb)
|
|
)
|
|
|
|
# Page indicator (non-clickable)
|
|
nav_buttons.append(
|
|
InlineKeyboardButton(f"Pagina {page+1}/{total_pages}", callback_data="noop")
|
|
)
|
|
|
|
# Next button
|
|
if page < total_pages - 1:
|
|
nav_buttons.append(
|
|
InlineKeyboardButton("Urmator >", callback_data=next_cb)
|
|
)
|
|
|
|
keyboard.append(nav_buttons)
|
|
|
|
# Filtrare A-Z button
|
|
keyboard.append([
|
|
InlineKeyboardButton("Filtrare A-Z", callback_data="clients_alpha_menu")
|
|
])
|
|
|
|
# Back button: to A-Z menu if filtering, otherwise to main menu
|
|
back_callback = "clients_alpha_menu" if letter else "action:menu"
|
|
keyboard.append([
|
|
InlineKeyboardButton("< Inapoi", callback_data=back_callback)
|
|
])
|
|
|
|
return InlineKeyboardMarkup(keyboard)
|
|
|
|
|
|
def create_supplier_list_keyboard(suppliers: List[Dict], max_items: int = 20, page: int = 0, letter: str = None) -> InlineKeyboardMarkup:
|
|
"""
|
|
Create supplier list keyboard (Level 2) with supplier buttons and pagination.
|
|
|
|
Layout: 1 column for suppliers, pagination controls, 2 columns for navigation
|
|
|
|
Args:
|
|
suppliers: List of supplier dicts with keys: id, name, balance
|
|
max_items: Maximum number of suppliers per page (default: 10)
|
|
page: Current page number (0-indexed)
|
|
letter: Optional letter filter (e.g. "A", "ALL") - when set, uses alpha pagination
|
|
|
|
Returns:
|
|
InlineKeyboardMarkup with supplier list buttons and pagination
|
|
"""
|
|
keyboard = []
|
|
|
|
# Sort suppliers alphabetically by name
|
|
sorted_suppliers = sorted(suppliers, key=lambda x: x.get('name', '').lower())
|
|
|
|
# Calculate pagination
|
|
total_suppliers = len(sorted_suppliers)
|
|
total_pages = (total_suppliers + max_items - 1) // max_items # Ceiling division
|
|
start_idx = page * max_items
|
|
end_idx = min(start_idx + max_items, total_suppliers)
|
|
|
|
# Display suppliers for current page
|
|
display_suppliers = sorted_suppliers[start_idx:end_idx]
|
|
|
|
# Add supplier buttons (1 per row)
|
|
for supplier in display_suppliers:
|
|
supplier_name = supplier.get('name', 'N/A')
|
|
balance = supplier.get('balance', 0)
|
|
|
|
# Format balance with thousands separator
|
|
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:{safe_name}:0" # name:page
|
|
)
|
|
])
|
|
|
|
# Pagination controls (only if more than one page)
|
|
if total_pages > 1:
|
|
nav_buttons = []
|
|
|
|
# Choose pagination callback based on whether letter filter is active
|
|
if letter:
|
|
prev_cb = f"suppliers_alpha_page:{page-1}:{letter}"
|
|
next_cb = f"suppliers_alpha_page:{page+1}:{letter}"
|
|
else:
|
|
prev_cb = f"suppliers_page:{page-1}"
|
|
next_cb = f"suppliers_page:{page+1}"
|
|
|
|
# Previous button
|
|
if page > 0:
|
|
nav_buttons.append(
|
|
InlineKeyboardButton("< Anterior", callback_data=prev_cb)
|
|
)
|
|
|
|
# Page indicator (non-clickable)
|
|
nav_buttons.append(
|
|
InlineKeyboardButton(f"Pagina {page+1}/{total_pages}", callback_data="noop")
|
|
)
|
|
|
|
# Next button
|
|
if page < total_pages - 1:
|
|
nav_buttons.append(
|
|
InlineKeyboardButton("Urmator >", callback_data=next_cb)
|
|
)
|
|
|
|
keyboard.append(nav_buttons)
|
|
|
|
# Filtrare A-Z button
|
|
keyboard.append([
|
|
InlineKeyboardButton("Filtrare A-Z", callback_data="suppliers_alpha_menu")
|
|
])
|
|
|
|
# Back button: to A-Z menu if filtering, otherwise to main menu
|
|
back_callback = "suppliers_alpha_menu" if letter else "action:menu"
|
|
keyboard.append([
|
|
InlineKeyboardButton("< Inapoi", callback_data=back_callback)
|
|
])
|
|
|
|
return InlineKeyboardMarkup(keyboard)
|
|
|
|
|
|
def create_invoice_list_keyboard(
|
|
invoices: List[Dict],
|
|
partner_type: str,
|
|
partner_name: str,
|
|
max_items: int = 10,
|
|
page: int = 0
|
|
) -> InlineKeyboardMarkup:
|
|
"""
|
|
Create invoice list keyboard (Level 3) with invoice buttons and pagination.
|
|
|
|
Layout: 1 column for invoices, pagination controls, 2 columns for navigation
|
|
|
|
Args:
|
|
invoices: List of invoice dicts with keys: id, number, amount, status
|
|
partner_type: "CLIENTI" or "FURNIZORI"
|
|
partner_name: Client/supplier name (for back navigation)
|
|
max_items: Maximum number of invoices per page (default: 10)
|
|
page: Current page number (0-indexed)
|
|
|
|
Returns:
|
|
InlineKeyboardMarkup with invoice list buttons and pagination
|
|
"""
|
|
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
|
|
start_idx = page * max_items
|
|
end_idx = min(start_idx + max_items, total_invoices)
|
|
|
|
# Display invoices for current page
|
|
display_invoices = invoices[start_idx:end_idx]
|
|
|
|
# Add invoice buttons (1 per row)
|
|
for invoice in display_invoices:
|
|
invoice_id = invoice.get('id', 0)
|
|
invoice_number = invoice.get('number', 'N/A')
|
|
amount = invoice.get('amount', 0)
|
|
status = invoice.get('status', 'unknown')
|
|
|
|
# Format amount with thousands separator
|
|
amount_str = f"{amount:,.0f}" if amount else "0"
|
|
|
|
# Status text indicator (no emojis)
|
|
status_text = "[NEPLATIT]" if status in ['unpaid', 'overdue'] else "[PLATIT]"
|
|
|
|
button_text = f"{status_text} {invoice_number} - {amount_str} RON"
|
|
keyboard.append([
|
|
InlineKeyboardButton(
|
|
button_text,
|
|
callback_data=f"invoice:{partner_type}:{invoice_id}"
|
|
)
|
|
])
|
|
|
|
# Pagination controls (only if more than one page)
|
|
if total_pages > 1:
|
|
nav_buttons = []
|
|
|
|
# Previous button
|
|
if page > 0:
|
|
nav_buttons.append(
|
|
InlineKeyboardButton("< Anterior", callback_data=f"invoices_page:{partner_type}:{safe_partner_name}:{page-1}")
|
|
)
|
|
|
|
# Page indicator (non-clickable)
|
|
nav_buttons.append(
|
|
InlineKeyboardButton(f"Pagina {page+1}/{total_pages}", callback_data="noop")
|
|
)
|
|
|
|
# Next button
|
|
if page < total_pages - 1:
|
|
nav_buttons.append(
|
|
InlineKeyboardButton("Următor >", callback_data=f"invoices_page:{partner_type}:{safe_partner_name}:{page+1}")
|
|
)
|
|
|
|
keyboard.append(nav_buttons)
|
|
|
|
# Navigation row: Back and Export (2 buttons per row)
|
|
back_target = "clienti" if partner_type == "CLIENTI" else "furnizori"
|
|
keyboard.append([
|
|
InlineKeyboardButton("< Înapoi", callback_data=f"nav:back:{back_target}"),
|
|
InlineKeyboardButton("Export", callback_data=f"action:export:{partner_type.lower()}")
|
|
])
|
|
|
|
return InlineKeyboardMarkup(keyboard)
|
|
|
|
|
|
def create_navigation_buttons(back_to: str) -> InlineKeyboardMarkup:
|
|
"""
|
|
Create simple navigation buttons (just Back button).
|
|
|
|
Args:
|
|
back_to: Target location identifier (e.g., "menu", "clienti", "furnizori")
|
|
|
|
Returns:
|
|
InlineKeyboardMarkup with navigation button
|
|
"""
|
|
keyboard = [
|
|
[
|
|
InlineKeyboardButton(
|
|
f"< Înapoi la {back_to}",
|
|
callback_data=f"nav:back:{back_to}"
|
|
)
|
|
]
|
|
]
|
|
|
|
return InlineKeyboardMarkup(keyboard)
|