release: publish saldo 0.1.0

This commit is contained in:
2026-04-21 21:17:36 +02:00
commit 6f5e704739
95 changed files with 9196 additions and 0 deletions
+9
View File
@@ -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 %}
+74
View File
@@ -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 %}
+33
View File
@@ -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>
+46
View File
@@ -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>
+91
View File
@@ -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>
+105
View File
@@ -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 %}
+196
View File
@@ -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 %}
+34
View File
@@ -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 %}
+815
View File
@@ -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 %}