""" Response formatters for bot commands. Formats API responses into user-friendly Telegram messages. """ from typing import Dict, List, Any def format_dashboard_response(data: Dict[str, Any], company_name: str = None) -> str: """ Format dashboard data for Telegram (content only, no header). Note: company_name parameter kept for backwards compatibility but not used. Use format_response_with_company() in handlers to add company header. """ text = "" # Sold total trezorerie (casa + banca) - rotunjit la leu treasury_totals = data.get('treasury_totals_by_currency', {}) sold_trezorerie = round(float(treasury_totals.get('RON', 0))) text += f"**Sold Trezorerie:** {sold_trezorerie:,}\n\n" # Sold Clienți - rotunjit la leu clienti_sold = round(float(data.get('clienti_sold_total', 0))) clienti_in_termen = round(float(data.get('clienti_sold_in_termen', 0))) clienti_restant = round(float(data.get('clienti_sold_restant', 0))) text += f"**Sold Clienți:** {clienti_sold:,}\n" text += f" - În termen: {clienti_in_termen:,}\n" text += f" - Restanță: {clienti_restant:,}\n\n" # Sold Furnizori BRUT (pentru consistență cu detaliile) - rotunjit la leu furnizori_in_termen = round(float(data.get('furnizori_sold_in_termen', 0))) furnizori_restant = round(float(data.get('furnizori_sold_restant', 0))) furnizori_sold_brut = furnizori_in_termen + furnizori_restant furnizori_avansuri = round(float(data.get('furnizori_avansuri', 0))) furnizori_sold_net = round(float(data.get('furnizori_sold_total', 0))) text += f"**Sold Furnizori:** {furnizori_sold_brut:,}\n" text += f" - În termen: {furnizori_in_termen:,}\n" text += f" - Restanță: {furnizori_restant:,}\n" if furnizori_avansuri != 0: text += f" - Avansuri: {furnizori_avansuri:,}\n" text += f" - Net (după avansuri): {furnizori_sold_net:,}" else: text += f" - Net: {furnizori_sold_net:,}" # Datorii la Buget - două secțiuni: luna precedentă și luna curentă budget_breakdown = data.get('budget_debt_breakdown', []) if budget_breakdown: grupe_prec = [g for g in budget_breakdown if round(float(g.get('datorat', g.get('precedent', 0)))) > 0] grupe_crt = [g for g in budget_breakdown if round(float(g.get('curent', 0))) > 0] if grupe_prec or grupe_crt: text += "\n\n**Datorii la Buget:**\n" if grupe_prec: total_sold = sum(round(float(g.get('sold', 0))) for g in grupe_prec) total_dat = sum(round(float(g.get('datorat', g.get('precedent', 0)))) for g in grupe_prec) sold_total_str = f"{total_sold:,}" if total_sold > 0 else "0 \u2713" text += f"\n _Precedent: dat: {total_dat:,}, sold: {sold_total_str}_\n" for g in grupe_prec: datorat = round(float(g.get('datorat', g.get('precedent', 0)))) sold = round(float(g.get('sold', 0))) label = g.get('label', '') sold_str = f"{sold:,}" if sold > 0 else "0 \u2713" text += f" {label:<6} {datorat:,} · {sold_str}\n" if grupe_crt: items = [f"{g.get('label', '')} {round(float(g.get('curent', 0))):,}" for g in grupe_crt] total_crt = sum(round(float(g.get('curent', 0))) for g in grupe_crt) text += f"\n _Curent: {' \u00b7 '.join(items)} = {total_crt:,}_\n" return text def format_invoices_response( invoices: List[Dict[str, Any]], company_name: str = None, limit: int = 10 ) -> str: """ Format invoices list for Telegram - COMPACT TABLE FORMAT. Args: invoices: List of invoice dicts company_name: Company name (kept for compatibility, not used) limit: Maximum number of invoices to display Returns: Formatted Markdown string for Telegram (compact, no emojis) """ if not invoices: return "Nu s-au gasit facturi cu aceste criterii." # Header (o singură dată) text = f"**Facturi** ({len(invoices)} total)\n\n" text += "Nr | Client | Suma | Status\n" text += "---|--------|------|-------\n" # Lista facturi - compact, o linie per factură for idx, inv in enumerate(invoices[:limit], 1): seria = inv.get('seria', '') numar = inv.get('numar', '') client = inv.get('client', 'N/A') suma = inv.get('suma_totala', 0) status = inv.get('status', 'N/A') # Truncate long client names for compact display client_short = client[:20] + "..." if len(client) > 20 else client # Status marker (no emoji) status_marker = "PLATIT" if status == "platit" else "NEPLATIT" text += f"{seria}{numar} | {client_short} | {suma:,.0f} | {status_marker}\n" if len(invoices) > limit: text += f"\n+{len(invoices) - limit} facturi" return text # ========================================================================= # FAZA 2: New Formatter Functions for Button Interface # ========================================================================= def format_treasury_casa_response(data: Dict[str, Any], company_name: str = None) -> str: """ Format treasury CASH data for Telegram (content only, no header). Args: data: Dict with casa accounts and total from treasury breakdown company_name: Company name (kept for compatibility, not used) Returns: Formatted Markdown string for Telegram Example: data = {'accounts': [...], 'total': 5000} text = format_treasury_casa_response(data) """ text = "" # Total cash balance - rotunjit la leu (0 zecimale) total_cash = round(data.get('total', 0)) text += f"**Sold Total Cash:** {total_cash:,} RON\n\n" # Cash accounts casa_accounts = data.get('accounts', []) if casa_accounts: text += "**Conturi de Casa:**\n" 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" else: text += "Nu exista conturi de casa configurate." return text def format_treasury_banca_response(data: Dict[str, Any], company_name: str = None) -> str: """ Format treasury BANK data for Telegram (content only, no header). Args: data: Dict with banca accounts and total from treasury breakdown company_name: Company name (kept for compatibility, not used) Returns: Formatted Markdown string for Telegram Example: data = {'accounts': [...], 'total': 15000} text = format_treasury_banca_response(data) """ text = "" # Total bank balance - rotunjit la leu (0 zecimale) total_bank = round(data.get('total', 0)) text += f"**Sold Total Banca:** {total_bank:,} RON\n\n" # Bank accounts bank_accounts = data.get('accounts', []) if bank_accounts: text += "**Conturi Bancare:**\n" 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" else: text += "Nu exista conturi bancare configurate." return text def format_treasury_combined_response(data: Dict[str, Any], company_name: str = None) -> str: """ Format combined treasury data (Casa + Banca) for Telegram. Shows grand total, Casa section with accounts, and Banca section with accounts in a single unified message. Compact format without section titles. Args: data: Dict with 'casa' and 'banca' keys from get_treasury_breakdown_split() company_name: Company name (kept for compatibility, not used) Returns: Formatted Markdown string with grand total and both sections Example: data = {'casa': {...}, 'banca': {...}} text = format_treasury_combined_response(data) """ def format_amount(amount: int) -> str: """Format amount with period as thousands separator (Romanian style).""" return f"{amount:,}".replace(",", ".") text = "" # Extract totals - rounded to whole RON casa_total = round(data.get('casa', {}).get('total', 0)) banca_total = round(data.get('banca', {}).get('total', 0)) grand_total = casa_total + banca_total # Grand total header text += f"**Sold Trezorerie:** {format_amount(grand_total)} RON\n\n" # Casa section - compact text += f"**Casa:** {format_amount(casa_total)} RON\n" casa_accounts = data.get('casa', {}).get('accounts', []) if casa_accounts: for acc in casa_accounts: name = acc.get('name', 'N/A') balance = round(acc.get('balance', 0)) text += f" - {name}: {format_amount(balance)} RON\n" text += "\n" # Banca section - compact text += f"**Banca:** {format_amount(banca_total)} RON\n" banca_accounts = data.get('banca', {}).get('accounts', []) if banca_accounts: for acc in banca_accounts: name = acc.get('name', 'N/A') balance = round(acc.get('balance', 0)) text += f" - {name}: {format_amount(balance)} RON\n" return text def format_clients_balance_response( clients: List[Dict[str, Any]], maturity_data: Dict[str, Any], company_name: str = None ) -> str: """ Format clients balance with maturity breakdown (content only, no header). Args: clients: List of client dicts with id, name, balance maturity_data: Dict with in_term, overdue, total company_name: Company name (kept for compatibility, not used) Returns: Formatted Markdown string for Telegram Example: clients = [{'id': 1, 'name': 'Client A', 'balance': 15000}] maturity = {'in_term': 10000, 'overdue': 5000, 'total': 15000} text = format_clients_balance_response(clients, maturity) """ text = "" # Maturity breakdown - rotunjit la leu (0 zecimale) total = round(maturity_data.get('total', 0)) in_term = round(maturity_data.get('in_term', 0)) overdue = round(maturity_data.get('overdue', 0)) text += f"**Sold Total:** {total:,} RON\n\n" text += "**Defalcare:**\n" text += f" - In termen: {in_term:,} RON\n" text += f" - Restanta: {overdue:,} RON\n\n" # Top 10 clients if clients: text += f"**Top 10 Clienti** ({len(clients)} total):\n" # Sort by balance descending sorted_clients = sorted( clients, key=lambda x: x.get('balance', 0), reverse=True ) for idx, client in enumerate(sorted_clients[:10], 1): name = client.get('name', 'N/A') balance = round(client.get('balance', 0)) text += f"{idx}. {name}: {balance:,} RON\n" if len(clients) > 10: text += f"\nApasa butonul pentru lista completa" else: text += "Nu exista clienti cu solduri." return text def format_suppliers_balance_response( suppliers: List[Dict[str, Any]], maturity_data: Dict[str, Any], company_name: str = None ) -> str: """ Format suppliers balance with maturity breakdown (content only, no header). Args: suppliers: List of supplier dicts with id, name, balance maturity_data: Dict with in_term, overdue, total company_name: Company name (kept for compatibility, not used) Returns: Formatted Markdown string for Telegram Example: suppliers = [{'id': 1, 'name': 'Supplier A', 'balance': 5000}] maturity = {'in_term': 4000, 'overdue': 1000, 'total': 5000} text = format_suppliers_balance_response(suppliers, maturity) """ text = "" # Maturity breakdown - rotunjit la leu (0 zecimale) total = round(maturity_data.get('total', 0)) in_term = round(maturity_data.get('in_term', 0)) overdue = round(maturity_data.get('overdue', 0)) text += f"**Sold Total:** {total:,} RON\n\n" text += "**Defalcare:**\n" text += f" - In termen: {in_term:,} RON\n" text += f" - Restanta: {overdue:,} RON\n\n" # Top 10 suppliers if suppliers: text += f"**Top 10 Furnizori** ({len(suppliers)} total):\n" # Sort by balance descending sorted_suppliers = sorted( suppliers, key=lambda x: x.get('balance', 0), reverse=True ) for idx, supplier in enumerate(sorted_suppliers[:10], 1): name = supplier.get('name', 'N/A') balance = round(supplier.get('balance', 0)) text += f"{idx}. {name}: {balance:,} RON\n" if len(suppliers) > 10: text += f"\nApasa butonul pentru lista completa" else: text += "Nu exista furnizori cu solduri." return text def format_cashflow_evolution_response( performance_data: Dict[str, Any], monthly_data: Dict[str, Any], company_name: str = None ) -> str: """ Format cash flow evolution data - Table format with mini-charts. Args: performance_data: Dict with current_year and previous_year YTD data monthly_data: Dict with months, incasari, plati arrays + prev year data company_name: Company name (kept for compatibility, not used) Returns: Formatted Markdown string for Telegram (monospace table) Example: YTD 2024 vs 2023: 2024 2023 Δ Trend Inc: 500,000 480,000 +4.2% ████░ Plt: 450,000 440,000 +2.3% ███░ Net: 50,000 40,000 +25.0% █████ """ text = "" # Helper functions def calc_percent_change(current: float, previous: float) -> str: """Calculate percentage change: +4.2% or -3.5%""" if previous == 0: return "+100%" if current > 0 else "0.0%" change = ((current - previous) / previous) * 100 sign = "+" if change >= 0 else "" return f"{sign}{change:.1f}%" def create_mini_chart(current: float, previous: float, width: int = 5) -> str: """Create mini bar chart: ████░ (proportional bars)""" if current == 0 and previous == 0: return "─" * width max_val = max(current, previous) if max_val == 0: return "─" * width curr_bars = int((current / max_val) * width) prev_bars = int((previous / max_val) * width) # Use filled and light blocks filled = "█" * curr_bars light = "░" * (width - curr_bars) return filled + light def get_trend_arrow(current: float, previous: float) -> str: """Get trend arrow: ↑ or ↓ or →""" if current > previous * 1.02: # More than 2% increase return "↑" elif current < previous * 0.98: # More than 2% decrease return "↓" else: return "→" # Extract YTD data current = performance_data.get('current_year', {}) previous = performance_data.get('previous_year', {}) current_year = current.get('year', '2024') previous_year = previous.get('year', '2023') inc_cur = round(current.get('incasari', 0)) plt_cur = round(current.get('plati', 0)) net_cur = round(current.get('net', 0)) inc_prev = round(previous.get('incasari', 0)) plt_prev = round(previous.get('plati', 0)) net_prev = round(previous.get('net', 0)) # YTD Table Header text += f"**YTD {current_year} vs {previous_year}:**\n" text += f"` {current_year:>10} {previous_year:>10} Δ `\n" # YTD Rows inc_pct = calc_percent_change(inc_cur, inc_prev) text += f"`Inc: {inc_cur:>10,} {inc_prev:>10,} {inc_pct:>6}`\n" plt_pct = calc_percent_change(plt_cur, plt_prev) text += f"`Plt: {plt_cur:>10,} {plt_prev:>10,} {plt_pct:>6}`\n" net_pct = calc_percent_change(net_cur, net_prev) text += f"`Net: {net_cur:>10,} {net_prev:>10,} {net_pct:>6}`\n\n" # Monthly Evolution Table - Simplified (Net only) months = monthly_data.get('months', []) incasari = monthly_data.get('incasari', []) plati = monthly_data.get('plati', []) incasari_prev = monthly_data.get('incasari_prev', []) plati_prev = monthly_data.get('plati_prev', []) if months and len(months) > 0: text += "**Evolutie Net (12 luni):**\n" text += f"` {current_year:>10} {previous_year:>10} Δ `\n" for i, month in enumerate(months): inc = incasari[i] if i < len(incasari) else 0 plt = plati[i] if i < len(plati) else 0 inc_p = incasari_prev[i] if i < len(incasari_prev) else 0 plt_p = plati_prev[i] if i < len(plati_prev) else 0 net = inc - plt net_p = inc_p - plt_p # Extract short month name (first 3 chars before apostrophe) month_short = month.split("'")[0][:3] if "'" in month else month[:3] # Calculate percentage change net_pct = calc_percent_change(net, net_p) # Format row: Luna Net'current Net'prev Δ (aligned with YTD) text += f"`{month_short:<4} {int(net):>10,} {int(net_p):>10,} {net_pct:>6}`\n" else: text += "Nu exista date lunare disponibile." return text def format_client_detail_response( client: Dict[str, Any], invoices: List[Dict[str, Any]], company_name: str = None ) -> str: """ Format client details with invoices - COMPACT TABLE FORMAT. Args: client: Dict with client info (id, name, balance) invoices: List of invoice dicts for this client company_name: Company name (kept for compatibility, not used) Returns: Formatted Markdown string for Telegram (compact, no emojis) Example: client = {'id': 1, 'name': 'Client A', 'balance': 15000} invoices = [{'id': 1, 'number': 'FV001', 'amount': 5000, 'status': 'unpaid'}] text = format_client_detail_response(client, invoices) """ client_name = client.get('name', 'N/A') balance = client.get('balance', 0) # Header with client info text = f"**{client_name}**\n" text += f"**Sold total: {balance:,.2f} RON**" if invoices and len(invoices) > 1: text += f" • {len(invoices)} facturi" text += "\n\n" # Invoices - compact table format (no emojis) if invoices: from datetime import datetime # Sort invoices by date (most recent first) sorted_invoices = sorted(invoices, key=lambda x: x.get('dataact') or datetime.min, reverse=True) # Invoice list - simple format without table text += "Facturi cu sold:\n" text += "━━━━━━━━━━━━━━━━━━━━\n" # Invoice rows - one line each, simple format for inv in sorted_invoices[:10]: # Backend returns: nract, totctva, soldfinal, datascad, dataact, achitat number = str(inv.get('nract', 'N/A')) dataact = inv.get('dataact') # Parse date - handle various formats to ensure dd.mm.yyyy if dataact: if isinstance(dataact, str): try: # Try ISO format first: "2024-10-25" or "2024-10-25 00:00:00" if '-' in dataact and len(dataact) >= 10: parsed_date = datetime.strptime(dataact[:10], '%Y-%m-%d') date_str = parsed_date.strftime('%d.%m.%Y') # Already in dd.mm.yyyy format elif '.' in dataact: date_str = dataact.split()[0][:10] # Take just date part else: date_str = dataact[:10] if len(dataact) >= 10 else dataact except: date_str = dataact[:10] if len(dataact) >= 10 else dataact else: # Datetime object - format as dd.mm.yyyy date_str = dataact.strftime('%d.%m.%Y') else: date_str = 'N/A' sold = float(inv.get('soldfinal', 0) or 0) # Simple format: Nr • Data • Sold text += f"Nr {number} • {date_str} • {sold:,.2f} RON\n" if len(invoices) > 10: text += f"\n\n+{len(invoices) - 10} facturi" else: text += "Nu exista facturi neachitate" return text def format_supplier_detail_response( supplier: Dict[str, Any], invoices: List[Dict[str, Any]], company_name: str = None ) -> str: """ Format supplier details with invoices - COMPACT TABLE FORMAT. Args: supplier: Dict with supplier info (id, name, balance) invoices: List of invoice dicts for this supplier company_name: Company name (kept for compatibility, not used) Returns: Formatted Markdown string for Telegram (compact, no emojis) Example: supplier = {'id': 1, 'name': 'Supplier A', 'balance': 5000} invoices = [{'id': 1, 'number': 'FC001', 'amount': 2000, 'status': 'unpaid'}] text = format_supplier_detail_response(supplier, invoices) """ supplier_name = supplier.get('name', 'N/A') balance = supplier.get('balance', 0) # Header with supplier info text = f"**{supplier_name}**\n" text += f"**Sold total: {balance:,.2f} RON**" if invoices and len(invoices) > 1: text += f" • {len(invoices)} facturi" text += "\n\n" # Invoices - compact table format (no emojis) if invoices: from datetime import datetime # Sort invoices by date (most recent first) sorted_invoices = sorted(invoices, key=lambda x: x.get('dataact') or datetime.min, reverse=True) # Invoice list - simple format without table text += "Facturi cu sold:\n" text += "━━━━━━━━━━━━━━━━━━━━\n" # Invoice rows - one line each, simple format for inv in sorted_invoices[:10]: # Backend returns: nract, totctva, soldfinal, datascad, dataact, achitat number = str(inv.get('nract', 'N/A')) dataact = inv.get('dataact') # Parse date - handle various formats to ensure dd.mm.yyyy if dataact: if isinstance(dataact, str): try: # Try ISO format first: "2024-10-25" or "2024-10-25 00:00:00" if '-' in dataact and len(dataact) >= 10: parsed_date = datetime.strptime(dataact[:10], '%Y-%m-%d') date_str = parsed_date.strftime('%d.%m.%Y') # Already in dd.mm.yyyy format elif '.' in dataact: date_str = dataact.split()[0][:10] # Take just date part else: date_str = dataact[:10] if len(dataact) >= 10 else dataact except: date_str = dataact[:10] if len(dataact) >= 10 else dataact else: # Datetime object - format as dd.mm.yyyy date_str = dataact.strftime('%d.%m.%Y') else: date_str = 'N/A' sold = float(inv.get('soldfinal', 0) or 0) # Simple format: Nr • Data • Sold text += f"Nr {number} • {date_str} • {sold:,.2f} RON\n" if len(invoices) > 10: text += f"\n\n+{len(invoices) - 10} facturi" else: text += "Nu exista facturi neachitate" return text # ========================================================================= # FAZA 6: Performance Footer for Cache Monitoring # ========================================================================= def add_performance_footer(message: str, cache_hit: bool, time_ms: float, cache_source: str = None) -> str: """ Add compact performance footer to bot responses. Shows data source (cached L1/L2 or database) and response time. Format: "cached L1 | 15ms", "cached L2 | 25ms" or "db | 285ms" Args: message: Existing message text cache_hit: True if data came from cache time_ms: Response time in milliseconds cache_source: Cache source ("L1" for memory, "L2" for SQLite) if cache_hit is True Returns: Message with performance footer appended Example: >>> add_performance_footer("Dashboard data...", True, 52.3, "L1") "Dashboard data...\n\ncached L1 | 52ms" >>> add_performance_footer("Dashboard data...", True, 25.8, "L2") "Dashboard data...\n\ncached L2 | 26ms" >>> add_performance_footer("Dashboard data...", False, 285.7) "Dashboard data...\n\ndb | 286ms" """ if cache_hit and cache_source: source = f"cached {cache_source}" elif cache_hit: source = "cached" # Fallback if source not provided else: source = "db" footer = f"\n\n`{source} | {time_ms:.0f}ms`" return message + footer