V3.29
This commit is contained in:
@@ -123,3 +123,18 @@ Die Gemeinschaftsfinanzierung zeigt je Person zusätzlich Prozentwerte. **Kosten
|
|||||||
### v3.27 – gerichtete Personentransfers und verfügbare Liquidität
|
### 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.
|
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`.
|
||||||
|
|||||||
+1
-1
@@ -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 DATA_CACHE='fixfin-data-v1';
|
||||||
const APP_SHELL=['/','/manifest.webmanifest','/icons/icon-192.png','/icons/icon-512.png','/icons/apple-touch-icon.png'];
|
const APP_SHELL=['/','/manifest.webmanifest','/icons/icon-192.png','/icons/icon-512.png','/icons/apple-touch-icon.png'];
|
||||||
|
|
||||||
|
|||||||
+58
-59
@@ -541,30 +541,33 @@ function Icon({ name }) {
|
|||||||
const PAGE_META={
|
const PAGE_META={
|
||||||
dashboard:{label:'Übersicht',description:'Monatliche Salden und die wichtigsten Hinweise auf einen Blick.'},
|
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.'},
|
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.'},
|
planning:{label:'Kontenbedarf',description:'Liquiditätsbedarf und Rücklagen für die laufende Planung.'},
|
||||||
year:{label:'Jahr',description:'Jahresplanung zwischen wirtschaftlicher Sicht und Kontoliquidität.'},
|
year:{label:'Jahresplanung',description:'Fälligkeiten und Liquidität über das gesamte Jahr planen.'},
|
||||||
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.'},
|
|
||||||
scenarios:{label:'Szenarien',description:'Was-wäre-wenn-Varianten ohne Änderung der echten Planung.'},
|
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={
|
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:[
|
analytics:{label:'Auswertung',items:[
|
||||||
{tab:'year',icon:'calendar',label:'Jahr',description:'Jahresplan & Liquidität'},
|
{tab:'stats',icon:'chart',label:'Statistik',description:'Kennzahlen & Verteilungen'},
|
||||||
{tab:'stats',icon:'chart',label:'Statistik',description:'Kategorien & Kennzahlen'},
|
|
||||||
{tab:'statements',icon:'statement',label:'Auszüge',description:'PDF- & Excel-Kontoauszüge'},
|
{tab:'statements',icon:'statement',label:'Auszüge',description:'PDF- & Excel-Kontoauszüge'},
|
||||||
]},
|
]},
|
||||||
more:{label:'Mehr',items:[
|
more:{label:'Mehr',items:[
|
||||||
{tab:'accounts',icon:'wallet',label:'Konten & Personen',description:'Zuordnungen verwalten'},
|
{tab:'accounts',icon:'wallet',label:'Stammdaten',description:'Konten, Personen & Kategorien'},
|
||||||
{tab:'scenarios',icon:'flask',label:'Szenarien',description:'Was-wäre-wenn-Planungen'},
|
{tab:'data',icon:'data',label:'Einstellungen & Daten',description:'Benachrichtigungen, Backup & Import'},
|
||||||
{tab:'data',icon:'data',label:'Daten',description:'Backup, Import & Pushover'},
|
|
||||||
]},
|
]},
|
||||||
};
|
};
|
||||||
const MAIN_NAV=[
|
const MAIN_NAV=[
|
||||||
{tab:'dashboard',icon:'dashboard',label:'Übersicht'},
|
{tab:'dashboard',icon:'dashboard',label:'Übersicht'},
|
||||||
{tab:'movements',icon:'transfer',label:'Bewegungen'},
|
{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:'analytics',icon:'chart',label:'Auswertung'},
|
||||||
{menu:'more',icon:'more',label:'Mehr'},
|
{menu:'more',icon:'more',label:'Mehr'},
|
||||||
];
|
];
|
||||||
@@ -629,14 +632,14 @@ function App() {
|
|||||||
<main><header className="topbar"><div><h1>{meta.label}</h1><p>{meta.description}</p></div></header>
|
<main><header className="topbar"><div><h1>{meta.label}</h1><p>{meta.description}</p></div></header>
|
||||||
{error&&<div className="alert"><span>{error}</span><button onClick={()=>setError('')}>×</button></div>}
|
{error&&<div className="alert"><span>{error}</span><button onClick={()=>setError('')}>×</button></div>}
|
||||||
{tab==='dashboard'&&<Dashboard data={data} totals={totals} onToggle={togglePeriodic} includeTransfers={includeTransfers} onTransfersChange={setIncludeTransfers}/>}
|
{tab==='dashboard'&&<Dashboard data={data} totals={totals} onToggle={togglePeriodic} includeTransfers={includeTransfers} onTransfersChange={setIncludeTransfers}/>}
|
||||||
{tab==='accounts'&&<Accounts data={data} onEdit={x=>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'&&<MasterDataPage data={data} onEditAccount={x=>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'&&<MovementsPage data={data} onEdit={(type,x)=>setDialog({type,item:x})} onDuplicate={(type,x)=>setDialog({type,item:{...x,id:null},duplicate:true})} onDelete={removeItem} onAdd={type=>setDialog({type})}/>}
|
{tab==='movements'&&<MovementsPage data={data} onEdit={(type,x)=>setDialog({type,item:x})} onDuplicate={(type,x)=>setDialog({type,item:{...x,id:null},duplicate:true})} onDelete={removeItem} onAdd={type=>setDialog({type})}/>}
|
||||||
{tab==='year'&&<YearPage data={data} includeTransfers={includeTransfers} onTransfersChange={setIncludeTransfers}/>}
|
{tab==='year'&&<YearPage data={data} includeTransfers={includeTransfers} onTransfersChange={setIncludeTransfers}/>}
|
||||||
{tab==='planning'&&<PlanningPage data={data} totals={totals}/>}
|
{tab==='planning'&&<PlanningPage data={data} totals={totals}/>}
|
||||||
{tab==='stats'&&<StatisticsPage data={data} includeTransfers={includeTransfers} onTransfersChange={setIncludeTransfers} onAddCategory={()=>setDialog({type:'categories'})} onEditCategory={x=>setDialog({type:'categories',item:x})} onDeleteCategory={removeCategory}/>}
|
{tab==='stats'&&<StatisticsPage data={data} includeTransfers={includeTransfers} onTransfersChange={setIncludeTransfers}/>}
|
||||||
{tab==='statements'&&<StatementsPage data={data} includeTransfers={includeTransfers} onTransfersChange={setIncludeTransfers}/>}
|
{tab==='statements'&&<StatementsPage data={data} includeTransfers={includeTransfers} onTransfersChange={setIncludeTransfers}/>}
|
||||||
{tab==='scenarios'&&<ScenariosPage data={data} persist={persist}/>}
|
{tab==='scenarios'&&<ScenariosPage data={data} persist={persist}/>}
|
||||||
{tab==='data'&&<DataPage data={data} importFile={importFile}/>}
|
{tab==='data'&&<SettingsDataPage data={data} importFile={importFile}/>}
|
||||||
</main>
|
</main>
|
||||||
<nav className="mobile-nav">{MAIN_NAV.map(item=><button key={item.tab||item.menu} className={mainActive(item)?'active':''} onClick={()=>activateMain(item)}><Icon name={item.icon}/><span>{item.label}</span></button>)}</nav>
|
<nav className="mobile-nav">{MAIN_NAV.map(item=><button key={item.tab||item.menu} className={mainActive(item)?'active':''} onClick={()=>activateMain(item)}><Icon name={item.icon}/><span>{item.label}</span></button>)}</nav>
|
||||||
{navMenu&&<NavGroupMenu group={navMenu} tab={tab} onClose={()=>setNavMenu(null)} onSelect={selectTab}/>}
|
{navMenu&&<NavGroupMenu group={navMenu} tab={tab} onClose={()=>setNavMenu(null)} onSelect={selectTab}/>}
|
||||||
@@ -711,13 +714,12 @@ function PlanningPage({data,totals}){
|
|||||||
return <div className="content-stack planning-page">
|
return <div className="content-stack planning-page">
|
||||||
<div className="info-note"><strong>Liquiditätsplanung:</strong> 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.</div>
|
<div className="info-note"><strong>Liquiditätsplanung:</strong> 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.</div>
|
||||||
<section className="summary-grid planning-summary"><Metric label="Echte Kosten / Monat" value={totalRequired} negative/><Metric label="Zusätzliche Zuführung" value={totalFundingNeed} negative={totalFundingNeed>0} positive={totalFundingNeed===0}/><Metric label="Rücklage / Monat" value={reserves.monthly}/><Metric label="Periodischer Bedarf / Jahr" value={reserves.annual}/></section>
|
<section className="summary-grid planning-summary"><Metric label="Echte Kosten / Monat" value={totalRequired} negative/><Metric label="Zusätzliche Zuführung" value={totalFundingNeed} negative={totalFundingNeed>0} positive={totalFundingNeed===0}/><Metric label="Rücklage / Monat" value={reserves.monthly}/><Metric label="Periodischer Bedarf / Jahr" value={reserves.annual}/></section>
|
||||||
<section className="panel"><div className="section-head"><div><h2>Benötigt pro Konto</h2><p>Liquiditätsbedarf je Konto: echte Ausgaben plus Transfers raus, gedeckt durch echte Eingänge plus Transfers rein.</p></div></div>{totals.accounts.length===0?<Empty text="Lege zuerst ein Konto an."/>:<div className="funding-grid">{totals.accounts.map(s=><div className="funding-card" key={s.account.id}><div className="funding-head"><strong>{s.account.name}</strong><span className={s.fundingNeed>0?'bad-text':'good-text'}>{s.fundingNeed>0?`${formatMoney(s.fundingNeed)} fehlen`:`${formatMoney(s.surplus)} übrig`}</span></div><div className="funding-numbers"><div><span>Echte Ausgaben</span><strong>{formatMoney(s.expense)}</strong></div><div><span>Transfers raus</span><strong>{formatMoney(s.transferOut)}</strong></div><div><span>Echte Eingänge</span><strong>{formatMoney(s.income)}</strong></div><div><span>Transfers rein</span><strong>{formatMoney(s.transferIn)}</strong></div><div><span>Zusätzliche Zuführung</span><strong className={s.fundingNeed>0?'bad-text':'good-text'}>{formatMoney(s.fundingNeed)}</strong></div></div></div>)}</div>}</section>
|
<section className="panel"><div className="section-head"><div><h2>Benötigt pro Konto</h2><p>Die geschlossene Zeile zeigt nur Konto und Ergebnis. Aufklappen zeigt, wie sich der Liquiditätsbedarf zusammensetzt.</p></div></div>{totals.accounts.length===0?<Empty text="Lege zuerst ein Konto an."/>:<div className="funding-grid funding-accordion-list">{totals.accounts.map(s=><details className="funding-card funding-accordion" key={s.account.id}><summary><div className="funding-summary-copy"><strong>{s.account.name}</strong><div className="compact-badges"><span className={`ownership-badge ${s.account.ownership||'unassigned'}`}>{accountOwnerships[s.account.ownership]||accountOwnerships.unassigned}</span><span className="account-function-badge">{accountFunctionLabel(s.account)}</span></div></div><div className="funding-summary-right"><span className={s.fundingNeed>0?'bad-text':'good-text'}>{s.fundingNeed>0?`${formatMoney(s.fundingNeed)} fehlen`:`${formatMoney(s.surplus)} übrig`}</span><span className="accordion-chevron" aria-hidden="true">⌄</span></div></summary><div className="funding-detail"><div className="funding-numbers"><div><span>Echte Ausgaben</span><strong>{formatMoney(s.expense)}</strong></div><div><span>Transfers raus</span><strong>{formatMoney(s.transferOut)}</strong></div><div><span>Echte Eingänge</span><strong>{formatMoney(s.income)}</strong></div><div><span>Transfers rein</span><strong>{formatMoney(s.transferIn)}</strong></div><div className="funding-result"><span>Zusätzliche Zuführung</span><strong className={s.fundingNeed>0?'bad-text':'good-text'}>{formatMoney(s.fundingNeed)}</strong></div></div><p className="funding-formula">Ausgaben + Transfers raus − Eingänge − Transfers rein. Nur ein positiver Rest ist zusätzliche Zuführung.</p></div></details>)}</div>}</section>
|
||||||
<SharedFundingOverview data={data}/>
|
<SharedFundingOverview data={data}/>
|
||||||
<section className="panel reserve-panel"><div className="section-head"><div><h2>Rücklagenübersicht</h2><p>Periodische echte Ausgaben auf einen gleichmäßigen Monatsbetrag heruntergerechnet.</p></div><div className="reserve-total"><span>{formatMoney(reserves.annual)} / Jahr</span><strong>{formatMoney(reserves.monthly)} / Monat</strong></div></div>{reserves.items.length===0?<Empty text="Keine periodischen Ausgaben vorhanden."/>:<div className="reserve-list">{reserves.items.map(x=><div className="reserve-row" key={x.id}><div><strong>{x.name}</strong><span>{accountName(data,x.accountId)} · {categoryName(data,x.categoryId)} · {intervals[x.interval].label}</span></div><div><span>{formatMoney(x.annualNeed)} / Jahr</span><strong>{formatMoney(x.monthlyReserve)} / Monat</strong></div></div>)}</div>}</section>
|
<section className="panel reserve-panel"><div className="section-head"><div><h2>Rücklagenübersicht</h2><p>Periodische echte Ausgaben auf einen gleichmäßigen Monatsbetrag heruntergerechnet.</p></div><div className="reserve-total"><span>{formatMoney(reserves.annual)} / Jahr</span><strong>{formatMoney(reserves.monthly)} / Monat</strong></div></div>{reserves.items.length===0?<Empty text="Keine periodischen Ausgaben vorhanden."/>:<div className="reserve-list">{reserves.items.map(x=><div className="reserve-row" key={x.id}><div><strong>{x.name}</strong><span>{accountName(data,x.accountId)} · {categoryName(data,x.categoryId)} · {intervals[x.interval].label}</span></div><div><span>{formatMoney(x.annualNeed)} / Jahr</span><strong>{formatMoney(x.monthlyReserve)} / Monat</strong></div></div>)}</div>}</section>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function Metric({label,value,positive,negative,featured}){return <div className={`metric ${featured?'featured':''}`}><span>{label}</span><strong className={positive?'good-text':negative?'bad-text':''}>{formatMoney(value)}</strong></div>}
|
function Metric({label,value,positive,negative,featured}){return <div className={`metric ${featured?'featured':''}`}><span>{label}</span><strong className={positive?'good-text':negative?'bad-text':''}>{formatMoney(value)}</strong></div>}
|
||||||
function TextMetric({label,value,hint}){return <div className="metric text-metric"><span>{label}</span><strong>{value}</strong>{hint&&<small>{hint}</small>}</div>}
|
function TextMetric({label,value,hint}){return <div className="metric text-metric"><span>{label}</span><strong>{value}</strong>{hint&&<small>{hint}</small>}</div>}
|
||||||
function CountMetric({label,value}){return <div className="metric"><span>{label}</span><strong>{Number(value||0).toLocaleString('de-DE')}</strong></div>}
|
function CountMetric({label,value}){return <div className="metric"><span>{label}</span><strong>{Number(value||0).toLocaleString('de-DE')}</strong></div>}
|
||||||
@@ -732,18 +734,26 @@ function PersonMultiSelect({persons,selectedIds,onChange,label='Personen',help='
|
|||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Accounts({data,onEdit,onDelete,onAdd,onAddPerson,onEditPerson,onDeletePerson}){
|
function SectionTabs({items,value,onChange,label}){
|
||||||
|
return <div className="section-tabs" role="tablist" aria-label={label}>{items.map(item=><button type="button" role="tab" aria-selected={value===item.id} className={value===item.id?'active':''} key={item.id} onClick={()=>onChange(item.id)}><span>{item.label}</span>{item.count!=null&&<small>{Number(item.count).toLocaleString('de-DE')}</small>}</button>)}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 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 unassigned=[...data.incomes,...data.expenses,...data.transfers].filter(x=>personIdsOf(x).length===0).length;
|
||||||
const unconfiguredAccounts=(data.accounts||[]).filter(a=>!ownershipDomainKey(a)).length;
|
const unconfiguredAccounts=(data.accounts||[]).filter(a=>!ownershipDomainKey(a)).length;
|
||||||
return <div className="content-stack accounts-people-page">
|
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}];
|
||||||
<section className="panel"><div className="section-head"><div><h2>Konten</h2><p>Eigentum bestimmt, wem Geld wirtschaftlich gehört. Die Kontofunktion bestimmt, ob eine Umbuchung z. B. Rücklage, Investment oder Bausparen ist.</p></div><button className="secondary" onClick={onAdd}>+ Konto</button></div>{unconfiguredAccounts>0&&<div className="account-config-note"><strong>{unconfiguredAccounts} {unconfiguredAccounts===1?'Konto braucht':'Konten brauchen'} noch eine Eigentumszuordnung.</strong><span>Bestehende Konten wurden absichtlich nicht automatisch geraten. Bitte einmal bearbeiten, Eigentum festlegen und die passende Kontofunktion wählen.</span></div>}{data.accounts.length===0?<Empty text="Noch keine Konten vorhanden."/>:<div className="table-wrap account-manager-table"><table><thead><tr><th>Konto</th><th>Eigentum</th><th>Funktion</th><th></th></tr></thead><tbody>{data.accounts.map(a=><tr key={a.id}><td><strong>{a.name}</strong></td><td><span className={`ownership-badge ${a.ownership||'unassigned'}`}>{accountOwnerships[a.ownership]||accountOwnerships.unassigned}</span><small className="account-owner-copy">{accountOwnerLabel(data,a)}</small></td><td><span className="account-function-badge">{accountFunctionLabel(a)}</span></td><td className="actions"><button onClick={()=>onEdit(a)}>Bearbeiten</button><button className="danger" onClick={()=>onDelete(a.id)}>Löschen</button></td></tr>)}</tbody></table></div>}</section>
|
return <div className="content-stack master-data-page">
|
||||||
<section className="panel"><div className="section-head"><div><h2>Personen</h2><p>Personen können Konten besitzen und einzelnen Bewegungen gemeinsam zugeordnet werden.</p></div><button className="secondary" onClick={onAddPerson}>+ Person</button></div>{unassigned>0&&<div className="person-unassigned-note"><strong>{unassigned} bestehende {unassigned===1?'Bewegung ist':'Bewegungen sind'} noch ohne Person.</strong><span>Diese Altbestände bleiben erhalten und können beim Bearbeiten zugeordnet werden. Neue Bewegungen benötigen mindestens eine Person.</span></div>}{(data.persons||[]).length===0?<Empty text="Noch keine Personen angelegt."/>:<div className="category-manager-list">{data.persons.map(person=><div className="category-manager-row" key={person.id}><div><strong>{person.name}</strong><span>{personUsage(person.id)} {personUsage(person.id)===1?'Bewegung':'Bewegungen'} · {ownedAccounts(person.id)} {ownedAccounts(person.id)===1?'Konto':'Konten'}</span></div><div className="actions"><button onClick={()=>onEditPerson(person)}>Bearbeiten</button><button className="danger" onClick={()=>onDeletePerson(person.id)}>Löschen</button></div></div>)}</div>}</section>
|
<section className="panel master-data-header"><div className="section-head"><div><h2>Stammdaten</h2><p>Hier definierst du die Bausteine, auf die Bewegungen und Auswertungen zugreifen. Änderungen wirken zentral in der gesamten Planung.</p></div><div className="master-data-counts"><span>{data.accounts.length} Konten</span><span>{(data.persons||[]).length} Personen</span><span>{data.categories.length} Kategorien</span></div></div><SectionTabs items={tabs} value={section} onChange={setSection} label="Stammdaten auswählen"/></section>
|
||||||
|
{section==='accounts'&&<section className="panel"><div className="section-head"><div><h2>Konten</h2><p>Eigentum bestimmt, wem Geld wirtschaftlich gehört. Die Kontofunktion bestimmt, ob eine Umbuchung z. B. Rücklage, Investment oder Bausparen ist.</p></div><button className="secondary" onClick={onAddAccount}>+ Konto</button></div>{unconfiguredAccounts>0&&<div className="account-config-note"><strong>{unconfiguredAccounts} {unconfiguredAccounts===1?'Konto braucht':'Konten brauchen'} noch eine Eigentumszuordnung.</strong><span>Bestehende Konten wurden absichtlich nicht automatisch geraten. Bitte einmal bearbeiten, Eigentum festlegen und die passende Kontofunktion wählen.</span></div>}{data.accounts.length===0?<Empty text="Noch keine Konten vorhanden."/>:<div className="table-wrap account-manager-table"><table><thead><tr><th>Konto</th><th>Eigentum</th><th>Funktion</th><th></th></tr></thead><tbody>{data.accounts.map(a=><tr key={a.id}><td><strong>{a.name}</strong></td><td><span className={`ownership-badge ${a.ownership||'unassigned'}`}>{accountOwnerships[a.ownership]||accountOwnerships.unassigned}</span><small className="account-owner-copy">{accountOwnerLabel(data,a)}</small></td><td><span className="account-function-badge">{accountFunctionLabel(a)}</span></td><td className="actions"><button onClick={()=>onEditAccount(a)}>Bearbeiten</button><button className="danger" onClick={()=>onDeleteAccount(a.id)}>Löschen</button></td></tr>)}</tbody></table></div>}</section>}
|
||||||
|
{section==='persons'&&<section className="panel"><div className="section-head"><div><h2>Personen</h2><p>Personen können Konten besitzen und einzelnen Bewegungen gemeinsam zugeordnet werden.</p></div><button className="secondary" onClick={onAddPerson}>+ Person</button></div>{unassigned>0&&<div className="person-unassigned-note"><strong>{unassigned} bestehende {unassigned===1?'Bewegung ist':'Bewegungen sind'} noch ohne Person.</strong><span>Diese Altbestände bleiben erhalten und können beim Bearbeiten zugeordnet werden. Neue Bewegungen benötigen mindestens eine Person.</span></div>}{(data.persons||[]).length===0?<Empty text="Noch keine Personen angelegt."/>:<div className="category-manager-list">{data.persons.map(person=><div className="category-manager-row" key={person.id}><div><strong>{person.name}</strong><span>{personUsage(person.id)} {personUsage(person.id)===1?'Bewegung':'Bewegungen'} · {ownedAccounts(person.id)} {ownedAccounts(person.id)===1?'Konto':'Konten'}</span></div><div className="actions"><button onClick={()=>onEditPerson(person)}>Bearbeiten</button><button className="danger" onClick={()=>onDeletePerson(person.id)}>Löschen</button></div></div>)}</div>}</section>}
|
||||||
|
{section==='categories'&&<section className="panel"><div className="section-head"><div><h2>Kategorien</h2><p>Kategorien gelten zentral für Eingänge, Ausgänge und Transfers. Die Statistik wertet sie aus, verwaltet werden sie ausschließlich hier.</p></div><button className="secondary" onClick={onAddCategory}>+ Kategorie</button></div>{data.categories.length===0?<Empty text="Noch keine Kategorien angelegt."/>:<div className="category-manager-list">{data.categories.map(c=><div className="category-manager-row" key={c.id}><div><strong>{c.name}</strong><span>{categoryUsage(c.id)} {categoryUsage(c.id)===1?'zugeordneter Eintrag':'zugeordnete Einträge'}</span></div><div className="actions"><button onClick={()=>onEditCategory(c)}>Bearbeiten</button><button className="danger" onClick={()=>onDeleteCategory(c.id)}>Löschen</button></div></div>)}</div>}</section>}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function MovementsPage({data,onEdit,onDuplicate,onDelete,onAdd}){
|
function MovementsPage({data,onEdit,onDuplicate,onDelete,onAdd}){
|
||||||
const [section,setSection]=useState('incomes');
|
const [section,setSection]=useState('incomes');
|
||||||
const tabs=[['incomes','Eingänge',data.incomes.length],['expenses','Ausgänge',data.expenses.length],['transfers','Transfers',data.transfers.length]];
|
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 isIncome=type==='incomes';
|
||||||
const dueText=x=>x.interval==='monthly'?'Jeden Monat':x.dueMonth?months[Number(x.dueMonth)-1]:'Nicht festgelegt';
|
const dueText=x=>x.interval==='monthly'?'Jeden Monat':x.dueMonth?months[Number(x.dueMonth)-1]:'Nicht festgelegt';
|
||||||
const currentAmount=x=>effectiveAmount(x);
|
const currentAmount=x=>effectiveAmount(x);
|
||||||
return <section className="panel movement-list-panel"><div className="section-head"><div><h2>{isIncome?'Fixe Eingänge':'Fixe Ausgänge'}</h2><p>Betrag gilt pro gewähltem Intervall. Periodische Werte werden zusätzlich als Monatsanteil gezeigt.</p></div></div>{items.length===0?<Empty text={`Noch keine ${isIncome?'Eingänge':'Ausgänge'} vorhanden.`}/>:<>
|
return <section className="panel movement-list-panel"><div className="section-head"><div><h2>{isIncome?'Fixe Eingänge':'Fixe Ausgänge'}</h2><p>Nur Basisinfos bleiben sichtbar. Details und Aktionen öffnen sich je Bewegung per Akkordeon.</p></div></div>{items.length===0?<Empty text={`Noch keine ${isIncome?'Eingänge':'Ausgänge'} vorhanden.`}/>:<div className="movement-accordion-list">{items.map(x=><details className="movement-accordion" key={x.id}>
|
||||||
<div className="table-wrap movement-desktop-table"><table><thead><tr><th>Bezeichnung</th><th>Personen</th><th>Kategorie</th><th>Konto</th><th>Intervall</th><th>Monat</th><th className="number">Betrag</th><th className="number">Ø / Monat</th><th></th></tr></thead><tbody>{items.map(x=><tr key={x.id}><td><strong>{x.name}</strong>{!isIncome&&x.pushoverReminder&&<span className="reminder-badge">Reminder</span>}{(x.amountChanges||[]).length>0&&<span className="change-badge">{x.amountChanges.length} Änderung{x.amountChanges.length===1?'':'en'}</span>}{x.endDate&&<span className="end-badge">bis {formatDate(x.endDate)}</span>}</td><td><span className={`person-badge ${personIdsOf(x).length?'':'muted-person'}`}>{personLabelLocal(x)}</span></td><td><span className={`category-badge ${x.categoryId?'':'muted-category'}`}>{categoryNameLocal(x.categoryId)}</span></td><td>{accountNameLocal(x.accountId)}</td><td><span className="interval-badge">{intervals[x.interval||'monthly'].label}</span></td><td>{dueText(x)}</td><td className={`number ${isIncome?'good-text':'bad-text'}`}>{isIncome?'+':'−'}{formatMoney(currentAmount(x))}</td><td className="number">{formatMoney(monthlyEquivalent(x))}</td><td className="actions"><button onClick={()=>onDuplicate(x)}>Duplizieren</button><button onClick={()=>onEdit(x)}>Bearbeiten</button><button className="danger" onClick={()=>onDelete(type,x.id)}>Löschen</button></td></tr>)}</tbody></table></div>
|
<summary><div className="movement-summary-main"><div className="movement-summary-copy"><strong>{x.name}</strong><div className="compact-badges"><span className={`category-badge ${x.categoryId?'':'muted-category'}`}>{categoryNameLocal(x.categoryId)}</span><span className={`person-badge ${personIdsOf(x).length?'':'muted-person'}`}>{personLabelLocal(x)}</span><span className="account-badge">{accountNameLocal(x.accountId)}</span><span className="interval-badge">{intervals[x.interval||'monthly'].label}</span>{x.interval!=='monthly'&&<span className="due-badge">{dueText(x)}</span>}{!isIncome&&x.pushoverReminder&&<span className="reminder-badge">Reminder</span>}{(x.amountChanges||[]).length>0&&<span className="change-badge">{x.amountChanges.length} geplant</span>}{x.endDate&&<span className="end-badge">bis {formatDate(x.endDate)}</span>}</div></div><div className="movement-summary-value"><strong className={isIncome?'good-text':'bad-text'}>{isIncome?'+':'−'}{formatMoney(currentAmount(x))}</strong><small>{formatMoney(monthlyEquivalent(x))} / Monat</small></div><span className="accordion-chevron" aria-hidden="true">⌄</span></div></summary>
|
||||||
<div className="movement-mobile-list">{items.map(x=><article className="movement-card" key={x.id}>
|
<div className="movement-accordion-body"><div className="movement-detail-grid"><div><span>Personen</span><strong>{personLabelLocal(x)}</strong></div><div><span>Konto</span><strong>{accountNameLocal(x.accountId)}</strong></div><div><span>Kategorie</span><strong>{categoryNameLocal(x.categoryId)}</strong></div><div><span>Intervall</span><strong>{intervals[x.interval||'monthly'].label}</strong></div><div><span>Fälligkeit</span><strong>{dueText(x)}</strong></div><div><span>Ø / Monat</span><strong>{formatMoney(monthlyEquivalent(x))}</strong></div>{x.endDate&&<div><span>Enddatum</span><strong>{formatDate(x.endDate)}</strong></div>}{!isIncome&&x.pushoverReminder&&<div><span>Erinnerung</span><strong>Am 1. des Fälligkeitsmonats</strong></div>}</div>{(x.amountChanges||[]).length>0&&<div className="movement-change-preview"><span>Geplante Betragsänderungen</span>{x.amountChanges.map(c=><div key={c.id||c.effectiveFrom}><strong>ab {formatDate(c.effectiveFrom)}</strong><span>{formatMoney(c.amount)}</span></div>)}</div>}<div className="movement-card-actions"><button onClick={()=>onDuplicate(x)}>Duplizieren</button><button onClick={()=>onEdit(x)}>Bearbeiten</button><button className="danger" onClick={()=>onDelete(type,x.id)}>Löschen</button></div></div>
|
||||||
<div className="movement-card-head"><div><strong>{x.name}</strong><span className={`category-badge ${x.categoryId?'':'muted-category'}`}>{categoryNameLocal(x.categoryId)}</span>{!isIncome&&x.pushoverReminder&&<span className="reminder-badge">Reminder am 1.</span>}{(x.amountChanges||[]).length>0&&<span className="change-badge">{x.amountChanges.length} geplant</span>}{x.endDate&&<span className="end-badge">bis {formatDate(x.endDate)}</span>}</div><strong className={isIncome?'good-text':'bad-text'}>{isIncome?'+':'−'}{formatMoney(currentAmount(x))}</strong></div>
|
</details>)}</div>}</section>;
|
||||||
<div className="movement-card-grid">
|
|
||||||
<div><span>Personen</span><strong>{personLabelLocal(x)}</strong></div>
|
|
||||||
<div><span>Konto</span><strong>{accountNameLocal(x.accountId)}</strong></div>
|
|
||||||
<div><span>Intervall</span><strong>{intervals[x.interval||'monthly'].label}</strong></div>
|
|
||||||
<div><span>Fälligkeit</span><strong>{dueText(x)}</strong></div>
|
|
||||||
<div><span>Ø / Monat</span><strong>{formatMoney(monthlyEquivalent(x))}</strong></div>
|
|
||||||
</div>
|
|
||||||
<div className="movement-card-actions"><button onClick={()=>onDuplicate(x)}>Duplizieren</button><button onClick={()=>onEdit(x)}>Bearbeiten</button><button className="danger" onClick={()=>onDelete(type,x.id)}>Löschen</button></div>
|
|
||||||
</article>)}</div>
|
|
||||||
</>}</section>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Transfers({data,onEdit,onDuplicate,onDelete}){
|
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 categoryNameLocal=id=>data.categories.find(c=>c.id===id)?.name||'Ohne Kategorie';
|
||||||
const personLabelLocal=item=>transferPeopleLabel(data,item);
|
const personLabelLocal=item=>transferPeopleLabel(data,item);
|
||||||
const dueText=x=>x.interval==='monthly'?'Jeden Monat':x.dueMonth?months[Number(x.dueMonth)-1]:'Nicht festgelegt';
|
const dueText=x=>x.interval==='monthly'?'Jeden Monat':x.dueMonth?months[Number(x.dueMonth)-1]:'Nicht festgelegt';
|
||||||
return <section className="panel movement-list-panel"><div className="section-head"><div><h2>Fixe Transfers</h2><p>Transfers werden automatisch anhand von Eigentum und Kontofunktion klassifiziert. Sie beeinflussen Kontoliquidität, sind aber keine Konsumausgaben.</p></div></div>{data.transfers.length===0?<Empty text="Noch keine Transfers vorhanden."/>:<>
|
return <section className="panel movement-list-panel"><div className="section-head"><div><h2>Fixe Transfers</h2><p>Quelle, Ziel, Betrag und Einordnung bleiben direkt sichtbar. Weitere Details und Aktionen liegen im Akkordeon.</p></div></div>{data.transfers.length===0?<Empty text="Noch keine Transfers vorhanden."/>:<div className="movement-accordion-list">{data.transfers.map(x=>{const cls=transferClassification(data,x);return <details className="movement-accordion transfer-accordion" key={x.id}>
|
||||||
<div className="table-wrap movement-desktop-table"><table><thead><tr><th>Bezeichnung</th><th>Typ</th><th>Personen</th><th>Kategorie</th><th>Von</th><th>Nach</th><th>Intervall</th><th className="number">Ø / Monat</th><th></th></tr></thead><tbody>{data.transfers.map(x=>{const cls=transferClassification(data,x);return <tr key={x.id}><td><strong>{x.name}</strong>{x.endDate&&<span className="end-badge">bis {formatDate(x.endDate)}</span>}</td><td><span className={`transfer-class-badge ${cls.key}`}>{cls.label}</span></td><td><span className={`person-badge ${personIdsOf(x).length?'':'muted-person'}`}>{personLabelLocal(x)}</span></td><td><span className={`category-badge ${x.categoryId?'':'muted-category'}`}>{categoryNameLocal(x.categoryId)}</span></td><td>{accountNameLocal(x.fromAccountId)}</td><td>{accountNameLocal(x.toAccountId)}</td><td><span className="interval-badge">{intervals[x.interval||'monthly'].label}</span></td><td className="number">{formatMoney(monthlyEquivalent(x))}</td><td className="actions"><button onClick={()=>onDuplicate(x)}>Duplizieren</button><button onClick={()=>onEdit(x)}>Bearbeiten</button><button className="danger" onClick={()=>onDelete('transfers',x.id)}>Löschen</button></td></tr>})}</tbody></table></div>
|
<summary><div className="movement-summary-main"><div className="movement-summary-copy"><strong>{x.name}</strong><span className="movement-summary-route">{accountNameLocal(x.fromAccountId)} <b>→</b> {accountNameLocal(x.toAccountId)}</span><div className="compact-badges"><span className={`transfer-class-badge ${cls.key}`}>{cls.label}</span><span className={`person-badge ${cls.key==='person-transfer'||personIdsOf(x).length?'':'muted-person'}`}>{personLabelLocal(x)}</span><span className={`category-badge ${x.categoryId?'':'muted-category'}`}>{categoryNameLocal(x.categoryId)}</span><span className="interval-badge">{intervals[x.interval||'monthly'].label}</span>{x.interval!=='monthly'&&<span className="due-badge">{dueText(x)}</span>}{x.endDate&&<span className="end-badge">bis {formatDate(x.endDate)}</span>}</div></div><div className="movement-summary-value"><strong>{formatMoney(effectiveAmount(x))}</strong><small>{formatMoney(monthlyEquivalent(x))} / Monat</small></div><span className="accordion-chevron" aria-hidden="true">⌄</span></div></summary>
|
||||||
<div className="movement-mobile-list">{data.transfers.map(x=>{const cls=transferClassification(data,x);return <article className="movement-card transfer-card" key={x.id}>
|
<div className="movement-accordion-body"><div className="movement-detail-grid"><div><span>Von</span><strong>{accountNameLocal(x.fromAccountId)}</strong></div><div><span>Nach</span><strong>{accountNameLocal(x.toAccountId)}</strong></div><div><span>Personen</span><strong>{personLabelLocal(x)}</strong></div><div><span>Einordnung</span><strong>{cls.description}</strong></div><div><span>Kategorie</span><strong>{categoryNameLocal(x.categoryId)}</strong></div><div><span>Intervall</span><strong>{intervals[x.interval||'monthly'].label}</strong></div><div><span>Fälligkeit</span><strong>{dueText(x)}</strong></div><div><span>Ø / Monat</span><strong>{formatMoney(monthlyEquivalent(x))}</strong></div>{x.endDate&&<div><span>Enddatum</span><strong>{formatDate(x.endDate)}</strong></div>}</div><div className="movement-card-actions"><button onClick={()=>onDuplicate(x)}>Duplizieren</button><button onClick={()=>onEdit(x)}>Bearbeiten</button><button className="danger" onClick={()=>onDelete('transfers',x.id)}>Löschen</button></div></div>
|
||||||
<div className="movement-card-head"><div><strong>{x.name}</strong><span className={`transfer-class-badge ${cls.key}`}>{cls.label}</span><span className={`category-badge ${x.categoryId?'':'muted-category'}`}>{categoryNameLocal(x.categoryId)}</span>{x.endDate&&<span className="end-badge">bis {formatDate(x.endDate)}</span>}</div><strong>{formatMoney(x.amount)}</strong></div>
|
</details>})}</div>}</section>;
|
||||||
<div className="transfer-route"><div><span>Von</span><strong>{accountNameLocal(x.fromAccountId)}</strong></div><span className="route-arrow">→</span><div><span>Nach</span><strong>{accountNameLocal(x.toAccountId)}</strong></div></div>
|
|
||||||
<div className="movement-card-grid"><div><span>Personen</span><strong>{personLabelLocal(x)}</strong></div><div><span>Einordnung</span><strong>{cls.description}</strong></div><div><span>Intervall</span><strong>{intervals[x.interval||'monthly'].label}</strong></div><div><span>Ø / Monat</span><strong>{formatMoney(monthlyEquivalent(x))}</strong></div></div>
|
|
||||||
<div className="movement-card-actions"><button onClick={()=>onDuplicate(x)}>Duplizieren</button><button onClick={()=>onEdit(x)}>Bearbeiten</button><button className="danger" onClick={()=>onDelete('transfers',x.id)}>Löschen</button></div>
|
|
||||||
</article>})}</div>
|
|
||||||
</>}</section>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function transferTypeStats(data,accountId='all',personId='all'){
|
function transferTypeStats(data,accountId='all',personId='all'){
|
||||||
const isAll=accountId==='all';const buckets=new Map();
|
const isAll=accountId==='all';const buckets=new Map();
|
||||||
for(const item of data.transfers||[]){
|
for(const item of data.transfers||[]){
|
||||||
@@ -822,7 +816,7 @@ function CategoryDistribution({title,subtitle,stats,tone,data}){
|
|||||||
</section>;
|
</section>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatisticsPage({data,includeTransfers,onTransfersChange,onAddCategory,onEditCategory,onDeleteCategory}){
|
function StatisticsPage({data,includeTransfers,onTransfersChange}){
|
||||||
const [accountId,setAccountId]=useState('all');
|
const [accountId,setAccountId]=useState('all');
|
||||||
const [personId,setPersonId]=useState('all');
|
const [personId,setPersonId]=useState('all');
|
||||||
const stats=useMemo(()=>categoryStats(data,accountId,personId,includeTransfers),[data,accountId,personId,includeTransfers]);
|
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 kpis=useMemo(()=>dashboardKpis(data,totals),[data,totals]);
|
||||||
const selectedName=data.accounts.find(a=>a.id===accountId)?.name||'Alle Konten';
|
const selectedName=data.accounts.find(a=>a.id===accountId)?.name||'Alle Konten';
|
||||||
const selectedPerson=personId==='all'?'Alle Personen':personName(data,personId);
|
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 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 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 <div className="content-stack stats-page">
|
return <div className="content-stack stats-page">
|
||||||
<section className="panel stats-overview-panel"><div className="section-head"><div><h2>Gesamtkennzahlen</h2><p>Wirtschaftliche Kennzahlen verwenden nur echte Einnahmen und echte Ausgaben. Umbuchungen verändern diese Werte nicht.</p></div></div><div className="stats-kpi-grid"><Metric label="Wirtschaftlicher Saldo / Monat" value={totals.balance} positive={totals.balance>=0} negative={totals.balance<0} featured/><TextMetric label="Fixkostenquote" value={`${kpis.fixedCostRatio.toFixed(1).replace('.',',')} %`} hint="Echte Ausgänge ÷ echte Eingänge"/><Metric label="Echte Kosten / Jahr" value={kpis.annualExpense} negative/><Metric label="Rücklage periodisch / Monat" value={kpis.reserveMonthly}/><TextMetric label="Teuerster Monat" value={kpis.expensive?.name||'–'} hint={kpis.expensive?formatMoney(kpis.expensive.displayExpense):'–'}/><TextMetric label="Günstigster Monat" value={kpis.cheap?.name||'–'} hint={kpis.cheap?formatMoney(kpis.cheap.displayExpense):'–'}/></div></section>
|
<section className="panel stats-overview-panel"><div className="section-head"><div><h2>Gesamtkennzahlen</h2><p>Wirtschaftliche Kennzahlen verwenden nur echte Einnahmen und echte Ausgaben. Umbuchungen verändern diese Werte nicht.</p></div></div><div className="stats-kpi-grid"><Metric label="Wirtschaftlicher Saldo / Monat" value={totals.balance} positive={totals.balance>=0} negative={totals.balance<0} featured/><TextMetric label="Fixkostenquote" value={`${kpis.fixedCostRatio.toFixed(1).replace('.',',')} %`} hint="Echte Ausgänge ÷ echte Eingänge"/><Metric label="Echte Kosten / Jahr" value={kpis.annualExpense} negative/><Metric label="Rücklage periodisch / Monat" value={kpis.reserveMonthly}/><TextMetric label="Teuerster Monat" value={kpis.expensive?.name||'–'} hint={kpis.expensive?formatMoney(kpis.expensive.displayExpense):'–'}/><TextMetric label="Günstigster Monat" value={kpis.cheap?.name||'–'} hint={kpis.cheap?formatMoney(kpis.cheap.displayExpense):'–'}/></div></section>
|
||||||
<section className="year-account-filter stats-filter"><div className="year-filter-copy"><span>Statistik für</span><strong>{selectedName} · {selectedPerson}</strong></div><div className="stats-filter-controls"><label><span>Konto auswählen</span><select value={accountId} onChange={e=>setAccountId(e.target.value)}><option value="all">Alle Konten</option>{data.accounts.map(a=><option key={a.id} value={a.id}>{a.name}</option>)}</select></label><label><span>Person auswählen</span><select value={personId} onChange={e=>setPersonId(e.target.value)}><option value="all">Alle Personen</option>{(data.persons||[]).map(p=><option key={p.id} value={p.id}>{p.name}</option>)}</select></label></div></section>
|
<section className="year-account-filter stats-filter"><div className="year-filter-copy"><span>Statistik für</span><strong>{selectedName} · {selectedPerson}</strong></div><div className="stats-filter-controls"><label><span>Konto auswählen</span><select value={accountId} onChange={e=>setAccountId(e.target.value)}><option value="all">Alle Konten</option>{data.accounts.map(a=><option key={a.id} value={a.id}>{a.name}</option>)}</select></label><label><span>Person auswählen</span><select value={personId} onChange={e=>setPersonId(e.target.value)}><option value="all">Alle Personen</option>{(data.persons||[]).map(p=><option key={p.id} value={p.id}>{p.name}</option>)}</select></label></div></section>
|
||||||
<TransferToggle enabled={includeTransfers} onChange={onTransfersChange}/>
|
<TransferToggle enabled={includeTransfers} onChange={onTransfersChange}/>
|
||||||
<section className="summary-grid stats-summary"><Metric label={personalIncomeLabel} value={stats.incomes.total} positive/><Metric label="Echte Kosten Ø / Monat" value={stats.expenses.total} negative/><CountMetric label="Kategorien" value={data.categories.length}/><CountMetric label="Ohne Kategorie" value={uncategorized}/></section>
|
<section className="summary-grid stats-summary"><Metric label={personalIncomeLabel} value={stats.incomes.total} positive/><Metric label="Echte Kosten Ø / Monat" value={stats.expenses.total} negative/><Metric label={selectionBalanceLabel} value={selectionBalance} positive={selectionBalance>=0} negative={selectionBalance<0}/><CountMetric label="Ohne Kategorie" value={uncategorized}/></section>
|
||||||
<PersonFlowOverview stats={personFlows}/>
|
<PersonFlowOverview stats={personFlows}/>
|
||||||
{accountId==='all'&&<SharedFundingOverview data={data} personId={personId} showTransfers={includeTransfers}/>}
|
{accountId==='all'&&<SharedFundingOverview data={data} personId={personId} showTransfers={includeTransfers}/>}
|
||||||
{accountId==='all'&&<SharedIncomeBurdenOverview data={data} personId={personId} showTransfers={includeTransfers}/>}
|
{accountId==='all'&&<SharedIncomeBurdenOverview data={data} personId={personId} showTransfers={includeTransfers}/>}
|
||||||
@@ -846,7 +841,6 @@ function StatisticsPage({data,includeTransfers,onTransfersChange,onAddCategory,o
|
|||||||
<CategoryDistribution data={data} title="Kosten nach Kategorien" subtitle="Nur echte Ausgaben. Gemeinsam zugeordnete Kosten werden beim Personenfilter gleichmäßig geteilt; Transfers erscheinen hier niemals als Kosten." stats={stats.expenses} tone="expense"/>
|
<CategoryDistribution data={data} title="Kosten nach Kategorien" subtitle="Nur echte Ausgaben. Gemeinsam zugeordnete Kosten werden beim Personenfilter gleichmäßig geteilt; Transfers erscheinen hier niemals als Kosten." stats={stats.expenses} tone="expense"/>
|
||||||
<CategoryDistribution data={data} title={personId==='all'?'Eingänge nach Kategorien':'Eigene Eingänge nach Kategorien'} subtitle={personId==='all'?'Nur echte Eingänge. Transfers bleiben getrennt.':'Nur Eingänge auf persönlich zugeordneten Konten dieser Person. Gemeinsame Einnahmen wie Kindergeld werden nicht als persönliche Einnahmen gezählt.'} stats={stats.incomes} tone="income"/>
|
<CategoryDistribution data={data} title={personId==='all'?'Eingänge nach Kategorien':'Eigene Eingänge nach Kategorien'} subtitle={personId==='all'?'Nur echte Eingänge. Transfers bleiben getrennt.':'Nur Eingänge auf persönlich zugeordneten Konten dieser Person. Gemeinsame Einnahmen wie Kindergeld werden nicht als persönliche Einnahmen gezählt.'} stats={stats.incomes} tone="income"/>
|
||||||
{includeTransfers&&<CategoryDistribution data={data} title="Transfer-/Finanzierungsströme nach Kategorien" subtitle="Separat von Kosten und Einnahmen. Dazu gehören interne Umbuchungen, Vermögensaufbau und Gemeinschaftsfinanzierung." stats={stats.transfers} tone="transfer"/>}
|
{includeTransfers&&<CategoryDistribution data={data} title="Transfer-/Finanzierungsströme nach Kategorien" subtitle="Separat von Kosten und Einnahmen. Dazu gehören interne Umbuchungen, Vermögensaufbau und Gemeinschaftsfinanzierung." stats={stats.transfers} tone="transfer"/>}
|
||||||
<section className="panel"><div className="section-head"><div><h2>Kategorien verwalten</h2><p>Kategorien gelten für Eingänge, Ausgänge und Transfers gemeinsam.</p></div><button className="secondary" onClick={onAddCategory}>+ Kategorie</button></div>{data.categories.length===0?<Empty text="Noch keine Kategorien angelegt."/>:<div className="category-manager-list">{data.categories.map(c=><div className="category-manager-row" key={c.id}><div><strong>{c.name}</strong><span>{usage(c.id)} {usage(c.id)===1?'zugeordneter Eintrag':'zugeordnete Einträge'}</span></div><div className="actions"><button onClick={()=>onEditCategory(c)}>Bearbeiten</button><button className="danger" onClick={()=>onDeleteCategory(c.id)}>Löschen</button></div></div>)}</div>}</section>
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -995,7 +989,8 @@ function StatementsPage({data,includeTransfers,onTransfersChange}){
|
|||||||
return <div className="content-stack statements-page"><TransferToggle enabled={includeTransfers} onChange={onTransfersChange} context="export"/><section className="panel report-export-panel"><div className="section-head"><div><h2>Kontoauszug erstellen</h2><p>Nur die geplanten Bewegungen des gewählten Kontos. Keine Stammdaten, Szenarien oder Systemeinstellungen.</p></div><span className="report-badge">PDF · XLSX</span></div><div className="report-controls"><label><span>Jahr</span><select value={year} onChange={e=>setYear(Number(e.target.value))}>{Array.from({length:7},(_,i)=>currentYear-2+i).map(y=><option key={y} value={y}>{y}</option>)}</select></label><label><span>Konto</span><select value={accountId} onChange={e=>setAccountId(e.target.value)}>{data.accounts.map(a=><option key={a.id} value={a.id}>{a.name}</option>)}</select></label><label><span>Person</span><select value={personId} onChange={e=>setPersonId(e.target.value)}><option value="all">Alle Personen</option>{(data.persons||[]).map(p=><option key={p.id} value={p.id}>{p.name}</option>)}</select></label></div><div className="report-preview"><div><span>Kontoauszug</span><strong>{account} · {selectedPerson} · {year}</strong></div><p>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.</p></div><div className="button-row report-actions"><a className="primary button-link" href={`/api/report.pdf?${query}`}>PDF herunterladen</a><a className="secondary button-link" href={`/api/report.xlsx?${query}`}>Excel herunterladen</a></div><p className="report-disclaimer">Erstellt aus FixFin-Planungsdaten, kein von einer Bank ausgestellter Kontoauszug.</p></section></div>;
|
return <div className="content-stack statements-page"><TransferToggle enabled={includeTransfers} onChange={onTransfersChange} context="export"/><section className="panel report-export-panel"><div className="section-head"><div><h2>Kontoauszug erstellen</h2><p>Nur die geplanten Bewegungen des gewählten Kontos. Keine Stammdaten, Szenarien oder Systemeinstellungen.</p></div><span className="report-badge">PDF · XLSX</span></div><div className="report-controls"><label><span>Jahr</span><select value={year} onChange={e=>setYear(Number(e.target.value))}>{Array.from({length:7},(_,i)=>currentYear-2+i).map(y=><option key={y} value={y}>{y}</option>)}</select></label><label><span>Konto</span><select value={accountId} onChange={e=>setAccountId(e.target.value)}>{data.accounts.map(a=><option key={a.id} value={a.id}>{a.name}</option>)}</select></label><label><span>Person</span><select value={personId} onChange={e=>setPersonId(e.target.value)}><option value="all">Alle Personen</option>{(data.persons||[]).map(p=><option key={p.id} value={p.id}>{p.name}</option>)}</select></label></div><div className="report-preview"><div><span>Kontoauszug</span><strong>{account} · {selectedPerson} · {year}</strong></div><p>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.</p></div><div className="button-row report-actions"><a className="primary button-link" href={`/api/report.pdf?${query}`}>PDF herunterladen</a><a className="secondary button-link" href={`/api/report.xlsx?${query}`}>Excel herunterladen</a></div><p className="report-disclaimer">Erstellt aus FixFin-Planungsdaten, kein von einer Bank ausgestellter Kontoauszug.</p></section></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataPage({data,importFile}){
|
function SettingsDataPage({data,importFile}){
|
||||||
|
const [section,setSection]=useState('notifications');
|
||||||
const summary=[
|
const summary=[
|
||||||
['Konten',data.accounts.length],
|
['Konten',data.accounts.length],
|
||||||
['Kategorien',data.categories.length],
|
['Kategorien',data.categories.length],
|
||||||
@@ -1006,14 +1001,18 @@ function DataPage({data,importFile}){
|
|||||||
['Szenarien',(data.scenarios||[]).length],
|
['Szenarien',(data.scenarios||[]).length],
|
||||||
];
|
];
|
||||||
const json=JSON.stringify(data,null,2);
|
const json=JSON.stringify(data,null,2);
|
||||||
return <div className="content-stack data-page">
|
const tabs=[{id:'notifications',label:'Benachrichtigungen'},{id:'backup',label:'Backup & Daten'}];
|
||||||
<PushoverSettings/>
|
return <div className="content-stack data-page settings-data-page">
|
||||||
<section className="panel data-backup-panel"><div className="section-head"><div><h2>Daten sichern</h2><p>Alle Daten liegen in einer JSON-Datei. Vor jedem Speichern wird serverseitig automatisch eine <code>.bak</code>-Datei angelegt.</p></div></div><div className="button-row data-actions"><a className="primary button-link" href="/api/export">JSON herunterladen</a><label className="secondary button-link">JSON importieren<input type="file" accept="application/json,.json" hidden onChange={e=>e.target.files?.[0]&&importFile(e.target.files[0])}/></label></div></section>
|
<section className="panel settings-data-header"><div className="section-head"><div><h2>Einstellungen & Daten</h2><p>Systemfunktionen sind bewusst von den fachlichen Stammdaten getrennt.</p></div></div><SectionTabs items={tabs} value={section} onChange={setSection} label="Einstellungen und Daten auswählen"/></section>
|
||||||
<section className="panel data-status-panel"><div className="section-head"><div><h2>Aktueller Datenstand</h2><p>Kurzübersicht über den Inhalt deiner FixFin-Datei.</p></div></div>
|
{section==='notifications'&&<PushoverSettings/>}
|
||||||
<div className="data-summary-grid">{summary.map(([label,value])=><div className="data-summary-card" key={label}><span>{label}</span><strong>{Number(value).toLocaleString('de-DE')}</strong></div>)}</div>
|
{section==='backup'&&<>
|
||||||
<div className="data-json-desktop"><div className="data-subhead"><strong>JSON-Rohdaten</strong><span>Nur zur Kontrolle und Fehlersuche.</span></div><pre className="json-preview">{json}</pre></div>
|
<section className="panel data-backup-panel"><div className="section-head"><div><h2>Backup & Import</h2><p>Alle Planungsdaten liegen in einer JSON-Datei. Vor jedem Speichern wird serverseitig automatisch eine <code>.bak</code>-Datei angelegt.</p></div></div><div className="button-row data-actions"><a className="primary button-link" href="/api/export">JSON herunterladen</a><label className="secondary button-link">JSON importieren<input type="file" accept="application/json,.json" hidden onChange={e=>e.target.files?.[0]&&importFile(e.target.files[0])}/></label></div></section>
|
||||||
<details className="data-json-mobile"><summary><span>JSON-Rohdaten anzeigen</span><small>Nur bei Bedarf öffnen</small></summary><pre className="json-preview">{json}</pre></details>
|
<section className="panel data-status-panel"><div className="section-head"><div><h2>Aktueller Datenstand</h2><p>Kurzübersicht über den Inhalt deiner FixFin-Datei.</p></div></div>
|
||||||
</section>
|
<div className="data-summary-grid">{summary.map(([label,value])=><div className="data-summary-card" key={label}><span>{label}</span><strong>{Number(value).toLocaleString('de-DE')}</strong></div>)}</div>
|
||||||
|
<div className="data-json-desktop"><div className="data-subhead"><strong>JSON-Rohdaten</strong><span>Nur zur Kontrolle und Fehlersuche.</span></div><pre className="json-preview">{json}</pre></div>
|
||||||
|
<details className="data-json-mobile"><summary><span>JSON-Rohdaten anzeigen</span><small>Nur bei Bedarf öffnen</small></summary><pre className="json-preview">{json}</pre></details>
|
||||||
|
</section>
|
||||||
|
</>}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
function Empty({text}){return <div className="empty">{text}</div>}
|
function Empty({text}){return <div className="empty">{text}</div>}
|
||||||
|
|||||||
@@ -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,.shared-income-person-row.compact { grid-template-columns:1fr 1fr;gap:7px 10px; }
|
||||||
.shared-income-person-row > .person-badge { grid-column:1/-1; }
|
.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; }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user