"""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: Mon–So 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/", 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//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//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"))