2 Commits

Author SHA1 Message Date
hnzio 2f2e543a79 feat: refine quick wins workflow and calendar layout 2026-04-15 12:17:58 +02:00
hnzio 4aa4447c01 feat: add shared quick wins workflow 2026-04-15 11:49:46 +02:00
18 changed files with 736 additions and 159 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
"author": "hnzio <mail@example.com>", "author": "hnzio <mail@example.com>",
"description": "Spielerische Haushalts-App mit Aufgaben, Punkten, Monats-Highscore, Kalender und PWA-Push.", "description": "Spielerische Haushalts-App mit Aufgaben, Punkten, Monats-Highscore, Kalender und PWA-Push.",
"tagline": "Haushalt mit Liga-Gefühl", "tagline": "Haushalt mit Liga-Gefühl",
"version": "0.6.0", "version": "0.6.5",
"manifestVersion": 2, "manifestVersion": 2,
"healthCheckPath": "/healthz", "healthCheckPath": "/healthz",
"httpPort": 8000, "httpPort": 8000,
+23 -5
View File
@@ -7,7 +7,7 @@ Putzliga ist eine moderne, leichte Haushaltsaufgaben-Web-App mit spielerischem C
- Mehrere Nutzer mit Login, Admin-Nutzermanagement und Profil-/Avatar-Einstellungen - Mehrere Nutzer mit Login, Admin-Nutzermanagement und Profil-/Avatar-Einstellungen
- Trennung zwischen `TaskTemplate` und `TaskInstance` - Trennung zwischen `TaskTemplate` und `TaskInstance`
- Aufgaben anlegen, bearbeiten, zuweisen und erledigen - Aufgaben anlegen, bearbeiten, zuweisen und erledigen
- globale Schnellaufgabe per Plus-Button mit Titel + Aufwand und automatisch passender Punktezahl - globale Quick-Wins per Plus-Button mit gemeinsamen Vorlagen und freiem „Sonstiges“-Fallback
- Wiederholungen für einmalig, alle X Tage, alle X Wochen und alle X Monate - Wiederholungen für einmalig, alle X Tage, alle X Wochen und alle X Monate
- Saubere Erledigungslogik für fremd zugewiesene Aufgaben mit Auswahl, wer wirklich erledigt hat - Saubere Erledigungslogik für fremd zugewiesene Aufgaben mit Auswahl, wer wirklich erledigt hat
- Statuslogik für offen, bald fällig, überfällig und erledigt - Statuslogik für offen, bald fällig, überfällig und erledigt
@@ -129,11 +129,13 @@ Admins können Nutzer zusätzlich direkt in der App unter `Optionen -> Profil &
Badges werden dauerhaft pro Nutzer gespeichert und automatisch freigeschaltet. Die Badge-Regeln werden für Admins auf einer eigenen Seite unter `Optionen -> Badges` gepflegt. Badges werden dauerhaft pro Nutzer gespeichert und automatisch freigeschaltet. Die Badge-Regeln werden für Admins auf einer eigenen Seite unter `Optionen -> Badges` gepflegt.
## Schnellaufgabe ## Quick-Wins
Über den global sichtbaren Plus-Button rechts unten kannst du auf jeder eingeloggten Seite eine Schnellaufgabe anlegen. Über den global sichtbaren Plus-Button rechts unten kannst du auf jeder eingeloggten Seite Quick-Wins nutzen.
- nur `Titel` und `Aufwand` - gemeinsame Quick-Wins sind für alle Nutzer sichtbar und direkt klickbar
- alle Nutzer können im separaten Optionen-Tab neue Quick-Wins anlegen
- für `Sonstiges (bitte auch nutzen)` lassen sich Titel und Aufwand frei wählen
- die Aufgabe wird automatisch dem gerade eingeloggten Nutzer zugewiesen - die Aufgabe wird automatisch dem gerade eingeloggten Nutzer zugewiesen
- Fälligkeit ist direkt `heute` - Fälligkeit ist direkt `heute`
- die Punkte hängen vom Aufwand ab - die Punkte hängen vom Aufwand ab
@@ -144,8 +146,9 @@ Die Aufwand-Stufen sind:
- Normal - Normal
- Dauert etwas - Dauert etwas
- Aufwendig - Aufwendig
- Super aufwendig
Admins können die Punkte je Aufwand unter `Optionen -> Profil & Team` anpassen. Admins können die Punkte je Aufwand unter `Optionen -> Quick-Wins` anpassen.
### 5. Entwicklungsserver starten ### 5. Entwicklungsserver starten
@@ -343,6 +346,21 @@ Der ausgegebene `VAPID_PRIVATE_KEY` ist bereits `.env`-freundlich mit escaped Ne
## Release Notes ## Release Notes
### 0.6.5
- Quick-Wins als gemeinsames Team-Feature ausgebaut
- neuer Optionen-Tab zum Anlegen und Verwalten gemeinsamer Quick-Wins
- bestehende Quick-Wins in den Optionen direkt bearbeitbar gemacht
- Plus-Dialog von Einzelkarten auf kompakte auswählbare Quick-Win-Chips umgestellt
- mehrere Quick-Wins lassen sich gesammelt als erledigt speichern
- „Sonstiges“ blendet Titel und Aufwand jetzt nur bei Auswahl ein
- neue Aufwand-Stufe `super aufwendig`
- Quick-Win-Popup visuell mit übernommenem Sparkles-Icon aus `heinz.marketing` aufgewertet
- Kalenderdarstellung für lange deutsche Begriffe und Namen bei der Worttrennung nachgeschärft
- deutsche Silbentrennung serverseitig vorbereitet, mit optionalem `pyphen`-Fallback ohne Startfehler im lokalen Dev-Setup
- Footer auf Versionslink, Herkunftshinweis und `hnz.io`-Verweis umgebaut
- Cloudron-Version auf `0.6.5` angehoben
### 0.6.0 ### 0.6.0
- Persönlicher read-only ICS-Feed pro Nutzer für externe Kalender ergänzt - Persönlicher read-only ICS-Feed pro Nutzer für externe Kalender ergänzt
+46
View File
@@ -4,12 +4,18 @@ import json
from pathlib import Path from pathlib import Path
from flask import Flask from flask import Flask
from markupsafe import escape
try:
import pyphen
except ModuleNotFoundError: # pragma: no cover - optional dependency in local dev
pyphen = None
from config import Config from config import Config
from .cli import register_cli, seed_badges from .cli import register_cli, seed_badges
from .extensions import csrf, db, login_manager from .extensions import csrf, db, login_manager
from .forms import QuickTaskForm from .forms import QuickTaskForm
from .models import QuickWin
from .routes import auth, main, scoreboard, settings, tasks from .routes import auth, main, scoreboard, settings, tasks
from .routes.main import load_icon_svg from .routes.main import load_icon_svg
from .services.app_settings import get_quick_task_config from .services.app_settings import get_quick_task_config
@@ -18,6 +24,12 @@ from .services.bootstrap import ensure_schema_and_admins
from .services.dates import MONTH_NAMES, local_now from .services.dates import MONTH_NAMES, local_now
from .services.monthly import archive_months_missing_up_to_previous from .services.monthly import archive_months_missing_up_to_previous
DE_HYPHENATOR = pyphen.Pyphen(lang="de_DE") if pyphen else None
def _fallback_soft_hyphenate(word: str) -> str:
return word
def create_app(config_class: type[Config] = Config) -> Flask: def create_app(config_class: type[Config] = Config) -> Flask:
app = Flask(__name__, static_folder="static", template_folder="templates") app = Flask(__name__, static_folder="static", template_folder="templates")
@@ -64,6 +76,7 @@ def create_app(config_class: type[Config] = Config) -> Flask:
(key, values["label"]) (key, values["label"])
for key, values in quick_task_config.items() for key, values in quick_task_config.items()
] ]
quick_wins = QuickWin.query.filter_by(active=True).order_by(QuickWin.id.asc()).all()
def asset_version(filename: str) -> int: def asset_version(filename: str) -> int:
path = Path(app.static_folder) / filename path = Path(app.static_folder) / filename
try: try:
@@ -94,6 +107,7 @@ def create_app(config_class: type[Config] = Config) -> Flask:
"now_local": local_now(), "now_local": local_now(),
"quick_task_form": quick_task_form, "quick_task_form": quick_task_form,
"quick_task_config": quick_task_config, "quick_task_config": quick_task_config,
"quick_wins": quick_wins,
} }
@app.template_filter("date_de") @app.template_filter("date_de")
@@ -108,4 +122,36 @@ def create_app(config_class: type[Config] = Config) -> Flask:
def month_name(value): def month_name(value):
return MONTH_NAMES[value] return MONTH_NAMES[value]
@app.template_filter("hyphenate_de")
def hyphenate_de(value):
if not value:
return ""
text = str(value)
parts: list[str] = []
current = []
def flush_word():
if not current:
return
word = "".join(current)
if len(word) >= 6:
if DE_HYPHENATOR:
parts.append(DE_HYPHENATOR.inserted(word, "\u00AD"))
else:
parts.append(_fallback_soft_hyphenate(word))
else:
parts.append(word)
current.clear()
for char in text:
if char.isalpha() or char in "ÄÖÜäöüß":
current.append(char)
else:
flush_word()
parts.append(char)
flush_word()
return escape("".join(parts))
return app return app
+15 -3
View File
@@ -115,13 +115,13 @@ class AdminUserForm(FlaskForm):
class QuickTaskForm(FlaskForm): class QuickTaskForm(FlaskForm):
title = StringField("Titel", validators=[DataRequired(), Length(min=2, max=160)]) title = StringField("Titel", validators=[Optional(), Length(min=2, max=160)])
effort = SelectField( effort = SelectField(
"Aufwand", "Aufwand",
choices=[(key, key) for key, _, _ in QUICK_TASK_EFFORTS], choices=[(key, key) for key, _, _ in QUICK_TASK_EFFORTS],
validators=[DataRequired()], validators=[Optional()],
) )
submit = SubmitField("Schnellaufgabe speichern") submit = SubmitField("Quick-Win speichern")
class QuickTaskConfigForm(FlaskForm): class QuickTaskConfigForm(FlaskForm):
@@ -133,4 +133,16 @@ class QuickTaskConfigForm(FlaskForm):
medium_points = IntegerField("Dauert etwas", validators=[DataRequired(), NumberRange(min=1, max=500)]) medium_points = IntegerField("Dauert etwas", validators=[DataRequired(), NumberRange(min=1, max=500)])
heavy_label = StringField("Bezeichnung", validators=[DataRequired(), Length(min=2, max=60)]) heavy_label = StringField("Bezeichnung", validators=[DataRequired(), Length(min=2, max=60)])
heavy_points = IntegerField("Aufwendig", validators=[DataRequired(), NumberRange(min=1, max=500)]) heavy_points = IntegerField("Aufwendig", validators=[DataRequired(), NumberRange(min=1, max=500)])
super_heavy_label = StringField("Bezeichnung", validators=[DataRequired(), Length(min=2, max=60)])
super_heavy_points = IntegerField("Super aufwendig", validators=[DataRequired(), NumberRange(min=1, max=500)])
submit = SubmitField("Aufwand speichern") submit = SubmitField("Aufwand speichern")
class QuickWinForm(FlaskForm):
title = StringField("Quick-Win", validators=[DataRequired(), Length(min=2, max=160)])
effort = SelectField(
"Aufwand",
choices=[(key, key) for key, _, _ in QUICK_TASK_EFFORTS],
validators=[DataRequired()],
)
submit = SubmitField("Quick-Win speichern")
+9
View File
@@ -48,6 +48,7 @@ class User(UserMixin, TimestampMixin, db.Model):
lazy=True, lazy=True,
) )
subscriptions = db.relationship("PushSubscription", backref="user", lazy=True, cascade="all, delete-orphan") subscriptions = db.relationship("PushSubscription", backref="user", lazy=True, cascade="all, delete-orphan")
created_quick_wins = db.relationship("QuickWin", backref="created_by_user", lazy=True)
awarded_badges = db.relationship( awarded_badges = db.relationship(
"UserBadge", "UserBadge",
backref="user", backref="user",
@@ -106,6 +107,14 @@ class TaskTemplate(TimestampMixin, db.Model):
return f"Alle {self.recurrence_interval_value} {unit_label}" return f"Alle {self.recurrence_interval_value} {unit_label}"
class QuickWin(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(160), nullable=False, index=True)
effort = db.Column(db.String(40), nullable=False, index=True)
active = db.Column(db.Boolean, nullable=False, default=True)
created_by_user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False, index=True)
class TaskInstance(TimestampMixin, db.Model): class TaskInstance(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
task_template_id = db.Column(db.Integer, db.ForeignKey("task_template.id"), nullable=False, index=True) task_template_id = db.Column(db.Integer, db.ForeignKey("task_template.id"), nullable=False, index=True)
+107 -18
View File
@@ -8,8 +8,8 @@ from flask_login import current_user, login_required
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from ..extensions import csrf, db from ..extensions import csrf, db
from ..forms import AdminUserForm, QuickTaskConfigForm, SettingsProfileForm from ..forms import AdminUserForm, QuickTaskConfigForm, QuickWinForm, SettingsProfileForm
from ..models import BadgeDefinition, MonthlyScoreSnapshot, NotificationLog, PushSubscription, TaskInstance, TaskTemplate, User from ..models import BadgeDefinition, MonthlyScoreSnapshot, NotificationLog, PushSubscription, QuickWin, TaskInstance, TaskTemplate, User
from ..services.app_settings import get_quick_task_config, set_setting_int, set_setting_str from ..services.app_settings import get_quick_task_config, set_setting_int, set_setting_str
from ..services.badges import earned_badges_for_user from ..services.badges import earned_badges_for_user
from ..services.notifications import push_enabled from ..services.notifications import push_enabled
@@ -26,7 +26,10 @@ def _require_admin():
def _settings_tabs(): def _settings_tabs():
tabs = [("settings.index", "Profil & Team", "gear")] tabs = [
("settings.index", "Profil & Team", "gear"),
("settings.quick_wins", "Quick-Wins", "plus"),
]
if current_user.is_admin: if current_user.is_admin:
tabs.append(("settings.badges", "Badges", "award")) tabs.append(("settings.badges", "Badges", "award"))
return tabs return tabs
@@ -48,17 +51,6 @@ def index():
current_user.ensure_calendar_feed_token() current_user.ensure_calendar_feed_token()
form = SettingsProfileForm(original_email=current_user.email, obj=current_user) form = SettingsProfileForm(original_email=current_user.email, obj=current_user)
admin_form = AdminUserForm(prefix="admin") admin_form = AdminUserForm(prefix="admin")
quick_task_config_form = QuickTaskConfigForm(prefix="quickconfig")
quick_task_config = get_quick_task_config()
if request.method == "GET":
quick_task_config_form.fast_label.data = quick_task_config["fast"]["label"]
quick_task_config_form.fast_points.data = quick_task_config["fast"]["points"]
quick_task_config_form.normal_label.data = quick_task_config["normal"]["label"]
quick_task_config_form.normal_points.data = quick_task_config["normal"]["points"]
quick_task_config_form.medium_label.data = quick_task_config["medium"]["label"]
quick_task_config_form.medium_points.data = quick_task_config["medium"]["points"]
quick_task_config_form.heavy_label.data = quick_task_config["heavy"]["label"]
quick_task_config_form.heavy_points.data = quick_task_config["heavy"]["points"]
if form.validate_on_submit(): if form.validate_on_submit():
current_user.name = form.name.data.strip() current_user.name = form.name.data.strip()
current_user.email = form.email.data.lower().strip() current_user.email = form.email.data.lower().strip()
@@ -77,8 +69,6 @@ def index():
"settings/index.html", "settings/index.html",
form=form, form=form,
admin_form=admin_form, admin_form=admin_form,
quick_task_config_form=quick_task_config_form,
quick_task_config=quick_task_config,
users=User.query.order_by(User.is_admin.desc(), User.name.asc()).all(), users=User.query.order_by(User.is_admin.desc(), User.name.asc()).all(),
earned_badges=earned_badges_for_user(current_user.id), earned_badges=earned_badges_for_user(current_user.id),
calendar_feed_url=url_for("main.calendar_feed", token=current_user.calendar_feed_token, _external=True), calendar_feed_url=url_for("main.calendar_feed", token=current_user.calendar_feed_token, _external=True),
@@ -89,6 +79,53 @@ def index():
) )
@bp.route("/quick-wins", methods=["GET", "POST"])
@login_required
def quick_wins():
quick_win_form = QuickWinForm(prefix="quickwin")
quick_task_config_form = QuickTaskConfigForm(prefix="quickconfig")
quick_task_config = get_quick_task_config()
quick_win_form.effort.choices = [(key, values["label"]) for key, values in quick_task_config.items()]
quick_task_config_form.fast_label.data = quick_task_config["fast"]["label"]
quick_task_config_form.fast_points.data = quick_task_config["fast"]["points"]
quick_task_config_form.normal_label.data = quick_task_config["normal"]["label"]
quick_task_config_form.normal_points.data = quick_task_config["normal"]["points"]
quick_task_config_form.medium_label.data = quick_task_config["medium"]["label"]
quick_task_config_form.medium_points.data = quick_task_config["medium"]["points"]
quick_task_config_form.heavy_label.data = quick_task_config["heavy"]["label"]
quick_task_config_form.heavy_points.data = quick_task_config["heavy"]["points"]
quick_task_config_form.super_heavy_label.data = quick_task_config["super_heavy"]["label"]
quick_task_config_form.super_heavy_points.data = quick_task_config["super_heavy"]["points"]
if quick_win_form.validate_on_submit():
existing_quick_win = QuickWin.query.filter_by(title=quick_win_form.title.data.strip(), active=True).first()
if existing_quick_win:
flash("Diesen Quick-Win gibt es bereits.", "error")
return redirect(url_for("settings.quick_wins"))
quick_win = QuickWin(
title=quick_win_form.title.data.strip(),
effort=quick_win_form.effort.data,
active=True,
created_by_user_id=current_user.id,
)
db.session.add(quick_win)
db.session.commit()
flash(f"Quick-Win „{quick_win.title}“ wurde gespeichert.", "success")
return redirect(url_for("settings.quick_wins"))
return render_template(
"settings/quick_wins.html",
quick_win_form=quick_win_form,
quick_task_config_form=quick_task_config_form,
quick_task_config=quick_task_config,
quick_wins=QuickWin.query.filter_by(active=True).order_by(QuickWin.id.asc()).all(),
settings_tabs=_settings_tabs(),
active_settings_tab="settings.quick_wins",
)
@bp.route("/calendar-feed/regenerate", methods=["POST"]) @bp.route("/calendar-feed/regenerate", methods=["POST"])
@login_required @login_required
def regenerate_calendar_feed(): def regenerate_calendar_feed():
@@ -174,9 +211,61 @@ def update_quick_task_config():
set_setting_int("quick_task_points_medium", form.medium_points.data) set_setting_int("quick_task_points_medium", form.medium_points.data)
set_setting_str("quick_task_label_heavy", form.heavy_label.data) set_setting_str("quick_task_label_heavy", form.heavy_label.data)
set_setting_int("quick_task_points_heavy", form.heavy_points.data) set_setting_int("quick_task_points_heavy", form.heavy_points.data)
set_setting_str("quick_task_label_super_heavy", form.super_heavy_label.data)
set_setting_int("quick_task_points_super_heavy", form.super_heavy_points.data)
db.session.commit() db.session.commit()
flash("Schnellaufgaben-Aufwand und Punkte wurden aktualisiert.", "success") flash("Quick-Win-Aufwand und Punkte wurden aktualisiert.", "success")
return redirect(url_for("settings.index")) return redirect(url_for("settings.quick_wins"))
@bp.route("/quick-wins/<int:quick_win_id>/delete", methods=["POST"])
@login_required
def delete_quick_win(quick_win_id: int):
quick_win = QuickWin.query.get_or_404(quick_win_id)
if quick_win.created_by_user_id != current_user.id and not current_user.is_admin:
flash("Diesen Quick-Win kannst du nicht entfernen.", "error")
return redirect(url_for("settings.quick_wins"))
quick_win.active = False
db.session.commit()
flash("Quick-Win wurde ausgeblendet.", "success")
return redirect(url_for("settings.quick_wins"))
@bp.route("/quick-wins/<int:quick_win_id>/update", methods=["POST"])
@login_required
def update_quick_win(quick_win_id: int):
quick_win = QuickWin.query.get_or_404(quick_win_id)
quick_task_config = get_quick_task_config()
title = (request.form.get("title") or "").strip()
effort = request.form.get("effort") or ""
if len(title) < 2:
flash("Quick-Wins brauchen einen Titel mit mindestens 2 Zeichen.", "error")
return redirect(url_for("settings.quick_wins"))
if len(title) > 160:
flash("Quick-Win-Titel dürfen maximal 160 Zeichen lang sein.", "error")
return redirect(url_for("settings.quick_wins"))
if effort not in quick_task_config:
flash("Bitte wähle einen gültigen Aufwand.", "error")
return redirect(url_for("settings.quick_wins"))
duplicate = (
QuickWin.query.filter(QuickWin.id != quick_win.id, QuickWin.title == title, QuickWin.active.is_(True))
.first()
)
if duplicate:
flash("Diesen Quick-Win gibt es bereits.", "error")
return redirect(url_for("settings.quick_wins"))
quick_win.title = title
quick_win.effort = effort
db.session.commit()
flash(f"Quick-Win „{quick_win.title}“ wurde aktualisiert.", "success")
return redirect(url_for("settings.quick_wins"))
@bp.route("/users/<int:user_id>/toggle-admin", methods=["POST"]) @bp.route("/users/<int:user_id>/toggle-admin", methods=["POST"])
+34 -15
View File
@@ -8,7 +8,7 @@ from flask import Blueprint, flash, redirect, render_template, request, url_for
from flask_login import current_user, login_required from flask_login import current_user, login_required
from ..forms import QuickTaskForm, TaskForm from ..forms import QuickTaskForm, TaskForm
from ..models import TaskInstance, User from ..models import QuickWin, TaskInstance, User
from ..services.app_settings import get_quick_task_config from ..services.app_settings import get_quick_task_config
from ..services.dates import month_label, today_local from ..services.dates import month_label, today_local
from ..services.tasks import ( from ..services.tasks import (
@@ -109,26 +109,45 @@ def create():
@bp.route("/tasks/quick", methods=["POST"]) @bp.route("/tasks/quick", methods=["POST"])
@login_required @login_required
def quick_create(): def quick_create():
form = QuickTaskForm(prefix="quick")
config = get_quick_task_config() config = get_quick_task_config()
form.effort.choices = [ created_titles: list[str] = []
(key, values["label"])
for key, values in config.items()
]
if not form.validate_on_submit(): selected_ids = request.form.getlist("quick_win_ids")
for field_errors in form.errors.values(): if selected_ids:
for error in field_errors: quick_wins = QuickWin.query.filter(QuickWin.id.in_(selected_ids), QuickWin.active.is_(True)).order_by(QuickWin.id.asc()).all()
for quick_win in quick_wins:
task = create_quick_task(quick_win.title, quick_win.effort, current_user, description="Quick-Win")
complete_task(task, current_user.id)
created_titles.append(task.title)
if request.form.get("include_custom") == "1":
form = QuickTaskForm(prefix="quick")
form.effort.choices = [(key, values["label"]) for key, values in config.items()]
custom_title = (form.title.data or "").strip()
extra_errors: list[str] = []
if not custom_title:
extra_errors.append("Bitte gib für „Sonstiges“ einen Titel ein.")
if not form.effort.data or form.effort.data not in config:
extra_errors.append("Bitte wähle für „Sonstiges“ einen Aufwand aus.")
if not form.validate_on_submit() or extra_errors:
for field_errors in form.errors.values():
for error in field_errors:
flash(error, "error")
for error in extra_errors:
flash(error, "error") flash(error, "error")
return redirect(request.referrer or url_for("tasks.my_tasks"))
task = create_quick_task(custom_title, form.effort.data, current_user, description="Quick-Win")
complete_task(task, current_user.id)
created_titles.append(task.title)
if not created_titles:
flash("Bitte wähle mindestens einen Quick-Win aus.", "error")
return redirect(request.referrer or url_for("tasks.my_tasks")) return redirect(request.referrer or url_for("tasks.my_tasks"))
quick_action = request.form.get("quick_action", "save") if len(created_titles) == 1:
task = create_quick_task(form.title.data, form.effort.data, current_user) flash(f"Quick-Win „{created_titles[0]}“ wurde als erledigt gespeichert.", "success")
if quick_action == "complete":
complete_task(task, current_user.id)
flash(f"Schnellaufgabe „{task.title}“ wurde direkt als erledigt gespeichert.", "success")
else: else:
flash(f"Schnellaufgabe „{task.title}“ wurde für dich angelegt.", "success") flash(f"{len(created_titles)} Quick-Wins wurden als erledigt gespeichert.", "success")
return redirect(request.referrer or url_for("tasks.my_tasks")) return redirect(request.referrer or url_for("tasks.my_tasks"))
+3
View File
@@ -9,10 +9,12 @@ QUICK_TASK_DEFAULTS = {
"quick_task_label_normal": "Normal", "quick_task_label_normal": "Normal",
"quick_task_label_medium": "Dauert etwas", "quick_task_label_medium": "Dauert etwas",
"quick_task_label_heavy": "Aufwendig", "quick_task_label_heavy": "Aufwendig",
"quick_task_label_super_heavy": "Super aufwendig",
"quick_task_points_fast": 4, "quick_task_points_fast": 4,
"quick_task_points_normal": 8, "quick_task_points_normal": 8,
"quick_task_points_medium": 12, "quick_task_points_medium": 12,
"quick_task_points_heavy": 18, "quick_task_points_heavy": 18,
"quick_task_points_super_heavy": 28,
} }
QUICK_TASK_EFFORTS = [ QUICK_TASK_EFFORTS = [
@@ -20,6 +22,7 @@ QUICK_TASK_EFFORTS = [
("normal", "quick_task_label_normal", "quick_task_points_normal"), ("normal", "quick_task_label_normal", "quick_task_points_normal"),
("medium", "quick_task_label_medium", "quick_task_points_medium"), ("medium", "quick_task_label_medium", "quick_task_points_medium"),
("heavy", "quick_task_label_heavy", "quick_task_points_heavy"), ("heavy", "quick_task_label_heavy", "quick_task_points_heavy"),
("super_heavy", "quick_task_label_super_heavy", "quick_task_points_super_heavy"),
] ]
+37 -6
View File
@@ -5,7 +5,7 @@ import os
from sqlalchemy import inspect, text from sqlalchemy import inspect, text
from ..extensions import db from ..extensions import db
from ..models import User from ..models import QuickWin, User
from .app_settings import ensure_app_settings from .app_settings import ensure_app_settings
@@ -30,17 +30,48 @@ def ensure_schema_and_admins() -> None:
db.session.commit() db.session.commit()
admin_exists = User.query.filter_by(is_admin=True).first() admin_exists = User.query.filter_by(is_admin=True).first()
if admin_exists: default_quick_win_user = admin_exists
return
preferred_admin_email = os.getenv("DEFAULT_ADMIN_EMAIL", "mail@hnz.io").lower().strip() preferred_admin_email = os.getenv("DEFAULT_ADMIN_EMAIL", "mail@hnz.io").lower().strip()
preferred_user = User.query.filter(User.email.ilike(preferred_admin_email)).first() preferred_user = User.query.filter(User.email.ilike(preferred_admin_email)).first()
if preferred_user: if preferred_user and not admin_exists:
preferred_user.is_admin = True preferred_user.is_admin = True
db.session.commit() db.session.commit()
return default_quick_win_user = preferred_user
first_user = User.query.order_by(User.id.asc()).first() first_user = User.query.order_by(User.id.asc()).first()
if first_user: if first_user and not User.query.filter_by(is_admin=True).first():
first_user.is_admin = True first_user.is_admin = True
db.session.commit() db.session.commit()
default_quick_win_user = first_user
_ensure_default_quick_wins(default_quick_win_user or User.query.order_by(User.id.asc()).first())
def _ensure_default_quick_wins(default_user: User | None) -> None:
if not default_user:
return
defaults = [
("Schnell Aufräumen", "fast"),
("Spülmaschine ausräumen", "normal"),
("Bett machen", "normal"),
("Lüften", "fast"),
("Wäsche zusammenlegen", "medium"),
]
existing_titles = {quick_win.title for quick_win in QuickWin.query.all()}
created = False
for title, effort in defaults:
if title not in existing_titles:
db.session.add(
QuickWin(
title=title,
effort=effort,
active=True,
created_by_user_id=default_user.id,
)
)
created = True
if created:
db.session.commit()
+3 -3
View File
@@ -129,12 +129,12 @@ def complete_task(task: TaskInstance, completed_by_user_id: int) -> TaskInstance
return task return task
def create_quick_task(title: str, effort: str, creator: User) -> TaskInstance: def create_quick_task(title: str, effort: str, creator: User, description: str = "Quick-Win") -> TaskInstance:
config = get_quick_task_config() config = get_quick_task_config()
effort_config = config[effort] effort_config = config[effort]
template = TaskTemplate( template = TaskTemplate(
title=title.strip(), title=title.strip(),
description="Schnellaufgabe", description=description,
default_points=effort_config["points"], default_points=effort_config["points"],
default_assigned_user_id=creator.id, default_assigned_user_id=creator.id,
recurrence_interval_value=None, recurrence_interval_value=None,
@@ -147,7 +147,7 @@ def create_quick_task(title: str, effort: str, creator: User) -> TaskInstance:
task = TaskInstance( task = TaskInstance(
task_template_id=template.id, task_template_id=template.id,
title=template.title, title=template.title,
description="Schnellaufgabe", description=description,
assigned_user_id=creator.id, assigned_user_id=creator.id,
due_date=today_local(), due_date=today_local(),
points_awarded=template.default_points, points_awarded=template.default_points,
+195 -12
View File
@@ -188,19 +188,32 @@ p {
.app-footer { .app-footer {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: space-between;
gap: 10px; gap: 14px;
flex-wrap: wrap;
padding: 18px 0 8px; padding: 18px 0 8px;
color: var(--muted); color: var(--muted);
font-size: 0.88rem; font-size: 0.88rem;
text-align: center; text-align: left;
}
.app-footer__left,
.app-footer__right {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.app-footer__right {
margin-left: auto;
} }
.app-footer a { .app-footer a {
color: inherit; color: inherit;
} }
.app-footer span { .app-footer__left span {
opacity: 0.7; opacity: 0.7;
} }
@@ -857,26 +870,28 @@ p {
} }
.calendar-task__title { .calendar-task__title {
display: -webkit-box; display: block;
overflow: hidden; overflow: visible;
min-width: 0; min-width: 0;
font-size: 0.96rem; font-size: 0.96rem;
line-height: 1.15; line-height: 1.15;
font-weight: 600; font-weight: 600;
color: var(--text); color: var(--text);
word-break: break-word; word-break: normal;
-webkit-line-clamp: 2; overflow-wrap: break-word;
-webkit-box-orient: vertical; hyphens: manual;
} }
.calendar-task__person { .calendar-task__person {
display: block; display: block;
overflow: hidden; overflow: visible;
min-width: 0; min-width: 0;
font-size: 0.74rem; font-size: 0.74rem;
line-height: 1.2; line-height: 1.2;
white-space: nowrap; white-space: normal;
text-overflow: ellipsis; word-break: normal;
overflow-wrap: break-word;
hyphens: manual;
} }
.calendar-task--open { .calendar-task--open {
@@ -1030,6 +1045,174 @@ p {
margin: 0; margin: 0;
} }
.quick-win-list {
display: grid;
gap: 12px;
}
.quick-win-manage-card {
display: grid;
gap: 12px;
padding: 18px;
border-radius: var(--radius-md);
background: var(--surface-soft);
border: 1px solid rgba(132, 152, 190, 0.22);
}
.quick-win-manage-card {
align-items: stretch;
}
.quick-win-manage-form {
display: grid;
gap: 12px;
}
.quick-win-manage-card__actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.quick-win-grid {
display: grid;
gap: 12px;
}
.quick-win-dialog-header {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 14px;
align-items: start;
}
.quick-win-dialog-header__badge {
width: 52px;
height: 52px;
border-radius: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba(37, 99, 235, 0.18), rgba(52, 211, 153, 0.24));
border: 1px solid rgba(132, 152, 190, 0.22);
color: var(--primary-strong);
box-shadow: 0 16px 30px rgba(37, 99, 235, 0.16);
}
.quick-win-dialog-header__badge svg {
width: 24px;
height: 24px;
}
.quick-win-manage-card p {
color: var(--muted);
}
.quick-win-tag-grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.quick-win-tag {
position: relative;
min-width: 0;
flex: 0 0 auto;
max-width: 100%;
}
.quick-win-tag input {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
}
.quick-win-tag span {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 0;
max-width: 100%;
padding: 10px 16px;
border-radius: 999px;
border: 1px solid rgba(132, 152, 190, 0.22);
background: var(--surface-soft);
color: var(--text);
font-weight: 700;
font-size: 0.97rem;
text-align: center;
line-height: 1.2;
white-space: nowrap;
transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease, color 0.18s ease;
cursor: pointer;
}
.quick-win-tag input:checked + span {
background: linear-gradient(135deg, rgba(37, 99, 235, 0.18), rgba(52, 211, 153, 0.16));
border-color: rgba(37, 99, 235, 0.34);
color: var(--primary-strong);
box-shadow: 0 16px 28px rgba(37, 99, 235, 0.16);
transform: translateY(-1px);
}
.quick-win-tag--custom span {
border-style: dashed;
}
.quick-win-tag--custom {
flex-basis: 100%;
}
.quick-win-tag--custom span {
width: fit-content;
}
.quick-win-custom-fields {
display: grid;
gap: 14px;
}
.quick-win-custom-fields[hidden] {
display: none !important;
}
.dialog-actions--stack {
display: grid;
gap: 12px;
}
@media (max-width: 640px) {
.quick-win-dialog-header {
grid-template-columns: 1fr;
}
.quick-win-dialog-header__badge {
width: 48px;
height: 48px;
}
.quick-win-tag-grid {
gap: 10px;
}
.quick-win-tag {
max-width: 100%;
}
.quick-win-tag span {
width: auto;
max-width: 100%;
white-space: normal;
text-align: left;
justify-content: flex-start;
}
.quick-win-tag--custom span {
width: auto;
}
}
.push-box__state { .push-box__state {
align-items: flex-start; align-items: flex-start;
padding: 16px; padding: 16px;
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--! Font Awesome Pro 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2025 Fonticons, Inc. --><path opacity=".4" fill="currentColor" d="M376 512L448 544L480 616L512 544L584 512L512 480L480 408L448 480L376 512zM408 128L480 160L512 232L544 160L616 128L544 96L512 24L480 96L408 128z"/><path fill="currentColor" d="M160 256L224 112L288 256L432 320L288 384L224 528L160 384L16 320L160 256z"/></svg>

After

Width:  |  Height:  |  Size: 528 B

+48
View File
@@ -7,6 +7,12 @@
const quickTaskDialog = document.getElementById("quickTaskDialog"); const quickTaskDialog = document.getElementById("quickTaskDialog");
const quickTaskOpen = document.getElementById("quickTaskOpen"); const quickTaskOpen = document.getElementById("quickTaskOpen");
const quickTaskClose = document.getElementById("quickTaskClose"); const quickTaskClose = document.getElementById("quickTaskClose");
const quickWinsSubmit = document.getElementById("quickWinsSubmit");
const quickWinInputs = document.querySelectorAll("[data-quick-win-input]");
const quickWinCustomToggle = document.querySelector("[data-quick-win-custom-toggle]");
const quickWinCustomFields = document.getElementById("quickWinCustomFields");
const quickWinTitle = document.getElementById("quick-title");
const quickWinEffort = document.getElementById("quick-effort");
document.querySelectorAll("[data-complete-action]").forEach((button) => { document.querySelectorAll("[data-complete-action]").forEach((button) => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
@@ -39,6 +45,48 @@
quickTaskClose.addEventListener("click", () => quickTaskDialog.close()); quickTaskClose.addEventListener("click", () => quickTaskDialog.close());
} }
function updateQuickWinsState() {
const selectedPresetCount = [...quickWinInputs].filter((input) => input.checked).length;
const customSelected = quickWinCustomToggle?.checked === true;
const totalCount = selectedPresetCount + (customSelected ? 1 : 0);
if (quickWinCustomFields) {
quickWinCustomFields.hidden = !customSelected;
}
if (quickWinTitle) {
quickWinTitle.disabled = !customSelected;
quickWinTitle.required = customSelected;
}
if (quickWinEffort) {
quickWinEffort.disabled = !customSelected;
quickWinEffort.required = customSelected;
}
if (quickWinsSubmit) {
quickWinsSubmit.disabled = totalCount === 0;
quickWinsSubmit.textContent = totalCount <= 1 ? "Quick-Win sichern" : "Quick Wins sichern";
}
}
quickWinInputs.forEach((input) => input.addEventListener("change", updateQuickWinsState));
if (quickWinCustomToggle) {
quickWinCustomToggle.addEventListener("change", updateQuickWinsState);
}
updateQuickWinsState();
if (quickTaskDialog) {
quickTaskDialog.addEventListener("close", () => {
const quickWinsForm = document.getElementById("quickWinsForm");
if (!quickWinsForm) {
return;
}
quickWinsForm.reset();
updateQuickWinsState();
});
}
const pushButton = document.getElementById("pushToggle"); const pushButton = document.getElementById("pushToggle");
const pushHint = document.getElementById("pushHint"); const pushHint = document.getElementById("pushHint");
const vapidKey = document.body.dataset.pushKey; const vapidKey = document.body.dataset.pushKey;
+41 -18
View File
@@ -88,9 +88,14 @@
</main> </main>
<footer class="app-footer"> <footer class="app-footer">
<a href="https://git.hnz.io/hnzio/putzliga/releases" target="_blank" rel="noreferrer">Version {{ app_version }}</a> <div class="app-footer__left">
<span>·</span> <a href="https://git.hnz.io/hnzio/putzliga/releases" target="_blank" rel="noreferrer">Version {{ app_version }}</a>
<a href="https://hnz.io" target="_blank" rel="noreferrer">(c) 2026 @hnz.io</a> <span>·</span>
<span>Made with ❤️ in Göttingen.</span>
</div>
<div class="app-footer__right">
<a href="https://hnz.io" target="_blank" rel="noreferrer">© 2026 @ hnz.io</a>
</div>
</footer> </footer>
</div> </div>
</div> </div>
@@ -105,7 +110,7 @@
{% endfor %} {% endfor %}
</nav> </nav>
<button type="button" class="fab-quick-task" id="quickTaskOpen" aria-label="Schnellaufgabe anlegen"> <button type="button" class="fab-quick-task" id="quickTaskOpen" aria-label="Quick-Wins öffnen">
{{ nav_icon('plus') }} {{ nav_icon('plus') }}
</button> </button>
@@ -123,23 +128,41 @@
</dialog> </dialog>
<dialog class="complete-dialog" id="quickTaskDialog"> <dialog class="complete-dialog" id="quickTaskDialog">
<form method="post" action="{{ url_for('tasks.quick_create') }}" class="complete-dialog__surface complete-dialog__surface--task"> <form method="post" action="{{ url_for('tasks.quick_create') }}" class="complete-dialog__surface complete-dialog__surface--task" id="quickWinsForm">
{{ quick_task_form.hidden_tag() }} {{ quick_task_form.hidden_tag() }}
<p class="eyebrow">Schnellaufgabe</p> <div class="quick-win-dialog-header">
<h2>Direkt etwas für dich anlegen</h2> <span class="quick-win-dialog-header__badge" aria-hidden="true">{{ icon_svg('quick-wins-sparkles')|safe }}</span>
<p class="muted">Titel und Aufwand reichen. Die Aufgabe wird automatisch dir zugewiesen und auf heute gesetzt.</p> <div>
<div class="field"> <p class="eyebrow">Quick-Wins</p>
{{ quick_task_form.title.label }} <h2>Schnell Punkte abstauben</h2>
{{ quick_task_form.title(placeholder="Zum Beispiel: Küche kurz aufräumen") }} <p class="muted">Alle Quick-Wins sind für das ganze Team sichtbar. Für „Sonstiges“ kannst du Titel und Aufwand frei wählen.</p>
</div>
</div> </div>
<div class="field"> <div class="quick-win-tag-grid">
{{ quick_task_form.effort.label }} {% for quick_win in quick_wins %}
{{ quick_task_form.effort() }} <label class="quick-win-tag" data-quick-win-tag>
<input type="checkbox" name="quick_win_ids" value="{{ quick_win.id }}" data-quick-win-input>
<span>{{ quick_win.title }}</span>
</label>
{% endfor %}
<label class="quick-win-tag quick-win-tag--custom" data-quick-win-tag>
<input type="checkbox" name="include_custom" value="1" data-quick-win-custom-toggle>
<span>Sonstiges</span>
</label>
</div> </div>
<div class="dialog-actions"> <div class="quick-win-custom-fields" id="quickWinCustomFields" hidden>
<button type="submit" class="button" name="quick_action" value="save">Aufgabe speichern</button> <div class="field">
<button type="submit" class="button button--secondary" name="quick_action" value="complete">Aufgabe als erledigt speichern</button> {{ quick_task_form.title.label }}
<button type="button" class="button button--ghost" id="quickTaskClose">Abbrechen</button> {{ quick_task_form.title(placeholder="Zum Beispiel: Flur kurz aufräumen", required=False) }}
</div>
<div class="field">
{{ quick_task_form.effort.label }}
{{ quick_task_form.effort(required=False) }}
</div>
</div>
<div class="dialog-actions dialog-actions--stack">
<button type="submit" class="button button--wide" id="quickWinsSubmit" disabled>Quick-Win sichern</button>
<button type="button" class="button button--ghost button--wide" id="quickTaskClose">Abbrechen</button>
</div> </div>
</form> </form>
</dialog> </dialog>
-68
View File
@@ -182,73 +182,5 @@
{% endfor %} {% endfor %}
</div> </div>
</section> </section>
<section class="panel">
<p class="eyebrow">Admin</p>
<h2>Schnellaufgabe-Aufwand</h2>
<p class="muted">Hier definierst du die sichtbaren Aufwand-Stufen und die dazugehörigen Punkte. Im Schnellaufgaben-Dialog wird nur die Bezeichnung angezeigt.</p>
<form method="post" action="{{ url_for('settings.update_quick_task_config') }}" class="badge-settings">
{{ quick_task_config_form.hidden_tag() }}
<div class="badge-setting-card">
<div>
<strong>Slot 1</strong>
<p class="muted">Kleine Sache für zwischendurch.</p>
</div>
<div class="field">
{{ quick_task_config_form.fast_label.label }}
{{ quick_task_config_form.fast_label() }}
</div>
<div class="field">
<label for="{{ quick_task_config_form.fast_points.id }}">Punkte</label>
{{ quick_task_config_form.fast_points() }}
</div>
</div>
<div class="badge-setting-card">
<div>
<strong>Slot 2</strong>
<p class="muted">Typische Alltagssache.</p>
</div>
<div class="field">
{{ quick_task_config_form.normal_label.label }}
{{ quick_task_config_form.normal_label() }}
</div>
<div class="field">
<label for="{{ quick_task_config_form.normal_points.id }}">Punkte</label>
{{ quick_task_config_form.normal_points() }}
</div>
</div>
<div class="badge-setting-card">
<div>
<strong>Slot 3</strong>
<p class="muted">Braucht etwas mehr Zeit oder Konzentration.</p>
</div>
<div class="field">
{{ quick_task_config_form.medium_label.label }}
{{ quick_task_config_form.medium_label() }}
</div>
<div class="field">
<label for="{{ quick_task_config_form.medium_points.id }}">Punkte</label>
{{ quick_task_config_form.medium_points() }}
</div>
</div>
<div class="badge-setting-card">
<div>
<strong>Slot 4</strong>
<p class="muted">Spürbarer Aufwand mit mehr Punkten.</p>
</div>
<div class="field">
{{ quick_task_config_form.heavy_label.label }}
{{ quick_task_config_form.heavy_label() }}
</div>
<div class="field">
<label for="{{ quick_task_config_form.heavy_points.id }}">Punkte</label>
{{ quick_task_config_form.heavy_points() }}
</div>
</div>
<div class="field field--full">
{{ quick_task_config_form.submit(class_='button button--secondary') }}
</div>
</form>
</section>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
+162
View File
@@ -0,0 +1,162 @@
{% extends "base.html" %}
{% from "partials/macros.html" import nav_icon %}
{% block title %}Quick-Wins · Putzliga{% endblock %}
{% block page_title %}Quick-Wins{% endblock %}
{% block content %}
<section class="settings-tabs">
{% for endpoint, label, icon in settings_tabs %}
<a href="{{ url_for(endpoint) }}" class="settings-tab {% if active_settings_tab == endpoint %}is-active{% endif %}">
{{ nav_icon(icon) }}
<span>{{ label }}</span>
</a>
{% endfor %}
</section>
<section class="two-column">
<article class="panel">
<p class="eyebrow">Gemeinsame Vorlagen</p>
<h2>Quick-Win anlegen</h2>
<p class="muted">Alle hier angelegten Quick-Wins sind direkt für das ganze Team im Plus-Menü verfügbar.</p>
<form method="post" class="form-grid">
{{ quick_win_form.hidden_tag() }}
<div class="field">
{{ quick_win_form.title.label }}
{{ quick_win_form.title(placeholder="Zum Beispiel: Müllbeutel wechseln") }}
{% for error in quick_win_form.title.errors %}<small class="error">{{ error }}</small>{% endfor %}
</div>
<div class="field">
{{ quick_win_form.effort.label }}
{{ quick_win_form.effort() }}
{% for error in quick_win_form.effort.errors %}<small class="error">{{ error }}</small>{% endfor %}
</div>
{{ quick_win_form.submit(class_='button') }}
</form>
</article>
<article class="panel">
<p class="eyebrow">Direkt sichtbar</p>
<h2>Aktive Quick-Wins bearbeiten</h2>
<div class="quick-win-list">
{% for quick_win in quick_wins %}
<article class="quick-win-manage-card">
<form method="post" action="{{ url_for('settings.update_quick_win', quick_win_id=quick_win.id) }}" class="quick-win-manage-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="field">
<label for="quick-win-title-{{ quick_win.id }}">Titel</label>
<input id="quick-win-title-{{ quick_win.id }}" type="text" name="title" value="{{ quick_win.title }}" minlength="2" maxlength="160" required>
</div>
<div class="field">
<label for="quick-win-effort-{{ quick_win.id }}">Aufwand</label>
<select id="quick-win-effort-{{ quick_win.id }}" name="effort" required>
{% for effort_key, effort_values in quick_task_config.items() %}
<option value="{{ effort_key }}" {% if quick_win.effort == effort_key %}selected{% endif %}>{{ effort_values.label }}</option>
{% endfor %}
</select>
</div>
<p class="muted">Von {{ quick_win.created_by_user.name }}</p>
<div class="quick-win-manage-card__actions">
<button type="submit" class="button button--secondary">Speichern</button>
{% if quick_win.created_by_user_id == current_user.id or current_user.is_admin %}
<button
type="submit"
class="button button--ghost"
formaction="{{ url_for('settings.delete_quick_win', quick_win_id=quick_win.id) }}"
formmethod="post"
>
Entfernen
</button>
{% endif %}
</div>
</form>
</article>
{% else %}
<div class="empty-state">Noch keine Quick-Wins angelegt. Der erste steht gleich oben bereit.</div>
{% endfor %}
</div>
</article>
</section>
{% if current_user.is_admin %}
<section class="panel">
<p class="eyebrow">Admin</p>
<h2>Quick-Win-Aufwand</h2>
<p class="muted">Hier definierst du die sichtbaren Aufwand-Stufen und die dazugehörigen Punkte. Im Quick-Wins-Dialog wird nur die Bezeichnung angezeigt.</p>
<form method="post" action="{{ url_for('settings.update_quick_task_config') }}" class="badge-settings">
{{ quick_task_config_form.hidden_tag() }}
<div class="badge-setting-card">
<div>
<strong>Slot 1</strong>
<p class="muted">Kleine Sache für zwischendurch.</p>
</div>
<div class="field">
{{ quick_task_config_form.fast_label.label }}
{{ quick_task_config_form.fast_label() }}
</div>
<div class="field">
<label for="{{ quick_task_config_form.fast_points.id }}">Punkte</label>
{{ quick_task_config_form.fast_points() }}
</div>
</div>
<div class="badge-setting-card">
<div>
<strong>Slot 2</strong>
<p class="muted">Typische Alltagssache.</p>
</div>
<div class="field">
{{ quick_task_config_form.normal_label.label }}
{{ quick_task_config_form.normal_label() }}
</div>
<div class="field">
<label for="{{ quick_task_config_form.normal_points.id }}">Punkte</label>
{{ quick_task_config_form.normal_points() }}
</div>
</div>
<div class="badge-setting-card">
<div>
<strong>Slot 3</strong>
<p class="muted">Braucht etwas mehr Zeit oder Konzentration.</p>
</div>
<div class="field">
{{ quick_task_config_form.medium_label.label }}
{{ quick_task_config_form.medium_label() }}
</div>
<div class="field">
<label for="{{ quick_task_config_form.medium_points.id }}">Punkte</label>
{{ quick_task_config_form.medium_points() }}
</div>
</div>
<div class="badge-setting-card">
<div>
<strong>Slot 4</strong>
<p class="muted">Spürbarer Aufwand mit mehr Punkten.</p>
</div>
<div class="field">
{{ quick_task_config_form.heavy_label.label }}
{{ quick_task_config_form.heavy_label() }}
</div>
<div class="field">
<label for="{{ quick_task_config_form.heavy_points.id }}">Punkte</label>
{{ quick_task_config_form.heavy_points() }}
</div>
</div>
<div class="badge-setting-card">
<div>
<strong>Slot 5</strong>
<p class="muted">Extra große Aufgabe für echte Kraftakte.</p>
</div>
<div class="field">
{{ quick_task_config_form.super_heavy_label.label }}
{{ quick_task_config_form.super_heavy_label() }}
</div>
<div class="field">
<label for="{{ quick_task_config_form.super_heavy_points.id }}">Punkte</label>
{{ quick_task_config_form.super_heavy_points() }}
</div>
</div>
<div class="field field--full">
{{ quick_task_config_form.submit(class_='button button--secondary') }}
</div>
</form>
</section>
{% endif %}
{% endblock %}
+10 -10
View File
@@ -71,14 +71,14 @@
<div class="calendar-mobile-day__tasks"> <div class="calendar-mobile-day__tasks">
{% for task in group.tasks %} {% for task in group.tasks %}
<a href="{{ url_for('tasks.edit', task_id=task.id) }}" class="calendar-task calendar-task--{{ task.status }}"> <a href="{{ url_for('tasks.edit', task_id=task.id) }}" class="calendar-task calendar-task--{{ task.status }}">
<strong class="calendar-task__title">{{ task.title }}</strong> <strong class="calendar-task__title" lang="de">{{ task.title|hyphenate_de }}</strong>
<small class="calendar-task__person"> <small class="calendar-task__person" lang="de">
{% if task.completed_by_user %} {% if task.completed_by_user %}
{{ task.completed_by_user.name }} {{ task.completed_by_user.name|hyphenate_de }}
{% elif task.assigned_user %} {% elif task.assigned_user %}
{{ task.assigned_user.name }} {{ task.assigned_user.name|hyphenate_de }}
{% else %} {% else %}
Ohne Zuweisung {{ 'Ohne Zuweisung'|hyphenate_de }}
{% endif %} {% endif %}
</small> </small>
</a> </a>
@@ -104,14 +104,14 @@
<div class="calendar-day__tasks"> <div class="calendar-day__tasks">
{% for task in tasks_by_day.get(day, []) %} {% 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 }}"> <a href="{{ url_for('tasks.edit', task_id=task.id) }}" class="calendar-task calendar-task--{{ task.status }}">
<strong class="calendar-task__title">{{ task.title }}</strong> <strong class="calendar-task__title" lang="de">{{ task.title|hyphenate_de }}</strong>
<small class="calendar-task__person"> <small class="calendar-task__person" lang="de">
{% if task.completed_by_user %} {% if task.completed_by_user %}
{{ task.completed_by_user.name }} {{ task.completed_by_user.name|hyphenate_de }}
{% elif task.assigned_user %} {% elif task.assigned_user %}
{{ task.assigned_user.name }} {{ task.assigned_user.name|hyphenate_de }}
{% else %} {% else %}
Ohne Zuweisung {{ 'Ohne Zuweisung'|hyphenate_de }}
{% endif %} {% endif %}
</small> </small>
</a> </a>
+1
View File
@@ -4,5 +4,6 @@ Flask-SQLAlchemy==3.1.1
Flask-WTF==1.2.2 Flask-WTF==1.2.2
email-validator==2.2.0 email-validator==2.2.0
gunicorn==23.0.0 gunicorn==23.0.0
pyphen==0.17.2
pywebpush==2.0.3 pywebpush==2.0.3
python-dotenv==1.0.1 python-dotenv==1.0.1