diff --git a/README.md b/README.md index a4d49cb..e2eb9ee 100644 --- a/README.md +++ b/README.md @@ -123,3 +123,18 @@ Die Gemeinschaftsfinanzierung zeigt je Person zusätzlich Prozentwerte. **Kosten ### v3.27 – gerichtete Personentransfers und verfügbare Liquidität Transfers zwischen persönlichen Konten unterschiedlicher Eigentümer werden in personenbezogenen Auswertungen aus den Kontoeigentümern als Sender → Empfänger abgeleitet. Der globale Schalter blendet Transferdetails nur noch ein oder aus; Kontoliquidität und verfügbare Beträge bleiben immer inklusive Transfers. In der persönlichen Statistik werden wirtschaftlich verfügbar und liquid verfügbar getrennt ausgewiesen. + +### v3.28 – kompakte Akkordeons in Planung und Bewegungen + +**Benötigt pro Konto** zeigt geschlossen nur Konto, Eigentum/Funktion und den resultierenden Fehlbetrag bzw. Überschuss. Die Zusammensetzung aus echten Ein-/Ausgängen und Transfers wird erst beim Aufklappen sichtbar. **Bewegungen** werden ebenfalls als einzelne Akkordeons dargestellt: Bezeichnung, Betrag sowie kompakte Badges bleiben sichtbar; Personen, Fälligkeit, Monatswert, Klassifikation, geplante Betragsänderungen und Bearbeitungsaktionen liegen im Detailbereich. + +### v3.29 – gebündelte Navigation und Stammdaten + +Die Hauptnavigation bleibt bei fünf Punkten, die fachlichen Unterseiten sind aber neu gruppiert: + +- **Planung**: Kontenbedarf, Jahresplanung und Szenarien. +- **Auswertung**: Statistik und Auszüge. +- **Mehr → Stammdaten**: Konten, Personen und Kategorien auf einer gemeinsamen Verwaltungsseite mit Tabs. +- **Mehr → Einstellungen & Daten**: Benachrichtigungen sowie Backup/Import und Datenstand auf getrennten Tabs. + +Die Kategorienverwaltung wurde vollständig aus der Statistik entfernt. Kategorieauswertungen bleiben dort erhalten. Der bisherige reine Kategorie-Zähler in der Statistik wurde durch einen zur aktuellen Konto-/Personenauswahl passenden Saldo- bzw. Nettoflusswert ersetzt. Die Änderung betrifft nur Navigation und UI-Struktur; das JSON-Datenmodell bleibt bei `version: 11`. diff --git a/public/sw.js b/public/sw.js index 2c6a552..d605803 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -const SHELL_CACHE='fixfin-shell-v3.27'; +const SHELL_CACHE='fixfin-shell-v3.29'; 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 f22311b..51d5646 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -541,30 +541,33 @@ function Icon({ name }) { const PAGE_META={ dashboard:{label:'Übersicht',description:'Monatliche Salden und die wichtigsten Hinweise auf einen Blick.'}, movements:{label:'Bewegungen',description:'Eingänge, Ausgänge und interne Transfers verwalten.'}, - planning:{label:'Planung',description:'Kontobedarf und Rücklagen für die laufende Planung.'}, - year:{label:'Jahr',description:'Jahresplanung zwischen wirtschaftlicher Sicht und Kontoliquidität.'}, - stats:{label:'Statistik',description:'Kategorien, Verteilungen und Kennzahlen aus der Planung.'}, - statements:{label:'Auszüge',description:'Schlanke Kontoauszüge aus den geplanten Kontobewegungen.'}, - accounts:{label:'Konten & Personen',description:'Konten und Personen für deine Bewegungen verwalten.'}, + planning:{label:'Kontenbedarf',description:'Liquiditätsbedarf und Rücklagen für die laufende Planung.'}, + year:{label:'Jahresplanung',description:'Fälligkeiten und Liquidität über das gesamte Jahr planen.'}, scenarios:{label:'Szenarien',description:'Was-wäre-wenn-Varianten ohne Änderung der echten Planung.'}, - data:{label:'Daten',description:'Backup, Import und Pushover-Einstellungen.'}, + stats:{label:'Statistik',description:'Verteilungen, Personenanteile und wirtschaftliche Kennzahlen auswerten.'}, + statements:{label:'Auszüge',description:'Schlanke Kontoauszüge aus den geplanten Kontobewegungen.'}, + accounts:{label:'Stammdaten',description:'Konten, Personen und Kategorien zentral verwalten.'}, + data:{label:'Einstellungen & Daten',description:'Benachrichtigungen, Backup, Import und Datenstand verwalten.'}, }; const NAV_GROUPS={ + planning:{label:'Planung',items:[ + {tab:'planning',icon:'planning',label:'Kontenbedarf',description:'Liquiditätsbedarf & Rücklagen'}, + {tab:'year',icon:'calendar',label:'Jahresplanung',description:'Fälligkeiten & Jahresliquidität'}, + {tab:'scenarios',icon:'flask',label:'Szenarien',description:'Was-wäre-wenn-Planungen'}, + ]}, analytics:{label:'Auswertung',items:[ - {tab:'year',icon:'calendar',label:'Jahr',description:'Jahresplan & Liquidität'}, - {tab:'stats',icon:'chart',label:'Statistik',description:'Kategorien & Kennzahlen'}, + {tab:'stats',icon:'chart',label:'Statistik',description:'Kennzahlen & Verteilungen'}, {tab:'statements',icon:'statement',label:'Auszüge',description:'PDF- & Excel-Kontoauszüge'}, ]}, more:{label:'Mehr',items:[ - {tab:'accounts',icon:'wallet',label:'Konten & Personen',description:'Zuordnungen verwalten'}, - {tab:'scenarios',icon:'flask',label:'Szenarien',description:'Was-wäre-wenn-Planungen'}, - {tab:'data',icon:'data',label:'Daten',description:'Backup, Import & Pushover'}, + {tab:'accounts',icon:'wallet',label:'Stammdaten',description:'Konten, Personen & Kategorien'}, + {tab:'data',icon:'data',label:'Einstellungen & Daten',description:'Benachrichtigungen, Backup & Import'}, ]}, }; const MAIN_NAV=[ {tab:'dashboard',icon:'dashboard',label:'Übersicht'}, {tab:'movements',icon:'transfer',label:'Bewegungen'}, - {tab:'planning',icon:'planning',label:'Planung'}, + {menu:'planning',icon:'planning',label:'Planung'}, {menu:'analytics',icon:'chart',label:'Auswertung'}, {menu:'more',icon:'more',label:'Mehr'}, ]; @@ -629,14 +632,14 @@ function App() {

