manage-your-printer/utils/template_helpers.py
2025-06-04 10:03:22 +02:00

507 lines
18 KiB
Python

"""
Template Helpers für MYP Platform
Jinja2 Helper-Funktionen für UI-Komponenten
"""
from flask import current_app, url_for, request
from markupsafe import Markup
import json
from datetime import datetime
from typing import Dict, Any, Optional, List
import calendar
import random
class UIHelpers:
"""UI-Helper-Klasse für Template-Funktionen"""
@staticmethod
def component_button(text: str, type: str = "primary", size: str = "md",
classes: str = "", icon: str = "", onclick: str = "",
disabled: bool = False, **attrs) -> Markup:
"""
Erstellt einen Button mit Tailwind-Klassen
Args:
text: Button-Text
type: Button-Typ (primary, secondary, danger, success)
size: Button-Größe (sm, md, lg)
classes: Zusätzliche CSS-Klassen
icon: SVG-Icon-Code
onclick: JavaScript-Code für onclick
disabled: Button deaktiviert
**attrs: Zusätzliche HTML-Attribute
"""
base_classes = ["btn"]
# Typ-spezifische Klassen
type_classes = {
"primary": "btn-primary",
"secondary": "btn-secondary",
"danger": "btn-danger",
"success": "btn-success"
}
base_classes.append(type_classes.get(type, "btn-primary"))
# Größen-spezifische Klassen
size_classes = {
"sm": "btn-sm",
"md": "",
"lg": "btn-lg"
}
if size_classes.get(size):
base_classes.append(size_classes[size])
if disabled:
base_classes.append("opacity-50 cursor-not-allowed")
# Zusätzliche Klassen hinzufügen
if classes:
base_classes.append(classes)
# HTML-Attribute aufbauen
attrs_str = ""
for key, value in attrs.items():
attrs_str += f' {key.replace("_", "-")}="{value}"'
if onclick:
attrs_str += f' onclick="{onclick}"'
if disabled:
attrs_str += ' disabled'
# Icon und Text kombinieren
content = ""
if icon:
content += f'<span class="inline-block mr-2">{icon}</span>'
content += text
html = f'''<button class="{" ".join(base_classes)}"{attrs_str}>
{content}
</button>'''
return Markup(html)
@staticmethod
def component_badge(text: str, type: str = "blue", classes: str = "") -> Markup:
"""
Erstellt ein Badge/Tag-Element
Args:
text: Badge-Text
type: Badge-Typ (blue, green, red, yellow, purple)
classes: Zusätzliche CSS-Klassen
"""
base_classes = ["badge", f"badge-{type}"]
if classes:
base_classes.append(classes)
html = f'<span class="{" ".join(base_classes)}">{text}</span>'
return Markup(html)
@staticmethod
def component_status_badge(status: str, type: str = "job") -> Markup:
"""
Erstellt ein Status-Badge für Jobs oder Drucker
Args:
status: Status-Wert
type: Typ (job, printer)
"""
if type == "job":
class_name = f"job-status job-{status}"
else:
class_name = f"printer-status printer-{status}"
# Status-Text übersetzen
translations = {
"job": {
"queued": "In Warteschlange",
"printing": "Wird gedruckt",
"completed": "Abgeschlossen",
"failed": "Fehlgeschlagen",
"cancelled": "Abgebrochen",
"paused": "Pausiert"
},
"printer": {
"ready": "Bereit",
"busy": "Beschäftigt",
"error": "Fehler",
"offline": "Offline",
"maintenance": "Wartung"
}
}
display_text = translations.get(type, {}).get(status, status)
html = f'<span class="{class_name}">{display_text}</span>'
return Markup(html)
@staticmethod
def component_card(title: str = "", content: str = "", footer: str = "",
classes: str = "", hover: bool = False) -> Markup:
"""
Erstellt eine Karte
Args:
title: Karten-Titel
content: Karten-Inhalt
footer: Karten-Footer
classes: Zusätzliche CSS-Klassen
hover: Hover-Effekt aktivieren
"""
base_classes = ["card"]
if hover:
base_classes.append("card-hover")
if classes:
base_classes.append(classes)
html_parts = [f'<div class="{" ".join(base_classes)}">']
if title:
html_parts.append(f'<h3 class="text-lg font-semibold mb-4 text-slate-900 dark:text-white">{title}</h3>')
if content:
html_parts.append(f'<div class="text-slate-600 dark:text-slate-300">{content}</div>')
if footer:
html_parts.append(f'<div class="mt-4 pt-4 border-t border-light-border dark:border-dark-border">{footer}</div>')
html_parts.append('</div>')
return Markup("".join(html_parts))
@staticmethod
def component_alert(message: str, type: str = "info", dismissible: bool = False) -> Markup:
"""
Erstellt eine Alert-Benachrichtigung
Args:
message: Alert-Nachricht
type: Alert-Typ (info, success, warning, error)
dismissible: Schließbar machen
"""
base_classes = ["alert", f"alert-{type}"]
html_parts = [f'<div class="{" ".join(base_classes)}">']
if dismissible:
html_parts.append('''
<div class="flex justify-between">
<div>
''')
html_parts.append(f'<p>{message}</p>')
if dismissible:
html_parts.append('''
</div>
<button onclick="this.parentElement.parentElement.remove()"
class="text-current opacity-70 hover:opacity-100">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
</div>
''')
html_parts.append('</div>')
return Markup("".join(html_parts))
@staticmethod
def component_modal(modal_id: str, title: str, content: str,
footer: str = "", size: str = "md") -> Markup:
"""
Erstellt ein Modal-Dialog
Args:
modal_id: Eindeutige Modal-ID
title: Modal-Titel
content: Modal-Inhalt
footer: Modal-Footer
size: Modal-Größe (sm, md, lg, xl)
"""
size_classes = {
"sm": "max-w-md",
"md": "max-w-lg",
"lg": "max-w-2xl",
"xl": "max-w-4xl"
}
max_width = size_classes.get(size, "max-w-lg")
html = f'''
<div id="{modal_id}" class="fixed inset-0 z-50 hidden">
<div class="flex items-center justify-center min-h-screen px-4">
<div class="modal-content bg-white dark:bg-slate-800 rounded-lg shadow-xl transform scale-95 opacity-0 transition-all duration-150 w-full {max_width}">
<div class="px-6 py-4 border-b border-slate-200 dark:border-slate-700">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">{title}</h3>
<button data-modal-close="{modal_id}" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<div class="px-6 py-4">
{content}
</div>
{f'<div class="px-6 py-4 border-t border-slate-200 dark:border-slate-700">{footer}</div>' if footer else ''}
</div>
</div>
</div>
'''
return Markup(html)
@staticmethod
def component_table(headers: List[str], rows: List[List[str]],
classes: str = "", striped: bool = True) -> Markup:
"""
Erstellt eine styled Tabelle
Args:
headers: Tabellen-Kopfzeilen
rows: Tabellen-Zeilen
classes: Zusätzliche CSS-Klassen
striped: Zebra-Streifen aktivieren
"""
html_parts = ['<div class="table-container">']
table_classes = ["table-styled"]
if classes:
table_classes.append(classes)
html_parts.append(f'<table class="{" ".join(table_classes)}">')
# Kopfzeilen
html_parts.append('<thead><tr>')
for header in headers:
html_parts.append(f'<th>{header}</th>')
html_parts.append('</tr></thead>')
# Zeilen
html_parts.append('<tbody>')
for i, row in enumerate(rows):
row_classes = ""
if striped and i % 2 == 1:
row_classes = 'class="bg-slate-50 dark:bg-slate-800/50"'
html_parts.append(f'<tr {row_classes}>')
for cell in row:
html_parts.append(f'<td>{cell}</td>')
html_parts.append('</tr>')
html_parts.append('</tbody>')
html_parts.append('</table></div>')
return Markup("".join(html_parts))
@staticmethod
def format_datetime_german(dt: datetime, format_str: str = "%d.%m.%Y %H:%M") -> str:
"""
Formatiert Datetime für deutsche Anzeige
Args:
dt: Datetime-Objekt
format_str: Format-String
"""
if not dt:
return ""
return dt.strftime(format_str)
@staticmethod
def format_duration(minutes: int) -> str:
"""
Formatiert Dauer in Minuten zu lesbarem Format
Args:
minutes: Dauer in Minuten
"""
if not minutes:
return "0 Min"
if minutes < 60:
return f"{minutes} Min"
hours = minutes // 60
remaining_minutes = minutes % 60
if remaining_minutes == 0:
return f"{hours} Std"
return f"{hours} Std {remaining_minutes} Min"
@staticmethod
def json_encode(data: Any) -> str:
"""
Enkodiert Python-Daten als JSON für JavaScript
Args:
data: Zu enkodierendes Objekt
"""
return json.dumps(data, default=str, ensure_ascii=False)
def register_template_helpers(app):
"""
Registriert alle Template-Helper bei der Flask-App
Args:
app: Flask-App-Instanz
"""
# Funktionen registrieren
app.jinja_env.globals['ui_button'] = UIHelpers.component_button
app.jinja_env.globals['ui_badge'] = UIHelpers.component_badge
app.jinja_env.globals['ui_status_badge'] = UIHelpers.component_status_badge
app.jinja_env.globals['ui_card'] = UIHelpers.component_card
app.jinja_env.globals['ui_alert'] = UIHelpers.component_alert
app.jinja_env.globals['ui_modal'] = UIHelpers.component_modal
app.jinja_env.globals['ui_table'] = UIHelpers.component_table
# Filter registrieren
app.jinja_env.filters['german_datetime'] = UIHelpers.format_datetime_german
app.jinja_env.filters['duration'] = UIHelpers.format_duration
app.jinja_env.filters['json'] = UIHelpers.json_encode
# Zusätzliche globale Variablen
app.jinja_env.globals['current_year'] = datetime.now().year
# Icons als globale Variablen
icons = {
'check': '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>',
'x': '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>',
'plus': '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>',
'edit': '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>',
'trash': '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>',
'printer': '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"></path></svg>',
'dashboard': '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"></path></svg>',
}
app.jinja_env.globals['icons'] = icons
@app.context_processor
def utility_processor():
"""Fügt nützliche Hilfsfunktionen zu Jinja hinzu."""
return dict(
active_page=active_page,
format_datetime=format_datetime,
format_date=format_date,
format_time=format_time,
random_avatar_color=random_avatar_color,
get_initials=get_initials,
render_progress_bar=render_progress_bar
)
def active_page(path):
"""
Überprüft, ob der aktuelle Pfad mit dem gegebenen Pfad übereinstimmt.
"""
if request.path == path:
return 'active'
return ''
def format_datetime(value, format='%d.%m.%Y %H:%M'):
"""
Formatiert ein Datum mit Uhrzeit nach deutschem Format.
"""
if value is None:
return ""
if isinstance(value, str):
try:
value = datetime.fromisoformat(value)
except ValueError:
return value
return value.strftime(format)
def format_date(value, format='%d.%m.%Y'):
"""
Formatiert ein Datum nach deutschem Format.
"""
if value is None:
return ""
if isinstance(value, str):
try:
value = datetime.fromisoformat(value)
except ValueError:
return value
return value.strftime(format)
def format_time(value, format='%H:%M'):
"""
Formatiert eine Uhrzeit nach deutschem Format.
"""
if value is None:
return ""
if isinstance(value, str):
try:
value = datetime.fromisoformat(value)
except ValueError:
return value
return value.strftime(format)
def random_avatar_color():
"""
Gibt eine zufällige Hintergrundfarbe für Avatare zurück.
"""
colors = [
'bg-blue-100 text-blue-800',
'bg-green-100 text-green-800',
'bg-yellow-100 text-yellow-800',
'bg-red-100 text-red-800',
'bg-indigo-100 text-indigo-800',
'bg-purple-100 text-purple-800',
'bg-pink-100 text-pink-800',
'bg-gray-100 text-gray-800',
]
return random.choice(colors)
def get_initials(name, max_length=2):
"""
Extrahiert die Initialen eines Namens.
"""
if not name:
return "?"
parts = name.split()
if len(parts) == 1:
return name[0:max_length].upper()
initials = ""
for part in parts:
if part and len(initials) < max_length:
initials += part[0].upper()
return initials
def render_progress_bar(value, color='blue'):
"""
Rendert einen Fortschrittsbalken ohne Inline-Styles.
Args:
value (int): Der Prozentwert für den Fortschrittsbalken (0-100)
color (str): Die Farbe des Balkens (blue, green, purple, red)
Returns:
str: HTML-Markup für den Fortschrittsbalken
"""
css_class = f"progress-bar-fill-{color}"
# Sicherstellen, dass der Wert im gültigen Bereich liegt
if value < 0:
value = 0
elif value > 100:
value = 100
# Erstellen des DOM-Struktur für den Fortschrittsbalken
html = f"""
<div class="progress-bar">
<div class="progress-bar-fill {css_class}" data-width="{value}"></div>
</div>
"""
return Markup(html)