Phase 5 Batch 5 Task 5.18: Report Generator PDF-Support & Druck-Funktionen
- Installed WeasyPrint 69.0 for PDF generation - Created 5 Jinja2 HTML templates: contact_list, calendar_week, calendar_month, company_list, audit_log - All templates: A4 landscape, print-optimized CSS (@media print, page margins) - Extended output_format in schemas.py: added pdf and print - Created pdf_generator.py: Jinja2 + WeasyPrint pipeline with preset support - Modified routes.py: PDF/print generation, preset endpoints (/presets, /presets/generate) - Added RBAC (require_permission) to all report endpoints - Generate endpoints return StreamingResponse (file download) directly - 7 backend tests: presets listing, PDF/CSV generation, template CRUD, RBAC
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@page {
|
||||
size: A4 landscape;
|
||||
margin: 1.5cm 2cm;
|
||||
@bottom-center {
|
||||
content: "Seite " counter(page) " von " counter(pages);
|
||||
font-size: 9px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
body {
|
||||
font-family: 'Helvetica', 'Arial', sans-serif;
|
||||
font-size: 10px;
|
||||
color: #222;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
color: #1a365d;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
thead th {
|
||||
background: #1a365d;
|
||||
color: #fff;
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
tbody td {
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
tbody tr:nth-child(even) {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.action-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.action-create { background: #e6f4ea; color: #1b7334; }
|
||||
.action-update { background: #e8f0fe; color: #1a56c4; }
|
||||
.action-delete { background: #fce8e6; color: #c5221f; }
|
||||
.action-read { background: #f0f0f0; color: #555; }
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
thead { display: table-header-group; }
|
||||
tr { page-break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ title|default('Audit-Log') }}</h1>
|
||||
<div class="subtitle">Erstellt am: {{ generated_at|default('') }} | Einträge: {{ entries|length }}{% if date_from|default('') %} | Von: {{ date_from }}{% endif %}{% if date_to|default('') %} | Bis: {{ date_to }}{% endif %}</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Zeitstempel</th>
|
||||
<th>Benutzer</th>
|
||||
<th>Aktion</th>
|
||||
<th>Entität</th>
|
||||
<th>Entität-ID</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ entry.timestamp|default('') }}</td>
|
||||
<td>{{ entry.user|default('—') }}</td>
|
||||
<td>
|
||||
{% set action = entry.action|default('read') %}
|
||||
<span class="action-badge action-{{ action|lower }}">{{ action|upper }}</span>
|
||||
</td>
|
||||
<td>{{ entry.entity|default('—') }}</td>
|
||||
<td>{{ entry.entity_id|default('—') }}</td>
|
||||
<td>{{ entry.details|default('—') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if entries|length == 0 %}
|
||||
<p style="text-align:center; padding:40px; color:#999;">Keine Audit-Log-Einträge gefunden.</p>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,118 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@page {
|
||||
size: A4 landscape;
|
||||
margin: 1.5cm 1.5cm;
|
||||
@bottom-center {
|
||||
content: "Seite " counter(page) " von " counter(pages);
|
||||
font-size: 9px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
body {
|
||||
font-family: 'Helvetica', 'Arial', sans-serif;
|
||||
font-size: 9px;
|
||||
color: #222;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
color: #1a365d;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.month-grid {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.month-grid th {
|
||||
background: #1a365d;
|
||||
color: #fff;
|
||||
padding: 6px 4px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
border: 1px solid #1a365d;
|
||||
}
|
||||
.month-grid td {
|
||||
border: 1px solid #ccc;
|
||||
vertical-align: top;
|
||||
width: 14.28%;
|
||||
height: 70px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
.day-num {
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
color: #333;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.day-num.today {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
.day-num.other-month { color: #ccc; }
|
||||
.weekend { background: #fafafa; }
|
||||
.event {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
border-radius: 2px;
|
||||
padding: 1px 3px;
|
||||
font-size: 8px;
|
||||
margin: 1px 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.event.all-day { background: #10b981; }
|
||||
.event.task { background: #f59e0b; }
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
thead { display: table-header-group; }
|
||||
tr { page-break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ title|default('Monatskalender') }}</h1>
|
||||
<div class="subtitle">{{ month_name|default('') }} {{ year|default('') }} | Erstellt am: {{ generated_at|default('') }}</div>
|
||||
<table class="month-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for day_name in weekday_names|default(['Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag','Sonntag']) %}
|
||||
<th>{{ day_name }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for week in weeks %}
|
||||
<tr>
|
||||
{% for day in week %}
|
||||
<td class="{{ day.is_weekend|default(False) and 'weekend' or '' }}">
|
||||
<div class="day-num {{ day.is_today|default(False) and 'today' or '' }} {{ day.is_other_month|default(False) and 'other-month' or '' }}">{{ day.day }}</div>
|
||||
{% for event in day.events|default([]) %}
|
||||
<div class="event {{ event.type|default('') }} {{ event.all_day|default(False) and 'all-day' or '' }}">{{ event.title }}</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@page {
|
||||
size: A4 landscape;
|
||||
margin: 1.5cm 1.5cm;
|
||||
@bottom-center {
|
||||
content: "Seite " counter(page) " von " counter(pages);
|
||||
font-size: 9px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
body {
|
||||
font-family: 'Helvetica', 'Arial', sans-serif;
|
||||
font-size: 9px;
|
||||
color: #222;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
color: #1a365d;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.calendar-grid {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.calendar-grid th, .calendar-grid td {
|
||||
border: 1px solid #ccc;
|
||||
vertical-align: top;
|
||||
}
|
||||
.time-col {
|
||||
width: 50px;
|
||||
background: #f5f5f5;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
padding: 2px;
|
||||
}
|
||||
.day-header {
|
||||
background: #1a365d;
|
||||
color: #fff;
|
||||
padding: 6px 4px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.day-header .date {
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
}
|
||||
.slot {
|
||||
height: 28px;
|
||||
padding: 1px 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.event {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
border-radius: 2px;
|
||||
padding: 1px 4px;
|
||||
font-size: 8px;
|
||||
margin: 1px 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.event.all-day {
|
||||
background: #10b981;
|
||||
}
|
||||
.event.task {
|
||||
background: #f59e0b;
|
||||
}
|
||||
.weekend .day-header { background: #6b7280; }
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
thead { display: table-header-group; }
|
||||
tr { page-break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ title|default('Wochenkalender') }}</h1>
|
||||
<div class="subtitle">Woche {{ week_number|default('') }} | {{ week_start|default('') }} – {{ week_end|default('') }} | Erstellt am: {{ generated_at|default('') }}</div>
|
||||
<table class="calendar-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="time-col">Zeit</th>
|
||||
{% for day in days %}
|
||||
<th class="day-header {{ day.is_weekend|default(False) and 'weekend' or '' }}">
|
||||
{{ day.name }}
|
||||
<span class="date">{{ day.date }}</span>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for hour in hours %}
|
||||
<tr>
|
||||
<td class="time-col">{{ hour.label }}</td>
|
||||
{% for day in days %}
|
||||
<td class="slot">
|
||||
{% for event in day.events|default([]) %}
|
||||
{% if event.hour == hour.value %}
|
||||
<div class="event {{ event.type|default('') }} {{ event.all_day|default(False) and 'all-day' or '' }}">
|
||||
{{ event.title }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@page {
|
||||
size: A4 landscape;
|
||||
margin: 1.5cm 2cm;
|
||||
@bottom-center {
|
||||
content: "Seite " counter(page) " von " counter(pages);
|
||||
font-size: 9px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
body {
|
||||
font-family: 'Helvetica', 'Arial', sans-serif;
|
||||
font-size: 11px;
|
||||
color: #222;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
color: #1a365d;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
thead th {
|
||||
background: #1a365d;
|
||||
color: #fff;
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
tbody td {
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
tbody tr:nth-child(even) {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
thead { display: table-header-group; }
|
||||
tr { page-break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ title|default('Firmenliste') }}</h1>
|
||||
<div class="subtitle">Erstellt am: {{ generated_at|default('') }} | Anzahl: {{ companies|length }}</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Firmenname</th>
|
||||
<th>Adresse</th>
|
||||
<th>PLZ / Ort</th>
|
||||
<th>Telefon</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Ansprechpartner</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for company in companies %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ company.name|default('') }}</td>
|
||||
<td>{{ company.address|default('—') }}</td>
|
||||
<td>{{ company.zip|default('') }} {{ company.city|default('') }}</td>
|
||||
<td>{{ company.phone|default('—') }}</td>
|
||||
<td>{{ company.email|default('—') }}</td>
|
||||
<td>{{ company.contact_person|default('—') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if companies|length == 0 %}
|
||||
<p style="text-align:center; padding:40px; color:#999;">Keine Firmen gefunden.</p>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@page {
|
||||
size: A4 landscape;
|
||||
margin: 1.5cm 2cm;
|
||||
@bottom-center {
|
||||
content: "Seite " counter(page) " von " counter(pages);
|
||||
font-size: 9px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
body {
|
||||
font-family: 'Helvetica', 'Arial', sans-serif;
|
||||
font-size: 11px;
|
||||
color: #222;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
color: #1a365d;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
thead th {
|
||||
background: #1a365d;
|
||||
color: #fff;
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
tbody td {
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
tbody tr:nth-child(even) {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.contact-type {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.type-customer { background: #e6f4ea; color: #1b7334; }
|
||||
.type-supplier { background: #fef7e0; color: #8a6d00; }
|
||||
.type-partner { background: #e8f0fe; color: #1a56c4; }
|
||||
.type-other { background: #f0f0f0; color: #555; }
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
thead { display: table-header-group; }
|
||||
tr { page-break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ title|default('Kontaktliste') }}</h1>
|
||||
<div class="subtitle">Erstellt am: {{ generated_at|default('') }} | Anzahl: {{ contacts|length }}</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Telefon</th>
|
||||
<th>Typ</th>
|
||||
<th>Firma</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for contact in contacts %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ contact.name|default('') }}</td>
|
||||
<td>{{ contact.email|default('—') }}</td>
|
||||
<td>{{ contact.phone|default('—') }}</td>
|
||||
<td>
|
||||
{% set ctype = contact.type|default('other') %}
|
||||
<span class="contact-type type-{{ ctype }}">{{ ctype|capitalize }}</span>
|
||||
</td>
|
||||
<td>{{ contact.company|default('—') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if contacts|length == 0 %}
|
||||
<p style="text-align:center; padding:40px; color:#999;">Keine Kontakte gefunden.</p>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user