diff --git a/pyproject.toml b/pyproject.toml
index ff60057..3b7a5fd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,6 +8,7 @@ dependencies = [
"alembic>=1.17.0",
"fastapi[standard]>=0.116.0",
"python-dotenv>=1.1.1",
+ "reportlab>=4.4.4",
"sqlalchemy>=2.0.44",
"uvicorn[standard]>=0.35.0",
]
diff --git a/src/meal_manager/main.py b/src/meal_manager/main.py
index ed92a88..d20ed7e 100644
--- a/src/meal_manager/main.py
+++ b/src/meal_manager/main.py
@@ -6,7 +6,7 @@ from functools import partial
from typing import Annotated
import starlette.status as status
-from fastapi import Depends, FastAPI, HTTPException, Request
+from fastapi import Depends, FastAPI, HTTPException, Request, Response
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
@@ -21,6 +21,7 @@ from meal_manager.models import (
Subscription,
TeamRegistration,
)
+from meal_manager.pdf import build_dinner_overview_pdf
sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
@@ -332,3 +333,21 @@ async def delete_team_registration(
session.delete(session.scalars(statement).one())
session.commit()
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
+
+@app.get("/event/{event_id}/pdf")
+def get_event_attendance_pdf(event_id: int, session: SessionDep):
+
+ statement = select(Event).where(Event.id == event_id)
+ event = session.scalars(statement).one()
+
+ pdf_buffer = build_dinner_overview_pdf(event)
+
+ headers = {
+ "Content-Disposition": f"inline; filename=attendance_event_{event_id}.pdf"
+ }
+
+ return Response(
+ content=pdf_buffer.getvalue(),
+ media_type="application/pdf",
+ headers=headers
+ )
\ No newline at end of file
diff --git a/src/meal_manager/pdf.py b/src/meal_manager/pdf.py
new file mode 100644
index 0000000..72a93f2
--- /dev/null
+++ b/src/meal_manager/pdf.py
@@ -0,0 +1,101 @@
+from io import BytesIO
+
+from reportlab.lib import colors
+from reportlab.lib.pagesizes import A4, portrait
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
+
+from meal_manager.models import Event
+
+
+def build_dinner_overview_pdf(event: Event) -> BytesIO:
+ """Build a PDF with an overview of the event's attendance."""
+ # Create an in-memory PDF
+ buffer = BytesIO()
+ doc = SimpleDocTemplate(buffer, pagesize=portrait(A4), topMargin=30, bottomMargin=30, leftMargin=40, rightMargin=40)
+ styles = getSampleStyleSheet()
+ elements = []
+
+ # Title
+ title_style = styles["Title"]
+ title_style.fontSize = 16
+ title_style.spaceAfter = 20
+ elements.append(Paragraph(f"Anwesenheitsliste – {event.title} ({event.event_time.date().strftime('%d.%m.%y')})",
+ title_style))
+ elements.append(Spacer(1, 20))
+
+ # Team overview section
+ elements.append(Paragraph("Dienste", styles["Heading2"]))
+ elements.append(Spacer(1, 12))
+
+ team_data = [["Team", "Personen"]]
+ team_types = {
+ "Kochen": [r for r in event.team if r.work_type == "cooking"],
+ "Abwaschen": [r for r in event.team if r.work_type == "dishes"],
+ "Tische decken": [r for r in event.team if r.work_type == "tables"]
+ }
+
+ for team_name, registrations in team_types.items():
+ members = ", ".join(r.person_name for r in registrations)
+ team_data.append([team_name, members])
+
+ team_table = Table(team_data, repeatRows=1, colWidths=[100, '*'])
+ team_table.setStyle(TableStyle([
+ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor('#E8E8E8')),
+ ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor('#A0A0A0')),
+ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
+ ("FONTSIZE", (0, 0), (-1, -1), 10),
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
+ ("TOPPADDING", (0, 0), (-1, -1), 8),
+ ]))
+
+ elements.append(team_table)
+ elements.append(Spacer(1, 25))
+
+ sum_adults = 0
+ sum_children = 0
+ sum_small_children = 0
+ for r in event.registrations:
+ sum_adults += r.num_adult_meals
+ sum_children += r.num_children_meals
+ sum_small_children += r.num_small_children_meals
+
+ # Attendance section
+ elements.append(Paragraph("Teilnehmende", styles["Heading2"]))
+ elements.append(Paragraph(f"Gesamt: {sum_adults} Erwachsene, {sum_children} Kinder, {sum_small_children} Kleinkinder"))
+ elements.append(Spacer(1, 12))
+
+ # Table header
+ data = [["Haushalt", "Erwachsene", "Kinder >7", "Kinder <7", "Kommentar", "Anwesend?"]]
+
+ # Table rows
+ for r in event.registrations:
+ data.append([
+ r.household.name,
+ r.num_adult_meals,
+ r.num_children_meals,
+ r.num_small_children_meals,
+ Paragraph(r.comment),
+ ""
+ ])
+ for _ in range(5):
+ data.append([""] * 6)
+
+ # Create table
+ table = Table(data, repeatRows=1, colWidths=[120, 70, 60, 60, '*', 65])
+ table.setStyle(TableStyle([
+ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor('#E8E8E8')),
+ ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor('#A0A0A0')),
+ ("ALIGN", (1, 1), (-2, -1), "CENTER"),
+ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
+ ("FONTSIZE", (0, 0), (-1, -1), 10),
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
+ ("TOPPADDING", (0, 0), (-1, -1), 8),
+ ]))
+
+ elements.append(table)
+ doc.build(elements)
+ buffer.seek(0)
+ return buffer
diff --git a/src/meal_manager/templates/event.html b/src/meal_manager/templates/event.html
index 715162f..d8ea601 100644
--- a/src/meal_manager/templates/event.html
+++ b/src/meal_manager/templates/event.html
@@ -34,6 +34,9 @@
Original Rezept ansehen
{% endif %}
+
+ Druckansicht
+
{% if user and user.admin %}