From f973e673115166ae1fa4e8054e736b24a1e7fd38 Mon Sep 17 00:00:00 2001 From: SimolZimol <70102430+SimolZimol@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:28:30 +0200 Subject: [PATCH] modified: Dockerfile new file: pdf_export.py modified: requirements.txt modified: routes/journal_routes.py modified: static/css/style.css modified: templates/base.html modified: templates/day.html new file: templates/edit_future.html new file: templates/edit_note.html modified: templates/future.html modified: templates/monat.html new file: templates/report.html modified: templates/week.html --- Dockerfile | 5 + pdf_export.py | 159 +++++++++++++++++ requirements.txt | 1 + routes/journal_routes.py | 343 +++++++++++++++++++++++++++++++++++++ static/css/style.css | 23 +++ templates/base.html | 1 + templates/day.html | 5 + templates/edit_future.html | 26 +++ templates/edit_note.html | 54 ++++++ templates/future.html | 7 + templates/monat.html | 5 + templates/report.html | 117 +++++++++++++ templates/week.html | 5 + 13 files changed, 751 insertions(+) create mode 100644 pdf_export.py create mode 100644 templates/edit_future.html create mode 100644 templates/edit_note.html create mode 100644 templates/report.html diff --git a/Dockerfile b/Dockerfile index 2876c42..4acd3c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,11 @@ FROM python:3.11-slim # Arbeitsverzeichnis erstellen WORKDIR /app +# System-Dependencies: DejaVu-Schriftarten für PDF-Export (Umlaute) +RUN apt-get update && apt-get install -y --no-install-recommends \ + fonts-dejavu-core \ + && rm -rf /var/lib/apt/lists/* + # Kopiere die requirements-Datei und installiere die Abhängigkeiten COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt diff --git a/pdf_export.py b/pdf_export.py new file mode 100644 index 0000000..5dcb96f --- /dev/null +++ b/pdf_export.py @@ -0,0 +1,159 @@ +"""PDF-Export für Wochen-/Monatsberichte (ReportLab). + +Unterstützt Umlaute über die DejaVu-Schriftarten, die im Docker-Image +(fonts-dejavu-core) enthalten sind. Falls keine TTF gefunden wird, +fällt die Generierung auf Helvetica zurück (dann evtl. ohne Umlaute). +""" +import io +import os + +from reportlab.lib import colors +from reportlab.lib.enums import TA_LEFT +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet +from reportlab.lib.units import mm +from reportlab.platypus import ( + SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, +) + +_DEJAVU_PATHS = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/DejaVuSans.ttf", +] +_DEJAVU_BOLD_PATHS = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", + "/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf", +] + + +def _register_fonts(): + from reportlab.pdfbase import pdfmetrics + from reportlab.pdfbase.ttfonts import TTFont + + regular = next((p for p in _DEJAVU_PATHS if os.path.exists(p)), None) + bold = next((p for p in _DEJAVU_BOLD_PATHS if os.path.exists(p)), None) + + if regular: + pdfmetrics.registerFont(TTFont("DejaVu", regular)) + if bold: + pdfmetrics.registerFont(TTFont("DejaVu-Bold", bold)) + return "DejaVu", "DejaVu-Bold" + return "Helvetica", "Helvetica-Bold" + + +def _build_styles(font_name: str, font_bold: str): + styles = getSampleStyleSheet() + base = ParagraphStyle( + "ReportBase", + fontName=font_name, + fontSize=9, + leading=12, + textColor=colors.HexColor("#2b2b2b"), + ) + return { + "title": ParagraphStyle( + "ReportTitle", parent=styles["Title"], fontName=font_bold, + fontSize=18, leading=22, spaceAfter=4, textColor=colors.HexColor("#2b2b2b"), + ), + "subtitle": ParagraphStyle( + "ReportSub", parent=styles["Normal"], fontName=font_name, + fontSize=10, leading=14, textColor=colors.HexColor("#8a867e"), + ), + "h2": ParagraphStyle( + "ReportH2", parent=styles["Heading2"], fontName=font_bold, + fontSize=12, leading=16, spaceBefore=10, spaceAfter=4, + textColor=colors.HexColor("#a8871d"), + ), + "body": base, + "cell": ParagraphStyle( + "Cell", parent=base, fontSize=8.5, leading=11, + alignment=TA_LEFT, + ), + "cellheader": ParagraphStyle( + "CellHeader", parent=base, fontName=font_bold, + fontSize=8.5, leading=11, textColor=colors.white, + ), + } + + +def build_pdf(title: str, subtitle: str, rows: list[dict], + summary: dict | None = None) -> bytes: + """Erzeugt ein PDF mit einer Übersichtstabelle der Einträge. + + rows: Liste von dicts mit Schluesseln + date_str, weekday, bullet, bullet_label, note_text, priority, + status, refs + summary: optionales dict mit open/done/total/Zeitraum. + """ + font_name, font_bold = _register_fonts() + st = _build_styles(font_name, font_bold) + + buffer = io.BytesIO() + + doc = SimpleDocTemplate( + buffer, pagesize=A4, + leftMargin=15 * mm, rightMargin=15 * mm, + topMargin=18 * mm, bottomMargin=18 * mm, + title=title, + ) + + story = [] + story.append(Paragraph(title, st["title"])) + story.append(Paragraph(subtitle, st["subtitle"])) + story.append(Spacer(1, 4)) + + # Zusammenfassung + if summary: + story.append(Paragraph("Zusammenfassung", st["h2"])) + sum_rows = [ + ["Offen", "Erledigt", "Gesamt"], + [str(summary.get("open", 0)), str(summary.get("done", 0)), + str(summary.get("total", 0))], + ] + sum_table = Table(sum_rows, colWidths=[40 * mm, 40 * mm, 40 * mm]) + sum_table.setStyle(TableStyle([ + ("FONTNAME", (0, 0), (-1, 0), font_bold), + ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#a8871d")), + ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), + ("FONTNAME", (0, 1), (-1, -1), font_name), + ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#e5ded2")), + ("ALIGN", (0, 0), (-1, -1), "CENTER"), + ("TOPPADDING", (0, 0), (-1, -1), 4), + ("BOTTOMPADDING", (0, 0), (-1, -1), 4), + ])) + story.append(sum_table) + story.append(Spacer(1, 6)) + + # Einträge + story.append(Paragraph("Einträge", st["h2"])) + header = ["Datum", "Wochentag", "Bullet", "Notiz", "Priorität", "Status", "Verweise"] + table_data = [[Paragraph(h, st["cellheader"]) for h in header]] + + for r in rows: + table_data.append([ + Paragraph(r.get("date_str", ""), st["cell"]), + Paragraph(r.get("weekday", ""), st["cell"]), + Paragraph(f"{r.get('bullet', '')} {r.get('bullet_label', '')}", st["cell"]), + Paragraph(r.get("note_text", ""), st["cell"]), + Paragraph(r.get("priority", ""), st["cell"]), + Paragraph(r.get("status", ""), st["cell"]), + Paragraph(r.get("refs", ""), st["cell"]), + ]) + + col_widths = [18 * mm, 18 * mm, 20 * mm, 60 * mm, 16 * mm, 16 * mm, 30 * mm] + table = Table(table_data, colWidths=col_widths, repeatRows=1) + table.setStyle(TableStyle([ + ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#a8871d")), + ("GRID", (0, 0), (-1, -1), 0.3, colors.HexColor("#e5ded2")), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ("ROWBACKGROUNDS", (0, 1), (-1, -1), + [colors.white, colors.HexColor("#faf7ef")]), + ("TOPPADDING", (0, 0), (-1, -1), 3), + ("BOTTOMPADDING", (0, 0), (-1, -1), 3), + ])) + story.append(table) + + doc.build(story) + buffer.seek(0) + return buffer.getvalue() diff --git a/requirements.txt b/requirements.txt index 7cb7203..300859e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ PyMySQL==1.1.1 Werkzeug==3.0.3 gunicorn==22.0.0 python-dotenv==1.0.1 +reportlab==4.2.2 diff --git a/routes/journal_routes.py b/routes/journal_routes.py index 9a35879..35618ad 100644 --- a/routes/journal_routes.py +++ b/routes/journal_routes.py @@ -111,6 +111,122 @@ def bullet_label(bullet_type: str) -> str: return BULLET_LABELS.get(bullet_type, "Notiz") +PRIORITY_LABELS = {0: "Niedrig", 1: "Mittel", 2: "Hoch"} +STATUS_LABELS = {"open": "Offen", "done": "Erledigt", "moved": "Verschoben"} + + +def _report_filters() -> dict: + """Liest die Report-Filter aus request.args (GET-Formular/Export-Links).""" + args = request.args + range_type = args.get("range", "week") + + if range_type == "month": + month_param = args.get("month", "") + try: + y, mo = (int(x) for x in month_param.split("-")) + start = date(y, mo, 1) + except (ValueError, TypeError): + start = date.today().replace(day=1) + end = date(start.year, start.month, + calendar.monthrange(start.year, start.month)[1]) + label = start.strftime("%B %Y") + elif range_type == "custom": + start = parse_date(args.get("from", "")) + end = parse_date(args.get("to", "")) + if not start or not end or end < start: + start = monday_of_week(date.today()) + end = start + timedelta(days=6) + label = f"{start.strftime('%d.%m.%Y')} – {end.strftime('%d.%m.%Y')}" + range_type = "custom" + else: # week + week_param = args.get("week", "") + try: + iso_year, iso_week = week_param.split("-W") + start = date.fromisocalendar(int(iso_year), int(iso_week), 1) + except (ValueError, TypeError): + start = monday_of_week(date.today()) + end = start + timedelta(days=6) + label = f"Woche {start.strftime('%d.%m.%Y')} – {end.strftime('%d.%m.%Y')}" + + # Filter (leere Auswahl = alle zulassen) + weekdays = [int(x) for x in args.getlist("wd")] + bullets = [b for b in args.getlist("bullet") if b in BULLET_LABELS] + statuses = [s for s in args.getlist("status") if s in STATUS_LABELS] + priorities = [int(p) for p in args.getlist("priority") if p.isdigit()] + + if not weekdays: + weekdays = list(range(7)) + if not bullets: + bullets = list(BULLET_LABELS.keys()) + if not statuses: + statuses = ["open", "done"] + if not priorities: + priorities = [0, 1, 2] + + return { + "range_type": range_type, + "start": start, + "end": end, + "label": label, + "weekdays": weekdays, + "bullets": bullets, + "statuses": statuses, + "priorities": priorities, + } + + +def _run_report(uid: int, f: dict) -> tuple[list[dict], dict]: + """Führt den Report aus und liefert (rows, summary).""" + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "SELECT * FROM notes WHERE user_id = %s " + "AND note_date BETWEEN %s AND %s " + "ORDER BY note_date ASC, id ASC", + (uid, f["start"], f["end"]), + ) + rows = cur.fetchall() + + filtered = [] + for n in rows: + if n["note_date"].weekday() not in f["weekdays"]: + continue + if n["bullet_type"] not in f["bullets"]: + continue + if n["status"] not in f["statuses"]: + continue + if n["priority"] not in f["priorities"]: + continue + filtered.append(n) + + rows_out = [] + for n in filtered: + refs = ", ".join( + f"{ref_type_label(r.get('type', ''))} {r.get('value', '')}" + for r in parse_refs(n.get("refs")) + ) + rows_out.append({ + "id": n["id"], + "date_str": n["note_date"].strftime("%d.%m.%Y"), + "iso": n["note_date"].strftime("%Y-%m-%d"), + "weekday": WEEKDAYS[n["note_date"].weekday()], + "bullet": n["bullet_type"], + "bullet_label": bullet_label(n["bullet_type"]), + "note_text": n["note_text"], + "priority": PRIORITY_LABELS.get(n.get("priority", 0), str(n.get("priority", ""))), + "status": STATUS_LABELS.get(n.get("status"), n.get("status", "")), + "status_code": n.get("status"), + "refs": refs, + }) + + summary = { + "total": len(rows_out), + "open": sum(1 for r in rows_out if r["status_code"] == "open"), + "done": sum(1 for r in rows_out if r["status_code"] == "done"), + } + return rows_out, summary + + @journal_bp.route("/") @login_required def index(): @@ -409,6 +525,233 @@ def export_month(): return _csv_response("Monatsübersicht", filename, rows) +@journal_bp.route("/report") +@login_required +def report(): + """Report-Generator: dynamischer Wochen-/Monatsbericht mit Filtern.""" + filters = _report_filters() + rows, summary = _run_report(_user_id(), filters) + today = date.today() + + # Für Export-Links: aktuelle Filter als Query-String aufbauen + def _qs(fmt): + params = [ + ("range", filters["range_type"]), + ("format", fmt), + ] + for wd in filters["weekdays"]: + params.append(("wd", str(wd))) + for b in filters["bullets"]: + params.append(("bullet", b)) + for s in filters["statuses"]: + params.append(("status", s)) + for p in filters["priorities"]: + params.append(("priority", str(p))) + if filters["range_type"] == "month": + params.append(("month", filters["start"].strftime("%Y-%m"))) + elif filters["range_type"] == "custom": + params.append(("from", filters["start"].strftime("%Y-%m-%d"))) + params.append(("to", filters["end"].strftime("%Y-%m-%d"))) + else: + params.append(("week", filters["start"].strftime("%G-W%V"))) + return url_for("journal.report_export", **dict(params)) + + export_csv_url = _qs("csv") + export_pdf_url = _qs("pdf") + + current_week = monday_of_week(today).strftime("%G-W%V") + current_month = today.strftime("%Y-%m") + + filters["week_value"] = current_week + filters["from_value"] = filters["start"].strftime("%Y-%m-%d") if filters["range_type"] == "custom" else "" + filters["to_value"] = filters["end"].strftime("%Y-%m-%d") if filters["range_type"] == "custom" else "" + + return render_template( + "report.html", + f=filters, + rows=rows, + summary=summary, + weekdays=WEEKDAYS, + bullet_labels=BULLET_LABELS, + current_week=current_week, + current_month=current_month, + today_str=today.strftime("%Y-%m-%d"), + export_csv_url=export_csv_url, + export_pdf_url=export_pdf_url, + ) + + +@journal_bp.route("/report/export") +@login_required +def report_export(): + """Export des Report-Generators als CSV oder PDF (format=pdf|csv).""" + filters = _report_filters() + rows, summary = _run_report(_user_id(), filters) + fmt = request.args.get("format", "csv") + + safe_name = filters["label"].replace(" ", "_").replace("–", "-") + filename = f"report_{safe_name}" + + if fmt == "pdf": + from pdf_export import build_pdf + pdf_bytes = build_pdf( + title="Bullet-Journal-Report", + subtitle=filters["label"], + rows=rows, + summary=summary, + ) + return Response( + pdf_bytes, + mimetype="application/pdf", + headers={ + "Content-Disposition": f"attachment; filename={filename}.pdf" + }, + ) + + # CSV + output = io.StringIO() + writer = csv.writer(output) + writer.writerow([ + "Datum", "Wochentag", "Bullet", "Typ", "Notiz", "Priorität", + "Status", "Verweise", + ]) + for r in rows: + writer.writerow([ + r["date_str"], r["weekday"], r["bullet"], r["bullet_label"], + r["note_text"], r["priority"], r["status"], r["refs"], + ]) + output.seek(0) + return Response( + "\ufeff" + output.getvalue(), + mimetype="text/csv; charset=utf-8", + headers={"Content-Disposition": f"attachment; filename={filename}.csv"}, + ) + + +@journal_bp.route("/note//edit", methods=["GET", "POST"]) +@login_required +def edit_note(note_id: int): + """Bearbeitet eine Notiz (Text, Bullet, Priorität, Verweise).""" + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "SELECT * FROM notes WHERE id = %s AND user_id = %s", + (note_id, _user_id()), + ) + note = cur.fetchone() + + if not note: + flash("Notiz nicht gefunden.", "danger") + return redirect(url_for("journal.index")) + + if request.method == "POST": + bullet = request.form.get("bullet", "-") + note_text = request.form.get("note_text", "").strip() + try: + priority = int(request.form.get("priority", "0")) + except ValueError: + priority = 0 + ref_type = request.form.get("ref_type", "").strip() + ref_value = request.form.get("ref_value", "").strip() + refs = None + if ref_value: + refs = json.dumps([{"type": ref_type or "link", "value": ref_value}]) + + if bullet not in (".", "x", "-", "?"): + bullet = "-" + if not note_text: + flash("Notiz darf nicht leer sein.", "danger") + else: + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "UPDATE notes SET bullet_type = %s, note_text = %s, " + "priority = %s, refs = %s WHERE id = %s AND user_id = %s", + (bullet, note_text, priority, refs, note_id, _user_id()), + ) + flash("Notiz aktualisiert.", "success") + return redirect(url_for("journal.day", date_str=note["note_date"].strftime("%Y-%m-%d"))) + + refs = parse_refs(note.get("refs")) + current_ref = refs[0] if refs else {} + + return render_template( + "edit_note.html", + note=note, + bullet_labels=BULLET_LABELS, + current_ref=current_ref, + ) + + +@journal_bp.route("/note//delete", methods=["POST"]) +@login_required +def delete_note(note_id: int): + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "SELECT note_date FROM notes WHERE id = %s AND user_id = %s", + (note_id, _user_id()), + ) + note = cur.fetchone() + if not note: + flash("Notiz nicht gefunden.", "danger") + return redirect(url_for("journal.index")) + cur.execute( + "DELETE FROM notes WHERE id = %s AND user_id = %s", + (note_id, _user_id()), + ) + flash("Notiz gelöscht.", "success") + return redirect(request.referrer or url_for("journal.index")) + + +@journal_bp.route("/future//edit", methods=["GET", "POST"]) +@login_required +def edit_future(entry_id: int): + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "SELECT * FROM future_log WHERE id = %s AND user_id = %s", + (entry_id, _user_id()), + ) + entry = cur.fetchone() + + if not entry: + flash("Eintrag nicht gefunden.", "danger") + return redirect(url_for("journal.future")) + + if request.method == "POST": + note_text = request.form.get("note_text", "").strip() + month_str = request.form.get("month", "") + month = parse_date(month_str) + if note_text and month: + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "UPDATE future_log SET note_text = %s, month_date = %s " + "WHERE id = %s AND user_id = %s", + (note_text, month, entry_id, _user_id()), + ) + flash("Eintrag aktualisiert.", "success") + else: + flash("Bitte Text und Monat angeben.", "danger") + return redirect(url_for("journal.future")) + + return render_template("edit_future.html", entry=entry) + + +@journal_bp.route("/future//delete", methods=["POST"]) +@login_required +def delete_future(entry_id: int): + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "DELETE FROM future_log WHERE id = %s AND user_id = %s", + (entry_id, _user_id()), + ) + flash("Eintrag aus dem Future Log gelöscht.", "success") + return redirect(url_for("journal.future")) + + @journal_bp.route("/tag/", methods=["GET", "POST"]) @login_required def day(date_str: str): diff --git a/static/css/style.css b/static/css/style.css index f838ebc..50fa686 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -257,3 +257,26 @@ input:focus, select:focus { outline: 2px solid var(--accent); } .card { box-shadow: none; border: 1px solid #ccc; break-inside: avoid; } .month-cell { min-height: 70px; } } + +/* --- Report-Generator --- */ +.report-form { display: flex; flex-direction: column; gap: .9rem; } +.report-block { + display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; + padding: .6rem; background: #fdfaf2; border: 1px solid var(--line); border-radius: 8px; +} +.report-block-title { + font-weight: 600; font-size: .8rem; color: var(--accent-dark); + flex-basis: 100%; margin-bottom: .1rem; +} +.report-block label { flex-direction: row; align-items: center; gap: .3rem; font-size: .85rem; } +.report-block label.grow { flex: 1 1 180px; } +.report-actions { display: flex; gap: .5rem; } +.chk { display: inline-flex; } +.report-exports { display: flex; gap: .5rem; } + +/* Textarea */ +textarea { + width: 100%; padding: .5rem .6rem; border: 1px solid var(--line); + border-radius: 6px; font-size: .95rem; font-family: inherit; +} +textarea:focus { outline: 2px solid var(--accent); } diff --git a/templates/base.html b/templates/base.html index 3059860..9802296 100644 --- a/templates/base.html +++ b/templates/base.html @@ -15,6 +15,7 @@ Dashboard Wochenübersicht Monatsübersicht + Report Future Log Heute {% if session.get('is_admin') %} diff --git a/templates/day.html b/templates/day.html index a2f6c0d..935b394 100644 --- a/templates/day.html +++ b/templates/day.html @@ -106,6 +106,11 @@ + Bearbeiten +
+ +
{% endfor %} diff --git a/templates/edit_future.html b/templates/edit_future.html new file mode 100644 index 0000000..b921be5 --- /dev/null +++ b/templates/edit_future.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} +{% block title %}Future-Log-Eintrag bearbeiten – Bullet Journal{% endblock %} +{% block content %} +
+
+

