first commit
This commit is contained in:
48
app/templates/auth/login.html
Normal file
48
app/templates/auth/login.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Login · Putzliga{% endblock %}
|
||||
{% block content %}
|
||||
<section class="auth-layout">
|
||||
<div class="hero-card hero-card--brand">
|
||||
<p class="eyebrow">Leichtgewichtige Haushalts-App</p>
|
||||
<h2>Putzliga bringt Punkte, Rhythmus und ein bisschen Liga-Stimmung in den Alltag.</h2>
|
||||
<p>Mehrere Nutzer, wiederkehrende Aufgaben, Monats-Highscore, Archiv, Kalender und PWA-Pushs in einer bewusst schlanken Flask-App.</p>
|
||||
<div class="hero-stats">
|
||||
<div>
|
||||
<strong>Mobile first</strong>
|
||||
<span>Bottom Navigation wie in einer iPhone-App</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Fair verteilt</strong>
|
||||
<span>Punkte landen bei der Person, die wirklich erledigt hat</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Erweiterbar</strong>
|
||||
<span>SQLite, Flask, Jinja und saubere Services statt Overengineering</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="panel auth-panel">
|
||||
<p class="eyebrow">Einloggen</p>
|
||||
<h2>Willkommen zurück</h2>
|
||||
<form method="post" class="form-grid">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="field">
|
||||
{{ form.email.label }}
|
||||
{{ form.email(placeholder="anna@putzliga.local") }}
|
||||
{% for error in form.email.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
<div class="field">
|
||||
{{ form.password.label }}
|
||||
{{ form.password(placeholder="Passwort") }}
|
||||
{% for error in form.password.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
<label class="checkbox">{{ form.remember_me() }} <span>Angemeldet bleiben</span></label>
|
||||
{{ form.submit(class_="button button--wide") }}
|
||||
</form>
|
||||
<p class="inline-note">Demo-Logins nach dem Seeden: `anna@putzliga.local` / `putzliga123` und `ben@putzliga.local` / `putzliga123`.</p>
|
||||
<p class="inline-note">Noch kein Konto? <a href="{{ url_for('auth.register') }}">Neu registrieren</a></p>
|
||||
</section>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
42
app/templates/auth/register.html
Normal file
42
app/templates/auth/register.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Registrieren · Putzliga{% endblock %}
|
||||
{% block content %}
|
||||
<section class="auth-layout">
|
||||
<div class="hero-card hero-card--brand">
|
||||
<p class="eyebrow">Gemeinsam sauberer</p>
|
||||
<h2>Erstelle dein Konto und steig direkt in die Liga ein.</h2>
|
||||
<p>Nach dem Login landest du sofort bei „Meine Aufgaben“ und kannst Aufgaben anlegen, verteilen und Punkte sammeln.</p>
|
||||
</div>
|
||||
|
||||
<section class="panel auth-panel">
|
||||
<p class="eyebrow">Registrieren</p>
|
||||
<h2>Neues Konto</h2>
|
||||
<form method="post" class="form-grid">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="field">
|
||||
{{ form.name.label }}
|
||||
{{ form.name(placeholder="Dein Name") }}
|
||||
{% for error in form.name.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
<div class="field">
|
||||
{{ form.email.label }}
|
||||
{{ form.email(placeholder="mail@example.com") }}
|
||||
{% for error in form.email.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
<div class="field">
|
||||
{{ form.password.label }}
|
||||
{{ form.password(placeholder="Mindestens 6 Zeichen") }}
|
||||
{% for error in form.password.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
<div class="field">
|
||||
{{ form.password_confirm.label }}
|
||||
{{ form.password_confirm(placeholder="Passwort wiederholen") }}
|
||||
{% for error in form.password_confirm.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
{{ form.submit(class_="button button--wide") }}
|
||||
</form>
|
||||
<p class="inline-note">Schon dabei? <a href="{{ url_for('auth.login') }}">Zum Login</a></p>
|
||||
</section>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
121
app/templates/base.html
Normal file
121
app/templates/base.html
Normal file
@@ -0,0 +1,121 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#f5f7ff">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<meta name="apple-mobile-web-app-title" content="{{ app_name }}">
|
||||
<meta name="description" content="Putzliga macht Haushaltsaufgaben leichter, motivierender und fair verteilt.">
|
||||
<title>{% block title %}{{ app_name }}{% endblock %}</title>
|
||||
<link rel="manifest" href="{{ url_for('main.manifest') }}">
|
||||
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='images/favicon.svg') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='images/apple-touch-icon.png') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body data-push-key="{{ config['VAPID_PUBLIC_KEY'] if current_user.is_authenticated else '' }}">
|
||||
{% from "partials/macros.html" import nav_icon %}
|
||||
<div class="app-shell {% if not current_user.is_authenticated %}app-shell--auth{% endif %}">
|
||||
{% if current_user.is_authenticated %}
|
||||
<aside class="sidebar">
|
||||
<a class="brand" href="{{ url_for('tasks.my_tasks') }}">
|
||||
<img src="{{ url_for('static', filename='images/logo.svg') }}" alt="Putzliga Logo" class="brand__logo">
|
||||
<div>
|
||||
<strong>Putzliga</strong>
|
||||
<span>Haushalt mit Punktestand</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<nav class="sidebar-nav" aria-label="Hauptnavigation">
|
||||
{% for endpoint, label, icon in nav_items %}
|
||||
<a href="{{ url_for(endpoint) }}" class="nav-link {% if request.endpoint == endpoint %}is-active{% endif %}">
|
||||
{{ nav_icon(icon) }}
|
||||
<span>{{ label }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
|
||||
<section class="sidebar-card">
|
||||
<div class="sidebar-card__row">
|
||||
<img class="avatar avatar--lg" src="{% if current_user.avatar_path and current_user.avatar_path.startswith('avatars/') %}{{ url_for('main.uploads', filename=current_user.avatar_path) }}{% else %}{{ url_for('static', filename=current_user.display_avatar) }}{% endif %}" alt="Avatar von {{ current_user.name }}">
|
||||
<div>
|
||||
<strong>{{ current_user.name }}</strong>
|
||||
<p>{{ current_user.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a class="text-link" href="{{ url_for('auth.logout') }}">Abmelden</a>
|
||||
</section>
|
||||
</aside>
|
||||
{% endif %}
|
||||
|
||||
<div class="page-shell">
|
||||
<header class="topbar">
|
||||
{% if current_user.is_authenticated %}
|
||||
<div>
|
||||
<p class="eyebrow">Spielerisch sauber bleiben</p>
|
||||
<h1>{% block page_title %}{{ app_name }}{% endblock %}</h1>
|
||||
</div>
|
||||
<a class="topbar-user" href="{{ url_for('settings.index') }}">
|
||||
<span>{{ current_user.name }}</span>
|
||||
<img class="avatar" src="{% if current_user.avatar_path and current_user.avatar_path.startswith('avatars/') %}{{ url_for('main.uploads', filename=current_user.avatar_path) }}{% else %}{{ url_for('static', filename=current_user.display_avatar) }}{% endif %}" alt="">
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="brand brand--public" href="{{ url_for('auth.login') }}">
|
||||
<img src="{{ url_for('static', filename='images/logo.svg') }}" alt="Putzliga Logo" class="brand__logo">
|
||||
<div>
|
||||
<strong>Putzliga</strong>
|
||||
<span>Haushaltsaufgaben mit Liga-Gefühl</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
<main class="content">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="flash-stack">
|
||||
{% for category, message in messages %}
|
||||
<div class="flash flash--{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<nav class="bottom-nav" aria-label="Mobile Navigation">
|
||||
{% for endpoint, label, icon in nav_items %}
|
||||
<a href="{{ url_for(endpoint) }}" class="bottom-nav__item {% if request.endpoint == endpoint %}is-active{% endif %}">
|
||||
{{ nav_icon(icon) }}
|
||||
<span>{{ label }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
|
||||
<dialog class="complete-dialog" id="completeDialog">
|
||||
<form method="dialog" class="complete-dialog__surface">
|
||||
<p class="eyebrow">Punkte fair verbuchen</p>
|
||||
<h2>Wer hat diese Aufgabe erledigt?</h2>
|
||||
<p id="completeDialogText">Bitte wähle aus, wem die Punkte gutgeschrieben werden sollen.</p>
|
||||
<div class="choice-grid">
|
||||
<button type="button" class="button button--secondary" data-complete-choice="assigned">Zugewiesene Person</button>
|
||||
<button type="button" class="button" data-complete-choice="me">Ich</button>
|
||||
</div>
|
||||
<button type="button" class="button button--ghost" id="completeDialogClose">Abbrechen</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<form method="post" class="sr-only" id="completeDialogForm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="completed_for" value="me" id="completeDialogChoice">
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<script src="{{ url_for('static', filename='js/app.js') }}" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
92
app/templates/partials/macros.html
Normal file
92
app/templates/partials/macros.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{% macro nav_icon(name) -%}
|
||||
<span class="nav-icon">{{ icon_svg(name)|safe }}</span>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro status_badge(task) -%}
|
||||
<span class="status-badge status-badge--{{ task.status }}">{{ task.status_label }}</span>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro avatar(user, size='') -%}
|
||||
{% if user %}
|
||||
{% if user.avatar_path and user.avatar_path.startswith('avatars/') %}
|
||||
<img class="avatar {{ size }}" src="{{ url_for('main.uploads', filename=user.avatar_path) }}" alt="Avatar von {{ user.name }}">
|
||||
{% else %}
|
||||
<img class="avatar {{ size }}" src="{{ url_for('static', filename=user.display_avatar) }}" alt="Avatar von {{ user.name }}">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro task_card(task, current_user, compact=false) -%}
|
||||
<article class="task-card {% if compact %}task-card--compact{% endif %}">
|
||||
<div class="task-card__top">
|
||||
<div>
|
||||
<div class="chip-row">
|
||||
{{ status_badge(task) }}
|
||||
<span class="point-pill">{{ task.points_awarded }} Punkte</span>
|
||||
</div>
|
||||
<h3>{{ task.title }}</h3>
|
||||
</div>
|
||||
<a class="icon-button" href="{{ url_for('tasks.edit', task_id=task.id) }}" aria-label="Aufgabe bearbeiten">
|
||||
{{ nav_icon('gear') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if task.description %}
|
||||
<p class="muted">{{ task.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<dl class="task-meta">
|
||||
<div>
|
||||
<dt>Fällig</dt>
|
||||
<dd>{{ task.due_date|date_de }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Zuständig</dt>
|
||||
<dd>{{ task.assigned_user.name if task.assigned_user else 'Unverteilt' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Rhythmus</dt>
|
||||
<dd>{{ task.task_template.recurrence_label }}</dd>
|
||||
</div>
|
||||
{% if task.completed_at %}
|
||||
<div>
|
||||
<dt>Erledigt von</dt>
|
||||
<dd>{{ task.completed_by_user.name if task.completed_by_user else '—' }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
</dl>
|
||||
|
||||
<div class="task-card__footer">
|
||||
<div class="task-assignee">
|
||||
{{ avatar(task.assigned_user) }}
|
||||
<span>{{ task.assigned_user.name if task.assigned_user else 'Ohne Person' }}</span>
|
||||
</div>
|
||||
|
||||
{% if not task.completed_at %}
|
||||
{% if task.assigned_user_id and task.assigned_user_id != current_user.id %}
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
data-complete-action="{{ url_for('tasks.complete', task_id=task.id) }}"
|
||||
data-complete-title="{{ task.title }}"
|
||||
data-complete-assigned="{{ task.assigned_user.name if task.assigned_user else 'Zugewiesene Person' }}"
|
||||
>
|
||||
{{ nav_icon('check') }}
|
||||
<span>Erledigen</span>
|
||||
</button>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url_for('tasks.complete', task_id=task.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="completed_for" value="me">
|
||||
<button type="submit" class="button">
|
||||
{{ nav_icon('check') }}
|
||||
<span>Erledigen</span>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="done-hint">Am {{ task.completed_at|datetime_de }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
{%- endmacro %}
|
||||
84
app/templates/scoreboard/index.html
Normal file
84
app/templates/scoreboard/index.html
Normal file
@@ -0,0 +1,84 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "partials/macros.html" import avatar %}
|
||||
{% block title %}Highscoreboard · Putzliga{% endblock %}
|
||||
{% block page_title %}Highscoreboard{% endblock %}
|
||||
{% block content %}
|
||||
<section class="hero-grid">
|
||||
<article class="hero-card">
|
||||
<p class="eyebrow">Aktueller Monat</p>
|
||||
<h2>{{ current_label }}</h2>
|
||||
<p>Aufgabenpunkte und Badge-Boni zählen nur im aktuellen Monat. Zum Monatswechsel landet alles im Archiv und die Liga startet wieder bei null.</p>
|
||||
</article>
|
||||
<article class="panel highlight-panel">
|
||||
<p class="eyebrow">Archiv</p>
|
||||
<form method="get" class="inline-form">
|
||||
<select name="archive">
|
||||
{% for year, month in archive_options %}
|
||||
{% set archive_value = year ~ '-' ~ '%02d'|format(month) %}
|
||||
<option value="{{ archive_value }}" {% if selected_archive == archive_value %}selected{% endif %}>{{ month|month_name }} {{ year }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="button button--secondary">Anzeigen</button>
|
||||
</form>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="scoreboard">
|
||||
{% for row in current_rows %}
|
||||
<article class="score-row {% if row.rank == 1 %}score-row--leader{% endif %}">
|
||||
<div class="score-row__head">
|
||||
<div class="score-row__person">
|
||||
<span class="rank-badge">#{{ row.rank }}</span>
|
||||
{{ avatar(row.user) }}
|
||||
<div>
|
||||
<strong>{{ row.user.name }}</strong>
|
||||
<p>{{ row.completed_tasks_count }} erledigte Aufgaben</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="score-row__points">
|
||||
<strong>{{ row.total_points }}</strong>
|
||||
<span>Punkte</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="score-bar">
|
||||
<span style="width: {{ 0 if max_points == 0 else (row.total_points / max_points * 100) }}%"></span>
|
||||
</div>
|
||||
<div class="score-row__meta">
|
||||
<span>Basis: {{ row.base_points }}</span>
|
||||
<span>Badges: +{{ row.bonus_points }}</span>
|
||||
</div>
|
||||
{% if row.badges %}
|
||||
<div class="badge-cloud">
|
||||
{% for badge in row.badges %}
|
||||
<span class="reward-chip">{{ badge.definition.name }} +{{ badge.bonus_points }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% else %}
|
||||
<div class="empty-state">Noch keine Punkte in diesem Monat.</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<p class="eyebrow">Monatsarchiv</p>
|
||||
<h2>{{ archive_label or 'Noch kein Archiv' }}</h2>
|
||||
<div class="archive-list">
|
||||
{% for row in archived_rows %}
|
||||
<article class="archive-row">
|
||||
<div class="archive-row__left">
|
||||
<span class="rank-badge">#{{ row.rank }}</span>
|
||||
{{ avatar(row.user) }}
|
||||
<strong>{{ row.user.name }}</strong>
|
||||
</div>
|
||||
<div class="archive-row__right">
|
||||
<span>{{ row.total_points }} Punkte</span>
|
||||
<small>{{ row.completed_tasks_count }} Aufgaben</small>
|
||||
</div>
|
||||
</article>
|
||||
{% else %}
|
||||
<div class="empty-state">Sobald ein Monat archiviert wurde, taucht er hier auf.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
100
app/templates/settings/index.html
Normal file
100
app/templates/settings/index.html
Normal file
@@ -0,0 +1,100 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "partials/macros.html" import avatar, nav_icon %}
|
||||
{% block title %}Optionen · Putzliga{% endblock %}
|
||||
{% block page_title %}Optionen{% endblock %}
|
||||
{% block content %}
|
||||
<section class="two-column">
|
||||
<article class="panel">
|
||||
<p class="eyebrow">Profil & Benachrichtigungen</p>
|
||||
<h2>Persönliche Einstellungen</h2>
|
||||
<form method="post" enctype="multipart/form-data" class="form-grid">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="field">
|
||||
{{ form.name.label }}
|
||||
{{ form.name() }}
|
||||
{% for error in form.name.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
<div class="field">
|
||||
{{ form.email.label }}
|
||||
{{ form.email() }}
|
||||
{% for error in form.email.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
<div class="field">
|
||||
{{ form.password.label }}
|
||||
{{ form.password(placeholder="Leer lassen, wenn nichts geändert werden soll") }}
|
||||
{% for error in form.password.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
<div class="field">
|
||||
{{ form.avatar.label }}
|
||||
{{ form.avatar() }}
|
||||
{% for error in form.avatar.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
<label class="checkbox">
|
||||
{{ form.notification_task_due_enabled() }}
|
||||
<span>Push für heute oder morgen fällige Aufgaben</span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
{{ form.notification_monthly_winner_enabled() }}
|
||||
<span>Push zum Monatssieger am 1. um 09:00 Uhr</span>
|
||||
</label>
|
||||
{{ form.submit(class_='button') }}
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<p class="eyebrow">Push & App-Install</p>
|
||||
<h2>Web-Push vorbereiten</h2>
|
||||
<p class="muted">Putzliga nutzt echten Web-Push mit Service Worker und gespeicherten Subscriptions. Auf iPhone funktioniert das nur, wenn die App zum Home-Bildschirm hinzugefügt wurde.</p>
|
||||
<div class="push-box">
|
||||
<div class="push-box__state {% if push_ready %}is-ready{% else %}is-disabled{% endif %}">
|
||||
{{ nav_icon('bell') }}
|
||||
<div>
|
||||
<strong>{% if push_ready %}VAPID konfiguriert{% else %}VAPID fehlt{% endif %}</strong>
|
||||
<p>{% if push_ready %}Push kann im Browser aktiviert werden.{% else %}Bitte zuerst Public/Private Key in der Umgebung setzen.{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="button button--wide"
|
||||
id="pushToggle"
|
||||
{% if not push_ready %}disabled{% endif %}
|
||||
data-subscribed="{{ '1' if has_subscription else '0' }}"
|
||||
>
|
||||
{% if has_subscription %}Push deaktivieren{% else %}Push aktivieren{% endif %}
|
||||
</button>
|
||||
<p class="inline-note" id="pushHint">
|
||||
Auf iPhone/iPad bitte zuerst in Safari zum Home-Bildschirm hinzufügen und als Web-App öffnen.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<p class="eyebrow">Gamification</p>
|
||||
<h2>Badge-Regeln pflegen</h2>
|
||||
<div class="badge-settings">
|
||||
{% for badge in badges %}
|
||||
<form method="post" action="{{ url_for('settings.update_badge', badge_id=badge.id) }}" class="badge-setting-card">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div>
|
||||
<strong>{{ badge.name }}</strong>
|
||||
<p class="muted">{{ badge.description }}</p>
|
||||
</div>
|
||||
<div class="field field--compact">
|
||||
<label>Schwelle</label>
|
||||
<input type="number" name="threshold" min="1" value="{{ badge.threshold }}">
|
||||
</div>
|
||||
<div class="field field--compact">
|
||||
<label>Bonus</label>
|
||||
<input type="number" name="bonus_points" min="0" value="{{ badge.bonus_points }}">
|
||||
</div>
|
||||
<label class="checkbox checkbox--compact">
|
||||
<input type="checkbox" name="active" {% if badge.active %}checked{% endif %}>
|
||||
<span>Aktiv</span>
|
||||
</label>
|
||||
<button type="submit" class="button button--secondary">Badge speichern</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
51
app/templates/tasks/all_tasks.html
Normal file
51
app/templates/tasks/all_tasks.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "partials/macros.html" import task_card %}
|
||||
{% block title %}Alle Aufgaben · Putzliga{% endblock %}
|
||||
{% block page_title %}Alle Aufgaben{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<form method="get" class="filter-bar">
|
||||
<div class="field field--compact">
|
||||
<label for="status">Status</label>
|
||||
<select name="status" id="status">
|
||||
<option value="all" {% if filters.status == 'all' %}selected{% endif %}>Alle</option>
|
||||
<option value="open" {% if filters.status == 'open' %}selected{% endif %}>Offen</option>
|
||||
<option value="soon" {% if filters.status == 'soon' %}selected{% endif %}>Bald fällig</option>
|
||||
<option value="overdue" {% if filters.status == 'overdue' %}selected{% endif %}>Überfällig</option>
|
||||
<option value="completed" {% if filters.status == 'completed' %}selected{% endif %}>Erledigt</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field field--compact">
|
||||
<label for="user_id">Nutzer</label>
|
||||
<select name="user_id" id="user_id">
|
||||
<option value="">Alle</option>
|
||||
{% for user in users %}
|
||||
<option value="{{ user.id }}" {% if filters.user_id == user.id %}selected{% endif %}>{{ user.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field field--compact">
|
||||
<label for="sort">Sortierung</label>
|
||||
<select name="sort" id="sort">
|
||||
<option value="due" {% if filters.sort == 'due' %}selected{% endif %}>Fälligkeit</option>
|
||||
<option value="points" {% if filters.sort == 'points' %}selected{% endif %}>Punkte</option>
|
||||
<option value="user" {% if filters.sort == 'user' %}selected{% endif %}>Nutzer</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="checkbox checkbox--compact">
|
||||
<input type="checkbox" name="mine" value="1" {% if filters.mine == '1' %}checked{% endif %}>
|
||||
<span>Nur meine</span>
|
||||
</label>
|
||||
<button type="submit" class="button">Filter anwenden</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="task-grid">
|
||||
{% for task in tasks %}
|
||||
{{ task_card(task, current_user) }}
|
||||
{% else %}
|
||||
<div class="empty-state">Für diese Filter gibt es gerade keine Aufgaben.</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
66
app/templates/tasks/calendar.html
Normal file
66
app/templates/tasks/calendar.html
Normal file
@@ -0,0 +1,66 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "partials/macros.html" import status_badge %}
|
||||
{% block title %}Kalender · Putzliga{% endblock %}
|
||||
{% block page_title %}Kalender & Liste{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel panel--toolbar">
|
||||
<div>
|
||||
<p class="eyebrow">Monatsansicht</p>
|
||||
<h2>{{ current_label }}</h2>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<a class="button button--secondary" href="{{ url_for('tasks.calendar_view', year=current_year if current_month > 1 else current_year - 1, month=current_month - 1 if current_month > 1 else 12, view=view) }}">Zurück</a>
|
||||
<a class="button button--secondary" href="{{ url_for('tasks.calendar_view', year=current_year if current_month < 12 else current_year + 1, month=current_month + 1 if current_month < 12 else 1, view=view) }}">Weiter</a>
|
||||
<div class="segmented">
|
||||
<a href="{{ url_for('tasks.calendar_view', year=current_year, month=current_month, view='calendar') }}" class="{% if view == 'calendar' %}is-active{% endif %}">Kalender</a>
|
||||
<a href="{{ url_for('tasks.calendar_view', year=current_year, month=current_month, view='list') }}" class="{% if view == 'list' %}is-active{% endif %}">Liste</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if view == 'calendar' %}
|
||||
<section class="calendar-grid">
|
||||
<div class="calendar-grid__weekdays">
|
||||
{% for label in ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'] %}
|
||||
<span>{{ label }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% for week in month_calendar %}
|
||||
{% for day in week %}
|
||||
<article class="calendar-day {% if day == 0 %}calendar-day--empty{% endif %}">
|
||||
{% if day != 0 %}
|
||||
<strong>{{ day }}</strong>
|
||||
<div class="calendar-day__tasks">
|
||||
{% for task in tasks_by_day.get(day, []) %}
|
||||
<a href="{{ url_for('tasks.edit', task_id=task.id) }}" class="calendar-task calendar-task--{{ task.status }}">
|
||||
<span>{{ task.title }}</span>
|
||||
<small>{{ task.points_awarded }} P</small>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="stack">
|
||||
{% for task in tasks %}
|
||||
<article class="panel list-row">
|
||||
<div>
|
||||
<strong>{{ task.title }}</strong>
|
||||
<p class="muted">{{ task.description or 'Ohne Zusatzbeschreibung' }}</p>
|
||||
</div>
|
||||
<div class="list-row__meta">
|
||||
{{ status_badge(task) }}
|
||||
<span>{{ task.due_date|date_de }}</span>
|
||||
<a href="{{ url_for('tasks.edit', task_id=task.id) }}" class="text-link">Bearbeiten</a>
|
||||
</div>
|
||||
</article>
|
||||
{% else %}
|
||||
<div class="empty-state">In diesem Monat sind noch keine Aufgaben hinterlegt.</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
88
app/templates/tasks/my_tasks.html
Normal file
88
app/templates/tasks/my_tasks.html
Normal file
@@ -0,0 +1,88 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "partials/macros.html" import task_card, nav_icon %}
|
||||
{% block title %}Meine Aufgaben · Putzliga{% endblock %}
|
||||
{% block page_title %}Meine Aufgaben{% endblock %}
|
||||
{% block content %}
|
||||
<section class="hero-grid">
|
||||
<article class="hero-card">
|
||||
<p class="eyebrow">Heute im Fokus</p>
|
||||
<h2>{{ current_user.name }}, deine Liga für den Haushalt läuft.</h2>
|
||||
<p>Erledige offene Aufgaben, sammle Punkte und halte deinen Monatslauf stabil.</p>
|
||||
<div class="progress-card">
|
||||
<div class="progress-card__top">
|
||||
<span>Erledigungsquote</span>
|
||||
<strong>{{ completion_ratio }}%</strong>
|
||||
</div>
|
||||
<div class="progress"><span style="width: {{ completion_ratio }}%"></span></div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="panel highlight-panel">
|
||||
<p class="eyebrow">Schnellzugriff</p>
|
||||
<a class="button button--wide" href="{{ url_for('tasks.create') }}">
|
||||
{{ nav_icon('plus') }}
|
||||
<span>Neue Aufgabe anlegen</span>
|
||||
</a>
|
||||
<a class="button button--secondary button--wide" href="{{ url_for('scoreboard.index') }}">
|
||||
{{ nav_icon('trophy') }}
|
||||
<span>Zum aktuellen Highscore</span>
|
||||
</a>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="stack">
|
||||
<div class="section-heading">
|
||||
<h2>Überfällig</h2>
|
||||
<span class="section-heading__count">{{ sections.overdue|length }}</span>
|
||||
</div>
|
||||
<div class="task-grid">
|
||||
{% for task in sections.overdue %}
|
||||
{{ task_card(task, current_user) }}
|
||||
{% else %}
|
||||
<div class="empty-state">Nichts überfällig. Genau so darf es bleiben.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stack">
|
||||
<div class="section-heading">
|
||||
<h2>Bald fällig</h2>
|
||||
<span class="section-heading__count">{{ sections.soon|length }}</span>
|
||||
</div>
|
||||
<div class="task-grid">
|
||||
{% for task in sections.soon %}
|
||||
{{ task_card(task, current_user) }}
|
||||
{% else %}
|
||||
<div class="empty-state">Gerade nichts, was in den nächsten Tagen drängt.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stack">
|
||||
<div class="section-heading">
|
||||
<h2>Offen</h2>
|
||||
<span class="section-heading__count">{{ sections.open|length }}</span>
|
||||
</div>
|
||||
<div class="task-grid">
|
||||
{% for task in sections.open %}
|
||||
{{ task_card(task, current_user) }}
|
||||
{% else %}
|
||||
<div class="empty-state">Alles leer. Zeit für eine kleine Siegerrunde.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stack">
|
||||
<div class="section-heading">
|
||||
<h2>Erledigt</h2>
|
||||
<span class="section-heading__count">{{ sections.completed|length }}</span>
|
||||
</div>
|
||||
<div class="task-grid">
|
||||
{% for task in sections.completed %}
|
||||
{{ task_card(task, current_user, compact=true) }}
|
||||
{% else %}
|
||||
<div class="empty-state">Noch keine erledigten Aufgaben in deiner Liste.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
64
app/templates/tasks/task_form.html
Normal file
64
app/templates/tasks/task_form.html
Normal file
@@ -0,0 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{% if mode == 'edit' %}Aufgabe bearbeiten{% else %}Aufgabe erstellen{% endif %} · Putzliga{% endblock %}
|
||||
{% block page_title %}{% if mode == 'edit' %}Aufgabe bearbeiten{% else %}Aufgabe erstellen{% endif %}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel form-panel">
|
||||
<p class="eyebrow">{% if mode == 'edit' %}Bestehende Aufgabe anpassen{% else %}Neue Aufgabe und Vorlage{% endif %}</p>
|
||||
<h2>{% if mode == 'edit' %}Änderungen für {{ task.title }}{% else %}Neue Aufgabe anlegen{% endif %}</h2>
|
||||
<form method="post" class="form-grid form-grid--two">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="field field--full">
|
||||
{{ form.title.label }}
|
||||
{{ form.title(placeholder="Zum Beispiel: Küche wischen") }}
|
||||
{% for error in form.title.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="field field--full">
|
||||
{{ form.description.label }}
|
||||
{{ form.description(rows="4", placeholder="Optional: kurze Hinweise zur Aufgabe") }}
|
||||
{% for error in form.description.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
{{ form.default_points.label }}
|
||||
{{ form.default_points() }}
|
||||
{% for error in form.default_points.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
{{ form.assigned_user_id.label }}
|
||||
{{ form.assigned_user_id() }}
|
||||
{% for error in form.assigned_user_id.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
{{ form.due_date.label }}
|
||||
{{ form.due_date() }}
|
||||
{% for error in form.due_date.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
{{ form.recurrence_interval_unit.label }}
|
||||
{{ form.recurrence_interval_unit() }}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
{{ form.recurrence_interval_value.label }}
|
||||
{{ form.recurrence_interval_value() }}
|
||||
{% for error in form.recurrence_interval_value.errors %}<small class="error">{{ error }}</small>{% endfor %}
|
||||
</div>
|
||||
|
||||
<label class="checkbox">
|
||||
{{ form.active() }}
|
||||
<span>Vorlage bleibt aktiv und erzeugt bei Wiederholung weitere Aufgaben</span>
|
||||
</label>
|
||||
|
||||
<div class="form-actions field--full">
|
||||
{{ form.submit(class_='button') }}
|
||||
<a class="button button--ghost" href="{{ url_for('tasks.all_tasks') }}">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user