render yearly summary card
This commit is contained in:
parent
101e4861d5
commit
71d528f827
|
@ -1,6 +1,9 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ananthakumaran/paisa/internal/model/posting"
|
||||
"github.com/ananthakumaran/paisa/internal/service"
|
||||
"github.com/ananthakumaran/paisa/internal/utils"
|
||||
|
@ -8,44 +11,106 @@ import (
|
|||
"github.com/samber/lo"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type YearlyCard struct {
|
||||
StartDate time.Time `json:"start_date"`
|
||||
EndDate time.Time `json:"end_date"`
|
||||
Postings []posting.Posting `json:"postings"`
|
||||
StartDate time.Time `json:"start_date"`
|
||||
EndDate time.Time `json:"end_date"`
|
||||
Postings []posting.Posting `json:"postings"`
|
||||
GrossSalaryIncome float64 `json:"gross_salary_income"`
|
||||
GrossOtherIncome float64 `json:"gross_other_income"`
|
||||
NetTax float64 `json:"net_tax"`
|
||||
NetIncome float64 `json:"net_income"`
|
||||
NetInvestment float64 `json:"net_investment"`
|
||||
}
|
||||
|
||||
func GetInvestment(db *gorm.DB) gin.H {
|
||||
var postings []posting.Posting
|
||||
result := db.Where("account like ?", "Asset:%").Find(&postings)
|
||||
var incomes []posting.Posting
|
||||
var taxes []posting.Posting
|
||||
result := db.Where("account like ? order by date asc", "Asset:%").Find(&postings)
|
||||
if result.Error != nil {
|
||||
log.Fatal(result.Error)
|
||||
}
|
||||
postings = lo.Filter(postings, func(p posting.Posting, _ int) bool { return !service.IsInterest(db, p) })
|
||||
|
||||
result = db.Where("account like ? order by date asc", "Income:%").Find(&incomes)
|
||||
if result.Error != nil {
|
||||
log.Fatal(result.Error)
|
||||
}
|
||||
|
||||
postings = lo.Filter(postings, func(p posting.Posting, _ int) bool { return !service.IsInterest(db, p) })
|
||||
return gin.H{"postings": postings, "yearly_cards": computeYearlyCard(postings)}
|
||||
result = db.Where("account = ? order by date asc", "Tax").Find(&taxes)
|
||||
if result.Error != nil {
|
||||
log.Fatal(result.Error)
|
||||
}
|
||||
|
||||
var p posting.Posting
|
||||
result = db.Order("date ASC").First(&p)
|
||||
if result.Error != nil {
|
||||
log.Fatal(result.Error)
|
||||
}
|
||||
|
||||
return gin.H{"postings": postings, "yearly_cards": computeYearlyCard(p.Date, postings, taxes, incomes)}
|
||||
}
|
||||
|
||||
func computeYearlyCard(postings []posting.Posting) []YearlyCard {
|
||||
func computeYearlyCard(start time.Time, assets []posting.Posting, taxes []posting.Posting, incomes []posting.Posting) []YearlyCard {
|
||||
var yearlyCards []YearlyCard = make([]YearlyCard, 0)
|
||||
|
||||
if len(postings) == 0 {
|
||||
if len(assets) == 0 {
|
||||
return yearlyCards
|
||||
}
|
||||
|
||||
var p posting.Posting
|
||||
end := time.Now()
|
||||
for start := utils.BeginningOfFinancialYear(postings[0].Date); start.Before(end); start = start.AddDate(1, 0, 0) {
|
||||
for start = utils.BeginningOfFinancialYear(start); start.Before(end); start = start.AddDate(1, 0, 0) {
|
||||
yearEnd := utils.EndOfFinancialYear(start)
|
||||
var currentMonthPostings []posting.Posting = make([]posting.Posting, 0)
|
||||
for len(postings) > 0 && (postings[0].Date.Before(yearEnd) || postings[0].Date.Equal(start)) {
|
||||
p, postings = postings[0], postings[1:]
|
||||
currentMonthPostings = append(currentMonthPostings, p)
|
||||
var currentYearPostings []posting.Posting = make([]posting.Posting, 0)
|
||||
for len(assets) > 0 && utils.IsWithDate(assets[0].Date, start, yearEnd) {
|
||||
p, assets = assets[0], assets[1:]
|
||||
currentYearPostings = append(currentYearPostings, p)
|
||||
}
|
||||
|
||||
yearlyCards = append(yearlyCards, YearlyCard{StartDate: start, EndDate: yearEnd, Postings: currentMonthPostings})
|
||||
var currentYearTaxes []posting.Posting = make([]posting.Posting, 0)
|
||||
for len(taxes) > 0 && utils.IsWithDate(taxes[0].Date, start, yearEnd) {
|
||||
p, taxes = taxes[0], taxes[1:]
|
||||
currentYearTaxes = append(currentYearTaxes, p)
|
||||
}
|
||||
|
||||
netTax := lo.SumBy(currentYearTaxes, func(p posting.Posting) float64 { return p.Amount })
|
||||
|
||||
var currentYearIncomes []posting.Posting = make([]posting.Posting, 0)
|
||||
for len(incomes) > 0 && utils.IsWithDate(incomes[0].Date, start, yearEnd) {
|
||||
p, incomes = incomes[0], incomes[1:]
|
||||
currentYearIncomes = append(currentYearIncomes, p)
|
||||
}
|
||||
|
||||
grossSalaryIncome := lo.SumBy(currentYearIncomes, func(p posting.Posting) float64 {
|
||||
if strings.HasPrefix(p.Account, "Income:Salary") {
|
||||
return -p.Amount
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
grossOtherIncome := lo.SumBy(currentYearIncomes, func(p posting.Posting) float64 {
|
||||
if !strings.HasPrefix(p.Account, "Income:Salary") {
|
||||
return -p.Amount
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
netInvestment := lo.SumBy(currentYearPostings, func(p posting.Posting) float64 { return p.Amount })
|
||||
|
||||
yearlyCards = append(yearlyCards, YearlyCard{
|
||||
StartDate: start,
|
||||
EndDate: yearEnd,
|
||||
Postings: currentYearPostings,
|
||||
NetTax: netTax,
|
||||
GrossSalaryIncome: grossSalaryIncome,
|
||||
GrossOtherIncome: grossOtherIncome,
|
||||
NetIncome: grossSalaryIncome + grossOtherIncome - netTax,
|
||||
NetInvestment: netInvestment,
|
||||
})
|
||||
|
||||
}
|
||||
return yearlyCards
|
||||
|
|
|
@ -35,3 +35,7 @@ func BeginningOfMonth(date time.Time) time.Time {
|
|||
func EndOfMonth(date time.Time) time.Time {
|
||||
return date.AddDate(0, 1, -date.Day())
|
||||
}
|
||||
|
||||
func IsWithDate(date time.Time, start time.Time, end time.Time) bool {
|
||||
return (date.Equal(start) || date.After(start)) && (date.Before(end) || date.Equal(end))
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
forEachMonth,
|
||||
formatCurrency,
|
||||
formatCurrencyCrude,
|
||||
formatFloat,
|
||||
Posting,
|
||||
secondName,
|
||||
skipTicks,
|
||||
|
@ -25,6 +26,13 @@ export default async function () {
|
|||
});
|
||||
renderMonthlyInvestmentTimeline(postings);
|
||||
renderYearlyInvestmentTimeline(yearlyCards);
|
||||
renderYearlyCards(yearlyCards);
|
||||
}
|
||||
|
||||
function financialYear(card: YearlyCard) {
|
||||
return `${card.start_date_timestamp.format(
|
||||
"YYYY"
|
||||
)}-${card.end_date_timestamp.format("YYYY")}`;
|
||||
}
|
||||
|
||||
function renderMonthlyInvestmentTimeline(postings: Posting[]) {
|
||||
|
@ -273,9 +281,7 @@ function renderYearlyInvestmentTimeline(yearlyCards: YearlyCard[]) {
|
|||
points.push(
|
||||
_.merge(
|
||||
{
|
||||
year: `${card.start_date_timestamp.format(
|
||||
"YYYY"
|
||||
)}-${card.end_date_timestamp.format("YYYY")}`,
|
||||
year: financialYear(card),
|
||||
postings: postings
|
||||
},
|
||||
defaultValues,
|
||||
|
@ -384,3 +390,74 @@ function renderYearlyInvestmentTimeline(yearlyCards: YearlyCard[]) {
|
|||
|
||||
svg.select(".legendOrdinal").call(legendOrdinal as any);
|
||||
}
|
||||
|
||||
function renderYearlyCards(yearlyCards: YearlyCard[]) {
|
||||
const id = "#d3-yearly-investment-cards";
|
||||
const root = d3.select(id);
|
||||
|
||||
const card = root
|
||||
.selectAll("div.column")
|
||||
.data(_.reverse(yearlyCards))
|
||||
.enter()
|
||||
.append("div")
|
||||
.attr("class", "column is-4")
|
||||
.append("div")
|
||||
.attr("class", "card");
|
||||
|
||||
card
|
||||
.append("header")
|
||||
.attr("class", "card-header")
|
||||
.append("p")
|
||||
.attr("class", "card-header-title")
|
||||
.text((c) => financialYear(c));
|
||||
|
||||
card
|
||||
.append("div")
|
||||
.attr("class", "card-content p-1")
|
||||
.append("div")
|
||||
.attr("class", "content")
|
||||
.html((card) => {
|
||||
return `
|
||||
<table class="table is-narrow is-fullwidth is-size-7">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Gross Salary Income</td>
|
||||
<td class='has-text-right has-text-weigh-bold'>${formatCurrency(
|
||||
card.gross_salary_income
|
||||
)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gross Other Income</td>
|
||||
<td class='has-text-right has-text-weigh-bold'>${formatCurrency(
|
||||
card.gross_other_income
|
||||
)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Net Tax</td>
|
||||
<td class='has-text-right has-text-weigh-bold'>${formatCurrency(
|
||||
card.net_tax
|
||||
)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Net Income</td>
|
||||
<td class='has-text-right has-text-weigh-bold'>${formatCurrency(
|
||||
card.net_income
|
||||
)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Net Investment</td>
|
||||
<td class='has-text-right has-text-weigh-bold'>${formatCurrency(
|
||||
card.net_investment
|
||||
)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Savings Rate</td>
|
||||
<td class='has-text-right has-text-weigh-bold'>${formatFloat(
|
||||
(card.net_investment / card.net_income) * 100
|
||||
)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -73,6 +73,11 @@ export interface YearlyCard {
|
|||
start_date: string;
|
||||
end_date: string;
|
||||
postings: Posting[];
|
||||
net_tax: number;
|
||||
gross_salary_income: number;
|
||||
gross_other_income: number;
|
||||
net_income: number;
|
||||
net_investment: number;
|
||||
|
||||
start_date_timestamp: dayjs.Dayjs;
|
||||
end_date_timestamp: dayjs.Dayjs;
|
||||
|
|
|
@ -108,15 +108,17 @@
|
|||
<div class="container is-fluid">
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<svg id="d3-yearly-investment-timeline" width="100%"></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column is-6 has-text-centered">
|
||||
<div>
|
||||
<div class="p-3">
|
||||
<svg id="d3-yearly-investment-timeline" width="100%"></svg>
|
||||
</div>
|
||||
<div class="p-3 has-text-centered">
|
||||
<p class="heading">Financial Year Investment Timeline</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="columns is-mobile is-flex-wrap-wrap" id="d3-yearly-investment-cards">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
Loading…
Reference in New Issue