🎉 Refactor & Update Backend Code, Add Utils 🖥️📊

This commit is contained in:
2025-05-31 23:54:57 +02:00
parent d4f899d280
commit 193164964e
14 changed files with 5346 additions and 241 deletions

View File

@@ -0,0 +1,936 @@
"""
Erweitertes Tabellen-System für das MYP-System
=============================================
Dieses Modul stellt erweiterte Tabellen-Funktionalität bereit:
- Sortierung nach allen Spalten
- Erweiterte Filter-Optionen
- Pagination mit anpassbaren Seitengrößen
- Spalten-Auswahl und -anpassung
- Export-Funktionen
- Responsive Design
"""
import re
import json
import math
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Tuple, Union, Callable
from dataclasses import dataclass, asdict
from enum import Enum
from flask import request, jsonify
from sqlalchemy import func, text, or_, and_
from sqlalchemy.orm import Query
from utils.logging_config import get_logger
from models import Job, User, Printer, GuestRequest, get_db_session
logger = get_logger("advanced_tables")
class SortDirection(Enum):
ASC = "asc"
DESC = "desc"
class FilterOperator(Enum):
EQUALS = "eq"
NOT_EQUALS = "ne"
CONTAINS = "contains"
NOT_CONTAINS = "not_contains"
STARTS_WITH = "starts_with"
ENDS_WITH = "ends_with"
GREATER_THAN = "gt"
LESS_THAN = "lt"
GREATER_EQUAL = "gte"
LESS_EQUAL = "lte"
BETWEEN = "between"
IN = "in"
NOT_IN = "not_in"
IS_NULL = "is_null"
IS_NOT_NULL = "is_not_null"
@dataclass
class SortConfig:
"""Sortierung-Konfiguration"""
column: str
direction: SortDirection = SortDirection.ASC
@dataclass
class FilterConfig:
"""Filter-Konfiguration"""
column: str
operator: FilterOperator
value: Any = None
values: List[Any] = None
@dataclass
class PaginationConfig:
"""Pagination-Konfiguration"""
page: int = 1
page_size: int = 25
max_page_size: int = 100
@dataclass
class ColumnConfig:
"""Spalten-Konfiguration"""
key: str
label: str
sortable: bool = True
filterable: bool = True
searchable: bool = True
visible: bool = True
width: Optional[str] = None
align: str = "left" # left, center, right
format_type: str = "text" # text, number, date, datetime, boolean, currency
format_options: Dict[str, Any] = None
@dataclass
class TableConfig:
"""Gesamt-Tabellen-Konfiguration"""
table_id: str
columns: List[ColumnConfig]
default_sort: List[SortConfig] = None
default_filters: List[FilterConfig] = None
pagination: PaginationConfig = None
searchable: bool = True
exportable: bool = True
selectable: bool = False
row_actions: List[Dict[str, Any]] = None
class AdvancedTableQuery:
"""Builder für erweiterte Tabellen-Abfragen"""
def __init__(self, base_query: Query, model_class):
self.base_query = base_query
self.model_class = model_class
self.filters = []
self.sorts = []
self.search_term = None
self.search_columns = []
def add_filter(self, filter_config: FilterConfig):
"""Fügt einen Filter hinzu"""
self.filters.append(filter_config)
return self
def add_sort(self, sort_config: SortConfig):
"""Fügt eine Sortierung hinzu"""
self.sorts.append(sort_config)
return self
def set_search(self, term: str, columns: List[str]):
"""Setzt globale Suche"""
self.search_term = term
self.search_columns = columns
return self
def build_query(self) -> Query:
"""Erstellt die finale Query"""
query = self.base_query
# Filter anwenden
for filter_config in self.filters:
query = self._apply_filter(query, filter_config)
# Globale Suche anwenden
if self.search_term and self.search_columns:
query = self._apply_search(query)
# Sortierung anwenden
for sort_config in self.sorts:
query = self._apply_sort(query, sort_config)
return query
def _apply_filter(self, query: Query, filter_config: FilterConfig) -> Query:
"""Wendet einen Filter auf die Query an"""
column = getattr(self.model_class, filter_config.column, None)
if not column:
logger.warning(f"Spalte {filter_config.column} nicht gefunden in {self.model_class}")
return query
op = filter_config.operator
value = filter_config.value
values = filter_config.values
if op == FilterOperator.EQUALS:
return query.filter(column == value)
elif op == FilterOperator.NOT_EQUALS:
return query.filter(column != value)
elif op == FilterOperator.CONTAINS:
return query.filter(column.ilike(f"%{value}%"))
elif op == FilterOperator.NOT_CONTAINS:
return query.filter(~column.ilike(f"%{value}%"))
elif op == FilterOperator.STARTS_WITH:
return query.filter(column.ilike(f"{value}%"))
elif op == FilterOperator.ENDS_WITH:
return query.filter(column.ilike(f"%{value}"))
elif op == FilterOperator.GREATER_THAN:
return query.filter(column > value)
elif op == FilterOperator.LESS_THAN:
return query.filter(column < value)
elif op == FilterOperator.GREATER_EQUAL:
return query.filter(column >= value)
elif op == FilterOperator.LESS_EQUAL:
return query.filter(column <= value)
elif op == FilterOperator.BETWEEN and values and len(values) >= 2:
return query.filter(column.between(values[0], values[1]))
elif op == FilterOperator.IN and values:
return query.filter(column.in_(values))
elif op == FilterOperator.NOT_IN and values:
return query.filter(~column.in_(values))
elif op == FilterOperator.IS_NULL:
return query.filter(column.is_(None))
elif op == FilterOperator.IS_NOT_NULL:
return query.filter(column.isnot(None))
return query
def _apply_search(self, query: Query) -> Query:
"""Wendet globale Suche an"""
if not self.search_term or not self.search_columns:
return query
search_conditions = []
for column_name in self.search_columns:
column = getattr(self.model_class, column_name, None)
if column:
# Konvertiere zu String für Suche in numerischen Spalten
search_conditions.append(
func.cast(column, sqlalchemy.String).ilike(f"%{self.search_term}%")
)
if search_conditions:
return query.filter(or_(*search_conditions))
return query
def _apply_sort(self, query: Query, sort_config: SortConfig) -> Query:
"""Wendet Sortierung an"""
column = getattr(self.model_class, sort_config.column, None)
if not column:
logger.warning(f"Spalte {sort_config.column} für Sortierung nicht gefunden")
return query
if sort_config.direction == SortDirection.DESC:
return query.order_by(column.desc())
else:
return query.order_by(column.asc())
class TableDataProcessor:
"""Verarbeitet Tabellendaten für die Ausgabe"""
def __init__(self, config: TableConfig):
self.config = config
def process_data(self, data: List[Any]) -> List[Dict[str, Any]]:
"""Verarbeitet rohe Daten für Tabellen-Ausgabe"""
processed_rows = []
for item in data:
row = {}
for column in self.config.columns:
if not column.visible:
continue
# Wert extrahieren
value = self._extract_value(item, column.key)
# Formatieren
formatted_value = self._format_value(value, column)
row[column.key] = {
'raw': value,
'formatted': formatted_value,
'sortable': column.sortable,
'filterable': column.filterable
}
# Row Actions hinzufügen
if self.config.row_actions:
row['_actions'] = self._get_row_actions(item)
# Row Metadata
row['_id'] = getattr(item, 'id', None)
row['_type'] = item.__class__.__name__.lower()
processed_rows.append(row)
return processed_rows
def _extract_value(self, item: Any, key: str) -> Any:
"""Extrahiert Wert aus einem Objekt"""
try:
# Unterstützung für verschachtelte Attribute (z.B. "user.name")
if '.' in key:
obj = item
for part in key.split('.'):
obj = getattr(obj, part, None)
if obj is None:
break
return obj
else:
return getattr(item, key, None)
except AttributeError:
return None
def _format_value(self, value: Any, column: ColumnConfig) -> str:
"""Formatiert einen Wert basierend auf dem Spaltentyp"""
if value is None:
return ""
format_type = column.format_type
options = column.format_options or {}
if format_type == "date" and isinstance(value, datetime):
date_format = options.get('format', '%d.%m.%Y')
return value.strftime(date_format)
elif format_type == "datetime" and isinstance(value, datetime):
datetime_format = options.get('format', '%d.%m.%Y %H:%M')
return value.strftime(datetime_format)
elif format_type == "number" and isinstance(value, (int, float)):
decimals = options.get('decimals', 0)
return f"{value:.{decimals}f}"
elif format_type == "currency" and isinstance(value, (int, float)):
currency = options.get('currency', '')
decimals = options.get('decimals', 2)
return f"{value:.{decimals}f} {currency}"
elif format_type == "boolean":
true_text = options.get('true_text', 'Ja')
false_text = options.get('false_text', 'Nein')
return true_text if value else false_text
elif format_type == "truncate":
max_length = options.get('max_length', 50)
text = str(value)
if len(text) > max_length:
return text[:max_length-3] + "..."
return text
return str(value)
def _get_row_actions(self, item: Any) -> List[Dict[str, Any]]:
"""Generiert verfügbare Aktionen für eine Zeile"""
actions = []
for action_config in self.config.row_actions:
# Prüfe Bedingungen für Aktion
if self._check_action_condition(item, action_config):
actions.append({
'type': action_config['type'],
'label': action_config['label'],
'icon': action_config.get('icon'),
'url': self._build_action_url(item, action_config),
'method': action_config.get('method', 'GET'),
'confirm': action_config.get('confirm'),
'class': action_config.get('class', '')
})
return actions
def _check_action_condition(self, item: Any, action_config: Dict[str, Any]) -> bool:
"""Prüft ob eine Aktion für ein Item verfügbar ist"""
condition = action_config.get('condition')
if not condition:
return True
try:
# Einfache Bedingungsprüfung
if isinstance(condition, dict):
for key, expected_value in condition.items():
actual_value = self._extract_value(item, key)
if actual_value != expected_value:
return False
return True
except Exception:
return False
def _build_action_url(self, item: Any, action_config: Dict[str, Any]) -> str:
"""Erstellt URL für eine Aktion"""
url_template = action_config.get('url', '')
# Ersetze Platzhalter in URL
try:
return url_template.format(id=getattr(item, 'id', ''))
except Exception:
return url_template
def parse_table_request(request_data: Dict[str, Any]) -> Tuple[List[SortConfig], List[FilterConfig], PaginationConfig, str]:
"""Parst Tabellen-Request-Parameter"""
# Sortierung parsen
sorts = []
sort_data = request_data.get('sort', [])
if isinstance(sort_data, dict):
sort_data = [sort_data]
for sort_item in sort_data:
if isinstance(sort_item, dict):
column = sort_item.get('column')
direction = SortDirection(sort_item.get('direction', 'asc'))
if column:
sorts.append(SortConfig(column=column, direction=direction))
# Filter parsen
filters = []
filter_data = request_data.get('filters', [])
if isinstance(filter_data, dict):
filter_data = [filter_data]
for filter_item in filter_data:
if isinstance(filter_item, dict):
column = filter_item.get('column')
operator = FilterOperator(filter_item.get('operator', 'eq'))
value = filter_item.get('value')
values = filter_item.get('values')
if column:
filters.append(FilterConfig(
column=column,
operator=operator,
value=value,
values=values
))
# Pagination parsen
page = int(request_data.get('page', 1))
page_size = min(int(request_data.get('page_size', 25)), 100)
pagination = PaginationConfig(page=page, page_size=page_size)
# Suche parsen
search = request_data.get('search', '')
return sorts, filters, pagination, search
def get_advanced_table_javascript() -> str:
"""JavaScript für erweiterte Tabellen"""
return """
class AdvancedTable {
constructor(tableId, config = {}) {
this.tableId = tableId;
this.config = {
apiUrl: '/api/table-data',
pageSize: 25,
searchDelay: 500,
sortable: true,
filterable: true,
searchable: true,
...config
};
this.currentSort = [];
this.currentFilters = [];
this.currentPage = 1;
this.currentSearch = '';
this.totalPages = 1;
this.totalItems = 0;
this.searchTimeout = null;
this.init();
}
init() {
this.setupTable();
this.setupEventListeners();
this.loadData();
}
setupTable() {
const table = document.getElementById(this.tableId);
if (!table) return;
table.classList.add('advanced-table');
// Add table wrapper
const wrapper = document.createElement('div');
wrapper.className = 'table-wrapper';
table.parentNode.insertBefore(wrapper, table);
wrapper.appendChild(table);
// Add controls
this.createControls(wrapper);
}
createControls(wrapper) {
const controls = document.createElement('div');
controls.className = 'table-controls';
controls.innerHTML = `
<div class="table-controls-left">
<div class="search-box">
<input type="text" id="${this.tableId}-search" placeholder="Suchen..." class="search-input">
<span class="search-icon">🔍</span>
</div>
<div class="page-size-selector">
<label>Einträge pro Seite:</label>
<select id="${this.tableId}-page-size">
<option value="10">10</option>
<option value="25" selected>25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
<div class="table-controls-right">
<button class="btn-filter" id="${this.tableId}-filter-btn">Filter</button>
<button class="btn-export" id="${this.tableId}-export-btn">Export</button>
<button class="btn-refresh" id="${this.tableId}-refresh-btn">↻</button>
</div>
`;
wrapper.insertBefore(controls, wrapper.firstChild);
// Add pagination
const pagination = document.createElement('div');
pagination.className = 'table-pagination';
pagination.id = `${this.tableId}-pagination`;
wrapper.appendChild(pagination);
}
setupEventListeners() {
// Search
const searchInput = document.getElementById(`${this.tableId}-search`);
searchInput?.addEventListener('input', (e) => {
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
this.currentSearch = e.target.value;
this.currentPage = 1;
this.loadData();
}, this.config.searchDelay);
});
// Page size
const pageSizeSelect = document.getElementById(`${this.tableId}-page-size`);
pageSizeSelect?.addEventListener('change', (e) => {
this.config.pageSize = parseInt(e.target.value);
this.currentPage = 1;
this.loadData();
});
// Refresh
const refreshBtn = document.getElementById(`${this.tableId}-refresh-btn`);
refreshBtn?.addEventListener('click', () => {
this.loadData();
});
// Export
const exportBtn = document.getElementById(`${this.tableId}-export-btn`);
exportBtn?.addEventListener('click', () => {
this.exportData();
});
// Table header clicks (sorting)
const table = document.getElementById(this.tableId);
table?.addEventListener('click', (e) => {
const th = e.target.closest('th[data-sortable="true"]');
if (th) {
const column = th.dataset.column;
this.toggleSort(column);
}
});
}
toggleSort(column) {
const existingSort = this.currentSort.find(s => s.column === column);
if (existingSort) {
if (existingSort.direction === 'asc') {
existingSort.direction = 'desc';
} else {
// Remove sort
this.currentSort = this.currentSort.filter(s => s.column !== column);
}
} else {
this.currentSort.push({ column, direction: 'asc' });
}
this.updateSortHeaders();
this.loadData();
}
updateSortHeaders() {
const table = document.getElementById(this.tableId);
const headers = table?.querySelectorAll('th[data-column]');
headers?.forEach(th => {
const column = th.dataset.column;
const sort = this.currentSort.find(s => s.column === column);
th.classList.remove('sort-asc', 'sort-desc');
if (sort) {
th.classList.add(`sort-${sort.direction}`);
}
});
}
async loadData() {
try {
const params = {
page: this.currentPage,
page_size: this.config.pageSize,
search: this.currentSearch,
sort: this.currentSort,
filters: this.currentFilters
};
const response = await fetch(this.config.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params)
});
const data = await response.json();
if (data.success) {
this.renderTable(data.data);
this.updatePagination(data.pagination);
} else {
console.error('Table data loading failed:', data.error);
}
} catch (error) {
console.error('Table data loading error:', error);
}
}
renderTable(data) {
const table = document.getElementById(this.tableId);
const tbody = table?.querySelector('tbody');
if (!tbody) return;
tbody.innerHTML = '';
data.forEach(row => {
const tr = document.createElement('tr');
tr.dataset.id = row._id;
// Render cells
Object.keys(row).forEach(key => {
if (key.startsWith('_')) return; // Skip metadata
const td = document.createElement('td');
const cellData = row[key];
if (typeof cellData === 'object' && cellData.formatted !== undefined) {
td.innerHTML = cellData.formatted;
td.dataset.raw = cellData.raw;
} else {
td.textContent = cellData;
}
tr.appendChild(td);
});
// Add actions column if exists
if (row._actions && row._actions.length > 0) {
const actionsTd = document.createElement('td');
actionsTd.className = 'actions-cell';
actionsTd.innerHTML = this.renderActions(row._actions);
tr.appendChild(actionsTd);
}
tbody.appendChild(tr);
});
}
renderActions(actions) {
return actions.map(action => {
const confirmAttr = action.confirm ? `onclick="return confirm('${action.confirm}')"` : '';
const icon = action.icon ? `<span class="action-icon">${action.icon}</span>` : '';
return `<a href="${action.url}" class="action-btn ${action.class}" ${confirmAttr}>
${icon}${action.label}
</a>`;
}).join(' ');
}
updatePagination(pagination) {
this.currentPage = pagination.page;
this.totalPages = pagination.total_pages;
this.totalItems = pagination.total_items;
const paginationEl = document.getElementById(`${this.tableId}-pagination`);
if (!paginationEl) return;
paginationEl.innerHTML = `
<div class="pagination-info">
Zeige ${pagination.start_item}-${pagination.end_item} von ${pagination.total_items} Einträgen
</div>
<div class="pagination-controls">
${this.renderPaginationButtons()}
</div>
`;
// Event listeners für Pagination
paginationEl.querySelectorAll('.page-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const page = parseInt(btn.dataset.page);
if (page !== this.currentPage) {
this.currentPage = page;
this.loadData();
}
});
});
}
renderPaginationButtons() {
const buttons = [];
const maxButtons = 7;
// Previous button
buttons.push(`
<button class="page-btn ${this.currentPage === 1 ? 'disabled' : ''}"
data-page="${this.currentPage - 1}" ${this.currentPage === 1 ? 'disabled' : ''}>
</button>
`);
// Page number buttons
let startPage = Math.max(1, this.currentPage - Math.floor(maxButtons / 2));
let endPage = Math.min(this.totalPages, startPage + maxButtons - 1);
if (endPage - startPage + 1 < maxButtons) {
startPage = Math.max(1, endPage - maxButtons + 1);
}
for (let i = startPage; i <= endPage; i++) {
buttons.push(`
<button class="page-btn ${i === this.currentPage ? 'active' : ''}"
data-page="${i}">
${i}
</button>
`);
}
// Next button
buttons.push(`
<button class="page-btn ${this.currentPage === this.totalPages ? 'disabled' : ''}"
data-page="${this.currentPage + 1}" ${this.currentPage === this.totalPages ? 'disabled' : ''}>
</button>
`);
return buttons.join('');
}
exportData() {
const params = new URLSearchParams({
search: this.currentSearch,
sort: JSON.stringify(this.currentSort),
filters: JSON.stringify(this.currentFilters),
format: 'csv'
});
window.open(`${this.config.apiUrl}/export?${params}`, '_blank');
}
}
// Auto-initialize tables with data-advanced-table attribute
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('[data-advanced-table]').forEach(table => {
const config = JSON.parse(table.dataset.advancedTable || '{}');
new AdvancedTable(table.id, config);
});
});
"""
def get_advanced_table_css() -> str:
"""CSS für erweiterte Tabellen"""
return """
.table-wrapper {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.table-controls {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.table-controls-left {
display: flex;
align-items: center;
gap: 1rem;
}
.search-box {
position: relative;
}
.search-input {
padding: 0.5rem 0.75rem;
padding-right: 2rem;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.875rem;
}
.search-icon {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
color: #6b7280;
}
.page-size-selector {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
.table-controls-right {
display: flex;
gap: 0.5rem;
}
.advanced-table {
width: 100%;
border-collapse: collapse;
}
.advanced-table th {
background: #f8f9fa;
padding: 0.75rem;
text-align: left;
font-weight: 600;
border-bottom: 2px solid #e9ecef;
position: relative;
}
.advanced-table th[data-sortable="true"] {
cursor: pointer;
user-select: none;
}
.advanced-table th[data-sortable="true"]:hover {
background: #e9ecef;
}
.advanced-table th.sort-asc::after {
content: "";
color: #3b82f6;
}
.advanced-table th.sort-desc::after {
content: "";
color: #3b82f6;
}
.advanced-table td {
padding: 0.75rem;
border-bottom: 1px solid #e9ecef;
}
.advanced-table tbody tr:hover {
background: #f8f9fa;
}
.actions-cell {
white-space: nowrap;
}
.action-btn {
display: inline-block;
padding: 0.25rem 0.5rem;
margin: 0 0.125rem;
font-size: 0.75rem;
text-decoration: none;
border-radius: 4px;
background: #e5e7eb;
color: #374151;
}
.action-btn:hover {
background: #d1d5db;
}
.action-btn.btn-primary {
background: #3b82f6;
color: white;
}
.action-btn.btn-danger {
background: #ef4444;
color: white;
}
.table-pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
}
.pagination-controls {
display: flex;
gap: 0.25rem;
}
.page-btn {
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
background: white;
cursor: pointer;
border-radius: 4px;
}
.page-btn:hover:not(.disabled) {
background: #f3f4f6;
}
.page-btn.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.page-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media (max-width: 768px) {
.table-controls {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.table-controls-left,
.table-controls-right {
justify-content: center;
}
.advanced-table {
font-size: 0.875rem;
}
.advanced-table th,
.advanced-table td {
padding: 0.5rem;
}
.table-pagination {
flex-direction: column;
gap: 1rem;
}
}
"""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,909 @@
"""
Multi-Format-Report-Generator für das MYP-System
===============================================
Dieses Modul stellt umfassende Report-Generierung in verschiedenen Formaten bereit:
- PDF-Reports mit professionellem Layout
- Excel-Reports mit Diagrammen und Formatierungen
- CSV-Export für Datenanalyse
- JSON-Export für API-Integration
"""
import os
import io
import json
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Union, BinaryIO
from dataclasses import dataclass, asdict
from abc import ABC, abstractmethod
# PDF-Generation
try:
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4, letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch, cm
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image, PageBreak
from reportlab.graphics.shapes import Drawing
from reportlab.graphics.charts.lineplots import LinePlot
from reportlab.graphics.charts.barcharts import VerticalBarChart
from reportlab.graphics.charts.piecharts import Pie
from reportlab.lib.validators import Auto
PDF_AVAILABLE = True
except ImportError:
PDF_AVAILABLE = False
# Excel-Generation
try:
import xlsxwriter
from xlsxwriter.workbook import Workbook
from xlsxwriter.worksheet import Worksheet
EXCEL_AVAILABLE = True
except ImportError:
EXCEL_AVAILABLE = False
import csv
from flask import make_response, jsonify
from utils.logging_config import get_logger
from models import Job, User, Printer, Stats, GuestRequest, get_db_session
logger = get_logger("reports")
@dataclass
class ReportConfig:
"""Konfiguration für Report-Generierung"""
title: str
subtitle: str = ""
author: str = "MYP System"
date_range: tuple = None
include_charts: bool = True
include_summary: bool = True
template: str = "standard"
logo_path: str = None
footer_text: str = "Generiert vom MYP-System"
@dataclass
class ChartData:
"""Daten für Diagramme"""
chart_type: str # 'line', 'bar', 'pie'
title: str
data: List[Dict[str, Any]]
labels: List[str] = None
colors: List[str] = None
class BaseReportGenerator(ABC):
"""Abstrakte Basis-Klasse für Report-Generatoren"""
def __init__(self, config: ReportConfig):
self.config = config
self.data = {}
self.charts = []
@abstractmethod
def generate(self, output_stream: BinaryIO) -> bool:
"""Generiert den Report in den angegebenen Stream"""
pass
def add_data_section(self, name: str, data: List[Dict[str, Any]], headers: List[str] = None):
"""Fügt eine Datensektion hinzu"""
self.data[name] = {
'data': data,
'headers': headers or (list(data[0].keys()) if data else [])
}
def add_chart(self, chart: ChartData):
"""Fügt ein Diagramm hinzu"""
self.charts.append(chart)
class PDFReportGenerator(BaseReportGenerator):
"""PDF-Report-Generator mit professionellem Layout"""
def __init__(self, config: ReportConfig):
super().__init__(config)
if not PDF_AVAILABLE:
raise ImportError("ReportLab ist nicht installiert. Verwenden Sie: pip install reportlab")
self.doc = None
self.story = []
self.styles = getSampleStyleSheet()
self._setup_custom_styles()
def _setup_custom_styles(self):
"""Richtet benutzerdefinierte Styles ein"""
# Titel-Style
self.styles.add(ParagraphStyle(
name='CustomTitle',
parent=self.styles['Heading1'],
fontSize=24,
spaceAfter=30,
alignment=1, # Zentriert
textColor=colors.HexColor('#1f2937')
))
# Untertitel-Style
self.styles.add(ParagraphStyle(
name='CustomSubtitle',
parent=self.styles['Heading2'],
fontSize=16,
spaceAfter=20,
alignment=1,
textColor=colors.HexColor('#6b7280')
))
# Sektions-Header
self.styles.add(ParagraphStyle(
name='SectionHeader',
parent=self.styles['Heading2'],
fontSize=14,
spaceBefore=20,
spaceAfter=10,
textColor=colors.HexColor('#374151'),
borderWidth=1,
borderColor=colors.HexColor('#d1d5db'),
borderPadding=5
))
def generate(self, output_stream: BinaryIO) -> bool:
"""Generiert PDF-Report"""
try:
self.doc = SimpleDocTemplate(
output_stream,
pagesize=A4,
rightMargin=2*cm,
leftMargin=2*cm,
topMargin=2*cm,
bottomMargin=2*cm
)
self._build_header()
self._build_summary()
self._build_data_sections()
self._build_charts()
self._build_footer()
self.doc.build(self.story)
return True
except Exception as e:
logger.error(f"Fehler bei PDF-Generierung: {str(e)}")
return False
def _build_header(self):
"""Erstellt den Report-Header"""
# Logo (falls vorhanden)
if self.config.logo_path and os.path.exists(self.config.logo_path):
try:
logo = Image(self.config.logo_path, width=2*inch, height=1*inch)
self.story.append(logo)
self.story.append(Spacer(1, 0.2*inch))
except Exception as e:
logger.warning(f"Logo konnte nicht geladen werden: {str(e)}")
# Titel
title = Paragraph(self.config.title, self.styles['CustomTitle'])
self.story.append(title)
# Untertitel
if self.config.subtitle:
subtitle = Paragraph(self.config.subtitle, self.styles['CustomSubtitle'])
self.story.append(subtitle)
# Generierungsdatum
date_text = f"Generiert am: {datetime.now().strftime('%d.%m.%Y %H:%M')}"
date_para = Paragraph(date_text, self.styles['Normal'])
self.story.append(date_para)
# Autor
author_text = f"Erstellt von: {self.config.author}"
author_para = Paragraph(author_text, self.styles['Normal'])
self.story.append(author_para)
self.story.append(Spacer(1, 0.3*inch))
def _build_summary(self):
"""Erstellt die Zusammenfassung"""
if not self.config.include_summary:
return
header = Paragraph("Zusammenfassung", self.styles['SectionHeader'])
self.story.append(header)
# Sammle Statistiken aus den Daten
total_records = sum(len(section['data']) for section in self.data.values())
summary_data = [
['Gesamtanzahl Datensätze', str(total_records)],
['Berichtszeitraum', self._format_date_range()],
['Anzahl Sektionen', str(len(self.data))],
['Anzahl Diagramme', str(len(self.charts))]
]
summary_table = Table(summary_data, colWidths=[4*inch, 2*inch])
summary_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f3f4f6')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 12),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.white),
('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#d1d5db'))
]))
self.story.append(summary_table)
self.story.append(Spacer(1, 0.2*inch))
def _build_data_sections(self):
"""Erstellt die Datensektionen"""
for section_name, section_data in self.data.items():
# Sektions-Header
header = Paragraph(section_name, self.styles['SectionHeader'])
self.story.append(header)
# Daten-Tabelle
table_data = [section_data['headers']]
table_data.extend([
[str(row.get(header, '')) for header in section_data['headers']]
for row in section_data['data']
])
# Spaltenbreiten berechnen
col_count = len(section_data['headers'])
col_width = (self.doc.width - 2*inch) / col_count
col_widths = [col_width] * col_count
table = Table(table_data, colWidths=col_widths, repeatRows=1)
table.setStyle(TableStyle([
# Header-Styling
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#3b82f6')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 10),
# Daten-Styling
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 1), (-1, -1), 9),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f9fafb')]),
# Rahmen
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#d1d5db')),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('LEFTPADDING', (0, 0), (-1, -1), 6),
('RIGHTPADDING', (0, 0), (-1, -1), 6),
('TOPPADDING', (0, 0), (-1, -1), 8),
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
]))
self.story.append(table)
self.story.append(Spacer(1, 0.2*inch))
# Seitenumbruch bei vielen Daten
if len(section_data['data']) > 20:
self.story.append(PageBreak())
def _build_charts(self):
"""Erstellt die Diagramme"""
if not self.config.include_charts or not self.charts:
return
header = Paragraph("Diagramme", self.styles['SectionHeader'])
self.story.append(header)
for chart in self.charts:
chart_title = Paragraph(chart.title, self.styles['Heading3'])
self.story.append(chart_title)
# Diagramm basierend auf Typ erstellen
drawing = self._create_chart_drawing(chart)
if drawing:
self.story.append(drawing)
self.story.append(Spacer(1, 0.2*inch))
def _create_chart_drawing(self, chart: ChartData) -> Optional[Drawing]:
"""Erstellt ein Diagramm-Drawing"""
try:
drawing = Drawing(400, 300)
if chart.chart_type == 'bar':
bar_chart = VerticalBarChart()
bar_chart.x = 50
bar_chart.y = 50
bar_chart.height = 200
bar_chart.width = 300
# Daten vorbereiten
values = [[item.get('value', 0) for item in chart.data]]
categories = [item.get('label', f'Item {i}') for i, item in enumerate(chart.data)]
bar_chart.data = values
bar_chart.categoryAxis.categoryNames = categories
bar_chart.valueAxis.valueMin = 0
# Farben setzen
if chart.colors:
bar_chart.bars[0].fillColor = colors.HexColor(chart.colors[0] if chart.colors else '#3b82f6')
drawing.add(bar_chart)
elif chart.chart_type == 'pie':
pie_chart = Pie()
pie_chart.x = 150
pie_chart.y = 100
pie_chart.width = 100
pie_chart.height = 100
# Daten vorbereiten
pie_chart.data = [item.get('value', 0) for item in chart.data]
pie_chart.labels = [item.get('label', f'Item {i}') for i, item in enumerate(chart.data)]
# Farben setzen
if chart.colors:
pie_chart.slices.fillColor = colors.HexColor(chart.colors[0] if chart.colors else '#3b82f6')
drawing.add(pie_chart)
return drawing
except Exception as e:
logger.error(f"Fehler bei Diagramm-Erstellung: {str(e)}")
return None
def _build_footer(self):
"""Erstellt den Report-Footer"""
footer_text = self.config.footer_text
footer = Paragraph(footer_text, self.styles['Normal'])
self.story.append(Spacer(1, 0.3*inch))
self.story.append(footer)
def _format_date_range(self) -> str:
"""Formatiert den Datumsbereich"""
if not self.config.date_range:
return "Alle verfügbaren Daten"
start_date, end_date = self.config.date_range
return f"{start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}"
class ExcelReportGenerator(BaseReportGenerator):
"""Excel-Report-Generator mit Diagrammen und Formatierungen"""
def __init__(self, config: ReportConfig):
super().__init__(config)
if not EXCEL_AVAILABLE:
raise ImportError("XlsxWriter ist nicht installiert. Verwenden Sie: pip install xlsxwriter")
self.workbook = None
self.formats = {}
def generate(self, output_stream: BinaryIO) -> bool:
"""Generiert Excel-Report"""
try:
self.workbook = xlsxwriter.Workbook(output_stream, {'in_memory': True})
self._setup_formats()
# Zusammenfassungs-Arbeitsblatt
if self.config.include_summary:
self._create_summary_worksheet()
# Daten-Arbeitsblätter
for section_name, section_data in self.data.items():
self._create_data_worksheet(section_name, section_data)
# Diagramm-Arbeitsblätter
if self.config.include_charts and self.charts:
self._create_charts_worksheet()
self.workbook.close()
return True
except Exception as e:
logger.error(f"Fehler bei Excel-Generierung: {str(e)}")
return False
def _setup_formats(self):
"""Richtet Excel-Formate ein"""
self.formats = {
'title': self.workbook.add_format({
'font_size': 18,
'bold': True,
'align': 'center',
'bg_color': '#1f2937',
'font_color': 'white',
'border': 1
}),
'header': self.workbook.add_format({
'font_size': 12,
'bold': True,
'bg_color': '#3b82f6',
'font_color': 'white',
'align': 'center',
'border': 1
}),
'data': self.workbook.add_format({
'align': 'center',
'border': 1
}),
'data_alt': self.workbook.add_format({
'align': 'center',
'bg_color': '#f9fafb',
'border': 1
}),
'number': self.workbook.add_format({
'num_format': '#,##0',
'align': 'right',
'border': 1
}),
'currency': self.workbook.add_format({
'num_format': '#,##0.00 €',
'align': 'right',
'border': 1
}),
'percentage': self.workbook.add_format({
'num_format': '0.00%',
'align': 'right',
'border': 1
}),
'date': self.workbook.add_format({
'num_format': 'dd.mm.yyyy',
'align': 'center',
'border': 1
})
}
def _create_summary_worksheet(self):
"""Erstellt das Zusammenfassungs-Arbeitsblatt"""
worksheet = self.workbook.add_worksheet('Zusammenfassung')
# Titel
worksheet.merge_range('A1:E1', self.config.title, self.formats['title'])
# Untertitel
if self.config.subtitle:
worksheet.merge_range('A2:E2', self.config.subtitle, self.formats['header'])
# Metadaten
row = 4
metadata = [
['Generiert am:', datetime.now().strftime('%d.%m.%Y %H:%M')],
['Erstellt von:', self.config.author],
['Berichtszeitraum:', self._format_date_range()],
['Anzahl Sektionen:', str(len(self.data))],
['Anzahl Diagramme:', str(len(self.charts))]
]
for label, value in metadata:
worksheet.write(row, 0, label, self.formats['header'])
worksheet.write(row, 1, value, self.formats['data'])
row += 1
# Statistiken pro Sektion
row += 2
worksheet.write(row, 0, 'Sektions-Übersicht:', self.formats['header'])
row += 1
for section_name, section_data in self.data.items():
worksheet.write(row, 0, section_name, self.formats['data'])
worksheet.write(row, 1, len(section_data['data']), self.formats['number'])
row += 1
# Spaltenbreiten anpassen
worksheet.set_column('A:A', 25)
worksheet.set_column('B:B', 20)
def _create_data_worksheet(self, section_name: str, section_data: Dict[str, Any]):
"""Erstellt ein Daten-Arbeitsblatt"""
# Ungültige Zeichen für Arbeitsblatt-Namen ersetzen
safe_name = ''.join(c for c in section_name if c.isalnum() or c in ' -_')[:31]
worksheet = self.workbook.add_worksheet(safe_name)
# Header schreiben
headers = section_data['headers']
for col, header in enumerate(headers):
worksheet.write(0, col, header, self.formats['header'])
# Daten schreiben
for row_idx, row_data in enumerate(section_data['data'], start=1):
for col_idx, header in enumerate(headers):
value = row_data.get(header, '')
# Format basierend auf Datentyp wählen
cell_format = self._get_cell_format(value, row_idx)
worksheet.write(row_idx, col_idx, value, cell_format)
# Autofilter hinzufügen
if section_data['data']:
worksheet.autofilter(0, 0, len(section_data['data']), len(headers) - 1)
# Spaltenbreiten anpassen
for col_idx, header in enumerate(headers):
max_length = max(
len(str(header)),
max(len(str(row.get(header, ''))) for row in section_data['data']) if section_data['data'] else 0
)
worksheet.set_column(col_idx, col_idx, min(max_length + 2, 50))
def _create_charts_worksheet(self):
"""Erstellt das Diagramm-Arbeitsblatt"""
worksheet = self.workbook.add_worksheet('Diagramme')
row = 0
for chart_idx, chart_data in enumerate(self.charts):
# Diagramm-Titel
worksheet.write(row, 0, chart_data.title, self.formats['header'])
row += 2
# Daten für Diagramm vorbereiten
data_worksheet_name = f'Chart_Data_{chart_idx}'
data_worksheet = self.workbook.add_worksheet(data_worksheet_name)
# Daten ins Data-Arbeitsblatt schreiben
labels = [item.get('label', f'Item {i}') for i, item in enumerate(chart_data.data)]
values = [item.get('value', 0) for item in chart_data.data]
data_worksheet.write_column('A1', ['Label'] + labels)
data_worksheet.write_column('B1', ['Value'] + values)
# Excel-Diagramm erstellen
if chart_data.chart_type == 'bar':
chart = self.workbook.add_chart({'type': 'column'})
elif chart_data.chart_type == 'line':
chart = self.workbook.add_chart({'type': 'line'})
elif chart_data.chart_type == 'pie':
chart = self.workbook.add_chart({'type': 'pie'})
else:
chart = self.workbook.add_chart({'type': 'column'})
# Datenreihe hinzufügen
chart.add_series({
'name': chart_data.title,
'categories': [data_worksheet_name, 1, 0, len(labels), 0],
'values': [data_worksheet_name, 1, 1, len(values), 1],
})
chart.set_title({'name': chart_data.title})
chart.set_x_axis({'name': 'Kategorien'})
chart.set_y_axis({'name': 'Werte'})
# Diagramm ins Arbeitsblatt einfügen
worksheet.insert_chart(row, 0, chart)
row += 15 # Platz für nächstes Diagramm
def _get_cell_format(self, value: Any, row_idx: int):
"""Bestimmt das Zellformat basierend auf dem Wert"""
# Alternierende Zeilenfarben
base_format = self.formats['data'] if row_idx % 2 == 1 else self.formats['data_alt']
# Spezielle Formate für Zahlen, Daten, etc.
if isinstance(value, (int, float)):
return self.formats['number']
elif isinstance(value, datetime):
return self.formats['date']
elif isinstance(value, str) and value.endswith('%'):
return self.formats['percentage']
elif isinstance(value, str) and '' in value:
return self.formats['currency']
return base_format
def _format_date_range(self) -> str:
"""Formatiert den Datumsbereich"""
if not self.config.date_range:
return "Alle verfügbaren Daten"
start_date, end_date = self.config.date_range
return f"{start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}"
class CSVReportGenerator(BaseReportGenerator):
"""CSV-Report-Generator für Datenanalyse"""
def generate(self, output_stream: BinaryIO) -> bool:
"""Generiert CSV-Report"""
try:
# Text-Stream für CSV-Writer
text_stream = io.TextIOWrapper(output_stream, encoding='utf-8-sig', newline='')
writer = csv.writer(text_stream, delimiter=';', quoting=csv.QUOTE_MINIMAL)
# Header mit Metadaten
writer.writerow([f'# {self.config.title}'])
writer.writerow([f'# Generiert am: {datetime.now().strftime("%d.%m.%Y %H:%M")}'])
writer.writerow([f'# Erstellt von: {self.config.author}'])
writer.writerow(['']) # Leerzeile
# Daten-Sektionen
for section_name, section_data in self.data.items():
writer.writerow([f'# Sektion: {section_name}'])
# Headers
writer.writerow(section_data['headers'])
# Daten
for row in section_data['data']:
csv_row = [str(row.get(header, '')) for header in section_data['headers']]
writer.writerow(csv_row)
writer.writerow(['']) # Leerzeile zwischen Sektionen
text_stream.flush()
return True
except Exception as e:
logger.error(f"Fehler bei CSV-Generierung: {str(e)}")
return False
class JSONReportGenerator(BaseReportGenerator):
"""JSON-Report-Generator für API-Integration"""
def generate(self, output_stream: BinaryIO) -> bool:
"""Generiert JSON-Report"""
try:
report_data = {
'metadata': {
'title': self.config.title,
'subtitle': self.config.subtitle,
'author': self.config.author,
'generated_at': datetime.now().isoformat(),
'date_range': {
'start': self.config.date_range[0].isoformat() if self.config.date_range else None,
'end': self.config.date_range[1].isoformat() if self.config.date_range else None
} if self.config.date_range else None
},
'data': self.data,
'charts': [asdict(chart) for chart in self.charts] if self.charts else []
}
json_str = json.dumps(report_data, ensure_ascii=False, indent=2, default=str)
output_stream.write(json_str.encode('utf-8'))
return True
except Exception as e:
logger.error(f"Fehler bei JSON-Generierung: {str(e)}")
return False
class ReportFactory:
"""Factory für Report-Generatoren"""
GENERATORS = {
'pdf': PDFReportGenerator,
'excel': ExcelReportGenerator,
'xlsx': ExcelReportGenerator,
'csv': CSVReportGenerator,
'json': JSONReportGenerator
}
@classmethod
def create_generator(cls, format_type: str, config: ReportConfig) -> BaseReportGenerator:
"""Erstellt einen Report-Generator für das angegebene Format"""
format_type = format_type.lower()
if format_type not in cls.GENERATORS:
raise ValueError(f"Unbekanntes Report-Format: {format_type}")
generator_class = cls.GENERATORS[format_type]
return generator_class(config)
@classmethod
def get_available_formats(cls) -> List[str]:
"""Gibt verfügbare Report-Formate zurück"""
available = []
for format_type, generator_class in cls.GENERATORS.items():
try:
# Test ob Generator funktioniert
if format_type in ['pdf'] and not PDF_AVAILABLE:
continue
elif format_type in ['excel', 'xlsx'] and not EXCEL_AVAILABLE:
continue
available.append(format_type)
except ImportError:
continue
return available
# Vordefinierte Report-Templates
class JobReportBuilder:
"""Builder für Job-Reports"""
@staticmethod
def build_jobs_report(
start_date: datetime = None,
end_date: datetime = None,
user_id: int = None,
printer_id: int = None,
include_completed: bool = True,
include_cancelled: bool = False
) -> Dict[str, Any]:
"""Erstellt Job-Report-Daten"""
with get_db_session() as db_session:
query = db_session.query(Job)
# Filter anwenden
if start_date:
query = query.filter(Job.created_at >= start_date)
if end_date:
query = query.filter(Job.created_at <= end_date)
if user_id:
query = query.filter(Job.user_id == user_id)
if printer_id:
query = query.filter(Job.printer_id == printer_id)
status_filters = []
if include_completed:
status_filters.append('finished')
if include_cancelled:
status_filters.append('cancelled')
if not include_cancelled and not include_completed:
status_filters = ['scheduled', 'running', 'paused']
if status_filters:
query = query.filter(Job.status.in_(status_filters))
jobs = query.all()
# Daten vorbereiten
job_data = []
for job in jobs:
job_data.append({
'ID': job.id,
'Name': job.name,
'Benutzer': job.user.name if job.user else 'Unbekannt',
'Drucker': job.printer.name if job.printer else 'Unbekannt',
'Status': job.status,
'Erstellt': job.created_at.strftime('%d.%m.%Y %H:%M') if job.created_at else '',
'Gestartet': job.start_at.strftime('%d.%m.%Y %H:%M') if job.start_at else '',
'Beendet': job.end_at.strftime('%d.%m.%Y %H:%M') if job.end_at else '',
'Dauer (Min)': job.duration_minutes or 0,
'Material (g)': job.material_used or 0,
'Beschreibung': job.description or ''
})
return {
'data': job_data,
'headers': ['ID', 'Name', 'Benutzer', 'Drucker', 'Status', 'Erstellt', 'Gestartet', 'Beendet', 'Dauer (Min)', 'Material (g)', 'Beschreibung']
}
class UserReportBuilder:
"""Builder für Benutzer-Reports"""
@staticmethod
def build_users_report(include_inactive: bool = False) -> Dict[str, Any]:
"""Erstellt Benutzer-Report-Daten"""
with get_db_session() as db_session:
query = db_session.query(User)
if not include_inactive:
query = query.filter(User.active == True)
users = query.all()
# Daten vorbereiten
user_data = []
for user in users:
user_data.append({
'ID': user.id,
'Name': user.name,
'E-Mail': user.email,
'Benutzername': user.username,
'Rolle': user.role,
'Aktiv': 'Ja' if user.active else 'Nein',
'Abteilung': user.department or '',
'Position': user.position or '',
'Erstellt': user.created_at.strftime('%d.%m.%Y') if user.created_at else '',
'Letzter Login': user.last_login.strftime('%d.%m.%Y %H:%M') if user.last_login else 'Nie'
})
return {
'data': user_data,
'headers': ['ID', 'Name', 'E-Mail', 'Benutzername', 'Rolle', 'Aktiv', 'Abteilung', 'Position', 'Erstellt', 'Letzter Login']
}
class PrinterReportBuilder:
"""Builder für Drucker-Reports"""
@staticmethod
def build_printers_report(include_inactive: bool = False) -> Dict[str, Any]:
"""Erstellt Drucker-Report-Daten"""
with get_db_session() as db_session:
query = db_session.query(Printer)
if not include_inactive:
query = query.filter(Printer.active == True)
printers = query.all()
# Daten vorbereiten
printer_data = []
for printer in printers:
printer_data.append({
'ID': printer.id,
'Name': printer.name,
'Modell': printer.model or '',
'Standort': printer.location or '',
'IP-Adresse': printer.ip_address or '',
'MAC-Adresse': printer.mac_address,
'Plug-IP': printer.plug_ip,
'Status': printer.status,
'Aktiv': 'Ja' if printer.active else 'Nein',
'Erstellt': printer.created_at.strftime('%d.%m.%Y') if printer.created_at else '',
'Letzte Prüfung': printer.last_checked.strftime('%d.%m.%Y %H:%M') if printer.last_checked else 'Nie'
})
return {
'data': printer_data,
'headers': ['ID', 'Name', 'Modell', 'Standort', 'IP-Adresse', 'MAC-Adresse', 'Plug-IP', 'Status', 'Aktiv', 'Erstellt', 'Letzte Prüfung']
}
def generate_comprehensive_report(
format_type: str,
start_date: datetime = None,
end_date: datetime = None,
include_jobs: bool = True,
include_users: bool = True,
include_printers: bool = True,
user_id: int = None
) -> bytes:
"""Generiert einen umfassenden System-Report"""
# Konfiguration
config = ReportConfig(
title="MYP System Report",
subtitle="Umfassende Systemübersicht",
author="MYP System",
date_range=(start_date, end_date) if start_date and end_date else None,
include_charts=True,
include_summary=True
)
# Generator erstellen
generator = ReportFactory.create_generator(format_type, config)
# Daten hinzufügen
if include_jobs:
job_data = JobReportBuilder.build_jobs_report(
start_date=start_date,
end_date=end_date,
user_id=user_id
)
generator.add_data_section("Jobs", job_data['data'], job_data['headers'])
# Job-Status-Diagramm
status_counts = {}
for job in job_data['data']:
status = job['Status']
status_counts[status] = status_counts.get(status, 0) + 1
chart_data = ChartData(
chart_type='pie',
title='Job-Status-Verteilung',
data=[{'label': status, 'value': count} for status, count in status_counts.items()]
)
generator.add_chart(chart_data)
if include_users:
user_data = UserReportBuilder.build_users_report()
generator.add_data_section("Benutzer", user_data['data'], user_data['headers'])
if include_printers:
printer_data = PrinterReportBuilder.build_printers_report()
generator.add_data_section("Drucker", printer_data['data'], printer_data['headers'])
# Report generieren
output = io.BytesIO()
success = generator.generate(output)
if success:
output.seek(0)
return output.getvalue()
else:
raise Exception("Report-Generierung fehlgeschlagen")
# Zusätzliche Abhängigkeiten zu requirements.txt hinzufügen
ADDITIONAL_REQUIREMENTS = [
"reportlab>=4.0.0",
"xlsxwriter>=3.0.0"
]