Enhance admin dashboard and team management features
- Added a quick admin link section in the admin dashboard for managing team cards. - Updated the admin licenses template to correct German language spelling. - Improved the appointments template with conditional rendering for test package options. - Enhanced the base template with a logo image and adjusted brand styling. - Updated the license management template for consistent German language usage. - Removed the tutorial template and replaced it with a new admin team management template. - Introduced a new admin team management page allowing for the addition, editing, and deletion of team members.
This commit is contained in:
+264
-39
@@ -40,6 +40,7 @@ def set_security_headers(response):
|
|||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
INVOICE_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "invoices")
|
INVOICE_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "invoices")
|
||||||
|
TEAM_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "team")
|
||||||
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017")
|
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017")
|
||||||
MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "Invario_Website")
|
MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "Invario_Website")
|
||||||
|
|
||||||
@@ -56,6 +57,13 @@ def _is_allowed_invoice_filename(filename: str) -> bool:
|
|||||||
return bool(filename) and filename.lower().endswith(".pdf")
|
return bool(filename) and filename.lower().endswith(".pdf")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_allowed_image_filename(filename: str) -> bool:
|
||||||
|
if not filename:
|
||||||
|
return False
|
||||||
|
lowered = filename.lower()
|
||||||
|
return lowered.endswith(".jpg") or lowered.endswith(".jpeg") or lowered.endswith(".png") or lowered.endswith(".webp")
|
||||||
|
|
||||||
|
|
||||||
def _save_invoice_pdf(file_obj, invoice_number: str) -> str | None:
|
def _save_invoice_pdf(file_obj, invoice_number: str) -> str | None:
|
||||||
if not file_obj or not file_obj.filename:
|
if not file_obj or not file_obj.filename:
|
||||||
return None
|
return None
|
||||||
@@ -70,6 +78,21 @@ def _save_invoice_pdf(file_obj, invoice_number: str) -> str | None:
|
|||||||
return f"uploads/invoices/{unique_name}"
|
return f"uploads/invoices/{unique_name}"
|
||||||
|
|
||||||
|
|
||||||
|
def _save_team_photo(file_obj, identifier: 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_image_filename(original_name):
|
||||||
|
return None
|
||||||
|
os.makedirs(TEAM_UPLOAD_DIR, exist_ok=True)
|
||||||
|
extension = os.path.splitext(original_name)[1].lower() or ".jpg"
|
||||||
|
safe_identifier = secure_filename(identifier or "member")
|
||||||
|
unique_name = f"team_{safe_identifier}_{datetime.utcnow().strftime('%Y%m%d%H%M%S')}{extension}"
|
||||||
|
target = os.path.join(TEAM_UPLOAD_DIR, unique_name)
|
||||||
|
file_obj.save(target)
|
||||||
|
return f"uploads/team/{unique_name}"
|
||||||
|
|
||||||
|
|
||||||
def _get_mongo_client() -> MongoClient:
|
def _get_mongo_client() -> MongoClient:
|
||||||
return MongoClient(MONGO_URI, serverSelectionTimeoutMS=1200)
|
return MongoClient(MONGO_URI, serverSelectionTimeoutMS=1200)
|
||||||
|
|
||||||
@@ -119,14 +142,14 @@ def _activate_test_license_for_user(username: str, school_name: str, package_nam
|
|||||||
user_name = _sanitize_text(username or "", 80)
|
user_name = _sanitize_text(username or "", 80)
|
||||||
school = _sanitize_text(school_name or "", 200)
|
school = _sanitize_text(school_name or "", 200)
|
||||||
if not user_name:
|
if not user_name:
|
||||||
return False, "Benutzername fehlt fuer Test-Key."
|
return False, "Benutzername fehlt für Test-Key."
|
||||||
|
|
||||||
client = None
|
client = None
|
||||||
try:
|
try:
|
||||||
client, col = _get_collection("licenses")
|
client, col = _get_collection("licenses")
|
||||||
|
|
||||||
normalized_package = _sanitize_text(package_name or "Normal", 40)
|
normalized_package = _sanitize_text(package_name or "Normal", 40)
|
||||||
if normalized_package not in {"Normal", "Pro", "Buecherei", "Bücherei"}:
|
if normalized_package not in {"Normal", "Pro", "Bücherei", "Bücherei"}:
|
||||||
normalized_package = "Normal"
|
normalized_package = "Normal"
|
||||||
|
|
||||||
existing = col.find_one(
|
existing = col.find_one(
|
||||||
@@ -137,7 +160,7 @@ def _activate_test_license_for_user(username: str, school_name: str, package_nam
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if existing:
|
if existing:
|
||||||
return False, "Ein Test-Key ist fuer dieses Schulkonto bereits vorhanden."
|
return False, "Ein Test-Key ist für dieses Schulkonto bereits vorhanden."
|
||||||
|
|
||||||
test_key = f"TEST-{verify.key_generator()[:18].upper()}"
|
test_key = f"TEST-{verify.key_generator()[:18].upper()}"
|
||||||
col.insert_one(
|
col.insert_one(
|
||||||
@@ -239,6 +262,42 @@ def _sanitize_html(html_content: str, max_length: int = 50000) -> str:
|
|||||||
return bleach.clean(html_content, tags=allowed_tags, attributes=allowed_attrs, strip=True)
|
return bleach.clean(html_content, tags=allowed_tags, attributes=allowed_attrs, strip=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_team_members() -> list:
|
||||||
|
members = []
|
||||||
|
client = None
|
||||||
|
try:
|
||||||
|
client, col = _get_collection("team_members")
|
||||||
|
members = list(col.find().sort([("sort_order", 1), ("created_at", 1)]))
|
||||||
|
except PyMongoError:
|
||||||
|
return []
|
||||||
|
finally:
|
||||||
|
if client:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
normalized = []
|
||||||
|
for member in members:
|
||||||
|
public_id = str(member.get("_id") or "")
|
||||||
|
sort_raw = member.get("sort_order", 999)
|
||||||
|
try:
|
||||||
|
sort_order = int(sort_raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
sort_order = 999
|
||||||
|
normalized.append(
|
||||||
|
{
|
||||||
|
"id": public_id,
|
||||||
|
"sort_order": sort_order,
|
||||||
|
"name": _sanitize_text(member.get("name") or "", 120),
|
||||||
|
"role": _sanitize_text(member.get("role") or "", 120),
|
||||||
|
"work": _sanitize_text(member.get("work") or "", 220),
|
||||||
|
"bio": _sanitize_text(member.get("bio") or "", 500),
|
||||||
|
"photo": _sanitize_text(member.get("photo") or "", 255),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
normalized.sort(key=lambda value: (value.get("sort_order", 999), value.get("name", "")))
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
def _validate_username(username: str) -> bool:
|
def _validate_username(username: str) -> bool:
|
||||||
"""Validate username format: alphanumeric, underscore, dash, 3-30 chars."""
|
"""Validate username format: alphanumeric, underscore, dash, 3-30 chars."""
|
||||||
if not username or not isinstance(username, str):
|
if not username or not isinstance(username, str):
|
||||||
@@ -337,14 +396,10 @@ def inventarsystem():
|
|||||||
return render_template("inventarsystem.html")
|
return render_template("inventarsystem.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tutorial')
|
|
||||||
def tutorial():
|
|
||||||
return render_template("tutorial.html")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/team')
|
@app.route('/team')
|
||||||
def team():
|
def team():
|
||||||
return render_template("team.html")
|
team_members = _get_team_members()
|
||||||
|
return render_template("team.html", team_members=team_members)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/kontakt')
|
@app.route('/kontakt')
|
||||||
@@ -388,7 +443,7 @@ def login():
|
|||||||
|
|
||||||
if request.is_json:
|
if request.is_json:
|
||||||
return jsonify({"error": "Invalid credentials"}), 401
|
return jsonify({"error": "Invalid credentials"}), 401
|
||||||
flash('Login fehlgeschlagen. Bitte pruefen Sie Ihre Eingaben.', 'error')
|
flash('Login fehlgeschlagen. Bitte prüfen Sie Ihre Eingaben.', 'error')
|
||||||
get_flashed_messages()
|
get_flashed_messages()
|
||||||
|
|
||||||
return render_template('login.html')
|
return render_template('login.html')
|
||||||
@@ -406,7 +461,7 @@ def register():
|
|||||||
password_repeat = request.form.get("password_repeat") or ""
|
password_repeat = request.form.get("password_repeat") or ""
|
||||||
|
|
||||||
if not username or not school_name or not contact_person or not email or not password or not password_repeat:
|
if not username or not school_name or not contact_person or not email or not password or not password_repeat:
|
||||||
flash("Bitte alle Felder ausfuellen.", "error")
|
flash("Bitte alle Felder ausfüllen.", "error")
|
||||||
return redirect(url_for("register"))
|
return redirect(url_for("register"))
|
||||||
|
|
||||||
if not _validate_username(username):
|
if not _validate_username(username):
|
||||||
@@ -418,7 +473,7 @@ def register():
|
|||||||
return redirect(url_for("register"))
|
return redirect(url_for("register"))
|
||||||
|
|
||||||
if password != password_repeat:
|
if password != password_repeat:
|
||||||
flash("Die Passwoerter stimmen nicht ueberein.", "error")
|
flash("Die Passwörter stimmen nicht überein.", "error")
|
||||||
return redirect(url_for("register"))
|
return redirect(url_for("register"))
|
||||||
|
|
||||||
if _find_user(username):
|
if _find_user(username):
|
||||||
@@ -426,7 +481,7 @@ def register():
|
|||||||
return redirect(url_for("register"))
|
return redirect(url_for("register"))
|
||||||
|
|
||||||
if not _validate_email(email):
|
if not _validate_email(email):
|
||||||
flash("Bitte eine gueltige E-Mail-Adresse angeben.", "error")
|
flash("Bitte eine gültige E-Mail-Adresse angeben.", "error")
|
||||||
return redirect(url_for("register"))
|
return redirect(url_for("register"))
|
||||||
|
|
||||||
school_name = _sanitize_text(school_name, 120)
|
school_name = _sanitize_text(school_name, 120)
|
||||||
@@ -462,20 +517,30 @@ def register():
|
|||||||
@app.route('/appointments', methods=['GET'])
|
@app.route('/appointments', methods=['GET'])
|
||||||
def appointments():
|
def appointments():
|
||||||
software_packages = [
|
software_packages = [
|
||||||
|
{
|
||||||
|
"slug": "test",
|
||||||
|
"name": "Testversion",
|
||||||
|
"headline": "30 Tage kostenlos zum Ausprobieren",
|
||||||
|
"features": [
|
||||||
|
"Sofortiger Zugang mit Test-Key",
|
||||||
|
"Ideal für Erstbewertung im Schulalltag",
|
||||||
|
"Ohne langfristige Bindung",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"slug": "normal",
|
"slug": "normal",
|
||||||
"name": "Normal",
|
"name": "Normal",
|
||||||
"headline": "Stabiler Einstieg fuer den Schulalltag",
|
"headline": "Stabiler Einstieg für den Schulalltag",
|
||||||
"features": [
|
"features": [
|
||||||
"Inventarverwaltung mit Rollenrechten",
|
"Inventarverwaltung mit Rollenrechten",
|
||||||
"Basis-Support und Ticketing",
|
"Basis-Support und Ticketing",
|
||||||
"Schulweite Uebersichten",
|
"Schulweite Übersichten",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "pro",
|
"slug": "pro",
|
||||||
"name": "Pro",
|
"name": "Pro",
|
||||||
"headline": "Fuer Schulen mit erweitertem Bedarf",
|
"headline": "Für Schulen mit erweitertem Bedarf",
|
||||||
"features": [
|
"features": [
|
||||||
"Erweiterte Lizenzverwaltung",
|
"Erweiterte Lizenzverwaltung",
|
||||||
"Detailberichte und Admin-Insights",
|
"Detailberichte und Admin-Insights",
|
||||||
@@ -485,10 +550,10 @@ def appointments():
|
|||||||
{
|
{
|
||||||
"slug": "buecherei",
|
"slug": "buecherei",
|
||||||
"name": "Bücherei",
|
"name": "Bücherei",
|
||||||
"headline": "Optimiert fuer Bibliothek und Medien",
|
"headline": "Optimiert für Bibliothek und Medien",
|
||||||
"features": [
|
"features": [
|
||||||
"Ausleih- und Rueckgabeprozesse",
|
"Ausleih- und Rückgabeprozesse",
|
||||||
"Bestandsuebersichten fuer Medien",
|
"Bestandsübersichten für Medien",
|
||||||
"Transparente Historie pro Medium",
|
"Transparente Historie pro Medium",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -525,26 +590,69 @@ def appointments():
|
|||||||
def start_test_package():
|
def start_test_package():
|
||||||
package_raw = _sanitize_text(request.form.get("package") or "normal", 40).lower()
|
package_raw = _sanitize_text(request.form.get("package") or "normal", 40).lower()
|
||||||
package_map = {
|
package_map = {
|
||||||
|
"test": "Normal",
|
||||||
"normal": "Normal",
|
"normal": "Normal",
|
||||||
"pro": "Pro",
|
"pro": "Pro",
|
||||||
"buecherei": "Buecherei",
|
"buecherei": "Bücherei",
|
||||||
}
|
}
|
||||||
selected_package = package_map.get(package_raw)
|
selected_package = package_map.get(package_raw)
|
||||||
if not selected_package:
|
if not selected_package:
|
||||||
flash("Ungueltiges Paket ausgewaehlt.", "error")
|
flash("Ungültiges Paket ausgewählt.", "error")
|
||||||
return redirect(url_for("appointments"))
|
return redirect(url_for("appointments"))
|
||||||
|
|
||||||
school_name = _sanitize_text(session.get("display_name") or session.get("username") or "", 200)
|
school_name = _sanitize_text(session.get("display_name") or session.get("username") or "", 200)
|
||||||
activated, message = _activate_test_license_for_user(session.get("username") or "", school_name, selected_package)
|
activated, message = _activate_test_license_for_user(session.get("username") or "", school_name, selected_package)
|
||||||
|
|
||||||
if activated:
|
if activated:
|
||||||
flash(f"Testversion fuer Paket {selected_package} gestartet. Key: {message}", "success")
|
flash(f"Testversion für Paket {selected_package} gestartet. Key: {message}", "success")
|
||||||
else:
|
else:
|
||||||
flash(message, "error")
|
flash(message, "error")
|
||||||
|
|
||||||
return redirect(url_for("appointments"))
|
return redirect(url_for("appointments"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/appointments/book-option', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def book_option_package():
|
||||||
|
package_raw = _sanitize_text(request.form.get("package") or "", 40).lower()
|
||||||
|
package_map = {
|
||||||
|
"normal": "Normal",
|
||||||
|
"pro": "Pro",
|
||||||
|
"buecherei": "Bücherei",
|
||||||
|
}
|
||||||
|
selected_package = package_map.get(package_raw)
|
||||||
|
if not selected_package:
|
||||||
|
flash("Ungültige Buchungsoption ausgewählt.", "error")
|
||||||
|
return redirect(url_for("appointments"))
|
||||||
|
|
||||||
|
message = (
|
||||||
|
f"Ich möchte das Paket {selected_package} buchen. "
|
||||||
|
"Bitte kontaktieren Sie mich für die nächsten Schritte."
|
||||||
|
)
|
||||||
|
|
||||||
|
client = None
|
||||||
|
try:
|
||||||
|
client, col = _get_collection("chat_messages")
|
||||||
|
col.insert_one(
|
||||||
|
{
|
||||||
|
"username": session.get("username"),
|
||||||
|
"sender": session.get("display_name") or session.get("username"),
|
||||||
|
"sender_role": "user",
|
||||||
|
"message": message,
|
||||||
|
"created_at": _utc_now_iso(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except PyMongoError:
|
||||||
|
flash("Buchungsanfrage konnte nicht gesendet werden.", "error")
|
||||||
|
return redirect(url_for("appointments"))
|
||||||
|
finally:
|
||||||
|
if client:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
flash(f"Buchungsanfrage für Paket {selected_package} wurde an den Admin gesendet.", "success")
|
||||||
|
return redirect(url_for("user_chat"))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin/dashboard')
|
@app.route('/admin/dashboard')
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_dashboard():
|
def admin_dashboard():
|
||||||
@@ -604,7 +712,7 @@ def admin_block_day():
|
|||||||
try:
|
try:
|
||||||
date.fromisoformat(block_date)
|
date.fromisoformat(block_date)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
flash("Bitte ein gueltiges Datum zum Sperren waehlen.", "error")
|
flash("Bitte ein gültiges Datum zum Sperren wählen.", "error")
|
||||||
return redirect(url_for("admin_dashboard"))
|
return redirect(url_for("admin_dashboard"))
|
||||||
|
|
||||||
if col.find_one({"date": block_date}):
|
if col.find_one({"date": block_date}):
|
||||||
@@ -627,7 +735,7 @@ def admin_block_day():
|
|||||||
return redirect(url_for("admin_dashboard"))
|
return redirect(url_for("admin_dashboard"))
|
||||||
flash("Sperrtag entfernt.", "success")
|
flash("Sperrtag entfernt.", "success")
|
||||||
else:
|
else:
|
||||||
flash("Ungueltige Aktion fuer Kalendersperre.", "error")
|
flash("Ungültige Aktion für Kalendersperre.", "error")
|
||||||
return redirect(url_for("admin_dashboard"))
|
return redirect(url_for("admin_dashboard"))
|
||||||
except PyMongoError:
|
except PyMongoError:
|
||||||
flash("Kalendersperre konnte nicht gespeichert werden.", "error")
|
flash("Kalendersperre konnte nicht gespeichert werden.", "error")
|
||||||
@@ -645,7 +753,7 @@ def update_appointment(appointment_id):
|
|||||||
response_text = _sanitize_text(request.form.get("response") or "", 5000)
|
response_text = _sanitize_text(request.form.get("response") or "", 5000)
|
||||||
|
|
||||||
if action not in ["confirm", "reject"]:
|
if action not in ["confirm", "reject"]:
|
||||||
flash("Ungueltige Aktion.", "error")
|
flash("Ungültige Aktion.", "error")
|
||||||
return redirect(url_for("admin_dashboard"))
|
return redirect(url_for("admin_dashboard"))
|
||||||
|
|
||||||
query = _appointment_query_from_id(appointment_id)
|
query = _appointment_query_from_id(appointment_id)
|
||||||
@@ -695,7 +803,7 @@ def admin_blog():
|
|||||||
excerpt = _sanitize_text(request.form.get("excerpt") or "", 500)
|
excerpt = _sanitize_text(request.form.get("excerpt") or "", 500)
|
||||||
|
|
||||||
if not title or not content:
|
if not title or not content:
|
||||||
flash("Bitte Titel und inhalt ausfuellen.", "error")
|
flash("Bitte Titel und inhalt ausfüllen.", "error")
|
||||||
return redirect(url_for("admin_blog"))
|
return redirect(url_for("admin_blog"))
|
||||||
client = None
|
client = None
|
||||||
try:
|
try:
|
||||||
@@ -725,14 +833,14 @@ def admin_blog():
|
|||||||
post_id = (request.form.get("post_id") or "").strip()
|
post_id = (request.form.get("post_id") or "").strip()
|
||||||
query = _post_query_from_id(post_id)
|
query = _post_query_from_id(post_id)
|
||||||
if not query:
|
if not query:
|
||||||
flash("Ungueltige Beitrag-ID.", "error")
|
flash("Ungültige Beitrag-ID.", "error")
|
||||||
return redirect(url_for("admin_blog"))
|
return redirect(url_for("admin_blog"))
|
||||||
client = None
|
client = None
|
||||||
try:
|
try:
|
||||||
client, col = _get_collection("posts")
|
client, col = _get_collection("posts")
|
||||||
result = col.delete_one(query)
|
result = col.delete_one(query)
|
||||||
except PyMongoError:
|
except PyMongoError:
|
||||||
flash("Beitrag konnte nicht geloescht werden.", "error")
|
flash("Beitrag konnte nicht gelöscht werden.", "error")
|
||||||
return redirect(url_for("admin_blog"))
|
return redirect(url_for("admin_blog"))
|
||||||
finally:
|
finally:
|
||||||
if client:
|
if client:
|
||||||
@@ -742,7 +850,7 @@ def admin_blog():
|
|||||||
flash("Beitrag nicht gefunden.", "error")
|
flash("Beitrag nicht gefunden.", "error")
|
||||||
return redirect(url_for("admin_blog"))
|
return redirect(url_for("admin_blog"))
|
||||||
|
|
||||||
flash("Beitrag geloescht.", "success")
|
flash("Beitrag gelöscht.", "success")
|
||||||
return redirect(url_for("admin_blog"))
|
return redirect(url_for("admin_blog"))
|
||||||
|
|
||||||
posts = []
|
posts = []
|
||||||
@@ -783,7 +891,7 @@ def blog():
|
|||||||
def blog_post(post_id):
|
def blog_post(post_id):
|
||||||
query = _post_query_from_id(post_id)
|
query = _post_query_from_id(post_id)
|
||||||
if not query:
|
if not query:
|
||||||
flash("Ungueltige Beitrag-ID.", "error")
|
flash("Ungültige Beitrag-ID.", "error")
|
||||||
return redirect(url_for("blog"))
|
return redirect(url_for("blog"))
|
||||||
client = None
|
client = None
|
||||||
post = None
|
post = None
|
||||||
@@ -892,7 +1000,7 @@ def user_tickets():
|
|||||||
description = _sanitize_text(request.form.get("description") or "", 5000)
|
description = _sanitize_text(request.form.get("description") or "", 5000)
|
||||||
priority = _sanitize_text(request.form.get("priority") or "Normal", 30)
|
priority = _sanitize_text(request.form.get("priority") or "Normal", 30)
|
||||||
if not title or not description:
|
if not title or not description:
|
||||||
flash("Bitte Titel und Beschreibung ausfuellen.", "error")
|
flash("Bitte Titel und Beschreibung ausfüllen.", "error")
|
||||||
return redirect(url_for("user_tickets"))
|
return redirect(url_for("user_tickets"))
|
||||||
try:
|
try:
|
||||||
client, col = _get_collection("support_tickets")
|
client, col = _get_collection("support_tickets")
|
||||||
@@ -951,18 +1059,135 @@ def admin_users():
|
|||||||
flash("Admin-Rechte entfernt.", "success")
|
flash("Admin-Rechte entfernt.", "success")
|
||||||
elif action == "delete_user":
|
elif action == "delete_user":
|
||||||
if username == session.get("username"):
|
if username == session.get("username"):
|
||||||
flash("Sie koennen Ihr eigenes Konto nicht loeschen.", "error")
|
flash("Sie können Ihr eigenes Konto nicht löschen.", "error")
|
||||||
return redirect(url_for("admin_users"))
|
return redirect(url_for("admin_users"))
|
||||||
user_store.delete_user(username)
|
user_store.delete_user(username)
|
||||||
flash("Benutzer geloescht.", "success")
|
flash("Benutzer gelöscht.", "success")
|
||||||
else:
|
else:
|
||||||
flash("Ungueltige Aktion.", "error")
|
flash("Ungültige Aktion.", "error")
|
||||||
return redirect(url_for("admin_users"))
|
return redirect(url_for("admin_users"))
|
||||||
|
|
||||||
users = _list_users_for_admin()
|
users = _list_users_for_admin()
|
||||||
return render_template("admin_users.html", users=users)
|
return render_template("admin_users.html", users=users)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/admin/team', methods=['GET', 'POST'])
|
||||||
|
@admin_required
|
||||||
|
def admin_team():
|
||||||
|
if request.method == 'POST':
|
||||||
|
action = _sanitize_text(request.form.get("action") or "upsert", 20)
|
||||||
|
member_id = _sanitize_text(request.form.get("member_id") or "", 64)
|
||||||
|
|
||||||
|
client = None
|
||||||
|
try:
|
||||||
|
client, col = _get_collection("team_members")
|
||||||
|
|
||||||
|
if action == "delete":
|
||||||
|
try:
|
||||||
|
result = col.delete_one({"_id": ObjectId(member_id)})
|
||||||
|
except Exception:
|
||||||
|
flash("Ungültige Team-ID.", "error")
|
||||||
|
return redirect(url_for("admin_team"))
|
||||||
|
if result.deleted_count:
|
||||||
|
flash("Teammitglied entfernt.", "success")
|
||||||
|
else:
|
||||||
|
flash("Teammitglied nicht gefunden.", "error")
|
||||||
|
return redirect(url_for("admin_team"))
|
||||||
|
|
||||||
|
name = _sanitize_text(request.form.get("name") or "", 120)
|
||||||
|
role = _sanitize_text(request.form.get("role") or "", 120)
|
||||||
|
work = _sanitize_text(request.form.get("work") or "", 220)
|
||||||
|
bio = _sanitize_text(request.form.get("bio") or "", 500)
|
||||||
|
sort_raw = _sanitize_text(request.form.get("sort_order") or "999", 10)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sort_order = int(sort_raw)
|
||||||
|
except ValueError:
|
||||||
|
sort_order = 999
|
||||||
|
sort_order = max(1, min(sort_order, 999))
|
||||||
|
|
||||||
|
if not name or not role or not work:
|
||||||
|
flash("Bitte Name, Rolle und Arbeit ausfüllen.", "error")
|
||||||
|
return redirect(url_for("admin_team"))
|
||||||
|
|
||||||
|
current = None
|
||||||
|
if member_id:
|
||||||
|
try:
|
||||||
|
current = col.find_one({"_id": ObjectId(member_id)})
|
||||||
|
except Exception:
|
||||||
|
flash("Ungültige Team-ID.", "error")
|
||||||
|
return redirect(url_for("admin_team"))
|
||||||
|
|
||||||
|
photo_identifier = member_id or name[:20]
|
||||||
|
photo_path = _save_team_photo(request.files.get("photo"), photo_identifier)
|
||||||
|
|
||||||
|
if request.files.get("photo") and request.files.get("photo").filename and not photo_path:
|
||||||
|
flash("Foto muss JPG, JPEG, PNG oder WEBP sein.", "error")
|
||||||
|
return redirect(url_for("admin_team"))
|
||||||
|
|
||||||
|
if not photo_path:
|
||||||
|
photo_path = (current or {}).get("photo", "")
|
||||||
|
|
||||||
|
if action == "create":
|
||||||
|
if not photo_path:
|
||||||
|
flash("Bitte ein Foto für das neue Teammitglied hochladen.", "error")
|
||||||
|
return redirect(url_for("admin_team"))
|
||||||
|
col.insert_one(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"role": role,
|
||||||
|
"work": work,
|
||||||
|
"bio": bio,
|
||||||
|
"photo": photo_path,
|
||||||
|
"sort_order": sort_order,
|
||||||
|
"created_by": session.get("username") or "admin",
|
||||||
|
"created_at": _utc_now_iso(),
|
||||||
|
"updated_at": _utc_now_iso(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
flash("Teammitglied hinzugefuegt.", "success")
|
||||||
|
elif action == "update":
|
||||||
|
if not member_id:
|
||||||
|
flash("Team-ID fehlt.", "error")
|
||||||
|
return redirect(url_for("admin_team"))
|
||||||
|
try:
|
||||||
|
result = col.update_one(
|
||||||
|
{"_id": ObjectId(member_id)},
|
||||||
|
{
|
||||||
|
"$set": {
|
||||||
|
"name": name,
|
||||||
|
"role": role,
|
||||||
|
"work": work,
|
||||||
|
"bio": bio,
|
||||||
|
"photo": photo_path,
|
||||||
|
"sort_order": sort_order,
|
||||||
|
"updated_by": session.get("username") or "admin",
|
||||||
|
"updated_at": _utc_now_iso(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
flash("Ungültige Team-ID.", "error")
|
||||||
|
return redirect(url_for("admin_team"))
|
||||||
|
if result.matched_count == 0:
|
||||||
|
flash("Teammitglied nicht gefunden.", "error")
|
||||||
|
return redirect(url_for("admin_team"))
|
||||||
|
flash("Teammitglied aktualisiert.", "success")
|
||||||
|
else:
|
||||||
|
flash("Ungültige Aktion.", "error")
|
||||||
|
return redirect(url_for("admin_team"))
|
||||||
|
except PyMongoError:
|
||||||
|
flash("Teamdaten konnten nicht gespeichert werden.", "error")
|
||||||
|
finally:
|
||||||
|
if client:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
return redirect(url_for("admin_team"))
|
||||||
|
|
||||||
|
team_members = _get_team_members()
|
||||||
|
return render_template("admin_team.html", team_members=team_members)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin/chats', methods=['GET', 'POST'])
|
@app.route('/admin/chats', methods=['GET', 'POST'])
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_chats():
|
def admin_chats():
|
||||||
@@ -1114,9 +1339,9 @@ def admin_licenses():
|
|||||||
|
|
||||||
elif action == "delete" and license_id:
|
elif action == "delete" and license_id:
|
||||||
col.delete_one({"_id": ObjectId(license_id)})
|
col.delete_one({"_id": ObjectId(license_id)})
|
||||||
flash("Lizenz geloescht.", "success")
|
flash("Lizenz gelöscht.", "success")
|
||||||
else:
|
else:
|
||||||
flash("Ungueltige Aktion.", "error")
|
flash("Ungültige Aktion.", "error")
|
||||||
except Exception:
|
except Exception:
|
||||||
flash("Lizenzverwaltung fehlgeschlagen.", "error")
|
flash("Lizenzverwaltung fehlgeschlagen.", "error")
|
||||||
finally:
|
finally:
|
||||||
@@ -1214,9 +1439,9 @@ def admin_invoices():
|
|||||||
|
|
||||||
elif action == "delete" and invoice_id:
|
elif action == "delete" and invoice_id:
|
||||||
col.delete_one({"_id": ObjectId(invoice_id)})
|
col.delete_one({"_id": ObjectId(invoice_id)})
|
||||||
flash("Rechnung geloescht.", "success")
|
flash("Rechnung gelöscht.", "success")
|
||||||
else:
|
else:
|
||||||
flash("Ungueltige Aktion.", "error")
|
flash("Ungültige Aktion.", "error")
|
||||||
except Exception:
|
except Exception:
|
||||||
flash("Rechnungsverwaltung fehlgeschlagen.", "error")
|
flash("Rechnungsverwaltung fehlgeschlagen.", "error")
|
||||||
finally:
|
finally:
|
||||||
@@ -1292,7 +1517,7 @@ def allocate_key():
|
|||||||
license_key = _sanitize_text(request.form.get("license_key") or "", 160)
|
license_key = _sanitize_text(request.form.get("license_key") or "", 160)
|
||||||
|
|
||||||
if not username or not license_key:
|
if not username or not license_key:
|
||||||
flash("Bitte Benutzer und Lizenz-Key auswaehlen.", "error")
|
flash("Bitte Benutzer und Lizenz-Key auswählen.", "error")
|
||||||
return redirect(url_for('admin_license_keys'))
|
return redirect(url_for('admin_license_keys'))
|
||||||
|
|
||||||
if not _find_user(username):
|
if not _find_user(username):
|
||||||
|
|||||||
@@ -42,6 +42,28 @@
|
|||||||
box-shadow: 0 14px 32px rgba(20, 39, 55, 0.06);
|
box-shadow: 0 14px 32px rgba(20, 39, 55, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quick-admin-link {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid #d8e3ec;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f6fbff;
|
||||||
|
padding: 0.85rem 0.95rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-admin-link a {
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.42rem 0.8rem;
|
||||||
|
border: 1px solid #b8ccda;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #0b4d73;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.appointments-section h2 {
|
.appointments-section h2 {
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
color: #0f354d;
|
color: #0f354d;
|
||||||
@@ -248,6 +270,10 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="appointments-section">
|
<section class="appointments-section">
|
||||||
|
<div class="quick-admin-link">
|
||||||
|
<p><strong>Team verwalten:</strong> Bilder, Rolle und Arbeit der zwei Team-Cards zentral pflegen.</p>
|
||||||
|
<a href="{{ url_for('admin_team') }}">Team Cards öffnen</a>
|
||||||
|
</div>
|
||||||
<h2>Buchungsanfragen verwalten</h2>
|
<h2>Buchungsanfragen verwalten</h2>
|
||||||
<div class="block-days-section">
|
<div class="block-days-section">
|
||||||
<h3>Kalender-Tage sperren</h3>
|
<h3>Kalender-Tage sperren</h3>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<input type="text" name="school_name" placeholder="Schulname" required>
|
<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="license_key" placeholder="Lizenz-Key (optional, eigener Wert möglich)">
|
||||||
<input type="text" name="plan" placeholder="Plan (z. B. Standard)">
|
<input type="text" name="plan" placeholder="Plan (z. B. Standard)">
|
||||||
<input type="text" name="status" placeholder="Status (Aktiv, Pausiert)">
|
<input type="text" name="status" placeholder="Status (Aktiv, Pausiert)">
|
||||||
<input type="text" name="valid_until" placeholder="Gültig bis (YYYY-MM-DD)">
|
<input type="text" name="valid_until" placeholder="Gültig bis (YYYY-MM-DD)">
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Admin | Team Cards{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="panel">
|
||||||
|
<h1>Team Cards verwalten</h1>
|
||||||
|
<p>Beliebig viele Teammitglieder können hinzugefügt und sortiert werden. Jedes Mitglied hat Name, Rolle, Arbeit, Kurztext und ein Foto.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card add-card">
|
||||||
|
<h2>Neues Teammitglied hinzufügen</h2>
|
||||||
|
<form method="post" enctype="multipart/form-data" class="form-grid">
|
||||||
|
<input type="hidden" name="action" value="create">
|
||||||
|
|
||||||
|
<label>Name</label>
|
||||||
|
<input name="name" maxlength="120" required>
|
||||||
|
|
||||||
|
<label>Rolle</label>
|
||||||
|
<input name="role" maxlength="120" required>
|
||||||
|
|
||||||
|
<label>Arbeit</label>
|
||||||
|
<input name="work" maxlength="220" required>
|
||||||
|
|
||||||
|
<label>Kurzbeschreibung</label>
|
||||||
|
<textarea name="bio" rows="3" maxlength="500"></textarea>
|
||||||
|
|
||||||
|
<label>Reihenfolge (1-999)</label>
|
||||||
|
<input type="number" name="sort_order" min="1" max="999" value="100" required>
|
||||||
|
|
||||||
|
<label>Foto (JPG, JPEG, PNG, WEBP)</label>
|
||||||
|
<input type="file" name="photo" accept=".jpg,.jpeg,.png,.webp" required>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<button type="submit" class="btn primary">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card-grid">
|
||||||
|
{% for member in team_members %}
|
||||||
|
<article class="card">
|
||||||
|
<h2>{{ member.name }}</h2>
|
||||||
|
<form method="post" enctype="multipart/form-data" class="form-grid">
|
||||||
|
<input type="hidden" name="action" value="update">
|
||||||
|
<input type="hidden" name="member_id" value="{{ member.id }}">
|
||||||
|
|
||||||
|
<label>Name</label>
|
||||||
|
<input name="name" value="{{ member.name }}" maxlength="120" required>
|
||||||
|
|
||||||
|
<label>Rolle</label>
|
||||||
|
<input name="role" value="{{ member.role }}" maxlength="120" required>
|
||||||
|
|
||||||
|
<label>Arbeit</label>
|
||||||
|
<input name="work" value="{{ member.work }}" maxlength="220" required>
|
||||||
|
|
||||||
|
<label>Kurzbeschreibung</label>
|
||||||
|
<textarea name="bio" rows="3" maxlength="500">{{ member.bio }}</textarea>
|
||||||
|
|
||||||
|
<label>Reihenfolge (1-999)</label>
|
||||||
|
<input type="number" name="sort_order" min="1" max="999" value="{{ member.sort_order or 100 }}" required>
|
||||||
|
|
||||||
|
<label>Foto (JPG, JPEG, PNG, WEBP)</label>
|
||||||
|
<input type="file" name="photo" accept=".jpg,.jpeg,.png,.webp">
|
||||||
|
|
||||||
|
{% if member.photo %}
|
||||||
|
<img class="preview" src="{{ url_for('static', filename=member.photo) }}" alt="Teamfoto {{ member.name }}">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<button type="submit" class="btn primary">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="post" class="delete-form" onsubmit="return confirm('Mitglied wirklich entfernen?');">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="member_id" value="{{ member.id }}">
|
||||||
|
<button type="submit" class="btn danger">Entfernen</button>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
{% else %}
|
||||||
|
<article class="card">
|
||||||
|
<h2>Noch keine Teammitglieder</h2>
|
||||||
|
<p>Über das Formular oben kann jetzt das erste Teammitglied erstellt werden.</p>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.panel {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d8e1e8;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 10px 24px rgba(20, 40, 55, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-card {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
margin: 0 0 0.7rem;
|
||||||
|
color: #113d59;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 0.86rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #4b677c;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
border: 1px solid #bdd0dd;
|
||||||
|
border-radius: 9px;
|
||||||
|
padding: 0.5rem 0.6rem;
|
||||||
|
font: inherit;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid #d0d9de;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.5rem 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary {
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(120deg, #0b5b89 0%, #0b4262 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.danger {
|
||||||
|
color: #842f2f;
|
||||||
|
background: #fff4f4;
|
||||||
|
border: 1px solid #e7b9b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-form {
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
<section class="packages-hero">
|
<section class="packages-hero">
|
||||||
<h1>Buchungsoptionen als Softwarepakete</h1>
|
<h1>Buchungsoptionen als Softwarepakete</h1>
|
||||||
<p>Alle Pakete sind frei einsehbar, auch ohne Admin. Die Testversion kann nur mit aktivem Konto gestartet werden.</p>
|
<p>Alle Pakete sind frei einsehbar, auch ohne Admin. Die Testversion kann nur mit aktivem Konto gestartet werden.</p>
|
||||||
<p>Jedes Paket basiert auf derselben Sicherheitsarchitektur mit Rollenrechten, geschuetzten Sessions und nachvollziehbaren Aktionen.</p>
|
<p>Jedes Paket basiert auf derselben Sicherheitsarchitektur mit Rollenrechten, geschützten Sessions und nachvollziehbaren Aktionen.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="packages-grid">
|
<section class="packages-grid">
|
||||||
@@ -119,12 +119,23 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="package-actions">
|
<div class="package-actions">
|
||||||
{% if 'username' in session %}
|
{% if 'username' in session %}
|
||||||
<form method="POST" action="{{ url_for('start_test_package') }}">
|
{% if package.slug == 'test' %}
|
||||||
<input type="hidden" name="package" value="{{ package.slug }}">
|
<form method="POST" action="{{ url_for('start_test_package') }}">
|
||||||
<button class="btn" type="submit">Testversion starten</button>
|
<input type="hidden" name="package" value="{{ package.slug }}">
|
||||||
</form>
|
<button class="btn" type="submit">Testversion starten</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form method="POST" action="{{ url_for('book_option_package') }}">
|
||||||
|
<input type="hidden" name="package" value="{{ package.slug }}">
|
||||||
|
<button class="btn" type="submit">Option buchen</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="btn secondary" href="{{ url_for('login') }}">Mit Konto starten</a>
|
{% if package.slug == 'test' %}
|
||||||
|
<a class="btn secondary" href="{{ url_for('login') }}">Mit Konto testen</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn secondary" href="{{ url_for('login') }}">Mit Konto buchen</a>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@@ -134,9 +145,9 @@
|
|||||||
<section class="notice">
|
<section class="notice">
|
||||||
{% if 'username' in session %}
|
{% if 'username' in session %}
|
||||||
{% if active_test_license %}
|
{% if active_test_license %}
|
||||||
<p><strong>Aktive Testversion:</strong> {{ active_test_license.plan }} | Key: {{ active_test_license.license_key }} | Gueltig bis: {{ active_test_license.valid_until }}</p>
|
<p><strong>Aktive Testversion:</strong> {{ active_test_license.plan }} | Key: {{ active_test_license.license_key }} | Gültig bis: {{ active_test_license.valid_until }}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p><strong>Hinweis:</strong> Fuer Ihr Konto ist noch keine Testversion aktiv. Starten Sie oben eine Testversion fuer Normal, Pro oder Buecherei.</p>
|
<p><strong>Hinweis:</strong> Für Ihr Konto ist noch keine Testversion aktiv. Starten Sie oben die Testversion beim Paket Testversion.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p><strong>Hinweis:</strong> Bitte einloggen oder registrieren, um die Testversion zu starten.</p>
|
<p><strong>Hinweis:</strong> Bitte einloggen oder registrieren, um die Testversion zu starten.</p>
|
||||||
|
|||||||
@@ -116,6 +116,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.62rem;
|
||||||
font-family: "Merriweather", Georgia, serif;
|
font-family: "Merriweather", Georgia, serif;
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -123,6 +126,16 @@
|
|||||||
color: var(--brand-strong);
|
color: var(--brand-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brand img {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 10px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid #bccdc4;
|
||||||
|
box-shadow: 0 8px 16px rgba(19, 49, 54, 0.14);
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-links {
|
.nav-links {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -352,12 +365,14 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
<div class="site-shell site-nav-inner">
|
<div class="site-shell site-nav-inner">
|
||||||
<a class="brand" href="{{ url_for('default') }}">Invario Schule</a>
|
<a class="brand" href="{{ url_for('default') }}">
|
||||||
|
<img src="{{ url_for('static', filename='images/logo-1.jpeg') }}" alt="Invario Logo">
|
||||||
|
<span>Invario Schule</span>
|
||||||
|
</a>
|
||||||
<div class="nav-links" aria-label="Main Navigation">
|
<div class="nav-links" aria-label="Main Navigation">
|
||||||
<a href="{{ url_for('dienstleistungen') }}">Leistungen für Schulen</a>
|
<a href="{{ url_for('dienstleistungen') }}">Leistungen für Schulen</a>
|
||||||
<a href="{{ url_for('projekte') }}">Praxisbeispiele</a>
|
<a href="{{ url_for('projekte') }}">Praxisbeispiele</a>
|
||||||
<a href="{{ url_for('tutorial') }}">Tutorial</a>
|
<a href="{{ url_for('team') }}">Team</a>
|
||||||
<a href="{{ url_for('team') }}">Unser Ansatz</a>
|
|
||||||
<a href="{{ url_for('kontakt') }}">Kontakt</a>
|
<a href="{{ url_for('kontakt') }}">Kontakt</a>
|
||||||
<a href="{{ url_for('inventarsystem') }}">Inventarsystem</a>
|
<a href="{{ url_for('inventarsystem') }}">Inventarsystem</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -387,6 +402,7 @@
|
|||||||
<a href="{{ url_for('admin_chats') }}">Admin-Chat</a>
|
<a href="{{ url_for('admin_chats') }}">Admin-Chat</a>
|
||||||
<a href="{{ url_for('admin_tickets') }}">Support-Center</a>
|
<a href="{{ url_for('admin_tickets') }}">Support-Center</a>
|
||||||
<a href="{{ url_for('admin_blog') }}">Blog verwalten</a>
|
<a href="{{ url_for('admin_blog') }}">Blog verwalten</a>
|
||||||
|
<a href="{{ url_for('admin_team') }}">Team Cards</a>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -424,7 +440,7 @@
|
|||||||
<ul class="footer-links">
|
<ul class="footer-links">
|
||||||
<li><a href="{{ url_for('default') }}">Start</a></li>
|
<li><a href="{{ url_for('default') }}">Start</a></li>
|
||||||
<li><a href="{{ url_for('dienstleistungen') }}">Leistungsübersicht</a></li>
|
<li><a href="{{ url_for('dienstleistungen') }}">Leistungsübersicht</a></li>
|
||||||
<li><a href="{{ url_for('tutorial') }}">Tutorial</a></li>
|
<li><a href="{{ url_for('team') }}">Team</a></li>
|
||||||
<li><a href="{{ url_for('blog') }}">Neuigkeiten</a></li>
|
<li><a href="{{ url_for('blog') }}">Neuigkeiten</a></li>
|
||||||
{% if 'username' in session %}
|
{% if 'username' in session %}
|
||||||
<li><a href="{{ url_for('appointments') }}">Buchungsoption</a></li>
|
<li><a href="{{ url_for('appointments') }}">Buchungsoption</a></li>
|
||||||
|
|||||||
@@ -183,7 +183,7 @@
|
|||||||
<form method="POST" action="{{ url_for('allocate_key') }}" class="key-form">
|
<form method="POST" action="{{ url_for('allocate_key') }}" class="key-form">
|
||||||
<label for="alloc_user">Benutzer</label>
|
<label for="alloc_user">Benutzer</label>
|
||||||
<select id="alloc_user" name="username" required>
|
<select id="alloc_user" name="username" required>
|
||||||
<option value="">Benutzer auswaehlen</option>
|
<option value="">Benutzer auswählen</option>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<option value="{{ user.username }}">{{ user.username }} ({{ user.display_name }})</option>
|
<option value="{{ user.username }}">{{ user.username }} ({{ user.display_name }})</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -191,7 +191,7 @@
|
|||||||
|
|
||||||
<label for="alloc_key">Lizenz-Key</label>
|
<label for="alloc_key">Lizenz-Key</label>
|
||||||
<select id="alloc_key" name="license_key" required>
|
<select id="alloc_key" name="license_key" required>
|
||||||
<option value="">Key auswaehlen</option>
|
<option value="">Key auswählen</option>
|
||||||
{% for item in keys %}
|
{% for item in keys %}
|
||||||
<option value="{{ item.license_key }}">{{ item.user_id or '----' }} | {{ item.license_key[:22] }}{% if item.license_key|length > 22 %}...{% endif %}</option>
|
<option value="{{ item.license_key }}">{{ item.user_id or '----' }} | {{ item.license_key[:22] }}{% if item.license_key|length > 22 %}...{% endif %}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if item.user_id %}
|
{% if item.user_id %}
|
||||||
<form method="POST" action="{{ url_for('remove_key', user_id=item.user_id) }}" class="remove-form" onsubmit="return confirm('Key fuer User {{ item.user_id }} wirklich loeschen?');">
|
<form method="POST" action="{{ url_for('remove_key', user_id=item.user_id) }}" class="remove-form" onsubmit="return confirm('Key für User {{ item.user_id }} wirklich löschen?');">
|
||||||
<button type="submit" class="btn-danger">Entfernen</button>
|
<button type="submit" class="btn-danger">Entfernen</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
+12
-89
@@ -102,21 +102,6 @@
|
|||||||
border: 1px solid #3a6970;
|
border: 1px solid #3a6970;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-logo-strip {
|
|
||||||
margin-bottom: 0.7rem;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-logo-strip img {
|
|
||||||
width: 100%;
|
|
||||||
height: 64px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #4f7c82;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-card h3 {
|
.hero-card h3 {
|
||||||
font-size: clamp(1rem, 1.8vw, 1.35rem);
|
font-size: clamp(1rem, 1.8vw, 1.35rem);
|
||||||
@@ -284,40 +269,6 @@
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-wall {
|
|
||||||
margin-top: 2rem;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 18px;
|
|
||||||
border: 1px solid #d4ddd4;
|
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 14px 30px rgba(28, 45, 41, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-wall h2 {
|
|
||||||
color: #21444a;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-grid {
|
|
||||||
margin-top: 1rem;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-item {
|
|
||||||
border: 1px solid #d8e0d8;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #f8faf7;
|
|
||||||
padding: 0.65rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-item img {
|
|
||||||
width: 100%;
|
|
||||||
height: 160px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rise {
|
@keyframes rise {
|
||||||
from {
|
from {
|
||||||
@@ -365,13 +316,6 @@
|
|||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-logo-strip {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -382,7 +326,7 @@
|
|||||||
<span class="eyebrow">Lehrmittelverwaltungssoftware für Schulen</span>
|
<span class="eyebrow">Lehrmittelverwaltungssoftware für Schulen</span>
|
||||||
<h1>Lehrmittel, Schul-IT und Ausleihen in einem sicheren System.</h1>
|
<h1>Lehrmittel, Schul-IT und Ausleihen in einem sicheren System.</h1>
|
||||||
<p>
|
<p>
|
||||||
Invario ist eine Lehrmittelverwaltungssoftware, die Schulbibliothek, Inventar und digitale Ausleihe zusammenfuehrt. Prozesse bleiben bewusst schlank und schnell, waehrend Sicherheitsfunktionen wie rollenbasierte Rechte, Sitzungsabsicherung und nachvollziehbare Protokolle im Hintergrund dauerhaft aktiv sind.
|
Invario ist eine Lehrmittelverwaltungssoftware, die Schulbibliothek, Inventar und digitale Ausleihe zusammenführt. Prozesse bleiben bewusst schlank und schnell, während Sicherheitsfunktionen wie rollenbasierte Rechte, Sitzungsabsicherung und nachvollziehbare Protokolle im Hintergrund dauerhaft aktiv sind.
|
||||||
</p>
|
</p>
|
||||||
<div class="cta-row">
|
<div class="cta-row">
|
||||||
{% if 'username' in session %}
|
{% if 'username' in session %}
|
||||||
@@ -391,16 +335,11 @@
|
|||||||
<a class="btn btn-primary" href="{{ url_for('login') }}">Buchungsoption nutzen</a>
|
<a class="btn btn-primary" href="{{ url_for('login') }}">Buchungsoption nutzen</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="btn btn-secondary" href="{{ url_for('dienstleistungen') }}">Leistungen ansehen</a>
|
<a class="btn btn-secondary" href="{{ url_for('dienstleistungen') }}">Leistungen ansehen</a>
|
||||||
<a class="btn btn-secondary" href="{{ url_for('tutorial') }}">Tutorial ansehen</a>
|
<a class="btn btn-secondary" href="{{ url_for('team') }}">Team ansehen</a>
|
||||||
<a class="btn btn-secondary" href="{{ url_for('kontakt') }}">Direkt kontaktieren</a>
|
<a class="btn btn-secondary" href="{{ url_for('kontakt') }}">Direkt kontaktieren</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<aside class="hero-card" aria-label="Kurzfakten für Schulen">
|
<aside class="hero-card" aria-label="Kurzfakten für Schulen">
|
||||||
<div class="hero-logo-strip" aria-label="Logo Varianten">
|
|
||||||
<img src="{{ url_for('static', filename='images/logo-1.jpeg', v='20260330') }}" alt="Invario Logo 1">
|
|
||||||
<img src="{{ url_for('static', filename='images/logo-2.jpeg', v='20260330') }}" alt="Invario Logo 2">
|
|
||||||
<img src="{{ url_for('static', filename='images/logo-3.jpeg', v='20260330') }}" alt="Invario Logo 3">
|
|
||||||
</div>
|
|
||||||
<h3>Was Schulen besonders schätzen</h3>
|
<h3>Was Schulen besonders schätzen</h3>
|
||||||
<p>Klare Abläufe, feste Ansprechpartner und verständliche Oberflächen für den täglichen Einsatz.</p>
|
<p>Klare Abläufe, feste Ansprechpartner und verständliche Oberflächen für den täglichen Einsatz.</p>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
@@ -412,16 +351,16 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section" id="nutzen" style="animation-delay: 100ms;">
|
<section class="section" id="nutzen" style="animation-delay: 100ms;">
|
||||||
<h2>Vorteile fuer Verwaltung und Bibliothek</h2>
|
<h2>Vorteile für Verwaltung und Bibliothek</h2>
|
||||||
<p>Alle Daten zu Lehrmitteln, Geraeten und Ausleihen sind gebuendelt, sodass Rückfragen schnell geklaert werden koennen.</p>
|
<p>Alle Daten zu Lehrmitteln, Geraeten und Ausleihen sind gebündelt, sodass Rückfragen schnell geklärt werden können.</p>
|
||||||
<div class="section-grid">
|
<div class="section-grid">
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<h3>Lehrmittel pro Klasse steuern</h3>
|
<h3>Lehrmittel pro Klasse steuern</h3>
|
||||||
<p>Buecher und Unterrichtsmaterialien lassen sich klassenweise ausgeben und zum Schuljahresende sauber rueckfuehren.</p>
|
<p>Bücher und Unterrichtsmaterialien lassen sich klassenweise ausgeben und zum Schuljahresende sauber rückführen.</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<h3>Scanner-gestuetzte Ausleihe</h3>
|
<h3>Scanner-gestuetzte Ausleihe</h3>
|
||||||
<p>Barcode- oder QR-Workflows beschleunigen Ausgabe und Ruecknahme deutlich, auch bei grossen Bestandsmengen.</p>
|
<p>Barcode- oder QR-Workflows beschleunigen Ausgabe und Rücknahme deutlich, auch bei grossen Bestandsmengen.</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<h3>IT- und Medienbestand kombiniert</h3>
|
<h3>IT- und Medienbestand kombiniert</h3>
|
||||||
@@ -443,8 +382,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="routine">
|
<div class="routine">
|
||||||
<div>
|
<div>
|
||||||
<b>Rueckfrage zu ausgeliehenen Medien</b>
|
<b>Rückfrage zu ausgeliehenen Medien</b>
|
||||||
Verfuegbarkeit, Entleiher und Rueckgabedatum sind sofort sichtbar, ohne manuelle Listenpflege.
|
Verfügbarkeit, Entleiher und Rückgabedatum sind sofort sichtbar, ohne manuelle Listenpflege.
|
||||||
</div>
|
</div>
|
||||||
<span>Ausleihe</span>
|
<span>Ausleihe</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -459,8 +398,8 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section" id="sicherheit" style="animation-delay: 220ms;">
|
<section class="section" id="sicherheit" style="animation-delay: 220ms;">
|
||||||
<h2>Erweiterte Sicherheit fuer den Schulbetrieb</h2>
|
<h2>Erweiterte Sicherheit für den Schulbetrieb</h2>
|
||||||
<p>Die Plattform setzt auf Datenschutz und Betriebssicherheit, damit personenbezogene Daten und Inventardaten langfristig geschuetzt bleiben.</p>
|
<p>Die Plattform setzt auf Datenschutz und Betriebssicherheit, damit personenbezogene Daten und Inventardaten langfristig geschützt bleiben.</p>
|
||||||
<div class="section-grid">
|
<div class="section-grid">
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<h3>Rollen- und Bereichstrennung</h3>
|
<h3>Rollen- und Bereichstrennung</h3>
|
||||||
@@ -468,31 +407,15 @@
|
|||||||
</article>
|
</article>
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<h3>Protokollierung und Nachweis</h3>
|
<h3>Protokollierung und Nachweis</h3>
|
||||||
<p>Ausgaben, Ruecknahmen und kritische Admin-Aktionen werden dokumentiert und sind jederzeit auditierbar.</p>
|
<p>Ausgaben, Rücknahmen und kritische Admin-Aktionen werden dokumentiert und sind jederzeit auditierbar.</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<h3>Gesicherte Web-Session</h3>
|
<h3>Gesicherte Web-Session</h3>
|
||||||
<p>Sicherheits-Header, HttpOnly-Cookies und klare Session-Richtlinien schuetzen den laufenden Betrieb vor typischen Webangriffen.</p>
|
<p>Sicherheits-Header, HttpOnly-Cookies und klare Session-Richtlinien schützen den laufenden Betrieb vor typischen Webangriffen.</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="logo-wall" id="logos">
|
|
||||||
<h2>Einblicke und Logo-Varianten</h2>
|
|
||||||
<p>Die folgenden Grafiken sind direkt aus dem statischen Bereich Ihrer Website eingebunden.</p>
|
|
||||||
<div class="logo-grid">
|
|
||||||
<figure class="logo-item">
|
|
||||||
<img src="{{ url_for('static', filename='images/logo-1.jpeg') }}" alt="Invario Logo Variante 1">
|
|
||||||
</figure>
|
|
||||||
<figure class="logo-item">
|
|
||||||
<img src="{{ url_for('static', filename='images/logo-2.jpeg') }}" alt="Invario Logo Variante 2">
|
|
||||||
</figure>
|
|
||||||
<figure class="logo-item">
|
|
||||||
<img src="{{ url_for('static', filename='images/logo-3.jpeg') }}" alt="Invario Logo Variante 3">
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section" id="ablauf" style="animation-delay: 260ms;">
|
<section class="section" id="ablauf" style="animation-delay: 260ms;">
|
||||||
<h2>So verläuft die Zusammenarbeit</h2>
|
<h2>So verläuft die Zusammenarbeit</h2>
|
||||||
<p>Ein klarer Prozess sorgt für Sicherheit, besonders bei begrenzten Zeitfenstern im Schulbetrieb.</p>
|
<p>Ein klarer Prozess sorgt für Sicherheit, besonders bei begrenzten Zeitfenstern im Schulbetrieb.</p>
|
||||||
|
|||||||
+109
-33
@@ -1,47 +1,123 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Unser Ansatz | Invario{% endblock %}
|
{% block title %}Team | Invario{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="page-hero">
|
<section class="page-hero">
|
||||||
<h1>Unser Ansatz für Schulen</h1>
|
<h1>Unser Team für Schulen</h1>
|
||||||
<p>Die Arbeitsweise ist strukturiert, persönlich und auf den praktischen Nutzen in der täglichen Verwaltung ausgerichtet.</p>
|
<p>Unsere Ansprechpartner mit klaren Verantwortungen. Inhalte und Fotos werden direkt im Admin-Bereich gepflegt.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="grid">
|
<section class="team-grid" aria-label="Team Cards">
|
||||||
<article class="member">
|
{% for member in team_members %}
|
||||||
<h2>Verständliche Beratung</h2>
|
<article class="member-card">
|
||||||
<p>Technische Möglichkeiten werden in klare Entscheidungen übersetzt: Was bringt im Schulalltag wirklich Entlastung und was nicht.</p>
|
{% if member and member.photo %}
|
||||||
<p><strong>Ergebnis:</strong> Keine unnötigen Funktionen, sondern ein klarer Fahrplan mit nachvollziehbaren Prioritäten.</p>
|
<img class="member-photo" src="{{ url_for('static', filename=member.photo) }}" alt="Teamfoto {{ member.name }}">
|
||||||
</article>
|
{% endif %}
|
||||||
<article class="member">
|
|
||||||
<h2>Saubere Einführung</h2>
|
<div class="member-content">
|
||||||
<p>Das System wird gemeinsam mit den verantwortlichen Teams eingerichtet, ohne den laufenden Betrieb zu überlasten.</p>
|
<p class="member-slot">Teammitglied</p>
|
||||||
<p><strong>Ergebnis:</strong> Schrittweise Umstellung mit Schulung direkt an typischen Arbeitsabläufen.</p>
|
<h2>{{ member.name }}</h2>
|
||||||
</article>
|
<p class="member-role">{{ member.role }}</p>
|
||||||
<article class="member">
|
<p><strong>Arbeit:</strong> {{ member.work }}</p>
|
||||||
<h2>Zuverlässige Begleitung</h2>
|
{% if member.bio %}
|
||||||
<p>Nach der Einführung bleibt das Team erreichbar und unterstützt bei Anpassungen, Fragen und neuen Anforderungen.</p>
|
<p>{{ member.bio }}</p>
|
||||||
<p><strong>Ergebnis:</strong> Feste Ansprechpartner und verlässliche Rückmeldung statt wechselnder Kontaktwege.</p>
|
{% endif %}
|
||||||
</article>
|
</div>
|
||||||
|
</article>
|
||||||
|
{% else %}
|
||||||
|
<article class="member-card">
|
||||||
|
<div class="member-content">
|
||||||
|
<p class="member-slot">Team</p>
|
||||||
|
<h2>Noch keine Einträge vorhanden</h2>
|
||||||
|
<p class="member-role">Der Admin kann Teammitglieder im Bereich „Admin > Team Cards“ hinzufügen.</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="references">
|
<section class="values">
|
||||||
<h2>Werte, nach denen wir arbeiten</h2>
|
<h2>Arbeitsprinzipien</h2>
|
||||||
<ul>
|
<p>Direkte Kommunikation, saubere Einführung und verbindliche Betreuung im laufenden Betrieb.</p>
|
||||||
<li>Klarheit vor Komplexität</li>
|
|
||||||
<li>Datenschutz als Standard, nicht als Zusatz</li>
|
|
||||||
<li>Respekt für die Arbeitsrealität in Schulen</li>
|
|
||||||
<li>Verbindliche Kommunikation und transparente Schritte</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.page-hero { margin-bottom: 1.2rem; }
|
.page-hero {
|
||||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 0.9rem; }
|
margin-bottom: 1.2rem;
|
||||||
.member, .references { background: #fff; border: 1px solid #d8dfd7; border-radius: 14px; padding: 1rem; box-shadow: 0 10px 24px rgba(25, 45, 41, 0.04); }
|
}
|
||||||
.member h2, .references h2 { color: #265057; margin-bottom: 0.45rem; font-size: clamp(1.05rem, 1.8vw, 1.35rem); }
|
|
||||||
.references { margin-top: 0.9rem; }
|
.team-grid {
|
||||||
.references ul { margin: 0; padding-left: 1rem; color: #4a6267; display: grid; gap: 0.5rem; }
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #d8dfd7;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 12px 24px rgba(25, 45, 41, 0.06);
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-photo {
|
||||||
|
width: 100%;
|
||||||
|
height: 220px;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #eef2ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-content {
|
||||||
|
padding: 1rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-slot {
|
||||||
|
margin: 0;
|
||||||
|
color: #5f7479;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-content h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #264f56;
|
||||||
|
font-size: clamp(1.12rem, 1.8vw, 1.42rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-role {
|
||||||
|
margin: 0;
|
||||||
|
color: #3b6168;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.values {
|
||||||
|
margin-top: 1rem;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #d8dfd7;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.values h2 {
|
||||||
|
color: #265057;
|
||||||
|
margin-bottom: 0.45rem;
|
||||||
|
font-size: clamp(1.05rem, 1.8vw, 1.35rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.team-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-photo {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Tutorial | Invario{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<section class="page-hero">
|
|
||||||
<h1>Tutorial: Sicherer Start mit Invario</h1>
|
|
||||||
<p>Diese Schritt-für-Schritt-Anleitung zeigt den Einstieg in Inventar, Lizenzen und Supportprozesse mit besonderem Fokus auf Sicherheit im Alltag.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="grid">
|
|
||||||
<article class="card">
|
|
||||||
<h2>1. Nutzerkonten und Rollen anlegen</h2>
|
|
||||||
<p>Zuerst werden Rollen für Verwaltung und Technik eingerichtet. Jede Rolle erhält nur die benötigten Rechte, um Risiken durch Überberechtigungen zu vermeiden.</p>
|
|
||||||
</article>
|
|
||||||
<article class="card">
|
|
||||||
<h2>2. Inventardaten strukturiert übernehmen</h2>
|
|
||||||
<p>Geräte, Räume und Zuständigkeiten werden einheitlich erfasst. Klare Datenstrukturen verbessern die Nachvollziehbarkeit und unterstützen Prüfungen.</p>
|
|
||||||
</article>
|
|
||||||
<article class="card">
|
|
||||||
<h2>3. Lizenzfristen aktiv überwachen</h2>
|
|
||||||
<p>Lizenzlaufzeiten und Erinnerungen werden zentral gepflegt. So lassen sich Ausfälle vermeiden und Fristen rechtzeitig bearbeiten.</p>
|
|
||||||
</article>
|
|
||||||
<article class="card">
|
|
||||||
<h2>4. Supportabläufe definieren</h2>
|
|
||||||
<p>Tickets erhalten klare Prioritäten und feste Statuswerte. Das schafft Transparenz und beschleunigt die Bearbeitung im Tagesbetrieb.</p>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="security-box">
|
|
||||||
<h2>Sicherheitsleitfaden für den Betrieb</h2>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Zugriffsschutz:</strong> Rollenbasiertes Arbeiten mit getrennten Zuständigkeiten.</li>
|
|
||||||
<li><strong>Sitzungssicherheit:</strong> Session-Cookies mit sicheren Standardeinstellungen.</li>
|
|
||||||
<li><strong>Übertragung:</strong> Einsatz sicherer Protokolle und Schutz-Header für Webanfragen.</li>
|
|
||||||
<li><strong>Nachvollziehbarkeit:</strong> Protokollierung relevanter Änderungen und Aktionen.</li>
|
|
||||||
<li><strong>Datenschutz:</strong> Datenminimierung und dokumentierte Verarbeitungsprozesse.</li>
|
|
||||||
<li><strong>Kontinuität:</strong> Regelmäßige Backups und Wiederanlaufplanung.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="next-step">
|
|
||||||
<h2>Nächster Schritt</h2>
|
|
||||||
<p>Für die Einführung empfiehlt sich ein kurzer Pilotbetrieb mit realen Beispieldaten, bevor die vollständige Übernahme in den Produktivbetrieb erfolgt.</p>
|
|
||||||
{% if 'username' in session %}
|
|
||||||
<a class="btn" href="{{ url_for('appointments') }}">Pilotstart planen</a>
|
|
||||||
{% else %}
|
|
||||||
<a class="btn" href="{{ url_for('login') }}">Login für Buchung</a>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.page-hero { margin-bottom: 1.2rem; }
|
|
||||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 0.9rem; }
|
|
||||||
.card { background: #fff; border: 1px solid #d8dfd7; border-radius: 14px; padding: 1rem; box-shadow: 0 10px 24px rgba(25, 45, 41, 0.04); }
|
|
||||||
.card h2 { color: #265057; margin-bottom: 0.45rem; font-size: clamp(1.05rem, 1.8vw, 1.35rem); }
|
|
||||||
.security-box { margin-top: 0.9rem; background: linear-gradient(135deg, #f8f4eb 0%, #eef3eb 100%); border: 1px solid #d4dbd2; border-radius: 14px; padding: 1.1rem; }
|
|
||||||
.security-box h2 { color: #2a4d53; margin-bottom: 0.45rem; font-size: clamp(1.1rem, 2.2vw, 1.55rem); }
|
|
||||||
.security-box ul { margin: 0; padding-left: 1.1rem; color: #4a6267; display: grid; gap: 0.5rem; }
|
|
||||||
.next-step { margin-top: 0.9rem; background: #fff; border: 1px solid #d8dfd7; border-radius: 14px; padding: 1rem; box-shadow: 0 10px 24px rgba(25, 45, 41, 0.04); }
|
|
||||||
.next-step h2 { color: #265057; margin-bottom: 0.45rem; font-size: clamp(1.05rem, 1.8vw, 1.35rem); }
|
|
||||||
.btn { margin-top: 0.5rem; display: inline-flex; align-items: center; justify-content: center; border-radius: 999px; border: 1px solid #1d5a63; background: linear-gradient(120deg, #296f78 0%, #1a5057 100%); color: #fff; padding: 0.54rem 0.92rem; font-weight: 700; }
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
Reference in New Issue
Block a user