Enhance admin dashboard with block day functionality, improve appointment details, and update footer animations

- Added a section in the admin dashboard for blocking calendar days with a form to submit block dates and reasons.
- Implemented a list of blocked days with the ability to unblock them.
- Enhanced appointment details to include meeting type and location information.
- Updated styles for blocked days in the appointment calendar.
- Improved footer design with animations and responsive adjustments.
- Removed unused license transfer forms from admin and user license templates.
- Changed database name references from 'Inventarsystem' to 'Invario_Website' in user management functions.
This commit is contained in:
2026-03-24 16:18:39 +01:00
parent 7e45cec5f8
commit 40db9e9576
8 changed files with 704 additions and 313 deletions
-58
View File
@@ -1,58 +0,0 @@
[
{
"id": "a-1774281965267",
"username": "Aiirondev",
"display_name": "Maximilian Gr\u00fcndinger",
"date": "2026-03-24",
"time": "10:00",
"subject": "Test",
"note": "",
"status": "Abgelehnt",
"created_at": "2026-03-23T16:06:05Z",
"response": "Es geht leider nicht um die von ihnen angefragte Uhrzeit, bitte versuchen sie es zwei Tage sp\u00e4ter",
"responded_at": "2026-03-23T16:06:52Z",
"responded_by": "Aiirondev"
},
{
"id": "a-1774287654152",
"username": "Aiirondev",
"display_name": "Maximilian Gr\u00fcndinger",
"date": "2026-03-26",
"time": "10:00",
"subject": "Test",
"note": "Test",
"status": "Bestaetigt",
"created_at": "2026-03-23T17:40:54Z",
"response": "Wunderbar bis dann!",
"responded_at": "2026-03-23T17:41:10Z",
"responded_by": "Aiirondev"
},
{
"id": "a-1774296722216",
"username": "Aiirondev",
"display_name": "Maximilian Gr\u00fcndinger",
"date": "2026-03-27",
"time": "14:30",
"subject": "Erst Gespr\u00e4ch f\u00fcr Grunschule ...",
"note": "Erste kontakt aufnahme",
"status": "Bestaetigt",
"created_at": "2026-03-23T20:12:02Z",
"response": "Ja passt so, wir kommen vorbei",
"responded_at": "2026-03-23T20:12:43Z",
"responded_by": "Aiirondev"
},
{
"id": "a-1774335231538",
"username": "Aiirondev",
"display_name": "Maximilian Gr\u00fcndinger",
"date": "2026-03-26",
"time": "13:00",
"subject": "Erst Gespr\u00e4ch",
"note": "Test",
"status": "Bestaetigt",
"created_at": "2026-03-24T06:53:51Z",
"response": "Es w\u00fcrde an dem von ihnen andgefragten Datum eine Stunde gehen",
"responded_at": "2026-03-24T06:54:42Z",
"responded_by": "Aiirondev"
}
]
+292 -187
View File
@@ -1,10 +1,8 @@
from flask_jwt_extended import JWTManager, create_access_token, jwt_required from flask_jwt_extended import JWTManager, create_access_token, jwt_required
from flask import Flask, render_template, request, jsonify, flash, redirect, url_for, get_flashed_messages, session from flask import Flask, render_template, request, jsonify, flash, redirect, url_for, get_flashed_messages, session
import os import os
import json
import calendar import calendar
from datetime import timedelta, datetime, date from datetime import timedelta, datetime, date
from pathlib import Path
from functools import wraps from functools import wraps
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@@ -36,14 +34,10 @@ def set_security_headers(response):
response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com https://fonts.googleapis.com; img-src 'self' data:; connect-src 'self';" response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com https://fonts.googleapis.com; img-src 'self' data:; connect-src 'self';"
return response return response
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = BASE_DIR / "data" INVOICE_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "invoices")
INVOICE_UPLOAD_DIR = BASE_DIR / "static" / "uploads" / "invoices"
USERS_FILE = DATA_DIR / "users.json"
APPOINTMENTS_FILE = DATA_DIR / "appointments.json"
POSTS_FILE = DATA_DIR / "posts.json"
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", "Inventarsystem") MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "Invario_Website")
def _issue_access_token() -> str: def _issue_access_token() -> str:
@@ -64,10 +58,10 @@ def _save_invoice_pdf(file_obj, invoice_number: str) -> str | None:
original_name = secure_filename(file_obj.filename) original_name = secure_filename(file_obj.filename)
if not _is_allowed_invoice_filename(original_name): if not _is_allowed_invoice_filename(original_name):
return None return None
INVOICE_UPLOAD_DIR.mkdir(parents=True, exist_ok=True) os.makedirs(INVOICE_UPLOAD_DIR, exist_ok=True)
safe_invoice = secure_filename(invoice_number or "invoice") safe_invoice = secure_filename(invoice_number or "invoice")
unique_name = f"{datetime.utcnow().strftime('%Y%m%d%H%M%S')}_{safe_invoice}_{original_name}" unique_name = f"{datetime.utcnow().strftime('%Y%m%d%H%M%S')}_{safe_invoice}_{original_name}"
target = INVOICE_UPLOAD_DIR / unique_name target = os.path.join(INVOICE_UPLOAD_DIR, unique_name)
file_obj.save(target) file_obj.save(target)
return f"uploads/invoices/{unique_name}" return f"uploads/invoices/{unique_name}"
@@ -108,78 +102,6 @@ def _list_users_for_admin() -> list:
return users return users
def _ensure_user_license(username: str, display_name: str) -> None:
client = None
try:
client, licenses = _get_collection("licenses")
if licenses.find_one({"username": username}):
return
licenses.insert_one(
{
"username": username,
"school_name": f"{display_name} Schule",
"license_key": f"LIC-{int(datetime.utcnow().timestamp())}-{username[:3].upper()}",
"plan": "Standard",
"status": "Aktiv",
"valid_until": "2027-12-31",
"created_at": _utc_now_iso(),
}
)
except PyMongoError:
return
finally:
if client:
client.close()
def _ensure_user_invoice(username: str) -> None:
client = None
try:
client, invoices = _get_collection("invoices")
if invoices.find_one({"username": username}):
return
invoices.insert_one(
{
"username": username,
"invoice_number": f"INV-{datetime.utcnow().strftime('%Y%m')}-{username[:3].upper()}",
"period": datetime.utcnow().strftime("%m/%Y"),
"amount_eur": 79.0,
"status": "Offen",
"due_date": "2026-12-31",
"pdf_path": "",
"created_at": _utc_now_iso(),
}
)
except PyMongoError:
return
finally:
if client:
client.close()
def _ensure_data_files() -> None:
DATA_DIR.mkdir(parents=True, exist_ok=True)
if not USERS_FILE.exists():
USERS_FILE.write_text("[]", encoding="utf-8")
if not APPOINTMENTS_FILE.exists():
APPOINTMENTS_FILE.write_text("[]", encoding="utf-8")
if not POSTS_FILE.exists():
POSTS_FILE.write_text("[]", encoding="utf-8")
def _read_json(file_path: Path) -> list:
_ensure_data_files()
try:
return json.loads(file_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return []
def _write_json(file_path: Path, payload: list) -> None:
_ensure_data_files()
file_path.write_text(json.dumps(payload, indent=2, ensure_ascii=True), encoding="utf-8")
def _sanitize_text(text: str, max_length: int = 255) -> str: def _sanitize_text(text: str, max_length: int = 255) -> str:
"""Sanitize user text input: strip, limit length, and escape HTML.""" """Sanitize user text input: strip, limit length, and escape HTML."""
text = (text or "").strip() text = (text or "").strip()
@@ -188,6 +110,74 @@ def _sanitize_text(text: str, max_length: int = 255) -> str:
return text return text
def _with_public_id(doc: dict | None) -> dict | None:
if not doc:
return doc
if not doc.get("id") and doc.get("_id") is not None:
doc["id"] = str(doc.get("_id"))
return doc
def _appointment_query_from_id(appointment_id: str) -> dict | None:
lookup = (appointment_id or "").strip()
if not lookup:
return None
if lookup.startswith("a-"):
return {"id": lookup}
try:
return {"_id": ObjectId(lookup)}
except Exception:
return {"id": lookup}
def _post_query_from_id(post_id: str) -> dict | None:
lookup = (post_id or "").strip()
if not lookup:
return None
if lookup.startswith("p-"):
return {"id": lookup}
try:
return {"_id": ObjectId(lookup)}
except Exception:
return {"id": lookup}
def _get_blocked_days() -> list:
client = None
entries = []
try:
client, col = _get_collection("blocked_days")
entries = list(col.find({}, {"_id": 0}).sort("date", 1))
except PyMongoError:
return []
finally:
if client:
client.close()
normalized = []
for item in entries:
day = (item.get("date") or "").strip()
if not day:
continue
normalized.append(
{
"date": day,
"reason": (item.get("reason") or "").strip()[:200],
"blocked_by": (item.get("blocked_by") or "").strip()[:80],
"created_at": item.get("created_at") or "",
}
)
normalized.sort(key=lambda value: value.get("date", ""))
return normalized
def _get_blocked_day_map() -> dict:
blocked = {}
for item in _get_blocked_days():
blocked[item.get("date", "")] = item
return blocked
def _sanitize_html(html_content: str, max_length: int = 50000) -> str: def _sanitize_html(html_content: str, max_length: int = 50000) -> str:
"""Sanitize HTML content: allow safe tags only.""" """Sanitize HTML content: allow safe tags only."""
if not html_content: if not html_content:
@@ -321,8 +311,6 @@ def login():
session['display_name'] = stored_user.get("display_name") or stored_user.get("username") session['display_name'] = stored_user.get("display_name") or stored_user.get("username")
session['is_admin'] = stored_user.get("is_admin", False) session['is_admin'] = stored_user.get("is_admin", False)
session['access_token'] = _issue_access_token() session['access_token'] = _issue_access_token()
_ensure_user_license(session['username'], session['display_name'])
_ensure_user_invoice(session['username'])
if request.is_json: if request.is_json:
return jsonify({"access_token": session['access_token'], "token_type": "Bearer"}), 200 return jsonify({"access_token": session['access_token'], "token_type": "Bearer"}), 200
@@ -400,6 +388,11 @@ def appointments():
appointment_time = (request.form.get("appointment_time") or "").strip() appointment_time = (request.form.get("appointment_time") or "").strip()
subject = (request.form.get("subject") or "").strip() subject = (request.form.get("subject") or "").strip()
note = (request.form.get("note") or "").strip() note = (request.form.get("note") or "").strip()
meeting_type = (request.form.get("meeting_type") or "digital").strip().lower()
location_name = _sanitize_text(request.form.get("location_name") or "", 200)
location_maps_url = _sanitize_text(request.form.get("location_maps_url") or "", 1000)
blocked_day_map = _get_blocked_day_map()
try: try:
date.fromisoformat(selected_date) date.fromisoformat(selected_date)
@@ -407,15 +400,33 @@ def appointments():
flash("Bitte einen gueltigen Termin im Kalender auswaehlen.", "error") flash("Bitte einen gueltigen Termin im Kalender auswaehlen.", "error")
return redirect(url_for("appointments", month=month, year=year)) return redirect(url_for("appointments", month=month, year=year))
if selected_date in blocked_day_map:
flash("Dieser Tag ist im Kalender gesperrt. Bitte einen anderen Termin waehlen.", "error")
return redirect(url_for("appointments", month=month, year=year))
if not appointment_time or not subject: if not appointment_time or not subject:
flash("Bitte Uhrzeit und Betreff ausfuellen.", "error") flash("Bitte Uhrzeit und Betreff ausfuellen.", "error")
return redirect(url_for("appointments", month=month, year=year)) return redirect(url_for("appointments", month=month, year=year))
if meeting_type not in ["digital", "vor_ort"]:
flash("Bitte eine gueltige Terminart waehlen.", "error")
return redirect(url_for("appointments", month=month, year=year))
if meeting_type == "vor_ort" and not location_name:
flash("Bei Vor-Ort-Terminen 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) subject = _sanitize_text(subject, 200)
note = _sanitize_text(note, 2000) note = _sanitize_text(note, 2000)
entries = _read_json(APPOINTMENTS_FILE) client = None
entries.append( try:
client, col = _get_collection("appointments")
col.insert_one(
{ {
"id": f"a-{int(datetime.utcnow().timestamp() * 1000)}", "id": f"a-{int(datetime.utcnow().timestamp() * 1000)}",
"username": session.get("username"), "username": session.get("username"),
@@ -423,12 +434,21 @@ def appointments():
"date": selected_date, "date": selected_date,
"time": appointment_time, "time": appointment_time,
"subject": subject, "subject": subject,
"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, "note": note,
"status": "Angefragt", "status": "Angefragt",
"created_at": datetime.utcnow().isoformat(timespec="seconds") + "Z", "created_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
} }
) )
_write_json(APPOINTMENTS_FILE, entries) except PyMongoError:
flash("Termin konnte nicht gespeichert werden.", "error")
return redirect(url_for("appointments", month=month, year=year))
finally:
if client:
client.close()
flash("Termin erfolgreich angefragt.", "success") flash("Termin erfolgreich angefragt.", "success")
selected = date.fromisoformat(selected_date) selected = date.fromisoformat(selected_date)
@@ -450,9 +470,20 @@ def appointments():
next_month = 1 next_month = 1
next_year += 1 next_year += 1
all_appointments = _read_json(APPOINTMENTS_FILE) user_appointments = []
user_appointments = [item for item in all_appointments if item.get("username") == session.get("username")] client = None
user_appointments.sort(key=lambda item: (item.get("date", ""), item.get("time", "")), reverse=False) 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("Termine 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",
@@ -465,6 +496,7 @@ def appointments():
previous_year=previous_year, previous_year=previous_year,
next_month=next_month, next_month=next_month,
next_year=next_year, next_year=next_year,
blocked_day_map=blocked_day_map,
user_appointments=user_appointments, user_appointments=user_appointments,
) )
@@ -472,8 +504,20 @@ def appointments():
@app.route('/admin/dashboard') @app.route('/admin/dashboard')
@admin_required @admin_required
def admin_dashboard(): def admin_dashboard():
all_appointments = _read_json(APPOINTMENTS_FILE) all_appointments = []
all_appointments.sort(key=lambda x: (x.get("date", ""), x.get("time", "")), reverse=True) client = None
try:
client, col = _get_collection("appointments")
all_appointments = list(col.find().sort([("date", -1), ("time", -1)]))
for item in all_appointments:
_with_public_id(item)
except PyMongoError:
flash("Terminanfragen konnten nicht geladen werden.", "error")
finally:
if client:
client.close()
blocked_days = _get_blocked_days()
status_counts = { status_counts = {
"Angefragt": len([a for a in all_appointments if a.get("status") == "Angefragt"]), "Angefragt": len([a for a in all_appointments if a.get("status") == "Angefragt"]),
@@ -481,17 +525,75 @@ def admin_dashboard():
"Abgelehnt": len([a for a in all_appointments if a.get("status") == "Abgelehnt"]), "Abgelehnt": len([a for a in all_appointments if a.get("status") == "Abgelehnt"]),
} }
posts = _read_json(POSTS_FILE) total_posts = 0
total_posts = len(posts) client = None
try:
client, col = _get_collection("posts")
total_posts = col.count_documents({})
except PyMongoError:
total_posts = 0
finally:
if client:
client.close()
return render_template( return render_template(
"admin_dashboard.html", "admin_dashboard.html",
appointments=all_appointments, appointments=all_appointments,
blocked_days=blocked_days,
status_counts=status_counts, status_counts=status_counts,
total_posts=total_posts, total_posts=total_posts,
) )
@app.route('/admin/appointments/block-day', methods=['POST'])
@admin_required
def admin_block_day():
action = _sanitize_text(request.form.get("action") or "", 30)
block_date = (request.form.get("block_date") or "").strip()
reason = _sanitize_text(request.form.get("reason") or "", 200)
client = None
try:
client, col = _get_collection("blocked_days")
if action == "add":
try:
date.fromisoformat(block_date)
except ValueError:
flash("Bitte ein gueltiges Datum zum Sperren waehlen.", "error")
return redirect(url_for("admin_dashboard"))
if col.find_one({"date": block_date}):
flash("Der Tag ist bereits gesperrt.", "error")
return redirect(url_for("admin_dashboard"))
col.insert_one(
{
"date": block_date,
"reason": reason,
"blocked_by": session.get("username") or "admin",
"created_at": _utc_now_iso(),
}
)
flash("Tag im Kalender gesperrt.", "success")
elif action == "remove":
result = col.delete_one({"date": block_date})
if not result.deleted_count:
flash("Sperrtag nicht gefunden.", "error")
return redirect(url_for("admin_dashboard"))
flash("Sperrtag entfernt.", "success")
else:
flash("Ungueltige Aktion fuer Kalendersperre.", "error")
return redirect(url_for("admin_dashboard"))
except PyMongoError:
flash("Kalendersperre konnte nicht gespeichert werden.", "error")
finally:
if client:
client.close()
return redirect(url_for("admin_dashboard"))
@app.route('/admin/appointment/<appointment_id>', methods=['POST']) @app.route('/admin/appointment/<appointment_id>', methods=['POST'])
@admin_required @admin_required
def update_appointment(appointment_id): def update_appointment(appointment_id):
@@ -502,27 +604,37 @@ def update_appointment(appointment_id):
flash("Ungueltige Aktion.", "error") flash("Ungueltige Aktion.", "error")
return redirect(url_for("admin_dashboard")) return redirect(url_for("admin_dashboard"))
all_appointments = _read_json(APPOINTMENTS_FILE) query = _appointment_query_from_id(appointment_id)
appointment = None if not query:
for item in all_appointments:
if item.get("id") == appointment_id:
appointment = item
break
if not appointment:
flash("Termin nicht gefunden.", "error") flash("Termin nicht gefunden.", "error")
return redirect(url_for("admin_dashboard")) return redirect(url_for("admin_dashboard"))
if action == "confirm": new_status = "Bestaetigt" if action == "confirm" else "Abgelehnt"
appointment["status"] = "Bestaetigt"
else:
appointment["status"] = "Abgelehnt"
appointment["response"] = response_text client = None
appointment["responded_at"] = datetime.utcnow().isoformat(timespec="seconds") + "Z" try:
appointment["responded_by"] = session.get("username") client, col = _get_collection("appointments")
result = col.update_one(
query,
{
"$set": {
"status": new_status,
"response": response_text,
"responded_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
"responded_by": session.get("username"),
}
},
)
if result.matched_count == 0:
flash("Termin nicht gefunden.", "error")
return redirect(url_for("admin_dashboard"))
except PyMongoError:
flash("Termin konnte nicht aktualisiert werden.", "error")
return redirect(url_for("admin_dashboard"))
finally:
if client:
client.close()
_write_json(APPOINTMENTS_FILE, all_appointments)
flash(f"Termin wurde {('bestaetigt' if action == 'confirm' else 'abgelehnt')}.", "success") flash(f"Termin wurde {('bestaetigt' if action == 'confirm' else 'abgelehnt')}.", "success")
return redirect(url_for("admin_dashboard")) return redirect(url_for("admin_dashboard"))
@@ -541,9 +653,11 @@ def admin_blog():
if not title or not content: if not title or not content:
flash("Bitte Titel und inhalt ausfuellen.", "error") flash("Bitte Titel und inhalt ausfuellen.", "error")
return redirect(url_for("admin_blog")) return redirect(url_for("admin_blog"))
client = None
posts = _read_json(POSTS_FILE) try:
posts.append({ client, col = _get_collection("posts")
col.insert_one(
{
"id": f"p-{int(datetime.utcnow().timestamp() * 1000)}", "id": f"p-{int(datetime.utcnow().timestamp() * 1000)}",
"title": title, "title": title,
"excerpt": excerpt or (content[:150] + "...") if len(content) > 150 else content, "excerpt": excerpt or (content[:150] + "...") if len(content) > 150 else content,
@@ -551,50 +665,93 @@ def admin_blog():
"author": session.get("username"), "author": session.get("username"),
"created_at": datetime.utcnow().isoformat(timespec="seconds") + "Z", "created_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
"published": True, "published": True,
}) }
_write_json(POSTS_FILE, posts) )
except PyMongoError:
flash("Beitrag konnte nicht gespeichert werden.", "error")
return redirect(url_for("admin_blog"))
finally:
if client:
client.close()
flash("Beitrag veroeffentlicht.", "success") flash("Beitrag veroeffentlicht.", "success")
return redirect(url_for("admin_blog")) return redirect(url_for("admin_blog"))
elif action == "delete": elif action == "delete":
post_id = (request.form.get("post_id") or "").strip() post_id = (request.form.get("post_id") or "").strip()
if not post_id or not post_id.startswith("p-"): query = _post_query_from_id(post_id)
if not query:
flash("Ungueltige Beitrag-ID.", "error") flash("Ungueltige Beitrag-ID.", "error")
return redirect(url_for("admin_blog")) return redirect(url_for("admin_blog"))
posts = _read_json(POSTS_FILE) client = None
original_count = len(posts) try:
posts = [p for p in posts if p.get("id") != post_id] client, col = _get_collection("posts")
if len(posts) == original_count: result = col.delete_one(query)
except PyMongoError:
flash("Beitrag konnte nicht geloescht werden.", "error")
return redirect(url_for("admin_blog"))
finally:
if client:
client.close()
if not result.deleted_count:
flash("Beitrag nicht gefunden.", "error") flash("Beitrag nicht gefunden.", "error")
return redirect(url_for("admin_blog")) return redirect(url_for("admin_blog"))
_write_json(POSTS_FILE, posts)
flash("Beitrag geloescht.", "success") flash("Beitrag geloescht.", "success")
return redirect(url_for("admin_blog")) return redirect(url_for("admin_blog"))
posts = _read_json(POSTS_FILE) posts = []
posts.sort(key=lambda x: x.get("created_at", ""), reverse=True) client = None
try:
client, col = _get_collection("posts")
posts = list(col.find().sort("created_at", -1))
for item in posts:
_with_public_id(item)
except PyMongoError:
flash("Blogbeitraege konnten nicht geladen werden.", "error")
finally:
if client:
client.close()
return render_template("admin_blog.html", posts=posts) return render_template("admin_blog.html", posts=posts)
@app.route('/blog') @app.route('/blog')
def blog(): def blog():
posts = _read_json(POSTS_FILE) posts = []
posts.sort(key=lambda x: x.get("created_at", ""), reverse=True) client = None
try:
client, col = _get_collection("posts")
posts = list(col.find({"published": True}).sort("created_at", -1))
for item in posts:
_with_public_id(item)
except PyMongoError:
posts = []
finally:
if client:
client.close()
return render_template("blog.html", posts=posts) return render_template("blog.html", posts=posts)
@app.route('/blog/<post_id>') @app.route('/blog/<post_id>')
def blog_post(post_id): def blog_post(post_id):
if not post_id or not post_id.startswith("p-"): query = _post_query_from_id(post_id)
if not query:
flash("Ungueltige Beitrag-ID.", "error") flash("Ungueltige Beitrag-ID.", "error")
return redirect(url_for("blog")) return redirect(url_for("blog"))
client = None
posts = _read_json(POSTS_FILE)
post = None post = None
for p in posts: try:
if p.get("id") == post_id: client, col = _get_collection("posts")
post = p post = col.find_one(query)
break _with_public_id(post)
except PyMongoError:
post = None
finally:
if client:
client.close()
if not post: if not post:
flash("Beitrag nicht gefunden.", "error") flash("Beitrag nicht gefunden.", "error")
@@ -607,40 +764,7 @@ def blog_post(post_id):
@login_required @login_required
def my_licenses(): def my_licenses():
if request.method == 'POST': if request.method == 'POST':
action = _sanitize_text(request.form.get("action") or "", 30) flash("Weitergabe von Lizenzen ist deaktiviert.", "error")
license_id = _sanitize_text(request.form.get("license_id") or "", 64)
target_username = _sanitize_text(request.form.get("target_username") or "", 80)
if action != "transfer" or not license_id or not target_username:
flash("Ungueltige Weitergabe-Angaben.", "error")
return redirect(url_for("my_licenses"))
if target_username == session.get("username"):
flash("Bitte einen anderen Nutzer waehlen.", "error")
return redirect(url_for("my_licenses"))
client = None
try:
client, col = _get_collection("licenses")
result = col.update_one(
{"_id": ObjectId(license_id), "username": session.get("username")},
{
"$set": {
"username": target_username,
"transferred_at": _utc_now_iso(),
"transferred_by": session.get("username"),
}
},
)
if result.modified_count:
flash("Lizenz wurde erfolgreich weitergegeben.", "success")
else:
flash("Lizenz konnte nicht weitergegeben werden.", "error")
except Exception:
flash("Weitergabe fehlgeschlagen.", "error")
finally:
if client:
client.close()
return redirect(url_for("my_licenses")) return redirect(url_for("my_licenses"))
licenses = [] licenses = []
@@ -656,8 +780,7 @@ def my_licenses():
if client: if client:
client.close() client.close()
transfer_users = [u for u in _list_users_for_admin() if u.get("username") != session.get("username")] return render_template("my_licenses.html", licenses=licenses)
return render_template("my_licenses.html", licenses=licenses, transfer_users=transfer_users)
@app.route('/my/invoices') @app.route('/my/invoices')
@@ -945,24 +1068,6 @@ def admin_licenses():
) )
flash("Lizenz aktualisiert.", "success") flash("Lizenz aktualisiert.", "success")
elif action == "transfer" and license_id:
target_username = _sanitize_text(request.form.get("target_username") or "", 80)
if not target_username:
flash("Bitte Zielnutzer waehlen.", "error")
return redirect(url_for("admin_licenses"))
col.update_one(
{"_id": ObjectId(license_id)},
{
"$set": {
"username": target_username,
"transferred_at": _utc_now_iso(),
"transferred_by": session.get("username"),
}
},
)
flash("Lizenz weitergegeben.", "success")
elif action == "delete" and license_id: 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 geloescht.", "success")
+111
View File
@@ -48,6 +48,76 @@
margin-bottom: 0.8rem; margin-bottom: 0.8rem;
} }
.block-days-section {
margin-bottom: 1.2rem;
padding: 1rem;
border: 1px solid #d8e3ec;
border-radius: 12px;
background: #f9fbfd;
}
.block-days-section h3 {
margin: 0 0 0.6rem;
color: #143a55;
font-size: 1.05rem;
}
.block-form {
display: grid;
grid-template-columns: 180px 1fr auto;
gap: 0.5rem;
margin-bottom: 0.8rem;
}
.block-form input {
width: 100%;
border: 1px solid #c4d4e0;
border-radius: 8px;
padding: 0.45rem 0.55rem;
font: inherit;
}
.block-btn,
.unblock-btn {
border: none;
border-radius: 8px;
padding: 0.45rem 0.65rem;
font-weight: 700;
cursor: pointer;
}
.block-btn {
color: #ffffff;
background: linear-gradient(120deg, #0b5b89 0%, #0b4262 100%);
}
.blocked-list {
display: grid;
gap: 0.45rem;
}
.blocked-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
border: 1px solid #d5dee6;
border-radius: 9px;
padding: 0.5rem 0.65rem;
background: #ffffff;
}
.blocked-item p {
margin: 0;
font-size: 0.88rem;
}
.unblock-btn {
color: #7e2d2d;
background: #fff3f3;
border: 1px solid #e8b8b8;
}
.appointment-card { .appointment-card {
border: 1px solid #d8e3ec; border: 1px solid #d8e3ec;
border-radius: 12px; border-radius: 12px;
@@ -141,6 +211,10 @@
.admin-grid { .admin-grid {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
.block-form {
grid-template-columns: 1fr;
}
} }
@media (max-width: 600px) { @media (max-width: 600px) {
@@ -175,12 +249,49 @@
<section class="appointments-section"> <section class="appointments-section">
<h2>Terminanfragen verwalten</h2> <h2>Terminanfragen verwalten</h2>
<div class="block-days-section">
<h3>Kalender-Tage sperren</h3>
<form method="POST" action="{{ url_for('admin_block_day') }}" class="block-form">
<input type="hidden" name="action" value="add">
<input type="date" name="block_date" required>
<input type="text" name="reason" placeholder="Grund (optional)">
<button type="submit" class="block-btn">Tag sperren</button>
</form>
<div class="blocked-list">
{% if blocked_days %}
{% for blocked in blocked_days %}
<div class="blocked-item">
<p>
<strong>{{ blocked.date }}</strong>
{% if blocked.reason %}
- {{ blocked.reason }}
{% endif %}
</p>
<form method="POST" action="{{ url_for('admin_block_day') }}">
<input type="hidden" name="action" value="remove">
<input type="hidden" name="block_date" value="{{ blocked.date }}">
<button type="submit" class="unblock-btn">Entsperren</button>
</form>
</div>
{% endfor %}
{% else %}
<p>Aktuell sind keine Tage gesperrt.</p>
{% endif %}
</div>
</div>
{% if appointments %} {% if appointments %}
{% for appointment in appointments %} {% for appointment in appointments %}
<article class="appointment-card"> <article class="appointment-card">
<div class="appointment-info"> <div class="appointment-info">
<strong>{{ appointment.display_name }} - {{ appointment.subject }}</strong> <strong>{{ appointment.display_name }} - {{ appointment.subject }}</strong>
<p><strong>Datum:</strong> {{ appointment.date }} um {{ appointment.time }}</p> <p><strong>Datum:</strong> {{ appointment.date }} um {{ appointment.time }}</p>
<p><strong>Terminart:</strong> {{ appointment.meeting_label or ('Vor Ort' if appointment.meeting_type == 'vor_ort' else 'Digital') }}</p>
{% if appointment.location_name %}
<p><strong>Ort:</strong> {{ appointment.location_name }}</p>
{% endif %}
{% if appointment.location_maps_url %}
<p><strong>Maps:</strong> <a href="{{ appointment.location_maps_url }}" target="_blank" rel="noopener noreferrer">Link öffnen</a></p>
{% endif %}
<p><strong>Angefragt:</strong> {{ appointment.created_at[:10] }}</p> <p><strong>Angefragt:</strong> {{ appointment.created_at[:10] }}</p>
{% if appointment.note %} {% if appointment.note %}
<p><strong>Notiz:</strong> {{ appointment.note }}</p> <p><strong>Notiz:</strong> {{ appointment.note }}</p>
-13
View File
@@ -45,19 +45,6 @@
<input type="text" name="valid_until" value="{{ item.valid_until }}" placeholder="Gültig bis"> <input type="text" name="valid_until" value="{{ item.valid_until }}" placeholder="Gültig bis">
<button type="submit">Aktualisieren</button> <button type="submit">Aktualisieren</button>
</form> </form>
<form method="post" class="inline-form" style="margin-top:0.45rem;">
<input type="hidden" name="action" value="transfer">
<input type="hidden" name="license_id" value="{{ item.id }}">
<select name="target_username" required>
<option value="">Lizenz weitergeben an...</option>
{% for user in users %}
{% if user.username != item.username %}
<option value="{{ user.username }}">{{ user.username }} ({{ user.display_name }})</option>
{% endif %}
{% endfor %}
</select>
<button type="submit">Weitergeben</button>
</form>
<form method="post"> <form method="post">
<input type="hidden" name="action" value="delete"> <input type="hidden" name="action" value="delete">
<input type="hidden" name="license_id" value="{{ item.id }}"> <input type="hidden" name="license_id" value="{{ item.id }}">
+103 -2
View File
@@ -94,6 +94,18 @@
color: #ffffff; 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, .booking-form h2,
.appointment-list h2 { .appointment-list h2 {
font-size: 1.2rem; font-size: 1.2rem;
@@ -124,7 +136,8 @@
} }
.field input, .field input,
.field textarea { .field textarea,
.field select {
width: 100%; width: 100%;
border: 1px solid #c4d4e0; border: 1px solid #c4d4e0;
border-radius: 10px; border-radius: 10px;
@@ -180,6 +193,41 @@
background: #eef4f8; 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) { @media (max-width: 960px) {
.appointments-grid { .appointments-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -216,10 +264,15 @@
<button type="button" class="day empty" disabled></button> <button type="button" class="day empty" disabled></button>
{% else %} {% else %}
{% set day_iso = "%04d-%02d-%02d"|format(year, month, day) %} {% set day_iso = "%04d-%02d-%02d"|format(year, month, day) %}
{% set blocked_day = blocked_day_map.get(day_iso) %}
<button <button
type="button" type="button"
class="day {% if day_iso == today_iso %}today{% endif %}" class="day {% if day_iso == today_iso %}today{% endif %} {% if blocked_day %}blocked{% endif %}"
data-date="{{ day_iso }}" data-date="{{ day_iso }}"
{% if blocked_day %}
disabled
title="Gesperrt{% if blocked_day.reason %}: {{ blocked_day.reason }}{% endif %}"
{% endif %}
> >
{{ day }} {{ day }}
</button> </button>
@@ -231,6 +284,7 @@
<aside class="panel booking-form"> <aside class="panel booking-form">
<h2>Termin anfragen</h2> <h2>Termin anfragen</h2>
<p class="meeting-note">Persoenliches Gespraechstermin: Digital oder Vor Ort.</p>
<span class="selected-date-badge" id="selectedDateBadge">Kein Datum gewählt</span> <span class="selected-date-badge" id="selectedDateBadge">Kein Datum gewählt</span>
<form method="POST" action="{{ url_for('appointments', month=month, year=year) }}"> <form method="POST" action="{{ url_for('appointments', month=month, year=year) }}">
<input type="hidden" id="selectedDateInput" name="selected_date" required> <input type="hidden" id="selectedDateInput" name="selected_date" required>
@@ -238,10 +292,26 @@
<label for="appointment_time">Uhrzeit</label> <label for="appointment_time">Uhrzeit</label>
<input id="appointment_time" name="appointment_time" type="time" required> <input id="appointment_time" name="appointment_time" type="time" required>
</div> </div>
<div class="field">
<label for="meeting_type">Terminart</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"> <div class="field">
<label for="subject">Betreff</label> <label for="subject">Betreff</label>
<input id="subject" name="subject" type="text" placeholder="z. B. Projekt Kickoff" required> <input id="subject" name="subject" type="text" placeholder="z. B. Projekt Kickoff" required>
</div> </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"> <div class="field">
<label for="note">Notiz (optional)</label> <label for="note">Notiz (optional)</label>
<textarea id="note" name="note" placeholder="Kurzbeschreibung der Anfrage"></textarea> <textarea id="note" name="note" placeholder="Kurzbeschreibung der Anfrage"></textarea>
@@ -260,6 +330,13 @@
{% if item.note %} {% if item.note %}
<p>{{ item.note }}</p> <p>{{ item.note }}</p>
{% endif %} {% endif %}
<p class="entry-meta"><strong>Terminart:</strong> {{ item.meeting_label or ('Vor Ort' if item.meeting_type == 'vor_ort' else 'Digital') }}</p>
{% 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 %} {% if item.response %}
<p><strong>Admin-Antwort:</strong> {{ item.response }}</p> <p><strong>Admin-Antwort:</strong> {{ item.response }}</p>
{% endif %} {% endif %}
@@ -276,6 +353,10 @@
const dayGrid = document.getElementById("dayGrid"); const dayGrid = document.getElementById("dayGrid");
const selectedDateInput = document.getElementById("selectedDateInput"); const selectedDateInput = document.getElementById("selectedDateInput");
const selectedDateBadge = document.getElementById("selectedDateBadge"); const selectedDateBadge = document.getElementById("selectedDateBadge");
const meetingType = document.getElementById("meeting_type");
const locationField = document.getElementById("locationField");
const locationNameInput = document.getElementById("location_name");
const mapsSearchBtn = document.getElementById("mapsSearchBtn");
function setSelection(dateValue, button) { function setSelection(dateValue, button) {
selectedDateInput.value = dateValue; selectedDateInput.value = dateValue;
@@ -290,6 +371,16 @@
} }
} }
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) { dayGrid.addEventListener("click", function (event) {
const target = event.target.closest(".day[data-date]"); const target = event.target.closest(".day[data-date]");
if (!target) { if (!target) {
@@ -297,6 +388,16 @@
} }
setSelection(target.dataset.date, target); 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> </script>
{% endblock %} {% endblock %}
+162 -3
View File
@@ -42,6 +42,12 @@
var(--bg-main); var(--bg-main);
} }
body {
min-height: 100vh;
display: flex;
flex-direction: column;
}
h1 { h1 {
margin: 0; margin: 0;
font-family: "Space Grotesk", system-ui, sans-serif; font-family: "Space Grotesk", system-ui, sans-serif;
@@ -210,6 +216,7 @@
} }
.main-wrap { .main-wrap {
flex: 1 0 auto;
padding: 2rem 0 3.4rem; padding: 2rem 0 3.4rem;
} }
@@ -354,22 +361,66 @@
.site-footer { .site-footer {
background: linear-gradient(135deg, var(--brand-strong) 0%, #0a4c74 100%); background: linear-gradient(135deg, var(--brand-strong) 0%, #0a4c74 100%);
color: #ffffff; color: #ffffff;
margin-top: 4rem; margin-top: auto;
padding: 3rem 0 1.5rem; padding: 3rem 0 1.5rem;
border-top: 1px solid var(--line); border-top: 1px solid var(--line);
position: relative;
overflow: hidden;
animation: footerEnter 0.7s ease both;
will-change: transform, opacity;
transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.45s ease, box-shadow 0.45s ease;
}
body.footer-scroll-mode .site-footer {
opacity: 0.9;
transform: translateY(26px);
box-shadow: 0 -8px 22px rgba(5, 27, 41, 0.14);
}
body.footer-scroll-mode.footer-reveal .site-footer {
opacity: 1;
transform: translateY(0);
box-shadow: 0 -14px 34px rgba(5, 27, 41, 0.2);
}
.site-footer::before {
content: "";
position: absolute;
inset: -40% -10% auto -10%;
height: 180px;
background: radial-gradient(circle at 20% 50%, rgba(242, 140, 40, 0.25) 0%, rgba(242, 140, 40, 0) 62%);
transform: translateX(-12%);
animation: footerGlow 7s ease-in-out infinite;
pointer-events: none;
} }
.footer-inner { .footer-inner {
position: relative;
z-index: 1;
display: grid;
gap: 2rem;
margin-bottom: 2rem;
}
.footer-content {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem; gap: 2rem;
margin-bottom: 2rem;
} }
.footer-section { .footer-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 0.75rem;
animation: footerSectionRise 0.65s ease both;
}
.footer-section:nth-child(2) {
animation-delay: 0.08s;
}
.footer-section:nth-child(3) {
animation-delay: 0.16s;
} }
.footer-section h4 { .footer-section h4 {
@@ -395,11 +446,12 @@
.footer-links a { .footer-links a {
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
font-size: 0.95rem; font-size: 0.95rem;
transition: color 0.2s ease; transition: color 0.2s ease, transform 0.2s ease;
} }
.footer-links a:hover { .footer-links a:hover {
color: #ffffff; color: #ffffff;
transform: translateX(4px);
} }
.footer-bottom { .footer-bottom {
@@ -414,8 +466,63 @@
margin: 0; margin: 0;
} }
@keyframes footerEnter {
from {
opacity: 0;
transform: translateY(22px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes footerSectionRise {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes footerGlow {
0%,
100% {
transform: translateX(-12%);
opacity: 0.65;
}
50% {
transform: translateX(12%);
opacity: 0.95;
}
}
@media (prefers-reduced-motion: reduce) {
.site-footer,
.site-footer::before,
.footer-section,
.footer-links a {
animation: none;
transition: none;
}
body.footer-scroll-mode .site-footer,
body.footer-scroll-mode.footer-reveal .site-footer {
opacity: 1;
transform: none;
box-shadow: none;
}
}
@media (max-width: 700px) { @media (max-width: 700px) {
.footer-inner { .footer-inner {
gap: 1.5rem;
}
.footer-content {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 1.5rem; gap: 1.5rem;
} }
@@ -425,5 +532,57 @@
<!-- Mobile compatibility scripts --> <!-- Mobile compatibility scripts -->
<script src="{{ url_for('static', filename='js/mobile_compatibility.js') }}"></script> <script src="{{ url_for('static', filename='js/mobile_compatibility.js') }}"></script>
<script src="{{ url_for('static', filename='js/ios_fixes.js') }}"></script> <script src="{{ url_for('static', filename='js/ios_fixes.js') }}"></script>
<script>
(function () {
var footer = document.querySelector(".site-footer");
if (!footer) {
return;
}
var body = document.body;
var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
var ticking = false;
function updateFooterMode() {
ticking = false;
var maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
body.classList.remove("footer-scroll-mode", "footer-reveal");
if (reduceMotion.matches || maxScroll < 140) {
return;
}
body.classList.add("footer-scroll-mode");
var currentScroll = window.scrollY || window.pageYOffset || 0;
var distanceToBottom = maxScroll - currentScroll;
var progress = maxScroll > 0 ? currentScroll / maxScroll : 1;
var shouldReveal = progress >= 0.78 || distanceToBottom <= 320;
body.classList.toggle("footer-reveal", shouldReveal);
}
function requestUpdate() {
if (ticking) {
return;
}
ticking = true;
window.requestAnimationFrame(updateFooterMode);
}
window.addEventListener("scroll", requestUpdate, { passive: true });
window.addEventListener("resize", requestUpdate);
window.addEventListener("load", requestUpdate);
if (typeof reduceMotion.addEventListener === "function") {
reduceMotion.addEventListener("change", requestUpdate);
} else if (typeof reduceMotion.addListener === "function") {
reduceMotion.addListener(requestUpdate);
}
requestUpdate();
})();
</script>
</body> </body>
</html> </html>
-14
View File
@@ -17,17 +17,6 @@
<p><strong>Lizenzschlüssel:</strong> {{ item.license_key }}</p> <p><strong>Lizenzschlüssel:</strong> {{ item.license_key }}</p>
<p><strong>Status:</strong> {{ item.status }}</p> <p><strong>Status:</strong> {{ item.status }}</p>
<p><strong>Gültig bis:</strong> {{ item.valid_until }}</p> <p><strong>Gültig bis:</strong> {{ item.valid_until }}</p>
<form method="post" class="transfer-form">
<input type="hidden" name="action" value="transfer">
<input type="hidden" name="license_id" value="{{ item.id }}">
<select name="target_username" required>
<option value="">Lizenz weitergeben an...</option>
{% for user in transfer_users %}
<option value="{{ user.username }}">{{ user.username }} ({{ user.display_name }})</option>
{% endfor %}
</select>
<button type="submit">Weitergeben</button>
</form>
</article> </article>
{% endfor %} {% endfor %}
{% else %} {% else %}
@@ -43,8 +32,5 @@
.grid { display: grid; gap: 0.8rem; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); } .grid { display: grid; gap: 0.8rem; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.card { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; } .card { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; }
.card p { margin-top: 0.35rem; } .card p { margin-top: 0.35rem; }
.transfer-form { margin-top: 0.8rem; display: grid; gap: 0.45rem; }
select, button { border: 1px solid #c9d8e3; border-radius: 10px; padding: 0.52rem; font: inherit; }
button { border-radius: 999px; color: #fff; background: linear-gradient(120deg, #0c5a86 0%, #08486c 100%); border-color: #0a4c74; font-weight: 700; }
</style> </style>
{% endblock %} {% endblock %}
+12 -12
View File
@@ -43,7 +43,7 @@ def check_nm_pwd(username, password):
dict: User document if credentials are valid, None otherwise dict: User document if credentials are valid, None otherwise
""" """
client = MongoClient('localhost', 27017) client = MongoClient('localhost', 27017)
db = client['Inventarsystem'] db = client['Invario_Website']
users = db['users'] users = db['users']
hashed_password = hashlib.sha512(password.encode()).hexdigest() hashed_password = hashlib.sha512(password.encode()).hexdigest()
user = users.find_one({'Username': username, 'Password': hashed_password}) user = users.find_one({'Username': username, 'Password': hashed_password})
@@ -63,7 +63,7 @@ def add_user(username, password, name, last_name):
bool: True if user was added successfully, False if password was too weak bool: True if user was added successfully, False if password was too weak
""" """
client = MongoClient('localhost', 27017) client = MongoClient('localhost', 27017)
db = client['Inventarsystem'] db = client['Invario_Website']
users = db['users'] users = db['users']
if not check_password_strength(password): if not check_password_strength(password):
return False return False
@@ -83,7 +83,7 @@ def make_admin(username):
bool: True if user was promoted successfully bool: True if user was promoted successfully
""" """
client = MongoClient('localhost', 27017) client = MongoClient('localhost', 27017)
db = client['Inventarsystem'] db = client['Invario_Website']
users = db['users'] users = db['users']
users.update_one({'Username': username}, {'$set': {'Admin': True}}) users.update_one({'Username': username}, {'$set': {'Admin': True}})
client.close() client.close()
@@ -100,7 +100,7 @@ def remove_admin(username):
bool: True if user was demoted successfully bool: True if user was demoted successfully
""" """
client = MongoClient('localhost', 27017) client = MongoClient('localhost', 27017)
db = client['Inventarsystem'] db = client['Invario_Website']
users = db['users'] users = db['users']
users.update_one({'Username': username}, {'$set': {'Admin': False}}) users.update_one({'Username': username}, {'$set': {'Admin': False}})
client.close() client.close()
@@ -117,7 +117,7 @@ def get_user(username):
dict: User document or None if not found dict: User document or None if not found
""" """
client = MongoClient('localhost', 27017) client = MongoClient('localhost', 27017)
db = client['Inventarsystem'] db = client['Invario_Website']
users = db['users'] users = db['users']
users_return = users.find_one({'Username': username}) users_return = users.find_one({'Username': username})
client.close() client.close()
@@ -135,7 +135,7 @@ def check_admin(username):
bool: True if user is an administrator, False otherwise bool: True if user is an administrator, False otherwise
""" """
client = MongoClient('localhost', 27017) client = MongoClient('localhost', 27017)
db = client['Inventarsystem'] db = client['Invario_Website']
users = db['users'] users = db['users']
user = users.find_one({'Username': username}) user = users.find_one({'Username': username})
client.close() client.close()
@@ -153,7 +153,7 @@ def delete_user(username):
bool: True if user was deleted successfully, False otherwise bool: True if user was deleted successfully, False otherwise
""" """
client = MongoClient('localhost', 27017) client = MongoClient('localhost', 27017)
db = client['Inventarsystem'] db = client['Invario_Website']
users = db['users'] users = db['users']
result = users.delete_one({'username': username}) result = users.delete_one({'username': username})
client.close() client.close()
@@ -175,7 +175,7 @@ def get_name(username):
str: String of name str: String of name
""" """
client = MongoClient('localhost', 27017) client = MongoClient('localhost', 27017)
db = client['Inventarsystem'] db = client['Invario_Website']
users = db['users'] users = db['users']
user = users.find_one({'Username': username}) user = users.find_one({'Username': username})
name = user.get("name") name = user.get("name")
@@ -189,7 +189,7 @@ def get_last_name(username):
str: String of last_name str: String of last_name
""" """
client = MongoClient('localhost', 27017) client = MongoClient('localhost', 27017)
db = client['Inventarsystem'] db = client['Invario_Website']
users = db['users'] users = db['users']
user = users.find_one({'Username': username}) user = users.find_one({'Username': username})
name = user.get("last_name") name = user.get("last_name")
@@ -206,7 +206,7 @@ def get_all_users():
""" """
try: try:
client = MongoClient('localhost', 27017) client = MongoClient('localhost', 27017)
db = client['Inventarsystem'] db = client['Invario_Website']
users = db['users'] users = db['users']
all_users = list(users.find()) all_users = list(users.find())
client.close() client.close()
@@ -230,7 +230,7 @@ def update_password(username, new_password):
return False return False
client = MongoClient('localhost', 27017) client = MongoClient('localhost', 27017)
db = client['Inventarsystem'] db = client['Invario_Website']
users = db['users'] users = db['users']
# Hash the new password # Hash the new password
@@ -262,7 +262,7 @@ def update_user_name(username, name, last_name):
""" """
try: try:
client = MongoClient('localhost', 27017) client = MongoClient('localhost', 27017)
db = client['Inventarsystem'] db = client['Invario_Website']
users = db['users'] users = db['users']
result = users.update_one( result = users.update_one(