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:
+2
-1
@@ -3,4 +3,5 @@ __pycache__
|
||||
Inventarsystem_Lizenz_Verwaltung
|
||||
main.dist
|
||||
.mongo-data
|
||||
db
|
||||
db
|
||||
build
|
||||
+129
-12
@@ -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
File diff suppressed because it is too large
Load Diff
+8
-5
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; } }
|
||||
|
||||
@@ -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; } }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user