v31
This commit is contained in:
+24
-2
@@ -139,8 +139,30 @@ function Entries({type,items,accounts,onEdit,onDelete}){const accountName=id=>ac
|
||||
function Transfers({data,onEdit,onDelete}){const accountName=id=>data.accounts.find(a=>a.id===id)?.name||'Unbekannt';return <section className="panel"><div className="section-head"><div><h2>Fixe Transfers</h2><p>Verschiebungen zwischen eigenen Konten. Global saldoneutral.</p></div></div>{data.transfers.length===0?<Empty text="Noch keine Transfers vorhanden."/>:<div className="table-wrap"><table><thead><tr><th>Bezeichnung</th><th>Von</th><th>Nach</th><th>Intervall</th><th>Monat</th><th className="number">Betrag</th><th className="number">Ø / Monat</th><th></th></tr></thead><tbody>{data.transfers.map(x=><tr key={x.id}><td><strong>{x.name}</strong></td><td>{accountName(x.fromAccountId)}</td><td>{accountName(x.toAccountId)}</td><td><span className="interval-badge">{intervals[x.interval||'monthly'].label}</span></td><td>{x.interval==='monthly'?'Jeden Monat':x.dueMonth?months[Number(x.dueMonth)-1]:'–'}</td><td className="number">{formatMoney(x.amount)}</td><td className="number">{formatMoney(monthlyEquivalent(x))}</td><td className="actions"><button onClick={()=>onEdit(x)}>Bearbeiten</button><button className="danger" onClick={()=>onDelete('transfers',x.id)}>Löschen</button></td></tr>)}</tbody></table></div>}</section>}
|
||||
|
||||
function YearPage({forecast}){
|
||||
return <div className="content-stack"><section className="summary-grid year-summary"><Metric label="Einnahmen / Jahr" value={forecast.yearIncome} positive/><Metric label="Ausgaben / Jahr" value={forecast.yearExpense} negative/><Metric label="Saldo / Jahr" value={forecast.yearIncome-forecast.yearExpense} positive={forecast.yearIncome>=forecast.yearExpense} negative={forecast.yearIncome<forecast.yearExpense}/><Metric label="Nicht zugeordnet" value={forecast.unassigned.length}/></section>
|
||||
<section className="panel"><div className="section-head"><div><h2>Jahresprognose</h2><p>Tatsächliche Fälligkeiten nach Monat. Transfers sind im Gesamtsaldo neutral.</p></div></div><div className="table-wrap"><table className="year-table"><thead><tr><th>Monat</th><th className="number">Eingänge</th><th className="number">Ausgänge</th><th className="number">Saldo</th><th>Fällige Positionen</th></tr></thead><tbody>{forecast.rows.map(r=><tr key={r.name}><td><strong>{r.name}</strong></td><td className="number good-text">+{formatMoney(r.income)}</td><td className="number bad-text">−{formatMoney(r.expense)}</td><td className={`number ${r.balance>=0?'good-text':'bad-text'}`}>{formatMoney(r.balance)}</td><td><div className="due-items">{r.items.length===0?<span className="muted">Keine</span>:r.items.map((x,i)=><span key={`${x.id}-${i}`} className={`due-chip ${x.kind}`}>{x.name} · {formatMoney(x.amount)}</span>)}</div></td></tr>)}</tbody></table></div></section>
|
||||
const kindLabel={income:'Eingang',expense:'Ausgang',transfer:'Transfer'};
|
||||
const kindSign={income:'+',expense:'−',transfer:''};
|
||||
return <div className="content-stack year-page">
|
||||
<section className="summary-grid year-summary"><Metric label="Einnahmen / Jahr" value={forecast.yearIncome} positive/><Metric label="Ausgaben / Jahr" value={forecast.yearExpense} negative/><Metric label="Saldo / Jahr" value={forecast.yearIncome-forecast.yearExpense} positive={forecast.yearIncome>=forecast.yearExpense} negative={forecast.yearIncome<forecast.yearExpense}/><Metric label="Nicht zugeordnet" value={forecast.unassigned.length}/></section>
|
||||
<section className="panel year-forecast-panel"><div className="section-head"><div><h2>Jahresprognose</h2><p>Monatliche Basiswerte auf einen Blick. Details bei Bedarf ausklappen.</p></div></div>
|
||||
<div className="year-month-list">{forecast.rows.map(r=><details className="year-month-card" key={r.name}>
|
||||
<summary>
|
||||
<div className="year-month-heading"><strong>{r.name}</strong><span>{r.items.length===0?'Keine Fälligkeiten':`${r.items.length} ${r.items.length===1?'Position':'Positionen'}`}</span></div>
|
||||
<div className="year-month-basics">
|
||||
<div><span>Eingänge</span><strong className="good-text">+{formatMoney(r.income)}</strong></div>
|
||||
<div><span>Ausgänge</span><strong className="bad-text">−{formatMoney(r.expense)}</strong></div>
|
||||
<div className="year-month-balance"><span>Saldo</span><strong className={r.balance>=0?'good-text':'bad-text'}>{r.balance>=0?'+':''}{formatMoney(r.balance)}</strong></div>
|
||||
</div>
|
||||
<span className="accordion-chevron" aria-hidden="true">⌄</span>
|
||||
</summary>
|
||||
<div className="year-month-details">
|
||||
{r.items.length===0?<div className="year-detail-empty">In diesem Monat sind keine einzelnen Fixpositionen fällig.</div>:r.items.map((x,i)=><div className="year-detail-row" key={`${x.id}-${i}`}>
|
||||
<div className="year-detail-name"><span className={`year-kind ${x.kind}`}>{kindLabel[x.kind]}</span><strong>{x.name}</strong></div>
|
||||
<span className={`year-detail-amount ${x.kind==='income'?'good-text':x.kind==='expense'?'bad-text':''}`}>{kindSign[x.kind]}{formatMoney(x.amount)}</span>
|
||||
</div>)}
|
||||
{(r.transferIn>0||r.transferOut>0)&&<div className="year-transfer-note">Transfers in diesem Monat: {formatMoney(r.transferOut)}. Sie verändern den Gesamtsaldo nicht.</div>}
|
||||
</div>
|
||||
</details>)}</div>
|
||||
</section>
|
||||
{forecast.unassigned.length>0&&<section className="panel warning-panel"><div className="section-head"><div><h2>Periodische Positionen ohne Monat</h2><p>Diese Werte können in den Monats-Saldo eingerechnet werden, sind aber keiner konkreten Jahresprognose zugeordnet.</p></div></div><div className="due-items">{forecast.unassigned.map(x=><span className={`due-chip ${x.kind}`} key={`${x.kind}-${x.id}`}>{x.name} · {intervals[x.interval].label} · {formatMoney(x.amount)}</span>)}</div></section>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -148,3 +148,67 @@ code { color:#b8c3ff; }
|
||||
.warning-panel { border-color:#5a4e2b; }
|
||||
@media (max-width:760px) { .toggle-panel { align-items:flex-start; } .year-summary { grid-template-columns:1fr 1fr; } .mobile-nav { grid-template-columns:repeat(7,1fr); } }
|
||||
@media (max-width:430px) { .year-summary { grid-template-columns:1fr; } }
|
||||
|
||||
/* v1.3: mobile Jahresansicht als Akkordeon */
|
||||
.year-month-list { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; }
|
||||
.year-month-card { background:#0d131c; border:1px solid #29364a; border-radius:14px; overflow:hidden; }
|
||||
.year-month-card[open] { border-color:#46577a; background:#0f1621; }
|
||||
.year-month-card summary { list-style:none; cursor:pointer; display:grid; grid-template-columns:minmax(105px,.75fr) minmax(0,2fr) 22px; gap:16px; align-items:center; padding:15px 16px; user-select:none; }
|
||||
.year-month-card summary::-webkit-details-marker { display:none; }
|
||||
.year-month-heading { min-width:0; }
|
||||
.year-month-heading strong { display:block; font-size:15px; }
|
||||
.year-month-heading span { display:block; color:var(--muted); font-size:10px; margin-top:3px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.year-month-basics { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; min-width:0; }
|
||||
.year-month-basics div { min-width:0; }
|
||||
.year-month-basics span { display:block; color:#718096; font-size:9px; text-transform:uppercase; letter-spacing:.055em; font-weight:800; margin-bottom:3px; }
|
||||
.year-month-basics strong { display:block; font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.accordion-chevron { color:#8793a4; font-size:20px; line-height:1; text-align:center; transition:transform .18s ease; }
|
||||
.year-month-card[open] .accordion-chevron { transform:rotate(180deg); }
|
||||
.year-month-details { border-top:1px solid #202a39; padding:7px 16px 11px; }
|
||||
.year-detail-row { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:9px 0; border-bottom:1px solid #1e2937; }
|
||||
.year-detail-row:last-of-type { border-bottom:0; }
|
||||
.year-detail-name { display:flex; align-items:center; gap:8px; min-width:0; }
|
||||
.year-detail-name strong { font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.year-kind { flex:0 0 auto; border-radius:999px; padding:3px 6px; font-size:8px; font-weight:850; text-transform:uppercase; letter-spacing:.04em; background:#1c2635; border:1px solid #304057; color:#b8c5d7; }
|
||||
.year-kind.income { border-color:#245947; color:#7de0b5; background:#112c24; }
|
||||
.year-kind.expense { border-color:#67333b; color:#ff9aa2; background:#321a20; }
|
||||
.year-kind.transfer { border-color:#414d7d; color:#adb9ff; background:#1c2340; }
|
||||
.year-detail-amount { flex:0 0 auto; font-size:12px; font-weight:800; white-space:nowrap; }
|
||||
.year-detail-empty { color:var(--muted); font-size:12px; padding:10px 0 6px; }
|
||||
.year-transfer-note { color:#8793a4; font-size:10px; border-top:1px dashed #2d394b; padding-top:9px; margin-top:5px; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.year-month-list { grid-template-columns:1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.year-page { min-width:0; }
|
||||
.year-forecast-panel { padding:14px; }
|
||||
.year-month-list { gap:9px; }
|
||||
.year-month-card summary { grid-template-columns:1fr 22px; gap:9px 10px; padding:13px 12px; }
|
||||
.year-month-heading { grid-column:1; }
|
||||
.accordion-chevron { grid-column:2; grid-row:1; align-self:center; }
|
||||
.year-month-basics { grid-column:1 / -1; grid-row:2; gap:6px; }
|
||||
.year-month-basics div { background:#101823; border:1px solid #202c3c; border-radius:9px; padding:8px; }
|
||||
.year-month-basics strong { font-size:12px; }
|
||||
.year-month-basics span { font-size:8px; }
|
||||
.year-month-balance { background:#141c2c !important; border-color:#2b3850 !important; }
|
||||
.year-month-details { padding:5px 12px 9px; }
|
||||
.year-detail-row { padding:9px 1px; }
|
||||
.year-summary { grid-template-columns:1fr 1fr; gap:8px; }
|
||||
.year-summary .metric { min-height:82px; padding:12px; }
|
||||
.year-summary .metric strong { font-size:18px; }
|
||||
.mobile-nav { width:100vw; max-width:100vw; overflow-x:auto; overscroll-behavior-x:contain; scrollbar-width:none; }
|
||||
.mobile-nav::-webkit-scrollbar { display:none; }
|
||||
.mobile-nav button { min-width:50px; }
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.year-summary { grid-template-columns:1fr 1fr; }
|
||||
.year-month-heading strong { font-size:14px; }
|
||||
.year-detail-name strong { font-size:11px; }
|
||||
.year-kind { font-size:7px; padding:3px 5px; }
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.mobile-nav { grid-template-columns:repeat(7,minmax(50px,1fr)); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user