diff --git a/README.md b/README.md index 19ceb8f..4ba281b 100644 --- a/README.md +++ b/README.md @@ -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. + diff --git a/public/sw.js b/public/sw.js index 30c5f45..556dc27 100644 --- a/public/sw.js +++ b/public/sw.js @@ -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']; diff --git a/src/main.jsx b/src/main.jsx index 5c3d07d..1aee0a1 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -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

Gemeinschaftsfinanzierung

Gemeinsame Einnahmen wie Kindergeld bleiben im gemeinsamen Topf. Sie reduzieren den Finanzierungsbedarf, werden aber keiner Person als persönliche Einnahme zugerechnet.

{stats.groups.map(group=>
{group.accounts.map(a=>a.name).join(' + ')}{group.ownerPersonIds.map(id=>personName(data,id)).join(', ')}
=0?'good-text':'bad-text'}>{group.netAssetChange>=0?'+':''}{formatMoney(group.netAssetChange)} / Monat
Gemeinsame Einnahmen+{formatMoney(group.income)}
Gemeinsame Kosten−{formatMoney(group.expense)}
Kosten-Finanzierungsbedarf{formatMoney(group.costNeed)}
{showTransfers&&
Tatsächliche Finanzierung{formatMoney(group.fundingIn)}
}
{showTransfers&&group.internalTransfers>0&&
Interne Umbuchungen innerhalb dieses Gemeinschaftsbereichs: {formatMoney(group.internalTransfers)} / Monat. Sie sind keine Kosten.
}
{group.personRows.map(row=>
{row.name}
Kostenanteil{formatMoney(row.grossCost)}
Soll-Finanzierung{formatMoney(row.targetFunding)}
{showTransfers&&<>
Ist-Finanzierung{formatMoney(row.actualFunding)}
Differenz=0?'good-text':'bad-text'}>{row.difference>=0?'+':''}{formatMoney(row.difference)}
}
)}
{!showTransfers&&
Transferdetails sind ausgeblendet. Soll-Finanzierung und Kostenbedarf bleiben sichtbar, Ist-Finanzierung wird nicht angezeigt.
}
)}
; + const percent=value=>value==null?'–':`${value.toFixed(1).replace('.',',')} %`; + return

Gemeinschaftsfinanzierung

Gemeinsame Einnahmen wie Kindergeld bleiben im gemeinsamen Topf. Sie reduzieren den Finanzierungsbedarf, werden aber keiner Person als persönliche Einnahme zugerechnet.

{stats.groups.map(group=>
{group.accounts.map(a=>a.name).join(' + ')}{group.ownerPersonIds.map(id=>personName(data,id)).join(', ')}
=0?'good-text':'bad-text'}>{group.netAssetChange>=0?'+':''}{formatMoney(group.netAssetChange)} / Monat
Gemeinsame Einnahmen+{formatMoney(group.income)}
Gemeinsame Kosten−{formatMoney(group.expense)}
Kosten-Finanzierungsbedarf{formatMoney(group.costNeed)}
{showTransfers&&
Tatsächliche Finanzierung{formatMoney(group.fundingIn)}
}
{showTransfers&&group.internalTransfers>0&&
Interne Umbuchungen innerhalb dieses Gemeinschaftsbereichs: {formatMoney(group.internalTransfers)} / Monat. Sie sind keine Kosten.
}
{group.personRows.map(row=>
{row.name}
Kostenanteil{formatMoney(row.grossCost)} · {percent(row.costShare)}
Soll-Finanzierung{formatMoney(row.targetFunding)} · {percent(row.targetFundingShare)}
{showTransfers&&<>
Ist-Beitrag{formatMoney(row.actualFunding)} · {percent(row.actualFundingShare)}
Anteil am Bedarf{percent(row.needCoverageShare)}
Differenz=0?'good-text':'bad-text'}>{row.difference>=0?'+':''}{formatMoney(row.difference)}
}
)}
{showTransfers&&
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.
}{!showTransfers&&
Transferdetails sind ausgeblendet. Kostenanteil und Soll-Finanzierung bleiben sichtbar, tatsächliche Beiträge werden nicht angezeigt.
}
)}
; } @@ -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

Gemeinsame Kosten gemessen am persönlichen Einkommen

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.

