Files
Journal/routes/journal_routes.py
SimolZimol 428dccc5b0 modified: app.py
modified:   db.py
	modified:   routes/journal_routes.py
	modified:   static/css/style.css
	modified:   static/js/main.js
	modified:   templates/base.html
	modified:   templates/day.html
	modified:   templates/future.html
	modified:   templates/index.html
	new file:   templates/monat.html
	modified:   templates/week.html
2026-08-02 21:12:17 +02:00

544 lines
18 KiB
Python
Raw 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 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 _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")
@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 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."""
output = io.StringIO()
writer = csv.writer(output)
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("/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"))