{meta.label}

{meta.description}

{error&&
{error}
} {tab==='dashboard'&&} - {tab==='accounts'&&setDialog({type:'accounts',item:x})} onDelete={removeAccount} onAdd={()=>setDialog({type:'accounts'})} onAddPerson={()=>setDialog({type:'persons'})} onEditPerson={x=>setDialog({type:'persons',item:x})} onDeletePerson={removePerson}/>} + {tab==='accounts'&&setDialog({type:'accounts',item:x})} onDeleteAccount={removeAccount} onAddAccount={()=>setDialog({type:'accounts'})} onAddPerson={()=>setDialog({type:'persons'})} onEditPerson={x=>setDialog({type:'persons',item:x})} onDeletePerson={removePerson} onAddCategory={()=>setDialog({type:'categories'})} onEditCategory={x=>setDialog({type:'categories',item:x})} onDeleteCategory={removeCategory}/>} {tab==='movements'&&setDialog({type,item:x})} onDuplicate={(type,x)=>setDialog({type,item:{...x,id:null},duplicate:true})} onDelete={removeItem} onAdd={type=>setDialog({type})}/>} {tab==='year'&&} {tab==='planning'&&} - {tab==='stats'&&setDialog({type:'categories'})} onEditCategory={x=>setDialog({type:'categories',item:x})} onDeleteCategory={removeCategory}/>} + {tab==='stats'&&} {tab==='statements'&&} {tab==='scenarios'&&} - {tab==='data'&&} + {tab==='data'&&}
{navMenu&&setNavMenu(null)} onSelect={selectTab}/>} @@ -711,13 +714,12 @@ function PlanningPage({data,totals}){ return
Liquiditätsplanung: Transfers zählen hier immer als echte Kontobewegungen. Eine Überweisung zu Depot, Bausparer oder Gemeinschaftskonto reduziert deshalb den Monatssaldo des Quellkontos, ohne zur Kostenstatistik zu werden.
0} positive={totalFundingNeed===0}/>
-

Benötigt pro Konto

Liquiditätsbedarf je Konto: echte Ausgaben plus Transfers raus, gedeckt durch echte Eingänge plus Transfers rein.

