Add SQLite passive income dashboard with Excel import and deployment tools

This commit is contained in:
kai
2026-09-09 08:46:27 +02:00
parent 729a13bfca
commit afc6f74f36
27 changed files with 1109 additions and 67 deletions
+39
View File
@@ -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.';
})();
+10
View File
@@ -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; });
});