diff --git a/.gitignore b/.gitignore index befef02..7095590 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__/ # Local environment files .env backend/.env +perl_mail.txt # Local SQLite databases data/*.db diff --git a/assets/news_consulenza.jpg b/assets/news_consulenza.jpg new file mode 100644 index 0000000..1af533e Binary files /dev/null and b/assets/news_consulenza.jpg differ diff --git a/assets/news_cucciolo.jpg b/assets/news_cucciolo.jpg new file mode 100644 index 0000000..fe3118d Binary files /dev/null and b/assets/news_cucciolo.jpg differ diff --git a/assets/news_gatto.jpg b/assets/news_gatto.jpg new file mode 100644 index 0000000..6984665 Binary files /dev/null and b/assets/news_gatto.jpg differ diff --git a/assets/services_dermatologia.jpg b/assets/services_dermatologia.jpg new file mode 100644 index 0000000..491bc78 Binary files /dev/null and b/assets/services_dermatologia.jpg differ diff --git a/assets/services_ecografia.jpg b/assets/services_ecografia.jpg new file mode 100644 index 0000000..ec590a9 Binary files /dev/null and b/assets/services_ecografia.jpg differ diff --git a/assets/services_endoscopia.jpg b/assets/services_endoscopia.jpg new file mode 100644 index 0000000..9173728 Binary files /dev/null and b/assets/services_endoscopia.jpg differ diff --git a/assets/services_endoscopia.webp b/assets/services_endoscopia.webp new file mode 100644 index 0000000..f1181e6 Binary files /dev/null and b/assets/services_endoscopia.webp differ diff --git a/assets/services_laboratorio.jpg b/assets/services_laboratorio.jpg new file mode 100644 index 0000000..4fbf811 Binary files /dev/null and b/assets/services_laboratorio.jpg differ diff --git a/assets/services_laparoscopia.jpg b/assets/services_laparoscopia.jpg new file mode 100644 index 0000000..8557e84 Binary files /dev/null and b/assets/services_laparoscopia.jpg differ diff --git a/assets/services_oculistica.jpg b/assets/services_oculistica.jpg new file mode 100644 index 0000000..db4db22 Binary files /dev/null and b/assets/services_oculistica.jpg differ diff --git a/assets/services_radiologia.jpg b/assets/services_radiologia.jpg new file mode 100644 index 0000000..c26d8f5 Binary files /dev/null and b/assets/services_radiologia.jpg differ diff --git a/assets/services_visite_cliniche.jpg b/assets/services_visite_cliniche.jpg new file mode 100644 index 0000000..d60e896 Binary files /dev/null and b/assets/services_visite_cliniche.jpg differ diff --git a/backend/.env.example b/backend/.env.example index 0baa0f7..c2e92bd 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -3,3 +3,11 @@ APP_ENV=development APP_HOST=127.0.0.1 APP_PORT=8000 DATABASE_URL=sqlite:///C:/devel/clinica_veterinaria_formiginese/data/clinica.db +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USERNAME= +SMTP_PASSWORD= +SMTP_USE_TLS=true +BOOKING_EMAIL_FROM= +BOOKING_EMAIL_TO= +BOOKING_EMAIL_SUBJECT_PREFIX=[Clinica Veterinaria Formiginese] diff --git a/backend/app/api/booking.py b/backend/app/api/booking.py new file mode 100644 index 0000000..c2d722b --- /dev/null +++ b/backend/app/api/booking.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter, HTTPException, status + +from app.schemas.booking import BookingRequestCreate, BookingRequestResponse +from app.services.mailer import MailConfigurationError, send_booking_request_email + + +router = APIRouter(prefix="/api", tags=["booking"]) + + +@router.post("/booking-request", response_model=BookingRequestResponse) +def create_booking_request(payload: BookingRequestCreate) -> BookingRequestResponse: + try: + send_booking_request_email(payload) + except MailConfigurationError as exc: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Il servizio email non è ancora configurato.", + ) from exc + except Exception as exc: # pragma: no cover - mail delivery path + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Si è verificato un problema durante l'invio della richiesta.", + ) from exc + + return BookingRequestResponse( + message="La richiesta è stata inviata correttamente e sarà presa in carico al più presto dal team.", + ) diff --git a/backend/app/config.py b/backend/app/config.py index bfa3480..e131a89 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -6,9 +6,11 @@ from dotenv import load_dotenv BASE_DIR = Path(__file__).resolve().parent.parent PROJECT_ROOT = BASE_DIR.parent -ENV_FILE = PROJECT_ROOT / ".env" +ENV_FILE = BASE_DIR / ".env" +ROOT_ENV_FILE = PROJECT_ROOT / ".env" -load_dotenv(ENV_FILE) +load_dotenv(ROOT_ENV_FILE) +load_dotenv(ENV_FILE, override=True) class Settings: @@ -19,6 +21,17 @@ class Settings: self.app_port = int(os.getenv("APP_PORT", "8000")) default_sqlite_path = (PROJECT_ROOT / "data" / "clinica.db").resolve() self.database_url = os.getenv("DATABASE_URL", f"sqlite:///{default_sqlite_path.as_posix()}") + self.smtp_host = os.getenv("SMTP_HOST", "smtp.gmail.com") + self.smtp_port = int(os.getenv("SMTP_PORT", "587")) + self.smtp_username = os.getenv("SMTP_USERNAME", "") + self.smtp_password = os.getenv("SMTP_PASSWORD", "") + self.smtp_use_tls = os.getenv("SMTP_USE_TLS", "true").lower() in {"1", "true", "yes", "on"} + self.booking_email_from = os.getenv("BOOKING_EMAIL_FROM", self.smtp_username) + self.booking_email_to = os.getenv("BOOKING_EMAIL_TO", "") + self.booking_email_subject_prefix = os.getenv( + "BOOKING_EMAIL_SUBJECT_PREFIX", + "[Clinica Veterinaria Formiginese]", + ) @property def sqlite_file_path(self) -> str | None: @@ -27,5 +40,18 @@ class Settings: return self.database_url.removeprefix(sqlite_prefix) return None + @property + def booking_mail_enabled(self) -> bool: + return all( + [ + self.smtp_host, + self.smtp_port, + self.smtp_username, + self.smtp_password, + self.booking_email_from, + self.booking_email_to, + ] + ) + settings = Settings() diff --git a/backend/app/main.py b/backend/app/main.py index 5ad0c70..f611b3c 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,5 +1,6 @@ from fastapi import FastAPI +from app.api.booking import router as booking_router from app.api.health import router as health_router from app.config import settings from app.db.init_db import init_db @@ -9,3 +10,4 @@ init_db() app = FastAPI(title=settings.app_name) app.include_router(health_router) +app.include_router(booking_router) diff --git a/backend/app/schemas/booking.py b/backend/app/schemas/booking.py new file mode 100644 index 0000000..b4d6a14 --- /dev/null +++ b/backend/app/schemas/booking.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel, Field + + +class BookingRequestCreate(BaseModel): + name: str = Field(min_length=2, max_length=120) + phone: str = Field(min_length=5, max_length=40) + pet_name: str = Field(default="", max_length=120) + pet_type: str = Field(default="cane", max_length=40) + service: str = Field(min_length=2, max_length=120) + date: str = Field(min_length=8, max_length=20) + time: str = Field(default="", max_length=20) + notes: str = Field(default="", max_length=2000) + + +class BookingRequestResponse(BaseModel): + success: bool = True + message: str diff --git a/backend/app/services/mailer.py b/backend/app/services/mailer.py new file mode 100644 index 0000000..ad3c9fc --- /dev/null +++ b/backend/app/services/mailer.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from datetime import datetime +from email.message import EmailMessage +import smtplib + +from app.config import settings +from app.schemas.booking import BookingRequestCreate + + +class MailConfigurationError(RuntimeError): + pass + + +def _build_booking_subject(payload: BookingRequestCreate) -> str: + return f"{settings.booking_email_subject_prefix} Nuova richiesta visita - {payload.name}" + + +def _build_booking_html(payload: BookingRequestCreate) -> str: + submitted_at = datetime.now().strftime("%d/%m/%Y %H:%M") + return f""" + +
+È stata inviata una nuova richiesta di prenotazione non vincolante dal sito web.
+| Nome e cognome | {payload.name} |
| Telefono | {payload.phone} |
| Nome animale | {payload.pet_name or "-"} |
| Tipo animale | {payload.pet_type} |
| Tipo di visita | {payload.service} |
| Data preferita | {payload.date} |
| Orario preferito | {payload.time or "Qualsiasi orario"} |
| Note | {payload.notes or "-"} |
| Inviata il | {submitted_at} |
+ Questa richiesta non costituisce conferma automatica dell'appuntamento e richiede + presa in carico da parte dello staff. +
+ + + """ + + +def _build_booking_text(payload: BookingRequestCreate) -> str: + submitted_at = datetime.now().strftime("%d/%m/%Y %H:%M") + return ( + "Nuova richiesta di prenotazione visita\n\n" + f"Nome e cognome: {payload.name}\n" + f"Telefono: {payload.phone}\n" + f"Nome animale: {payload.pet_name or '-'}\n" + f"Tipo animale: {payload.pet_type}\n" + f"Tipo di visita: {payload.service}\n" + f"Data preferita: {payload.date}\n" + f"Orario preferito: {payload.time or 'Qualsiasi orario'}\n" + f"Note: {payload.notes or '-'}\n" + f"Inviata il: {submitted_at}\n\n" + "La richiesta non costituisce conferma automatica dell'appuntamento." + ) + + +def send_booking_request_email(payload: BookingRequestCreate) -> None: + if not settings.booking_mail_enabled: + raise MailConfigurationError("Configurazione SMTP incompleta.") + + message = EmailMessage() + message["Subject"] = _build_booking_subject(payload) + message["From"] = settings.booking_email_from + message["To"] = settings.booking_email_to + message.set_content(_build_booking_text(payload)) + message.add_alternative(_build_booking_html(payload), subtype="html") + + with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=30) as server: + if settings.smtp_use_tls: + server.starttls() + server.login(settings.smtp_username, settings.smtp_password) + server.send_message(message) diff --git a/backend/requirements.txt b/backend/requirements.txt index b188404..69abdff 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,3 +4,4 @@ sqlalchemy pydantic python-dotenv aiosqlite +pydantic[email] diff --git a/clinica-app/client/index.html b/clinica-app/client/index.html index cebd12e..284fd16 100644 --- a/clinica-app/client/index.html +++ b/clinica-app/client/index.html @@ -12,9 +12,5 @@ -