@@ -1,6 +1,14 @@
{% extends "base.html" %}
{% block title %}Szenarien – Finanzberatung{% endblock %}
{% block content %}
{% macro zeitraum(von, bis) -%}
{%- if von and bis -%}{{ von.strftime('%d.%m.%Y') }} – {{ bis.strftime('%d.%m.%Y') }}
{%- elif von -%}ab {{ von.strftime('%d.%m.%Y') }}
{%- elif bis -%}bis {{ bis.strftime('%d.%m.%Y') }}
{%- else -%}– {%- endif -%}
{%- endmacro %}
{% set TIP = 'Für diese Eintragsart nicht relevant' %}
< h1 > Szenarien< / h1 >
< section class = "planning-section" >
@@ -45,7 +53,7 @@
< h3 > Einträge< / h3 >
< table >
< thead >
< tr > < th > Was< / th > < th > Art< / th > < th > Wert/Betrag< / th > < th > Dat um< / th > < th > < / th > < / tr >
< tr > < th > Was< / th > < th > Art< / th > < th > Wert/Betrag< / th > < th > Zeitra um< / th > < th > < / th > < / tr >
< / thead >
< tbody >
{% for m in row.modifiers %}
@@ -55,8 +63,8 @@
{% else %}Posten: {{ recurring_names.get(m.target_id, m.target_id) }}{% endif %}
< / td >
< td > {{ m.kind|de_label }}< / td >
< td > {% if m.kind in ('percent', 'absolute') %}{{ m.value|eur }}{% else %}– {% endif %}< / td >
< td > {% if m.kind == 'ende' %}{{ m.end_date.strftime('%d.%m.%Y') if m.end_date else '– ' }}{% else %}– {% endif %}< / td >
< td > {% if m.kind in ('percent', 'absolute', 'set' ) %}{{ m.value|eur }}{% else %}– {% endif %}< / td >
< td > {% if m.kind == 'ende' %}{{ m.end_date.strftime('%d.%m.%Y') if m.end_date else '– ' }}{% else %}{{ zeitraum(m.start_date, m.end_date) }} {% endif %}< / td >
< td >
< button type = "button" onclick = "toggleEdit('mod', {{ m.id }}, true)" > Bearbeiten< / button >
< form class = "inline-form" hx-delete = "/api/scenarios/{{ sc.id }}/modifiers/{{ m.id }}" hx-swap = "none"
@@ -67,10 +75,15 @@
< / tr >
< tr id = "mod-edit-{{ m.id }}" hidden data-sort-follow >
< td colspan = "5" >
< form hx-ext = "json-form" hx-patch = "/api/scenarios/{{ sc.id }}/modifiers/{{ m.id }}" hx-swap = "none"
< form class = "aenderung-form" hx-ext = "json-form" hx-patch = "/api/scenarios/{{ sc.id }}/modifiers/{{ m.id }}" hx-swap = "none"
hx-on::after-request = "if(event.detail.successful){window.location.reload()}" >
< label > Art
< select name = "kind" data-type = "str" onchange = "FBSzenario.aenderungArt(this)" >
{% for k in modifier_kinds %}< option value = "{{ k }}" { % if k = = m . kind % } selected { % endif % } > {{ k|de_label }}< / option > {% endfor %}
< / select >
< / label >
< label > Ziel-Typ
< select name = "target_type" data-type = "str" onchange = "onModTargetTypeChange (this)" >
< select name = "target_type" data-type = "str" onchange = "FBSzenario.zielTyp (this)" >
< option value = "category" { % if m . target_type = = ' category ' % } selected { % endif % } > Kategorie< / option >
< option value = "recurring" { % if m . target_type = = ' recurring ' % } selected { % endif % } > Wiederkehrender Posten< / option >
< / select >
@@ -89,22 +102,59 @@
< / select >
< / label >
< / span >
< label > Art
< select name = "kind " data-type = "str " onchange = "onEntryArtChange(this)" >
{% for k in modifier_kinds %} < option value = "{{ k }}" { % if k = = m . kind % } selected { % endif % } > {{ k|de_label }} < / option > {% endfor %}
< / select >
< / label >
< label > Wert < input type = "text" name = "value" data-type = "amount" value = "{{ m.value|eur }}" { % if m . kind not in ( ' percent ' , ' absolute ' ) % } disabled { % endif % } > < / label >
< label > Endet am < input type = "date" name = "end_date" value = "{{ m.end_date.isoformat() if m.end_date else '' }}" { % if m . kind ! = ' ende ' % } disabled { % endif % } > < / label >
< label > < span class = "value-label" > Wert < / span > < input type = "text" name = "value" data-type = "amount" value = "{{ m.value|eur }}" { % if m . kind not in ( ' percent ' , ' absolute ' , ' set ' ) % } disabled title = "{{ TIP }}" { % endif % } > < / label >
< label > Gültig ab < input type = "date " name = "start_date " value = "{{ m.start_date.isoformat() if m.start_date else '' }}" { % if m . kind = = ' ende ' % } disabled title = "{{ TIP }}" { % endif % } > < / label >
< label > < span class = "bis-label" > {{ 'Endet am' if m.kind == 'ende' else 'Gültig bis' }} < / span > < input type = "date" name = "end_date" value = "{{ m.end_date.isoformat() if m.end_date else '' }}" > < / label >
< button type = "submit" > Speichern< / button >
< button type = "button" onclick = "toggleEdit('mod', {{ m.id }}, false)" > Abbrechen< / button >
< / form >
< / td >
< / tr >
{% endfor %}
{% for ri in row.recurring_items %}
< tr id = "sri-row-{{ ri.id }}" >
< td > {{ ri.name }}{% if ri.category_id %} < span class = "muted" > ({{ category_names.get(ri.category_id, '') }})< / span > {% endif %}< / td >
< td > Wiederkehrend ({{ ri.rhythm|de_label }}, am {{ ri.due_day }}.)< / td >
< td class = "{{ 'neg' if ri.amount < 0 else '' }}" > {{ ri.amount|eur }} €< / td >
< td > {{ zeitraum(ri.start_date, ri.end_date) }}< / td >
< td >
< button type = "button" onclick = "toggleEdit('sri', {{ ri.id }}, true)" > Bearbeiten< / button >
< form class = "inline-form" hx-delete = "/api/scenarios/{{ sc.id }}/recurring/{{ ri.id }}" hx-swap = "none"
hx-confirm = "„{{ ri.name }}“ wirklich löschen?"
hx-on::after-request = "if(event.detail.successful){window.location.reload()}" >
< button type = "submit" > Löschen< / button >
< / form >
< / td >
< / tr >
< tr id = "sri-edit-{{ ri.id }}" hidden data-sort-follow >
< td colspan = "5" >
< form hx-ext = "json-form" hx-patch = "/api/scenarios/{{ sc.id }}/recurring/{{ ri.id }}" hx-swap = "none"
hx-on::after-request = "if(event.detail.successful){window.location.reload()}" >
< label > Name < input type = "text" name = "name" value = "{{ ri.name }}" required > < / label >
< label > Betrag < input type = "text" name = "amount" data-type = "amount" value = "{{ ri.amount|eur }}" required > < / label >
< label > Kategorie
< select name = "category_id" data-type = "int" >
< option value = "" > (keine)< / option >
{% for c in categories %}< option value = "{{ c.id }}" { % if c . id = = ri . category_id % } selected { % endif % } > {{ c.name }}< / option > {% endfor %}
< / select >
< / label >
< label > Rhythmus
< select name = "rhythm" data-type = "str" >
{% for rh in rhythms %}< option value = "{{ rh }}" { % if rh = = ri . rhythm % } selected { % endif % } > {{ rh|de_label }}< / option > {% endfor %}
< / select >
< / label >
< label > Fälligkeitstag < input type = "number" name = "due_day" data-type = "int" min = "1" max = "31" value = "{{ ri.due_day }}" required > < / label >
< label > Start < input type = "date" name = "start_date" value = "{{ ri.start_date.isoformat() if ri.start_date else '' }}" > < / label >
< label > Ende < input type = "date" name = "end_date" value = "{{ ri.end_date.isoformat() if ri.end_date else '' }}" > < / label >
< button type = "submit" > Speichern< / button >
< button type = "button" onclick = "toggleEdit('sri', {{ ri.id }}, false)" > Abbrechen< / button >
< / form >
< / td >
< / tr >
{% endfor %}
{% for sp in row.planned_items %}
< tr id = "spi-row-{{ sp.id }}" >
< td > {{ sp.name }}< / td >
< td > {{ sp.name }}{% if sp.category_id %} < span class = "muted" > ({{ category_names.get(sp.category_id, '') }}) < / span > {% endif %} < / td >
< td > Einmalzahlung< / td >
< td class = "{{ 'neg' if sp.amount < 0 else '' }}" > {{ sp.amount|eur }} €< / td >
< td > {{ sp.due.strftime('%d.%m.%Y') }}< / td >
@@ -123,6 +173,12 @@
hx-on::after-request = "if(event.detail.successful){window.location.reload()}" >
< label > Name < input type = "text" name = "name" value = "{{ sp.name }}" required > < / label >
< label > Betrag < input type = "text" name = "amount" data-type = "amount" value = "{{ sp.amount|eur }}" required > < / label >
< label > Kategorie
< select name = "category_id" data-type = "int" >
< option value = "" > (keine)< / option >
{% for c in categories %}< option value = "{{ c.id }}" { % if c . id = = sp . category_id % } selected { % endif % } > {{ c.name }}< / option > {% endfor %}
< / select >
< / label >
< label > Fällig am < input type = "date" name = "due" value = "{{ sp.due.isoformat() }}" required > < / label >
< button type = "submit" > Speichern< / button >
< button type = "button" onclick = "toggleEdit('spi', {{ sp.id }}, false)" > Abbrechen< / button >
@@ -130,28 +186,25 @@
< / td >
< / tr >
{% endfor %}
{% if not row.modifiers and not row.planned_items %}
{% if not row.modifiers and not row.planned_items and not row.recurring_items %}
< tr > < td colspan = "5" > Keine Einträge.< / td > < / tr >
{% endif %}
< / tbody >
< / table >
< h4 > Neuer Eintrag < / h4 >
< form class = "entry-form" hx-ext = "json-form" hx-post = "/api/scenarios/{{ sc.id }}/modifiers" hx-swap = "none"
data-modifiers-url = "/api/scenarios/{{ sc.id }}/modifiers"
data-planned-url = "/api/scenarios/{{ sc.id }}/planned"
< h4 > Neue Änderung an Bestehendem < / h4 >
< form class = "entry-form aenderung-form " hx-ext = "json-form" hx-post = "/api/scenarios/{{ sc.id }}/modifiers" hx-swap = "none"
hx-on::after-request = "if(event.detail.successful){window.location.reload()}" >
< div class = "entry-art" >
< label > Eintragsa rt
< select name = "kind" data-type = "str" onchange = "onEntryArtChange (this)" >
< label > A rt
< select name = "kind" data-type = "str" onchange = "FBSzenario.aenderungArt (this)" >
{% for k in modifier_kinds %}< option value = "{{ k }}" > {{ k|de_label }}< / option > {% endfor %}
< option value = "einmal" > Einmalzahlung< / option >
< / select >
< / label >
< span class = "muted" > — die zur Art passenden Felder werden aktiv, gesperrte Felder sind ausgegraut.< / span >
< / div >
< label > Ziel-Typ
< select name = "target_type" data-type = "str" onchange = "onModTargetTypeChange (this)" >
< select name = "target_type" data-type = "str" onchange = "FBSzenario.zielTyp (this)" >
< option value = "category" > Kategorie< / option >
< option value = "recurring" > Wiederkehrender Posten< / option >
< / select >
@@ -165,16 +218,48 @@
< / span >
< span class = "mod-target-recurring" style = "display:none" >
< label > Posten
< select name = "target_id" data-type = "int" disabled title = "Für diese Eintragsart nicht relevant " >
< select name = "target_id" data-type = "int" disabled title = "{{ TIP }} " >
{% for r in recurring %}< option value = "{{ r.id }}" > {{ r.name }}< / option > {% endfor %}
< / select >
< / label >
< / span >
< label > < span class = "value-label" > Prozentsatz (%)< / span > < input type = "text" name = "value" data-type = "amount" value = "0" > < / label >
< label > Endet am < input type = "date" name = "end_date" disabled title = "Für diese Eintragsart nicht relevant "> < / label >
< label > Name < input type = "text" name = "nam e" disabled title = "Für diese Eintragsart nicht relevant " > < / label >
< label > Betrag < input type = "text" name = "amount" data-type = "amount" placeholder = "5000,00" disabled title = "Für diese Eintragsart nicht relevant" > < / label >
< label > Fällig am < input type = "date" name = "due" disabled title = "Für diese Eintragsart nicht relevant" > < / label >
< label > Gültig ab < input type = "date" name = "start_date " > < / label >
< label > < span class = "bis-label" > Gültig bis < / span > < input type = "dat e" name = "end_date " > < / label >
< button type = "submit" > Hinzufügen < / button >
< / form >
< h4 > Neue Zahlung nur in diesem Szenario< / h4 >
< form class = "entry-form zahlung-form" hx-ext = "json-form" hx-post = "/api/scenarios/{{ sc.id }}/planned" hx-swap = "none"
data-planned-url = "/api/scenarios/{{ sc.id }}/planned"
data-recurring-url = "/api/scenarios/{{ sc.id }}/recurring"
hx-on::after-request = "if(event.detail.successful){window.location.reload()}" >
< div class = "entry-art" >
< label > Art
< select class = "zahlung-art" onchange = "FBSzenario.zahlungArt(this)" >
< option value = "einmal" > Einmalzahlung< / option >
< option value = "wiederkehrend" > Wiederkehrend< / option >
< / select >
< / label >
< span class = "muted" > — zählt nur in diesem Szenario, auch ohne „Wiederkehrende Posten/Einmalposten einschließen“.< / span >
< / div >
< label > Name < input type = "text" name = "name" required > < / label >
< label > Betrag < input type = "text" name = "amount" data-type = "amount" placeholder = "-250,00" required > < / label >
< label > Kategorie
< select name = "category_id" data-type = "int" >
< option value = "" > (keine)< / option >
{% for c in categories %}< option value = "{{ c.id }}" > {{ c.name }}< / option > {% endfor %}
< / select >
< / label >
< label > Fällig am < input type = "date" name = "due" required > < / label >
< label > Rhythmus
< select name = "rhythm" data-type = "str" disabled title = "{{ TIP }}" >
{% for rh in rhythms %}< option value = "{{ rh }}" > {{ rh|de_label }}< / option > {% endfor %}
< / select >
< / label >
< label > Fälligkeitstag < input type = "number" name = "due_day" data-type = "int" min = "1" max = "31" required disabled title = "{{ TIP }}" > < / label >
< label > Start < input type = "date" name = "start_date" disabled title = "{{ TIP }}" > < / label >
< label > Ende < input type = "date" name = "end_date" disabled title = "{{ TIP }}" > < / label >
< button type = "submit" > Hinzufügen< / button >
< / form >
@@ -215,75 +300,6 @@
< / section >
< script >
function onModTargetTypeChange ( select ) {
var form = select . closest ( 'form' ) ;
var catSpan = form . querySelector ( '.mod-target-category' ) ;
var recSpan = form . querySelector ( '.mod-target-recurring' ) ;
var catSelect = catSpan . querySelector ( 'select' ) ;
var recSelect = recSpan . querySelector ( 'select' ) ;
var isCategory = select . value === 'category' ;
catSpan . style . display = isCategory ? '' : 'none' ;
recSpan . style . display = isCategory ? 'none' : '' ;
catSelect . disabled = ! isCategory ;
if ( catSelect . disabled ) { catSelect . title = 'Für diese Eintragsart nicht relevant' ; }
else { catSelect . removeAttribute ( 'title' ) ; }
recSelect . disabled = isCategory ;
if ( recSelect . disabled ) { recSelect . title = 'Für diese Eintragsart nicht relevant' ; }
else { recSelect . removeAttribute ( 'title' ) ; }
}
function onEntryArtChange ( select ) {
// Umschaltmatrix "Neuer Eintrag"/Modifikator-Edit: Felder nur per
// disabled schalten (UX-Regel). Fehlt ein Feld (Edit-Formulare haben
// kein name/amount/due), wird es uebersprungen.
var form = select . closest ( 'form' ) ;
var art = select . value ;
var isMod = art !== 'einmal' ;
function setDisabled ( name , disabled ) {
var el = form . querySelector ( '[name="' + name + '"]' ) ;
if ( el ) {
el . disabled = disabled ;
if ( disabled ) { el . title = 'Für diese Eintragsart nicht relevant' ; }
else { el . removeAttribute ( 'title' ) ; }
}
}
setDisabled ( 'value' , ! ( art === 'percent' || art === 'absolute' ) ) ;
var valueLabel = form . querySelector ( '.value-label' ) ;
if ( valueLabel ) {
valueLabel . textContent = art === 'percent' ? 'Prozentsatz (%)'
: art === 'absolute' ? 'Kürzung (€)' : 'Wert' ;
}
setDisabled ( 'end_date' , art !== 'ende' ) ;
setDisabled ( 'name' , isMod ) ;
setDisabled ( 'amount' , isMod ) ;
setDisabled ( 'due' , isMod ) ;
var typeSelect = form . querySelector ( '[name="target_type"]' ) ;
if ( typeSelect ) {
typeSelect . disabled = ! isMod ;
if ( ! isMod ) { typeSelect . title = 'Für diese Eintragsart nicht relevant' ; }
else { typeSelect . removeAttribute ( 'title' ) ; }
var isCategory = typeSelect . value === 'category' ;
var catSelect = form . querySelector ( '.mod-target-category select' ) ;
var recSelect = form . querySelector ( '.mod-target-recurring select' ) ;
if ( catSelect ) {
catSelect . disabled = ! isMod || ! isCategory ;
if ( catSelect . disabled ) { catSelect . title = 'Für diese Eintragsart nicht relevant' ; }
else { catSelect . removeAttribute ( 'title' ) ; }
}
if ( recSelect ) {
recSelect . disabled = ! isMod || isCategory ;
if ( recSelect . disabled ) { recSelect . title = 'Für diese Eintragsart nicht relevant' ; }
else { recSelect . removeAttribute ( 'title' ) ; }
}
}
// POST-Ziel nur beim Anlege-Formular wechseln (Edit-Formulare patchen fix).
if ( form . hasAttribute ( 'data-planned-url' ) ) {
form . setAttribute ( 'hx-post' , isMod ? form . getAttribute ( 'data-modifiers-url' )
: form . getAttribute ( 'data-planned-url' ) ) ;
htmx . process ( form ) ;
}
}
function toggleScenarioLoan ( scenarioId , loanId , checked , checkbox ) {
fetch ( '/api/scenarios/' + scenarioId + '/loans/' + loanId , { method : checked ? 'POST' : 'DELETE' } )
. then ( function ( resp ) {