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

52
app.py
View File

@@ -1,7 +1,10 @@
"""Einstiegspunkt der Flask-App. Registriert alle Blueprints.""" """Einstiegspunkt der Flask-App. Registriert alle Blueprints."""
import json
import re
from datetime import date, datetime from datetime import date, datetime
from flask import Flask from flask import Flask
from markupsafe import Markup, escape
from config import Config from config import Config
from db import init_db from db import init_db
@@ -21,14 +24,63 @@ def create_app(config_class=Config):
app.register_blueprint(admin_bp) app.register_blueprint(admin_bp)
# Template/Filter-Helfer in Kontext verfügbar machen # Template/Filter-Helfer in Kontext verfügbar machen
from routes.journal_routes import ref_href, ref_type_label, bullet_label
@app.context_processor @app.context_processor
def inject_globals(): def inject_globals():
today = date.today() today = date.today()
return { return {
"now": datetime.now(), "now": datetime.now(),
"today_str": today.strftime("%Y-%m-%d"), "today_str": today.strftime("%Y-%m-%d"),
"today_month": today.strftime("%Y-%m"),
"ref_href": ref_href,
"ref_type_label": ref_type_label,
"bullet_label": bullet_label,
} }
@app.template_filter("linkify")
def linkify_filter(text):
"""Macht URLs und E-Mail-Adressen im Notiztext klickbar."""
if not text:
return ""
safe = str(escape(text))
safe = re.sub(
r'(https?://[^\s<>]+)',
r'<a href="\1" target="_blank" rel="noopener">\1</a>',
safe,
)
safe = re.sub(
r'([A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,})',
r'<a href="mailto:\1">\1</a>',
safe,
)
safe = re.sub(
r'(\+?[0-9][0-9\s\-\/]{6,})',
r'<a href="tel:\1">\1</a>',
safe,
)
return Markup(safe)
@app.template_filter("refs")
def refs_filter(raw):
"""Parst die refs-Spalte (JSON) zu einer Liste von dicts."""
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 []
@app.template_filter("refvalue")
def ref_value_filter(ref):
"""Liefert den anzuzeigenden Wert eines Verweises."""
if isinstance(ref, dict):
return ref.get("value", "")
return str(ref)
# Schema anlegen + Bootstrap-Admin beim ersten Start # Schema anlegen + Bootstrap-Admin beim ersten Start
with app.app_context(): with app.app_context():
from db import get_connection from db import get_connection

26
db.py
View File

