// 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: "
", 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) }, } }