{totals.accounts.length===0?:
{totals.accounts.map(s=>
{s.account.name}0?'bad-text':'good-text'}>{s.fundingNeed>0?`${formatMoney(s.fundingNeed)} fehlen`:`${formatMoney(s.surplus)} übrig`}
Echte Ausgaben{formatMoney(s.expense)}
Transfers raus{formatMoney(s.transferOut)}
Echte Eingänge{formatMoney(s.income)}
Transfers rein{formatMoney(s.transferIn)}
Zusätzliche Zuführung0?'bad-text':'good-text'}>{formatMoney(s.fundingNeed)}
)}
}
+

Benötigt pro Konto

Die geschlossene Zeile zeigt nur Konto und Ergebnis. Aufklappen zeigt, wie sich der Liquiditätsbedarf zusammensetzt.

{totals.accounts.length===0?:
{totals.accounts.map(s=>
{s.account.name}
{accountOwnerships[s.account.ownership]||accountOwnerships.unassigned}{accountFunctionLabel(s.account)}
0?'bad-text':'good-text'}>{s.fundingNeed>0?`${formatMoney(s.fundingNeed)} fehlen`:`${formatMoney(s.surplus)} übrig`}
Echte Ausgaben{formatMoney(s.expense)}
Transfers raus{formatMoney(s.transferOut)}
Echte Eingänge{formatMoney(s.income)}
Transfers rein{formatMoney(s.transferIn)}
Zusätzliche Zuführung0?'bad-text':'good-text'}>{formatMoney(s.fundingNeed)}

Ausgaben + Transfers raus − Eingänge − Transfers rein. Nur ein positiver Rest ist zusätzliche Zuführung.

)}
}

Rücklagenübersicht

Periodische echte Ausgaben auf einen gleichmäßigen Monatsbetrag heruntergerechnet.