@@ -44,6 +44,8 @@ CREATE TABLE IF NOT EXISTS notes (
status ENUM('open', 'done', 'moved') NOT NULL DEFAULT 'open', status ENUM('open', 'done', 'moved') NOT NULL DEFAULT 'open',
moved_to_date DATE NULL, moved_to_date DATE NULL,
moved_to_future TINYINT(1) NOT NULL DEFAULT 0, moved_to_future TINYINT(1) NOT NULL DEFAULT 0,
moved_from_date DATE NULL,
refs JSON NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_notes_user FOREIGN KEY (user_id) CONSTRAINT fk_notes_user FOREIGN KEY (user_id)
@@ -55,6 +57,7 @@ CREATE TABLE IF NOT EXISTS future_log (
user_id INT NOT NULL, user_id INT NOT NULL,
month_date DATE NOT NULL, month_date DATE NOT NULL,
note_text TEXT NOT NULL, note_text TEXT NOT NULL,
refs JSON NULL,
status ENUM('open', 'done') NOT NULL DEFAULT 'open', status ENUM('open', 'done') NOT NULL DEFAULT 'open',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_future_user FOREIGN KEY (user_id) CONSTRAINT fk_future_user FOREIGN KEY (user_id)
@@ -63,6 +66,26 @@ CREATE TABLE IF NOT EXISTS future_log (
""" """
# Spalten, die für bereits bestehende Datenbanken nachgezogen werden müssen
MIGRATIONS = [
("notes", "moved_from_date", "DATE NULL"),
("notes", "refs", "JSON NULL"),
("future_log", "refs", "JSON NULL"),
]
def _ensure_column(conn, table: str, column: str, definition: str) -> None:
"""Fügt eine Spalte hinzu, falls sie noch nicht existiert (Migration)."""
with conn.cursor() as cur:
cur.execute(
"SELECT COUNT(*) AS c FROM information_schema.COLUMNS "
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = %s",
(table, column),
)
if cur.fetchone()["c"] == 0:
cur.execute(f"ALTER TABLE {table} ADD COLUMN {column} {definition}")
def init_db(): def init_db():
"""Erstellt das Schema, falls es noch nicht existiert.""" """Erstellt das Schema, falls es noch nicht existiert."""
conn = get_connection() conn = get_connection()
@@ -72,5 +95,8 @@ def init_db():
stmt = statement.strip() stmt = statement.strip()
if stmt: if stmt:
cur.execute(stmt) cur.execute(stmt)
# Migrationen für bestehende Datenbanken
for table, column, definition in MIGRATIONS:
_ensure_column(conn, table, column, definition)
finally: finally:
conn.close() conn.close()

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 Enthält auch Helfer für Wochen-/Monatsberechnung, Export (CSV) und
(erledigt markieren, in anderen Tag / Future Log verschieben). 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 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 auth import login_required
from db import get_connection 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"] 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("/") @journal_bp.route("/")
@login_required @login_required
def index(): def index():
@@ -143,7 +193,7 @@ def future():
@journal_bp.route("/woche") @journal_bp.route("/woche")
@login_required @login_required
def week(): 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", "") week_param = request.args.get("week", "")
if week_param: if week_param:
try: try:
@@ -155,46 +205,210 @@ def week():
monday = monday_of_week(date.today()) monday = monday_of_week(date.today())
week_days = get_week_days(monday) 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 = [] days_detail = []
for d in week_days: 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( days_detail.append(
{ {
"date": d, "date": d,
"weekday": WEEKDAYS[d.weekday()], "weekday": WEEKDAYS[d.weekday()],
"open": sum(1 for n in notes if n["status"] == "open"), "open": sum(1 for n in active if n["status"] == "open"),
"done": sum(1 for n in notes if n["status"] == "done"), "done": sum(1 for n in active if n["status"] == "done"),
"moved": sum(1 for n in notes if n["status"] == "moved"), "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) prev_monday = monday - timedelta(days=7)
next_monday = monday + timedelta(days=7) next_monday = monday + timedelta(days=7)
return render_template( return render_template(
"week.html", "week.html",
monday=monday, monday=monday,
sunday=sunday,
days_detail=days_detail, days_detail=days_detail,
open_notes=open_notes,
weekdays=WEEKDAYS, weekdays=WEEKDAYS,
prev_monday=prev_monday, prev_monday=prev_monday,
next_monday=next_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"]) @journal_bp.route("/tag/<date_str>", methods=["GET", "POST"])
@login_required @login_required
def day(date_str: str): def day(date_str: str):
@@ -213,6 +427,13 @@ def day(date_str: str):
except ValueError: except ValueError:
priority = 0 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", "-", "?"): if bullet not in (".", "x", "-", "?"):
bullet = "-" bullet = "-"
if note_text: if note_text:
@@ -220,8 +441,8 @@ def day(date_str: str):
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute( cur.execute(
"INSERT INTO notes (user_id, note_date, bullet_type, " "INSERT INTO notes (user_id, note_date, bullet_type, "
"note_text, priority) VALUES (%s, %s, %s, %s, %s)", "note_text, priority, refs) VALUES (%s, %s, %s, %s, %s, %s)",
(_user_id(), target, bullet, note_text, priority), (_user_id(), target, bullet, note_text, priority, refs),
) )
flash("Notiz hinzugefügt.", "success") flash("Notiz hinzugefügt.", "success")
else: else:
@@ -241,6 +462,7 @@ def day(date_str: str):
bullet_labels=BULLET_LABELS, bullet_labels=BULLET_LABELS,
prev=prev, prev=prev,
nxt=nxt, 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): def move_note(note_id: int):
"""Verschiebt eine Notiz in einen anderen Tag oder ins Future Log. """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 get_connection() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
@@ -285,12 +510,14 @@ def move_note(note_id: int):
return redirect(url_for("journal.index")) return redirect(url_for("journal.index"))
target = request.form.get("target", "") target = request.form.get("target", "")
if target == "future": if target == "future":
# In Future Log übernehmen # In Future Log übernehmen
refs = note.get("refs")
cur.execute( cur.execute(
"INSERT INTO future_log (user_id, month_date, note_text) " "INSERT INTO future_log (user_id, month_date, note_text, refs) "
"VALUES (%s, %s, %s)", "VALUES (%s, %s, %s, %s)",
(_user_id(), note["note_date"], note["note_text"]), (_user_id(), note["note_date"], note["note_text"], refs),
) )
cur.execute( cur.execute(
"UPDATE notes SET status = 'moved', moved_to_future = 1 " "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") flash("In Future Log verschoben.", "success")
else: else:
target_date = parse_date(target) target_date = parse_date(request.form.get("target_date", ""))
if target_date: if not target_date:
cur.execute( flash("Bitte ein Zieldatum auswählen.", "danger")
"UPDATE notes SET status = 'moved', moved_to_date = %s " elif target_date == note["note_date"]:
"WHERE id = %s", flash("Das Zieldatum entspricht dem aktuellen Tag.", "warning")
(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: 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")) return redirect(request.referrer or url_for("journal.index"))

View File

@@ -102,7 +102,6 @@ input:focus, select:focus { outline: 2px solid var(--accent); }
.note-form-row label { flex: 0 1 auto; } .note-form-row label { flex: 0 1 auto; }
.checkbox-label { flex-direction: row; align-items: center; gap: .4rem; } .checkbox-label { flex-direction: row; align-items: center; gap: .4rem; }
.inline { display: inline-flex; gap: .4rem; align-items: center; } .inline { display: inline-flex; gap: .4rem; align-items: center; }
.inline .btn { }
/* Wöchentliche Übersicht (Dashboard) */ /* Wöchentliche Übersicht (Dashboard) */
.week-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: .6rem; } .week-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: .6rem; }
@@ -128,10 +127,8 @@ input:focus, select:focus { outline: 2px solid var(--accent); }
} }
.note-item:last-child { border-bottom: none; } .note-item:last-child { border-bottom: none; }
.bullet { font-weight: 700; width: 1.4rem; text-align: center; font-size: 1.05rem; } .bullet { font-weight: 700; width: 1.4rem; text-align: center; font-size: 1.05rem; }
.bullet-\\. { color: var(--ink); }
.bullet-x { color: var(--red); } .bullet-x { color: var(--red); }
.bullet-- { color: var(--blue); } .bullet-- { color: var(--blue); }
.bullet-\\? { color: var(--accent-dark); }
.note-text { flex: 1; } .note-text { flex: 1; }
.note-date { color: var(--muted); font-size: .8rem; } .note-date { color: var(--muted); font-size: .8rem; }
.note-item.is-done .note-text { text-decoration: line-through; color: var(--muted); } .note-item.is-done .note-text { text-decoration: line-through; color: var(--muted); }
@@ -190,3 +187,73 @@ input:focus, select:focus { outline: 2px solid var(--accent); }
.notes-row .priority-badge, .notes-row .actions { grid-column: 2; } .notes-row .priority-badge, .notes-row .actions { grid-column: 2; }
.stat-grid { grid-template-columns: 1fr; } .stat-grid { grid-template-columns: 1fr; }
} }
/* --- Monatsübersicht --- */
.page-actions { display: flex; gap: .5rem; }
.month-grid { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.month-grid-head, .month-grid-row {
display: grid; grid-template-columns: repeat(7, 1fr);
}
.month-grid-head span {
padding: .5rem; text-align: center; font-size: .75rem; font-weight: 600;
color: var(--muted); background: #faf7ef; border-bottom: 1px solid var(--line);
}
.month-grid-row:not(:last-child) { border-bottom: 1px solid var(--line); }
.month-cell {
min-height: 92px; padding: .4rem; border-right: 1px solid var(--line);
cursor: pointer; background: var(--paper);
}
.month-cell:nth-child(7) { border-right: none; }
.month-cell.outside { background: #f6f3ec; opacity: .6; }
.month-cell.today { outline: 2px solid var(--accent); }
.month-cell:hover { background: #fffaf0; }
.month-daynum { display: inline-block; font-weight: 700; font-size: .85rem; margin-bottom: .3rem; }
.month-note {
display: flex; align-items: center; gap: .25rem; font-size: .72rem;
margin-bottom: .15rem; white-space: nowrap; overflow: hidden;
}
.month-note.is-done .month-note-text { text-decoration: line-through; color: var(--muted); }
.month-note-text { overflow: hidden; text-overflow: ellipsis; }
.month-more { font-size: .7rem; color: var(--muted); }
/* --- Verweise (refs) --- */
.refs { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .2rem; }
.ref {
display: inline-flex; align-items: center; gap: .2rem;
background: #eef3fb; color: var(--blue); border: 1px solid #cfddf2;
border-radius: 16px; padding: .05rem .5rem; font-size: .72rem;
text-decoration: none;
}
.ref:hover { background: #e3ebf9; }
.ref-hint { font-size: .75rem; align-self: center; }
/* --- Wochenübersicht Mini-Einträge --- */
.day-head { display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; }
.day-head h2 { margin-bottom: 0; }
.day-head-stats { display: flex; gap: .4rem; align-items: center; flex-wrap: wrap; }
.pill.moved { background: #ede7f6; color: #6a1b9a; }
.mini-notes { margin-top: .6rem; }
.mini-note {
display: grid; grid-template-columns: 30px 1fr 90px auto; gap: .6rem;
align-items: center; padding: .45rem 0; border-bottom: 1px dashed var(--line);
}
.mini-note:last-child { border-bottom: none; }
.mini-note.is-done .mini-note-text { text-decoration: line-through; color: var(--muted); }
.mini-note-text { word-break: break-word; }
.mini-note .refs { margin-top: 0; }
.moved-hint { font-size: .72rem; color: #6a1b9a; }
.mini-actions { display: flex; gap: .4rem; align-items: center; }
/* --- Move-Steuerung (Tag) --- */
.move-ctrl { display: inline-flex; gap: .3rem; align-items: center; }
.move-btn:disabled { opacity: .5; cursor: not-allowed; }
/* --- Drucken / PDF --- */
@media print {
.topbar, .footer, .no-print, .page-actions, .mini-actions,
.move-ctrl, .actions, .flash, .nav { display: none !important; }
body { background: #fff; color: #000; }
.container { max-width: 100%; }
.card { box-shadow: none; border: 1px solid #ccc; break-inside: avoid; }
.month-cell { min-height: 70px; }
}

View File

@@ -1,10 +1,26 @@
/* Bullet Journal kleine Frontend-Helfer */ /* Bullet Journal kleine Frontend-Helfer */
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Move-Formular: wenn ein Datum gesetzt ist, Verschieben-Button nutzen // Tag-Ansicht: Datums-Eingabe synchronisiert verstecktes Feld + aktiviert Button
document.querySelectorAll(".move-ctrl").forEach(function (ctrl) {
var dateInput = ctrl.querySelector(".move-date");
var hidden = ctrl.querySelector("input[name='target_date']");
var btn = ctrl.querySelector("button[name='target'][value='date']");
function update() {
if (hidden && btn) {
hidden.value = dateInput ? dateInput.value : "";
btn.disabled = !dateInput || !dateInput.value;
}
}
if (dateInput) {
dateInput.addEventListener("change", update);
update();
}
});
// Wochen-/Monatsansicht: klassisches move-form-Datum
document.querySelectorAll(".move-form").forEach(function (form) { document.querySelectorAll(".move-form").forEach(function (form) {
var dateInput = form.querySelector("input[name='target_date']"); var dateInput = form.querySelector("input[name='target_date']");
var dateBtn = form.querySelector("button[name='target'][value='date']"); var dateBtn = form.querySelector("button[name='target'][value='date']");
// Deaktiviere den Datums-Button, solange kein Datum gewählt wurde
function updateBtn() { function updateBtn() {
if (dateBtn) { if (dateBtn) {
dateBtn.disabled = !dateInput || !dateInput.value; dateBtn.disabled = !dateInput || !dateInput.value;

View File

@@ -14,6 +14,7 @@
<nav class="nav"> <nav class="nav">
<a href="{{ url_for('journal.index') }}">Dashboard</a> <a href="{{ url_for('journal.index') }}">Dashboard</a>
<a href="{{ url_for('journal.week') }}">Wochenübersicht</a> <a href="{{ url_for('journal.week') }}">Wochenübersicht</a>
<a href="{{ url_for('journal.month') }}">Monatsübersicht</a>
<a href="{{ url_for('journal.future') }}">Future Log</a> <a href="{{ url_for('journal.future') }}">Future Log</a>
<a href="{{ url_for('journal.day', date_str=today_str) }}" class="nav-today">Heute</a> <a href="{{ url_for('journal.day', date_str=today_str) }}" class="nav-today">Heute</a>
{% if session.get('is_admin') %} {% if session.get('is_admin') %}

View File

@@ -41,6 +41,23 @@
</label> </label>
<button type="submit" class="btn btn-primary">Hinzufügen</button> <button type="submit" class="btn btn-primary">Hinzufügen</button>
</div> </div>
<div class="note-form-row ref-row">
<label>
Verweis-Typ
<select name="ref_type">
<option value=""> kein </option>
<option value="email">📧 E-Mail</option>
<option value="link">🔗 Link/URL</option>
<option value="phone">📞 Telefon</option>
<option value="mention">@ Erwähnung</option>
</select>
</label>
<label class="grow">
Verweis
<input type="text" name="ref_value" placeholder="z. B. name@firma.de oder https://... (optional)">
</label>
<span class="ref-hint muted">Optional wird als klickbarer Verweis gespeichert.</span>
</div>
</form> </form>
</section> </section>
@@ -57,7 +74,21 @@
{% for n in notes %} {% for n in notes %}
<div class="notes-row {{ 'is-done' if n.status == 'done' else '' }}"> <div class="notes-row {{ 'is-done' if n.status == 'done' else '' }}">
<span class="bullet bullet-{{ n.bullet_type }}" title="{{ bullet_labels.get(n.bullet_type, '') }}">{{ n.bullet_type }}</span> <span class="bullet bullet-{{ n.bullet_type }}" title="{{ bullet_labels.get(n.bullet_type, '') }}">{{ n.bullet_type }}</span>
<span class="note-text">{{ n.note_text }}</span> <div class="note-cell">
<div class="note-text">{{ n.note_text|linkify }}</div>
{% if n.refs|refs %}
<div class="refs">
{% for r in n.refs|refs %}
<a class="ref" href="{{ ref_href(r.type, r.value) }}" target="_blank" rel="noopener">
{{ ref_type_label(r.type) }} {{ r.value }}
</a>
{% endfor %}
</div>
{% endif %}
{% if n.moved_from_date %}
<div class="moved-hint">↩ verschoben von {{ n.moved_from_date.strftime('%d.%m.%Y') }}</div>
{% endif %}
</div>
<span class="priority-badge priority-{{ n.priority }}">{{ ['Niedrig','Mittel','Hoch'][n.priority] if n.priority <= 2 else n.priority }}</span> <span class="priority-badge priority-{{ n.priority }}">{{ ['Niedrig','Mittel','Hoch'][n.priority] if n.priority <= 2 else n.priority }}</span>
<span class="actions"> <span class="actions">
<form method="post" action="{{ url_for('journal.mark_done', note_id=n.id) }}" class="inline"> <form method="post" action="{{ url_for('journal.mark_done', note_id=n.id) }}" class="inline">
@@ -65,11 +96,16 @@
{{ 'Erledigt' if n.status != 'done' else 'Wieder öffnen' }} {{ 'Erledigt' if n.status != 'done' else 'Wieder öffnen' }}
</button> </button>
</form> </form>
<form method="post" action="{{ url_for('journal.move_note', note_id=n.id) }}" class="inline move-form"> <span class="move-ctrl">
<input type="date" name="target_date" class="move-date" title="In anderen Tag verschieben"> <input type="date" id="move-date-{{ n.id }}" class="move-date" title="Zieldatum wählen">
<button type="submit" class="btn btn-small" name="target" value="date">Verschieben</button> <form method="post" action="{{ url_for('journal.move_note', note_id=n.id) }}" class="inline">
<button type="submit" class="btn btn-small btn-future" name="target" value="future">→ Future Log</button> <input type="hidden" name="target_date" id="move-target-{{ n.id }}">
</form> <button type="submit" class="btn btn-small" name="target" value="date" id="move-btn-{{ n.id }}" disabled>Verschieben</button>
</form>
<form method="post" action="{{ url_for('journal.move_note', note_id=n.id) }}" class="inline">
<button type="submit" class="btn btn-small btn-future" name="target" value="future">→ Future Log</button>
</form>
</span>
</span> </span>
</div> </div>
{% endfor %} {% endfor %}

View File

@@ -33,10 +33,19 @@
{% for e in entries %} {% for e in entries %}
<li class="note-item {{ 'is-done' if e.status == 'done' else '' }}"> <li class="note-item {{ 'is-done' if e.status == 'done' else '' }}">
<span class="bullet">{{ '✓' if e.status == 'done' else '' }}</span> <span class="bullet">{{ '✓' if e.status == 'done' else '' }}</span>
<span class="note-text">{{ e.note_text }}</span> <div class="note-text">
<form method="post" action="{{ url_for('journal.future') }}" class="inline"> <div>{{ e.note_text|linkify }}</div>
<input type="hidden" name="toggle_id" value="{{ e.id }}"> {% if e.refs|refs %}
</form> <div class="refs">
{% for r in e.refs|refs %}
<a class="ref" href="{{ ref_href(r.type, r.value) }}" target="_blank" rel="noopener">
{{ ref_type_label(r.type) }} {{ r.value }}
</a>
{% endfor %}
</div>
{% endif %}
<span class="note-date">{{ e.month_date.strftime('%m/%Y') }}</span>
</div>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@@ -35,8 +35,19 @@
<ul class="note-list"> <ul class="note-list">
{% for n in overdue %} {% for n in overdue %}
<li class="note-item"> <li class="note-item">
<span class="bullet bullet-{{ n.bullet_type|urlencode }}">{{ n.bullet_type }}</span> <span class="bullet bullet-{{ n.bullet_type }}">{{ n.bullet_type }}</span>
<span class="note-text">{{ n.note_text }}</span> <div class="note-text">
<div>{{ n.note_text|linkify }}</div>
{% if n.refs|refs %}
<div class="refs">
{% for r in n.refs|refs %}
<a class="ref" href="{{ ref_href(r.type, r.value) }}" target="_blank" rel="noopener">
{{ ref_type_label(r.type) }} {{ r.value }}
</a>
{% endfor %}
</div>
{% endif %}
</div>
<span class="note-date">{{ n.note_date.strftime('%d.%m.%Y') }}</span> <span class="note-date">{{ n.note_date.strftime('%d.%m.%Y') }}</span>
<form method="post" action="{{ url_for('journal.mark_done', note_id=n.id) }}" class="inline"> <form method="post" action="{{ url_for('journal.mark_done', note_id=n.id) }}" class="inline">
<button class="btn btn-small">Erledigt</button> <button class="btn btn-small">Erledigt</button>

82
templates/monat.html Normal file
View File

@@ -0,0 +1,82 @@
{% extends "base.html" %}
{% block title %}Monatsübersicht Bullet Journal{% endblock %}
{% block content %}
<div class="page-head">
<div>
<h1>Monatsübersicht</h1>
<p class="muted">
<a href="{{ url_for('journal.month', month=prev_month.strftime('%Y-%m')) }}" class="btn btn-small">← Voriger</a>
{{ first.strftime('%B %Y') }}
<a href="{{ url_for('journal.month', month=next_month.strftime('%Y-%m')) }}" class="btn btn-small">Nächster →</a>
</p>
</div>
<div class="page-actions">
<a href="{{ url_for('journal.export_month', month=first.strftime('%Y-%m')) }}" class="btn btn-small">⬇ CSV</a>
<button class="btn btn-small" onclick="window.print()">🖨 Drucken/PDF</button>
</div>
</div>
<section class="card">
<h2>{{ first.strftime('%B %Y') }}</h2>
<div class="month-grid">
<div class="month-grid-head">
{% for wd in weekdays %}<span>{{ wd[:2] }}</span>{% endfor %}
</div>
{% for week in weeks %}
<div class="month-grid-row">
{% for cell in week %}
<div class="month-cell {{ '' if cell.in_month else 'outside' }} {{ 'today' if cell.today else '' }}"
{% if cell.in_month %}onclick="location.href='{{ url_for('journal.day', date_str=cell.date.strftime('%Y-%m-%d')) }}'"{% endif %}>
<span class="month-daynum">{{ cell.date.day }}</span>
{% for n in cell.notes[:4] %}
<div class="month-note {{ 'is-done' if n.status == 'done' else '' }}" title="{{ n.note_text }}">
<span class="bullet bullet-{{ n.bullet_type }}">{{ n.bullet_type }}</span>
<span class="month-note-text">{{ n.note_text|truncate(22) }}</span>
</div>
{% endfor %}
{% if cell.notes|length > 4 %}
<div class="month-more">+{{ cell.notes|length - 4 }} weitere</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</section>
<section class="card">
<h2>Alle Einträge des Monats</h2>
{% if all_notes %}
<ul class="note-list">
{% for n in all_notes %}
{% if n.status != 'moved' %}
<li class="note-item {{ 'is-done' if n.status == 'done' else '' }}">
<span class="bullet bullet-{{ n.bullet_type }}">{{ n.bullet_type }}</span>
<span class="note-text">{{ n.note_text|linkify }}</span>
{% if n.refs|refs %}
<span class="refs">
{% for r in n.refs|refs %}
<a class="ref" href="{{ ref_href(r.type, r.value) }}" target="_blank" rel="noopener">
{{ ref_type_label(r.type) }} {{ r.value }}
</a>
{% endfor %}
</span>
{% endif %}
<span class="note-date">{{ n.note_date.strftime('%d.%m.') }}</span>
<span class="priority-badge priority-{{ n.priority }}">{{ ['Niedrig','Mittel','Hoch'][n.priority] if n.priority <= 2 else n.priority }}</span>
<span class="mini-actions no-print">
<form method="post" action="{{ url_for('journal.mark_done', note_id=n.id) }}" class="inline">
<button class="btn btn-small {{ 'btn-success' if n.status != 'done' else '' }}">
{{ 'Erledigt' if n.status != 'done' else 'Öffnen' }}
</button>
</form>
</span>
</li>
{% endif %}
{% endfor %}
</ul>
{% else %}
<p class="muted">Keine Einträge in diesem Monat.</p>
{% endif %}
</section>
{% endblock %}

View File

@@ -10,52 +10,60 @@
<a href="{{ url_for('journal.week', week=next_monday.strftime('%G-W%V')) }}" class="btn btn-small">Nächste →</a> <a href="{{ url_for('journal.week', week=next_monday.strftime('%G-W%V')) }}" class="btn btn-small">Nächste →</a>
</p> </p>
</div> </div>
<div class="page-actions">
<a href="{{ url_for('journal.export_week', week=monday.strftime('%G-W%V')) }}" class="btn btn-small">⬇ CSV</a>
<button class="btn btn-small" onclick="window.print()">🖨 Drucken/PDF</button>
</div>
</div> </div>
<section class="card"> {% for day in days_detail %}
<table class="table"> <section class="card day-section">
<thead> <div class="day-head">
<tr> <h2>{{ day.weekday }}, {{ day.date.strftime('%d.%m.%Y') }}</h2>
<th>Tag</th> <div class="day-head-stats">
<th>Datum</th> <span class="pill open">{{ day.open }} offen</span>
<th>Offen</th> <span class="pill done">{{ day.done }} erledigt</span>
<th>Erledigt</th> {% if day.moved %}<span class="pill moved">{{ day.moved }} verschoben</span>{% endif %}
<th>Gesamt</th> <a href="{{ url_for('journal.day', date_str=day.date.strftime('%Y-%m-%d')) }}" class="btn btn-small">Tag öffnen</a>
<th></th> </div>
</tr> </div>
</thead>
<tbody>
{% for day in days_detail %}
<tr>
<td>{{ day.weekday }}</td>
<td>{{ day.date.strftime('%d.%m.%Y') }}</td>
<td class="num open">{{ day.open }}</td>
<td class="num done">{{ day.done }}</td>
<td class="num">{{ day.total }}</td>
<td><a href="{{ url_for('journal.day', date_str=day.date.strftime('%Y-%m-%d')) }}" class="btn btn-small">Öffnen</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<section class="card"> {% if day.notes %}
<h2>Offene Aufgaben dieser Woche</h2> <div class="mini-notes">
{% if open_notes %} {% for n in day.notes %}
<ul class="note-list"> <div class="mini-note {{ 'is-done' if n.status == 'done' else '' }}">
{% for n in open_notes %} <span class="bullet bullet-{{ n.bullet_type }}" title="{{ bullet_labels.get(n.bullet_type, '') }}">{{ n.bullet_type }}</span>
<li class="note-item"> <span class="mini-note-text">{{ n.note_text|linkify }}</span>
<span class="bullet bullet-{{ n.bullet_type }}">{{ n.bullet_type }}</span> {% if n.refs|refs %}
<span class="note-text">{{ n.note_text }}</span> <span class="refs">
<span class="note-date">{{ n.note_date.strftime('%d.%m.') }} {{ 'Aufgabe' if n.bullet_type == '.' else ('Termin' if n.bullet_type == 'x' else ('Notiz' if n.bullet_type == '-' else 'Frage')) }}</span> {% for r in n.refs|refs %}
<form method="post" action="{{ url_for('journal.mark_done', note_id=n.id) }}" class="inline"> <a class="ref" href="{{ ref_href(r.type, r.value) }}" target="_blank" rel="noopener">
<button class="btn btn-small">Erledigt</button> {{ ref_type_label(r.type) }} {{ r.value }}
</form> </a>
</li> {% endfor %}
</span>
{% endif %}
{% if n.moved_from_date %}
<span class="moved-hint">↩ verschoben von {{ n.moved_from_date.strftime('%d.%m.%Y') }}</span>
{% endif %}
<span class="priority-badge priority-{{ n.priority }}">{{ ['Niedrig','Mittel','Hoch'][n.priority] if n.priority <= 2 else n.priority }}</span>
<span class="mini-actions no-print">
<form method="post" action="{{ url_for('journal.mark_done', note_id=n.id) }}" class="inline">
<button class="btn btn-small {{ 'btn-success' if n.status != 'done' else '' }}">
{{ 'Erledigt' if n.status != 'done' else 'Öffnen' }}
</button>
</form>
<form method="post" action="{{ url_for('journal.move_note', note_id=n.id) }}" class="inline move-form">
<input type="date" name="target_date" class="move-date" title="Verschiebe-Button nutzt dieses Datum">
<button type="submit" class="btn btn-small" name="target" value="date">Verschieben</button>
</form>
</span>
</div>
{% endfor %} {% endfor %}
</ul> </div>
{% else %} {% else %}
<p class="muted">Keine offenen Aufgaben in dieser Woche. 🎉</p> <p class="muted">Keine Einträge.</p>
{% endif %} {% endif %}
</section> </section>
{% endfor %}
{% endblock %} {% endblock %}