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:
52
app.py
52
app.py
@@ -1,7 +1,10 @@
|
||||
"""Einstiegspunkt der Flask-App. Registriert alle Blueprints."""
|
||||
import json
|
||||
import re
|
||||
from datetime import date, datetime
|
||||
|
||||
from flask import Flask
|
||||
from markupsafe import Markup, escape
|
||||
|
||||
from config import Config
|
||||
from db import init_db
|
||||
@@ -21,14 +24,63 @@ def create_app(config_class=Config):
|
||||
app.register_blueprint(admin_bp)
|
||||
|
||||
# Template/Filter-Helfer in Kontext verfügbar machen
|
||||
from routes.journal_routes import ref_href, ref_type_label, bullet_label
|
||||
|
||||
@app.context_processor
|
||||
def inject_globals():
|
||||
today = date.today()
|
||||
return {
|
||||
"now": datetime.now(),
|
||||
"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
|
||||
with app.app_context():
|
||||
from db import get_connection
|
||||
|
||||
26
db.py
26
db.py
@@ -44,6 +44,8 @@ CREATE TABLE IF NOT EXISTS notes (
|
||||
status ENUM('open', 'done', 'moved') NOT NULL DEFAULT 'open',
|
||||
moved_to_date DATE NULL,
|
||||
moved_to_future TINYINT(1) NOT NULL DEFAULT 0,
|
||||
moved_from_date DATE NULL,
|
||||
refs JSON NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_notes_user FOREIGN KEY (user_id)
|
||||
@@ -55,6 +57,7 @@ CREATE TABLE IF NOT EXISTS future_log (
|
||||
user_id INT NOT NULL,
|
||||
month_date DATE NOT NULL,
|
||||
note_text TEXT NOT NULL,
|
||||
refs JSON NULL,
|
||||
status ENUM('open', 'done') NOT NULL DEFAULT 'open',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
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():
|
||||
"""Erstellt das Schema, falls es noch nicht existiert."""
|
||||
conn = get_connection()
|
||||
@@ -72,5 +95,8 @@ def init_db():
|
||||
stmt = statement.strip()
|
||||
if stmt:
|
||||
cur.execute(stmt)
|
||||
# Migrationen für bestehende Datenbanken
|
||||
for table, column, definition in MIGRATIONS:
|
||||
_ensure_column(conn, table, column, definition)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -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: Mon–So mit Status je Tag + offene Aufgaben."""
|
||||
"""Wochenübersicht: Mon–So 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 Montag–Sonntag)
|
||||
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"))
|
||||
|
||||
@@ -102,7 +102,6 @@ input:focus, select:focus { outline: 2px solid var(--accent); }
|
||||
.note-form-row label { flex: 0 1 auto; }
|
||||
.checkbox-label { flex-direction: row; align-items: center; gap: .4rem; }
|
||||
.inline { display: inline-flex; gap: .4rem; align-items: center; }
|
||||
.inline .btn { }
|
||||
|
||||
/* Wöchentliche Übersicht (Dashboard) */
|
||||
.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; }
|
||||
.bullet { font-weight: 700; width: 1.4rem; text-align: center; font-size: 1.05rem; }
|
||||
.bullet-\\. { color: var(--ink); }
|
||||
.bullet-x { color: var(--red); }
|
||||
.bullet-- { color: var(--blue); }
|
||||
.bullet-\\? { color: var(--accent-dark); }
|
||||
.note-text { flex: 1; }
|
||||
.note-date { color: var(--muted); font-size: .8rem; }
|
||||
.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; }
|
||||
.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; }
|
||||
}
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
/* Bullet Journal – kleine Frontend-Helfer */
|
||||
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) {
|
||||
var dateInput = form.querySelector("input[name='target_date']");
|
||||
var dateBtn = form.querySelector("button[name='target'][value='date']");
|
||||
// Deaktiviere den Datums-Button, solange kein Datum gewählt wurde
|
||||
function updateBtn() {
|
||||
if (dateBtn) {
|
||||
dateBtn.disabled = !dateInput || !dateInput.value;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<nav class="nav">
|
||||
<a href="{{ url_for('journal.index') }}">Dashboard</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.day', date_str=today_str) }}" class="nav-today">Heute</a>
|
||||
{% if session.get('is_admin') %}
|
||||
|
||||
@@ -41,6 +41,23 @@
|
||||
</label>
|
||||
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
@@ -57,7 +74,21 @@
|
||||
{% for n in notes %}
|
||||
<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="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="actions">
|
||||
<form method="post" action="{{ url_for('journal.mark_done', note_id=n.id) }}" class="inline">
|
||||
@@ -65,12 +96,17 @@
|
||||
{{ 'Erledigt' if n.status != 'done' else 'Wieder ö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="In anderen Tag verschieben">
|
||||
<button type="submit" class="btn btn-small" name="target" value="date">Verschieben</button>
|
||||
<span class="move-ctrl">
|
||||
<input type="date" id="move-date-{{ n.id }}" class="move-date" title="Zieldatum wählen">
|
||||
<form method="post" action="{{ url_for('journal.move_note', note_id=n.id) }}" class="inline">
|
||||
<input type="hidden" name="target_date" id="move-target-{{ n.id }}">
|
||||
<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>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -33,10 +33,19 @@
|
||||
{% for e in entries %}
|
||||
<li class="note-item {{ 'is-done' if e.status == 'done' else '' }}">
|
||||
<span class="bullet">{{ '✓' if e.status == 'done' else '›' }}</span>
|
||||
<span class="note-text">{{ e.note_text }}</span>
|
||||
<form method="post" action="{{ url_for('journal.future') }}" class="inline">
|
||||
<input type="hidden" name="toggle_id" value="{{ e.id }}">
|
||||
</form>
|
||||
<div class="note-text">
|
||||
<div>{{ e.note_text|linkify }}</div>
|
||||
{% if e.refs|refs %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -35,8 +35,19 @@
|
||||
<ul class="note-list">
|
||||
{% for n in overdue %}
|
||||
<li class="note-item">
|
||||
<span class="bullet bullet-{{ n.bullet_type|urlencode }}">{{ n.bullet_type }}</span>
|
||||
<span class="note-text">{{ n.note_text }}</span>
|
||||
<span class="bullet bullet-{{ n.bullet_type }}">{{ n.bullet_type }}</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>
|
||||
<form method="post" action="{{ url_for('journal.mark_done', note_id=n.id) }}" class="inline">
|
||||
<button class="btn btn-small">Erledigt</button>
|
||||
|
||||
82
templates/monat.html
Normal file
82
templates/monat.html
Normal 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 %}
|
||||
@@ -10,52 +10,60 @@
|
||||
<a href="{{ url_for('journal.week', week=next_monday.strftime('%G-W%V')) }}" class="btn btn-small">Nächste →</a>
|
||||
</p>
|
||||
</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>
|
||||
|
||||
<section class="card">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tag</th>
|
||||
<th>Datum</th>
|
||||
<th>Offen</th>
|
||||
<th>Erledigt</th>
|
||||
<th>Gesamt</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</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>
|
||||
{% for day in days_detail %}
|
||||
<section class="card day-section">
|
||||
<div class="day-head">
|
||||
<h2>{{ day.weekday }}, {{ day.date.strftime('%d.%m.%Y') }}</h2>
|
||||
<div class="day-head-stats">
|
||||
<span class="pill open">{{ day.open }} offen</span>
|
||||
<span class="pill done">{{ day.done }} erledigt</span>
|
||||
{% if day.moved %}<span class="pill moved">{{ day.moved }} verschoben</span>{% endif %}
|
||||
<a href="{{ url_for('journal.day', date_str=day.date.strftime('%Y-%m-%d')) }}" class="btn btn-small">Tag öffnen</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<h2>Offene Aufgaben dieser Woche</h2>
|
||||
{% if open_notes %}
|
||||
<ul class="note-list">
|
||||
{% for n in open_notes %}
|
||||
<li class="note-item">
|
||||
<span class="bullet bullet-{{ n.bullet_type }}">{{ n.bullet_type }}</span>
|
||||
<span class="note-text">{{ n.note_text }}</span>
|
||||
<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>
|
||||
<form method="post" action="{{ url_for('journal.mark_done', note_id=n.id) }}" class="inline">
|
||||
<button class="btn btn-small">Erledigt</button>
|
||||
</form>
|
||||
</li>
|
||||
{% if day.notes %}
|
||||
<div class="mini-notes">
|
||||
{% for n in day.notes %}
|
||||
<div class="mini-note {{ '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="mini-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 %}
|
||||
</ul>
|
||||
</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 %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted">Keine offenen Aufgaben in dieser Woche. 🎉</p>
|
||||
<p class="muted">Keine Einträge.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user