{formatMoney(reserves.annual)} / Jahr{formatMoney(reserves.monthly)} / Monat
{reserves.items.length===0?:
{reserves.items.map(x=>
{x.name}{accountName(data,x.accountId)} · {categoryName(data,x.categoryId)} · {intervals[x.interval].label}
{formatMoney(x.annualNeed)} / Jahr{formatMoney(x.monthlyReserve)} / Monat
)}
}
; } - function Metric({label,value,positive,negative,featured}){return
{label}{formatMoney(value)}
} function TextMetric({label,value,hint}){return
{label}{value}{hint&&{hint}}
} function CountMetric({label,value}){return
{label}{Number(value||0).toLocaleString('de-DE')}
} @@ -732,18 +734,26 @@ function PersonMultiSelect({persons,selectedIds,onChange,label='Personen',help=' ; } -function Accounts({data,onEdit,onDelete,onAdd,onAddPerson,onEditPerson,onDeletePerson}){ +function SectionTabs({items,value,onChange,label}){ + return
{items.map(item=>)}
; +} + +function MasterDataPage({data,onEditAccount,onDeleteAccount,onAddAccount,onAddPerson,onEditPerson,onDeletePerson,onAddCategory,onEditCategory,onDeleteCategory}){ + const [section,setSection]=useState('accounts'); const personUsage=id=>data.incomes.filter(x=>personIdsOf(x).includes(id)).length+data.expenses.filter(x=>personIdsOf(x).includes(id)).length+data.transfers.filter(x=>personIdsOf(x).includes(id)).length+(data.scenarios||[]).reduce((sum,s)=>sum+(s.adjustments||[]).filter(x=>personIdsOf(x).includes(id)).length,0); + const categoryUsage=id=>data.incomes.filter(x=>x.categoryId===id).length+data.expenses.filter(x=>x.categoryId===id).length+data.transfers.filter(x=>x.categoryId===id).length+(data.scenarios||[]).reduce((sum,s)=>sum+(s.adjustments||[]).filter(x=>x.categoryId===id).length,0); const ownedAccounts=id=>(data.accounts||[]).filter(a=>ownerIdsOf(a).includes(id)).length; const unassigned=[...data.incomes,...data.expenses,...data.transfers].filter(x=>personIdsOf(x).length===0).length; const unconfiguredAccounts=(data.accounts||[]).filter(a=>!ownershipDomainKey(a)).length; - return
-

Konten

Eigentum bestimmt, wem Geld wirtschaftlich gehört. Die Kontofunktion bestimmt, ob eine Umbuchung z. B. Rücklage, Investment oder Bausparen ist.

{unconfiguredAccounts>0&&
{unconfiguredAccounts} {unconfiguredAccounts===1?'Konto braucht':'Konten brauchen'} noch eine Eigentumszuordnung.Bestehende Konten wurden absichtlich nicht automatisch geraten. Bitte einmal bearbeiten, Eigentum festlegen und die passende Kontofunktion wählen.
}{data.accounts.length===0?:
{data.accounts.map(a=>)}
KontoEigentumFunktion
{a.name}{accountOwnerships[a.ownership]||accountOwnerships.unassigned}{accountOwnerLabel(data,a)}{accountFunctionLabel(a)}
}
-

Personen

Personen können Konten besitzen und einzelnen Bewegungen gemeinsam zugeordnet werden.

{unassigned>0&&
{unassigned} bestehende {unassigned===1?'Bewegung ist':'Bewegungen sind'} noch ohne Person.Diese Altbestände bleiben erhalten und können beim Bearbeiten zugeordnet werden. Neue Bewegungen benötigen mindestens eine Person.
}{(data.persons||[]).length===0?:
{data.persons.map(person=>
{person.name}{personUsage(person.id)} {personUsage(person.id)===1?'Bewegung':'Bewegungen'} · {ownedAccounts(person.id)} {ownedAccounts(person.id)===1?'Konto':'Konten'}
)}
}
+ const tabs=[{id:'accounts',label:'Konten',count:data.accounts.length},{id:'persons',label:'Personen',count:(data.persons||[]).length},{id:'categories',label:'Kategorien',count:data.categories.length}]; + return
+

Stammdaten

Hier definierst du die Bausteine, auf die Bewegungen und Auswertungen zugreifen. Änderungen wirken zentral in der gesamten Planung.

{data.accounts.length} Konten{(data.persons||[]).length} Personen{data.categories.length} Kategorien
+ {section==='accounts'&&

Konten

Eigentum bestimmt, wem Geld wirtschaftlich gehört. Die Kontofunktion bestimmt, ob eine Umbuchung z. B. Rücklage, Investment oder Bausparen ist.

{unconfiguredAccounts>0&&
{unconfiguredAccounts} {unconfiguredAccounts===1?'Konto braucht':'Konten brauchen'} noch eine Eigentumszuordnung.Bestehende Konten wurden absichtlich nicht automatisch geraten. Bitte einmal bearbeiten, Eigentum festlegen und die passende Kontofunktion wählen.
}{data.accounts.length===0?:
{data.accounts.map(a=>)}
KontoEigentumFunktion
{a.name}{accountOwnerships[a.ownership]||accountOwnerships.unassigned}{accountOwnerLabel(data,a)}{accountFunctionLabel(a)}
}
} + {section==='persons'&&

Personen

Personen können Konten besitzen und einzelnen Bewegungen gemeinsam zugeordnet werden.

{unassigned>0&&
{unassigned} bestehende {unassigned===1?'Bewegung ist':'Bewegungen sind'} noch ohne Person.Diese Altbestände bleiben erhalten und können beim Bearbeiten zugeordnet werden. Neue Bewegungen benötigen mindestens eine Person.
}{(data.persons||[]).length===0?:
{data.persons.map(person=>
{person.name}{personUsage(person.id)} {personUsage(person.id)===1?'Bewegung':'Bewegungen'} · {ownedAccounts(person.id)} {ownedAccounts(person.id)===1?'Konto':'Konten'}
)}
}
} + {section==='categories'&&

Kategorien

Kategorien gelten zentral für Eingänge, Ausgänge und Transfers. Die Statistik wertet sie aus, verwaltet werden sie ausschließlich hier.

{data.categories.length===0?:
{data.categories.map(c=>
{c.name}{categoryUsage(c.id)} {categoryUsage(c.id)===1?'zugeordneter Eintrag':'zugeordnete Einträge'}
)}
}
}
; } - function MovementsPage({data,onEdit,onDuplicate,onDelete,onAdd}){ const [section,setSection]=useState('incomes'); const tabs=[['incomes','Eingänge',data.incomes.length],['expenses','Ausgänge',data.expenses.length],['transfers','Transfers',data.transfers.length]]; @@ -761,20 +771,10 @@ function Entries({type,items,accounts,categories,persons,onEdit,onDuplicate,onDe const isIncome=type==='incomes'; const dueText=x=>x.interval==='monthly'?'Jeden Monat':x.dueMonth?months[Number(x.dueMonth)-1]:'Nicht festgelegt'; const currentAmount=x=>effectiveAmount(x); - return

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

Betrag gilt pro gewähltem Intervall. Periodische Werte werden zusätzlich als Monatsanteil gezeigt.

{items.length===0?:<> -
{items.map(x=>)}
BezeichnungPersonenKategorieKontoIntervallMonatBetragØ / Monat
{x.name}{!isIncome&&x.pushoverReminder&&Reminder}{(x.amountChanges||[]).length>0&&{x.amountChanges.length} Änderung{x.amountChanges.length===1?'':'en'}}{x.endDate&&bis {formatDate(x.endDate)}}{personLabelLocal(x)}{categoryNameLocal(x.categoryId)}{accountNameLocal(x.accountId)}{intervals[x.interval||'monthly'].label}{dueText(x)}{isIncome?'+':'−'}{formatMoney(currentAmount(x))}{formatMoney(monthlyEquivalent(x))}
-
{items.map(x=>
-
{x.name}{categoryNameLocal(x.categoryId)}{!isIncome&&x.pushoverReminder&&Reminder am 1.}{(x.amountChanges||[]).length>0&&{x.amountChanges.length} geplant}{x.endDate&&bis {formatDate(x.endDate)}}
{isIncome?'+':'−'}{formatMoney(currentAmount(x))}
-
-
Personen{personLabelLocal(x)}
-
Konto{accountNameLocal(x.accountId)}
-
Intervall{intervals[x.interval||'monthly'].label}
-
Fälligkeit{dueText(x)}
-
Ø / Monat{formatMoney(monthlyEquivalent(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))}{formatMoney(monthlyEquivalent(x))} / Monat
+
Personen{personLabelLocal(x)}
Konto{accountNameLocal(x.accountId)}
Kategorie{categoryNameLocal(x.categoryId)}
Intervall{intervals[x.interval||'monthly'].label}
Fälligkeit{dueText(x)}
Ø / Monat{formatMoney(monthlyEquivalent(x))}
{x.endDate&&
Enddatum{formatDate(x.endDate)}
}{!isIncome&&x.pushoverReminder&&
ErinnerungAm 1. des Fälligkeitsmonats
}
{(x.amountChanges||[]).length>0&&
Geplante Betragsänderungen{x.amountChanges.map(c=>
ab {formatDate(c.effectiveFrom)}{formatMoney(c.amount)}
)}
}
+
)}
}
; } function Transfers({data,onEdit,onDuplicate,onDelete}){ @@ -782,18 +782,12 @@ 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=x=>x.interval==='monthly'?'Jeden Monat':x.dueMonth?months[Number(x.dueMonth)-1]:'Nicht festgelegt'; - return

Fixe Transfers

Transfers werden automatisch anhand von Eigentum und Kontofunktion klassifiziert. Sie beeinflussen Kontoliquidität, sind aber keine Konsumausgaben.

{data.transfers.length===0?:<> -
{data.transfers.map(x=>{const cls=transferClassification(data,x);return })}
BezeichnungTypPersonenKategorieVonNachIntervallØ / Monat
{x.name}{x.endDate&&bis {formatDate(x.endDate)}}{cls.label}{personLabelLocal(x)}{categoryNameLocal(x.categoryId)}{accountNameLocal(x.fromAccountId)}{accountNameLocal(x.toAccountId)}{intervals[x.interval||'monthly'].label}{formatMoney(monthlyEquivalent(x))}
-
{data.transfers.map(x=>{const cls=transferClassification(data,x);return
-
{x.name}{cls.label}{categoryNameLocal(x.categoryId)}{x.endDate&&bis {formatDate(x.endDate)}}
{formatMoney(x.amount)}
-
Von{accountNameLocal(x.fromAccountId)}
Nach{accountNameLocal(x.toAccountId)}
-
Personen{personLabelLocal(x)}
Einordnung{cls.description}
Intervall{intervals[x.interval||'monthly'].label}
Ø / Monat{formatMoney(monthlyEquivalent(x))}
-
-
})}
- }
; + 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.endDate&&bis {formatDate(x.endDate)}}
{formatMoney(effectiveAmount(x))}{formatMoney(monthlyEquivalent(x))} / 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)}
Ø / Monat{formatMoney(monthlyEquivalent(x))}
{x.endDate&&
Enddatum{formatDate(x.endDate)}
}
+
})}
}
; } - function transferTypeStats(data,accountId='all',personId='all'){ const isAll=accountId==='all';const buckets=new Map(); for(const item of data.transfers||[]){ @@ -822,7 +816,7 @@ function CategoryDistribution({title,subtitle,stats,tone,data}){ ; } -function StatisticsPage({data,includeTransfers,onTransfersChange,onAddCategory,onEditCategory,onDeleteCategory}){ +function StatisticsPage({data,includeTransfers,onTransfersChange}){ const [accountId,setAccountId]=useState('all'); const [personId,setPersonId]=useState('all'); const stats=useMemo(()=>categoryStats(data,accountId,personId,includeTransfers),[data,accountId,personId,includeTransfers]); @@ -831,14 +825,15 @@ function StatisticsPage({data,includeTransfers,onTransfersChange,onAddCategory,o const kpis=useMemo(()=>dashboardKpis(data,totals),[data,totals]); const selectedName=data.accounts.find(a=>a.id===accountId)?.name||'Alle Konten'; const selectedPerson=personId==='all'?'Alle Personen':personName(data,personId); - const usage=id=>data.incomes.filter(x=>x.categoryId===id).length+data.expenses.filter(x=>x.categoryId===id).length+data.transfers.filter(x=>x.categoryId===id).length; const uncategorized=(stats.incomes.rows.find(x=>x.id==='__uncategorized__')?.count||0)+(stats.expenses.rows.find(x=>x.id==='__uncategorized__')?.count||0)+(includeTransfers?(stats.transfers.rows.find(x=>x.id==='__uncategorized__')?.count||0):0); const personalIncomeLabel=personId==='all'?'Echte Eingänge Ø / Monat':'Eigene Kontoeingänge Ø / Monat'; + const selectionBalance=accountId==='all'?(personId==='all'?stats.incomes.total-stats.expenses.total:personFlows.totals.economicAvailable):personFlows.totals.net; + const selectionBalanceLabel=accountId==='all'?(personId==='all'?'Wirtschaftlicher Saldo Auswahl':'Wirtschaftlich verfügbar'):'Nettofluss Auswahl'; return

Gesamtkennzahlen

Wirtschaftliche Kennzahlen verwenden nur echte Einnahmen und echte Ausgaben. Umbuchungen verändern diese Werte nicht.

=0} negative={totals.balance<0} featured/>
Statistik für{selectedName} · {selectedPerson}
-
+
=0} negative={selectionBalance<0}/>
{accountId==='all'&&} {accountId==='all'&&} @@ -846,7 +841,6 @@ function StatisticsPage({data,includeTransfers,onTransfersChange,onAddCategory,o {includeTransfers&&} -

Kategorien verwalten

Kategorien gelten für Eingänge, Ausgänge und Transfers gemeinsam.

{data.categories.length===0?:
{data.categories.map(c=>
{c.name}{usage(c.id)} {usage(c.id)===1?'zugeordneter Eintrag':'zugeordnete Einträge'}
)}
}
; } @@ -995,7 +989,8 @@ function StatementsPage({data,includeTransfers,onTransfersChange}){ return

Kontoauszug erstellen

Nur die geplanten Bewegungen des gewählten Kontos. Keine Stammdaten, Szenarien oder Systemeinstellungen.

PDF · XLSX
Kontoauszug{account} · {selectedPerson} · {year}

PDF: Jahreswerte und monatlich gruppierte Kontobewegungen {includeTransfers?'inklusive Transfers':'ohne Transfers'}. Bei Person-Filter werden gemeinsam zugeordnete Bewegungen anteilig ausgewiesen. Excel: genau zwei Blätter, Kontoauszug und Monatsübersicht.

Erstellt aus FixFin-Planungsdaten, kein von einer Bank ausgestellter Kontoauszug.

; } -function DataPage({data,importFile}){ +function SettingsDataPage({data,importFile}){ + const [section,setSection]=useState('notifications'); const summary=[ ['Konten',data.accounts.length], ['Kategorien',data.categories.length], @@ -1006,14 +1001,18 @@ function DataPage({data,importFile}){ ['Szenarien',(data.scenarios||[]).length], ]; const json=JSON.stringify(data,null,2); - return
- -

Daten sichern

Alle Daten liegen in einer JSON-Datei. Vor jedem Speichern wird serverseitig automatisch eine .bak-Datei angelegt.

JSON herunterladen
-

Aktueller Datenstand

Kurzübersicht über den Inhalt deiner FixFin-Datei.

-
{summary.map(([label,value])=>
{label}{Number(value).toLocaleString('de-DE')}
)}
-
JSON-RohdatenNur zur Kontrolle und Fehlersuche.
{json}
-
JSON-Rohdaten anzeigenNur bei Bedarf öffnen
{json}
-
+ const tabs=[{id:'notifications',label:'Benachrichtigungen'},{id:'backup',label:'Backup & Daten'}]; + return
+

Einstellungen & Daten

Systemfunktionen sind bewusst von den fachlichen Stammdaten getrennt.

+ {section==='notifications'&&} + {section==='backup'&&<> +

Backup & Import

Alle Planungsdaten liegen in einer JSON-Datei. Vor jedem Speichern wird serverseitig automatisch eine .bak-Datei angelegt.

JSON herunterladen
+

Aktueller Datenstand

Kurzübersicht über den Inhalt deiner FixFin-Datei.

+
{summary.map(([label,value])=>
{label}{Number(value).toLocaleString('de-DE')}
)}
+
JSON-RohdatenNur zur Kontrolle und Fehlersuche.
{json}
+
JSON-Rohdaten anzeigenNur bei Bedarf öffnen
{json}
+
+ }
; } function Empty({text}){return
{text}
} diff --git a/src/styles.css b/src/styles.css index 4046e50..a51651c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1171,3 +1171,96 @@ html.modal-open, body.modal-open { overflow:hidden !important; overscroll-behavi .shared-income-person-row,.shared-income-person-row.compact { grid-template-columns:1fr 1fr;gap:7px 10px; } .shared-income-person-row > .person-badge { grid-column:1/-1; } } + +/* v3.28: kompakte Akkordeons für Planung und Bewegungen */ +.compact-badges { display:flex; flex-wrap:wrap; gap:5px; align-items:center; min-width:0; } +.account-badge,.due-badge { display:inline-flex;align-items:center;width:max-content;max-width:100%;padding:4px 8px;border-radius:999px;border:1px solid #344154;background:#151d29;color:#b9c4d3;font-size:9px;font-weight:800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; } +.due-badge { border-color:#5b4e31;background:#261f13;color:#dfca8d; } + +.funding-accordion-list { align-items:start; } +.funding-card.funding-accordion { padding:0; overflow:hidden; } +.funding-accordion > summary { list-style:none; cursor:pointer; display:flex; align-items:center; justify-content:space-between; gap:12px; padding:14px 15px; min-height:72px; } +.funding-accordion > summary::-webkit-details-marker { display:none; } +.funding-summary-copy { min-width:0; display:grid; gap:7px; } +.funding-summary-copy > strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:13px; } +.funding-summary-right { flex:0 0 auto; display:flex; align-items:center; gap:8px; } +.funding-summary-right > span:first-child { font-size:11px; font-weight:850; white-space:nowrap; } +.funding-accordion[open] > summary { border-bottom:1px solid #202a39; } +.funding-accordion[open] .accordion-chevron { transform:rotate(180deg); } +.funding-detail { padding:0 15px 14px; } +.funding-detail .funding-numbers { margin-top:12px; } +.funding-result { margin-top:3px; padding-top:9px; border-top:1px solid #202a39; } +.funding-formula { margin:10px 0 0; color:var(--muted); font-size:9px; line-height:1.45; } + +.movement-accordion-list { display:grid; gap:8px; } +.movement-accordion { border:1px solid #29364a; background:#0d131c; border-radius:13px; overflow:hidden; min-width:0; } +.movement-accordion > summary { list-style:none; cursor:pointer; padding:12px 13px; } +.movement-accordion > summary::-webkit-details-marker { display:none; } +.movement-summary-main { display:grid; grid-template-columns:minmax(0,1fr) auto 18px; align-items:center; gap:12px; min-width:0; } +.movement-summary-copy { min-width:0; display:grid; gap:7px; } +.movement-summary-copy > strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:13px; } +.movement-summary-route { color:#8f9caf; font-size:9px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } +.movement-summary-route b { color:#9aa8ff; font-weight:900; } +.movement-summary-value { flex:0 0 auto; text-align:right; display:grid; gap:2px; } +.movement-summary-value > strong { font-size:13px; white-space:nowrap; } +.movement-summary-value > small { color:var(--muted); font-size:8px; white-space:nowrap; } +.movement-accordion[open] > summary { background:#101823; border-bottom:1px solid #202a39; } +.movement-accordion[open] .accordion-chevron { transform:rotate(180deg); } +.movement-accordion-body { padding:12px 13px 13px; } +.movement-detail-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:9px 12px; } +.movement-detail-grid > div { min-width:0; padding:8px 9px; border:1px solid #243144; background:#101722; border-radius:9px; } +.movement-detail-grid span { display:block; color:#718096; font-size:8px; text-transform:uppercase; letter-spacing:.055em; font-weight:800; margin-bottom:3px; } +.movement-detail-grid strong { display:block; font-size:10px; line-height:1.35; overflow:hidden; text-overflow:ellipsis; } +.movement-change-preview { display:grid; gap:6px; margin-top:10px; padding:9px 10px; border:1px solid #30405b; background:#11192a; border-radius:10px; } +.movement-change-preview > span { color:#aebbe0; font-size:9px; font-weight:850; } +.movement-change-preview > div { display:flex; justify-content:space-between; gap:10px; font-size:9px; } +.movement-change-preview > div span { color:#c3cceb; } +.movement-accordion .movement-card-actions { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:7px; margin-top:11px; padding-top:11px; border-top:1px solid #202a39; } +.movement-accordion .movement-card-actions button { min-width:0; border:1px solid #2a374a; background:#171f2d; color:#d8e0ea; border-radius:9px; padding:8px 7px; font-size:9px; font-weight:750; } +.movement-accordion .movement-card-actions button.danger { color:#ff929a; } + +@media (max-width:760px) { + .funding-accordion > summary { min-height:68px; padding:12px; } + .funding-summary-copy .ownership-badge,.funding-summary-copy .account-function-badge { font-size:8px; padding:3px 6px; } + .funding-summary-right > span:first-child { font-size:10px; } + .movement-summary-main { grid-template-columns:minmax(0,1fr) auto 15px; gap:8px; } + .movement-accordion > summary { padding:11px; } + .movement-summary-copy { gap:6px; } + .movement-summary-copy > strong { font-size:12px; } + .movement-summary-value > strong { font-size:12px; } + .movement-summary-value > small { font-size:7px; } + .movement-accordion .category-badge,.movement-accordion .person-badge,.movement-accordion .interval-badge,.movement-accordion .account-badge,.movement-accordion .due-badge,.movement-accordion .transfer-class-badge,.movement-accordion .reminder-badge,.movement-accordion .change-badge,.movement-accordion .end-badge { font-size:7px; padding:3px 6px; margin-left:0; margin-top:0; } + .movement-detail-grid { grid-template-columns:1fr 1fr; gap:7px; } + .movement-accordion-body { padding:10px 11px 11px; } + .movement-summary-route { font-size:8px; } +} + +@media (max-width:430px) { + .movement-summary-main { grid-template-columns:minmax(0,1fr) auto 13px; } + .movement-summary-value > small { display:none; } + .movement-detail-grid { grid-template-columns:1fr 1fr; } + .compact-badges { gap:4px; } + .funding-summary-copy .ownership-badge { display:none; } +} + +/* v3.29 – gebündelte Navigation und zentrale Stammdaten */ +.section-tabs { display:flex; flex-wrap:wrap; gap:7px; padding:4px; border:1px solid #263247; background:#0b1119; border-radius:13px; } +.section-tabs button { display:flex; align-items:center; justify-content:center; gap:7px; min-height:38px; border:0; border-radius:9px; padding:8px 13px; background:transparent; color:var(--muted); font-weight:800; } +.section-tabs button:hover { background:#151e2b; color:var(--text); } +.section-tabs button.active { background:#202a39; color:var(--text); box-shadow:inset 0 0 0 1px #35445d; } +.section-tabs button small { min-width:20px; padding:2px 6px; border-radius:999px; background:#182130; color:#9aa8ba; font-size:9px; } +.section-tabs button.active small { background:#34415a; color:#dfe5ee; } +.master-data-header .section-head, +.settings-data-header .section-head { margin-bottom:12px; } +.master-data-counts { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:6px; } +.master-data-counts span { border:1px solid #2b394f; background:#101722; border-radius:999px; padding:5px 8px; color:#aab5c6; font-size:9px; font-weight:800; white-space:nowrap; } +.master-data-page > .panel:not(.master-data-header), +.settings-data-page > .panel:not(.settings-data-header) { margin-top:0; } + +@media (max-width:760px) { + .section-tabs { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:4px; } + .settings-data-header .section-tabs { grid-template-columns:repeat(2,minmax(0,1fr)); } + .section-tabs button { min-width:0; padding:8px 5px; font-size:11px; } + .master-data-header .section-head { align-items:flex-start; } + .master-data-counts { justify-content:flex-start; } +}