Skip to content

Commit

Permalink
show current month expenses in detail
Browse files Browse the repository at this point in the history
  • Loading branch information
ananthakumaran committed Aug 27, 2022
1 parent 76c9069 commit 3acb692
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 6 deletions.
33 changes: 32 additions & 1 deletion internal/server/expense.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package server

import (
"time"

"github.com/ananthakumaran/paisa/internal/model/posting"
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
Expand All @@ -14,5 +17,33 @@ func GetExpense(db *gorm.DB) gin.H {
log.Fatal(result.Error)
}

return gin.H{"expenses": expenses}
now := time.Now()
start := utils.BeginningOfMonth(now)
end := utils.EndOfMonth(now)

var currentExpenses []posting.Posting
result = db.Debug().Where("account like ? and account != ? and date >= ? and date <= ? order by date asc", "Expenses:%", "Expenses:Tax", start, end).Find(&currentExpenses)
if result.Error != nil {
log.Fatal(result.Error)
}

var currentIncomes []posting.Posting
result = db.Where("account like ? and date >= ? and date <= ? order by date asc", "Income:%", start, end).Find(&currentIncomes)
if result.Error != nil {
log.Fatal(result.Error)
}

var currentInvestments []posting.Posting
result = db.Where("account like ? and account != ? and date >= ? and date <= ? order by date asc", "Assets:%", "Assets:Checking", start, end).Find(&currentInvestments)
if result.Error != nil {
log.Fatal(result.Error)
}

var currentTaxes []posting.Posting
result = db.Where("account = ? and date >= ? and date <= ? order by date asc", "Expenses:Tax", start, end).Find(&currentTaxes)
if result.Error != nil {
log.Fatal(result.Error)
}

return gin.H{"expenses": expenses, "current_month": gin.H{"expenses": currentExpenses, "incomes": currentIncomes, "investments": currentInvestments, "taxes": currentTaxes}}
}
8 changes: 6 additions & 2 deletions internal/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,17 @@ func EndOfFinancialYear(date time.Time) time.Time {
}

func BeginningOfMonth(date time.Time) time.Time {
return date.AddDate(0, 0, -date.Day()+1)
return toDate(date.AddDate(0, 0, -date.Day()+1))
}

func EndOfMonth(date time.Time) time.Time {
return date.AddDate(0, 1, -date.Day())
return toDate(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))
}

