new file: .env.example
new file: .gitignore new file: Dockerfile new file: README.md new file: app.py new file: auth.py new file: config.py new file: db.py new file: db_helpers.py new file: docker-compose.yml new file: requirements.txt new file: routes/__init__.py new file: routes/admin_routes.py new file: routes/auth_routes.py new file: routes/journal_routes.py new file: static/css/style.css new file: static/js/main.js new file: templates/admin/dashboard.html new file: templates/admin/users.html new file: templates/base.html new file: templates/day.html new file: templates/future.html new file: templates/index.html new file: templates/login.html new file: templates/week.html
This commit is contained in:
49
templates/admin/dashboard.html
Normal file
49
templates/admin/dashboard.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Admin – Bullet Journal{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Admin-Panel</h1>
|
||||
<p class="muted">Zentrales Dashboard</p>
|
||||
</div>
|
||||
<a href="{{ url_for('admin.users') }}" class="btn">Benutzer verwalten</a>
|
||||
</div>
|
||||
|
||||
<section class="stat-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">{{ total_users }}</span>
|
||||
<span class="stat-label">Benutzer</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">{{ total_admins }}</span>
|
||||
<span class="stat-label">Admins</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">{{ total_notes }}</span>
|
||||
<span class="stat-label">Notizen</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Kürzlich angelegte Benutzer</h2>
|
||||
{% if recent %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Benutzername</th><th>E-Mail</th><th>Rolle</th><th>Erstellt</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in recent %}
|
||||
<tr>
|
||||
<td>{{ u.username }}</td>
|
||||
<td>{{ u.email or '–' }}</td>
|
||||
<td>{{ 'Admin' if u.is_admin else 'Benutzer' }}</td>
|
||||
<td>{{ u.created_at.strftime('%d.%m.%Y %H:%M') if u.created_at else '–' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="muted">Noch keine Benutzer.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
79
templates/admin/users.html
Normal file
79
templates/admin/users.html
Normal file
@@ -0,0 +1,79 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Benutzerverwaltung – Bullet Journal{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Benutzerverwaltung</h1>
|
||||
<p class="muted">Accounts erstellen und verwalten</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<h2>Neuen Benutzer anlegen</h2>
|
||||
<form method="post" action="{{ url_for('admin.users') }}" class="note-form">
|
||||
<div class="note-form-row">
|
||||
<label>
|
||||
Benutzername
|
||||
<input type="text" name="username" required>
|
||||
</label>
|
||||
<label>
|
||||
Passwort
|
||||
<input type="text" name="password" required>
|
||||
</label>
|
||||
<label>
|
||||
E-Mail
|
||||
<input type="email" name="email">
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="is_admin" value="1"> Admin
|
||||
</label>
|
||||
<button type="submit" class="btn btn-primary">Anlegen</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Alle Benutzer</h2>
|
||||
{% if users %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Benutzername</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Rolle</th>
|
||||
<th>Notizen</th>
|
||||
<th>Erstellt</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>{{ u.id }}</td>
|
||||
<td>{{ u.username }}</td>
|
||||
<td>{{ u.email or '–' }}</td>
|
||||
<td>{{ 'Admin' if u.is_admin else 'Benutzer' }}</td>
|
||||
<td>{{ u.note_count }}</td>
|
||||
<td>{{ u.created_at.strftime('%d.%m.%Y') if u.created_at else '–' }}</td>
|
||||
<td class="table-actions">
|
||||
<form method="post" action="{{ url_for('admin.reset_password', user_id=u.id) }}" class="inline">
|
||||
<input type="text" name="new_password" placeholder="Neues Passwort" required>
|
||||
<button class="btn btn-small">PW zurücksetzen</button>
|
||||
</form>
|
||||
{% if u.id != session.get('user_id') %}
|
||||
<form method="post" action="{{ url_for('admin.delete_user', user_id=u.id) }}" class="inline"
|
||||
onsubmit="return confirm('Benutzer wirklich löschen? Alle Notizen gehen verloren.');">
|
||||
<button class="btn btn-small btn-danger">Löschen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="muted">Keine Benutzer vorhanden.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
52
templates/base.html
Normal file
52
templates/base.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Bullet Journal{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="topbar-inner">
|
||||
<a href="{{ url_for('journal.index') }}" class="brand">📓 Bullet Journal</a>
|
||||
{% if session.get('user_id') %}
|
||||
<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.future') }}">Future Log</a>
|
||||
<a href="{{ url_for('journal.day', date_str=today_str) }}" class="nav-today">Heute</a>
|
||||
{% if session.get('is_admin') %}
|
||||
<a href="{{ url_for('admin.dashboard') }}" class="nav-admin">Admin</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<div class="user-area">
|
||||
<span class="username">{{ session.get('username') }}</span>
|
||||
<form method="post" action="{{ url_for('auth.logout') }}" class="inline">
|
||||
<button type="submit" class="btn btn-link">Abmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash flash-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>Bullet Journal © {{ now.year if now else 2026 }}</p>
|
||||
</footer>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
81
templates/day.html
Normal file
81
templates/day.html
Normal file
@@ -0,0 +1,81 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ weekday }} – Bullet Journal{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>{{ weekday }}, {{ target.strftime('%d.%m.%Y') }}</h1>
|
||||
<p class="muted">
|
||||
<a href="{{ url_for('journal.day', date_str=prev.strftime('%Y-%m-%d')) }}" class="btn btn-small">← Vortag</a>
|
||||
{% if target.strftime('%Y-%m-%d') != today_str %}
|
||||
<a href="{{ url_for('journal.day', date_str=today_str) }}" class="btn btn-small">Heute</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('journal.day', date_str=nxt.strftime('%Y-%m-%d')) }}" class="btn btn-small">Nächster Tag →</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<h2>Neue Notiz</h2>
|
||||
<form method="post" action="{{ url_for('journal.day', date_str=target.strftime('%Y-%m-%d')) }}" class="note-form">
|
||||
<div class="note-form-row">
|
||||
<label>
|
||||
Bullet
|
||||
<select name="bullet">
|
||||
<option value=".">• Aufgabe</option>
|
||||
<option value="x">✕ Termin</option>
|
||||
<option value="-">– Notiz</option>
|
||||
<option value="?">? Frage</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="grow">
|
||||
Notiz
|
||||
<input type="text" name="note_text" required placeholder="Was ist zu tun?">
|
||||
</label>
|
||||
<label>
|
||||
Priorität
|
||||
<select name="priority">
|
||||
<option value="0">Niedrig</option>
|
||||
<option value="1">Mittel</option>
|
||||
<option value="2">Hoch</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Notizen</h2>
|
||||
{% if notes %}
|
||||
<div class="notes-table">
|
||||
<div class="notes-header">
|
||||
<span>Bullet</span>
|
||||
<span>Notiz</span>
|
||||
<span>Priorität</span>
|
||||
<span>Aktionen</span>
|
||||
</div>
|
||||
{% 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>
|
||||
<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">
|
||||
<button class="btn btn-small {{ 'btn-success' if n.status != 'done' else '' }}">
|
||||
{{ '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>
|
||||
<button type="submit" class="btn btn-small btn-future" name="target" value="future">→ Future Log</button>
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted">Noch keine Notizen für diesen Tag.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
50
templates/future.html
Normal file
50
templates/future.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Future Log – Bullet Journal{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Future Log</h1>
|
||||
<p class="muted">Langfristiger Kalender – Aufgaben, die später erledigt werden.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<h2>Neuer Eintrag</h2>
|
||||
<form method="post" action="{{ url_for('journal.future') }}" class="note-form">
|
||||
<div class="note-form-row">
|
||||
<label>
|
||||
Monat
|
||||
<input type="month" name="month" required>
|
||||
</label>
|
||||
<label class="grow">
|
||||
Notiz
|
||||
<input type="text" name="note_text" required placeholder="Eintrag ins Future Log">
|
||||
</label>
|
||||
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% if grouped %}
|
||||
{% for month_key, entries in grouped.items() %}
|
||||
<section class="card">
|
||||
<h2>{{ month_key[5:7]|int }} / {{ month_key[0:4] }}</h2>
|
||||
<ul class="note-list">
|
||||
{% 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>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<section class="card">
|
||||
<p class="muted">Noch keine Einträge im Future Log.</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
49
templates/index.html
Normal file
49
templates/index.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard – Bullet Journal{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<p class="muted">Woche ab {{ monday.strftime('%d.%m.%Y') }}</p>
|
||||
</div>
|
||||
<a href="{{ url_for('journal.week') }}" class="btn">Zur Wochenübersicht</a>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<h2>Diese Woche</h2>
|
||||
<div class="week-grid">
|
||||
{% for day in day_summary %}
|
||||
<a class="day-card" href="{{ url_for('journal.day', date_str=day.date.strftime('%Y-%m-%d')) }}">
|
||||
<span class="day-name">{{ day.weekday }}</span>
|
||||
<span class="day-date">{{ day.date.strftime('%d.%m.') }}</span>
|
||||
<span class="day-stats">
|
||||
{% if day.total > 0 %}
|
||||
<span class="pill open">{{ day.open }} offen</span>
|
||||
<span class="pill done">{{ day.done }} erledigt</span>
|
||||
{% else %}
|
||||
<span class="pill none">leer</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if overdue %}
|
||||
<section class="card">
|
||||
<h2>Überfällige Aufgaben</h2>
|
||||
<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="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>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
19
templates/login.html
Normal file
19
templates/login.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Anmelden – Bullet Journal{% endblock %}
|
||||
{% block content %}
|
||||
<div class="auth-card">
|
||||
<h1>Anmelden</h1>
|
||||
<p class="muted">Bitte melde dich mit deinem Konto an.</p>
|
||||
<form method="post" action="{{ url_for('auth.login') }}" class="stack">
|
||||
<label>
|
||||
Benutzername
|
||||
<input type="text" name="username" required autofocus>
|
||||
</label>
|
||||
<label>
|
||||
Passwort
|
||||
<input type="password" name="password" required>
|
||||
</label>
|
||||
<button type="submit" class="btn btn-primary">Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
61
templates/week.html
Normal file
61
templates/week.html
Normal file
@@ -0,0 +1,61 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Wochenübersicht – Bullet Journal{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Wochenübersicht</h1>
|
||||
<p class="muted">
|
||||
<a href="{{ url_for('journal.week', week=prev_monday.strftime('%G-W%V')) }}" class="btn btn-small">← Vorige</a>
|
||||
Woche ab {{ monday.strftime('%d.%m.%Y') }}
|
||||
<a href="{{ url_for('journal.week', week=next_monday.strftime('%G-W%V')) }}" class="btn btn-small">Nächste →</a>
|
||||
</p>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="muted">Keine offenen Aufgaben in dieser Woche. 🎉</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user