Add new templates for Dienstleistungen, Kontakt, Projekte, and Team pages

- Created 'dienstleistungen.html' to showcase services offered, including inventory system, license management, support, consulting, security concepts, and training.
- Created 'kontakt.html' for contact information, including email and phone details, along with a booking section for appointments.
- Created 'projekte.html' to highlight current and past projects and clients, providing insights into collaborations with educational institutions.
- Created 'team.html' to introduce the team structure, roles, and selected references in the educational sector.
This commit is contained in:
Maximilian G.
2026-03-24 10:56:59 +00:00
committed by GitHub
parent 5d144e9214
commit bb501b345e
23 changed files with 1487 additions and 113 deletions
+2 -1
View File
@@ -3,4 +3,5 @@ __pycache__
Inventarsystem_Lizenz_Verwaltung
main.dist
.mongo-data
db
db
build
+129 -12
View File
@@ -7,6 +7,7 @@ from datetime import timedelta, datetime, date
from pathlib import Path
from functools import wraps
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import secure_filename
import bleach
from markupsafe import escape
from pymongo import MongoClient
@@ -37,6 +38,7 @@ def set_security_headers(response):
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
INVOICE_UPLOAD_DIR = BASE_DIR / "static" / "uploads" / "invoices"
USERS_FILE = DATA_DIR / "users.json"
APPOINTMENTS_FILE = DATA_DIR / "appointments.json"
POSTS_FILE = DATA_DIR / "posts.json"
@@ -52,6 +54,24 @@ def _utc_now_iso() -> str:
return datetime.utcnow().isoformat(timespec="seconds") + "Z"
def _is_allowed_invoice_filename(filename: str) -> bool:
return bool(filename) and filename.lower().endswith(".pdf")
def _save_invoice_pdf(file_obj, invoice_number: str) -> str | None:
if not file_obj or not file_obj.filename:
return None
original_name = secure_filename(file_obj.filename)
if not _is_allowed_invoice_filename(original_name):
return None
INVOICE_UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
safe_invoice = secure_filename(invoice_number or "invoice")
unique_name = f"{datetime.utcnow().strftime('%Y%m%d%H%M%S')}_{safe_invoice}_{original_name}"
target = INVOICE_UPLOAD_DIR / unique_name
file_obj.save(target)
return f"uploads/invoices/{unique_name}"
def _get_mongo_client() -> MongoClient:
return MongoClient(MONGO_URI, serverSelectionTimeoutMS=1200)
@@ -126,6 +146,7 @@ def _ensure_user_invoice(username: str) -> None:
"amount_eur": 79.0,
"status": "Offen",
"due_date": "2026-12-31",
"pdf_path": "",
"created_at": _utc_now_iso(),
}
)
@@ -247,6 +268,26 @@ def default():
return render_template("main.html")
@app.route('/dienstleistungen')
def dienstleistungen():
return render_template("dienstleistungen.html")
@app.route('/projekte')
def projekte():
return render_template("projekte.html")
@app.route('/team')
def team():
return render_template("team.html")
@app.route('/kontakt')
def kontakt():
return render_template("kontakt.html")
@app.route('/login', methods=['GET', 'POST'])
def login():
if 'username' in session:
@@ -557,20 +598,61 @@ def blog_post(post_id):
return render_template("blog_post.html", post=post)
@app.route('/my/licenses')
@app.route('/my/licenses', methods=['GET', 'POST'])
@login_required
def my_licenses():
if request.method == 'POST':
action = _sanitize_text(request.form.get("action") or "", 30)
license_id = _sanitize_text(request.form.get("license_id") or "", 64)
target_username = _sanitize_text(request.form.get("target_username") or "", 80)
if action != "transfer" or not license_id or not target_username:
flash("Ungueltige Weitergabe-Angaben.", "error")
return redirect(url_for("my_licenses"))
if target_username == session.get("username"):
flash("Bitte einen anderen Nutzer waehlen.", "error")
return redirect(url_for("my_licenses"))
client = None
try:
client, col = _get_collection("licenses")
result = col.update_one(
{"_id": ObjectId(license_id), "username": session.get("username")},
{
"$set": {
"username": target_username,
"transferred_at": _utc_now_iso(),
"transferred_by": session.get("username"),
}
},
)
if result.modified_count:
flash("Lizenz wurde erfolgreich weitergegeben.", "success")
else:
flash("Lizenz konnte nicht weitergegeben werden.", "error")
except Exception:
flash("Weitergabe fehlgeschlagen.", "error")
finally:
if client:
client.close()
return redirect(url_for("my_licenses"))
licenses = []
client = None
try:
client, col = _get_collection("licenses")
licenses = list(col.find({"username": session.get("username")}, {"_id": 0}).sort("created_at", -1))
licenses = list(col.find({"username": session.get("username")}).sort("created_at", -1))
for item in licenses:
item["id"] = str(item.get("_id"))
except PyMongoError:
flash("Lizenzdaten konnten nicht geladen werden.", "error")
finally:
if client:
client.close()
return render_template("my_licenses.html", licenses=licenses)
transfer_users = [u for u in _list_users_for_admin() if u.get("username") != session.get("username")]
return render_template("my_licenses.html", licenses=licenses, transfer_users=transfer_users)
@app.route('/my/invoices')
@@ -821,6 +903,7 @@ def admin_licenses():
if action == "create":
username = _sanitize_text(request.form.get("username") or "", 80)
school_name = _sanitize_text(request.form.get("school_name") or "", 200)
license_key = _sanitize_text(request.form.get("license_key") or "", 120)
plan = _sanitize_text(request.form.get("plan") or "Standard", 80)
status = _sanitize_text(request.form.get("status") or "Aktiv", 40)
valid_until = _sanitize_text(request.form.get("valid_until") or "", 40)
@@ -833,7 +916,7 @@ def admin_licenses():
{
"username": username,
"school_name": school_name,
"license_key": f"LIC-{int(datetime.utcnow().timestamp())}-{username[:3].upper()}",
"license_key": license_key or f"LIC-{int(datetime.utcnow().timestamp())}-{username[:3].upper()}",
"plan": plan,
"status": status,
"valid_until": valid_until or "2027-12-31",
@@ -847,6 +930,8 @@ def admin_licenses():
{"_id": ObjectId(license_id)},
{
"$set": {
"school_name": _sanitize_text(request.form.get("school_name") or "", 200),
"license_key": _sanitize_text(request.form.get("license_key") or "", 120),
"plan": _sanitize_text(request.form.get("plan") or "Standard", 80),
"status": _sanitize_text(request.form.get("status") or "Aktiv", 40),
"valid_until": _sanitize_text(request.form.get("valid_until") or "", 40),
@@ -855,6 +940,24 @@ def admin_licenses():
)
flash("Lizenz aktualisiert.", "success")
elif action == "transfer" and license_id:
target_username = _sanitize_text(request.form.get("target_username") or "", 80)
if not target_username:
flash("Bitte Zielnutzer waehlen.", "error")
return redirect(url_for("admin_licenses"))
col.update_one(
{"_id": ObjectId(license_id)},
{
"$set": {
"username": target_username,
"transferred_at": _utc_now_iso(),
"transferred_by": session.get("username"),
}
},
)
flash("Lizenz weitergegeben.", "success")
elif action == "delete" and license_id:
col.delete_one({"_id": ObjectId(license_id)})
flash("Lizenz geloescht.", "success")
@@ -880,7 +983,8 @@ def admin_licenses():
if client:
client.close()
return render_template("admin_licenses.html", licenses=licenses)
users = _list_users_for_admin()
return render_template("admin_licenses.html", licenses=licenses, users=users)
@app.route('/admin/invoices', methods=['GET', 'POST'])
@@ -896,10 +1000,13 @@ def admin_invoices():
if action == "create":
username = _sanitize_text(request.form.get("username") or "", 80)
invoice_number = _sanitize_text(request.form.get("invoice_number") or "", 120)
period = _sanitize_text(request.form.get("period") or "", 20)
due_date = _sanitize_text(request.form.get("due_date") or "", 20)
status = _sanitize_text(request.form.get("status") or "Offen", 40)
amount_text = _sanitize_text(request.form.get("amount_eur") or "0", 20)
normalized_invoice_number = invoice_number or f"INV-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
pdf_path = _save_invoice_pdf(request.files.get("invoice_pdf"), normalized_invoice_number)
try:
amount = float(amount_text)
@@ -913,11 +1020,12 @@ def admin_invoices():
col.insert_one(
{
"username": username,
"invoice_number": f"INV-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}",
"invoice_number": normalized_invoice_number,
"period": period,
"amount_eur": amount,
"status": status,
"due_date": due_date or "2026-12-31",
"pdf_path": pdf_path or "",
"created_at": _utc_now_iso(),
}
)
@@ -925,19 +1033,27 @@ def admin_invoices():
elif action == "update" and invoice_id:
amount_text = _sanitize_text(request.form.get("amount_eur") or "0", 20)
invoice_number = _sanitize_text(request.form.get("invoice_number") or "", 120)
pdf_path = _save_invoice_pdf(request.files.get("invoice_pdf"), invoice_number or "invoice")
try:
amount = float(amount_text)
except ValueError:
amount = 0.0
update_payload = {
"invoice_number": invoice_number,
"period": _sanitize_text(request.form.get("period") or "", 20),
"status": _sanitize_text(request.form.get("status") or "Offen", 40),
"due_date": _sanitize_text(request.form.get("due_date") or "", 20),
"amount_eur": amount,
}
if pdf_path:
update_payload["pdf_path"] = pdf_path
col.update_one(
{"_id": ObjectId(invoice_id)},
{
"$set": {
"status": _sanitize_text(request.form.get("status") or "Offen", 40),
"due_date": _sanitize_text(request.form.get("due_date") or "", 20),
"amount_eur": amount,
}
"$set": update_payload
},
)
flash("Rechnung aktualisiert.", "success")
@@ -967,7 +1083,8 @@ def admin_invoices():
if client:
client.close()
return render_template("admin_invoices.html", invoices=invoices)
users = _list_users_for_admin()
return render_template("admin_invoices.html", invoices=invoices, users=users)
@app.route('/datenschutz')
+1013
View File
File diff suppressed because it is too large Load Diff
+8 -5
View File
@@ -17,7 +17,10 @@ start_mongodb_if_needed() {
mkdir -p "$MONGO_DBPATH"
echo "Starting MongoDB with dbPath: $MONGO_DBPATH"
mongod --dbpath "$MONGO_DBPATH" --bind_ip 127.0.0.1 --port 27017
mongod --dbpath "$MONGO_DBPATH" --bind_ip 127.0.0.1 --port 27017 --fork --logpath "$SCRIPT_DIR/mongodb.log" || {
echo "Failed to start MongoDB. Check $SCRIPT_DIR/mongodb.log for details."
exit 1
}
}
start_mongodb_if_needed
@@ -95,12 +98,12 @@ if ! command -v patchelf >/dev/null 2>&1; then
fi
pip install --upgrade pip
pip install -U nuitka ordered-set zstandard flask flask-jwt-extended cryptography pyotp qrcode bleach
pip install -U nuitka ordered-set zstandard flask flask-jwt-extended cryptography pyotp qrcode bleach pymongo
python -m nuitka --standalone --follow-imports --include-data-dir=templates=templates --include-data-dir=static=static --include-data-dir=data=data --assume-yes-for-downloads --output-dir=build --remove-output main.py
if [[ -x "./build/main.bin" ]]; then
./build/main.bin
if [[ -x "./build/main.dist/main.bin" ]]; then
./build/main.dist/main.bin
else
echo "Build step finished but executable not found at ./build/main.bin"
echo "Build step finished but executable not found at ./build/main.dist/main.bin"
exit 1
fi
+5 -5
View File
@@ -172,12 +172,12 @@
></textarea>
</div>
<input type="hidden" name="action" value="create" />
<button type="submit" class="submit-btn">Veroeffentlichen</button>
<button type="submit" class="submit-btn">Veröffentlichen</button>
</form>
</aside>
<article>
<h2 style="font-size: 1.2rem; color: #0f354d; margin: 0 0 0.8rem;">Veroeffentlichte Beitraege</h2>
<h2 style="font-size: 1.2rem; color: #0f354d; margin: 0 0 0.8rem;">Veröffentlichte Beiträge</h2>
<section class="posts-list">
{% if posts %}
{% for post in posts %}
@@ -187,18 +187,18 @@
<p>{{ post.excerpt }}</p>
<div class="post-meta">
<strong>Autor:</strong> {{ post.author }}<br />
<strong>Veroeffentlicht:</strong> {{ post.created_at[:10] }}
<strong>Veröffentlicht:</strong> {{ post.created_at[:10] }}
</div>
</div>
<form method="POST" action="{{ url_for('admin_blog') }}" class="delete-form">
<input type="hidden" name="action" value="delete" />
<input type="hidden" name="post_id" value="{{ post.id }}" />
<button type="submit" class="delete-btn" onclick="return confirm('Sicher?')">Loeschen</button>
<button type="submit" class="delete-btn" onclick="return confirm('Sicher?')">Löschen</button>
</form>
</article>
{% endfor %}
{% else %}
<p>Noch keine Beitraege veroeffentlicht.</p>
<p>Noch keine Beiträge veröffentlicht.</p>
{% endif %}
</section>
</article>
+3 -3
View File
@@ -165,11 +165,11 @@
</article>
<article class="stat-card">
<div class="stat-value">{{ status_counts.Bestaetigt }}</div>
<div class="stat-label">Bestaetigt</div>
<div class="stat-label">Bestätigt</div>
</article>
<article class="stat-card">
<div class="stat-value">{{ total_posts }}</div>
<div class="stat-label">Blog-Beitraege</div>
<div class="stat-label">Blog-Beiträge</div>
</article>
</section>
@@ -195,7 +195,7 @@
<form method="POST" action="{{ url_for('update_appointment', appointment_id=appointment.id) }}" class="action-form" style="min-width: 280px;">
<textarea class="response-input" name="response" placeholder="Antwort (optional)"></textarea>
<div style="display: flex; gap: 0.4rem;">
<button type="submit" name="action" value="confirm" class="confirm-btn action-btn">Bestaetigen</button>
<button type="submit" name="action" value="confirm" class="confirm-btn action-btn">Bestätigen</button>
<button type="submit" name="action" value="reject" class="reject-btn action-btn">Ablehnen</button>
</div>
</form>
+24 -10
View File
@@ -5,18 +5,25 @@
{% block content %}
<section class="panel">
<h1>Rechnungsverwaltung</h1>
<p>Rechnungen fuer Nutzer erstellen, aktualisieren und loeschen.</p>
<p>Rechnungen für Nutzer erstellen, aktualisieren und löschen.</p>
</section>
<section class="layout">
<form method="post" class="form-card">
<form method="post" class="form-card" enctype="multipart/form-data">
<h3>Neue Rechnung</h3>
<input type="hidden" name="action" value="create">
<input type="text" name="username" placeholder="Benutzername" required>
<select name="username" required>
<option value="">Benutzer auswählen</option>
{% for user in users %}
<option value="{{ user.username }}">{{ user.username }} ({{ user.display_name }})</option>
{% endfor %}
</select>
<input type="text" name="invoice_number" placeholder="Rechnungsnummer (optional, eigener Wert möglich)">
<input type="text" name="period" placeholder="Zeitraum (MM/YYYY)" required>
<input type="text" name="amount_eur" placeholder="Betrag in EUR" required>
<input type="text" name="due_date" placeholder="Faelligkeit (YYYY-MM-DD)">
<input type="text" name="due_date" placeholder="Fälligkeit (YYYY-MM-DD)">
<input type="text" name="status" placeholder="Status (Offen, Bezahlt)">
<input type="file" name="invoice_pdf" accept="application/pdf">
<button type="submit">Rechnung anlegen</button>
</form>
@@ -25,19 +32,25 @@
<article class="entry">
<h3>{{ item.invoice_number }}</h3>
<p><strong>Nutzer:</strong> {{ item.username }} | <strong>Zeitraum:</strong> {{ item.period }}</p>
<p><strong>Betrag:</strong> {{ '%.2f'|format(item.amount_eur) }} EUR | <strong>Status:</strong> {{ item.status }} | <strong>Faellig:</strong> {{ item.due_date }}</p>
<form method="post" class="inline-form">
<p><strong>Betrag:</strong> {{ '%.2f'|format(item.amount_eur) }} EUR | <strong>Status:</strong> {{ item.status }} | <strong>Fälligkeit:</strong> {{ item.due_date }}</p>
{% if item.pdf_path %}
<p><strong>PDF:</strong> <a href="{{ url_for('static', filename=item.pdf_path) }}" target="_blank" rel="noopener">Ansehen</a></p>
{% endif %}
<form method="post" class="inline-form" enctype="multipart/form-data">
<input type="hidden" name="action" value="update">
<input type="hidden" name="invoice_id" value="{{ item.id }}">
<input type="text" name="invoice_number" value="{{ item.invoice_number }}" placeholder="Rechnungsnummer">
<input type="text" name="period" value="{{ item.period }}" placeholder="Zeitraum">
<input type="text" name="amount_eur" value="{{ item.amount_eur }}" placeholder="Betrag">
<input type="text" name="due_date" value="{{ item.due_date }}" placeholder="Faelligkeit">
<input type="text" name="due_date" value="{{ item.due_date }}" placeholder="Fälligkeit">
<input type="text" name="status" value="{{ item.status }}" placeholder="Status">
<input type="file" name="invoice_pdf" accept="application/pdf">
<button type="submit">Aktualisieren</button>
</form>
<form method="post">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="invoice_id" value="{{ item.id }}">
<button type="submit" class="danger">Loeschen</button>
<button type="submit" class="danger">Löschen</button>
</form>
</article>
{% else %}
@@ -52,8 +65,9 @@
.form-card, .entry { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; }
.form-card { display: grid; gap: 0.6rem; align-self: start; }
.list { display: grid; gap: 0.8rem; }
.inline-form { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.45rem; margin-top: 0.6rem; }
input { width: 100%; border: 1px solid #c9d8e3; border-radius: 10px; padding: 0.52rem; font: inherit; }
.inline-form { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 0.45rem; margin-top: 0.6rem; }
input, select { width: 100%; border: 1px solid #c9d8e3; border-radius: 10px; padding: 0.52rem; font: inherit; background: #fff; }
a { color: #0a4c74; font-weight: 700; }
button { border: 1px solid #0a4c74; background: linear-gradient(120deg, #0c5a86 0%, #08486c 100%); color: #fff; padding: 0.45rem 0.8rem; border-radius: 999px; font-weight: 700; }
button.danger { border-color: #d04a49; background: #fff; color: #922e2e; margin-top: 0.55rem; }
@media (max-width: 980px) { .layout { grid-template-columns: 1fr; } .inline-form { grid-template-columns: 1fr; } }
+32 -8
View File
@@ -5,18 +5,24 @@
{% block content %}
<section class="panel">
<h1>Lizenzverwaltung</h1>
<p>Lizenzen fuer Nutzer anlegen, bearbeiten und loeschen.</p>
<p>Lizenzen für Nutzer anlegen, bearbeiten und löschen.</p>
</section>
<section class="layout">
<form method="post" class="form-card">
<h3>Neue Lizenz</h3>
<input type="hidden" name="action" value="create">
<input type="text" name="username" placeholder="Benutzername" required>
<select name="username" required>
<option value="">Benutzer auswählen</option>
{% for user in users %}
<option value="{{ user.username }}">{{ user.username }} ({{ user.display_name }})</option>
{% endfor %}
</select>
<input type="text" name="school_name" placeholder="Schulname" required>
<input type="text" name="license_key" placeholder="Lizenz-Key (optional, eigener Wert moeglich)">
<input type="text" name="plan" placeholder="Plan (z. B. Standard)">
<input type="text" name="status" placeholder="Status (Aktiv, Pausiert)">
<input type="text" name="valid_until" placeholder="Gueltig bis (YYYY-MM-DD)">
<input type="text" name="valid_until" placeholder="Gültig bis (YYYY-MM-DD)">
<button type="submit">Lizenz anlegen</button>
</form>
@@ -25,19 +31,37 @@
<article class="entry">
<h3>{{ item.username }} - {{ item.school_name }}</h3>
<p><strong>Key:</strong> {{ item.license_key }} | <strong>Plan:</strong> {{ item.plan }}</p>
<p><strong>Status:</strong> {{ item.status }} | <strong>Gueltig bis:</strong> {{ item.valid_until }}</p>
<p><strong>Status:</strong> {{ item.status }} | <strong>Gültig bis:</strong> {{ item.valid_until }}</p>
{% if item.transferred_at %}
<p><strong>Weitergegeben:</strong> {{ item.transferred_at }}</p>
{% endif %}
<form method="post" class="inline-form">
<input type="hidden" name="action" value="update">
<input type="hidden" name="license_id" value="{{ item.id }}">
<input type="text" name="school_name" value="{{ item.school_name }}" placeholder="Schulname">
<input type="text" name="license_key" value="{{ item.license_key }}" placeholder="Lizenz-Key">
<input type="text" name="plan" value="{{ item.plan }}" placeholder="Plan">
<input type="text" name="status" value="{{ item.status }}" placeholder="Status">
<input type="text" name="valid_until" value="{{ item.valid_until }}" placeholder="Gueltig bis">
<input type="text" name="valid_until" value="{{ item.valid_until }}" placeholder="Gültig bis">
<button type="submit">Aktualisieren</button>
</form>
<form method="post" class="inline-form" style="margin-top:0.45rem;">
<input type="hidden" name="action" value="transfer">
<input type="hidden" name="license_id" value="{{ item.id }}">
<select name="target_username" required>
<option value="">Lizenz weitergeben an...</option>
{% for user in users %}
{% if user.username != item.username %}
<option value="{{ user.username }}">{{ user.username }} ({{ user.display_name }})</option>
{% endif %}
{% endfor %}
</select>
<button type="submit">Weitergeben</button>
</form>
<form method="post">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="license_id" value="{{ item.id }}">
<button type="submit" class="danger">Loeschen</button>
<button type="submit" class="danger">Löschen</button>
</form>
</article>
{% else %}
@@ -52,8 +76,8 @@
.form-card, .entry { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; }
.form-card { display: grid; gap: 0.6rem; align-self: start; }
.list { display: grid; gap: 0.8rem; }
.inline-form { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.45rem; margin-top: 0.6rem; }
input { width: 100%; border: 1px solid #c9d8e3; border-radius: 10px; padding: 0.52rem; font: inherit; }
.inline-form { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 0.45rem; margin-top: 0.6rem; }
input, select { width: 100%; border: 1px solid #c9d8e3; border-radius: 10px; padding: 0.52rem; font: inherit; background: #fff; }
button { border: 1px solid #0a4c74; background: linear-gradient(120deg, #0c5a86 0%, #08486c 100%); color: #fff; padding: 0.45rem 0.8rem; border-radius: 999px; font-weight: 700; }
button.danger { border-color: #d04a49; background: #fff; color: #922e2e; margin-top: 0.55rem; }
@media (max-width: 980px) { .layout { grid-template-columns: 1fr; } .inline-form { grid-template-columns: 1fr; } }
+2 -2
View File
@@ -12,14 +12,14 @@
{% for ticket in tickets %}
<article class="ticket">
<h3>{{ ticket.title }}</h3>
<p><strong>Nutzer:</strong> {{ ticket.username }} | <strong>Prioritaet:</strong> {{ ticket.priority }}</p>
<p><strong>Nutzer:</strong> {{ ticket.username }} | <strong>Priorität:</strong> {{ ticket.priority }}</p>
<p>{{ ticket.description }}</p>
<form method="post" class="update-form">
<input type="hidden" name="ticket_id" value="{{ ticket.id }}">
<select name="status">
<option value="Offen" {{ 'selected' if ticket.status == 'Offen' else '' }}>Offen</option>
<option value="In Bearbeitung" {{ 'selected' if ticket.status == 'In Bearbeitung' else '' }}>In Bearbeitung</option>
<option value="Geloest" {{ 'selected' if ticket.status == 'Geloest' else '' }}>Geloest</option>
<option value="Geloest" {{ 'selected' if ticket.status == 'Geloest' else '' }}>Gelöst</option>
</select>
<textarea name="admin_response" rows="3" placeholder="Antwort an Nutzer">{{ ticket.admin_response }}</textarea>
<button type="submit">Aktualisieren</button>
+15 -23
View File
@@ -25,26 +25,19 @@
<td>{{ user.display_name }}</td>
<td>{{ 'Admin' if user.is_admin else 'Nutzer' }}</td>
<td>
<div class="actions">
{% if not user.is_admin %}
<form method="post">
<input type="hidden" name="username" value="{{ user.username }}">
<input type="hidden" name="action" value="make_admin">
<button type="submit">Zu Admin</button>
</form>
{% else %}
<form method="post">
<input type="hidden" name="username" value="{{ user.username }}">
<input type="hidden" name="action" value="remove_admin">
<button type="submit" class="warn">Admin entziehen</button>
</form>
{% endif %}
<form method="post">
<input type="hidden" name="username" value="{{ user.username }}">
<input type="hidden" name="action" value="delete_user">
<button type="submit" class="danger">Loeschen</button>
</form>
</div>
<form method="post" class="actions">
<input type="hidden" name="username" value="{{ user.username }}">
<select name="action" required>
<option value="">Aktion wählen</option>
{% if not user.is_admin %}
<option value="make_admin">Zu Admin machen</option>
{% else %}
<option value="remove_admin">Admin-Rechte entziehen</option>
{% endif %}
<option value="delete_user">Benutzer löschen</option>
</select>
<button type="submit">Ausführen</button>
</form>
</td>
</tr>
{% endfor %}
@@ -57,9 +50,8 @@
.table-wrap { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 0.7rem; border-bottom: 1px solid #e8eef2; }
.actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
select { border: 1px solid #bcd0de; background: #fff; padding: 0.4rem 0.55rem; border-radius: 10px; font: inherit; }
button { border: 1px solid #bcd0de; background: #fff; padding: 0.4rem 0.65rem; border-radius: 999px; font-weight: 700; cursor: pointer; }
button.warn { border-color: #efcc95; }
button.danger { border-color: #f0b7b7; color: #8d2929; }
</style>
{% endblock %}
+3 -3
View File
@@ -194,7 +194,7 @@
<div class="calendar-header">
<h1>{{ month_name }} {{ year }}</h1>
<div class="month-nav">
<a href="{{ url_for('appointments', month=previous_month, year=previous_year) }}">Zurueck</a>
<a href="{{ url_for('appointments', month=previous_month, year=previous_year) }}">Zurück</a>
<a href="{{ url_for('appointments', month=next_month, year=next_year) }}">Weiter</a>
</div>
</div>
@@ -231,7 +231,7 @@
<aside class="panel booking-form">
<h2>Termin anfragen</h2>
<span class="selected-date-badge" id="selectedDateBadge">Kein Datum gewaehlt</span>
<span class="selected-date-badge" id="selectedDateBadge">Kein Datum gewählt</span>
<form method="POST" action="{{ url_for('appointments', month=month, year=year) }}">
<input type="hidden" id="selectedDateInput" name="selected_date" required>
<div class="field">
@@ -279,7 +279,7 @@
function setSelection(dateValue, button) {
selectedDateInput.value = dateValue;
selectedDateBadge.textContent = "Ausgewaehlt: " + dateValue;
selectedDateBadge.textContent = "Ausgewählt: " + dateValue;
dayGrid.querySelectorAll(".day").forEach(function (node) {
node.classList.remove("selected");
+6 -13
View File
@@ -7,7 +7,7 @@
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>{% block title %}IT Loesungen{% endblock %}</title>
<title>{% block title %}IT Lösungen{% endblock %}</title>
{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -264,10 +264,10 @@
<div class="site-shell site-nav-inner">
<a class="brand" href="{{ url_for('default') }}">Invario</a>
<div class="nav-links" aria-label="Main Navigation">
<a href="{{ url_for('default') }}#leistungen">Dienstleistungen</a>
<a href="{{ url_for('default') }}#projekte">Projekte</a>
<a href="{{ url_for('default') }}#team">Team</a>
<a href="{{ url_for('default') }}#kontakt">Kontakt</a>
<a href="{{ url_for('dienstleistungen') }}">Dienstleistungen</a>
<a href="{{ url_for('projekte') }}">Projekte</a>
<a href="{{ url_for('team') }}">Team</a>
<a href="{{ url_for('kontakt') }}">Kontakt</a>
</div>
<div class="nav-actions">
{% if 'username' in session %}
@@ -283,13 +283,6 @@
</div>
</details>
<details class="nav-dropdown">
<summary class="nav-btn">Inhalte</summary>
<div class="dropdown-menu">
<a href="{{ url_for('blog') }}">Blog</a>
</div>
</details>
{% if session.get('is_admin') %}
<details class="nav-dropdown">
<summary class="nav-btn">Admin</summary>
@@ -331,7 +324,7 @@
<div class="footer-content">
<div class="footer-section">
<h4>Über uns</h4>
<p>Wir bieten professionelle IT-Lösungen und Dienstleistungen für Ihr Unternehmen.</p>
<p>Wir bieten professionelle IT-Lösungen für Ihre Inventur.</p>
</div>
<div class="footer-section">
<h4>Schnelllinks</h4>
+2 -2
View File
@@ -93,7 +93,7 @@
{% block content %}
<section class="blog-header">
<h1>Blog</h1>
<p>Neuigkeiten und Insights rund um IT-Loesungen und Projekte</p>
<p>Neuigkeiten und Insights rund um Updates und Technologie</p>
</section>
{% if posts %}
@@ -112,6 +112,6 @@
{% endfor %}
</section>
{% else %}
<p style="text-align: center; color: #5a7a8f; padding: 2rem 0;">Noch keine Beitraege veroeffentlicht.</p>
<p style="text-align: center; color: #5a7a8f; padding: 2rem 0;">Noch keine Beiträge veröffentlicht.</p>
{% endif %}
{% endblock %}
+1 -1
View File
@@ -122,6 +122,6 @@
{{ post.content }}
</section>
<a href="{{ url_for('blog') }}" class="back-link">← Zurueck zum Blog</a>
<a href="{{ url_for('blog') }}" class="back-link">← Zurück zum Blog</a>
</article>
{% endblock %}
+44
View File
@@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block title %}Dienstleistungen | Invario{% endblock %}
{% block content %}
<section class="page-hero">
<h1>Unsere Dienstleistungen</h1>
<p>Praxisnahe IT-Lösungen für Schulen mit klaren Prozessen, direkter Betreuung und zuverlässiger Umsetzung.</p>
</section>
<section class="cards">
<article class="card">
<h2>Inventarsystem</h2>
<p>Strukturierte Verwaltung von Geräten, Zubehör und Standorten mit nachvollziehbaren Zuständigkeiten und sauberer Historie.</p>
</article>
<article class="card">
<h2>Lizenzverwaltung</h2>
<p>Zentrale Übersicht über aktive, pausierte und abgelaufene Lizenzen inklusive Laufzeiten, Zuständen und Übergaben.</p>
</article>
<article class="card">
<h2>24/7 Support</h2>
<p>Schnelle Reaktionszeiten bei Störungen, klare Eskalationspfade und direkte Hilfe bei betriebskritischen Themen.</p>
</article>
<article class="card">
<h2>Beratung</h2>
<p>Strategische und technische Beratung für Schulträger und Schulleitungen von der Planung bis zum laufenden Betrieb.</p>
</article>
<article class="card">
<h2>Sicherheitskonzepte</h2>
<p>DSGVO-konforme Abläufe, Zugriffsrollen und dokumentierte Sicherheitsstandards für sensible Schuldaten.</p>
</article>
<article class="card">
<h2>Einführung & Schulung</h2>
<p>Geordnete Einführung im Team, kurze Lernkurve und praxisorientierte Schulung für Verwaltung und Kollegium.</p>
</article>
</section>
<style>
.page-hero { margin-bottom: 1.2rem; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 0.9rem; }
.card { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; }
.card h2 { color: #0f334b; margin-bottom: 0.45rem; font-size: clamp(1.05rem, 1.8vw, 1.4rem); }
</style>
{% endblock %}
+42
View File
@@ -0,0 +1,42 @@
{% extends "base.html" %}
{% block title %}Kontakt | Invario{% endblock %}
{% block content %}
<section class="page-hero">
<h1>Kontakt</h1>
<p>Sie erreichen uns schnell über E-Mail oder Telefon. Für eine strukturierte Anfrage empfehlen wir die Terminbuchung.</p>
</section>
<section class="contact-grid">
<article class="contact-card">
<h2>Info</h2>
<p><strong>Info E-Mail:</strong> info@invario.de</p>
<p><strong>Telefonnummer:</strong> +49 (0) 123 / 456789</p>
</article>
<article class="contact-card">
<h2>Support</h2>
<p><strong>Support E-Mail:</strong> support@invario.de</p>
<p><strong>Support Telefonnummer:</strong> +49 (0) 123 / 456790</p>
</article>
<article class="contact-card">
<h2>Terminbuchung</h2>
<p>Für Erstgespräche, Rückfragen und Projektanfragen buchen Sie direkt einen Termin.</p>
{% if 'username' in session %}
<a class="btn" href="{{ url_for('appointments') }}">Zur Terminbuchung</a>
{% else %}
<a class="btn" href="{{ url_for('login') }}">Login zur Terminbuchung</a>
{% endif %}
</article>
</section>
<style>
.page-hero { margin-bottom: 1.2rem; }
.contact-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 0.9rem; }
.contact-card { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; }
.contact-card h2 { color: #0f334b; margin-bottom: 0.45rem; font-size: clamp(1.05rem, 1.8vw, 1.4rem); }
.btn { margin-top: 0.5rem; display: inline-flex; align-items: center; justify-content: center; border-radius: 999px; border: 1px solid #0a4c74; background: linear-gradient(120deg, #0c5a86 0%, #08486c 100%); color: #fff; padding: 0.52rem 0.92rem; font-weight: 700; }
</style>
{% endblock %}
+19 -19
View File
@@ -413,15 +413,15 @@
{% else %}
<a class="btn btn-primary" href="{{ url_for('login') }}">Termin anfragen</a>
{% endif %}
<a class="btn btn-primary" href="#kontakt">Kontakt aufnehmen</a>
<a class="btn btn-secondary" href="#leistungen">Dienstleistungen ansehen</a>
<a class="btn btn-primary" href="{{ url_for('kontakt') }}">Kontakt aufnehmen</a>
<a class="btn btn-secondary" href="{{ url_for('dienstleistungen') }}">Dienstleistungen ansehen</a>
</div>
</div>
<aside class="hero-card" aria-label="Kurzfakten">
<h3>Inventur mit Verstand</h3>
<p>
Von Infrastruktur bis Governance: Wir verbinden konzeptionelle Tiefe mit
pragmatischer Umsetzung im Tagesgeschaeft.
pragmatischer Umsetzung im Tagesgeschäft.
</p>
<div class="stats">
<div><strong>25+</strong><span>aktive Mandate</span></div>
@@ -436,7 +436,7 @@
<div class="partnership-content">
<h3>Entwickelt mit Schulen für Schulen</h3>
<p>
Unser Inventarsystem wurde nicht theoretisch konzipiert, sondern in enger Zusammenarbeit mit Einzelnen Schulen entwickelt. Wir verstehen die täglichen Herausforderungen im Schulbetrieb und entwickeln Lösungen, die wirklich einen Vorteil für den Schulaltag bringen.
Unser Inventarsystem wurde nicht theoretisch konzipiert, sondern in enger Zusammenarbeit mit einzelnen Schulen entwickelt. Wir verstehen die täglichen Herausforderungen im Schulbetrieb und entwickeln Lösungen, die wirklich einen Vorteil für den Schulalltag bringen.
</p>
<div class="partnership-badges">
<span class="badge">✓ Schulgetestet</span>
@@ -449,45 +449,45 @@
<section class="section" id="leistungen" style="animation-delay: 100ms;">
<h2>Leistungsbereiche</h2>
<p>Klare Gliederung in Konnektivitaet, Inventarverwaltung und laufenden Betrieb.</p>
<p>Klare Gliederung in Konnektivität, Inventarverwaltung und laufenden Betrieb.</p>
<div class="section-grid">
<article class="card">
<h3>Konnektivitaet und Netzwerk</h3>
<h3>Konnektivität und Netzwerk</h3>
<p>Stabile Anbindung Ihrer Schulstandorte, saubere Netzstruktur und sichere Erreichbarkeit aller relevanten Systeme.</p>
</article>
<article class="card">
<h3>Inventar und Lizenzmanagement</h3>
<p>Zentrale Verwaltung von Geraeten und Lizenzen mit schneller Uebersicht, nachvollziehbaren Prozessen und klaren Zustaendigkeiten.</p>
<p>Zentrale Verwaltung von Geräten und Lizenzen mit schneller Übersicht, nachvollziehbaren Prozessen und klaren Zuständigkeiten.</p>
</article>
<article class="card">
<h3>Support, Updates und Beratung</h3>
<p>Persoenliche Ansprechpartner, kurze Reaktionszeiten und planbare Updates fuer einen zuverlaessigen Betrieb im Schulalltag.</p>
<p>Persönliche Ansprechpartner, kurze Reaktionszeiten und planbare Updates für einen zuverlässigen Betrieb im Schulalltag.</p>
</article>
</div>
</section>
<section class="section" id="projekte" style="animation-delay: 180ms;">
<h2>Module im Ueberblick</h2>
<p>Alle Kernbereiche fuer Schulen in einer strukturierten Plattform: Konnektivitaet, Sicherheit und Betrieb.</p>
<h2>Sicherheit</h2>
<p>Wir legen großen Wert auf die Sicherheit Ihrer Daten und Systeme. Die Daten liegen ausschließlich gesichert bei ihnen auf dem Server und wird in keinem Fall an dritte weitergegeben.</p>
<div class="project-list">
<div class="project">
<div>
<b>Konnektivitaet</b>
Sichere Standortanbindung, stabile Verbindungen und klare Netzwerkstruktur fuer einen reibungslosen Zugriff auf alle Inventardaten.
<b>DSGVO</b>
Alle Daten werden DSGVO-konform verarbeitet, mit klaren Einwilligungen, transparenter Dokumentation und sicheren Löschprozessen.
</div>
<span>Netzwerk</span>
</div>
<div class="project">
<div>
<b>Sicherheit und DSGVO</b>
Datenschutz nach DSGVO, automatisierte Sicherheitspruefungen und klare Compliance-Standards fuer den Einsatz im oeffentlichen Bereich.
<b>Sicherheitsprüfungen</b>
Automatisierte Sicherheitsprüfungen und klare Compliance-Standards für den Einsatz im öffentlichen Bereich. Alle an uns herangetragene Fehler werden umgehend behoben und mit einem transparenten Prozess dokumentiert.
</div>
<span>Security</span>
</div>
<div class="project">
<div>
<b>Betrieb und Updates</b>
Regelmaessige Updates, schnelle Stoerungsbehebung und laufende Optimierung sorgen fuer einen dauerhaft stabilen Schulbetrieb.
Regelmäßige Updates, schnelle Störungsbehebung und laufende Optimierung sorgen für einen dauerhaft stabilen Schulbetrieb.
</div>
<span>Updates</span>
</div>
@@ -498,18 +498,18 @@
<h2>Zusammenarbeit</h2>
<p>Strukturiert, nachvollziehbar und mit festen Ansprechpartnern von der Analyse bis zum Betrieb.</p>
<div class="process" aria-label="Prozessschritte">
<div class="step"><strong>Zuhoeren</strong><small>Anforderungen erfassen</small></div>
<div class="step"><strong>Zuhören</strong><small>Anforderungen erfassen</small></div>
<div class="step"><strong>Verstehen</strong><small>Kontext analysieren</small></div>
<div class="step"><strong>Besprechen</strong><small>Optionen priorisieren</small></div>
<div class="step"><strong>Planen</strong><small>Roadmap festlegen</small></div>
<div class="step"><strong>Realisieren</strong><small>Loesung liefern</small></div>
<div class="step"><strong>Unterstuetzen</strong><small>Betrieb absichern</small></div>
<div class="step"><strong>Realisieren</strong><small>Lösung liefern</small></div>
<div class="step"><strong>Unterstützen</strong><small>Betrieb absichern</small></div>
</div>
</section>
<section class="contact" id="kontakt">
<div>
<h2>Bereit fuer das naechste IT-Projekt?</h2>
<h2>Bereit für das nächste IT-Projekt?</h2>
<p>In einem kurzen Erstgespräch klären wir gemeinsam Zielbild, Risiken und realistische Umsetzungsschritte. Erstellen Sie dafür einfach ein Konto über den Button oben rechts und buchen Sie anschließend über die Kalenderfunktion einen Termin mit Ihrem Schulnamen und dem Betreff „Erstgespräch“. Alternativ können Sie uns jederzeit per E-Mail kontaktieren oder unseren Website-Chat nutzen.</p>
</div>
{% if 'username' in session %}
+11 -2
View File
@@ -5,7 +5,7 @@
{% block content %}
<section class="panel">
<h1>Rechnungseinsicht</h1>
<p>Alle Rechnungen im Ueberblick.</p>
<p>Alle Rechnungen im Überblick.</p>
</section>
<section class="table-wrap">
@@ -17,6 +17,7 @@
<th>Betrag</th>
<th>Faelligkeit</th>
<th>Status</th>
<th>PDF</th>
</tr>
</thead>
<tbody>
@@ -27,10 +28,17 @@
<td>{{ '%.2f'|format(invoice.amount_eur) }} EUR</td>
<td>{{ invoice.due_date }}</td>
<td>{{ invoice.status }}</td>
<td>
{% if invoice.pdf_path %}
<a href="{{ url_for('static', filename=invoice.pdf_path) }}" target="_blank" rel="noopener">PDF ansehen</a>
{% else %}
-
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="5">Noch keine Rechnungen vorhanden.</td>
<td colspan="6">Noch keine Rechnungen vorhanden.</td>
</tr>
{% endfor %}
</tbody>
@@ -42,5 +50,6 @@
.table-wrap { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 0.7rem; border-bottom: 1px solid #e8eef2; }
a { color: #0a4c74; font-weight: 700; }
</style>
{% endblock %}
+16 -2
View File
@@ -14,9 +14,20 @@
<article class="card">
<h3>{{ item.plan }} Lizenz</h3>
<p><strong>Schule:</strong> {{ item.school_name }}</p>
<p><strong>Lizenzschluessel:</strong> {{ item.license_key }}</p>
<p><strong>Lizenzschlüssel:</strong> {{ item.license_key }}</p>
<p><strong>Status:</strong> {{ item.status }}</p>
<p><strong>Gueltig bis:</strong> {{ item.valid_until }}</p>
<p><strong>Gültig bis:</strong> {{ item.valid_until }}</p>
<form method="post" class="transfer-form">
<input type="hidden" name="action" value="transfer">
<input type="hidden" name="license_id" value="{{ item.id }}">
<select name="target_username" required>
<option value="">Lizenz weitergeben an...</option>
{% for user in transfer_users %}
<option value="{{ user.username }}">{{ user.username }} ({{ user.display_name }})</option>
{% endfor %}
</select>
<button type="submit">Weitergeben</button>
</form>
</article>
{% endfor %}
{% else %}
@@ -32,5 +43,8 @@
.grid { display: grid; gap: 0.8rem; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.card { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; }
.card p { margin-top: 0.35rem; }
.transfer-form { margin-top: 0.8rem; display: grid; gap: 0.45rem; }
select, button { border: 1px solid #c9d8e3; border-radius: 10px; padding: 0.52rem; font: inherit; }
button { border-radius: 999px; color: #fff; background: linear-gradient(120deg, #0c5a86 0%, #08486c 100%); border-color: #0a4c74; font-weight: 700; }
</style>
{% endblock %}
+61
View File
@@ -0,0 +1,61 @@
{% extends "base.html" %}
{% block title %}Projekte | Invario{% endblock %}
{% block content %}
<section class="page-hero">
<h1>Projekte und Kunden</h1>
<p>Einblick in aktuelle und vergangene Zusammenarbeit mit Schulen und Bildungsträgern.</p>
</section>
<section class="split">
<article class="panel">
<h2>Aktuelle Projekte</h2>
<ul>
<li><strong>Schulverbund Nord:</strong> Rollout Inventarsystem für 12 Standorte, inklusive Gerätehistorie und Lizenzmonitoring.</li>
<li><strong>Berufskolleg West:</strong> Einführung zentraler Termin- und Supportprozesse mit Admin-Dashboard.</li>
<li><strong>Gymnasium am Park:</strong> Migration von Altbeständen und strukturierte Rechtevergabe für Fachbereiche.</li>
</ul>
</article>
<article class="panel">
<h2>Aktuelle Kunden</h2>
<ul>
<li>Städtischer Schulträger Musterstadt</li>
<li>Campus Bildung Rhein-Main</li>
<li>Verbundschule Süd</li>
<li>Fachoberschule Technikzentrum</li>
</ul>
</article>
</section>
<section class="split" style="margin-top:0.9rem;">
<article class="panel">
<h2>Vergangene Projekte</h2>
<ul>
<li><strong>Realschule Mitte:</strong> Digitalisierung der Inventurabläufe und Schulung des Verwaltungsteams.</li>
<li><strong>Bildungswerk Ost:</strong> Aufbau eines Lizenz- und Rechnungsworkflows mit Dokumentenablage.</li>
<li><strong>Campus Linden:</strong> Sicherheitsreview und Stabilisierung der laufenden IT-Prozesse.</li>
</ul>
</article>
<article class="panel">
<h2>Vergangene Kunden</h2>
<ul>
<li>Privatschulzentrum Amberg</li>
<li>Schulträger Kreis Süd</li>
<li>Akademie für Gesundheit Nord</li>
<li>Campus Schöneck</li>
</ul>
</article>
</section>
<style>
.page-hero { margin-bottom: 1.2rem; }
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 0.9rem; }
.panel { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; }
.panel h2 { color: #0f334b; margin-bottom: 0.45rem; font-size: clamp(1.05rem, 1.8vw, 1.4rem); }
.panel ul { margin: 0; padding-left: 1rem; color: #466176; display: grid; gap: 0.55rem; }
@media (max-width: 900px) { .split { grid-template-columns: 1fr; } }
</style>
{% endblock %}
+1 -1
View File
@@ -78,7 +78,7 @@
{% block content %}
<section class="auth-wrap">
<h1>Registrierung</h1>
<p>Legen Sie ein Konto an, um den Kalender fuer Terminanfragen zu nutzen.</p>
<p>Legen Sie ein Konto an, um den Kalender für Terminanfragen zu nutzen.</p>
<form method="POST" action="{{ url_for('register') }}">
<div class="field">
<label for="display_name">Name</label>
+47
View File
@@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}Team | Invario{% endblock %}
{% block content %}
<section class="page-hero">
<h1>Unser Team</h1>
<p>Wir verbinden technische Expertise, Projekterfahrung und starke Referenzen im Bildungsumfeld.</p>
</section>
<section class="grid">
<article class="member">
<h2>Leitung & Strategie</h2>
<p>Verantwortlich für Projektsteuerung, Qualitätsstandards und die Ausrichtung unserer Lösungen auf den Schulbetrieb.</p>
<p><strong>Referenzen:</strong> Schulträgerprojekte mit mehr als 30 Standorten, Prozessstandardisierung im laufenden Betrieb.</p>
</article>
<article class="member">
<h2>Entwicklung</h2>
<p>Konzipiert und entwickelt unsere Plattform mit Fokus auf Zuverlässigkeit, Sicherheit und einfache Bedienung.</p>
<p><strong>Referenzen:</strong> Einführung modularer Inventarsysteme, automatisierte Verwaltungsabläufe, DSGVO-konforme Datenhaltung.</p>
</article>
<article class="member">
<h2>Support & Betrieb</h2>
<p>Sorgt für stabile Systeme, schnelle Unterstützung und nachvollziehbare Kommunikation mit Einrichtungen.</p>
<p><strong>Referenzen:</strong> 24/7 Betreuung für Bildungseinrichtungen, SLA-basierte Reaktionszeiten und strukturierte Ticketprozesse.</p>
</article>
</section>
<section class="references">
<h2>Ausgewählte Referenzen</h2>
<ul>
<li>Einführung zentraler Inventurprozesse in Schulverbünden</li>
<li>Beratung zur IT-Governance für Schulleitungen und Träger</li>
<li>Sicherheits- und Compliance-Begleitung bei Systemumstellungen</li>
<li>Operative Stabilisierung von Support- und Terminprozessen</li>
</ul>
</section>
<style>
.page-hero { margin-bottom: 1.2rem; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 0.9rem; }
.member, .references { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; }
.member h2, .references h2 { color: #0f334b; margin-bottom: 0.45rem; font-size: clamp(1.05rem, 1.8vw, 1.4rem); }
.references { margin-top: 0.9rem; }
.references ul { margin: 0; padding-left: 1rem; color: #466176; display: grid; gap: 0.5rem; }
</style>
{% endblock %}
+1 -1
View File
@@ -25,7 +25,7 @@
{% for item in tickets %}
<article class="ticket">
<h3>{{ item.title }}</h3>
<p><strong>Status:</strong> {{ item.status }} | <strong>Prioritaet:</strong> {{ item.priority }}</p>
<p><strong>Status:</strong> {{ item.status }} | <strong>Priorität:</strong> {{ item.priority }}</p>
<p>{{ item.description }}</p>
{% if item.admin_response %}
<p><strong>Admin Antwort:</strong> {{ item.admin_response }}</p>