func toDate(date time.Time) time.Time {
return time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC)
}
136 changes: 134 additions & 2 deletions web/src/expense.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as d3 from "d3";
import legend from "d3-svg-legend";
import { sprintf } from "sprintf-js";
import dayjs from "dayjs";
import _ from "lodash";
import {
Expand All @@ -8,15 +9,38 @@ import {
formatCurrency,
formatCurrencyCrude,
Posting,
restName,
secondName,
setHtml,
skipTicks,
tooltip
} from "./utils";

export default async function () {
const { expenses: expenses } = await ajax("/api/expense");
const {
expenses: expenses,
current_month: {
expenses: current_expenses,
incomes: current_incomes,
investments: current_investments,
taxes: current_taxes
}
} = await ajax("/api/expense");
_.each(expenses, (p) => (p.timestamp = dayjs(p.date)));
renderMonthlyExpensesTimeline(expenses);
_.each(current_expenses, (p) => (p.timestamp = dayjs(p.date)));
_.each(current_incomes, (p) => (p.timestamp = dayjs(p.date)));
const z = renderMonthlyExpensesTimeline(expenses);
renderCurrentExpensesBreakdown(current_expenses, z);

setHtml("current-month", dayjs().format("MMMM YYYY"));
setHtml("current-month-income", sum(current_incomes, -1));
setHtml("current-month-tax", sum(current_taxes));
setHtml("current-month-expenses", sum(current_expenses));
setHtml("current-month-investment", sum(current_investments));
}

function sum(postings: Posting[], sign = 1) {
return formatCurrency(sign * _.sumBy(postings, (p) => p.amount));
}

function renderMonthlyExpensesTimeline(postings: Posting[]) {
Expand Down Expand Up @@ -165,4 +189,112 @@ function renderMonthlyExpensesTimeline(postings: Posting[]) {
.scale(z);

svg.select(".legendOrdinal").call(legendOrdinal as any);
return z;
}

function renderCurrentExpensesBreakdown(
postings: Posting[],
z: d3.ScaleOrdinal<string, string, never>
) {
const id = "#d3-current-month-breakdown";
const BAR_HEIGHT = 20;
const svg = d3.select(id),
margin = { top: 0, right: 150, bottom: 20, left: 80 },
width =
document.getElementById(id.substring(1)).parentElement.clientWidth -
margin.left -
margin.right,
g = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

if (_.isEmpty(postings)) {
svg.style("display", "none");
return;
}

const categories = _.chain(postings)
.groupBy((p) => restName(p.account))
.mapValues((ps, category) => {
return {
category: category,
postings: ps,
total: _.sumBy(ps, (p) => p.amount)
};
})
.value();
const keys = _.chain(categories)
.sortBy((c) => c.total)
.map((c) => c.category)
.value();

const points = _.values(categories);
const total = _.sumBy(points, (p) => p.total);

const height = BAR_HEIGHT * keys.length;
svg.attr("height", height + margin.top + margin.bottom);

const x = d3.scaleLinear().range([0, width]);
const y = d3.scaleBand().range([height, 0]).paddingInner(0.1).paddingOuter(0);

y.domain(keys);
x.domain([0, d3.max(points, (p) => p.total)]);

g.append("g")
.attr("class", "axis y")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickSize(-height).tickFormat(formatCurrencyCrude));

g.append("g").attr("class", "axis y dark").call(d3.axisLeft(y));

const bar = g.append("g").selectAll("rect").data(points).enter();

bar
.append("rect")
.attr("fill", function (d) {
return z(d.category);
})
.attr("data-tippy-content", (d) => {
return tooltip(
d.postings.map((p) => {
return [
p.timestamp.format("DD MMM YYYY"),
p.payee,
[formatCurrency(p.amount), "has-text-weight-bold has-text-right"]
];
})
);
})
.attr("x", x(0))
.attr("y", function (d) {
return (
y(d.category) +
(y.bandwidth() - Math.min(y.bandwidth(), BAR_HEIGHT)) / 2
);
})
.attr("width", function (d) {
return x(d.total);
})
.attr("height", y.bandwidth());

bar
.append("text")
.attr("text-anchor", "end")
.attr("alignment-baseline", "middle")
.attr("x", width + 125)
.attr("y", function (d) {
console.log(sprintf("|%7.2f|", (d.total / total) * 100));
return y(d.category) + y.bandwidth() / 2;
})
.style("white-space", "pre")
.style("font-size", "12px")
.attr("fill", "#666")
.attr("class", "is-family-monospace")
.text(
(d) =>
`${formatCurrency(d.total)} ${sprintf(
"%6.2f",
(d.total / total) * 100
)}%`
);
}
11 changes: 11 additions & 0 deletions web/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ export function ajax(route: "/api/income"): Promise<{
}>;
export function ajax(route: "/api/expense"): Promise<{
expenses: Posting[];
current_month: {
expenses: Posting[];
incomes: Posting[];
investments: Posting[];
taxes: Posting[];
};
}>;
export async function ajax(route: string) {
const response = await fetch(route);
Expand All @@ -121,6 +127,11 @@ export function formatCurrency(value: number, precision = 0) {
return "00";
}

// minus 0
if (1 / value === -Infinity) {
value = 0;
}

return value.toLocaleString("hi", {
minimumFractionDigits: precision,
maximumFractionDigits: precision
Expand Down
50 changes: 49 additions & 1 deletion web/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
<div class="navbar-menu">
<div class="navbar-start">
<a id="overview" class="navbar-item is-active">Overview</a>
<a id="expense" class="navbar-item">Expense</a>
<a id="investment" class="navbar-item">Investment</a>
<a id="gain" class="navbar-item">Gain</a>
<a id="income" class="navbar-item">Income</a>
<a id="expense" class="navbar-item">Expense</a>
<a id="allocation" class="navbar-item">Allocation</a>
<a id="ledger" class="navbar-item">Ledger</a>
</div>
Expand Down Expand Up @@ -317,6 +317,54 @@
</div>
</div>
</section>
<section class="section tab-expense">
<div class="container is-fluid">
<div class="columns">
<div class="column is-full">
<div class="p-3 has-text-centered">
<p class="heading d3-current-month is-size-4"></p>
</div>
</div>
</div>
<div class="columns is-flex-wrap-wrap">
<div class="column is-full-tablet is-half-fullhd">
<div class="p-3">
<nav class="level">
<div class="level-item has-text-centered">
<div>
<p class="heading">Income</p>
<p class="d3-current-month-income title"></p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Tax</p>
<p class="d3-current-month-tax title"></p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Net Investment</p>
<p class="d3-current-month-investment title has-text-success"></p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Expenses</p>
<p class="d3-current-month-expenses title has-text-danger"></p>
</div>
</div>
</nav>
</div>
</div>
<div class="column is-full-tablet is-half-fullhd">
<div class="p-3">
<svg id="d3-current-month-breakdown" width="100%"></svg>
</div>
</div>
</div>
</div>
</section>
<section class="section tab-expense">
<div class="container is-fluid">
<div class="columns">
Expand Down

0 comments on commit 3acb692

Please sign in to comment.