V3.26
This commit is contained in:
@@ -113,3 +113,8 @@ Das JSON-Datenmodell verwendet aktuell `version: 11`. Neben Konten und Kategorie
|
||||
### Gemeinsame Kosten relativ zum persönlichen Einkommen (v3.25)
|
||||
|
||||
In der Statistik gibt es zusätzlich eine einkommensbasierte Vergleichssicht für Gemeinschaftsbereiche. Gemeinsame Einnahmen auf Gemeinschaftskonten werden zuerst zur Deckung gemeinsamer Kosten verwendet und niemals als persönliches Einkommen eines Eigentümers gezählt. Der verbleibende Finanzierungsbedarf wird für diese Vergleichsstatistik proportional zu den echten Einnahmen auf persönlich zugeordneten Konten der Gemeinschaftseigentümer verteilt. Angezeigt werden persönliches Einkommen, Einkommensanteil, Soll-Finanzierung nach Einkommen, Belastungsquote, tatsächliche Gemeinschaftsfinanzierung und deren Abweichung. Diese Vergleichssicht verändert keine Bewegungen, Kosten-Personenzuordnungen oder Kontosalden.
|
||||
|
||||
### Beitragsanteile zu gemeinsamen Ausgaben (v3.26)
|
||||
|
||||
Die Gemeinschaftsfinanzierung zeigt je Person zusätzlich Prozentwerte. **Kostenanteil** ist der Anteil der Person an den zugeordneten gemeinsamen Kosten. **Soll-Finanzierung %** zeigt den daraus resultierenden Anteil am verbleibenden Finanzierungsbedarf nach Abzug gemeinsamer Einnahmen. **Ist-Beitrag %** zeigt den Anteil der Person an allen tatsächlichen persönlichen Einzahlungen in diesen Gemeinschaftsbereich. **Anteil am Bedarf** zeigt, wie viel des verbleibenden Finanzierungsbedarfs durch die tatsächliche Einzahlung dieser Person gedeckt wird. In der einkommensbasierten Vergleichssicht werden Soll-% (Einkommensanteil) und Ist-% direkt gegenübergestellt; die Abweichung wird zusätzlich in Prozentpunkten angezeigt.
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const SHELL_CACHE='fixfin-shell-v3.25';
|
||||
const SHELL_CACHE='fixfin-shell-v3.26';
|
||||
const DATA_CACHE='fixfin-data-v1';
|
||||
const APP_SHELL=['/','/manifest.webmanifest','/icons/icon-192.png','/icons/icon-512.png','/icons/apple-touch-icon.png'];
|
||||
|
||||
|
||||
+20
-6
@@ -328,14 +328,23 @@ function sharedFinanceStats(data, personId='all'){
|
||||
for(const id of ids)ensure(id).grossCost+=amount/ids.length;
|
||||
}
|
||||
const totalGross=[...shares.values()].reduce((sum,row)=>sum+row.grossCost,0);
|
||||
for(const row of shares.values())row.targetFunding=totalGross>0?costNeed*(row.grossCost/totalGross):0;
|
||||
for(const row of shares.values()){
|
||||
row.costShare=totalGross>0?(row.grossCost/totalGross)*100:null;
|
||||
row.targetFunding=totalGross>0?costNeed*(row.grossCost/totalGross):0;
|
||||
row.targetFundingShare=costNeed>0?(row.targetFunding/costNeed)*100:null;
|
||||
}
|
||||
for(const item of incomingTransfers){
|
||||
const from=data.accounts.find(a=>a.id===item.fromAccountId);if(from?.ownership!=='personal')continue;
|
||||
const amount=monthlyEquivalent(item);const ids=ownerIdsOf(from);
|
||||
if(!ids.length){ensure('__unassigned__').actualFunding+=amount;continue;}
|
||||
for(const id of ids)ensure(id).actualFunding+=amount/ids.length;
|
||||
}
|
||||
for(const row of shares.values())row.difference=row.actualFunding-row.targetFunding;
|
||||
const totalPersonalFunding=[...shares.values()].reduce((sum,row)=>sum+row.actualFunding,0);
|
||||
for(const row of shares.values()){
|
||||
row.actualFundingShare=totalPersonalFunding>0?(row.actualFunding/totalPersonalFunding)*100:null;
|
||||
row.needCoverageShare=costNeed>0?(row.actualFunding/costNeed)*100:null;
|
||||
row.difference=row.actualFunding-row.targetFunding;
|
||||
}
|
||||
let personRows=[...shares.values()];
|
||||
if(personId!=='all')personRows=personRows.filter(x=>x.id===personId);
|
||||
personRows=personRows.filter(x=>x.grossCost||x.targetFunding||x.actualFunding||group.ownerPersonIds.includes(x.id));
|
||||
@@ -348,11 +357,14 @@ function sharedFinanceStats(data, personId='all'){
|
||||
const incomeBasedFunding=incomeAllocationAvailable?costNeed*incomeShare:null;
|
||||
const burdenRate=personalIncome>0&&incomeBasedFunding!=null?(incomeBasedFunding/personalIncome)*100:null;
|
||||
const actualFunding=ensure(id).actualFunding;
|
||||
return {id,name:personName(data,id),personalIncome,incomeShare,incomeBasedFunding,burdenRate,actualFunding,difference:incomeBasedFunding==null?null:actualFunding-incomeBasedFunding};
|
||||
const actualFundingShare=totalPersonalFunding>0?actualFunding/totalPersonalFunding:null;
|
||||
const needCoverageShare=costNeed>0?actualFunding/costNeed:null;
|
||||
const shareDifferencePoints=incomeShare==null||actualFundingShare==null?null:(actualFundingShare-incomeShare)*100;
|
||||
return {id,name:personName(data,id),personalIncome,incomeShare,incomeBasedFunding,burdenRate,actualFunding,actualFundingShare,needCoverageShare,shareDifferencePoints,difference:incomeBasedFunding==null?null:actualFunding-incomeBasedFunding};
|
||||
});
|
||||
if(personId!=='all')incomeBurdenRows=incomeBurdenRows.filter(x=>x.id===personId);
|
||||
const householdBurdenRate=incomeAllocationAvailable?(costNeed/personalIncomeTotal)*100:null;
|
||||
return {...group,income,expense,sharedIncomeCoverage,sharedIncomeSurplus,costNeed,fundingIn,withdrawalOut,internalTransfers,netAssetChange,personRows,personalIncomeTotal,incomeAllocationAvailable,incomeBurdenRows,householdBurdenRate};
|
||||
return {...group,income,expense,sharedIncomeCoverage,sharedIncomeSurplus,costNeed,fundingIn,withdrawalOut,internalTransfers,netAssetChange,totalPersonalFunding,personRows,personalIncomeTotal,incomeAllocationAvailable,incomeBurdenRows,householdBurdenRate};
|
||||
});
|
||||
return {groups};
|
||||
}
|
||||
@@ -392,7 +404,8 @@ function PersonFlowOverview({stats}){
|
||||
function SharedFundingOverview({data,personId='all',showTransfers=true}){
|
||||
const stats=useMemo(()=>sharedFinanceStats(data,personId),[data,personId]);
|
||||
if(!stats.groups.length)return null;
|
||||
return <section className="panel shared-finance-panel"><div className="section-head"><div><h2>Gemeinschaftsfinanzierung</h2><p>Gemeinsame Einnahmen wie Kindergeld bleiben im gemeinsamen Topf. Sie reduzieren den Finanzierungsbedarf, werden aber keiner Person als persönliche Einnahme zugerechnet.</p></div></div><div className="shared-finance-list">{stats.groups.map(group=><article className="shared-finance-card" key={group.key}><div className="shared-finance-head"><div><strong>{group.accounts.map(a=>a.name).join(' + ')}</strong><span>{group.ownerPersonIds.map(id=>personName(data,id)).join(', ')}</span></div><strong className={group.netAssetChange>=0?'good-text':'bad-text'}>{group.netAssetChange>=0?'+':''}{formatMoney(group.netAssetChange)} / Monat</strong></div><div className="shared-finance-summary"><div><span>Gemeinsame Einnahmen</span><strong className="good-text">+{formatMoney(group.income)}</strong></div><div><span>Gemeinsame Kosten</span><strong className="bad-text">−{formatMoney(group.expense)}</strong></div><div><span>Kosten-Finanzierungsbedarf</span><strong>{formatMoney(group.costNeed)}</strong></div>{showTransfers&&<div><span>Tatsächliche Finanzierung</span><strong>{formatMoney(group.fundingIn)}</strong></div>}</div>{showTransfers&&group.internalTransfers>0&&<div className="shared-internal-note">Interne Umbuchungen innerhalb dieses Gemeinschaftsbereichs: {formatMoney(group.internalTransfers)} / Monat. Sie sind keine Kosten.</div>}<div className="shared-person-table">{group.personRows.map(row=><div className={`shared-person-row ${showTransfers?'':'compact'}`} key={row.id}><span className={`person-badge ${row.id==='__unassigned__'?'muted-person':''}`}>{row.name}</span><div><span>Kostenanteil</span><strong>{formatMoney(row.grossCost)}</strong></div><div><span>Soll-Finanzierung</span><strong>{formatMoney(row.targetFunding)}</strong></div>{showTransfers&&<><div><span>Ist-Finanzierung</span><strong>{formatMoney(row.actualFunding)}</strong></div><div><span>Differenz</span><strong className={row.difference>=0?'good-text':'bad-text'}>{row.difference>=0?'+':''}{formatMoney(row.difference)}</strong></div></>}</div>)}</div>{!showTransfers&&<div className="shared-internal-note">Transferdetails sind ausgeblendet. Soll-Finanzierung und Kostenbedarf bleiben sichtbar, Ist-Finanzierung wird nicht angezeigt.</div>}</article>)}</div></section>;
|
||||
const percent=value=>value==null?'–':`${value.toFixed(1).replace('.',',')} %`;
|
||||
return <section className="panel shared-finance-panel"><div className="section-head"><div><h2>Gemeinschaftsfinanzierung</h2><p>Gemeinsame Einnahmen wie Kindergeld bleiben im gemeinsamen Topf. Sie reduzieren den Finanzierungsbedarf, werden aber keiner Person als persönliche Einnahme zugerechnet.</p></div></div><div className="shared-finance-list">{stats.groups.map(group=><article className="shared-finance-card" key={group.key}><div className="shared-finance-head"><div><strong>{group.accounts.map(a=>a.name).join(' + ')}</strong><span>{group.ownerPersonIds.map(id=>personName(data,id)).join(', ')}</span></div><strong className={group.netAssetChange>=0?'good-text':'bad-text'}>{group.netAssetChange>=0?'+':''}{formatMoney(group.netAssetChange)} / Monat</strong></div><div className="shared-finance-summary"><div><span>Gemeinsame Einnahmen</span><strong className="good-text">+{formatMoney(group.income)}</strong></div><div><span>Gemeinsame Kosten</span><strong className="bad-text">−{formatMoney(group.expense)}</strong></div><div><span>Kosten-Finanzierungsbedarf</span><strong>{formatMoney(group.costNeed)}</strong></div>{showTransfers&&<div><span>Tatsächliche Finanzierung</span><strong>{formatMoney(group.fundingIn)}</strong></div>}</div>{showTransfers&&group.internalTransfers>0&&<div className="shared-internal-note">Interne Umbuchungen innerhalb dieses Gemeinschaftsbereichs: {formatMoney(group.internalTransfers)} / Monat. Sie sind keine Kosten.</div>}<div className="shared-person-table">{group.personRows.map(row=><div className={`shared-person-row ${showTransfers?'':'compact'}`} key={row.id}><span className={`person-badge ${row.id==='__unassigned__'?'muted-person':''}`}>{row.name}</span><div><span>Kostenanteil</span><strong>{formatMoney(row.grossCost)} · {percent(row.costShare)}</strong></div><div><span>Soll-Finanzierung</span><strong>{formatMoney(row.targetFunding)} · {percent(row.targetFundingShare)}</strong></div>{showTransfers&&<><div><span>Ist-Beitrag</span><strong>{formatMoney(row.actualFunding)} · {percent(row.actualFundingShare)}</strong></div><div><span>Anteil am Bedarf</span><strong>{percent(row.needCoverageShare)}</strong></div><div><span>Differenz</span><strong className={row.difference>=0?'good-text':'bad-text'}>{row.difference>=0?'+':''}{formatMoney(row.difference)}</strong></div></>}</div>)}</div>{showTransfers&&<div className="shared-internal-note">Ist-Beitrag % = Anteil dieser Person an allen tatsächlichen persönlichen Einzahlungen in den Gemeinschaftsbereich. Anteil am Bedarf = wie viel des verbleibenden Finanzierungsbedarfs diese Person tatsächlich deckt.</div>}{!showTransfers&&<div className="shared-internal-note">Transferdetails sind ausgeblendet. Kostenanteil und Soll-Finanzierung bleiben sichtbar, tatsächliche Beiträge werden nicht angezeigt.</div>}</article>)}</div></section>;
|
||||
}
|
||||
|
||||
|
||||
@@ -400,7 +413,8 @@ function SharedIncomeBurdenOverview({data,personId='all',showTransfers=true}){
|
||||
const stats=useMemo(()=>sharedFinanceStats(data,personId),[data,personId]);
|
||||
if(!stats.groups.length)return null;
|
||||
const percent=value=>value==null?'–':`${value.toFixed(1).replace('.',',')} %`;
|
||||
return <section className="panel shared-income-burden-panel"><div className="section-head"><div><h2>Gemeinsame Kosten gemessen am persönlichen Einkommen</h2><p>Gemeinsame Einnahmen decken zuerst gemeinsame Kosten. Nur der verbleibende Finanzierungsbedarf wird im Verhältnis der persönlichen Einkommen auf die Eigentümer des Gemeinschaftsbereichs verteilt.</p></div></div><div className="shared-income-burden-list">{stats.groups.map(group=><article className="shared-income-burden-card" key={group.key}><div className="shared-income-burden-head"><div><strong>{group.accounts.map(a=>a.name).join(' + ')}</strong><span>{group.ownerPersonIds.map(id=>personName(data,id)).join(', ')}</span></div><div><span>Belastungsquote Haushalt</span><strong>{percent(group.householdBurdenRate)}</strong></div></div><div className="shared-income-burden-summary"><div><span>Gemeinsame Kosten</span><strong className="bad-text">−{formatMoney(group.expense)}</strong></div><div><span>Deckung gemeinsame Einnahmen</span><strong className="good-text">+{formatMoney(group.sharedIncomeCoverage)}</strong></div><div><span>Verbleibender Bedarf</span><strong>{formatMoney(group.costNeed)}</strong></div><div><span>Persönliche Einkommen gesamt</span><strong>{formatMoney(group.personalIncomeTotal)}</strong></div></div>{group.sharedIncomeSurplus>0&&<div className="shared-income-burden-note good-text">Gemeinsame Einnahmen übersteigen die gemeinsamen Kosten um {formatMoney(group.sharedIncomeSurplus)} / Monat. Es ist keine persönliche Finanzierung nötig.</div>}{!group.incomeAllocationAvailable?<div className="shared-income-burden-note">Keine persönlichen Einnahmen auf persönlich zugeordneten Konten vorhanden. Eine einkommensbasierte Verteilung kann deshalb nicht berechnet werden.</div>:<div className="shared-income-person-list">{group.incomeBurdenRows.map(row=><div className={`shared-income-person-row ${showTransfers?'':'compact'}`} key={row.id}><span className="person-badge">{row.name}</span><div><span>Persönliches Einkommen</span><strong>{formatMoney(row.personalIncome)}</strong></div><div><span>Einkommensanteil</span><strong>{percent(row.incomeShare==null?null:row.incomeShare*100)}</strong></div><div><span>Soll nach Einkommen</span><strong>{row.incomeBasedFunding==null?'–':formatMoney(row.incomeBasedFunding)}</strong></div><div><span>Belastung vom Einkommen</span><strong>{percent(row.burdenRate)}</strong></div>{showTransfers&&<><div><span>Ist-Finanzierung</span><strong>{formatMoney(row.actualFunding)}</strong></div><div><span>Abweichung</span><strong className={row.difference==null?'':row.difference>=0?'good-text':'bad-text'}>{row.difference==null?'–':`${row.difference>=0?'+':''}${formatMoney(row.difference)}`}</strong></div></>}</div>)}</div>}<div className="shared-income-burden-note">Diese Statistik ist eine Vergleichs- und Fairnesssicht. Sie verändert weder die hinterlegte Personenzuordnung der Kosten noch die tatsächlichen Kontosalden.</div></article>)}</div></section>;
|
||||
const percentPoints=value=>value==null?'–':`${value>=0?'+':''}${value.toFixed(1).replace('.',',')} %-Pkt.`;
|
||||
return <section className="panel shared-income-burden-panel"><div className="section-head"><div><h2>Gemeinsame Kosten gemessen am persönlichen Einkommen</h2><p>Gemeinsame Einnahmen decken zuerst gemeinsame Kosten. Nur der verbleibende Finanzierungsbedarf wird im Verhältnis der persönlichen Einkommen auf die Eigentümer des Gemeinschaftsbereichs verteilt.</p></div></div><div className="shared-income-burden-list">{stats.groups.map(group=><article className="shared-income-burden-card" key={group.key}><div className="shared-income-burden-head"><div><strong>{group.accounts.map(a=>a.name).join(' + ')}</strong><span>{group.ownerPersonIds.map(id=>personName(data,id)).join(', ')}</span></div><div><span>Belastungsquote Haushalt</span><strong>{percent(group.householdBurdenRate)}</strong></div></div><div className="shared-income-burden-summary"><div><span>Gemeinsame Kosten</span><strong className="bad-text">−{formatMoney(group.expense)}</strong></div><div><span>Deckung gemeinsame Einnahmen</span><strong className="good-text">+{formatMoney(group.sharedIncomeCoverage)}</strong></div><div><span>Verbleibender Bedarf</span><strong>{formatMoney(group.costNeed)}</strong></div><div><span>Persönliche Einkommen gesamt</span><strong>{formatMoney(group.personalIncomeTotal)}</strong></div></div>{group.sharedIncomeSurplus>0&&<div className="shared-income-burden-note good-text">Gemeinsame Einnahmen übersteigen die gemeinsamen Kosten um {formatMoney(group.sharedIncomeSurplus)} / Monat. Es ist keine persönliche Finanzierung nötig.</div>}{!group.incomeAllocationAvailable?<div className="shared-income-burden-note">Keine persönlichen Einnahmen auf persönlich zugeordneten Konten vorhanden. Eine einkommensbasierte Verteilung kann deshalb nicht berechnet werden.</div>:<div className="shared-income-person-list">{group.incomeBurdenRows.map(row=><div className={`shared-income-person-row ${showTransfers?'':'compact'}`} key={row.id}><span className="person-badge">{row.name}</span><div><span>Persönliches Einkommen</span><strong>{formatMoney(row.personalIncome)}</strong></div><div><span>Einkommensanteil</span><strong>{percent(row.incomeShare==null?null:row.incomeShare*100)}</strong></div><div><span>Soll nach Einkommen</span><strong>{row.incomeBasedFunding==null?'–':`${formatMoney(row.incomeBasedFunding)} · ${percent(row.incomeShare==null?null:row.incomeShare*100)}`}</strong></div><div><span>Belastung vom Einkommen</span><strong>{percent(row.burdenRate)}</strong></div>{showTransfers&&<><div><span>Ist-Beitrag</span><strong>{formatMoney(row.actualFunding)} · {percent(row.actualFundingShare==null?null:row.actualFundingShare*100)}</strong></div><div><span>Abweichung Soll / Ist</span><strong className={row.difference==null?'':row.difference>=0?'good-text':'bad-text'}>{row.difference==null?'–':`${row.difference>=0?'+':''}${formatMoney(row.difference)} · ${percentPoints(row.shareDifferencePoints)}`}</strong></div></>}</div>)}</div>}<div className="shared-income-burden-note">{showTransfers?'Soll-% entspricht dem Einkommensanteil. Ist-% zeigt den Anteil an allen tatsächlichen persönlichen Einzahlungen in den Gemeinschaftsbereich. Die %-Punkt-Abweichung macht sichtbar, wer relativ zum einkommensbasierten Soll mehr oder weniger beiträgt. ':'Diese Statistik ist eine Vergleichs- und Fairnesssicht. '}Sie verändert weder die hinterlegte Personenzuordnung der Kosten noch die tatsächlichen Kontosalden.</div></article>)}</div></section>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -1127,7 +1127,7 @@ html.modal-open, body.modal-open { overflow:hidden !important; overscroll-behavi
|
||||
.shared-finance-summary strong { font-size:11px; }
|
||||
.shared-internal-note { margin-top:10px;padding:8px 10px;border-radius:9px;background:#151d29;color:#9facbc;font-size:9px;line-height:1.45; }
|
||||
.shared-person-table { display:grid;gap:6px;margin-top:11px; }
|
||||
.shared-person-row { display:grid;grid-template-columns:minmax(120px,1.3fr) repeat(4,minmax(90px,1fr));gap:8px;align-items:center;padding:8px 9px;border:1px solid #253246;border-radius:10px;background:#0b121b; }
|
||||
.shared-person-row { display:grid;grid-template-columns:minmax(120px,1.3fr) repeat(5,minmax(88px,1fr));gap:8px;align-items:center;padding:8px 9px;border:1px solid #253246;border-radius:10px;background:#0b121b; }
|
||||
.shared-person-row.compact { grid-template-columns:minmax(120px,1.3fr) repeat(2,minmax(90px,1fr)); }
|
||||
.shared-person-row > div { min-width:0;display:grid;gap:2px; }
|
||||
.shared-person-row > div strong { overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px; }
|
||||
@@ -1144,7 +1144,7 @@ html.modal-open, body.modal-open { overflow:hidden !important; overscroll-behavi
|
||||
.transfer-class-badge.reserve-withdrawal,.transfer-class-badge.investment-withdrawal,.transfer-class-badge.building-withdrawal { border-color:#704f4a;background:#2c1e1c;color:#efc0b8; }
|
||||
.transfer-class-badge.asset-transfer { border-color:#4d526e;background:#1c2033;color:#c4caea; }
|
||||
|
||||
/* v3.25: gemeinsame Kosten relativ zum persönlichen Einkommen */
|
||||
/* v3.26: gemeinsame Kosten relativ zum persönlichen Einkommen + Beitragsanteile */
|
||||
.shared-income-burden-list { display:grid;gap:12px; }
|
||||
.shared-income-burden-card { border:1px solid #30405a;background:#0d141e;border-radius:14px;padding:13px; }
|
||||
.shared-income-burden-head { display:flex;justify-content:space-between;gap:14px;align-items:flex-start;padding-bottom:11px;border-bottom:1px solid #202b3b; }
|
||||
|
||||
Reference in New Issue
Block a user