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
This commit is contained in:
SimolZimol
2026-08-02 21:12:17 +02:00
parent 9ab350ac02
commit 428dccc5b0
11 changed files with 632 additions and 106 deletions

View File

@@ -1,11 +1,18 @@
"""Journal-Routen: Index/Dashboard, Future Log, Wochenübersicht, Tagesansicht.
"""Journal-Routen: Index/Dashboard, Future Log, Wochen-/Monatsübersicht, Tagesansicht.
Enthält auch Helfer für Wochenberechnung und Notiz-Aktionen
(erledigt markieren, in anderen Tag / Future Log verschieben).
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
from flask import (
Blueprint, render_template, request, redirect, url_for,
session, flash, Response,
)
from auth import login_required
from db import get_connection
@@ -61,6 +68,49 @@ 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():
@@ -143,7 +193,7 @@ def future():
@journal_bp.route("/woche")
@login_required
def week():
"""Wochenübersicht: MonSo mit Status je Tag + offene Aufgaben."""
"""Wochenübersicht: MonSo mit allen Einzeleinträgen pro Tag."""
week_param = request.args.get("week", "")
if week_param:
try:
@@ -155,46 +205,210 @@ def week():
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 = _active_notes(_user_id(), d)
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 notes if n["status"] == "open"),
"done": sum(1 for n in notes if n["status"] == "done"),
"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(notes),
"total": len(active),
"notes": active,
}
)
# 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,
sunday=sunday,
days_detail=days_detail,
open_notes=open_notes,
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):
@@ -213,6 +427,13 @@ def day(date_str: str):
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:
@@ -220,8 +441,8 @@ def day(date_str: str):
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),
"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:
@@ -241,6 +462,7 @@ def day(date_str: str):
bullet_labels=BULLET_LABELS,
prev=prev,
nxt=nxt,
ref_types=list(REF_TYPE_DISPLAY.keys()),
)
@@ -271,7 +493,10 @@ def mark_done(note_id: int):
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
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:
@@ -285,12 +510,14 @@ def move_note(note_id: int):
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) "
"VALUES (%s, %s, %s)",
(_user_id(), note["note_date"], note["note_text"]),
"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 "
@@ -299,27 +526,18 @@ def move_note(note_id: int):
)
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")
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:
flash("Kein gültiges Ziel angegeben.", "danger")
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"))