release: publish saldo 0.1.0
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
{% macro avatar(name, avatar_url=None, initials=None, size='md', class_name='') -%}
|
||||
<span class="avatar avatar-{{ size }} {{ class_name }}" aria-label="{{ name }}">
|
||||
{% if avatar_url %}
|
||||
<img src="{{ avatar_url }}" alt="{{ name }}">
|
||||
{% else %}
|
||||
<span>{{ initials or (name[:2] if name else '?') }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,74 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Optionen | {{ app_name }}{% endblock %}
|
||||
{% block content %}
|
||||
{% from "_ui.html" import avatar %}
|
||||
<section class="page-hero">
|
||||
<div>
|
||||
<div class="eyebrow">Optionen</div>
|
||||
<h1>Benutzerverwaltung</h1>
|
||||
<p class="muted">Kategorien, Einträge und Split-Personen werden jetzt direkt in der Planung gepflegt. Hier bleiben nur App-Zugänge und Rollen.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head"><h2>Benutzer</h2></div>
|
||||
<form method="post" action="{{ url_for('admin.create_user') }}" class="stack-form" enctype="multipart/form-data">
|
||||
<input name="username" placeholder="Benutzername" required>
|
||||
<input name="display_name" placeholder="Anzeigename" required>
|
||||
<input name="email" type="email" placeholder="E-Mail" required>
|
||||
<label>
|
||||
<span>Avatar hochladen</span>
|
||||
<input name="avatar_file" type="file" accept="image/*">
|
||||
</label>
|
||||
<input name="avatar_url" placeholder="Avatar-URL optional">
|
||||
<input name="password" type="password" placeholder="Passwort" required>
|
||||
<select name="role">
|
||||
<option value="editor">editor</option>
|
||||
<option value="admin">admin</option>
|
||||
</select>
|
||||
<button class="primary-button" type="submit">Benutzer anlegen</button>
|
||||
</form>
|
||||
{% for user in users %}
|
||||
<div class="month-row">
|
||||
<div class="list-row-main">
|
||||
{{ avatar(user.ui_name, user.avatar_url, user.avatar_initials, "sm") }}
|
||||
<div>
|
||||
<strong>{{ user.ui_name }}</strong>
|
||||
<small>{{ user.role }} · {{ "aktiv" if user.is_active else "deaktiviert" }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-actions">
|
||||
<button class="ghost-button" type="button" data-open-dialog="user-dialog-{{ user.id }}">Bearbeiten</button>
|
||||
<form method="post" action="{{ url_for('admin.toggle_user', user_id=user.id) }}">
|
||||
<button class="ghost-button" type="submit">Status ändern</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
{% for user in users %}
|
||||
<dialog id="user-dialog-{{ user.id }}" class="app-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('admin.update_user', user_id=user.id) }}" class="stack-form" enctype="multipart/form-data">
|
||||
<h3>{{ user.ui_name }} bearbeiten</h3>
|
||||
<input value="{{ user.username }}" disabled>
|
||||
<input name="display_name" value="{{ user.ui_name }}" required>
|
||||
<input name="email" type="email" value="{{ user.email }}" required>
|
||||
<label>
|
||||
<span>Avatar hochladen</span>
|
||||
<input name="avatar_file" type="file" accept="image/*">
|
||||
</label>
|
||||
<input name="avatar_url" value="{{ user.avatar_url or '' }}" placeholder="Avatar-URL optional">
|
||||
<select name="role">
|
||||
<option value="editor" {% if user.role == "editor" %}selected{% endif %}>editor</option>
|
||||
<option value="admin" {% if user.role == "admin" %}selected{% endif %}>admin</option>
|
||||
</select>
|
||||
<label class="check-label"><input type="checkbox" name="is_active" {% if user.is_active %}checked{% endif %}> Aktiv</label>
|
||||
<button class="primary-button" type="submit">Speichern</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Login | Saldo</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
|
||||
</head>
|
||||
<body class="login-body">
|
||||
<section class="login-card">
|
||||
<div class="eyebrow">Saldo</div>
|
||||
<h1>Monate planen, ohne Tabellenfrust.</h1>
|
||||
<p class="muted">Mehrbenutzer-Haushaltsplanung für wiederkehrende Überweisungen, variable Einkommen und faire Beteiligungen.</p>
|
||||
{% 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 %}
|
||||
<form method="post" class="stack-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<label>Benutzername
|
||||
<input name="username" required autocomplete="username">
|
||||
</label>
|
||||
<label>Passwort
|
||||
<input type="password" name="password" required autocomplete="current-password">
|
||||
</label>
|
||||
<button type="submit" class="primary-button">Einloggen</button>
|
||||
</form>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Setup | Saldo</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
|
||||
</head>
|
||||
<body class="login-body">
|
||||
<section class="login-card">
|
||||
<div class="eyebrow">Erststart</div>
|
||||
<h1>Saldo einrichten</h1>
|
||||
<p class="muted">Lege jetzt den ersten Admin an. Danach landest du direkt in der App und kannst Budgets, Personen und Monate selbst pflegen.</p>
|
||||
<div class="setup-card subtle">
|
||||
<strong>Was bereits vorhanden ist</strong>
|
||||
<p>Die Grundstruktur mit Budgets, Kategorien, Einträgen und Gemeinschaftskonten ist vorbereitet. Es wurden keine Beispielzugänge und keine Beispielpersonen angelegt.</p>
|
||||
</div>
|
||||
{% 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 %}
|
||||
<form method="post" class="stack-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<label>Benutzername
|
||||
<input name="username" required autocomplete="username">
|
||||
</label>
|
||||
<label>Anzeigename
|
||||
<input name="display_name" required autocomplete="name">
|
||||
</label>
|
||||
<label>E-Mail
|
||||
<input name="email" type="email" required autocomplete="email">
|
||||
</label>
|
||||
<label>Passwort
|
||||
<input name="password" type="password" required autocomplete="new-password">
|
||||
</label>
|
||||
<label>Passwort wiederholen
|
||||
<input name="password_confirm" type="password" required autocomplete="new-password">
|
||||
</label>
|
||||
<button type="submit" class="primary-button">Admin anlegen</button>
|
||||
</form>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,91 @@
|
||||
<!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="#f4efe6">
|
||||
<title>{% block title %}{{ app_name }}{% endblock %}</title>
|
||||
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
|
||||
<script defer src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app.js') }}"></script>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
{% from "_ui.html" import avatar %}
|
||||
<body data-vapid-public-key="{{ vapid_public_key }}" data-theme="light" data-csrf-token="{{ csrf_token }}">
|
||||
<div class="app-shell">
|
||||
{% if current_user.is_authenticated %}
|
||||
<aside class="sidebar">
|
||||
<a class="brand" href="{{ url_for('main.index') }}">
|
||||
<span class="brand-mark">S</span>
|
||||
<span>
|
||||
<strong>Saldo</strong>
|
||||
<small>Haushalt gemeinsam planen</small>
|
||||
</span>
|
||||
</a>
|
||||
<nav class="nav-group">
|
||||
<a href="{{ url_for('main.index') }}" class="nav-link"><img src="{{ url_for('static', filename='icons/house.svg') }}" alt="" class="ui-icon">Übersicht</a>
|
||||
<a href="{{ url_for('planning.current') }}" class="nav-link"><img src="{{ url_for('static', filename='icons/wallet.svg') }}" alt="" class="ui-icon">Planung</a>
|
||||
<a href="{{ url_for('months.index') }}" class="nav-link"><img src="{{ url_for('static', filename='icons/calendar.svg') }}" alt="" class="ui-icon">Monate</a>
|
||||
<a href="{{ url_for('main.analytics') }}" class="nav-link"><img src="{{ url_for('static', filename='icons/chart-simple.svg') }}" alt="" class="ui-icon">Auswertungen</a>
|
||||
{% if current_user.is_admin() %}
|
||||
<a href="{{ url_for('admin.index') }}" class="nav-link"><img src="{{ url_for('static', filename='icons/sliders.svg') }}" alt="" class="ui-icon">Optionen</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="sidebar-user">
|
||||
{{ avatar(current_user.ui_name, current_user.avatar_url, current_user.avatar_initials, 'md') }}
|
||||
<div>
|
||||
<strong>{{ current_user.ui_name }}</strong>
|
||||
<small>{{ "Admin" if current_user.is_admin() else "Mitglied" }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ghost-button theme-toggle" type="button" data-theme-toggle>
|
||||
<img src="{{ url_for('static', filename='icons/moon.svg') }}" alt="" class="ui-icon theme-icon-dark">
|
||||
<img src="{{ url_for('static', filename='icons/sun.svg') }}" alt="" class="ui-icon theme-icon-light">
|
||||
Design
|
||||
</button>
|
||||
<form method="post" action="{{ url_for('auth.logout') }}">
|
||||
<button class="ghost-button" type="submit"><img src="{{ url_for('static', filename='icons/arrow-right-to-bracket.svg') }}" alt="" class="ui-icon">Abmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
{% endif %}
|
||||
<main class="content">
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="content-toolbar">
|
||||
<button class="ghost-button theme-toggle mobile-theme-toggle" type="button" data-theme-toggle>
|
||||
<img src="{{ url_for('static', filename='icons/moon.svg') }}" alt="" class="ui-icon theme-icon-dark">
|
||||
<img src="{{ url_for('static', filename='icons/sun.svg') }}" alt="" class="ui-icon theme-icon-light">
|
||||
Dark Mode
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
{% if current_user.is_authenticated %}
|
||||
<nav class="bottom-nav">
|
||||
<a href="{{ url_for('main.index') }}"><img src="{{ url_for('static', filename='icons/house.svg') }}" alt="" class="ui-icon">Übersicht</a>
|
||||
<a href="{{ url_for('planning.current') }}"><img src="{{ url_for('static', filename='icons/wallet.svg') }}" alt="" class="ui-icon">Planung</a>
|
||||
<a href="{{ url_for('months.index') }}"><img src="{{ url_for('static', filename='icons/calendar.svg') }}" alt="" class="ui-icon">Monate</a>
|
||||
<a href="{{ url_for('main.analytics') }}"><img src="{{ url_for('static', filename='icons/chart-simple.svg') }}" alt="" class="ui-icon">Auswertungen</a>
|
||||
{% if current_user.is_admin() %}
|
||||
<a href="{{ url_for('admin.index') }}"><img src="{{ url_for('static', filename='icons/sliders.svg') }}" alt="" class="ui-icon">Optionen</a>
|
||||
{% else %}
|
||||
<a href="#" data-enable-push><img src="{{ url_for('static', filename='icons/bell.svg') }}" alt="" class="ui-icon">Push</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,105 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Auswertungen | {{ app_name }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="page-hero">
|
||||
<div>
|
||||
<div class="eyebrow">Auswertungen</div>
|
||||
<h1>Kosten, Kategorien und Zuordnung</h1>
|
||||
<p class="muted">Alle Diagramme beziehen sich auf den aktuellen Monat {{ month.label }}.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="chart-grid analytics-grid">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h2 id="category-chart-title">Kategorien im Monat</h2>
|
||||
<small id="category-chart-subtitle">Tippe auf einen Bereich, um die Untereinträge im selben Diagramm zu sehen.</small>
|
||||
</div>
|
||||
<button id="category-chart-back" class="ghost-button small-button" type="button" hidden>Zurück</button>
|
||||
</div>
|
||||
<div class="chart-shell">
|
||||
<canvas
|
||||
id="category-chart"
|
||||
class="chart"
|
||||
data-chart-type="pie"
|
||||
data-drilldown-source="true"
|
||||
data-labels='{{ category_labels|tojson }}'
|
||||
data-values='{{ category_values|tojson }}'
|
||||
data-detail-keys='{{ category_keys|tojson }}'
|
||||
data-detail-map='{{ category_entry_map|tojson }}'
|
||||
data-detail-title-target="category-chart-title"
|
||||
data-detail-subtitle-target="category-chart-subtitle"
|
||||
data-detail-back-target="category-chart-back"
|
||||
data-default-detail-key='{{ default_category_id }}'></canvas>
|
||||
</div>
|
||||
<script type="application/json" id="category-chart-root-title">{"title":"Kategorien im Monat","subtitle":"Tippe auf einen Bereich, um die Untereinträge im selben Diagramm zu sehen."}</script>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h2>Kosten nach Zuordnung</h2>
|
||||
<small>Welche Ausgaben welchen registrierten Nutzern zugeordnet sind.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-shell">
|
||||
<canvas
|
||||
class="chart"
|
||||
data-chart-type="bar"
|
||||
data-labels='{{ benefit_labels|tojson }}'
|
||||
data-values='{{ benefit_values|tojson }}'></canvas>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h2>Kosten nach Hauptbereichen</h2>
|
||||
<small>Hilft beim schnellen Blick auf Gemeinschaft, Sparen, Freizeit und persönliche Auszahlung.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-shell">
|
||||
<canvas
|
||||
class="chart"
|
||||
data-chart-type="bar"
|
||||
data-index-axis="y"
|
||||
data-labels='{{ account_labels|tojson }}'
|
||||
data-values='{{ account_values|tojson }}'></canvas>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel analytics-wide-panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h2>Budgets im Monatsverlauf</h2>
|
||||
<small>Zeigt, wie sich die einzelnen Budgetbereiche von Monat zu Monat entwickeln.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-shell chart-shell-tall">
|
||||
<canvas
|
||||
class="chart"
|
||||
data-chart-type="line"
|
||||
data-labels='{{ budget_timeline_labels|tojson }}'
|
||||
data-datasets='{{ budget_timeline_datasets|tojson }}'></canvas>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel analytics-wide-panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h2>Größte Einträge im Monat</h2>
|
||||
<small>Die teuersten Positionen über alle Kategorien hinweg.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-shell chart-shell-tall">
|
||||
<canvas
|
||||
class="chart"
|
||||
data-chart-type="bar"
|
||||
data-index-axis="y"
|
||||
data-labels='{{ top_entry_labels|tojson }}'
|
||||
data-values='{{ top_entry_values|tojson }}'></canvas>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,196 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Übersicht | {{ app_name }}{% endblock %}
|
||||
{% block content %}
|
||||
{% from "_ui.html" import avatar %}
|
||||
<section class="page-hero">
|
||||
<div>
|
||||
<div class="eyebrow">Aktueller Monat</div>
|
||||
<h1>{{ month.label }}</h1>
|
||||
<p class="muted">
|
||||
{% if month.auto_created %}
|
||||
Monat wurde automatisch vorbereitet und kann jetzt angepasst werden.
|
||||
{% else %}
|
||||
Planung, Vorschläge und externe Beteiligungen auf einen Blick.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<a href="{{ url_for('planning.detail', label=month.label) }}" class="primary-button">Planung öffnen</a>
|
||||
</section>
|
||||
|
||||
<section class="cards-grid">
|
||||
<article class="metric-card">
|
||||
<span>Einkommen</span>
|
||||
<strong>{{ summary.total_income|currency }}</strong>
|
||||
<small>Delta zum Vormonat {{ summary.deltas.income_delta|currency }}</small>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>Kosten</span>
|
||||
<strong>{{ summary.total_costs|currency }}</strong>
|
||||
<small>Delta zum Vormonat {{ summary.deltas.cost_delta|currency }}</small>
|
||||
</article>
|
||||
<article class="metric-card highlight">
|
||||
<span>Restbetrag</span>
|
||||
<strong>{{ summary.remainder|currency }}</strong>
|
||||
<small>Delta zum Vormonat {{ summary.deltas.remainder_delta|currency }}</small>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>Vorgeschlagene Verteilung</span>
|
||||
<strong>{{ summary.suggestion_total|currency }}</strong>
|
||||
<small>{{ summary.suggestions|length }} Zielkonten vorbereitet</small>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="spotlight-grid split-layout">
|
||||
<article class="panel featured-panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h2>Daueraufträge prüfen</h2>
|
||||
<small>Welche Unterkonten sich gegenüber dem Vormonat geändert haben.</small>
|
||||
</div>
|
||||
</div>
|
||||
{% if shared_account_changes %}
|
||||
{% for account in shared_account_changes %}
|
||||
<div class="list-row">
|
||||
<div>
|
||||
<strong>{{ account.community_account.name }}</strong>
|
||||
<small>{{ account.current_total|currency }} geplant · bisher {{ account.previous_total|currency }}</small>
|
||||
</div>
|
||||
<span class="badge badge-warn">{{ account.delta|currency }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-state">Alle Unterkonten passen zum Vormonat. Daueraufträge müssen gerade nicht angepasst werden.</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h2>Persönliche Auszahlung</h2>
|
||||
<small>Automatisch aus dem aktuellen Restbetrag verteilt.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="participant-card-grid">
|
||||
<div class="participant-manage-card">
|
||||
<div class="list-row-main">
|
||||
{{ avatar(personal_payouts.first.name, personal_payouts.first.avatar_url, personal_payouts.first.avatar_initials, "md") }}
|
||||
<strong>{{ personal_payouts.first.name }}</strong>
|
||||
</div>
|
||||
<span>{{ personal_payouts.first.amount|currency }}</span>
|
||||
</div>
|
||||
<div class="participant-manage-card">
|
||||
<div class="list-row-main">
|
||||
{{ avatar(personal_payouts.second.name, personal_payouts.second.avatar_url, personal_payouts.second.avatar_initials, "md") }}
|
||||
<strong>{{ personal_payouts.second.name }}</strong>
|
||||
</div>
|
||||
<span>{{ personal_payouts.second.amount|currency }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="split-layout">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Extern mitzuteilen</h2>
|
||||
</div>
|
||||
{% if summary.external_totals %}
|
||||
{% for person in summary.external_totals %}
|
||||
<button type="button" class="list-row clickable-list-row" data-open-dialog="external-person-{{ person.participant_id }}">
|
||||
<div class="list-row-main">
|
||||
{{ avatar(person.participant_name, person.participant_avatar_url, person.participant_avatar_initials, "sm") }}
|
||||
<div>
|
||||
<strong>{{ person.participant_name }}</strong>
|
||||
<small>{{ person["items"]|length }} Positionen</small>
|
||||
</div>
|
||||
</div>
|
||||
<strong>{{ person.total|currency }}</strong>
|
||||
</button>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-state">Für diesen Monat gibt es keine externen Anteile.</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Offene Hinweise</h2>
|
||||
</div>
|
||||
{% if notifications %}
|
||||
{% for note in notifications %}
|
||||
<div class="note-card">
|
||||
<strong>{{ note.title }}</strong>
|
||||
<p>{{ note.body }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-state">Keine offenen Hinweise. Das sieht gut aus.</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="split-layout">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Größte Änderungen</h2>
|
||||
</div>
|
||||
{% if summary.top_changes %}
|
||||
{% for item in summary.top_changes %}
|
||||
<div class="list-row">
|
||||
<div>
|
||||
<strong>{{ item.entry_name }}</strong>
|
||||
<small>{{ item.category_name }}</small>
|
||||
</div>
|
||||
<strong>{{ item.delta|currency }}</strong>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-state">Noch keine Vergleichsdaten zum Vormonat vorhanden.</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Letzte 6 Monate</h2>
|
||||
</div>
|
||||
{% for item in recent_months %}
|
||||
<a class="list-row link-row" href="{{ url_for('planning.detail', label=item.label) }}">
|
||||
<div>
|
||||
<strong>{{ item.label }}</strong>
|
||||
<small>{% if item.auto_created %}automatisch erstellt{% else %}manuell gepflegt{% endif %}</small>
|
||||
</div>
|
||||
<span>Öffnen</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</article>
|
||||
</section>
|
||||
|
||||
{% for person in summary.external_totals %}
|
||||
<dialog id="external-person-{{ person.participant_id }}" class="app-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<div class="stack-form">
|
||||
<div class="dialog-section-head">
|
||||
<div class="list-row-main">
|
||||
{{ avatar(person.participant_name, person.participant_avatar_url, person.participant_avatar_initials, "md") }}
|
||||
<div>
|
||||
<h3>{{ person.participant_name }}</h3>
|
||||
<small>{{ person.total|currency }} gesamt</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-entry-list">
|
||||
{% for item in person["items"] %}
|
||||
<div class="dialog-entry-row static-entry-row">
|
||||
<div>
|
||||
<strong>{{ item.entry_name }}</strong>
|
||||
<small>Extern mitzuteilen</small>
|
||||
</div>
|
||||
<span>{{ item.amount|currency }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Monate | {{ app_name }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="page-hero">
|
||||
<div>
|
||||
<div class="eyebrow">Monatsverwaltung</div>
|
||||
<h1>Monate vorbereiten und sperren</h1>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('months.create') }}" class="inline-form">
|
||||
<input type="month" name="label" required>
|
||||
<button class="primary-button" type="submit">Monat anlegen</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
{% for month in months %}
|
||||
<div class="month-row">
|
||||
<div>
|
||||
<strong>{{ month.label }}</strong>
|
||||
<small>
|
||||
{% if month.auto_created %}automatisch erstellt{% else %}manuell erstellt{% endif %}
|
||||
{% if month.is_locked %} · gesperrt{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
<div class="row-actions">
|
||||
<a class="ghost-button" href="{{ url_for('planning.detail', label=month.label) }}">Öffnen</a>
|
||||
<form method="post" action="{{ url_for('months.toggle_lock', label=month.label) }}">
|
||||
<button class="ghost-button" type="submit">{{ "Entsperren" if month.is_locked else "Sperren" }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,815 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Planung {{ month.label }} | {{ app_name }}{% endblock %}
|
||||
{% block content %}
|
||||
{% from "_ui.html" import avatar %}
|
||||
<section class="planning-hero planning-hero-strong">
|
||||
<div class="planning-title">
|
||||
<div class="eyebrow">Planung</div>
|
||||
<h1>{{ planning_heading }}</h1>
|
||||
<p class="muted">Monat direkt auf dieser Seite pflegen: Einkommen, Kategorien, Einträge, Verteilung und Split-Personen.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cards-grid cards-grid-four">
|
||||
<article class="metric-card">
|
||||
<span>Gesamteinkommen</span>
|
||||
<strong>{{ summary.total_income|currency }}</strong>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>Gesamtkosten</span>
|
||||
<strong>{{ summary.total_costs|currency }}</strong>
|
||||
</article>
|
||||
<article class="metric-card highlight">
|
||||
<span>Restbetrag</span>
|
||||
<strong>{{ summary.remainder|currency }}</strong>
|
||||
<small>Aktuelle Verteilung {{ summary.allocation_total|currency }}</small>
|
||||
</article>
|
||||
<article class="metric-card soft-accent">
|
||||
<span>Vorschläge</span>
|
||||
<strong>{{ summary.suggestion_total|currency }}</strong>
|
||||
<small>Bis Mindestziel offen</small>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="planning-hub-grid">
|
||||
<button type="button" class="summary-category-card utility-summary-card" data-open-dialog="income-dialog">
|
||||
<div class="summary-card-head">
|
||||
<strong>Einkommen</strong>
|
||||
<img src="{{ url_for('static', filename='icons/pencil.svg') }}" alt="" class="ui-icon small-ui-icon">
|
||||
</div>
|
||||
<div class="summary-card-meta">
|
||||
<span>{{ summary.total_income|currency }}</span>
|
||||
<small>{{ month.incomes|length }} Zeilen</small>
|
||||
</div>
|
||||
</button>
|
||||
<button type="button" class="summary-category-card utility-summary-card" data-open-dialog="split-people-dialog">
|
||||
<div class="summary-card-head">
|
||||
<strong>Personen für Splits</strong>
|
||||
<img src="{{ url_for('static', filename='icons/pencil.svg') }}" alt="" class="ui-icon small-ui-icon">
|
||||
</div>
|
||||
<div class="summary-card-meta">
|
||||
<span>{{ participants|length }}</span>
|
||||
<small>{{ participant_summary.internal_count }} Nutzer, {{ participant_summary.external_count }} {{ "Gast" if participant_summary.external_count == 1 else "Gäste" }}</small>
|
||||
</div>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="account-board">
|
||||
<article class="panel account-panel premium-panel">
|
||||
<div class="panel-head account-head">
|
||||
<div>
|
||||
<h2>Gemeinschaftskonten</h2>
|
||||
<small>{{ community_account_summary.shared_count }} Konten · {{ community_account_summary.personal_count }} privat</small>
|
||||
</div>
|
||||
<button class="ghost-button icon-button" type="button" data-open-dialog="community-account-create-dialog">
|
||||
<img src="{{ url_for('static', filename='icons/plus.svg') }}" alt="" class="ui-icon small-ui-icon">
|
||||
Konto
|
||||
</button>
|
||||
</div>
|
||||
<div class="category-summary-grid">
|
||||
{% for card in community_account_cards %}
|
||||
{% if card.is_read_only %}
|
||||
<div class="summary-category-card summary-static-card community-account-card">
|
||||
<div class="summary-card-head">
|
||||
<strong>{{ card.community_account.name }}</strong>
|
||||
<span class="icon-label muted-label">Nur Anzeige</span>
|
||||
</div>
|
||||
<div class="summary-card-meta">
|
||||
<span>{{ card.current_total|currency }}</span>
|
||||
<small>Persönliche Auszahlung</small>
|
||||
</div>
|
||||
{% if card.delta %}
|
||||
<div class="card-inline-note">
|
||||
<span class="badge {% if card.delta > 0 %}badge-warn{% else %}badge-good{% endif %}">
|
||||
{{ card.delta|currency }} zum Vormonat
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<button type="button" class="summary-category-card community-account-card" data-open-dialog="community-account-item-{{ card.community_account.id }}">
|
||||
<div class="summary-card-head">
|
||||
<strong>{{ card.community_account.name }}</strong>
|
||||
<img src="{{ url_for('static', filename='icons/pencil.svg') }}" alt="" class="ui-icon small-ui-icon">
|
||||
</div>
|
||||
<div class="summary-card-meta">
|
||||
<span>{{ card.current_total|currency }}</span>
|
||||
<small>{{ card.assigned_budget_names|length }} Budgets</small>
|
||||
</div>
|
||||
{% if card.delta %}
|
||||
<div class="card-inline-note">
|
||||
<span class="badge {% if card.delta > 0 %}badge-warn{% else %}badge-good{% endif %}">
|
||||
{{ card.delta|currency }} zum Vormonat
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if card.assigned_budget_names %}
|
||||
<div class="card-inline-note">
|
||||
<small>{{ card.assigned_budget_names|join(", ") }}</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="account-board">
|
||||
{% for account_data in planning_accounts %}
|
||||
{% if account_data.categories %}
|
||||
<article class="panel account-panel premium-panel">
|
||||
<div class="panel-head account-head">
|
||||
<div>
|
||||
<h2>{{ account_data.account.name }}</h2>
|
||||
<small>
|
||||
Gesamtkosten {{ account_data.total|currency }}
|
||||
{% if account_data.account.slug == "gemeinschaftskonto" %}
|
||||
· Jährlich {{ (account_data.total * 12)|currency }}
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
{% if account_data.account.slug == "sparen-und-verteilung" %}
|
||||
<button class="ghost-button icon-button" type="button" data-open-dialog="dialog-add-category" data-area="distribution" data-placeholder="Name Sparkonto" data-dialog-label="Sparkonto">
|
||||
<img src="{{ url_for('static', filename='icons/plus.svg') }}" alt="" class="ui-icon small-ui-icon">
|
||||
Sparkonto
|
||||
</button>
|
||||
{% elif account_data.account.slug == "gemeinschaftskonto" %}
|
||||
<button class="ghost-button icon-button" type="button" data-open-dialog="dialog-add-category" data-area="budget" data-placeholder="Name Budget">
|
||||
<img src="{{ url_for('static', filename='icons/plus.svg') }}" alt="" class="ui-icon small-ui-icon">
|
||||
Kategorie
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="ghost-button icon-button" type="button" data-open-dialog="dialog-add-category" data-account-id="{{ account_data.account.id }}">
|
||||
<img src="{{ url_for('static', filename='icons/plus.svg') }}" alt="" class="ui-icon small-ui-icon">
|
||||
Kategorie
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="category-summary-grid">
|
||||
{% for category_data in account_data.categories %}
|
||||
<button type="button" class="summary-category-card {% if category_data.distribution_hint and category_data.distribution_hint.status %}range-status-{{ category_data.distribution_hint.status }}{% endif %}" data-open-dialog="{{ category_data.dialog_id }}">
|
||||
<div class="summary-card-head">
|
||||
<div>
|
||||
<strong>{{ category_data.category.name }}</strong>
|
||||
{% if category_data.category.description %}
|
||||
<small>{{ category_data.category.description }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<img src="{{ url_for('static', filename='icons/pencil.svg') }}" alt="" class="ui-icon small-ui-icon">
|
||||
</div>
|
||||
<div class="summary-card-meta">
|
||||
<span>{{ category_data.total|currency }}</span>
|
||||
<small>{{ category_data.entry_count }} Einträge</small>
|
||||
</div>
|
||||
{% if category_data.distribution_hint %}
|
||||
<div class="card-inline-note">
|
||||
<small>{{ category_data.distribution_hint.range_label if category_data.distribution_hint.range_label else "Rest nach Zielkonten" }}</small>
|
||||
</div>
|
||||
<div class="card-inline-note">
|
||||
<small>Aktuell {{ category_data.distribution_hint.current_pct }} % vom Einkommen</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category_data.distribution_kind == "personal" and category_data.distribution_suggestion_total is not none %}
|
||||
<div class="card-inline-note">
|
||||
{% if category_data.distribution_suggestion_total > 0 %}
|
||||
<span class="badge">Automatisch +{{ category_data.distribution_suggestion_total|currency }}</span>
|
||||
{% elif category_data.distribution_suggestion_total < 0 %}
|
||||
<span class="badge">Fehlen {{ (-category_data.distribution_suggestion_total)|currency }}</span>
|
||||
{% else %}
|
||||
<span class="badge">Automatisch ausgeglichen</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<datalist id="category-suggestions">
|
||||
{% for category in categories %}
|
||||
<option value="{{ category.name }}">{{ category.account.name }}</option>
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
|
||||
<dialog id="income-dialog" class="app-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<div class="stack-form">
|
||||
<div class="dialog-section-head">
|
||||
<div>
|
||||
<h3>Einkommen</h3>
|
||||
<small>{{ summary.total_income|currency }} · {{ month.incomes|length }} Zeilen</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sheet-card-grid">
|
||||
{% for income in month.incomes|sort(attribute='sort_order') %}
|
||||
<form method="post" action="{{ url_for('planning.update_income', label=month.label) }}" class="sheet-card">
|
||||
<input type="hidden" name="income_id" value="{{ income.id }}">
|
||||
<input type="hidden" name="return_dialog" value="income-dialog">
|
||||
<input name="income_label" value="{{ income.label }}" placeholder="Name der Einkommenszeile" required>
|
||||
<input name="amount" type="number" step="0.01" inputmode="decimal" value="{{ income.amount }}">
|
||||
<div class="dialog-action-row dialog-action-spread">
|
||||
<button class="ghost-button small-button" type="submit">Speichern</button>
|
||||
{% if month.incomes|length > 1 %}
|
||||
<button class="ghost-button danger-button small-button" type="submit" formaction="{{ url_for('planning.delete_income', label=month.label, income_id=income.id) }}">Löschen</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('planning.create_income', label=month.label) }}" class="soft-form-section stack-form">
|
||||
<div class="dialog-section-head">
|
||||
<div>
|
||||
<strong>Neue Einkommenszeile</strong>
|
||||
<small>Zum Beispiel Gehalt, Bonus oder Nebenjob.</small>
|
||||
</div>
|
||||
</div>
|
||||
<input name="income_label" placeholder="Name der Einkommenszeile" required>
|
||||
<input name="amount" type="number" step="0.01" inputmode="decimal" placeholder="Betrag">
|
||||
<button class="primary-button" type="submit">Einkommen anlegen</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="split-people-dialog" class="app-dialog category-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<div class="stack-form">
|
||||
<div class="dialog-section-head">
|
||||
<div>
|
||||
<h3>Personen für Splits</h3>
|
||||
<small>{{ participant_summary.internal_count }} Nutzer, {{ participant_summary.external_count }} {{ "Gast" if participant_summary.external_count == 1 else "Gäste" }}</small>
|
||||
</div>
|
||||
<button class="ghost-button icon-button" type="button" data-open-dialog="dialog-add-participant" data-return-dialog="split-people-dialog">
|
||||
<img src="{{ url_for('static', filename='icons/plus.svg') }}" alt="" class="ui-icon small-ui-icon">
|
||||
Person
|
||||
</button>
|
||||
</div>
|
||||
<div class="participant-card-grid">
|
||||
{% for participant in participants %}
|
||||
{% if participant.is_app_user %}
|
||||
<div class="participant-manage-card summary-static-card">
|
||||
<span class="list-row-main">
|
||||
{{ avatar(participant.display_name, participant.avatar_url, participant.avatar_initials, "sm") }}
|
||||
<strong>{{ participant.display_name }}</strong>
|
||||
</span>
|
||||
<small>Nutzer · automatisch aus Benutzerkonto</small>
|
||||
<span class="icon-label muted-label">Automatisch</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<button class="participant-manage-card" type="button" data-open-dialog="participant-dialog-{{ participant.id }}">
|
||||
<span class="list-row-main">
|
||||
{{ avatar(participant.display_name, participant.avatar_url, participant.avatar_initials, "sm") }}
|
||||
<strong>{{ participant.display_name }}</strong>
|
||||
</span>
|
||||
<small>Gast · {{ "aktiv" if participant.is_active else "inaktiv" }}</small>
|
||||
<span class="icon-label"><img src="{{ url_for('static', filename='icons/pencil.svg') }}" alt="" class="ui-icon small-ui-icon">Details</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="community-account-create-dialog" class="app-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('planning.create_community_account', label=month.label) }}" class="stack-form">
|
||||
<div class="dialog-section-head">
|
||||
<div>
|
||||
<h3>Neues Gemeinschaftskonto</h3>
|
||||
<small>Zum Beispiel separates Fixkosten- oder Reisekonto.</small>
|
||||
</div>
|
||||
</div>
|
||||
<input name="name" placeholder="Kontoname" required>
|
||||
<textarea name="description" rows="3" placeholder="Beschreibung optional"></textarea>
|
||||
<button class="primary-button" type="submit">Konto anlegen</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
{% for card in community_account_cards if not card.is_read_only %}
|
||||
<dialog id="community-account-item-{{ card.community_account.id }}" class="app-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('planning.update_community_account', label=month.label, community_account_id=card.community_account.id) }}" class="stack-form">
|
||||
<input type="hidden" name="return_dialog" value="community-account-item-{{ card.community_account.id }}">
|
||||
<h3>{{ card.community_account.name }}</h3>
|
||||
<small>{{ card.current_total|currency }} aktuell{% if card.delta %} · {{ card.delta|currency }} zum Vormonat{% endif %}</small>
|
||||
<input name="name" value="{{ card.community_account.name }}" required>
|
||||
<textarea name="description" rows="3" placeholder="Beschreibung optional">{{ card.community_account.description or '' }}</textarea>
|
||||
<div class="distribution-note-card">
|
||||
<div>
|
||||
<strong>Budgets zuweisen</strong>
|
||||
<small>Diese Budget-Kategorien laufen über dieses Gemeinschaftskonto. Bereits anders zugewiesene Budgets sind hier nicht auswählbar.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="participant-chip-grid">
|
||||
{% for category in categories if category.account.slug == "gemeinschaftskonto" %}
|
||||
{% if category.community_account_id in [none, card.community_account.id] %}
|
||||
<label class="participant-chip budget-assignment-chip">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="category_ids"
|
||||
value="{{ category.id }}"
|
||||
{% if category.community_account_id == card.community_account.id %}checked{% endif %}>
|
||||
{{ category.name }}
|
||||
</label>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if card.assigned_budget_names %}
|
||||
<small>Aktuell zugewiesen: {{ card.assigned_budget_names|join(", ") }}</small>
|
||||
{% endif %}
|
||||
<div class="dialog-action-row dialog-action-spread">
|
||||
<button class="primary-button" type="submit">Konto speichern</button>
|
||||
<button class="ghost-button danger-button" type="button" data-open-dialog="confirm-delete-community-account-{{ card.community_account.id }}">Konto löschen</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="confirm-delete-community-account-{{ card.community_account.id }}" class="app-dialog confirm-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<div class="stack-form">
|
||||
<h3>Konto wirklich löschen?</h3>
|
||||
<p class="muted">`{{ card.community_account.name }}` wird ausgeblendet und seine Budget-Zuordnungen werden gelöst.</p>
|
||||
<form method="post" action="{{ url_for('planning.delete_community_account', label=month.label, community_account_id=card.community_account.id) }}" class="dialog-action-row dialog-action-spread">
|
||||
<button class="ghost-button" type="button" data-open-dialog="community-account-item-{{ card.community_account.id }}">Zurück</button>
|
||||
<button class="primary-button danger-fill-button" type="submit">Jetzt löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endfor %}
|
||||
|
||||
<dialog id="dialog-add-category" class="app-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('planning.create_category', label=month.label) }}" class="stack-form">
|
||||
<input type="hidden" name="area" value="budget" data-dialog-area>
|
||||
<input type="hidden" name="account_id" value="" data-dialog-account-id>
|
||||
<h3>Neue Kategorie</h3>
|
||||
<select name="community_account_id" data-community-account-field>
|
||||
<option value="">Gemeinschaftskonto zuweisen</option>
|
||||
{% for community_account in community_accounts if community_account.account_type == "shared" %}
|
||||
<option value="{{ community_account.id }}">{{ community_account.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input name="name" list="category-suggestions" placeholder="Name Budget" data-dialog-name-placeholder required>
|
||||
<button class="primary-button" type="submit">Kategorie anlegen</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="dialog-add-entry" class="app-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('planning.create_entry', label=month.label) }}" class="stack-form">
|
||||
<h3>Neuen Eintrag anlegen</h3>
|
||||
<input type="hidden" name="area" value="budget" data-dialog-area>
|
||||
<input type="hidden" name="account_id" value="" data-dialog-account-id>
|
||||
<input type="hidden" name="return_dialog" value="" data-dialog-return-dialog>
|
||||
<input name="category_name" list="category-suggestions" data-dialog-category-name placeholder="Kategorie" required>
|
||||
<input name="name" placeholder="Eintragsname" required>
|
||||
<div class="sheet-card-grid" data-annual-sync-wrapper>
|
||||
<label>
|
||||
Monatlich
|
||||
<input name="planned_amount" type="number" step="0.01" inputmode="decimal" placeholder="Monatlicher Betrag" data-annual-sync="monthly">
|
||||
</label>
|
||||
<label data-annual-visibility>
|
||||
Jährlich
|
||||
<input name="annual_amount" type="number" step="0.01" inputmode="decimal" placeholder="Jährlicher Betrag" data-annual-sync="yearly">
|
||||
</label>
|
||||
</div>
|
||||
<select name="benefit_scope">
|
||||
{% for option in benefit_options %}
|
||||
<option value="{{ option.value }}">Betrifft {{ option.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label class="check-label">
|
||||
<input type="checkbox" name="is_allocation_target">
|
||||
Sparkonto
|
||||
<small>Zeigt Zielbereich und direkte Budgetpflege über die Karte in Planung.</small>
|
||||
</label>
|
||||
<textarea name="note" rows="3" placeholder="Notiz optional"></textarea>
|
||||
<details class="split-picker">
|
||||
<summary class="ghost-button">Mit anderen Personen teilen</summary>
|
||||
<div class="participant-chip-grid split-panel">
|
||||
{% for participant in participants %}
|
||||
<label class="participant-chip"><input type="checkbox" name="participant_ids" value="{{ participant.id }}"> {{ participant.display_name }}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
<button class="primary-button" type="submit">Eintrag anlegen</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="dialog-add-participant" class="app-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('planning.create_participant', label=month.label) }}" class="stack-form" enctype="multipart/form-data">
|
||||
<input type="hidden" name="return_dialog" value="" data-dialog-return-dialog>
|
||||
<h3>Neue Person</h3>
|
||||
<input name="name" placeholder="Name" required>
|
||||
<label>
|
||||
<span>Avatar hochladen</span>
|
||||
<input name="avatar_file" type="file" accept="image/*">
|
||||
</label>
|
||||
<input name="avatar_url" placeholder="Avatar-URL optional">
|
||||
<label class="check-label"><input type="checkbox" name="is_external" checked> Extern ohne App-Zugang</label>
|
||||
<button class="primary-button" type="submit">Person anlegen</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
{% for account_data in planning_accounts %}
|
||||
{% for category_data in account_data.categories %}
|
||||
{% if not category_data.is_personal_split %}
|
||||
<dialog id="{{ category_data.dialog_id }}" class="app-dialog category-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<div class="stack-form">
|
||||
<div class="dialog-section-head">
|
||||
<div>
|
||||
<h3>{{ category_data.category.name }}</h3>
|
||||
<small>{{ category_data.total|currency }} · {{ category_data.entry_count }} Einträge</small>
|
||||
</div>
|
||||
{% if category_data.allow_new_entries %}
|
||||
<button class="ghost-button icon-button" type="button" data-open-dialog="dialog-add-entry" data-account-id="{{ category_data.category.account_id }}" data-category-name="{{ category_data.category.name }}" data-return-dialog="{{ category_data.dialog_id }}" data-area="{{ 'budget' if category_data.category.account.slug == 'gemeinschaftskonto' else 'distribution' }}">
|
||||
<img src="{{ url_for('static', filename='icons/plus.svg') }}" alt="" class="ui-icon small-ui-icon">
|
||||
Eintrag
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if category_data.distribution_kind == "single" %}
|
||||
{% set distribution_entry = category_data.entries|first %}
|
||||
{% set distribution_suggestion = distribution_entry.distribution_suggestion if distribution_entry else none %}
|
||||
{% if category_data.direct_entry %}
|
||||
<form method="post" action="{{ url_for('planning.update_entry', label=month.label) }}" class="soft-form-section stack-form">
|
||||
<input type="hidden" name="value_id" value="{{ category_data.direct_entry.value.id }}">
|
||||
<input type="hidden" name="return_dialog" value="{{ category_data.dialog_id }}">
|
||||
<input type="hidden" name="entry_name" value="{{ category_data.direct_entry.entry.name }}">
|
||||
<input type="hidden" name="category_id" value="{{ category_data.direct_entry.entry.category_id }}">
|
||||
<input type="hidden" name="benefit_scope" value="{{ category_data.direct_entry.entry.benefit_scope }}">
|
||||
<div class="dialog-section-head">
|
||||
<div>
|
||||
<strong>Budget direkt anpassen</strong>
|
||||
<small>Wenn `Sparkonto` aktiv ist, steuert dieser Eintrag das Budget direkt auf der Karte.</small>
|
||||
</div>
|
||||
</div>
|
||||
<label class="check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="is_allocation_target"
|
||||
{% if category_data.direct_entry.entry.is_allocation_target %}checked{% endif %}>
|
||||
Sparkonto
|
||||
<small>Aktiviert Zielbereich und direkte Budgetpflege über diese Karte.</small>
|
||||
</label>
|
||||
<label>
|
||||
Monatliches Budget
|
||||
<input
|
||||
name="planned_amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
inputmode="decimal"
|
||||
value="{{ category_data.direct_entry.value.planned_amount }}">
|
||||
</label>
|
||||
<label class="check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="allocation_is_locked"
|
||||
{% if category_data.direct_entry.distribution_allocation and category_data.direct_entry.distribution_allocation.is_locked %}checked{% endif %}>
|
||||
Budget fixieren
|
||||
</label>
|
||||
<button class="primary-button" type="submit">Budget speichern</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if category_data.direct_entry and category_data.direct_entry.entry.is_allocation_target %}
|
||||
<div class="distribution-note-card">
|
||||
<div>
|
||||
<strong>Verteilung</strong>
|
||||
<small>
|
||||
Der Betrag in dieser Kategorie steuert direkt die monatliche Verteilung.
|
||||
{% if category_data.distribution_hint %}
|
||||
Zielbereich {{ category_data.distribution_hint.range_label }} vom Einkommen.
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
{% if distribution_suggestion %}
|
||||
<div class="row-actions">
|
||||
<span class="badge">Noch offen {{ category_data.distribution_suggestion_total|currency }}</span>
|
||||
<form method="post" action="{{ url_for('planning.accept_single_suggestion', label=month.label, account_id=distribution_suggestion.target_account_id) }}">
|
||||
<input type="hidden" name="return_dialog" value="{{ category_data.dialog_id }}">
|
||||
<button class="ghost-button small-button" type="submit">Übernehmen</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if category_data.distribution_hint %}
|
||||
<form method="post" action="{{ url_for('planning.update_distribution_settings', label=month.label, slug=category_data.distribution_account_slug) }}" class="soft-form-section stack-form">
|
||||
<input type="hidden" name="return_dialog" value="{{ category_data.dialog_id }}">
|
||||
<div class="dialog-section-head">
|
||||
<div>
|
||||
<strong>Zielbereich anpassen</strong>
|
||||
<small>Der Vorschlag arbeitet innerhalb dieses Prozentbereichs.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sheet-card-grid">
|
||||
<label>
|
||||
Von %
|
||||
<input type="number" name="min_pct" min="0" max="100" step="1" value="{{ category_data.distribution_hint.min_pct }}">
|
||||
</label>
|
||||
<label>
|
||||
Bis %
|
||||
<input type="number" name="max_pct" min="0" max="100" step="1" value="{{ category_data.distribution_hint.max_pct }}">
|
||||
</label>
|
||||
</div>
|
||||
<button class="ghost-button small-button" type="submit">Bereich speichern</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if category_data.distribution_kind != "single" %}
|
||||
<form method="post" action="{{ url_for('planning.update_category', label=month.label, category_id=category_data.category.id) }}" class="stack-form soft-form-section">
|
||||
<input name="name" value="{{ category_data.category.name }}" required>
|
||||
{% if category_data.category.account.slug == "gemeinschaftskonto" %}
|
||||
<select name="community_account_id">
|
||||
<option value="">Kein Gemeinschaftskonto</option>
|
||||
{% for community_account in community_accounts if community_account.account_type == "shared" %}
|
||||
<option value="{{ community_account.id }}" {% if category_data.category.community_account_id == community_account.id %}selected{% endif %}>{{ community_account.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
<textarea name="description" rows="3" placeholder="Beschreibung optional">{{ category_data.category.description or '' }}</textarea>
|
||||
<div class="dialog-action-row">
|
||||
<button class="primary-button" type="submit">Kategorie speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<div class="dialog-entry-list">
|
||||
{% for item in category_data.entries %}
|
||||
{% if not (category_data.direct_entry and category_data.direct_entry.value.id == item.value.id) %}
|
||||
<button type="button" class="dialog-entry-row" data-open-dialog="{{ item.dialog_id }}">
|
||||
<div>
|
||||
<strong>{{ item.entry.name }}</strong>
|
||||
<small>
|
||||
{{ item.benefit_label }}
|
||||
{% if item.share_names %} · Split: {{ item.share_names }}{% endif %}
|
||||
{% if item.value.note %} · {{ item.value.note }}{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
<div class="entry-row-trailing">
|
||||
{% if item.entry.category and item.entry.category.account and item.entry.category.account.slug == "gemeinschaftskonto" and item.benefit_users %}
|
||||
<div class="stacked-avatars">
|
||||
{% for user in item.benefit_users %}
|
||||
{{ avatar(user.ui_name, user.avatar_url, user.avatar_initials, "sm", "stacked-avatar") }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<span>{{ item.amount|currency }}</span>
|
||||
</div>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ url_for('planning.delete_category', label=month.label, category_id=category_data.category.id) }}">
|
||||
<button class="ghost-button danger-button" type="submit">Kategorie löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{% for account_data in planning_accounts %}
|
||||
{% for category_data in account_data.categories %}
|
||||
{% if category_data.is_personal_split %}
|
||||
<dialog id="{{ category_data.dialog_id }}" class="app-dialog category-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<div class="stack-form">
|
||||
<div class="dialog-section-head">
|
||||
<div>
|
||||
<h3>Persönliche Auszahlung</h3>
|
||||
<small>{{ category_data.total|currency }} · {{ category_data.entry_count }} Einträge</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="distribution-note-card">
|
||||
<div>
|
||||
<strong>Split</strong>
|
||||
<small>Erst werden Sparen, Urlaub und Freizeit bedient. Der verbleibende Rest wird danach automatisch auf die persönliche Auszahlung verteilt.</small>
|
||||
</div>
|
||||
{% if category_data.distribution_suggestion_total > 0 %}
|
||||
<span class="badge">Automatisch +{{ category_data.distribution_suggestion_total|currency }}</span>
|
||||
{% elif category_data.distribution_suggestion_total < 0 %}
|
||||
<span class="badge">Fehlen {{ (-category_data.distribution_suggestion_total)|currency }}</span>
|
||||
{% else %}
|
||||
<span class="badge">Automatisch ausgeglichen</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ url_for('planning.update_personal_split', label=month.label) }}" class="soft-form-section stack-form">
|
||||
<input type="hidden" name="return_dialog" value="{{ category_data.dialog_id }}">
|
||||
<div class="dialog-section-head">
|
||||
<div>
|
||||
<strong>Aufteilung anpassen</strong>
|
||||
<small>Wenn du einen Wert aenderst, wird der andere automatisch auf 100 % ergaenzt.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sheet-card-grid">
|
||||
<label>
|
||||
{{ category_data.distribution_items[0].label if category_data.distribution_items|length > 0 else 'Person 1' }} in %
|
||||
<input
|
||||
type="number"
|
||||
name="flo_pct"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value="{{ personal_split.flo_pct }}"
|
||||
data-personal-split="flo">
|
||||
</label>
|
||||
<label>
|
||||
{{ category_data.distribution_items[1].label if category_data.distribution_items|length > 1 else 'Person 2' }} in %
|
||||
<input
|
||||
type="number"
|
||||
name="desi_pct"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value="{{ personal_split.desi_pct }}"
|
||||
data-personal-split="desi">
|
||||
</label>
|
||||
</div>
|
||||
<button class="ghost-button small-button" type="submit">Split speichern</button>
|
||||
</form>
|
||||
|
||||
<div class="personal-split-grid">
|
||||
{% for distribution_item in category_data.distribution_items %}
|
||||
<div class="sheet-card compact-sheet-card">
|
||||
<strong>{{ distribution_item.label }}</strong>
|
||||
<span>{{ distribution_item.auto_amount|currency }}</span>
|
||||
<small>Automatisch aus Restbetrag berechnet</small>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="dialog-entry-list">
|
||||
{% for item in category_data.entries %}
|
||||
<div class="dialog-entry-row static-entry-row">
|
||||
<div>
|
||||
<strong>{{ item.entry.name }}</strong>
|
||||
<small>
|
||||
{{ item.benefit_label }}
|
||||
{% if item.value.note %} · {{ item.value.note }}{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
<span>{{ item.amount|currency }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{% for account_data in planning_accounts %}
|
||||
{% for category_data in account_data.categories %}
|
||||
{% for item in category_data.entries %}
|
||||
<dialog id="{{ item.dialog_id }}" class="app-dialog entry-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('planning.update_entry', label=month.label) }}" class="stack-form">
|
||||
<input type="hidden" name="value_id" value="{{ item.value.id }}">
|
||||
<input type="hidden" name="return_dialog" value="{{ category_data.dialog_id }}">
|
||||
<div class="dialog-section-head">
|
||||
<h3>{{ item.entry.name }}</h3>
|
||||
</div>
|
||||
<input name="entry_name" value="{{ item.entry.name }}" required>
|
||||
<select name="category_id">
|
||||
{% for category in categories %}
|
||||
<option value="{{ category.id }}" {% if item.entry.category_id == category.id %}selected{% endif %}>{{ category.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if item.entry.category.account.slug == "gemeinschaftskonto" %}
|
||||
<div class="sheet-card-grid" data-annual-sync-wrapper>
|
||||
<label>
|
||||
Monatlich
|
||||
<input name="planned_amount" type="number" step="0.01" inputmode="decimal" value="{{ item.value.planned_amount }}" data-annual-sync="monthly">
|
||||
</label>
|
||||
<label>
|
||||
Jährlich
|
||||
<input name="annual_amount" type="number" step="0.01" inputmode="decimal" value="{{ '%.2f'|format(item.value.planned_amount * 12) }}" data-annual-sync="yearly">
|
||||
</label>
|
||||
</div>
|
||||
{% else %}
|
||||
<input name="planned_amount" type="number" step="0.01" inputmode="decimal" value="{{ item.value.planned_amount }}">
|
||||
{% endif %}
|
||||
<select name="benefit_scope">
|
||||
{% for option in benefit_options %}
|
||||
<option value="{{ option.value }}" {% if item.entry.benefit_scope == option.value %}selected{% endif %}>Betrifft {{ option.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label class="check-label">
|
||||
<input type="checkbox" name="is_allocation_target" {% if item.entry.is_allocation_target %}checked{% endif %}>
|
||||
Sparkonto
|
||||
<small>Zeigt Zielbereich und direkte Budgetpflege über die Karte in Planung.</small>
|
||||
</label>
|
||||
<textarea name="note" rows="4" placeholder="Notiz">{{ item.value.note or '' }}</textarea>
|
||||
{% if item.is_distribution_entry and item.distribution_allocation %}
|
||||
<div class="distribution-note-card">
|
||||
<div>
|
||||
<strong>Verteilung für {{ item.entry.name }}</strong>
|
||||
<small>
|
||||
Dieser Eintrag steuert den Zielbetrag in der Verteilung.
|
||||
{% if item.distribution_hint %}
|
||||
Zielbereich {{ item.distribution_hint.range_label }} vom Einkommen.
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
{% if item.distribution_suggestion %}
|
||||
<span class="badge">Noch offen {{ item.distribution_hint.remaining_amount if item.distribution_hint else item.distribution_suggestion.suggested_amount|currency }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<label class="check-label"><input type="checkbox" name="allocation_is_locked" {% if item.distribution_allocation.is_locked %}checked{% endif %}> Verteilung fixieren</label>
|
||||
{% endif %}
|
||||
<details class="split-picker" {% if item.entry.share_rules %}open{% endif %}>
|
||||
<summary class="ghost-button">Mit anderen Personen teilen</summary>
|
||||
<div class="participant-chip-grid split-panel">
|
||||
{% for participant in participants %}
|
||||
<label class="participant-chip">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="participant_ids"
|
||||
value="{{ participant.id }}"
|
||||
{% if item.entry.share_rules|selectattr('participant_id', 'equalto', participant.id)|list %}checked{% endif %}>
|
||||
{{ participant.display_name }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
<div class="dialog-action-row dialog-action-spread">
|
||||
<button class="primary-button" type="submit">Speichern</button>
|
||||
<button class="ghost-button danger-button" type="button" data-open-dialog="confirm-delete-entry-{{ item.value.id }}">Eintrag löschen</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="confirm-delete-entry-{{ item.value.id }}" class="app-dialog confirm-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<div class="stack-form">
|
||||
<h3>Eintrag wirklich löschen?</h3>
|
||||
<p class="muted">`{{ item.entry.name }}` wird ausgeblendet und erscheint nicht mehr in der Planung.</p>
|
||||
<form method="post" action="{{ url_for('planning.delete_entry', label=month.label, entry_id=item.entry.id) }}" class="dialog-action-row dialog-action-spread">
|
||||
<input type="hidden" name="return_dialog" value="{{ category_data.dialog_id }}">
|
||||
<button class="ghost-button" type="button" data-open-dialog="{{ item.dialog_id }}">Zurück</button>
|
||||
<button class="primary-button danger-fill-button" type="submit">Jetzt löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{% for participant in participants if not participant.is_app_user %}
|
||||
<dialog id="participant-dialog-{{ participant.id }}" class="app-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('planning.update_participant', label=month.label, participant_id=participant.id) }}" class="stack-form" enctype="multipart/form-data">
|
||||
<input type="hidden" name="return_dialog" value="split-people-dialog">
|
||||
<h3>Person bearbeiten</h3>
|
||||
<input name="name" value="{{ participant.display_name }}" required>
|
||||
<label>
|
||||
<span>Avatar hochladen</span>
|
||||
<input name="avatar_file" type="file" accept="image/*">
|
||||
</label>
|
||||
<input name="avatar_url" value="{{ participant.avatar_url or '' }}" placeholder="Avatar-URL optional">
|
||||
<label class="check-label"><input type="checkbox" name="is_external" {% if participant.is_external %}checked{% endif %}> Extern ohne App-Zugang</label>
|
||||
<label class="check-label"><input type="checkbox" name="is_active" {% if participant.is_active %}checked{% endif %}> Aktiv</label>
|
||||
<button class="primary-button" type="submit">Speichern</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user