From d91f45f3856c8da98054b80277577824b59a78f5 Mon Sep 17 00:00:00 2001 From: mboehmlaender Date: Fri, 21 Aug 2026 07:04:26 +0200 Subject: [PATCH] V3.36 --- README.md | 5 +++++ public/sw.js | 2 +- src/main.jsx | 59 +++++++++++++++++++++++++++++++++++++++----------- src/styles.css | 19 ++++++++++++++++ 4 files changed, 71 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 901a2c5..013b015 100644 --- a/README.md +++ b/README.md @@ -169,3 +169,8 @@ Die Statistik erklärt jetzt sämtliche fachlichen Kennzahlen und Zwischenwerte ### v3.35 – geplante Betragsänderungen für Transfers Transfers unterstützen jetzt dieselben zukünftigen Betragsänderungen wie Eingänge und Ausgänge. Der Basisbetrag gilt bis zur ersten Änderung; jede Änderung wird ab ihrem Wirksamkeitsdatum verwendet. Die geänderten Transferbeträge fließen korrekt in Monats-/Jahresplanung, Kontoliquidität, Statistik, Transferanalyse, Gemeinschaftsfinanzierung und PDF/XLSX-Kontoauszüge ein. In der Bewegungsansicht werden geplante Änderungen als Badge und im Detail-Akkordeon angezeigt. Bestehende Transfers werden automatisch mit einem leeren `amountChanges`-Array normalisiert; das JSON-Datenmodell bleibt bei `version: 11`. + + +### v3.36 – Betragsänderungen: aktuell, Historie und Planung + +Betragsänderungen werden nach ihrem Stichtag nicht mehr als geplant dargestellt. Der aktuell wirksame Betrag wird in Bewegungen als normaler Betrag angezeigt. Frühere Beträge und bereits wirksame Änderungen bleiben als Historie sichtbar und werden für vergangene Monate/Jahre weiterhin verwendet. Nur zukünftige Stichtage erhalten das Badge `geplant`. Das gilt einheitlich für Eingänge, Ausgänge und Transfers. diff --git a/public/sw.js b/public/sw.js index 18c097b..61879d8 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -const SHELL_CACHE='fixfin-shell-v3.35'; +const SHELL_CACHE='fixfin-shell-v3.36'; const DATA_CACHE='fixfin-data-v1'; const APP_SHELL=['/','/manifest.webmanifest','/icons/icon-192.png','/icons/icon-512.png','/icons/apple-touch-icon.png']; diff --git a/src/main.jsx b/src/main.jsx index 69f2954..2018f90 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -718,12 +718,44 @@ function movementAmountHint(item){ if((item?.interval||'monthly')==='once')return item.oneTimeDate?`einmalig am ${formatDate(item.oneTimeDate)}`:'einmalig'; return `${formatMoney(monthlyEquivalent(item))} / Monat`; } + +function previousMonthEndDate(value){ + const mk=dateMonth(value);if(!mk)return null; + const [year,month]=mk.split('-').map(Number); + const d=new Date(year,month-1,0,12,0,0); + const y=d.getFullYear(),m=String(d.getMonth()+1).padStart(2,'0'),day=String(d.getDate()).padStart(2,'0'); + return `${y}-${m}-${day}`; +} +function amountChangeState(item,asOf=new Date()){ + const currentKey=monthKey(asOf.getFullYear(),asOf.getMonth()); + const changes=[...(item?.amountChanges||[])].filter(c=>dateMonth(c.effectiveFrom)).sort((a,b)=>a.effectiveFrom.localeCompare(b.effectiveFrom)); + const applied=changes.filter(c=>dateMonth(c.effectiveFrom)<=currentKey); + const future=changes.filter(c=>dateMonth(c.effectiveFrom)>currentKey); + const activeChange=applied.length?applied[applied.length-1]:null; + const history=[]; + let previousAmount=Number(item?.amount||0);let previousFrom=null; + for(const change of applied){ + history.push({amount:previousAmount,from:previousFrom,until:previousMonthEndDate(change.effectiveFrom)}); + previousAmount=Number(change.amount||0);previousFrom=change.effectiveFrom; + } + return {changes,applied,future,activeChange,currentAmount:activeChange?Number(activeChange.amount||0):Number(item?.amount||0),currentSince:activeChange?.effectiveFrom||null,history}; +} +function AmountChangeTimeline({item}){ + const state=amountChangeState(item); + if(!state.changes.length)return null; + return
+ {state.history.length>0&&
Betragshistorie{state.history.map((row,index)=>
{row.from?`${formatDate(row.from)} bis ${formatDate(row.until)}`:`bis ${formatDate(row.until)}`}{formatMoney(row.amount)}
)}
} + {state.currentSince&&
Aktuell gültig
seit {formatDate(state.currentSince)}{formatMoney(state.currentAmount)}
} + {state.future.length>0&&
Geplante Betragsänderungen{state.future.map(c=>
ab {formatDate(c.effectiveFrom)}{formatMoney(c.amount)}
)}
} +
; +} function buildWarnings(data, includeTransfers = true){ const now=new Date();const year=now.getFullYear();const currentMonth=now.getMonth();const horizon=new Date(now);horizon.setDate(horizon.getDate()+90);const warnings=[]; for(const account of data.accounts){const f=annualForecast(data,account.id,year,'all',includeTransfers);const negatives=f.rows.slice(currentMonth).filter(r=>r.balance<0);if(negatives.length){const worst=negatives.reduce((a,b)=>b.balanceisPeriodic(x)&&occursInMonth(x,m,year));if(due.length>=3){const total=due.reduce((sum,x)=>sum+effectiveAmount(x,year,m),0);warnings.push({kind:'warning',title:`${due.length} periodische Ausgaben im ${months[m]}`,text:`Zusammen ${formatMoney(total)}. Dieser Monat bündelt mehrere größere Fälligkeiten.`,sort:1});}} - for(const [type,items] of [['Eingang',data.incomes||[]],['Ausgang',data.expenses||[]]])for(const item of items){for(const change of item.amountChanges||[]){const d=new Date(`${change.effectiveFrom}T12:00:00`);if(d>=now&&d<=horizon)warnings.push({kind:'info',title:`${item.name}: Betrag ändert sich`,text:`${type} auf ${accountName(data,item.accountId)} ab ${formatDate(change.effectiveFrom)}: ${formatMoney(change.amount)}.`,sort:2,date:d});}if(item.endDate){const d=new Date(`${item.endDate}T12:00:00`);if(d>=now&&d<=horizon)warnings.push({kind:'info',title:`${item.name} läuft aus`,text:`${type} auf ${accountName(data,item.accountId)} endet am ${formatDate(item.endDate)}.`,sort:3,date:d});}} - for(const item of data.transfers||[]){for(const change of item.amountChanges||[]){const d=new Date(`${change.effectiveFrom}T12:00:00`);if(d>=now&&d<=horizon)warnings.push({kind:'info',title:`${item.name}: Transferbetrag ändert sich`,text:`${accountName(data,item.fromAccountId)} → ${accountName(data,item.toAccountId)} ab ${formatDate(change.effectiveFrom)}: ${formatMoney(change.amount)}.`,sort:2,date:d});}} + const currentKey=monthKey(year,currentMonth);const horizonKey=monthKey(horizon.getFullYear(),horizon.getMonth()); + for(const [type,items] of [['Eingang',data.incomes||[]],['Ausgang',data.expenses||[]]])for(const item of items){for(const change of item.amountChanges||[]){const mk=dateMonth(change.effectiveFrom);const d=new Date(`${change.effectiveFrom}T12:00:00`);if(mk&&mk>currentKey&&mk<=horizonKey)warnings.push({kind:'info',title:`${item.name}: Betrag ändert sich`,text:`${type} auf ${accountName(data,item.accountId)} ab ${formatDate(change.effectiveFrom)}: ${formatMoney(change.amount)}.`,sort:2,date:d});}if(item.endDate){const d=new Date(`${item.endDate}T12:00:00`);if(d>=now&&d<=horizon)warnings.push({kind:'info',title:`${item.name} läuft aus`,text:`${type} auf ${accountName(data,item.accountId)} endet am ${formatDate(item.endDate)}.`,sort:3,date:d});}} + for(const item of data.transfers||[]){for(const change of item.amountChanges||[]){const mk=dateMonth(change.effectiveFrom);const d=new Date(`${change.effectiveFrom}T12:00:00`);if(mk&&mk>currentKey&&mk<=horizonKey)warnings.push({kind:'info',title:`${item.name}: Transferbetrag ändert sich`,text:`${accountName(data,item.fromAccountId)} → ${accountName(data,item.toAccountId)} ab ${formatDate(change.effectiveFrom)}: ${formatMoney(change.amount)}.`,sort:2,date:d});}} return warnings.sort((a,b)=>a.sort-b.sort||(a.date?.getTime()||0)-(b.date?.getTime()||0)).slice(0,10); } @@ -990,11 +1022,10 @@ function Entries({type,items,accounts,categories,persons,onEdit,onDuplicate,onDe const personLabelLocal=item=>{const ids=personIdsOf(item);return ids.length?ids.map(id=>persons.find(p=>p.id===id)?.name||'Unbekannt').join(', '):'Ohne Person';}; const isIncome=type==='incomes'; const dueText=movementDueText; - const currentAmount=x=>effectiveAmount(x); - return

{isIncome?'Fixe Eingänge':'Fixe Ausgänge'}

Nur Basisinfos bleiben sichtbar. Details und Aktionen öffnen sich je Bewegung per Akkordeon.

{items.length===0?:
{items.map(x=>
-
{x.name}
{categoryNameLocal(x.categoryId)}{personLabelLocal(x)}{accountNameLocal(x.accountId)}{intervals[x.interval||'monthly'].label}{x.interval!=='monthly'&&{dueText(x)}}{!isIncome&&x.pushoverReminder&&Reminder}{(x.amountChanges||[]).length>0&&{x.amountChanges.length} geplant}{x.endDate&&bis {formatDate(x.endDate)}}
{isIncome?'+':'−'}{formatMoney(currentAmount(x))}{movementAmountHint(x)}
-
Personen{personLabelLocal(x)}
Konto{accountNameLocal(x.accountId)}
Kategorie{categoryNameLocal(x.categoryId)}
Intervall{intervals[x.interval||'monthly'].label}
Fälligkeit{dueText(x)}
{x.interval==='once'?'Einmalig':'Ø / Monat'}{x.interval==='once'?formatMoney(effectiveAmount(x)):formatMoney(monthlyEquivalent(x))}
{x.endDate&&
Enddatum{formatDate(x.endDate)}
}{!isIncome&&x.pushoverReminder&&
Erinnerung{x.interval==='once'?'Am 1. des Zielmonats':'Am 1. des Fälligkeitsmonats'}
}
{(x.amountChanges||[]).length>0&&
Geplante Betragsänderungen{x.amountChanges.map(c=>
ab {formatDate(c.effectiveFrom)}{formatMoney(c.amount)}
)}
}
-
)}
}
; + return

{isIncome?'Fixe Eingänge':'Fixe Ausgänge'}

Der sichtbare Betrag ist immer der aktuell gültige Betrag. Vergangene Änderungen bleiben als Historie erhalten; nur zukünftige Stichtage werden als geplant markiert.

{items.length===0?:
{items.map(x=>{const changes=amountChangeState(x);return
+
{x.name}
{categoryNameLocal(x.categoryId)}{personLabelLocal(x)}{accountNameLocal(x.accountId)}{intervals[x.interval||'monthly'].label}{x.interval!=='monthly'&&{dueText(x)}}{!isIncome&&x.pushoverReminder&&Reminder}{changes.future.length>0&&{changes.future.length} geplant}{changes.applied.length>0&&Historie}{x.endDate&&bis {formatDate(x.endDate)}}
{isIncome?'+':'−'}{formatMoney(changes.currentAmount)}{x.interval==='once'?movementAmountHint(x):`${formatMoney(changes.currentAmount/(intervals[x.interval]?.divisor||1))} / Monat`}
+
Personen{personLabelLocal(x)}
Konto{accountNameLocal(x.accountId)}
Kategorie{categoryNameLocal(x.categoryId)}
Intervall{intervals[x.interval||'monthly'].label}
Fälligkeit{dueText(x)}
{x.interval==='once'?'Einmalig':'Aktuell Ø / Monat'}{x.interval==='once'?formatMoney(changes.currentAmount):formatMoney(changes.currentAmount/(intervals[x.interval]?.divisor||1))}
{changes.currentSince&&
Aktueller Betrag seit{formatDate(changes.currentSince)}
}{x.endDate&&
Enddatum{formatDate(x.endDate)}
}{!isIncome&&x.pushoverReminder&&
Erinnerung{x.interval==='once'?'Am 1. des Zielmonats':'Am 1. des Fälligkeitsmonats'}
}
+
})}
}
; } function Transfers({data,onEdit,onDuplicate,onDelete}){ @@ -1002,9 +1033,9 @@ function Transfers({data,onEdit,onDuplicate,onDelete}){ const categoryNameLocal=id=>data.categories.find(c=>c.id===id)?.name||'Ohne Kategorie'; const personLabelLocal=item=>transferPeopleLabel(data,item); const dueText=movementDueText; - return

Fixe Transfers

Quelle, Ziel, Betrag und Einordnung bleiben direkt sichtbar. Weitere Details und Aktionen liegen im Akkordeon.

{data.transfers.length===0?:
{data.transfers.map(x=>{const cls=transferClassification(data,x);return
-
{x.name}{accountNameLocal(x.fromAccountId)} {accountNameLocal(x.toAccountId)}
{cls.label}{personLabelLocal(x)}{categoryNameLocal(x.categoryId)}{intervals[x.interval||'monthly'].label}{x.interval!=='monthly'&&{dueText(x)}}{(x.amountChanges||[]).length>0&&{x.amountChanges.length} geplant}{x.endDate&&bis {formatDate(x.endDate)}}
{formatMoney(effectiveAmount(x))}{movementAmountHint(x)}
-
Von{accountNameLocal(x.fromAccountId)}
Nach{accountNameLocal(x.toAccountId)}
Personen{personLabelLocal(x)}
Einordnung{cls.description}
Kategorie{categoryNameLocal(x.categoryId)}
Intervall{intervals[x.interval||'monthly'].label}
Fälligkeit{dueText(x)}
{x.interval==='once'?'Einmalig':'Ø / Monat'}{x.interval==='once'?formatMoney(effectiveAmount(x)):formatMoney(monthlyEquivalent(x))}
{x.endDate&&
Enddatum{formatDate(x.endDate)}
}
{(x.amountChanges||[]).length>0&&
Geplante Betragsänderungen{x.amountChanges.map(c=>
ab {formatDate(c.effectiveFrom)}{formatMoney(c.amount)}
)}
}
+ return

Fixe Transfers

Der sichtbare Betrag ist immer der aktuell gültige Transferbetrag. Vergangene Stichtage wandern automatisch in die Historie, zukünftige bleiben als Planung sichtbar.

{data.transfers.length===0?:
{data.transfers.map(x=>{const cls=transferClassification(data,x);const changes=amountChangeState(x);return
+
{x.name}{accountNameLocal(x.fromAccountId)} {accountNameLocal(x.toAccountId)}
{cls.label}{personLabelLocal(x)}{categoryNameLocal(x.categoryId)}{intervals[x.interval||'monthly'].label}{x.interval!=='monthly'&&{dueText(x)}}{changes.future.length>0&&{changes.future.length} geplant}{changes.applied.length>0&&Historie}{x.endDate&&bis {formatDate(x.endDate)}}
{formatMoney(changes.currentAmount)}{x.interval==='once'?movementAmountHint(x):`${formatMoney(changes.currentAmount/(intervals[x.interval]?.divisor||1))} / Monat`}
+
Von{accountNameLocal(x.fromAccountId)}
Nach{accountNameLocal(x.toAccountId)}
Personen{personLabelLocal(x)}
Einordnung{cls.description}
Kategorie{categoryNameLocal(x.categoryId)}
Intervall{intervals[x.interval||'monthly'].label}
Fälligkeit{dueText(x)}
{x.interval==='once'?'Einmalig':'Aktuell Ø / Monat'}{x.interval==='once'?formatMoney(changes.currentAmount):formatMoney(changes.currentAmount/(intervals[x.interval]?.divisor||1))}
{changes.currentSince&&
Aktueller Betrag seit{formatDate(changes.currentSince)}
}{x.endDate&&
Enddatum{formatDate(x.endDate)}
}
})}
}
; } @@ -1252,7 +1283,7 @@ function TransferAnalysisPage({data}){

Diese Quote beantwortet: Wie viel des echten Einkommens der ausgewählten Quellkonten wird über die aktuell ausgewählten Transfers weiterverteilt?

Formel: ausgewähltes Transfervolumen ÷ echte Einnahmen der ausgewählten Quellkonten × 100.

Transfers, die auf einem Quellkonto eingehen, zählen nicht als Einnahmen. Die Quote kann über 100 % liegen, wenn zusätzlich vorhandenes Guthaben oder eingehende Transfers weiterverteilt werden.

}>
{sourceIds.length===0||targetIds.length===0?
:stats.rows.length===0?
:<> -

Wie viel kommt von welchem Konto?

Jede Quelle zeigt ihr gesamtes Transfervolumen, die Verteilung auf die ausgewählten Zielkonten und den Anteil dieses Volumens an den echten Einnahmen des Quellkontos.

Transferquote = ausgewählte Transfers dieses Quellkontos ÷ echte Einnahmen dieses Quellkontos × 100.

Nur echte Eingänge zählen als Einnahmen. Eingehende Transfers sind kein Einkommen. Deshalb kann die Quote auch über 100 % liegen.

{stats.sources.map(source=>
{accountName(data,source.accountId)}{source.items.length} {source.items.length===1?'Transfer':'Transfers'} · {percent(source.annual,stats.annual)} des ausgewählten Volumens · {percentValue(source.incomeRatio)} der Einnahmen
{formatMoney(source.annual)} / Jahr{formatMoney(source.monthly)} Ø / Monat
Echte Einnahmen{formatMoney(source.incomeAnnual)} / Jahr
Ausgewählte Transfers{formatMoney(source.annual)} / Jahr
Transferquote{percentValue(source.incomeRatio)}
{source.targets.map(target=>
{accountName(data,target.accountId)}{target.items.length} {target.items.length===1?'Position':'Positionen'}
{formatMoney(target.annual)}{percent(target.annual,source.annual)}
)}
{source.items.sort((a,b)=>b.annual-a.annual).map(item=>
{item.name}{accountName(data,item.fromAccountId)} → {accountName(data,item.toAccountId)}
{formatMoney(item.annual)} / Jahr{percentValue(item.incomeRatio)} der Einnahmen der Quelle
{item.classification.label}
{(item.amountChanges||[]).length?'Basisbetrag je Intervall':'Betrag je Intervall'}{formatMoney(item.amount)}
Intervall{intervalLabel(item)}
Fälligkeit{movementDueText(item)}
Personen{transferPeopleLabel(data,item)}
Kategorie{categoryName(data,item.categoryId)}
Anteil dieser Quelle{percent(item.annual,source.annual)}
Echte Einnahmen Quelle{formatMoney(item.sourceIncomeAnnual)} / Jahr
Transfer / Einnahmen Quelle{percentValue(item.incomeRatio)}
{(item.amountChanges||[]).length>0&&
Geplante Betragsänderungen{item.amountChanges.map(change=>`ab ${formatDate(change.effectiveFrom)}: ${formatMoney(change.amount)}`).join(' · ')}
}
)}
)}
+

Wie viel kommt von welchem Konto?

Jede Quelle zeigt ihr gesamtes Transfervolumen, die Verteilung auf die ausgewählten Zielkonten und den Anteil dieses Volumens an den echten Einnahmen des Quellkontos.

Transferquote = ausgewählte Transfers dieses Quellkontos ÷ echte Einnahmen dieses Quellkontos × 100.

Nur echte Eingänge zählen als Einnahmen. Eingehende Transfers sind kein Einkommen. Deshalb kann die Quote auch über 100 % liegen.

{stats.sources.map(source=>
{accountName(data,source.accountId)}{source.items.length} {source.items.length===1?'Transfer':'Transfers'} · {percent(source.annual,stats.annual)} des ausgewählten Volumens · {percentValue(source.incomeRatio)} der Einnahmen
{formatMoney(source.annual)} / Jahr{formatMoney(source.monthly)} Ø / Monat
Echte Einnahmen{formatMoney(source.incomeAnnual)} / Jahr
Ausgewählte Transfers{formatMoney(source.annual)} / Jahr
Transferquote{percentValue(source.incomeRatio)}
{source.targets.map(target=>
{accountName(data,target.accountId)}{target.items.length} {target.items.length===1?'Position':'Positionen'}
{formatMoney(target.annual)}{percent(target.annual,source.annual)}
)}
{source.items.sort((a,b)=>b.annual-a.annual).map(item=>
{item.name}{accountName(data,item.fromAccountId)} → {accountName(data,item.toAccountId)}
{formatMoney(item.annual)} / Jahr{percentValue(item.incomeRatio)} der Einnahmen der Quelle
{item.classification.label}
Aktuell gültiger Betrag je Intervall{formatMoney(amountChangeState(item).currentAmount)}
Intervall{intervalLabel(item)}
Fälligkeit{movementDueText(item)}
Personen{transferPeopleLabel(data,item)}
Kategorie{categoryName(data,item.categoryId)}
Anteil dieser Quelle{percent(item.annual,source.annual)}
Echte Einnahmen Quelle{formatMoney(item.sourceIncomeAnnual)} / Jahr
Transfer / Einnahmen Quelle{percentValue(item.incomeRatio)}
{(item.amountChanges||[]).length>0&&
}
)}
)}

Verteilung auf die Zielkonten

Wie viel des ausgewählten Transfervolumens insgesamt auf jedem Zielkonto landet. Zusätzlich siehst du, wie groß dieser Transferzufluss im Verhältnis zu den echten Einnahmen des Zielkontos ist.

Diese Zielquote ist eine Ergänzung zur Quellquote: eingehende ausgewählte Transfers ÷ echte Einnahmen des Zielkontos × 100.

Hat das Zielkonto keine echten Einnahmen, wird keine Prozentquote berechnet. Das ist kein Fehler, sondern bedeutet, dass das Konto ausschließlich oder zusätzlich durch Transfers gespeist wird.

{stats.targets.map(target=>
{accountName(data,target.accountId)}{target.items.length} {target.items.length===1?'Transfer':'Transfers'}
{formatMoney(target.annual)} / Jahr
{formatMoney(target.monthly)} Ø / Monat{percent(target.annual,stats.annual)}
Echte Einnahmen: {formatMoney(target.incomeAnnual)}Transferzufluss / Einnahmen: {percentValue(target.incomeRatio)}
)}
@@ -1368,16 +1399,18 @@ function EditorDialog({dialog,data,onClose,onSave}){ try{await onSave(next);}catch(e){setLocalError(e.message);} } const simpleType=type==='accounts'||type==='categories'||type==='persons'; + const editorBaseAmount=Number.isFinite(parseAmount(amount))?parseAmount(amount):Number(item?.amount||0); + const editorChanges=amountChangeState({amount:editorBaseAmount,amountChanges}); return
e.target===e.currentTarget&&onClose()}>
{dialog.duplicate?'DUPLIZIEREN':editing?'BEARBEITEN':'NEU'}

{labels[type]}

{localError&&
{localError}
} {type==='accounts'&&
{accountOwnership==='personal'?:}
} - {!simpleType&&<>{interval==='once'&&}{interval!=='monthly'&&interval!=='once'&&}{interval!=='once'&&}} + {!simpleType&&<>{editorChanges.currentSince&&
Aktuell gültiger Betrag{formatMoney(editorChanges.currentAmount)}seit {formatDate(editorChanges.currentSince)}
}{interval==='once'&&}{interval!=='monthly'&&interval!=='once'&&}{interval!=='once'&&}} {(type==='incomes'||type==='expenses'||type==='transfers')&&<>{categoryId==='__new__'&&}} {(type==='incomes'||type==='expenses'||type==='transfers')&&<> {!showNewPerson?:
} } {(type==='incomes'||type==='expenses')&&} - {(type==='incomes'||type==='expenses'||type==='transfers')&&interval!=='once'&&
Geplante BetragsänderungenEin neuer Betrag gilt ab dem Monat des gewählten Datums. Der Basisbetrag oben bleibt für frühere Monate erhalten.
{amountChanges.length}
{amountChanges.length>0&&
{amountChanges.map(c=>
ab {formatDate(c.effectiveFrom)}{formatMoney(c.amount)}
)}
}
} + {(type==='incomes'||type==='expenses'||type==='transfers')&&interval!=='once'&&
Betragsverlauf & PlanungBereits erreichte Stichtage sind Historie und bestimmen den aktuell gültigen Betrag. Nur spätere Stichtage gelten noch als geplante Änderungen.
{editorChanges.future.length}
{editorChanges.applied.length>0&&
Historische / bereits wirksame Änderungen
{editorChanges.applied.map((c,index)=>
ab {formatDate(c.effectiveFrom)} {index===editorChanges.applied.length-1?'aktuell':'Historie'}{formatMoney(c.amount)}
)}
}{editorChanges.future.length>0&&
Geplante Änderungen
{editorChanges.future.map(c=>
ab {formatDate(c.effectiveFrom)} geplant{formatMoney(c.amount)}
)}
}
} {type==='expenses'&&} {type==='transfers'&&<>}
; } diff --git a/src/styles.css b/src/styles.css index 5658eec..ba4728a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1566,3 +1566,22 @@ html.modal-open, body.modal-open { overflow:hidden !important; overscroll-behavi .stats-context-help { display:grid; gap:7px; padding-top:10px; border-top:1px solid #314157; } .stats-context-help > strong { color:#eef2f8; } + + +/* v3.36: aktive Betragsänderungen werden Historie statt Planung */ +.history-badge { display:inline-flex;align-items:center;margin-left:6px;padding:3px 6px;border-radius:999px;font-size:8px;font-weight:800;white-space:nowrap;vertical-align:middle;background:#16271f;border:1px solid #315c47;color:#9ad7b4; } +.amount-timeline-stack { display:grid;gap:8px;margin-top:10px; } +.movement-change-preview.current { background:#10241b;border-color:#315c47; } +.movement-change-preview.current > span,.movement-change-preview.current > div span { color:#a9e2bf; } +.movement-change-preview.history { background:#121923;border-color:#344154; } +.editor-current-amount { display:grid;grid-template-columns:1fr auto;gap:2px 12px;align-items:center;padding:11px 12px;border:1px solid #315c47;border-radius:10px;background:#10241b;margin-top:-2px; } +.editor-current-amount span { color:#9bb2a4;font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.05em; } +.editor-current-amount strong { font-size:16px;color:#d8f5e3; } +.editor-current-amount small { grid-column:1/-1;color:#92a99a;font-size:9px; } +.amount-change-group { display:grid;gap:7px;margin-top:10px; } +.amount-change-group-title { color:#aebbe0;font-size:9px;font-weight:850;text-transform:uppercase;letter-spacing:.04em; } +.change-status { display:inline-flex;margin-left:6px;padding:2px 5px;border-radius:999px;font-size:7px;text-transform:uppercase;letter-spacing:.03em; } +.change-status.current { background:#173325;color:#a9e2bf;border:1px solid #315c47; } +.change-status.history { background:#17202c;color:#a9b6c8;border:1px solid #344154; } +.change-status.planned { background:#1d2848;color:#b4c0ff;border:1px solid #41558b; } +@media(max-width:700px){.history-badge{font-size:7px;padding:3px 6px;margin-left:0}.editor-current-amount{grid-template-columns:1fr auto}}