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

@@ -2,8 +2,7 @@
Async SMTP Email Service with retry logic and proper error handling
"""
import aiosmtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.message import EmailMessage
import os
import logging
from typing import Optional
@@ -52,32 +51,33 @@ class EmailService:
Raises:
No exceptions - returns False on all failures
"""
subject = "Codul tău de autentificare ROA2WEB"
html_body = self._create_email_template(code, username)
subject = "Autentificare ROA2WEB"
text_body = self._create_email_template(code, username)
for attempt in range(1, self.max_retries + 1):
try:
await self._send_email(to_email, subject, html_body)
await self._send_email(to_email, subject, text_body)
logger.info(
f"Email sent successfully to {to_email} "
f"(attempt {attempt}/{self.max_retries})"
f"[EMAIL] ✅ Sent auth code to {to_email} "
f"(attempt {attempt}/{self.max_retries}) via {self.smtp_host}:{self.smtp_port}"
)
return True
except aiosmtplib.SMTPException as e:
logger.warning(
f"SMTP error on attempt {attempt}/{self.max_retries}: {e}"
logger.error(
f"[EMAIL] ❌ Attempt {attempt}/{self.max_retries} failed for {to_email}: "
f"{type(e).__name__}: {e}"
)
if attempt < self.max_retries:
# Exponential backoff: 2s, 4s, 8s
delay = self.retry_delay * (2 ** (attempt - 1))
logger.info(f"Retrying in {delay}s...")
logger.info(f"[EMAIL] Retrying in {delay}s...")
await asyncio.sleep(delay)
else:
logger.error(f"Failed to send email to {to_email} after {self.max_retries} attempts")
logger.error(f"[EMAIL] ❌ All {self.max_retries} attempts failed for {to_email}")
except Exception as e:
logger.error(f"Unexpected error sending email: {e}", exc_info=True)
logger.error(f"[EMAIL] ❌ Unexpected error on attempt {attempt}/{self.max_retries} for {to_email}: {type(e).__name__}: {e}", exc_info=True)
return False
return False
@@ -86,29 +86,24 @@ class EmailService:
self,
to_email: str,
subject: str,
html_body: str
text_body: str
) -> None:
"""
Internal async SMTP sender
Internal async SMTP sender (plain text to avoid spam filters)
Raises:
aiosmtplib.SMTPException: On SMTP errors
"""
message = MIMEMultipart("alternative")
message = EmailMessage()
message["From"] = f"{self.from_name} <{self.from_email}>"
message["To"] = to_email
message["Subject"] = subject
message.set_content(text_body)
# Attach HTML body
html_part = MIMEText(html_body, "html", "utf-8")
message.attach(html_part)
# Send via async SMTP with STARTTLS
# Using start_tls parameter for automatic STARTTLS handling
smtp = aiosmtplib.SMTP(
hostname=self.smtp_host,
port=self.smtp_port,
start_tls=self.use_tls, # Use start_tls instead of use_tls
start_tls=self.use_tls,
timeout=30
)
@@ -123,132 +118,15 @@ class EmailService:
pass
def _create_email_template(self, code: str, username: str) -> str:
"""Generate HTML email template"""
return f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}}
.container {{
max-width: 600px;
margin: 0 auto;
background: white;
}}
.header {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px 20px;
text-align: center;
}}
.header h1 {{
margin: 0;
font-size: 28px;
}}
.content {{
padding: 40px 20px;
}}
.code-box {{
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border: 3px solid #667eea;
border-radius: 12px;
padding: 30px;
margin: 30px 0;
text-align: center;
}}
.code {{
font-size: 42px;
font-weight: bold;
letter-spacing: 12px;
color: #667eea;
font-family: 'Courier New', monospace;
display: block;
margin: 15px 0;
}}
.warning {{
background-color: #fff3cd;
border-left: 4px solid #ffc107;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}}
.footer {{
text-align: center;
color: #666;
font-size: 12px;
padding: 20px;
border-top: 1px solid #e0e0e0;
background-color: #f9f9f9;
}}
.button {{
display: inline-block;
padding: 12px 24px;
background-color: #667eea;
color: white;
text-decoration: none;
border-radius: 6px;
margin-top: 20px;
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>ROA2WEB</h1>
<p style="margin: 10px 0 0 0; opacity: 0.9;">Autentificare Telegram Bot</p>
</div>
<div class="content">
<p>Salut <strong>{username}</strong>,</p>
<p>Ai solicitat autentificarea în aplicația ROA2WEB Telegram Bot.</p>
<div class="code-box">
<p style="margin: 0; font-size: 14px; color: #666; font-weight: 500;">
Codul tău de autentificare:
</p>
<span class="code">{code}</span>
<p style="margin: 0; font-size: 12px; color: #888;">
Introdu acest cod în conversația Telegram
</p>
</div>
<div class="warning">
<strong>Important:</strong> Acest cod expiră în <strong>5 minute</strong>
și poate fi folosit o singură dată.
</div>
<p>După introducerea codului, vei fi solicitat să introduci parola ta Oracle.</p>
<hr style="border: none; border-top: 1px solid #e0e0e0; margin: 30px 0;">
<p style="font-size: 14px; color: #666;">
<strong>Nu ai solicitat acest cod?</strong><br>
Dacă nu ai inițiat această autentificare, poți ignora acest email în siguranță.
Nimeni nu va avea acces la contul tău fără parola ta Oracle.
</p>
</div>
<div class="footer">
<p><strong>ROA2WEB</strong> - ERP Reports Application</p>
<p>Acest email a fost trimis automat. Te rugăm să nu răspunzi.</p>
<p style="margin-top: 10px; color: #999;">
© 2025 ROA2WEB. All rights reserved.
</p>
</div>
</div>
</body>
</html>
"""
"""Generate plain text email body (HTML blocked by spam filters)"""
return (
f"Codul tau de autentificare ROA2WEB:\n\n"
f" {code}\n\n"
f"Introdu acest cod in Telegram. Expira in 5 minute.\n\n"
f"---\n"
f"Solicitat pentru: {username}\n"
f"Daca nu ai initiat aceasta autentificare, ignora acest email."
)
# Singleton instance