156 lines
5.2 KiB
Go
156 lines
5.2 KiB
Go
// Package table renders an internal.AggregatorWriter as the human-readable
|
|
// Anexo J report table, for the terminal (Render) and for embedding in HTML
|
|
// documents (RenderHTML).
|
|
package table
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/biter777/countries"
|
|
"github.com/jedib0t/go-pretty/v6/table"
|
|
"github.com/jedib0t/go-pretty/v6/text"
|
|
"github.com/nmoniz/any2anexoj/internal"
|
|
)
|
|
|
|
// translator is the contract the table printer relies on for human-readable
|
|
// column labels. *Localizer satisfies it.
|
|
type translator interface {
|
|
Translate(key string, count int, values map[string]any) string
|
|
}
|
|
|
|
// Printer writes a simple, human readable, table row to the provided io.Writer for each
|
|
// ReportItem received. Render and RenderHTML each build a fresh table.Writer so that
|
|
// invoking them (in any order or repeatedly) on the same instance never duplicates rows.
|
|
type Printer struct {
|
|
output io.Writer
|
|
translator Translator
|
|
}
|
|
|
|
func NewPrinter(w io.Writer, tr translator) *Printer {
|
|
return &Printer{
|
|
output: w,
|
|
translator: tr,
|
|
}
|
|
}
|
|
|
|
// buildTable constructs a fully configured table.Writer from the items in aw.
|
|
// Each call returns a new table.Writer so callers can render the same data
|
|
// multiple times without duplicating rows. The returned table is not wired to
|
|
// pp.output via an output mirror; callers that want the rendered output
|
|
// written to pp.output must set the mirror themselves before calling .Render().
|
|
func (pp *Printer) buildTable(aw *internal.AggregatorWriter) table.Writer {
|
|
tw := table.NewWriter()
|
|
tw.SetAutoIndex(true)
|
|
tw.SetStyle(table.StyleLight)
|
|
tw.SetColumnConfigs([]table.ColumnConfig{
|
|
colCountry(1),
|
|
colOther(2),
|
|
colOther(3),
|
|
colOther(4),
|
|
colOther(5),
|
|
colEuros(6),
|
|
colOther(7),
|
|
colOther(8),
|
|
colOther(9),
|
|
colEuros(10),
|
|
colEuros(11),
|
|
colEuros(12),
|
|
colCountry(13),
|
|
})
|
|
|
|
realizationTxt := pp.translator.Translate("realization", 1, nil)
|
|
acquisitionTxt := pp.translator.Translate("acquisition", 1, nil)
|
|
yearTxt := pp.translator.Translate("year", 1, nil)
|
|
monthTxt := pp.translator.Translate("month", 1, nil)
|
|
dayTxt := pp.translator.Translate("day", 1, nil)
|
|
valorTxt := pp.translator.Translate("value", 1, nil)
|
|
|
|
tw.AppendHeader(table.Row{"", "", realizationTxt, realizationTxt, realizationTxt, realizationTxt, acquisitionTxt, acquisitionTxt, acquisitionTxt, acquisitionTxt, "", "", ""}, table.RowConfig{AutoMerge: true})
|
|
tw.AppendHeader(table.Row{
|
|
pp.translator.Translate("source_country", 1, nil), pp.translator.Translate("code", 1, nil),
|
|
yearTxt, monthTxt, dayTxt, valorTxt,
|
|
yearTxt, monthTxt, dayTxt, valorTxt,
|
|
pp.translator.Translate("expenses", 2, nil), pp.translator.Translate("foreign_tax_paid", 1, nil), pp.translator.Translate("counter_country", 1, nil),
|
|
})
|
|
|
|
for ri := range aw.Iter() {
|
|
tw.AppendRow(table.Row{
|
|
ri.AssetCountry, ri.Nature,
|
|
ri.SellTimestamp.Year(), int(ri.SellTimestamp.Month()), ri.SellTimestamp.Day(), ri.SellValue.StringFixed(2),
|
|
ri.BuyTimestamp.Year(), int(ri.BuyTimestamp.Month()), ri.BuyTimestamp.Day(), ri.BuyValue.StringFixed(2),
|
|
ri.Fees.StringFixed(2), ri.Taxes.StringFixed(2), ri.BrokerCountry,
|
|
})
|
|
}
|
|
|
|
sumTxt := pp.translator.Translate("sum", 1, nil)
|
|
// The footer lists one cell per column (13) so it never relies on
|
|
// go-pretty's implicit short-row padding; the trailing empty cell is the
|
|
// counter-country column, which has no total. Totals are formatted with
|
|
// StringFixed(2) so they match the two-decimal data rows.
|
|
tw.AppendFooter(table.Row{sumTxt, sumTxt, sumTxt, sumTxt, sumTxt, aw.TotalEarned().StringFixed(2), "", "", "", aw.TotalSpent().StringFixed(2), aw.TotalFees().StringFixed(2), aw.TotalTaxes().StringFixed(2), ""}, table.RowConfig{AutoMerge: true, AutoMergeAlign: text.AlignRight})
|
|
|
|
return tw
|
|
}
|
|
|
|
func (pp *Printer) Render(aw *internal.AggregatorWriter) {
|
|
t := pp.buildTable(aw)
|
|
t.SetOutputMirror(pp.output)
|
|
t.Render()
|
|
}
|
|
|
|
// RenderHTML builds a fresh table from aw and renders it as an HTML string
|
|
// using go-pretty's HTMLOptions.
|
|
func (pp *Printer) RenderHTML(aw *internal.AggregatorWriter) string {
|
|
t := pp.buildTable(aw)
|
|
t.Style().HTML = table.HTMLOptions{
|
|
CSSClass: "table table-striped table-hover table-bordered",
|
|
EscapeText: true,
|
|
Newline: "<br/>",
|
|
EmptyColumn: " ",
|
|
}
|
|
return t.RenderHTML()
|
|
}
|
|
|
|
func colEuros(n int) table.ColumnConfig {
|
|
return table.ColumnConfig{
|
|
Number: n,
|
|
Align: text.AlignRight,
|
|
AlignFooter: text.AlignRight,
|
|
AlignHeader: text.AlignRight,
|
|
WidthMin: 12,
|
|
WidthMax: 15,
|
|
Transformer: func(val any) string {
|
|
return fmt.Sprintf("%v €", val)
|
|
},
|
|
TransformerFooter: func(val any) string {
|
|
return fmt.Sprintf("%v €", val)
|
|
},
|
|
}
|
|
}
|
|
|
|
func colOther(n int) table.ColumnConfig {
|
|
return table.ColumnConfig{
|
|
Number: n,
|
|
Align: text.AlignLeft,
|
|
AlignFooter: text.AlignLeft,
|
|
AlignHeader: text.AlignLeft,
|
|
WidthMax: 12,
|
|
}
|
|
}
|
|
|
|
func colCountry(n int) table.ColumnConfig {
|
|
return table.ColumnConfig{
|
|
Number: n,
|
|
Align: text.AlignLeft,
|
|
AlignFooter: text.AlignLeft,
|
|
AlignHeader: text.AlignLeft,
|
|
WidthMax: 24,
|
|
WidthMaxEnforcer: text.Trim,
|
|
Transformer: func(val any) string {
|
|
countryCode := val.(int64)
|
|
return fmt.Sprintf("%v - %s", val, countries.ByNumeric(int(countryCode)).Info().Name)
|
|
},
|
|
}
|
|
}
|