dak.c2s/backend/app/services/pdf_guide.py
CCS Admin 3afe03b0a9 feat: auto-send invitation email with PDF onboarding guide
When an admin creates an invitation with an email address, an email is
now automatically sent containing the registration link and an attached
PDF guide (3 pages: registration steps, feature overview, contact info).

- Add fpdf2 for PDF generation with Unicode font support
- Add PDF guide generator (backend/app/services/pdf_guide.py)
- Extend send_email() to support file attachments
- Fire-and-forget email in create_invitation endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 11:05:19 +00:00

239 lines
7.6 KiB
Python

"""PDF guide generator for DAK onboarding invitations."""
from __future__ import annotations
from pathlib import Path
from fpdf import FPDF
ASSETS_DIR = Path(__file__).resolve().parent.parent / "assets"
LOGO_PATH = ASSETS_DIR / "dak_logo.png"
FONT_DIR = ASSETS_DIR
# Font family name used throughout the PDF
_FONT = "LiberationSans"
# Colours (DAK blue tones)
DAK_BLUE = (0, 82, 147)
DAK_DARK = (33, 37, 41)
DAK_GREY = (108, 117, 125)
DAK_LIGHT_BG = (240, 244, 248)
class _GuidePDF(FPDF):
"""Custom FPDF subclass with shared header/footer styling."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Register Liberation Sans (Unicode TTF) in Regular, Bold, Italic
self.add_font(_FONT, "", str(FONT_DIR / "LiberationSans-Regular.ttf"))
self.add_font(_FONT, "B", str(FONT_DIR / "LiberationSans-Bold.ttf"))
self.add_font(_FONT, "I", str(FONT_DIR / "LiberationSans-Italic.ttf"))
def header(self):
if self.page_no() == 1:
return # Page 1 has its own header with logo
self.set_font(_FONT, "I", 8)
self.set_text_color(*DAK_GREY)
self.cell(0, 8, "DAK Zweitmeinungs-Portal \u2014 Anleitung", align="R")
self.ln(12)
def footer(self):
self.set_y(-15)
self.set_font(_FONT, "I", 8)
self.set_text_color(*DAK_GREY)
self.cell(0, 10, f"Seite {self.page_no()}/{{nb}}", align="C")
# -- helpers ---------------------------------------------------------------
def section_title(self, text: str) -> None:
self.set_font(_FONT, "B", 14)
self.set_text_color(*DAK_BLUE)
self.cell(0, 10, text)
self.ln(8)
# blue underline
self.set_draw_color(*DAK_BLUE)
self.set_line_width(0.5)
self.line(self.l_margin, self.get_y(), self.w - self.r_margin, self.get_y())
self.ln(6)
def body_text(self, text: str) -> None:
self.set_font(_FONT, "", 10)
self.set_text_color(*DAK_DARK)
self.multi_cell(0, 6, text)
self.ln(2)
def numbered_step(self, number: int, text: str) -> None:
self.set_font(_FONT, "B", 10)
self.set_text_color(*DAK_BLUE)
self.cell(8, 6, f"{number}.")
self.set_font(_FONT, "", 10)
self.set_text_color(*DAK_DARK)
self.multi_cell(0, 6, text)
self.ln(1)
def bullet_item(self, title: str, description: str) -> None:
self.set_font(_FONT, "B", 10)
self.set_text_color(*DAK_DARK)
self.cell(4, 6, "\u2022 ")
self.cell(0, 6, title)
self.ln(6)
if description:
self.set_font(_FONT, "", 9)
self.set_text_color(*DAK_GREY)
self.set_x(self.l_margin + 6)
self.multi_cell(0, 5, description)
self.ln(2)
def info_box(self, text: str) -> None:
self.set_fill_color(*DAK_LIGHT_BG)
self.set_draw_color(*DAK_BLUE)
x = self.l_margin
y = self.get_y()
w = self.w - self.l_margin - self.r_margin
self.set_font(_FONT, "", 9)
self.set_text_color(*DAK_DARK)
# calculate height
self.set_xy(x + 4, y + 4)
self.multi_cell(w - 8, 5, text)
h = self.get_y() - y + 4
# draw box behind
self.rect(x, y, w, h, style="DF")
# re-render text on top
self.set_xy(x + 4, y + 4)
self.multi_cell(w - 8, 5, text)
self.ln(4)
def generate_guide_pdf(invite_url: str) -> bytes:
"""Generate the DAK onboarding guide as PDF bytes.
Parameters
----------
invite_url:
The personalised invitation/registration URL to embed in the document.
Returns
-------
bytes
The raw PDF content (no disk I/O).
"""
pdf = _GuidePDF(orientation="P", unit="mm", format="A4")
pdf.alias_nb_pages()
pdf.set_auto_page_break(auto=True, margin=20)
pdf.set_left_margin(20)
pdf.set_right_margin(20)
# ── Page 1: Welcome & Registration ────────────────────────────────────
pdf.add_page()
# Logo
if LOGO_PATH.exists():
pdf.image(str(LOGO_PATH), x=75, y=15, w=60)
pdf.ln(50)
else:
pdf.ln(15)
# Title
pdf.set_font(_FONT, "B", 20)
pdf.set_text_color(*DAK_BLUE)
pdf.cell(0, 12, "Willkommen im", align="C")
pdf.ln(12)
pdf.cell(0, 12, "DAK Zweitmeinungs-Portal", align="C")
pdf.ln(20)
# Registration section
pdf.section_title("Konto erstellen")
pdf.body_text(
"Um Ihr Konto zu erstellen, folgen Sie bitte diesen Schritten:"
)
pdf.numbered_step(1, "Registrierungslink im Browser öffnen (siehe unten)")
pdf.numbered_step(2, "Benutzername festlegen (mindestens 3 Zeichen)")
pdf.numbered_step(3, "E-Mail-Adresse eingeben")
pdf.numbered_step(4, "Passwort setzen (mindestens 8 Zeichen)")
pdf.numbered_step(5, "\u201eRegistrieren\u201c anklicken")
pdf.ln(2)
# Invite URL box
pdf.set_font(_FONT, "B", 10)
pdf.set_text_color(*DAK_BLUE)
pdf.cell(0, 6, "Ihr Registrierungslink:")
pdf.ln(7)
pdf.info_box(invite_url)
# First login
pdf.section_title("Erste Anmeldung")
pdf.body_text(
"Melden Sie sich mit Ihrem Benutzernamen (oder E-Mail-Adresse) "
"und dem gewählten Passwort an."
)
pdf.body_text(
"Empfehlung: Aktivieren Sie die Zwei-Faktor-Authentifizierung "
"unter Kontoverwaltung für zusätzliche Sicherheit."
)
# ── Page 2: Feature overview ──────────────────────────────────────────
pdf.add_page()
pdf.section_title("Funktionsübersicht")
pdf.body_text(
"Das DAK Zweitmeinungs-Portal bietet Ihnen folgende Funktionen:"
)
pdf.ln(2)
features = [
("Dashboard", "Übersicht über KPIs, Diagramme und Jahresvergleich."),
("Fälle", "Fallübersicht mit Such- und Filterfunktionen."),
("ICD-Eingabe", "ICD-Codes für Fälle erfassen und bearbeiten."),
("Berichte", "Wochenberichte als Excel-Datei herunterladen."),
(
"Wochenübersicht",
"Datenübersicht pro Kalenderwoche und ICD-Upload per Excel.",
),
(
"Freigaben",
"Personenbezogene Daten bei Bedarf anfordern (24 Stunden Gültigkeit).",
),
(
"Kontoverwaltung",
"Profil bearbeiten, Passwort ändern, Zwei-Faktor-Authentifizierung.",
),
]
for title, desc in features:
pdf.bullet_item(title, desc)
# ── Page 3: Closing notes ─────────────────────────────────────────────
pdf.add_page()
pdf.section_title("Weitere Informationen")
pdf.body_text(
"Ausführliche Anleitungen zu allen Funktionen finden Sie direkt "
"im Portal unter dem Menüpunkt \u201eAnleitung\u201c."
)
pdf.ln(4)
pdf.body_text(
"Bei Fragen oder Problemen wenden Sie sich bitte an Ihren "
"Ansprechpartner bei Complex Care Solutions:"
)
pdf.ln(2)
pdf.set_font(_FONT, "B", 10)
pdf.set_text_color(*DAK_DARK)
pdf.cell(0, 6, "Complex Care Solutions GmbH")
pdf.ln(6)
pdf.set_font(_FONT, "", 10)
pdf.cell(0, 6, "E-Mail: info@complexcaresolutions.de")
pdf.ln(6)
pdf.cell(0, 6, "Portal: https://dak.complexcaresolutions.de")
pdf.ln(16)
# Final info box
pdf.info_box(
"Dieses Dokument wurde automatisch generiert und enthält "
"Ihren persönlichen Registrierungslink. "
"Bitte geben Sie diesen Link nicht an Dritte weiter."
)
return pdf.output()