Refactor code structure for improved readability and maintainability
This commit is contained in:
+97
-116
@@ -114,7 +114,7 @@ def _sanitize_text(text: str, max_length: int = 255) -> str:
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def _activate_test_license_for_user(username: str, school_name: str) -> tuple[bool, str]:
|
def _activate_test_license_for_user(username: str, school_name: str, package_name: str = "Normal") -> tuple[bool, str]:
|
||||||
"""Create a one-time test license for the user if none exists yet."""
|
"""Create a one-time test license for the user if none exists yet."""
|
||||||
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)
|
||||||
@@ -125,11 +125,15 @@ def _activate_test_license_for_user(username: str, school_name: str) -> tuple[bo
|
|||||||
try:
|
try:
|
||||||
client, col = _get_collection("licenses")
|
client, col = _get_collection("licenses")
|
||||||
|
|
||||||
|
normalized_package = _sanitize_text(package_name or "Normal", 40)
|
||||||
|
if normalized_package not in {"Normal", "Pro", "Buecherei", "Bücherei"}:
|
||||||
|
normalized_package = "Normal"
|
||||||
|
|
||||||
existing = col.find_one(
|
existing = col.find_one(
|
||||||
{
|
{
|
||||||
"username": user_name,
|
"username": user_name,
|
||||||
"plan": "Test",
|
|
||||||
"status": {"$in": ["Aktiv", "Pausiert"]},
|
"status": {"$in": ["Aktiv", "Pausiert"]},
|
||||||
|
"plan": {"$regex": "^Test"},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if existing:
|
if existing:
|
||||||
@@ -141,7 +145,7 @@ def _activate_test_license_for_user(username: str, school_name: str) -> tuple[bo
|
|||||||
"username": user_name,
|
"username": user_name,
|
||||||
"school_name": school or user_name,
|
"school_name": school or user_name,
|
||||||
"license_key": test_key,
|
"license_key": test_key,
|
||||||
"plan": "Test",
|
"plan": f"Test {normalized_package}",
|
||||||
"status": "Aktiv",
|
"status": "Aktiv",
|
||||||
"valid_until": (datetime.utcnow() + timedelta(days=30)).date().isoformat(),
|
"valid_until": (datetime.utcnow() + timedelta(days=30)).date().isoformat(),
|
||||||
"hwid_uuid": "",
|
"hwid_uuid": "",
|
||||||
@@ -245,6 +249,19 @@ def _validate_username(username: str) -> bool:
|
|||||||
return all(c.isalnum() or c in "_-" for c in username)
|
return all(c.isalnum() or c in "_-" for c in username)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_email(email: str) -> bool:
|
||||||
|
"""Simple email validation for registration input."""
|
||||||
|
value = (email or "").strip()
|
||||||
|
if len(value) < 5 or len(value) > 254:
|
||||||
|
return False
|
||||||
|
if "@" not in value or value.count("@") != 1:
|
||||||
|
return False
|
||||||
|
local, domain = value.split("@", 1)
|
||||||
|
if not local or not domain or "." not in domain:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _find_user(username: str):
|
def _find_user(username: str):
|
||||||
username_key = (username or "").strip()
|
username_key = (username or "").strip()
|
||||||
if not username_key:
|
if not username_key:
|
||||||
@@ -383,11 +400,12 @@ def register():
|
|||||||
username = (request.form.get("username") or "").strip()
|
username = (request.form.get("username") or "").strip()
|
||||||
school_name = (request.form.get("school_name") or "").strip()
|
school_name = (request.form.get("school_name") or "").strip()
|
||||||
contact_person = (request.form.get("contact_person") or "").strip()
|
contact_person = (request.form.get("contact_person") or "").strip()
|
||||||
|
email = (request.form.get("email") or "").strip().lower()
|
||||||
activate_test_key = (request.form.get("activate_test_key") or "").strip().lower() in {"1", "on", "true", "yes"}
|
activate_test_key = (request.form.get("activate_test_key") or "").strip().lower() in {"1", "on", "true", "yes"}
|
||||||
password = request.form.get("password") or ""
|
password = request.form.get("password") or ""
|
||||||
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 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 ausfuellen.", "error")
|
||||||
return redirect(url_for("register"))
|
return redirect(url_for("register"))
|
||||||
|
|
||||||
@@ -407,13 +425,18 @@ def register():
|
|||||||
flash("Benutzername bereits vergeben.", "error")
|
flash("Benutzername bereits vergeben.", "error")
|
||||||
return redirect(url_for("register"))
|
return redirect(url_for("register"))
|
||||||
|
|
||||||
|
if not _validate_email(email):
|
||||||
|
flash("Bitte eine gueltige E-Mail-Adresse angeben.", "error")
|
||||||
|
return redirect(url_for("register"))
|
||||||
|
|
||||||
school_name = _sanitize_text(school_name, 120)
|
school_name = _sanitize_text(school_name, 120)
|
||||||
contact_person = _sanitize_text(contact_person, 120)
|
contact_person = _sanitize_text(contact_person, 120)
|
||||||
|
email = _sanitize_text(email, 254)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
existing_users = user_store.get_all_users() or []
|
existing_users = user_store.get_all_users() or []
|
||||||
is_first_user = len(existing_users) == 0
|
is_first_user = len(existing_users) == 0
|
||||||
if not user_store.add_user(username, password, school_name, contact_person):
|
if not user_store.add_user(username, password, school_name, contact_person, email):
|
||||||
flash("Benutzer konnte nicht erstellt werden.", "error")
|
flash("Benutzer konnte nicht erstellt werden.", "error")
|
||||||
return redirect(url_for("register"))
|
return redirect(url_for("register"))
|
||||||
|
|
||||||
@@ -436,134 +459,92 @@ def register():
|
|||||||
return render_template("register.html")
|
return render_template("register.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/appointments', methods=['GET', 'POST'])
|
@app.route('/appointments', methods=['GET'])
|
||||||
@login_required
|
|
||||||
def appointments():
|
def appointments():
|
||||||
today = date.today()
|
software_packages = [
|
||||||
month = request.args.get("month", type=int) or today.month
|
{
|
||||||
year = request.args.get("year", type=int) or today.year
|
"slug": "normal",
|
||||||
|
"name": "Normal",
|
||||||
month = 1 if month < 1 else 12 if month > 12 else month
|
"headline": "Stabiler Einstieg fuer den Schulalltag",
|
||||||
year = 1970 if year < 1970 else year
|
"features": [
|
||||||
|
"Inventarverwaltung mit Rollenrechten",
|
||||||
if request.method == 'POST':
|
"Basis-Support und Ticketing",
|
||||||
selected_date = (request.form.get("selected_date") or "").strip()
|
"Schulweite Uebersichten",
|
||||||
appointment_time = (request.form.get("appointment_time") or "").strip()
|
],
|
||||||
subject = (request.form.get("subject") or "").strip()
|
},
|
||||||
note = (request.form.get("note") or "").strip()
|
{
|
||||||
meeting_type = (request.form.get("meeting_type") or "digital").strip().lower()
|
"slug": "pro",
|
||||||
location_name = _sanitize_text(request.form.get("location_name") or "", 200)
|
"name": "Pro",
|
||||||
location_maps_url = _sanitize_text(request.form.get("location_maps_url") or "", 1000)
|
"headline": "Fuer Schulen mit erweitertem Bedarf",
|
||||||
|
"features": [
|
||||||
blocked_day_map = _get_blocked_day_map()
|
"Erweiterte Lizenzverwaltung",
|
||||||
|
"Detailberichte und Admin-Insights",
|
||||||
try:
|
"Priorisierter Support",
|
||||||
date.fromisoformat(selected_date)
|
],
|
||||||
except ValueError:
|
},
|
||||||
flash("Bitte eine gueltige Buchung im Kalender auswaehlen.", "error")
|
{
|
||||||
return redirect(url_for("appointments", month=month, year=year))
|
"slug": "buecherei",
|
||||||
|
"name": "Bücherei",
|
||||||
if selected_date in blocked_day_map:
|
"headline": "Optimiert fuer Bibliothek und Medien",
|
||||||
flash("Dieser Tag ist im Kalender gesperrt. Bitte eine andere Buchung waehlen.", "error")
|
"features": [
|
||||||
return redirect(url_for("appointments", month=month, year=year))
|
"Ausleih- und Rueckgabeprozesse",
|
||||||
|
"Bestandsuebersichten fuer Medien",
|
||||||
if not appointment_time or not subject:
|
"Transparente Historie pro Medium",
|
||||||
flash("Bitte Uhrzeit und Betreff ausfuellen.", "error")
|
],
|
||||||
return redirect(url_for("appointments", month=month, year=year))
|
},
|
||||||
|
]
|
||||||
if meeting_type not in ["digital", "vor_ort"]:
|
|
||||||
flash("Bitte eine gueltige Buchungsart waehlen.", "error")
|
|
||||||
return redirect(url_for("appointments", month=month, year=year))
|
|
||||||
|
|
||||||
if meeting_type == "vor_ort" and not location_name:
|
|
||||||
flash("Bei Vor-Ort-Buchungen ist ein Ort erforderlich.", "error")
|
|
||||||
return redirect(url_for("appointments", month=month, year=year))
|
|
||||||
|
|
||||||
if meeting_type == "digital":
|
|
||||||
location_name = ""
|
|
||||||
location_maps_url = ""
|
|
||||||
|
|
||||||
subject = _sanitize_text(subject, 200)
|
|
||||||
note = _sanitize_text(note, 2000)
|
|
||||||
|
|
||||||
|
active_test_license = None
|
||||||
|
if session.get("username"):
|
||||||
client = None
|
client = None
|
||||||
try:
|
try:
|
||||||
client, col = _get_collection("appointments")
|
client, col = _get_collection("licenses")
|
||||||
col.insert_one(
|
active_test_license = col.find_one(
|
||||||
{
|
{
|
||||||
"id": f"a-{int(datetime.utcnow().timestamp() * 1000)}",
|
|
||||||
"username": session.get("username"),
|
"username": session.get("username"),
|
||||||
"display_name": session.get("display_name"),
|
"status": {"$in": ["Aktiv", "Pausiert"]},
|
||||||
"date": selected_date,
|
"plan": {"$regex": "^Test"},
|
||||||
"time": appointment_time,
|
},
|
||||||
"subject": subject,
|
{"_id": 0},
|
||||||
"meeting_type": meeting_type,
|
|
||||||
"meeting_label": "Digital" if meeting_type == "digital" else "Vor Ort",
|
|
||||||
"location_name": location_name,
|
|
||||||
"location_maps_url": location_maps_url,
|
|
||||||
"note": note,
|
|
||||||
"status": "Angefragt",
|
|
||||||
"created_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
except PyMongoError:
|
except PyMongoError:
|
||||||
flash("Buchung konnte nicht gespeichert werden.", "error")
|
active_test_license = None
|
||||||
return redirect(url_for("appointments", month=month, year=year))
|
|
||||||
finally:
|
finally:
|
||||||
if client:
|
if client:
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
flash("Buchung erfolgreich angefragt.", "success")
|
|
||||||
selected = date.fromisoformat(selected_date)
|
|
||||||
return redirect(url_for("appointments", month=selected.month, year=selected.year))
|
|
||||||
|
|
||||||
cal = calendar.Calendar(firstweekday=0)
|
|
||||||
month_grid = cal.monthdayscalendar(year, month)
|
|
||||||
month_name = calendar.month_name[month]
|
|
||||||
|
|
||||||
previous_month = month - 1
|
|
||||||
previous_year = year
|
|
||||||
if previous_month == 0:
|
|
||||||
previous_month = 12
|
|
||||||
previous_year -= 1
|
|
||||||
|
|
||||||
next_month = month + 1
|
|
||||||
next_year = year
|
|
||||||
if next_month == 13:
|
|
||||||
next_month = 1
|
|
||||||
next_year += 1
|
|
||||||
|
|
||||||
user_appointments = []
|
|
||||||
client = None
|
|
||||||
try:
|
|
||||||
client, col = _get_collection("appointments")
|
|
||||||
user_appointments = list(col.find({"username": session.get("username")}).sort([("date", 1), ("time", 1)]))
|
|
||||||
for item in user_appointments:
|
|
||||||
_with_public_id(item)
|
|
||||||
except PyMongoError:
|
|
||||||
flash("Buchungen konnten nicht geladen werden.", "error")
|
|
||||||
finally:
|
|
||||||
if client:
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
blocked_day_map = _get_blocked_day_map()
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"appointments.html",
|
"appointments.html",
|
||||||
month=month,
|
software_packages=software_packages,
|
||||||
month_name=month_name,
|
active_test_license=active_test_license,
|
||||||
year=year,
|
|
||||||
month_grid=month_grid,
|
|
||||||
today_iso=today.isoformat(),
|
|
||||||
previous_month=previous_month,
|
|
||||||
previous_year=previous_year,
|
|
||||||
next_month=next_month,
|
|
||||||
next_year=next_year,
|
|
||||||
blocked_day_map=blocked_day_map,
|
|
||||||
user_appointments=user_appointments,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/appointments/start-test', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def start_test_package():
|
||||||
|
package_raw = _sanitize_text(request.form.get("package") or "normal", 40).lower()
|
||||||
|
package_map = {
|
||||||
|
"normal": "Normal",
|
||||||
|
"pro": "Pro",
|
||||||
|
"buecherei": "Buecherei",
|
||||||
|
}
|
||||||
|
selected_package = package_map.get(package_raw)
|
||||||
|
if not selected_package:
|
||||||
|
flash("Ungueltiges Paket ausgewaehlt.", "error")
|
||||||
|
return redirect(url_for("appointments"))
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if activated:
|
||||||
|
flash(f"Testversion fuer Paket {selected_package} gestartet. Key: {message}", "success")
|
||||||
|
else:
|
||||||
|
flash(message, "error")
|
||||||
|
|
||||||
|
return redirect(url_for("appointments"))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin/dashboard')
|
@app.route('/admin/dashboard')
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_dashboard():
|
def admin_dashboard():
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
+115
-373
@@ -1,235 +1,99 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Buchungsoption{% endblock %}
|
{% block title %}Buchungsoptionen{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<style>
|
<style>
|
||||||
.appointments-grid {
|
.packages-hero {
|
||||||
display: grid;
|
background: linear-gradient(140deg, #f6f2e6 0%, #e8efe4 55%, #dfe9ef 100%);
|
||||||
grid-template-columns: 1.2fr 0.8fr;
|
border: 1px solid #ced8d1;
|
||||||
gap: 1rem;
|
border-radius: 18px;
|
||||||
align-items: start;
|
padding: 1.4rem;
|
||||||
|
box-shadow: 0 14px 34px rgba(16, 36, 47, 0.08);
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.packages-hero h1 {
|
||||||
|
color: #173f57;
|
||||||
|
font-size: clamp(1.65rem, 3.2vw, 2.45rem);
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packages-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #d5e0e8;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 10px 24px rgba(23, 44, 57, 0.05);
|
||||||
|
display: grid;
|
||||||
|
gap: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-name {
|
||||||
|
margin: 0;
|
||||||
|
color: #184560;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-headline {
|
||||||
|
color: #3a6177;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.1rem;
|
||||||
|
color: #456274;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border: 1px solid #0d4f77;
|
||||||
|
background: linear-gradient(120deg, #0d6294 0%, #0b476a 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.45rem 0.88rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.secondary {
|
||||||
|
border-color: #9ab5c8;
|
||||||
|
background: #f2f8fc;
|
||||||
|
color: #275a78;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice {
|
||||||
|
margin-top: 1rem;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #d4e0e9;
|
border: 1px solid #d4e0e9;
|
||||||
border-radius: 16px;
|
border-radius: 14px;
|
||||||
padding: 1.2rem;
|
padding: 0.9rem;
|
||||||
box-shadow: 0 14px 32px rgba(20, 39, 55, 0.06);
|
color: #274f65;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-header {
|
.notice strong {
|
||||||
display: flex;
|
color: #173f57;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 0.8rem;
|
|
||||||
margin-bottom: 0.9rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-header h1 {
|
@media (max-width: 980px) {
|
||||||
font-size: 1.5rem;
|
.packages-grid {
|
||||||
color: #133f5d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.month-nav {
|
|
||||||
display: inline-flex;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.month-nav a {
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 1px solid #bfd1de;
|
|
||||||
padding: 0.38rem 0.7rem;
|
|
||||||
color: #185170;
|
|
||||||
font-weight: 700;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weekday-row,
|
|
||||||
.day-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
||||||
gap: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weekday {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #587689;
|
|
||||||
padding-bottom: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day {
|
|
||||||
min-height: 74px;
|
|
||||||
border: 1px solid #d8e3ec;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #f9fcff;
|
|
||||||
color: #0f3f5d;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 700;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day:hover {
|
|
||||||
border-color: #9ec0d6;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.day.empty {
|
|
||||||
background: #f2f6f9;
|
|
||||||
border-style: dashed;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day.today {
|
|
||||||
border-color: #2d7fab;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day.selected {
|
|
||||||
background: linear-gradient(140deg, #0d5c88 0%, #0a4464 100%);
|
|
||||||
border-color: #0d5c88;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day.blocked {
|
|
||||||
background: #f3f4f7;
|
|
||||||
color: #8b96a1;
|
|
||||||
border-color: #d2d8de;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day.blocked:hover {
|
|
||||||
transform: none;
|
|
||||||
border-color: #d2d8de;
|
|
||||||
}
|
|
||||||
|
|
||||||
.booking-form h2,
|
|
||||||
.appointment-list h2 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: #143f5e;
|
|
||||||
margin: 0 0 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-date-badge {
|
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 0.8rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.28rem 0.6rem;
|
|
||||||
border: 1px solid #bfd2df;
|
|
||||||
color: #23526c;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
margin-bottom: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.3rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1f4b65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field input,
|
|
||||||
.field textarea,
|
|
||||||
.field select {
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid #c4d4e0;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 0.65rem 0.72rem;
|
|
||||||
font: inherit;
|
|
||||||
color: #143f5d;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field textarea {
|
|
||||||
min-height: 86px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn {
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.74rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #ffffff;
|
|
||||||
background: linear-gradient(120deg, #0a5f8f 0%, #0b4567 100%);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry {
|
|
||||||
border: 1px solid #d7e2ea;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 0.72rem;
|
|
||||||
margin-bottom: 0.55rem;
|
|
||||||
background: #f9fbfd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry strong {
|
|
||||||
color: #113d59;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry p {
|
|
||||||
margin-top: 0.18rem;
|
|
||||||
font-size: 0.92rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
display: inline-block;
|
|
||||||
margin-top: 0.45rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 1px solid #c9d7e2;
|
|
||||||
padding: 0.15rem 0.55rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #2f5f79;
|
|
||||||
font-weight: 700;
|
|
||||||
background: #eef4f8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-note {
|
|
||||||
margin: 0 0 0.75rem;
|
|
||||||
color: #27526d;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.helper-line {
|
|
||||||
margin-top: 0.35rem;
|
|
||||||
font-size: 0.84rem;
|
|
||||||
color: #4f6f83;
|
|
||||||
}
|
|
||||||
|
|
||||||
.maps-tools {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.45rem;
|
|
||||||
margin-top: 0.45rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.maps-btn {
|
|
||||||
border: 1px solid #8fb1c8;
|
|
||||||
background: #f6fbff;
|
|
||||||
color: #1d5575;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.35rem 0.7rem;
|
|
||||||
font-size: 0.84rem;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-meta {
|
|
||||||
margin-top: 0.2rem;
|
|
||||||
color: #345e78;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 960px) {
|
|
||||||
.appointments-grid {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,167 +101,45 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="appointments-grid">
|
<section class="packages-hero">
|
||||||
<article class="panel">
|
<h1>Buchungsoptionen als Softwarepakete</h1>
|
||||||
<div class="calendar-header">
|
<p>Alle Pakete sind frei einsehbar, auch ohne Admin. Die Testversion kann nur mit aktivem Konto gestartet werden.</p>
|
||||||
<h1>{{ month_name }} {{ year }}</h1>
|
<p>Jedes Paket basiert auf derselben Sicherheitsarchitektur mit Rollenrechten, geschuetzten Sessions und nachvollziehbaren Aktionen.</p>
|
||||||
<div class="month-nav">
|
</section>
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="weekday-row" aria-hidden="true">
|
<section class="packages-grid">
|
||||||
<div class="weekday">Mo</div>
|
{% for package in software_packages %}
|
||||||
<div class="weekday">Di</div>
|
<article class="package-card">
|
||||||
<div class="weekday">Mi</div>
|
<h2 class="package-name">{{ package.name }}</h2>
|
||||||
<div class="weekday">Do</div>
|
<p class="package-headline">{{ package.headline }}</p>
|
||||||
<div class="weekday">Fr</div>
|
<ul class="feature-list">
|
||||||
<div class="weekday">Sa</div>
|
{% for feature in package.features %}
|
||||||
<div class="weekday">So</div>
|
<li>{{ feature }}</li>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="day-grid" id="dayGrid">
|
|
||||||
{% for week in month_grid %}
|
|
||||||
{% for day in week %}
|
|
||||||
{% if day == 0 %}
|
|
||||||
<button type="button" class="day empty" disabled></button>
|
|
||||||
{% else %}
|
|
||||||
{% set day_iso = "%04d-%02d-%02d"|format(year, month, day) %}
|
|
||||||
{% set blocked_day = blocked_day_map.get(day_iso) %}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="day {% if day_iso == today_iso %}today{% endif %} {% if blocked_day %}blocked{% endif %}"
|
|
||||||
data-date="{{ day_iso }}"
|
|
||||||
{% if blocked_day %}
|
|
||||||
disabled
|
|
||||||
title="Gesperrt{% if blocked_day.reason %}: {{ blocked_day.reason }}{% endif %}"
|
|
||||||
{% endif %}
|
|
||||||
>
|
|
||||||
{{ day }}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
</ul>
|
||||||
</div>
|
<div class="package-actions">
|
||||||
</article>
|
{% if 'username' in session %}
|
||||||
|
<form method="POST" action="{{ url_for('start_test_package') }}">
|
||||||
<aside class="panel booking-form">
|
<input type="hidden" name="package" value="{{ package.slug }}">
|
||||||
<h2>Buchung anfragen</h2>
|
<button class="btn" type="submit">Testversion starten</button>
|
||||||
<p class="meeting-note">Persoenliche Buchungsoption: Digital oder Vor Ort.</p>
|
|
||||||
<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">
|
|
||||||
<label for="appointment_time">Uhrzeit</label>
|
|
||||||
<input id="appointment_time" name="appointment_time" type="time" required>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="meeting_type">Buchungsart</label>
|
|
||||||
<select id="meeting_type" name="meeting_type" required>
|
|
||||||
<option value="digital">Digital</option>
|
|
||||||
<option value="vor_ort">Vor Ort</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="subject">Betreff</label>
|
|
||||||
<input id="subject" name="subject" type="text" placeholder="z. B. Projekt Kickoff" required>
|
|
||||||
</div>
|
|
||||||
<div class="field" id="locationField" style="display: none;">
|
|
||||||
<label for="location_name">Ort (bei Vor-Ort Pflicht)</label>
|
|
||||||
<input id="location_name" name="location_name" type="text" placeholder="Adresse oder Ort">
|
|
||||||
<div class="maps-tools">
|
|
||||||
<button type="button" id="mapsSearchBtn" class="maps-btn">In Google Maps suchen</button>
|
|
||||||
</div>
|
|
||||||
<div class="helper-line">Optional: Google-Maps-Link eintragen.</div>
|
|
||||||
<input id="location_maps_url" name="location_maps_url" type="url" placeholder="https://maps.google.com/..." style="margin-top: 0.4rem;">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="note">Notiz (optional)</label>
|
|
||||||
<textarea id="note" name="note" placeholder="Kurzbeschreibung der Anfrage"></textarea>
|
|
||||||
</div>
|
|
||||||
<button class="submit-btn" type="submit">Buchung anfragen</button>
|
|
||||||
</form>
|
</form>
|
||||||
</aside>
|
{% else %}
|
||||||
</section>
|
<a class="btn secondary" href="{{ url_for('login') }}">Mit Konto starten</a>
|
||||||
|
|
||||||
<section class="panel appointment-list" style="margin-top: 1rem;">
|
|
||||||
<h2>Meine Buchungsanfragen</h2>
|
|
||||||
{% if user_appointments %}
|
|
||||||
{% for item in user_appointments %}
|
|
||||||
<article class="entry">
|
|
||||||
<strong>{{ item.date }} um {{ item.time }} - {{ item.subject }}</strong>
|
|
||||||
{% if item.note %}
|
|
||||||
<p>{{ item.note }}</p>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="entry-meta"><strong>Buchungsart:</strong> {{ item.meeting_label or ('Vor Ort' if item.meeting_type == 'vor_ort' else 'Digital') }}</p>
|
</div>
|
||||||
{% if item.location_name %}
|
|
||||||
<p class="entry-meta"><strong>Ort:</strong> {{ item.location_name }}</p>
|
|
||||||
{% endif %}
|
|
||||||
{% if item.location_maps_url %}
|
|
||||||
<p class="entry-meta"><a href="{{ item.location_maps_url }}" target="_blank" rel="noopener noreferrer">Google Maps anzeigen</a></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if item.response %}
|
|
||||||
<p><strong>Admin-Antwort:</strong> {{ item.response }}</p>
|
|
||||||
{% endif %}
|
|
||||||
<span class="status">{{ item.status }}</span>
|
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
|
||||||
<p>Noch keine Anfragen vorhanden.</p>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script>
|
<section class="notice">
|
||||||
(function () {
|
{% if 'username' in session %}
|
||||||
const dayGrid = document.getElementById("dayGrid");
|
{% if active_test_license %}
|
||||||
const selectedDateInput = document.getElementById("selectedDateInput");
|
<p><strong>Aktive Testversion:</strong> {{ active_test_license.plan }} | Key: {{ active_test_license.license_key }} | Gueltig bis: {{ active_test_license.valid_until }}</p>
|
||||||
const selectedDateBadge = document.getElementById("selectedDateBadge");
|
{% else %}
|
||||||
const meetingType = document.getElementById("meeting_type");
|
<p><strong>Hinweis:</strong> Fuer Ihr Konto ist noch keine Testversion aktiv. Starten Sie oben eine Testversion fuer Normal, Pro oder Buecherei.</p>
|
||||||
const locationField = document.getElementById("locationField");
|
{% endif %}
|
||||||
const locationNameInput = document.getElementById("location_name");
|
{% else %}
|
||||||
const mapsSearchBtn = document.getElementById("mapsSearchBtn");
|
<p><strong>Hinweis:</strong> Bitte einloggen oder registrieren, um die Testversion zu starten.</p>
|
||||||
|
{% endif %}
|
||||||
function setSelection(dateValue, button) {
|
</section>
|
||||||
selectedDateInput.value = dateValue;
|
|
||||||
selectedDateBadge.textContent = "Ausgewählt: " + dateValue;
|
|
||||||
|
|
||||||
dayGrid.querySelectorAll(".day").forEach(function (node) {
|
|
||||||
node.classList.remove("selected");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (button) {
|
|
||||||
button.classList.add("selected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLocationRequirement() {
|
|
||||||
const isOnsite = meetingType.value === "vor_ort";
|
|
||||||
locationField.style.display = isOnsite ? "block" : "none";
|
|
||||||
locationNameInput.required = isOnsite;
|
|
||||||
if (!isOnsite) {
|
|
||||||
locationNameInput.value = "";
|
|
||||||
document.getElementById("location_maps_url").value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dayGrid.addEventListener("click", function (event) {
|
|
||||||
const target = event.target.closest(".day[data-date]");
|
|
||||||
if (!target) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSelection(target.dataset.date, target);
|
|
||||||
});
|
|
||||||
|
|
||||||
meetingType.addEventListener("change", updateLocationRequirement);
|
|
||||||
|
|
||||||
mapsSearchBtn.addEventListener("click", function () {
|
|
||||||
const query = (locationNameInput.value || "").trim();
|
|
||||||
const base = "https://www.google.com/maps/search/?api=1&query=";
|
|
||||||
window.open(base + encodeURIComponent(query || "meeting location"), "_blank", "noopener");
|
|
||||||
});
|
|
||||||
|
|
||||||
updateLocationRequirement();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+85
-30
@@ -268,6 +268,41 @@
|
|||||||
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 {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -313,6 +348,10 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logo-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -320,10 +359,10 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="hero" id="top">
|
<section class="hero" id="top">
|
||||||
<div>
|
<div>
|
||||||
<span class="eyebrow">Für moderne Schulverwaltung</span>
|
<span class="eyebrow">Lehrmittelverwaltungssoftware für Schulen</span>
|
||||||
<h1>Mehr Ruhe im Schulalltag durch klare digitale Verwaltung.</h1>
|
<h1>Lehrmittel, Schul-IT und Ausleihen in einem sicheren System.</h1>
|
||||||
<p>
|
<p>
|
||||||
Invario unterstützt Bildungseinrichtungen in Deutschland dabei, Inventar, Lizenzen und Anfragen nachvollziehbar zu organisieren. Die Bedienung bleibt bewusst einfach, während zentrale Sicherheitsfunktionen wie Rollenrechte, Protokollierung und geschützte Verbindungen standardmäßig integriert sind.
|
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.
|
||||||
</p>
|
</p>
|
||||||
<div class="cta-row">
|
<div class="cta-row">
|
||||||
{% if 'username' in session %}
|
{% if 'username' in session %}
|
||||||
@@ -348,71 +387,87 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section" id="nutzen" style="animation-delay: 100ms;">
|
<section class="section" id="nutzen" style="animation-delay: 100ms;">
|
||||||
<h2>Vorteile für Ihr Sekretariat</h2>
|
<h2>Vorteile fuer Verwaltung und Bibliothek</h2>
|
||||||
<p>Alle Informationen sind gebündelt, Vorgänge sind standardisiert, und Rückfragen lassen sich schneller beantworten.</p>
|
<p>Alle Daten zu Lehrmitteln, Geraeten und Ausleihen sind gebuendelt, sodass Rückfragen schnell geklaert werden koennen.</p>
|
||||||
<div class="section-grid">
|
<div class="section-grid">
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<h3>Inventar auf einen Blick</h3>
|
<h3>Lehrmittel pro Klasse steuern</h3>
|
||||||
<p>Geräte, Standorte und Zuständigkeiten sind zentral erfasst. Das reduziert Suchzeiten und doppelte Rückfragen.</p>
|
<p>Buecher und Unterrichtsmaterialien lassen sich klassenweise ausgeben und zum Schuljahresende sauber rueckfuehren.</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<h3>Lizenzfristen im Blick</h3>
|
<h3>Scanner-gestuetzte Ausleihe</h3>
|
||||||
<p>Auslaufende Lizenzen werden rechtzeitig sichtbar, damit keine Unterrichtsausfälle durch fehlende Software entstehen.</p>
|
<p>Barcode- oder QR-Workflows beschleunigen Ausgabe und Ruecknahme deutlich, auch bei grossen Bestandsmengen.</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<h3>Verständliche Prozesse</h3>
|
<h3>IT- und Medienbestand kombiniert</h3>
|
||||||
<p>Statt technischer Fachsprache zeigt die Plattform klare Schritte und eindeutige Statusmeldungen für jede Aufgabe.</p>
|
<p>Tablets, Laptops, Webcams und Print-Medien werden gemeinsam verwaltet statt in voneinander getrennten Listen.</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section" id="alltag" style="animation-delay: 180ms;">
|
<section class="section" id="alltag" style="animation-delay: 180ms;">
|
||||||
<h2>Typische Situationen aus dem Schulalltag</h2>
|
<h2>Typische Lehrmittel-Szenarien</h2>
|
||||||
<p>Die folgenden Beispiele zeigen, wie die Plattform bei wiederkehrenden Verwaltungsaufgaben hilft.</p>
|
<p>Die folgenden Beispiele orientieren sich an realen Schulablaeufen mit Fokus auf Tempo und Nachvollziehbarkeit.</p>
|
||||||
<div class="routine-list">
|
<div class="routine-list">
|
||||||
<div class="routine">
|
<div class="routine">
|
||||||
<div>
|
<div>
|
||||||
<b>Neues Gerät kommt an</b>
|
<b>Neue Klassensaetze treffen ein</b>
|
||||||
Seriennummer, Raum und Ansprechperson werden in wenigen Schritten eingetragen und sind sofort nachvollziehbar.
|
Lehrmittel werden etikettiert, Klassen zugeordnet und mit Ausgabehistorie gespeichert.
|
||||||
</div>
|
</div>
|
||||||
<span>Inventar</span>
|
<span>Lehrmittel</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="routine">
|
<div class="routine">
|
||||||
<div>
|
<div>
|
||||||
<b>Rückfrage zur Gerätehistorie</b>
|
<b>Rueckfrage zu ausgeliehenen Medien</b>
|
||||||
Status zu Bestellung, Nutzung oder Defekt ist direkt sichtbar, ohne mehrere Listen durchsuchen zu müssen.
|
Verfuegbarkeit, Entleiher und Rueckgabedatum sind sofort sichtbar, ohne manuelle Listenpflege.
|
||||||
</div>
|
</div>
|
||||||
<span>Transparenz</span>
|
<span>Ausleihe</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="routine">
|
<div class="routine">
|
||||||
<div>
|
<div>
|
||||||
<b>Supportfall im Unterricht</b>
|
<b>Defekt bei einem Endgeraet</b>
|
||||||
Tickets werden dokumentiert und priorisiert. Das Team sieht jederzeit, wer bereits daran arbeitet.
|
Zustand, Schaden und Verantwortlichkeiten werden dokumentiert, inklusive klarer Eskalationswege.
|
||||||
</div>
|
</div>
|
||||||
<span>Support</span>
|
<span>Sicherheit</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section" id="sicherheit" style="animation-delay: 220ms;">
|
<section class="section" id="sicherheit" style="animation-delay: 220ms;">
|
||||||
<h2>Allgemeine Sicherheit des Programms</h2>
|
<h2>Erweiterte Sicherheit fuer den Schulbetrieb</h2>
|
||||||
<p>Das Sicherheitskonzept verbindet technische Schutzmaßnahmen mit klaren Arbeitsabläufen, damit Daten im laufenden Betrieb zuverlässig geschützt bleiben.</p>
|
<p>Die Plattform setzt auf Datenschutz und Betriebssicherheit, damit personenbezogene Daten und Inventardaten langfristig geschuetzt bleiben.</p>
|
||||||
<div class="section-grid">
|
<div class="section-grid">
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<h3>Rollenbasierte Zugriffe</h3>
|
<h3>Rollen- und Bereichstrennung</h3>
|
||||||
<p>Nutzerkonten sehen nur Funktionen, die für die jeweilige Aufgabe benötigt werden. Das reduziert Fehlbedienungen und ungewollte Änderungen.</p>
|
<p>Bibliothek, Verwaltung und IT erhalten abgestufte Rechte, sodass jede Rolle nur ihre benoetigten Funktionen sieht.</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<h3>Dokumentierte Änderungen</h3>
|
<h3>Protokollierung und Nachweis</h3>
|
||||||
<p>Wichtige Aktionen werden nachvollziehbar protokolliert. Dadurch lassen sich Abläufe prüfen und Sicherheitsereignisse schneller einordnen.</p>
|
<p>Ausgaben, Ruecknahmen und kritische Admin-Aktionen werden dokumentiert und sind jederzeit auditierbar.</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<h3>Geschützte Übertragung</h3>
|
<h3>Gesicherte Web-Session</h3>
|
||||||
<p>Verbindungen laufen über sichere Protokolle, ergänzt durch Sicherheits-Header und strikte Session-Regeln für den täglichen Webbetrieb.</p>
|
<p>Sicherheits-Header, HttpOnly-Cookies und klare Session-Richtlinien schuetzen 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>
|
||||||
|
|||||||
@@ -105,6 +105,10 @@
|
|||||||
<label for="username">Benutzername</label>
|
<label for="username">Benutzername</label>
|
||||||
<input id="username" name="username" type="text" autocomplete="username" required>
|
<input id="username" name="username" type="text" autocomplete="username" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="email">E-Mail</label>
|
||||||
|
<input id="email" name="email" type="email" autocomplete="email" required>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="password">Passwort (mind. 8 Zeichen)</label>
|
<label for="password">Passwort (mind. 8 Zeichen)</label>
|
||||||
<input id="password" name="password" type="password" autocomplete="new-password" required>
|
<input id="password" name="password" type="password" autocomplete="new-password" required>
|
||||||
|
|||||||
+12
-2
@@ -63,7 +63,7 @@ def check_nm_pwd(username, password):
|
|||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
def add_user(username, password, name, last_name):
|
def add_user(username, password, name, last_name, email=""):
|
||||||
"""
|
"""
|
||||||
Add a new user to the database.
|
Add a new user to the database.
|
||||||
|
|
||||||
@@ -80,7 +80,17 @@ def add_user(username, password, name, last_name):
|
|||||||
client = None
|
client = None
|
||||||
try:
|
try:
|
||||||
client, users = _get_users_collection()
|
client, users = _get_users_collection()
|
||||||
users.insert_one({'Username': username, 'Password': hashing(password), 'Admin': False, 'active_ausleihung': None, 'name': name, 'last_name': last_name})
|
users.insert_one(
|
||||||
|
{
|
||||||
|
'Username': username,
|
||||||
|
'Password': hashing(password),
|
||||||
|
'Admin': False,
|
||||||
|
'active_ausleihung': None,
|
||||||
|
'name': name,
|
||||||
|
'last_name': last_name,
|
||||||
|
'email': (email or '').strip().lower(),
|
||||||
|
}
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
finally:
|
finally:
|
||||||
if client:
|
if client:
|
||||||
|
|||||||
Reference in New Issue
Block a user