Future-Log-Eintrag bearbeiten

+
+ ← Zurück zum Future Log +
+ +
+
+ + +
+ +
+
+
+{% endblock %} diff --git a/templates/edit_note.html b/templates/edit_note.html new file mode 100644 index 0000000..0703de0 --- /dev/null +++ b/templates/edit_note.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} +{% block title %}Notiz bearbeiten – Bullet Journal{% endblock %} +{% block content %} +
+
+

Notiz bearbeiten

+

{{ note.note_date.strftime('%d.%m.%Y') }}

+
+ ← Zurück zum Tag +
+ +
+
+ + + + + +
+ +
+
+
+{% endblock %} diff --git a/templates/future.html b/templates/future.html index d5adc86..f5b9de2 100644 --- a/templates/future.html +++ b/templates/future.html @@ -46,6 +46,13 @@ {% endif %} {{ e.month_date.strftime('%m/%Y') }} + + Bearbeiten +
+ +
+
{% endfor %} diff --git a/templates/monat.html b/templates/monat.html index df41b78..aaac859 100644 --- a/templates/monat.html +++ b/templates/monat.html @@ -70,6 +70,11 @@ {{ 'Erledigt' if n.status != 'done' else 'Öffnen' }} + Bearbeiten +
+ +
{% endif %} diff --git a/templates/report.html b/templates/report.html new file mode 100644 index 0000000..f786516 --- /dev/null +++ b/templates/report.html @@ -0,0 +1,117 @@ +{% extends "base.html" %} +{% block title %}Report-Generator – Bullet Journal{% endblock %} +{% block content %} +
+
+

