Add SQLite passive income dashboard with Excel import and deployment tools
This commit is contained in:
@@ -0,0 +1 @@
|
||||
:root{color-scheme:dark;--bg:#0b111c;--panel:#141e2c;--border:#293548;--text:#e7edf5;--muted:#a4b3c7;--green:#72e2b0;--red:#ff9696;--yellow:#f4c272}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:15px/1.6 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}a{color:var(--green);text-decoration:none}a:hover{text-decoration:underline}button,input,select,textarea{font:inherit}button,.button{background:var(--green);color:#10241d;border:1px solid transparent;border-radius:9px;padding:11px 17px;cursor:pointer;font-weight:650;display:inline-block;text-align:center}button:hover,.button:hover{background:#98edc7;text-decoration:none}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,summary:focus-visible{outline:3px solid var(--green);outline-offset:3px}.topbar{border-bottom:1px solid var(--border);padding:20px max(24px,calc((100vw - 1440px)/2));display:flex;justify-content:space-between;gap:20px;align-items:center}.brand{font-size:20px;font-weight:700;color:var(--text)}nav{display:flex;gap:24px;flex-wrap:wrap}main{max-width:1488px;margin:auto;padding:32px 24px}h1{font-size:clamp(25px,4vw,34px);line-height:1.2;letter-spacing:-.03em;margin:8px 0 12px}h2{font-size:18px;line-height:1.4;margin:0 0 10px}p{margin:8px 0 18px}.eyebrow{font-size:12px;letter-spacing:.14em;font-weight:700;color:var(--green);margin-bottom:8px}.page-heading,.section-heading{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-bottom:24px}.muted,small{color:var(--muted)}.positive{color:var(--green)}.negative{color:var(--red)}.warning{color:var(--yellow)}.kpi-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px;margin-bottom:24px}.kpi,.panel{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:24px}.kpi h2{font-size:13px;font-weight:500;color:var(--muted)}.kpi strong{display:block;font-size:clamp(23px,2.5vw,30px);font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.kpi small{display:block;margin-top:9px;font-size:12px}.panel{margin-bottom:24px;min-width:0}.table-scroll{overflow:auto;max-width:100%}table{width:100%;border-collapse:collapse;text-align:left;font-size:14px}th,td{padding:13px 12px;border-bottom:1px solid var(--border)}th{color:var(--muted);font-weight:600}tbody tr:hover{background:#ffffff03}.numeric,.comparison td,.comparison th:not(:first-child){text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}.comparison th:first-child{position:sticky;left:0;background:var(--panel)}tfoot{font-weight:700}.chart-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 24px}.chart-wide{grid-column:1/-1}.chart{position:relative;height:300px}.amount-list{list-style:none;padding:0}.amount-list li{display:flex;justify-content:space-between;gap:20px;border-bottom:1px solid var(--border);padding:8px 0}.amount-list strong{white-space:nowrap}.notice{padding:16px 20px;border:1px solid currentColor;border-radius:10px;margin-bottom:24px}.success{color:var(--green)}.form-panel{max-width:650px;margin:0 auto 24px}.entry-form{display:grid;gap:18px}label{display:grid;gap:6px;font-size:14px}input,select,textarea{width:100%;padding:11px 12px;border:1px solid #43516a;border-radius:8px;background:#0c1420;color:var(--text);min-width:0}textarea{resize:vertical}.checkbox{display:flex;align-items:center;gap:12px}.checkbox input{width:18px;height:18px;accent-color:var(--green)}.actions{display:flex;align-items:center;gap:14px}.actions form{margin:0}.danger{color:var(--red);background:transparent;border-color:#754343}.danger:hover{background:#45272d}.small{font-size:13px;padding:5px 9px}.nowrap,.badge{white-space:nowrap}.badge{font-size:12px}.note{min-width:120px;max-width:300px;overflow-wrap:anywhere;white-space:pre-wrap}.empty{text-align:center;color:var(--muted);padding:35px}.filters{display:flex;align-items:end;flex-wrap:wrap;gap:16px;margin-bottom:24px}.filters label{flex:1;min-width:140px}.pagination{display:flex;justify-content:center;gap:24px;flex-wrap:wrap;margin-top:22px;color:var(--muted)}footer{max-width:1488px;padding:0 24px 24px;margin:auto;color:var(--muted);font-size:12px;display:flex;justify-content:space-between;gap:16px}summary{cursor:pointer;color:var(--green);margin-top:15px}@media(max-width:1000px){.kpi-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:650px){.topbar{align-items:flex-start;flex-direction:column;padding:18px}nav{gap:18px;font-size:14px}main{padding:24px 14px}.page-heading,.section-heading{align-items:flex-start;flex-direction:column}.page-heading .button{width:100%}.kpi-grid{gap:10px}.kpi{padding:16px 12px}.kpi strong{font-size:23px}.panel{padding:18px 12px}.chart-grid{grid-template-columns:1fr}.chart{height:280px}.section-heading{gap:5px}footer{flex-direction:column}.filters{gap:12px}.actions{gap:10px}.comparison th:first-child{min-width:100px}}
|
||||
@@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
(() => {
|
||||
const status = document.getElementById('chart-status');
|
||||
if (typeof Chart === 'undefined') {
|
||||
status.textContent = 'Chart.js konnte nicht vom CDN geladen werden. Alle Werte bleiben in den Tabellen verfügbar.';
|
||||
return;
|
||||
}
|
||||
const data = JSON.parse(document.getElementById('chart-data').textContent);
|
||||
const euro = cents => new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(cents / 100);
|
||||
const palette = ['#72e2b0','#79b6ff','#f4c272','#bc9cfa','#f691aa','#67cbd0','#e5db89','#9ec28b','#b2bdec','#daaa81'];
|
||||
Chart.defaults.color = '#a4b3c7';
|
||||
Chart.defaults.borderColor = '#293548';
|
||||
Chart.defaults.font.family = 'system-ui, sans-serif';
|
||||
new Chart(document.getElementById('monthly-chart'), {
|
||||
type: 'line',
|
||||
data: {labels: data.months, datasets: data.years.map((year, i) => ({...year, borderColor: palette[i % palette.length], backgroundColor: palette[i % palette.length], tension: 0.2, pointRadius: 3}))},
|
||||
options: {responsive:true, maintainAspectRatio:false, animation:false, interaction:{mode:'index',intersect:false},
|
||||
scales:{y:{ticks:{callback:euro},title:{display:true,text:'Euro'}}}, plugins:{tooltip:{callbacks:{label:ctx => `${ctx.dataset.label}: ${euro(ctx.raw)}`}}}}
|
||||
});
|
||||
function shares(id, rows) {
|
||||
// Signed corrections cannot be represented truthfully as pie slices.
|
||||
const negative = rows.some(row => row.amount < 0);
|
||||
const total = rows.reduce((sum,row) => sum + row.amount, 0);
|
||||
new Chart(document.getElementById(id), {
|
||||
type: negative ? 'bar' : 'doughnut',
|
||||
data:{labels:rows.map(row => row.name),datasets:[{data:rows.map(row => row.amount),backgroundColor:rows.map((_,i) => palette[i % palette.length]),borderWidth:0}]},
|
||||
options:{responsive:true,maintainAspectRatio:false,animation:false,
|
||||
...(negative ? {scales:{y:{ticks:{callback:euro}}}} : {cutout:'68%'}),
|
||||
plugins:{legend:{display:!negative,position:'bottom',labels:{boxWidth:10,font:{size:11}}},tooltip:{callbacks:{label:ctx => {
|
||||
const amount = rows[ctx.dataIndex].amount;
|
||||
const share = total === 0 ? '–' : new Intl.NumberFormat('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}).format(amount / total * 100) + ' %';
|
||||
return `${ctx.label}: ${euro(amount)} (${share})`;
|
||||
}}}}}
|
||||
});
|
||||
}
|
||||
shares('shares-chart', data.shares);
|
||||
shares('kinds-chart', data.kinds);
|
||||
status.textContent = data.years.some(year => year.data.some(value => value !== 0)) ? 'Diagramme zeigen tatsächlich erhaltene Einnahmen. Negative Positionssummen werden als Balken dargestellt.' : 'Noch keine tatsächlichen Einnahmen vorhanden.';
|
||||
})();
|
||||
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
document.querySelectorAll('form[data-confirm]').forEach(form => {
|
||||
form.addEventListener('submit', event => {
|
||||
if (!window.confirm(form.dataset.confirm)) event.preventDefault();
|
||||
});
|
||||
});
|
||||
const filters = document.getElementById('income-filters');
|
||||
if (filters) filters.addEventListener('submit', () => {
|
||||
filters.querySelectorAll('select').forEach(select => { if (!select.value) select.disabled = true; });
|
||||
});
|
||||
Reference in New Issue
Block a user