{stats.groups.map(group=>
{group.accounts.map(a=>a.name).join(' + ')}{group.ownerPersonIds.map(id=>personName(data,id)).join(', ')}
Belastungsquote Haushalt{percent(group.householdBurdenRate)}
Gemeinsame Kosten−{formatMoney(group.expense)}
Deckung gemeinsame Einnahmen+{formatMoney(group.sharedIncomeCoverage)}
Verbleibender Bedarf{formatMoney(group.costNeed)}
Persönliche Einkommen gesamt{formatMoney(group.personalIncomeTotal)}
{group.sharedIncomeSurplus>0&&
Gemeinsame Einnahmen übersteigen die gemeinsamen Kosten um {formatMoney(group.sharedIncomeSurplus)} / Monat. Es ist keine persönliche Finanzierung nötig.
}{!group.incomeAllocationAvailable?
Keine persönlichen Einnahmen auf persönlich zugeordneten Konten vorhanden. Eine einkommensbasierte Verteilung kann deshalb nicht berechnet werden.
:
{group.incomeBurdenRows.map(row=>
{row.name}
Persönliches Einkommen{formatMoney(row.personalIncome)}
Einkommensanteil{percent(row.incomeShare==null?null:row.incomeShare*100)}
Soll nach Einkommen{row.incomeBasedFunding==null?'–':formatMoney(row.incomeBasedFunding)}
Belastung vom Einkommen{percent(row.burdenRate)}
{showTransfers&&<>
Ist-Finanzierung{formatMoney(row.actualFunding)}
Abweichung=0?'good-text':'bad-text'}>{row.difference==null?'–':`${row.difference>=0?'+':''}${formatMoney(row.difference)}`}
}
)}
}
Diese Statistik ist eine Vergleichs- und Fairnesssicht. Sie verändert weder die hinterlegte Personenzuordnung der Kosten noch die tatsächlichen Kontosalden.
)}
; + const percentPoints=value=>value==null?'–':`${value>=0?'+':''}${value.toFixed(1).replace('.',',')} %-Pkt.`; + return

Gemeinsame Kosten gemessen am persönlichen Einkommen

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.

{stats.groups.map(group=>
{group.accounts.map(a=>a.name).join(' + ')}{group.ownerPersonIds.map(id=>personName(data,id)).join(', ')}
Belastungsquote Haushalt{percent(group.householdBurdenRate)}
Gemeinsame Kosten−{formatMoney(group.expense)}
Deckung gemeinsame Einnahmen+{formatMoney(group.sharedIncomeCoverage)}
Verbleibender Bedarf{formatMoney(group.costNeed)}
Persönliche Einkommen gesamt{formatMoney(group.personalIncomeTotal)}
{group.sharedIncomeSurplus>0&&
Gemeinsame Einnahmen übersteigen die gemeinsamen Kosten um {formatMoney(group.sharedIncomeSurplus)} / Monat. Es ist keine persönliche Finanzierung nötig.
}{!group.incomeAllocationAvailable?
Keine persönlichen Einnahmen auf persönlich zugeordneten Konten vorhanden. Eine einkommensbasierte Verteilung kann deshalb nicht berechnet werden.
:
{group.incomeBurdenRows.map(row=>
{row.name}
Persönliches Einkommen{formatMoney(row.personalIncome)}
Einkommensanteil{percent(row.incomeShare==null?null:row.incomeShare*100)}
Soll nach Einkommen{row.incomeBasedFunding==null?'–':`${formatMoney(row.incomeBasedFunding)} · ${percent(row.incomeShare==null?null:row.incomeShare*100)}`}
Belastung vom Einkommen{percent(row.burdenRate)}
{showTransfers&&<>
Ist-Beitrag{formatMoney(row.actualFunding)} · {percent(row.actualFundingShare==null?null:row.actualFundingShare*100)}
Abweichung Soll / Ist=0?'good-text':'bad-text'}>{row.difference==null?'–':`${row.difference>=0?'+':''}${formatMoney(row.difference)} · ${percentPoints(row.shareDifferencePoints)}`}
}
)}
}
{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.
)}
; } diff --git a/src/styles.css b/src/styles.css index 2a5dc35..4046e50 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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; }