From 428dccc5b0f103e098c5fe8b12b1c7daf4c390dd Mon Sep 17 00:00:00 2001 From: SimolZimol <70102430+SimolZimol@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:12:17 +0200 Subject: [PATCH] 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 --- app.py | 52 +++++++ db.py | 26 ++++ routes/journal_routes.py | 314 +++++++++++++++++++++++++++++++++------ static/css/style.css | 73 ++++++++- static/js/main.js | 20 ++- templates/base.html | 1 + templates/day.html | 48 +++++- templates/future.html | 17 ++- templates/index.html | 15 +- templates/monat.html | 82 ++++++++++ templates/week.html | 90 ++++++----- 11 files changed, 632 insertions(+), 106 deletions(-) create mode 100644 templates/monat.html diff --git a/app.py b/app.py index a157941..ab91756 100644 --- a/app.py +++ b/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'\1', + safe, + ) + safe = re.sub( + r'([A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,})', + r'\1', + safe, + ) + safe = re.sub( + r'(\+?[0-9][0-9\s\-\/]{6,})', + r'\1', + 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 diff --git a/db.py b/db.py index 4104dbd..86dd03e 100644 --- a/db.py +++ b/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() diff --git a/routes/journal_routes.py b/routes/journal_routes.py index c47a3ed..9a35879 100644 --- a/routes/journal_routes.py +++ b/routes/journal_routes.py @@ -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/", 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")) diff --git a/static/css/style.css b/static/css/style.css index d674e24..f838ebc 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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; } +} diff --git a/static/js/main.js b/static/js/main.js index ef6e2c2..924727b 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -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; diff --git a/templates/base.html b/templates/base.html index 2233788..3059860 100644 --- a/templates/base.html +++ b/templates/base.html @@ -14,6 +14,7 @@