diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a5d8a83
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+docker run --rm -p 3030:3000 gotenberg/gotenberg:8
diff --git a/go.mod b/go.mod
index f9e115d..3b2ac3d 100644
--- a/go.mod
+++ b/go.mod
@@ -5,18 +5,20 @@ go 1.24.5
require (
code.gitea.io/sdk/gitea v0.21.0
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a
+ github.com/google/uuid v1.6.0
github.com/jedib0t/go-pretty/v6 v6.6.8
- golang.org/x/net v0.21.0
+ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
+ github.com/starwalkn/gotenberg-go-client/v8 v8.11.0
)
require (
- github.com/42wim/httpsig v1.2.2 // indirect
+ github.com/42wim/httpsig v1.2.3 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
- golang.org/x/crypto v0.33.0 // indirect
- golang.org/x/sys v0.30.0 // indirect
- golang.org/x/text v0.22.0 // indirect
+ golang.org/x/crypto v0.41.0 // indirect
+ golang.org/x/sys v0.35.0 // indirect
+ golang.org/x/text v0.28.0 // indirect
)
diff --git a/go.sum b/go.sum
index 1077c1f..b66aa7d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,7 @@
code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
-github.com/42wim/httpsig v1.2.2 h1:ofAYoHUNs/MJOLqQ8hIxeyz2QxOz8qdSVvp3PX/oPgA=
-github.com/42wim/httpsig v1.2.2/go.mod h1:P/UYo7ytNBFwc+dg35IubuAUIs8zj5zzFIgUCEl55WY=
+github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
+github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
@@ -10,6 +10,8 @@ github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A=
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc=
@@ -21,29 +23,31 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
+github.com/starwalkn/gotenberg-go-client/v8 v8.11.0 h1:9hKCcoWegZ0yrZ0tEl5gKlMyH2YraYO0R7zDyL0ZL/U=
+github.com/starwalkn/gotenberg-go-client/v8 v8.11.0/go.mod h1:5q9nAJ3/lub4hSCT6QYl8cOjnfQ/B+spEzC4TAPVXd8=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
-golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
-golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
+golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
-golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
+golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
+golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
+golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/index.html b/index.html
index 5e9c371..2372a62 100644
Binary files a/index.html and b/index.html differ
diff --git a/index.pdf b/index.pdf
new file mode 100644
index 0000000..079398a
Binary files /dev/null and b/index.pdf differ
diff --git a/invoice/invoice.go b/invoice/invoice.go
new file mode 100644
index 0000000..839c3c9
--- /dev/null
+++ b/invoice/invoice.go
@@ -0,0 +1,118 @@
+package invoice
+
+import (
+ "bytes"
+ "encoding/base64"
+ "fmt"
+ "html/template"
+ "strings"
+
+ "github.com/skip2/go-qrcode"
+)
+
+// Invoice contains all necessary information for the generation of an invoice.
+type Invoice struct {
+ ReceiverIBAN string `yaml:"receiver_iban" default:"CH44 3199 9123 0008 8901 2"`
+ IsQrIBAN bool `yaml:"is_qr_iban" default:"true"`
+ ReceiverName string `yaml:"receiver_name" default:"Robert Schneider AG"`
+ ReceiverStreet string `yaml:"receiver_street" default:"Rue du Lac"`
+ ReceiverNumber string `yaml:"receiver_number" default:"12"`
+ ReceiverZIPCode string `yaml:"receiver_zip_code" default:"2501"`
+ ReceiverPlace string `yaml:"receiver_place" default:"Biel"`
+ ReceiverCountry string `yaml:"receiver_country" default:"CH"`
+ PayeeName string `yaml:"payee_name" default:"Pia-Maria Rutschmann-Schnyder"`
+ PayeeStreet string `yaml:"payee_street" default:"Grosse Marktgasse"`
+ PayeeNumber string `yaml:"payee_number" default:"28"`
+ PayeeZIPCode string `yaml:"payee_zip_code" default:"9400"`
+ PayeePlace string `yaml:"payee_place" default:"Rorschach"`
+ PayeeCountry string `yaml:"payee_country" default:"CH"`
+ Reference string `yaml:"reference" default:"21 00000 00003 13947 14300 09017"`
+ AdditionalInfo string `yaml:"additional_info" default:"Rechnung Nr. 3139 für Gartenarbeiten"`
+ Amount string `yaml:"amount" default:"3 949.75"`
+ Currency string `yaml:"currency" default:"CHF"`
+}
+
+func (i Invoice) GetQR() (src string, err error) {
+ content, err := i.qrContent()
+ if err != nil {
+ return
+ }
+ qr, err := qrcode.New(content, qrcode.Medium)
+ if err != nil {
+ return
+ }
+ qr.DisableBorder = true
+ png, err := qr.PNG(512)
+ if err != nil {
+ return
+ }
+ b64 := base64.StdEncoding.EncodeToString(png)
+ src = fmt.Sprintf(`data:image/png;base64,%s`, b64)
+ return
+}
+
+// noPayee returns true if no fields of the payee are set
+func (i Invoice) noPayee() bool {
+ return i.PayeeName == "" && i.PayeeStreet == "" && i.PayeeZIPCode == "" && i.PayeePlace == ""
+}
+
+func (i Invoice) qrContent() (string, error) {
+ qrTpl := `SPC\r
+0200\r
+1\r
+{{ .iban }}\r
+S\r
+{{ .inv.ReceiverName }}\r
+{{ .inv.ReceiverStreet }}\r
+{{ .inv.ReceiverNumber }}\r
+{{ .inv.ReceiverZIPCode }}\r
+{{ .inv.ReceiverPlace }}\r
+{{ .inv.ReceiverCountry }}\r
+\r
+\r
+\r
+\r
+\r
+\r
+\r
+{{ .amount }}\r
+{{ .inv.Currency }}\r
+{{ .payeeAdrType }}\r
+{{ .inv.PayeeName }}\r
+{{ .inv.PayeeStreet }}\r
+{{ .inv.PayeeNumber }}\r
+{{ .inv.PayeeZIPCode }}\r
+{{ .inv.PayeePlace }}\r
+{{ .inv.PayeeCountry }}\r
+{{ .refType }}\r
+{{ .reference }}\r
+{{ .inv.AdditionalInfo }}\r
+EPD
+`
+ refType := "QRR"
+ if !i.IsQrIBAN {
+ refType = "SCOR"
+ }
+ payeeAdrType := "S"
+ if i.noPayee() {
+ payeeAdrType = ""
+ }
+ data := map[string]any{
+ "inv": i,
+ "iban": strings.ReplaceAll(i.ReceiverIBAN, " ", ""),
+ "amount": strings.ReplaceAll(i.Amount, " ", ""),
+ "payeeAdrType": payeeAdrType,
+ "reference": strings.ReplaceAll(i.Reference, " ", ""),
+ "refType": refType,
+ }
+ tpl, err := template.New("qr-content").Parse(qrTpl)
+ if err != nil {
+ return "", fmt.Errorf("error while creating qr template: %s", err)
+ }
+ var buf bytes.Buffer
+ if err := tpl.Execute(&buf, data); err != nil {
+ return "", fmt.Errorf("error while applying data to qr template: %s", err)
+ }
+ rsl := strings.ReplaceAll(buf.String(), "\\r", "\r")
+ return rsl, nil
+}
diff --git a/main.go b/main.go
index cbcc919..336165f 100644
--- a/main.go
+++ b/main.go
@@ -1,11 +1,13 @@
package main
import (
- "fmt"
+ "io"
+ "os"
"time"
"code.gitea.io/sdk/gitea"
"git.schreifuchs.ch/lou-taylor/accounting/issue"
+ "git.schreifuchs.ch/lou-taylor/accounting/pdf"
"git.schreifuchs.ch/lou-taylor/accounting/report"
)
@@ -27,7 +29,7 @@ func main() {
for _, repo := range []Repo{
{"lou-taylor", "lou-taylor-web"},
{"lou-taylor", "lou-taylor-api"},
- // {"lou-taylor", "accounting"},
+ {"lou-taylor", "accounting"},
} {
iss, _, err := client.ListRepoIssues(
repo.owner,
@@ -53,8 +55,44 @@ func main() {
},
)
issues := issue.FromGiteas(is, time.Minute*15)
- r := report.Report{Issues: issues}
- fmt.Print(r.ToHTML())
+ r := report.New(
+ issues,
+ report.Entity{
+ Name: "schreifuchs.ch",
+ IBAN: "CH06 0079 0042 5877 0443 7",
+ Address: report.Address{
+ Street: "Kilchbergerweg",
+ Number: "1",
+ ZIPCode: "3052",
+ Place: "Zollikofen",
+ Country: "Schweiz",
+ },
+ Contact: "Niklas Breitenstein",
+ },
+ report.Entity{
+ Name: "Lou Taylor",
+ Address: report.Address{
+ Street: "Alpenstrasse",
+ Number: "22",
+ ZIPCode: "4950",
+ Place: "Huttwil",
+ Country: "Schweiz",
+ },
+ Contact: "Loana Groux",
+ },
+ 16,
+ )
+ html := r.ToHTML()
+
+ // fmt.Print(html)
+ pdfs, err := pdf.New("http://localhost:3030")
+ if err != nil {
+ panic(err)
+ }
+
+ ducument, err := pdfs.HtmlToPdf(html)
+
+ io.Copy(os.Stdout, ducument)
}
func Filter[T any](slice []T, ok func(T) bool) []T {
diff --git a/pdf/resource.go b/pdf/resource.go
new file mode 100644
index 0000000..42419c1
--- /dev/null
+++ b/pdf/resource.go
@@ -0,0 +1,44 @@
+package pdf
+
+import (
+ "context"
+ "io"
+ "net/http"
+
+ "github.com/starwalkn/gotenberg-go-client/v8"
+ "github.com/starwalkn/gotenberg-go-client/v8/document"
+)
+
+type Service struct {
+ gotenberg *gotenberg.Client
+}
+
+func New(hostname string) (service Service, err error) {
+ service.gotenberg, err = gotenberg.NewClient(hostname, http.DefaultClient)
+ return
+}
+
+func (s Service) HtmlToPdf(html string) (pdf io.ReadCloser, err error) {
+ index, err := document.FromString("index.html", html)
+ if err != nil {
+ return
+ }
+ req := gotenberg.NewHTMLRequest(index)
+ req.PaperSize(gotenberg.A4)
+ req.Margins(gotenberg.PageMargins{
+ Top: 0.5,
+ Bottom: 0.5,
+ Left: 0.5,
+ Right: 0.6,
+ Unit: gotenberg.IN,
+ })
+
+ // Skips the IDLE events for faster PDF conversion.
+ req.SkipNetworkIdleEvent(true)
+ req.EmulatePrintMediaType()
+
+ resp, err := s.gotenberg.Send(context.Background(), req)
+ pdf = resp.Body
+
+ return
+}
diff --git a/report/html.go b/report/html.go
index 6acd0b4..f182c51 100644
--- a/report/html.go
+++ b/report/html.go
@@ -38,5 +38,6 @@ func offsetHTags(amount int, html template.HTML) template.HTML {
return fmt.Sprintf("<%sh%d%s>", closingSlash, newLevel, attrs)
})
+
return template.HTML(result)
}
diff --git a/report/invoice.go b/report/invoice.go
new file mode 100644
index 0000000..ef15ae0
--- /dev/null
+++ b/report/invoice.go
@@ -0,0 +1,25 @@
+package report
+
+import (
+ "html/template"
+
+ "git.schreifuchs.ch/lou-taylor/accounting/invoice"
+)
+
+func (r Report) QRInvoice() template.URL {
+ i := invoice.Invoice{
+ ReceiverIBAN: r.Company.IBAN,
+ IsQrIBAN: false,
+ ReceiverName: r.Company.Name,
+ ReceiverStreet: r.Company.Address.Street,
+ ReceiverNumber: r.Company.Address.Number,
+ ReceiverZIPCode: r.Company.Address.ZIPCode,
+ ReceiverPlace: r.Company.Address.Place,
+ ReceiverCountry: r.Company.Address.Country,
+ Reference: "21 00000 00003 13947 14300 09017",
+ Amount: "25",
+ Currency: "CHF",
+ }
+ src, _ := i.GetQR()
+ return template.URL(src)
+}
diff --git a/report/resource.go b/report/resource.go
index a72d051..319ffb0 100644
--- a/report/resource.go
+++ b/report/resource.go
@@ -5,11 +5,39 @@ import (
"time"
"git.schreifuchs.ch/lou-taylor/accounting/issue"
+ "github.com/google/uuid"
)
-type Report struct {
- Date time.Time
- Issues []issue.Issue
- Style template.CSS
- Total string
+func New(issues []issue.Issue, company, client Entity, rate float64) *Report {
+ return &Report{
+ Date: time.Now(),
+ Issues: issues,
+ Rate: rate,
+ Company: company,
+ Client: client,
+ ID: uuid.NewString(),
+ }
+}
+
+type Report struct {
+ Date time.Time
+ Issues []issue.Issue
+ Style template.CSS
+ Rate float64
+ ID string
+ Company Entity
+ Client Entity
+}
+type Entity struct {
+ Name string
+ Address Address
+ Contact string
+ IBAN string
+}
+type Address struct {
+ Street string
+ Number string
+ ZIPCode string
+ Place string
+ Country string
}
diff --git a/report/style.css b/report/style.css
index f00b460..b116261 100644
--- a/report/style.css
+++ b/report/style.css
@@ -14,6 +14,10 @@ header {
display: flex;
justify-content: space-between;
margin-bottom: 40px;
+
+ h2 {
+ margin-top: 0;
+ }
}
h1 {
margin: 0;
@@ -70,3 +74,49 @@ footer {
padding: 5px 10px;
text-align: right;
}
+
+@media print {
+ .page,
+ .page-break {
+ break-after: page;
+ }
+}
+.qr-section {
+ position: absolute;
+ bottom: 0;
+
+ display: grid;
+ grid-template-columns: 250px 1fr;
+ grid-template-rows: 1fr;
+ gap: 1em;
+
+ width: 100%;
+ /* border-top: 2px solid #000; */
+ /* padding-top: 1rem; */
+
+ h4 {
+ margin-bottom: 0.1em;
+ }
+
+ .qr-code {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ img {
+ width: 100%;
+ }
+
+ div {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: 1fr 1fr;
+ justify-items: center;
+ }
+ }
+
+ .payment-details {
+ font-size: 0.9rem;
+ line-height: 1.4;
+ }
+}
diff --git a/report/template.go b/report/template.go
index dc7233f..f1c4a8c 100644
--- a/report/template.go
+++ b/report/template.go
@@ -5,6 +5,8 @@ import (
_ "embed"
"fmt"
"html/template"
+ "math"
+ "strings"
"time"
)
@@ -19,27 +21,46 @@ func (r Report) ToHTML() string {
template.New("report").Funcs(template.FuncMap{
"md": mdToHTML,
"oh": offsetHTags,
- "price": applyRate,
- "time": fmtTime,
+ "price": r.applyRate,
+ "time": fmtDateTime,
+ "date": fmtDate,
"duration": fmtDuration,
+ "brakes": func(text string) template.HTML {
+ return template.HTML(strings.ReplaceAll(template.HTMLEscapeString(text), "\n", "
"))
+ },
}).Parse(htmlTemplate))
r.Style = style
buf := new(bytes.Buffer)
- tmpl.Execute(buf, r)
+ _ = tmpl.Execute(buf, r)
return buf.String()
}
-func applyRate(dur time.Duration) string {
- cost := dur.Hours() * 16
+func (r Report) applyRate(dur time.Duration) string {
+ cost := dur.Hours() * r.Rate
+ cost = math.Round(cost/0.05) * 0.05
return fmt.Sprintf("%.2f", cost)
}
-func fmtTime(t time.Time) string {
+func fmtDateTime(t time.Time) string {
return t.Format("02.08.2006 15:04")
}
+func fmtDate(t time.Time) string {
+ return t.Format("02.08.2006")
+}
+
func fmtDuration(d time.Duration) string {
return fmt.Sprintf("%.2f h", d.Hours())
}
+
+func (r Report) Total() string {
+ dur := time.Duration(0)
+
+ for _, i := range r.Issues {
+ dur += i.Duration
+ }
+
+ return r.applyRate(dur)
+}
diff --git a/report/template.html b/report/template.html
index 0ba9960..4842f1d 100644
--- a/report/template.html
+++ b/report/template.html
@@ -3,7 +3,7 @@