Report-Generator

+

Erstelle dynamische Wochen-/Monatsberichte und exportiere sie als CSV oder PDF.

+
+
+ +
+

Filter

+
+
+ Zeitraum + + + + + + + +
+ +
+ Wochentage + {% for i in range(7) %} + + {% endfor %} +
+ +
+ Bullet-Typen + {% for b, label in bullet_labels.items() %} + + {% endfor %} +
+ +
+ Status + + + +
+ +
+ Priorität + + + +
+ +
+ +
+
+
+ +
+
+

Vorschau: {{ f.label }}

+ +
+
+ {{ summary.open }} offen + {{ summary.done }} erledigt + {{ summary.total }} gesamt +
+ + {% if rows %} + + + + + + {% for r in rows %} + + + + + + + + + + {% endfor %} + +
DatumWochentagBulletNotizPrioritätStatusVerweise
{{ r.date_str }}{{ r.weekday }}{{ r.bullet }} {{ r.bullet_label }}{{ r.note_text|linkify }}{{ r.priority }}{{ r.status }}{% if r.refs %}{{ r.refs }}{% endif %}
+ {% else %} +

Keine Einträge für die gewählten Filter.

+ {% endif %} +
+{% endblock %} diff --git a/templates/week.html b/templates/week.html index a8594be..4a7a052 100644 --- a/templates/week.html +++ b/templates/week.html @@ -57,6 +57,11 @@ + Bearbeiten +
+ +
{% endfor %}