new file: .env.example

new file:   .gitignore
	new file:   Dockerfile
	new file:   README.md
	new file:   app.py
	new file:   auth.py
	new file:   config.py
	new file:   db.py
	new file:   db_helpers.py
	new file:   docker-compose.yml
	new file:   requirements.txt
	new file:   routes/__init__.py
	new file:   routes/admin_routes.py
	new file:   routes/auth_routes.py
	new file:   routes/journal_routes.py
	new file:   static/css/style.css
	new file:   static/js/main.js
	new file:   templates/admin/dashboard.html
	new file:   templates/admin/users.html
	new file:   templates/base.html
	new file:   templates/day.html
	new file:   templates/future.html
	new file:   templates/index.html
	new file:   templates/login.html
	new file:   templates/week.html
This commit is contained in:
SimolZimol
2026-08-02 20:41:24 +02:00
commit 0e7efe4b74
25 changed files with 1590 additions and 0 deletions

325
routes/journal_routes.py Normal file
View File

@@ -0,0 +1,325 @@
"""Journal-Routen: Index/Dashboard, Future Log, Wochenübersicht, Tagesansicht.
Enthält auch Helfer für Wochenberechnung und Notiz-Aktionen
(erledigt markieren, in anderen Tag / Future Log verschieben).
"""
from datetime import date, datetime, timedelta
from flask import Blueprint, render_template, request, redirect, url_for, session, flash
from auth import login_required
from db import get_connection
journal_bp = Blueprint("journal", __name__)
# Deutsche Wochentage / Monate
WEEKDAYS = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
MONTHS = [
"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember",
]
BULLET_LABELS = {
".": "Aufgabe",
"x": "Termin",
"-": "Notiz",
"?": "Frage",
}
def _user_id() -> int:
return session["user_id"]
def monday_of_week(d: date) -> date:
"""Liefert den Montag der Woche, in der d liegt."""
return d - timedelta(days=d.weekday())
def get_week_days(monday: date) -> list[date]:
return [monday + timedelta(days=i) for i in range(7)]
def parse_date(s: str) -> date | None:
try:
return datetime.strptime(s, "%Y-%m-%d").date()
except (ValueError, TypeError):
return None
def _fetch_notes(user_id: int, note_date: date) -> list[dict]:
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT * FROM notes WHERE user_id = %s AND note_date = %s "
"ORDER BY id ASC",
(user_id, note_date),
)
return cur.fetchall()
def _active_notes(user_id: int, note_date: date) -> list[dict]:
return [n for n in _fetch_notes(user_id, note_date) if n["status"] != "moved"]
@journal_bp.route("/")
@login_required
def index():
"""Dashboard: aktuelle Woche + offene Aufgaben."""
today = date.today()
monday = monday_of_week(today)
week_days = get_week_days(monday)
# Detaillierte Info pro Tag
day_summary = []
for d in week_days:
notes = _active_notes(_user_id(), d)
day_summary.append(
{
"date": d,
"weekday": WEEKDAYS[d.weekday()],
"open": sum(1 for n in notes if n["status"] == "open"),
"done": sum(1 for n in notes if n["status"] == "done"),
"total": len(notes),
}
)
# Offene Aufgaben außerhalb dieser Woche
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT * FROM notes WHERE user_id = %s AND status = 'open' "
"AND note_date < %s ORDER BY note_date ASC LIMIT 50",
(_user_id(), monday),
)
overdue = cur.fetchall()
return render_template(
"index.html",
today=today,
monday=monday,
day_summary=day_summary,
overdue=overdue,
weekdays=WEEKDAYS,
)
@journal_bp.route("/future", methods=["GET", "POST"])
@login_required
def future():
if request.method == "POST":
note_text = request.form.get("note_text", "").strip()
month_str = request.form.get("month", "")
month = parse_date(month_str)
if note_text and month:
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"INSERT INTO future_log (user_id, month_date, note_text) "
"VALUES (%s, %s, %s)",
(_user_id(), month, note_text),
)
flash("Eintrag ins Future Log hinzugefügt.", "success")
else:
flash("Bitte Text und Monat angeben.", "danger")
return redirect(url_for("journal.future"))
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT * FROM future_log WHERE user_id = %s ORDER BY month_date ASC, id ASC",
(_user_id(),),
)
entries = cur.fetchall()
# Gruppieren nach Monat
grouped = {}
for e in entries:
key = e["month_date"].strftime("%Y-%m")
grouped.setdefault(key, []).append(e)
return render_template("future.html", grouped=grouped, months=MONTHS)
@journal_bp.route("/woche")
@login_required
def week():
"""Wochenübersicht: MonSo mit Status je Tag + offene Aufgaben."""
week_param = request.args.get("week", "")
if week_param:
try:
iso_year, iso_week = week_param.split("-W")
monday = date.fromisocalendar(int(iso_year), int(iso_week), 1)
except (ValueError, TypeError):
monday = monday_of_week(date.today())
else:
monday = monday_of_week(date.today())
week_days = get_week_days(monday)
days_detail = []
for d in week_days:
notes = _active_notes(_user_id(), d)
days_detail.append(
{
"date": d,
"weekday": WEEKDAYS[d.weekday()],
"open": sum(1 for n in notes if n["status"] == "open"),
"done": sum(1 for n in notes if n["status"] == "done"),
"moved": sum(1 for n in notes if n["status"] == "moved"),
"total": len(notes),
}
)
# Offene Aufgaben der ausgewählten Woche
sunday = monday + timedelta(days=6)
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT * FROM notes WHERE user_id = %s AND status = 'open' "
"AND note_date BETWEEN %s AND %s ORDER BY note_date ASC, id ASC",
(_user_id(), monday, sunday),
)
open_notes = cur.fetchall()
prev_monday = monday - timedelta(days=7)
next_monday = monday + timedelta(days=7)
return render_template(
"week.html",
monday=monday,
days_detail=days_detail,
open_notes=open_notes,
weekdays=WEEKDAYS,
prev_monday=prev_monday,
next_monday=next_monday,
)
@journal_bp.route("/tag/<date_str>", methods=["GET", "POST"])
@login_required
def day(date_str: str):
"""Tagesansicht mit 3 Spalten (Bullet | Notiz | Priorität)."""
target = parse_date(date_str)
if target is None:
flash("Ungültiges Datum.", "danger")
return redirect(url_for("journal.index"))
if request.method == "POST":
bullet = request.form.get("bullet", "-")
note_text = request.form.get("note_text", "").strip()
priority = request.form.get("priority", "0")
try:
priority = int(priority)
except ValueError:
priority = 0
if bullet not in (".", "x", "-", "?"):
bullet = "-"
if note_text:
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"INSERT INTO notes (user_id, note_date, bullet_type, "
"note_text, priority) VALUES (%s, %s, %s, %s, %s)",
(_user_id(), target, bullet, note_text, priority),
)
flash("Notiz hinzugefügt.", "success")
else:
flash("Notiz darf nicht leer sein.", "danger")
return redirect(url_for("journal.day", date_str=date_str))
notes = _active_notes(_user_id(), target)
prev = target - timedelta(days=1)
nxt = target + timedelta(days=1)
return render_template(
"day.html",
target=target,
weekday=WEEKDAYS[target.weekday()],
notes=notes,
bullet_labels=BULLET_LABELS,
prev=prev,
nxt=nxt,
)
@journal_bp.route("/note/<int:note_id>/mark", methods=["POST"])
@login_required
def mark_done(note_id: int):
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT * FROM notes WHERE id = %s AND user_id = %s",
(note_id, _user_id()),
)
note = cur.fetchone()
if not note:
flash("Notiz nicht gefunden.", "danger")
return redirect(url_for("journal.index"))
new_status = "done" if note["status"] != "done" else "open"
cur.execute(
"UPDATE notes SET status = %s WHERE id = %s",
(new_status, note_id),
)
flash("Status aktualisiert.", "success")
return redirect(request.referrer or url_for("journal.index"))
@journal_bp.route("/note/<int:note_id>/move", methods=["POST"])
@login_required
def move_note(note_id: int):
"""Verschiebt eine Notiz in einen anderen Tag oder ins Future Log.
Formularfelder: target_date (YYYY-MM-DD) oder target=future
"""
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT * FROM notes WHERE id = %s AND user_id = %s",
(note_id, _user_id()),
)
note = cur.fetchone()
if not note:
flash("Notiz nicht gefunden.", "danger")
return redirect(url_for("journal.index"))
target = request.form.get("target", "")
if target == "future":
# In Future Log übernehmen
cur.execute(
"INSERT INTO future_log (user_id, month_date, note_text) "
"VALUES (%s, %s, %s)",
(_user_id(), note["note_date"], note["note_text"]),
)
cur.execute(
"UPDATE notes SET status = 'moved', moved_to_future = 1 "
"WHERE id = %s",
(note_id,),
)
flash("In Future Log verschoben.", "success")
else:
target_date = parse_date(target)
if target_date:
cur.execute(
"UPDATE notes SET status = 'moved', moved_to_date = %s "
"WHERE id = %s",
(target_date, note_id),
)
# Am Zieltag als offene Notiz neu anlegen
cur.execute(
"INSERT INTO notes (user_id, note_date, bullet_type, "
"note_text, priority) VALUES (%s, %s, %s, %s, %s)",
(
_user_id(),
target_date,
note["bullet_type"],
note["note_text"],
note["priority"],
),
)
flash(f"Verschoben nach {target_date}.", "success")
else:
flash("Kein gültiges Ziel angegeben.", "danger")
return redirect(request.referrer or url_for("journal.index"))