Files
Journal/routes/journal_routes.py
SimolZimol 208d0c0c54 modified: routes/journal_routes.py
modified:   templates/edit_future.html
	modified:   templates/future.html
	modified:   templates/report.html
2026-08-02 21:52:46 +02:00

946 lines
32 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Journal-Routen: Index/Dashboard, Future Log, Wochen-/Monatsübersicht, Tagesansicht.
Enthält auch Helfer für Wochen-/Monatsberechnung, Export (CSV) und
Notiz-Aktionen (erledigt markieren, in anderen Tag / Future Log verschieben).
"""
import csv
import io
import json
import calendar
from datetime import date, datetime, timedelta
from urllib.parse import urlencode
from flask import (
Blueprint, render_template, request, redirect, url_for,
session, flash, Response,
)
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 parse_month(s: str) -> date | None:
"""Akzeptiert 'YYYY-MM' oder 'YYYY-MM-DD'; liefert den 1. des Monats."""
s = (s or "").strip()
if not s:
return None
try:
if len(s) == 7: # YYYY-MM
y, mo = int(s[:4]), int(s[5:7])
return date(y, mo, 1)
d = parse_date(s) # YYYY-MM-DD
return d.replace(day=1) if d else None
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"]
def parse_refs(raw) -> list[dict]:
"""Parst den JSON-Spaltenwert der Verweise in eine Liste."""
if not raw:
return []
if isinstance(raw, (list, dict)):
return raw if isinstance(raw, list) else [raw]
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except (ValueError, TypeError):
return []
REF_TYPE_DISPLAY = {
"email": "📧",
"link": "🔗",
"phone": "📞",
"mention": "@",
}
def ref_type_label(ref_type: str) -> str:
return REF_TYPE_DISPLAY.get(ref_type, "🔖")
def ref_href(ref_type: str, value: str) -> str:
value = (value or "").strip()
if ref_type == "email":
return f"mailto:{value}"
if ref_type == "phone":
return f"tel:{value.replace(' ', '')}"
if ref_type == "mention":
return f"mailto:{value}"
# link und Standard
if value.startswith(("http://", "https://")):
return value
return f"https://{value}"
def bullet_label(bullet_type: str) -> str:
return BULLET_LABELS.get(bullet_type, "Notiz")
PRIORITY_LABELS = {0: "Niedrig", 1: "Mittel", 2: "Hoch"}
STATUS_LABELS = {"open": "Offen", "done": "Erledigt", "moved": "Verschoben"}
def _report_filters() -> dict:
"""Liest die Report-Filter aus request.args (GET-Formular/Export-Links)."""
args = request.args
range_type = args.get("range", "week")
if range_type == "month":
month_param = args.get("month", "")
start = parse_month(month_param) or date.today().replace(day=1)
end = date(start.year, start.month,
calendar.monthrange(start.year, start.month)[1])
label = start.strftime("%B %Y")
elif range_type == "custom":
start = parse_date(args.get("from", ""))
end = parse_date(args.get("to", ""))
if not start or not end or end < start:
start = monday_of_week(date.today())
end = start + timedelta(days=6)
label = f"{start.strftime('%d.%m.%Y')} {end.strftime('%d.%m.%Y')}"
range_type = "custom"
else: # week
week_param = args.get("week", "")
try:
iso_year, iso_week = week_param.split("-W")
start = date.fromisocalendar(int(iso_year), int(iso_week), 1)
except (ValueError, TypeError):
start = monday_of_week(date.today())
end = start + timedelta(days=6)
label = f"Woche {start.strftime('%d.%m.%Y')} {end.strftime('%d.%m.%Y')}"
# Filter (leere Auswahl = alle zulassen)
weekdays = [int(x) for x in args.getlist("wd")]
bullets = [b for b in args.getlist("bullet") if b in BULLET_LABELS]
statuses = [s for s in args.getlist("status") if s in STATUS_LABELS]
priorities = [int(p) for p in args.getlist("priority") if p.isdigit()]
if not weekdays:
weekdays = list(range(7))
if not bullets:
bullets = list(BULLET_LABELS.keys())
if not statuses:
statuses = ["open", "done"]
if not priorities:
priorities = [0, 1, 2]
return {
"range_type": range_type,
"start": start,
"end": end,
"label": label,
"weekdays": weekdays,
"bullets": bullets,
"statuses": statuses,
"priorities": priorities,
}
def _run_report(uid: int, f: dict) -> tuple[list[dict], dict]:
"""Führt den Report aus und liefert (rows, summary)."""
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT * FROM notes WHERE user_id = %s "
"AND note_date BETWEEN %s AND %s "
"ORDER BY note_date ASC, id ASC",
(uid, f["start"], f["end"]),
)
rows = cur.fetchall()
filtered = []
for n in rows:
if n["note_date"].weekday() not in f["weekdays"]:
continue
if n["bullet_type"] not in f["bullets"]:
continue
if n["status"] not in f["statuses"]:
continue
if n["priority"] not in f["priorities"]:
continue
filtered.append(n)
rows_out = []
for n in filtered:
refs = ", ".join(
f"{ref_type_label(r.get('type', ''))} {r.get('value', '')}"
for r in parse_refs(n.get("refs"))
)
rows_out.append({
"id": n["id"],
"date_str": n["note_date"].strftime("%d.%m.%Y"),
"iso": n["note_date"].strftime("%Y-%m-%d"),
"weekday": WEEKDAYS[n["note_date"].weekday()],
"bullet": n["bullet_type"],
"bullet_label": bullet_label(n["bullet_type"]),
"note_text": n["note_text"],
"priority": PRIORITY_LABELS.get(n.get("priority", 0), str(n.get("priority", ""))),
"status": STATUS_LABELS.get(n.get("status"), n.get("status", "")),
"status_code": n.get("status"),
"refs": refs,
})
summary = {
"total": len(rows_out),
"open": sum(1 for r in rows_out if r["status_code"] == "open"),
"done": sum(1 for r in rows_out if r["status_code"] == "done"),
}
return rows_out, summary
@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_month(month_str)
ref_type = request.form.get("ref_type", "").strip()
ref_value = request.form.get("ref_value", "").strip()
refs = None
if ref_value:
refs = json.dumps([{"type": ref_type or "link", "value": ref_value}])
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, refs) "
"VALUES (%s, %s, %s, %s)",
(_user_id(), month, note_text, refs),
)
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 (mit Zählern)
grouped = {}
for e in entries:
key = e["month_date"].strftime("%Y-%m")
grouped.setdefault(key, []).append(e)
month_stats = {}
for key, items in grouped.items():
month_stats[key] = {
"total": len(items),
"open": sum(1 for i in items if i["status"] == "open"),
"done": sum(1 for i in items if i["status"] == "done"),
}
today = date.today()
return render_template(
"future.html",
grouped=grouped,
month_stats=month_stats,
months=MONTHS,
ref_types=list(REF_TYPE_DISPLAY.keys()),
today_str=today.strftime("%Y-%m"),
)
@journal_bp.route("/future/<int:entry_id>/toggle", methods=["POST"])
@login_required
def toggle_future(entry_id: int):
"""Schaltet einen Future-Log-Eintrag zwischen offen und erledigt um."""
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT status FROM future_log WHERE id = %s AND user_id = %s",
(entry_id, _user_id()),
)
entry = cur.fetchone()
if not entry:
flash("Eintrag nicht gefunden.", "danger")
return redirect(url_for("journal.future"))
new_status = "done" if entry["status"] != "done" else "open"
cur.execute(
"UPDATE future_log SET status = %s WHERE id = %s AND user_id = %s",
(new_status, entry_id, _user_id()),
)
flash("Status aktualisiert.", "success")
return redirect(url_for("journal.future"))
@journal_bp.route("/woche")
@login_required
def week():
"""Wochenübersicht: MonSo mit allen Einzeleinträgen pro Tag."""
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)
sunday = monday + timedelta(days=6)
# Alle Notizen der Woche holen (inkl. erledigter) und nach Tag gruppieren
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT * FROM notes WHERE user_id = %s "
"AND note_date BETWEEN %s AND %s ORDER BY note_date ASC, id ASC",
(_user_id(), monday, sunday),
)
rows = cur.fetchall()
by_date: dict[date, list[dict]] = {d: [] for d in week_days}
for n in rows:
by_date.setdefault(n["note_date"], []).append(n)
days_detail = []
for d in week_days:
notes = by_date.get(d, [])
active = [n for n in notes if n["status"] != "moved"]
days_detail.append(
{
"date": d,
"weekday": WEEKDAYS[d.weekday()],
"open": sum(1 for n in active if n["status"] == "open"),
"done": sum(1 for n in active if n["status"] == "done"),
"moved": sum(1 for n in notes if n["status"] == "moved"),
"total": len(active),
"notes": active,
}
)
prev_monday = monday - timedelta(days=7)
next_monday = monday + timedelta(days=7)
return render_template(
"week.html",
monday=monday,
sunday=sunday,
days_detail=days_detail,
weekdays=WEEKDAYS,
prev_monday=prev_monday,
next_monday=next_monday,
bullet_labels=BULLET_LABELS,
)
@journal_bp.route("/monat")
@login_required
def month():
"""Monatsübersicht: Kalenderraster + Liste aller Einträge des Monats."""
month_param = request.args.get("month", "")
if month_param:
try:
year, mon = (int(x) for x in month_param.split("-"))
first = date(year, mon, 1)
except (ValueError, TypeError):
first = date.today().replace(day=1)
else:
first = date.today().replace(day=1)
last = date(first.year, first.month, calendar.monthrange(first.year, first.month)[1])
# Alle Notizen des Monats holen
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT * FROM notes WHERE user_id = %s "
"AND note_date BETWEEN %s AND %s ORDER BY note_date ASC, id ASC",
(_user_id(), first, last),
)
rows = cur.fetchall()
by_date: dict[int, list[dict]] = {}
for n in rows:
if n["status"] != "moved":
by_date.setdefault(n["note_date"].day, []).append(n)
# Kalenderraster aufbauen (Wochen MontagSonntag)
start_pad = first.weekday() # 0=Montag
start = first - timedelta(days=start_pad)
weeks: list[list[dict]] = []
current = start
for _ in range(6):
week_cells = []
for i in range(7):
d = current + timedelta(days=i)
in_month = d.month == first.month
week_cells.append(
{
"date": d,
"in_month": in_month,
"today": d == date.today(),
"notes": by_date.get(d.day, []) if in_month else [],
}
)
weeks.append(week_cells)
current += timedelta(days=7)
if current > last + timedelta(days=6):
break
now_first = date.today().replace(day=1)
prev_month = (first.replace(day=1) - timedelta(days=1)).replace(day=1)
next_month = (first.replace(day=28) + timedelta(days=7)).replace(day=1)
return render_template(
"monat.html",
first=first,
last=last,
weeks=weeks,
weekdays=WEEKDAYS,
prev_month=prev_month,
next_month=next_month,
all_notes=rows,
)
def _csv_response(title: str, filename: str, rows: list[dict]) -> Response:
"""Baut eine CSV-Datei als HTTP-Antwort zusammen.
Trennzeichen: Semikolon (deutsches Excel erwartet ';' statt Komma).
"""
output = io.StringIO()
writer = csv.writer(output, delimiter=";")
writer.writerow([
"Datum", "Wochentag", "Bullet", "Typ", "Notiz", "Priorität",
"Status", "Verweise",
])
for n in rows:
refs = ", ".join(
f"{ref_type_label(r.get('type', ''))} {r.get('value', '')}"
for r in parse_refs(n.get("refs"))
)
writer.writerow([
n["note_date"].strftime("%d.%m.%Y"),
WEEKDAYS[n["note_date"].weekday()],
n.get("bullet_type", ""),
bullet_label(n.get("bullet_type", "-")),
n.get("note_text", ""),
n.get("priority", 0),
{"open": "Offen", "done": "Erledigt", "moved": "Verschoben"}.get(
n.get("status"), n.get("status", "")
),
refs,
])
output.seek(0)
return Response(
"\ufeff" + output.getvalue(), # BOM für Excel
mimetype="text/csv; charset=utf-8",
headers={"Content-Disposition": f"attachment; filename={filename}.csv"},
)
@journal_bp.route("/woche/export")
@login_required
def export_week():
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())
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 note_date BETWEEN %s AND %s ORDER BY note_date ASC, id ASC",
(_user_id(), monday, sunday),
)
rows = cur.fetchall()
filename = f"woche_{monday.strftime('%Y-%m-%d')}"
return _csv_response("Wochenübersicht", filename, rows)
@journal_bp.route("/monat/export")
@login_required
def export_month():
month_param = request.args.get("month", "")
if month_param:
try:
year, mon = (int(x) for x in month_param.split("-"))
first = date(year, mon, 1)
except (ValueError, TypeError):
first = date.today().replace(day=1)
else:
first = date.today().replace(day=1)
last = date(first.year, first.month, calendar.monthrange(first.year, first.month)[1])
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT * FROM notes WHERE user_id = %s "
"AND note_date BETWEEN %s AND %s ORDER BY note_date ASC, id ASC",
(_user_id(), first, last),
)
rows = cur.fetchall()
filename = f"monat_{first.strftime('%Y-%m')}"
return _csv_response("Monatsübersicht", filename, rows)
@journal_bp.route("/report")
@login_required
def report():
"""Report-Generator: dynamischer Wochen-/Monatsbericht mit Filtern."""
filters = _report_filters()
rows, summary = _run_report(_user_id(), filters)
today = date.today()
# Für Export-Links: aktuelle Filter als Query-String aufbauen
# (urlencode erhält wiederholte Parameter wie wd=0&wd=1 dict() würde sie kollabieren)
def _qs(fmt):
params = [("range", filters["range_type"]), ("format", fmt)]
params += [("wd", str(wd)) for wd in filters["weekdays"]]
params += [("bullet", b) for b in filters["bullets"]]
params += [("status", s) for s in filters["statuses"]]
params += [("priority", str(p)) for p in filters["priorities"]]
if filters["range_type"] == "month":
params.append(("month", filters["start"].strftime("%Y-%m")))
elif filters["range_type"] == "custom":
params.append(("from", filters["start"].strftime("%Y-%m-%d")))
params.append(("to", filters["end"].strftime("%Y-%m-%d")))
else:
params.append(("week", filters["start"].strftime("%G-W%V")))
return url_for("journal.report_export") + "?" + urlencode(params)
export_csv_url = _qs("csv")
export_pdf_url = _qs("pdf")
current_week = monday_of_week(today).strftime("%G-W%V")
current_month = today.strftime("%Y-%m")
current_month_date = today.strftime("%Y-%m-%d")
filters["week_value"] = current_week
filters["from_value"] = filters["start"].strftime("%Y-%m-%d") if filters["range_type"] == "custom" else ""
filters["to_value"] = filters["end"].strftime("%Y-%m-%d") if filters["range_type"] == "custom" else ""
filters["month_value"] = (
filters["start"].strftime("%Y-%m-%d")
if filters["range_type"] == "month"
else current_month_date
)
return render_template(
"report.html",
f=filters,
rows=rows,
summary=summary,
weekdays=WEEKDAYS,
bullet_labels=BULLET_LABELS,
current_week=current_week,
current_month=current_month,
today_str=today.strftime("%Y-%m-%d"),
export_csv_url=export_csv_url,
export_pdf_url=export_pdf_url,
)
@journal_bp.route("/report/export")
@login_required
def report_export():
"""Export des Report-Generators als CSV oder PDF (format=pdf|csv)."""
filters = _report_filters()
rows, summary = _run_report(_user_id(), filters)
fmt = request.args.get("format", "csv")
safe_name = filters["label"].replace(" ", "_").replace("", "-")
filename = f"report_{safe_name}"
if fmt == "pdf":
from pdf_export import build_pdf
pdf_bytes = build_pdf(
title="Bullet-Journal-Report",
subtitle=filters["label"],
rows=rows,
summary=summary,
)
return Response(
pdf_bytes,
mimetype="application/pdf",
headers={
"Content-Disposition": f"attachment; filename={filename}.pdf"
},
)
# CSV Semikolon als Trennzeichen (deutsches Excel erwartet ';')
output = io.StringIO()
writer = csv.writer(output, delimiter=";")
writer.writerow([
"Datum", "Wochentag", "Bullet", "Typ", "Notiz", "Priorität",
"Status", "Verweise",
])
for r in rows:
writer.writerow([
r["date_str"], r["weekday"], r["bullet"], r["bullet_label"],
r["note_text"], r["priority"], r["status"], r["refs"],
])
output.seek(0)
return Response(
"\ufeff" + output.getvalue(),
mimetype="text/csv; charset=utf-8",
headers={"Content-Disposition": f"attachment; filename={filename}.csv"},
)
@journal_bp.route("/note/<int:note_id>/edit", methods=["GET", "POST"])
@login_required
def edit_note(note_id: int):
"""Bearbeitet eine Notiz (Text, Bullet, Priorität, Verweise)."""
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"))
if request.method == "POST":
bullet = request.form.get("bullet", "-")
note_text = request.form.get("note_text", "").strip()
try:
priority = int(request.form.get("priority", "0"))
except ValueError:
priority = 0
ref_type = request.form.get("ref_type", "").strip()
ref_value = request.form.get("ref_value", "").strip()
refs = None
if ref_value:
refs = json.dumps([{"type": ref_type or "link", "value": ref_value}])
if bullet not in (".", "x", "-", "?"):
bullet = "-"
if not note_text:
flash("Notiz darf nicht leer sein.", "danger")
else:
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"UPDATE notes SET bullet_type = %s, note_text = %s, "
"priority = %s, refs = %s WHERE id = %s AND user_id = %s",
(bullet, note_text, priority, refs, note_id, _user_id()),
)
flash("Notiz aktualisiert.", "success")
return redirect(url_for("journal.day", date_str=note["note_date"].strftime("%Y-%m-%d")))
refs = parse_refs(note.get("refs"))
current_ref = refs[0] if refs else {}
return render_template(
"edit_note.html",
note=note,
bullet_labels=BULLET_LABELS,
current_ref=current_ref,
)
@journal_bp.route("/note/<int:note_id>/delete", methods=["POST"])
@login_required
def delete_note(note_id: int):
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT note_date 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"))
cur.execute(
"DELETE FROM notes WHERE id = %s AND user_id = %s",
(note_id, _user_id()),
)
flash("Notiz gelöscht.", "success")
return redirect(request.referrer or url_for("journal.index"))
@journal_bp.route("/future/<int:entry_id>/edit", methods=["GET", "POST"])
@login_required
def edit_future(entry_id: int):
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT * FROM future_log WHERE id = %s AND user_id = %s",
(entry_id, _user_id()),
)
entry = cur.fetchone()
if not entry:
flash("Eintrag nicht gefunden.", "danger")
return redirect(url_for("journal.future"))
if request.method == "POST":
note_text = request.form.get("note_text", "").strip()
month_str = request.form.get("month", "")
month = parse_month(month_str)
if note_text and month:
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"UPDATE future_log SET note_text = %s, month_date = %s "
"WHERE id = %s AND user_id = %s",
(note_text, month, entry_id, _user_id()),
)
flash("Eintrag aktualisiert.", "success")
else:
flash("Bitte Text und Monat angeben.", "danger")
return redirect(url_for("journal.future"))
return render_template("edit_future.html", entry=entry)
@journal_bp.route("/future/<int:entry_id>/delete", methods=["POST"])
@login_required
def delete_future(entry_id: int):
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"DELETE FROM future_log WHERE id = %s AND user_id = %s",
(entry_id, _user_id()),
)
flash("Eintrag aus dem Future Log gelöscht.", "success")
return redirect(url_for("journal.future"))
@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
# Verweise (optional): Typ + Wert
ref_type = request.form.get("ref_type", "").strip()
ref_value = request.form.get("ref_value", "").strip()
refs = None
if ref_value:
refs = json.dumps([{"type": ref_type or "link", "value": ref_value}])
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, refs) VALUES (%s, %s, %s, %s, %s, %s)",
(_user_id(), target, bullet, note_text, priority, refs),
)
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,
ref_types=list(REF_TYPE_DISPLAY.keys()),
)
@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.
Verbessert: Beim Verschieben in einen anderen Tag wird KEINE Kopie
angelegt, sondern der Eintrag selbst auf das neue Datum gesetzt und
`moved_from_date` gespeichert (Herkunft bleibt nachvollziehbar).
Formularfelder: target=date|future, target_date=YYYY-MM-DD
"""
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
refs = note.get("refs")
cur.execute(
"INSERT INTO future_log (user_id, month_date, note_text, refs) "
"VALUES (%s, %s, %s, %s)",
(_user_id(), note["note_date"], note["note_text"], refs),
)
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(request.form.get("target_date", ""))
if not target_date:
flash("Bitte ein Zieldatum auswählen.", "danger")
elif target_date == note["note_date"]:
flash("Das Zieldatum entspricht dem aktuellen Tag.", "warning")
else:
cur.execute(
"UPDATE notes SET note_date = %s, status = 'open', "
"moved_to_future = 0, moved_to_date = NULL, "
"moved_from_date = %s WHERE id = %s",
(target_date, note["note_date"], note_id),
)
flash(f"Verschoben nach {target_date.strftime('%d.%m.%Y')}.", "success")
return redirect(request.referrer or url_for("journal.index"))