This commit is contained in:
2026-08-19 21:11:42 +02:00
parent 4ecbff359e
commit b4d260421b
6 changed files with 248 additions and 96 deletions
+30 -19
View File
@@ -22,38 +22,49 @@ function effectiveAmount(item,year,monthIndex){
function accountName(data,id){return data.accounts.find(a=>a.id===id)?.name||'Unbekannt';}
function categoryName(data,id){return data.categories.find(c=>c.id===id)?.name||'Ohne Kategorie';}
function personName(data,id){return data.persons?.find(p=>p.id===id)?.name||'Ohne Person';}
function personIdsOf(item){return Array.isArray(item?.personIds)?item.personIds.filter(Boolean):typeof item?.personId==='string'&&item.personId?[item.personId]:[];}
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'){
const personMatches=item=>personId==='all'||item.personId===personId;
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||[]) if(personMatches(item)&&item.accountId===accountId) for(let m=0;m<12;m++) if(occursInMonth(item,year,m)){const amount=effectiveAmount(item,year,m);rows[m].income+=amount;rows[m].items.push({...item,kind:'income',reportAmount:amount});}
for(const item of data.expenses||[]) if(personMatches(item)&&item.accountId===accountId) for(let m=0;m<12;m++) if(occursInMonth(item,year,m)){const amount=effectiveAmount(item,year,m);rows[m].expense+=amount;rows[m].items.push({...item,kind:'expense',reportAmount:amount});}
for(const item of data.transfers||[]) if(personMatches(item)&&(item.fromAccountId===accountId||item.toAccountId===accountId)) for(let m=0;m<12;m++) if(occursInMonth(item,year,m)){
const amount=Number(item.amount||0);
if(item.toAccountId===accountId){rows[m].transferIn+=amount;rows[m].items.push({...item,kind:'transfer',direction:'in',reportAmount:amount});}
if(item.fromAccountId===accountId){rows[m].transferOut+=amount;rows[m].items.push({...item,kind:'transfer',direction:'out',reportAmount:amount});}
for(const item of data.incomes||[]){
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].income+=amount;rows[m].items.push({...item,kind:'income',reportAmount:amount});}
}
for(const item of data.expenses||[]){
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||[]){
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;
if(item.toAccountId===accountId){rows[m].transferIn+=amount;rows[m].items.push({...item,kind:'transfer',direction:'in',reportAmount:amount});}
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;}
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'){
const personMatches=item=>personId==='all'||item.personId===personId;
const result=[];
const push=(item,kind,monthIndex,direction='')=>{
const amount=kind==='transfer'?Number(item.amount||0):effectiveAmount(item,year,monthIndex);
result.push({monthIndex,month:MONTHS[monthIndex],kind,direction,name:item.name,person:personName(data,item.personId),category:categoryName(data,item.categoryId),amount});
const push=(item,kind,monthIndex,direction='',factor=1)=>{
const amount=(kind==='transfer'?Number(item.amount||0):effectiveAmount(item,year,monthIndex))*factor;
result.push({monthIndex,month:MONTHS[monthIndex],kind,direction,name:item.name,person:personLabel(data,item),category:categoryName(data,item.categoryId),amount});
};
for(const item of data.incomes||[])if(personMatches(item)&&item.accountId===accountId)for(let m=0;m<12;m++)if(occursInMonth(item,year,m))push(item,'income',m);
for(const item of data.expenses||[])if(personMatches(item)&&item.accountId===accountId)for(let m=0;m<12;m++)if(occursInMonth(item,year,m))push(item,'expense',m);
for(const item of data.transfers||[])if(personMatches(item)&&(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');
if(item.fromAccountId===accountId)push(item,'transfer',m,'out');
}
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(item.toAccountId===accountId)push(item,'transfer',m,'in',factor);
if(item.fromAccountId===accountId)push(item,'transfer',m,'out',factor);
}}
const order={income:0,transferIn:1,expense:2,transferOut:3};
const typeOrder=r=>r.kind==='income'?order.income:r.kind==='expense'?order.expense:r.direction==='in'?order.transferIn:order.transferOut;
return result.sort((a,b)=>a.monthIndex-b.monthIndex||typeOrder(a)-typeOrder(b)||a.name.localeCompare(b.name,'de'));
}
function kindLabel(row){return row.kind==='income'?'Eingang':row.kind==='expense'?'Ausgabe':row.direction==='in'?'Transfer rein':'Transfer raus';}
function isInflow(row){return row.kind==='income'||row.direction==='in';}
@@ -74,7 +85,7 @@ export async function createExcelReport(data,{year,accountId,personId='all'}={})
const statement=wb.addWorksheet('Kontoauszug',{views:[{state:'frozen',ySplit:4}]});
addTitle(statement,`FixFin Kontoauszug ${reportYear}`,`${account.name} · ${selectedPerson} · Planungsdaten · Erstellt am ${new Intl.DateTimeFormat('de-DE',{dateStyle:'medium',timeStyle:'short'}).format(new Date())}`,'G');
statement.getRow(4).values=['Monat','Art','Bezeichnung','Person','Kategorie','Zufluss','Abfluss'];styleHeader(statement.getRow(4));
statement.getRow(4).values=['Monat','Art','Bezeichnung','Personen','Kategorie','Zufluss','Abfluss'];styleHeader(statement.getRow(4));
rows.forEach((r,i)=>{const rr=i+5;statement.getRow(rr).values=[r.month,kindLabel(r),r.name,r.person,r.category,isInflow(r)?r.amount:null,isInflow(r)?null:r.amount];euroCell(statement.getCell(rr,6));euroCell(statement.getCell(rr,7));if(isInflow(r))statement.getCell(rr,6).font={color:{argb:'FF067647'}};else statement.getCell(rr,7).font={color:{argb:'FFB42318'}};});
const totalRow=rows.length+6;statement.getCell(totalRow,1).value='Jahressumme';statement.getCell(totalRow,1).font={bold:true};statement.getCell(totalRow,6).value=forecast.yearIncome;statement.getCell(totalRow,7).value=forecast.yearExpense;euroCell(statement.getCell(totalRow,6));euroCell(statement.getCell(totalRow,7));statement.getCell(totalRow,6).font={bold:true,color:{argb:'FF067647'}};statement.getCell(totalRow,7).font={bold:true,color:{argb:'FFB42318'}};
statement.autoFilter={from:'A4',to:'G4'};widths(statement,[16,18,30,20,24,18,18]);
@@ -105,7 +116,7 @@ export async function createPdfReport(data,{year,accountId,personId='all'}={}){
drawPdfHeader(doc,`FixFin Kontoauszug ${reportYear}`,`${account.name} · ${selectedPerson} · 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:'Person',width:80},{label:'Kategorie',width:90},{label:'Zufluss',width:63,align:'right'},{label:'Abfluss',width:62,align:'right'}];
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'}];
for(let monthIndex=0;monthIndex<12;monthIndex++){
const monthRows=rows.filter(r=>r.monthIndex===monthIndex);const month=forecast.rows[monthIndex];