This commit is contained in:
2026-08-19 21:51:24 +02:00
parent b4d260421b
commit 1f50eeb429
4 changed files with 90 additions and 74 deletions
+11 -11
View File
@@ -26,7 +26,7 @@ function personIdsOf(item){return Array.isArray(item?.personIds)?item.personIds.
function personLabel(data,item){const ids=personIdsOf(item);return ids.length?ids.map(id=>personName(data,id)).join(', '):'Ohne Person';}
function personFactor(item,personId='all'){if(personId==='all')return 1;const ids=personIdsOf(item);return ids.includes(personId)&&ids.length?1/ids.length:0;}
export function buildAnnualForecast(data,year,accountId,personId='all'){
export function buildAnnualForecast(data,year,accountId,personId='all',includeTransfers=true){
const rows=MONTHS.map((name,index)=>({name,index,income:0,expense:0,transferIn:0,transferOut:0,displayIncome:0,displayExpense:0,balance:0,items:[]}));
for(const item of data.incomes||[]){
const factor=personFactor(item,personId); if(!factor||item.accountId!==accountId)continue;
@@ -36,7 +36,7 @@ export function buildAnnualForecast(data,year,accountId,personId='all'){
const factor=personFactor(item,personId); if(!factor||item.accountId!==accountId)continue;
for(let m=0;m<12;m++)if(occursInMonth(item,year,m)){const amount=effectiveAmount(item,year,m)*factor;rows[m].expense+=amount;rows[m].items.push({...item,kind:'expense',reportAmount:amount});}
}
for(const item of data.transfers||[]){
if(includeTransfers) for(const item of data.transfers||[]){
const factor=personFactor(item,personId); if(!factor||(item.fromAccountId!==accountId&&item.toAccountId!==accountId))continue;
for(let m=0;m<12;m++)if(occursInMonth(item,year,m)){
const amount=Number(item.amount||0)*factor;
@@ -44,11 +44,11 @@ export function buildAnnualForecast(data,year,accountId,personId='all'){
if(item.fromAccountId===accountId){rows[m].transferOut+=amount;rows[m].items.push({...item,kind:'transfer',direction:'out',reportAmount:amount});}
}
}
for(const r of rows){r.displayIncome=r.income+r.transferIn;r.displayExpense=r.expense+r.transferOut;r.balance=r.displayIncome-r.displayExpense;}
for(const r of rows){r.displayIncome=r.income+(includeTransfers?r.transferIn:0);r.displayExpense=r.expense+(includeTransfers?r.transferOut:0);r.balance=r.displayIncome-r.displayExpense;}
return {year,accountId,rows,yearIncome:rows.reduce((s,r)=>s+r.displayIncome,0),yearExpense:rows.reduce((s,r)=>s+r.displayExpense,0)};
}
function occurrenceRows(data,year,accountId,personId='all'){
function occurrenceRows(data,year,accountId,personId='all',includeTransfers=true){
const result=[];
const push=(item,kind,monthIndex,direction='',factor=1)=>{
const amount=(kind==='transfer'?Number(item.amount||0):effectiveAmount(item,year,monthIndex))*factor;
@@ -56,7 +56,7 @@ function occurrenceRows(data,year,accountId,personId='all'){
};
for(const item of data.incomes||[]){const factor=personFactor(item,personId);if(factor&&item.accountId===accountId)for(let m=0;m<12;m++)if(occursInMonth(item,year,m))push(item,'income',m,'',factor);}
for(const item of data.expenses||[]){const factor=personFactor(item,personId);if(factor&&item.accountId===accountId)for(let m=0;m<12;m++)if(occursInMonth(item,year,m))push(item,'expense',m,'',factor);}
for(const item of data.transfers||[]){const factor=personFactor(item,personId);if(factor&&(item.fromAccountId===accountId||item.toAccountId===accountId))for(let m=0;m<12;m++)if(occursInMonth(item,year,m)){
if(includeTransfers) for(const item of data.transfers||[]){const factor=personFactor(item,personId);if(factor&&(item.fromAccountId===accountId||item.toAccountId===accountId))for(let m=0;m<12;m++)if(occursInMonth(item,year,m)){
if(item.toAccountId===accountId)push(item,'transfer',m,'in',factor);
if(item.fromAccountId===accountId)push(item,'transfer',m,'out',factor);
}}
@@ -76,11 +76,11 @@ function addTitle(sheet,title,subtitle='',lastColumn='F'){
function widths(sheet,values){values.forEach((w,i)=>sheet.getColumn(i+1).width=w);}
function euroCell(cell){cell.numFmt='#,##0.00 [$€-de-DE]';}
export async function createExcelReport(data,{year,accountId,personId='all'}={}){
export async function createExcelReport(data,{year,accountId,personId='all',includeTransfers=true}={}){
const reportYear=Number(year)||new Date().getFullYear();
const account=data.accounts.find(a=>a.id===accountId); if(!account)throw new Error('Bitte ein gültiges Konto für den Kontoauszug auswählen.');
const selectedPerson=personId==='all'?'Alle Personen':personName(data,personId);
const forecast=buildAnnualForecast(data,reportYear,accountId,personId); const rows=occurrenceRows(data,reportYear,accountId,personId);
const forecast=buildAnnualForecast(data,reportYear,accountId,personId,includeTransfers); const rows=occurrenceRows(data,reportYear,accountId,personId,includeTransfers);
const wb=new ExcelJS.Workbook();wb.creator='FixFin';wb.created=new Date();wb.modified=new Date();
const statement=wb.addWorksheet('Kontoauszug',{views:[{state:'frozen',ySplit:4}]});
@@ -91,7 +91,7 @@ export async function createExcelReport(data,{year,accountId,personId='all'}={})
statement.autoFilter={from:'A4',to:'G4'};widths(statement,[16,18,30,20,24,18,18]);
const summary=wb.addWorksheet('Monatsübersicht',{views:[{state:'frozen',ySplit:4}]});
addTitle(summary,`Monatsübersicht ${reportYear}`,`${account.name} · ${selectedPerson} · Liquiditätssicht inklusive Transfers`,'D');
addTitle(summary,`Monatsübersicht ${reportYear}`,`${account.name} · ${selectedPerson} · ${includeTransfers?'Liquiditätssicht inklusive Transfers':'Auswertung ohne Transfers'}`,'D');
summary.getRow(4).values=['Monat','Zuflüsse','Abflüsse','Nettofluss'];styleHeader(summary.getRow(4));
forecast.rows.forEach((r,i)=>{const rr=i+5;summary.getRow(rr).values=[r.name,r.displayIncome,r.displayExpense,r.balance];for(let c=2;c<=4;c++)euroCell(summary.getCell(rr,c));summary.getCell(rr,4).font={color:{argb:r.balance<0?'FFB42318':'FF067647'}};});
summary.getRow(18).values=['Jahr',forecast.yearIncome,forecast.yearExpense,forecast.yearIncome-forecast.yearExpense];summary.getRow(18).font={bold:true};for(let c=2;c<=4;c++)euroCell(summary.getCell(18,c));summary.getCell(18,4).font={bold:true,color:{argb:forecast.yearIncome-forecast.yearExpense<0?'FFB42318':'FF067647'}};widths(summary,[20,20,20,20]);
@@ -107,13 +107,13 @@ function pdfMetricRow(doc,items){addPageIfNeeded(doc,48);const y=doc.y;const wid
function pdfTableHeader(doc,cols){const y=doc.y;doc.rect(50,y,495,20).fill('#263A67');let x=50;for(const col of cols){doc.fillColor('#FFFFFF').font('Helvetica-Bold').fontSize(7.5).text(col.label,x+5,y+6,{width:col.width-10,align:col.align||'left'});x+=col.width;}doc.y=y+20;}
function drawTableRow(doc,cols,values,{toneCols={}}={}){const y=doc.y;doc.rect(50,y,495,21).fill('#FFFFFF');doc.strokeColor('#E4E7EC').moveTo(50,y+21).lineTo(545,y+21).stroke();let x=50;cols.forEach((col,i)=>{doc.fillColor(toneCols[i]||'#344054').font('Helvetica').fontSize(7.5).text(String(values[i]??''),x+5,y+6,{width:col.width-10,align:col.align||'left',ellipsis:true});x+=col.width;});doc.y=y+21;}
export async function createPdfReport(data,{year,accountId,personId='all'}={}){
export async function createPdfReport(data,{year,accountId,personId='all',includeTransfers=true}={}){
const reportYear=Number(year)||new Date().getFullYear();
const account=data.accounts.find(a=>a.id===accountId); if(!account)throw new Error('Bitte ein gültiges Konto für den Kontoauszug auswählen.');
const selectedPerson=personId==='all'?'Alle Personen':personName(data,personId);
const forecast=buildAnnualForecast(data,reportYear,accountId,personId);const rows=occurrenceRows(data,reportYear,accountId,personId);
const forecast=buildAnnualForecast(data,reportYear,accountId,personId,includeTransfers);const rows=occurrenceRows(data,reportYear,accountId,personId,includeTransfers);
const doc=new PDFDocument({size:'A4',margin:50,info:{Title:`FixFin Kontoauszug ${reportYear} - ${account.name}`,Author:'FixFin'}});const done=collectPdfBuffer(doc);
drawPdfHeader(doc,`FixFin Kontoauszug ${reportYear}`,`${account.name} · ${selectedPerson} · Erstellt ${new Intl.DateTimeFormat('de-DE',{dateStyle:'medium',timeStyle:'short'}).format(new Date())}`);
drawPdfHeader(doc,`FixFin Kontoauszug ${reportYear}`,`${account.name} · ${selectedPerson} · ${includeTransfers?'inkl. Transfers':'ohne Transfers'} · Erstellt ${new Intl.DateTimeFormat('de-DE',{dateStyle:'medium',timeStyle:'short'}).format(new Date())}`);
pdfMetricRow(doc,[{label:'Zuflüsse / Jahr',value:pdfMoney(forecast.yearIncome),positive:true},{label:'Abflüsse / Jahr',value:pdfMoney(forecast.yearExpense),negative:true},{label:'Nettofluss / Jahr',value:pdfMoney(forecast.yearIncome-forecast.yearExpense),positive:forecast.yearIncome>=forecast.yearExpense,negative:forecast.yearIncome<forecast.yearExpense}]);
const cols=[{label:'Art',width:70},{label:'Bezeichnung',width:130},{label:'Personen',width:80},{label:'Kategorie',width:90},{label:'Zufluss',width:63,align:'right'},{label:'Abfluss',width:62,align:'right'}];