render yearly summary card

This commit is contained in:
Anantha Kumaran 2022-08-09 17:53:18 +05:30
parent 101e4861d5
commit 71d528f827
5 changed files with 177 additions and 24 deletions

View File

@ -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

View File

@ -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))
}

View File

@ -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>
`;
});
}

View File

@ -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;

View File

@ -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>