feat: Space Booking System - MVP complet
Sistem web pentru rezervarea de birouri și săli de ședință cu flux de aprobare administrativă. Stack: FastAPI + Vue.js 3 + SQLite + TypeScript Features implementate: - Autentificare JWT + Self-registration cu email verification - CRUD Spații, Utilizatori, Settings (Admin) - Calendar interactiv (FullCalendar) cu drag-and-drop - Creare rezervări cu validare (durată, program, overlap, max/zi) - Rezervări recurente (săptămânal) - Admin: aprobare/respingere/anulare cereri - Admin: creare directă rezervări (bypass approval) - Admin: editare orice rezervare - User: editare/anulare rezervări proprii - Notificări in-app (bell icon + dropdown) - Notificări email (async SMTP cu BackgroundTasks) - Jurnal acțiuni administrative (audit log) - Rapoarte avansate (utilizare, top users, approval rate) - Șabloane rezervări (booking templates) - Atașamente fișiere (upload/download) - Conflict warnings (verificare disponibilitate real-time) - Integrare Google Calendar (OAuth2) - Suport timezone (UTC storage + user preference) - 225+ teste backend Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
153
backend/app/services/email_service.py
Normal file
153
backend/app/services/email_service.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""Email service for sending booking notifications."""
|
||||
import logging
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from typing import Optional
|
||||
|
||||
import aiosmtplib
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models.booking import Booking
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def send_email(to: str, subject: str, body: str) -> bool:
|
||||
"""Send email via SMTP. Returns True if successful."""
|
||||
if not settings.smtp_enabled:
|
||||
# Development mode: just log the email
|
||||
logger.info(f"[EMAIL] To: {to}")
|
||||
logger.info(f"[EMAIL] Subject: {subject}")
|
||||
logger.info(f"[EMAIL] Body:\n{body}")
|
||||
print(f"\n--- EMAIL ---")
|
||||
print(f"To: {to}")
|
||||
print(f"Subject: {subject}")
|
||||
print(f"Body:\n{body}")
|
||||
print(f"--- END EMAIL ---\n")
|
||||
return True
|
||||
|
||||
try:
|
||||
message = MIMEMultipart()
|
||||
message["From"] = settings.smtp_from_address
|
||||
message["To"] = to
|
||||
message["Subject"] = subject
|
||||
message.attach(MIMEText(body, "plain", "utf-8"))
|
||||
|
||||
await aiosmtplib.send(
|
||||
message,
|
||||
hostname=settings.smtp_host,
|
||||
port=settings.smtp_port,
|
||||
username=settings.smtp_user if settings.smtp_user else None,
|
||||
password=settings.smtp_password if settings.smtp_password else None,
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send email to {to}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def generate_booking_email(
|
||||
booking: Booking,
|
||||
event_type: str,
|
||||
user_email: str,
|
||||
user_name: str,
|
||||
extra_data: Optional[dict] = None,
|
||||
) -> tuple[str, str]:
|
||||
"""Generate email subject and body for booking events.
|
||||
|
||||
Returns: (subject, body)
|
||||
"""
|
||||
extra_data = extra_data or {}
|
||||
space_name = booking.space.name if booking.space else "Unknown Space"
|
||||
start_str = booking.start_datetime.strftime("%d.%m.%Y %H:%M")
|
||||
end_str = booking.end_datetime.strftime("%H:%M")
|
||||
|
||||
if event_type == "created":
|
||||
subject = "Cerere Nouă de Rezervare"
|
||||
body = f"""Bună ziua,
|
||||
|
||||
O nouă cerere de rezervare necesită aprobarea dumneavoastră:
|
||||
|
||||
Utilizator: {user_name}
|
||||
Spațiu: {space_name}
|
||||
Data și ora: {start_str} - {end_str}
|
||||
Titlu: {booking.title}
|
||||
Descriere: {booking.description or 'N/A'}
|
||||
|
||||
Vă rugăm să accesați panoul de administrare pentru a aproba sau respinge această cerere.
|
||||
|
||||
Cu stimă,
|
||||
Sistemul de Rezervări
|
||||
"""
|
||||
|
||||
elif event_type == "approved":
|
||||
subject = "Rezervare Aprobată"
|
||||
body = f"""Bună ziua {user_name},
|
||||
|
||||
Rezervarea dumneavoastră a fost aprobată:
|
||||
|
||||
Spațiu: {space_name}
|
||||
Data și ora: {start_str} - {end_str}
|
||||
Titlu: {booking.title}
|
||||
|
||||
Vă așteptăm!
|
||||
|
||||
Cu stimă,
|
||||
Sistemul de Rezervări
|
||||
"""
|
||||
|
||||
elif event_type == "rejected":
|
||||
reason = extra_data.get("rejection_reason", "Nu a fost specificat")
|
||||
subject = "Rezervare Respinsă"
|
||||
body = f"""Bună ziua {user_name},
|
||||
|
||||
Rezervarea dumneavoastră a fost respinsă:
|
||||
|
||||
Spațiu: {space_name}
|
||||
Data și ora: {start_str} - {end_str}
|
||||
Titlu: {booking.title}
|
||||
Motiv: {reason}
|
||||
|
||||
Vă rugăm să contactați administratorul pentru detalii.
|
||||
|
||||
Cu stimă,
|
||||
Sistemul de Rezervări
|
||||
"""
|
||||
|
||||
elif event_type == "canceled":
|
||||
reason = extra_data.get("cancellation_reason", "Nu a fost specificat")
|
||||
subject = "Rezervare Anulată"
|
||||
body = f"""Bună ziua {user_name},
|
||||
|
||||
Rezervarea dumneavoastră a fost anulată de către administrator:
|
||||
|
||||
Spațiu: {space_name}
|
||||
Data și ora: {start_str} - {end_str}
|
||||
Titlu: {booking.title}
|
||||
Motiv: {reason}
|
||||
|
||||
Vă rugăm să contactați administratorul pentru detalii.
|
||||
|
||||
Cu stimă,
|
||||
Sistemul de Rezervări
|
||||
"""
|
||||
|
||||
else:
|
||||
subject = "Notificare Rezervare"
|
||||
body = f"Notificare despre rezervarea pentru {space_name} din {start_str}"
|
||||
|
||||
return subject, body
|
||||
|
||||
|
||||
async def send_booking_notification(
|
||||
booking: Booking,
|
||||
event_type: str,
|
||||
user_email: str,
|
||||
user_name: str,
|
||||
extra_data: Optional[dict] = None,
|
||||
) -> bool:
|
||||
"""Send booking notification email."""
|
||||
subject, body = generate_booking_email(
|
||||
booking, event_type, user_email, user_name, extra_data
|
||||
)
|
||||
return await send_email(user_email, subject, body)
|
||||
Reference in New Issue
Block a user