diff --git a/go.mod b/go.mod
index 811921a..f9e115d 100644
--- a/go.mod
+++ b/go.mod
@@ -2,13 +2,21 @@ module git.schreifuchs.ch/lou-taylor/accounting
go 1.24.5
-require code.gitea.io/sdk/gitea v0.21.0
+require (
+ code.gitea.io/sdk/gitea v0.21.0
+ github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a
+ github.com/jedib0t/go-pretty/v6 v6.6.8
+ golang.org/x/net v0.21.0
+)
require (
github.com/42wim/httpsig v1.2.2 // 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
)
diff --git a/go.sum b/go.sum
index 13077cc..1077c1f 100644
--- a/go.sum
+++ b/go.sum
@@ -8,10 +8,19 @@ github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454Wv
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
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/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=
+github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+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/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=
@@ -21,6 +30,8 @@ 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/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=
@@ -31,6 +42,8 @@ golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
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/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
new file mode 100644
index 0000000..d0e6187
--- /dev/null
+++ b/index.html
@@ -0,0 +1,210 @@
+
+
+
+
+
+ Rechnung vom 0001-01-01 00:00:00 +0000 UTC
+
+
+
+
+
+
+
.CompanyName
+ .CompanyAddress
+
.CompanyContact
+
+
+
Rechnung: .InvoiceNumber
+
Datum: .Date
+
Fällig am: .DueDate
+
+
+
+ Rechnung an:
+ .ClientName
+ .ClientAddress
+ .ClientContact
+
+
+
+
+ FID
+ Name
+ Zeitaufwand
+ Stundensatz
+ Preis CHF
+ Fertiggestellt
+
+
+
+
+
+ 6
+ Multilingual
+ 2.50 h
+ 16 CHF/h
+ 40.00 CHF
+
+ 21.08.2025 18:05
+
+
+
+ 1
+ asdf
+ 1.72 h
+ 16 CHF/h
+ 27.47 CHF
+
+ 01.08.2025 14:04
+
+
+
+
+
+
+
+
+ Gesamtbetrag:
+ CHF
+
+
+
+
+
+
+
+ Details zu den Features
+
+
+
+ 6: Multilingual
+ duration: 2h 30min
+
+
+The application must support English and German.
+
+TODO’s
+
+
+
+
+
+
+ 1: asdf
+ Hello
+
+duration: 1h 43min
+
+
+adökfjaösldkjflaa
+
+ASDFADS
+
+adllglggl
+
+
+
+
+
+
+ Bitte überweisen Sie den Gesamtbetrag bis zum Fälligkeitsdatum auf folgendes Konto:
+ .BankDetails
+ Vielen Dank für Ihr Vertrauen!
+
+
+
diff --git a/issue/issue.go b/issue/issue.go
new file mode 100644
index 0000000..79f34ef
--- /dev/null
+++ b/issue/issue.go
@@ -0,0 +1,76 @@
+package issue
+
+import (
+ "fmt"
+ "os"
+ "regexp"
+ "strings"
+ "time"
+
+ "code.gitea.io/sdk/gitea"
+ "github.com/jedib0t/go-pretty/v6/table"
+)
+
+func FromGiteas(is []*gitea.Issue, mindur time.Duration) []Issue {
+ issues := make([]Issue, 0, len(is))
+
+ for _, i := range is {
+ if i == nil {
+ continue
+ }
+ issue := FromGitea(*i)
+
+ if issue.Duration < mindur {
+ continue
+ }
+
+ issues = append(issues, issue)
+ }
+ return issues
+}
+
+func FromGitea(i gitea.Issue) Issue {
+ issue := Issue{Issue: i}
+
+ issue.Duration, _ = ExtractDuration(i.Body)
+
+ return issue
+}
+
+func ExtractDuration(text string) (duration time.Duration, err error) {
+ // First capture only the content inside ```info ... ```
+ reBlock := regexp.MustCompile("(?s)```info(.*?)```")
+ block := reBlock.FindStringSubmatch(text)
+ if len(block) < 2 {
+ err = fmt.Errorf("no info block found")
+ return
+ }
+
+ // Now extract the duration line from inside that block
+ reDuration := regexp.MustCompile(`duration:\s*([0-9hmin\s]+)`)
+ match := reDuration.FindStringSubmatch(block[1])
+ if len(match) < 2 {
+ err = fmt.Errorf("no duration found inside info block")
+ return
+ }
+ dur := strings.TrimSpace(match[1])
+ dur = strings.ReplaceAll(dur, "min", "m")
+ dur = strings.ReplaceAll(dur, " ", "") // remove spaces
+
+ return time.ParseDuration(dur)
+}
+
+func Print(issues []Issue) {
+ t := table.NewWriter()
+ t.SetOutputMirror(os.Stdout)
+ t.AppendHeader(table.Row{"#", "Title", "Body", "Duration", "Finished by"})
+
+ for i, iss := range issues {
+ if i != 0 {
+ t.AppendSeparator()
+ }
+ t.AppendRow(table.Row{iss.Index, iss.Title, iss.Body, iss.Duration, iss.Closed})
+ }
+
+ t.Render()
+}
diff --git a/issue/resource.go b/issue/resource.go
new file mode 100644
index 0000000..0fc9154
--- /dev/null
+++ b/issue/resource.go
@@ -0,0 +1,12 @@
+package issue
+
+import (
+ "time"
+
+ "code.gitea.io/sdk/gitea"
+)
+
+type Issue struct {
+ gitea.Issue
+ Duration time.Duration
+}
diff --git a/main.go b/main.go
index 03c1931..d27774b 100644
--- a/main.go
+++ b/main.go
@@ -1,11 +1,12 @@
package main
import (
- "encoding/json"
- "os"
+ "fmt"
"time"
"code.gitea.io/sdk/gitea"
+ "git.schreifuchs.ch/lou-taylor/accounting/issue"
+ "git.schreifuchs.ch/lou-taylor/accounting/report"
)
type Repo struct {
@@ -22,7 +23,7 @@ func main() {
panic(err)
}
- var issues []*gitea.Issue
+ var is []*gitea.Issue
for _, repo := range []Repo{
{"lou-taylor", "lou-taylor-web"},
{"lou-taylor", "lou-taylor-api"},
@@ -35,26 +36,25 @@ func main() {
ListOptions: gitea.ListOptions{Page: 0, PageSize: 99999},
Since: time.Now().AddDate(0, -1, 0),
Before: time.Now(),
+ State: gitea.StateClosed,
},
)
if err != nil {
panic(err)
}
- issues = append(issues, iss...)
+ is = append(is, iss...)
}
- // for _, issue := range issues {
- // fmt.Println(issue.Body)
- // }
- //
- issues = Filter(
- issues,
+
+ is = Filter(
+ is,
func(i *gitea.Issue) bool {
return i.Closed != nil && i.Closed.After(time.Now().AddDate(0, -1, 0))
},
)
-
- json.NewEncoder(os.Stdout).Encode(issues)
+ issues := issue.FromGiteas(is, time.Minute*15)
+ r := report.Report{Issues: issues}
+ fmt.Print(r.ToHTML())
}
func Filter[T any](slice []T, ok func(T) bool) []T {
diff --git a/report/html.go b/report/html.go
new file mode 100644
index 0000000..6acd0b4
--- /dev/null
+++ b/report/html.go
@@ -0,0 +1,42 @@
+package report
+
+import (
+ "fmt"
+ "html/template"
+ "regexp"
+ "strconv"
+)
+
+// OffsetHTags offsets all - tags in the HTML by the given amount.
+// If the resulting heading level exceeds 6, it is capped at 6.
+func offsetHTags(amount int, html template.HTML) template.HTML {
+ // Regular expression to match opening and closing h tags, e.g., or
+ re := regexp.MustCompile(`(?i)<(/?)h([1-6])(\s[^>]*)?>`)
+
+ // Replace all matches
+ result := re.ReplaceAllStringFunc(string(html), func(tag string) string {
+ matches := re.FindStringSubmatch(tag)
+ if len(matches) < 4 {
+ return tag
+ }
+
+ closingSlash := matches[1] // "/" if closing tag
+ levelStr := matches[2] // heading level
+ attrs := matches[3] // attributes like ' class="foo"'
+
+ level, err := strconv.Atoi(levelStr)
+ if err != nil {
+ return tag
+ }
+
+ newLevel := level + amount
+ if newLevel < 1 {
+ newLevel = 1
+ } else if newLevel > 6 {
+ newLevel = 6
+ }
+
+ return fmt.Sprintf("<%sh%d%s>", closingSlash, newLevel, attrs)
+ })
+ return template.HTML(result)
+}
diff --git a/report/html_test.go b/report/html_test.go
new file mode 100644
index 0000000..e709feb
--- /dev/null
+++ b/report/html_test.go
@@ -0,0 +1,79 @@
+package report
+
+import (
+ "html/template"
+ "testing"
+)
+
+func TestOffsetHTags(t *testing.T) {
+ tests := []struct {
+ name string
+ html string
+ amount int
+ expected string
+ }{
+ {
+ name: "simple offset",
+ html: `Main Sub `,
+ amount: 2,
+ expected: `Main Sub `,
+ },
+ {
+ name: "offset beyond h6",
+ html: `Almost max Max `,
+ amount: 3,
+ expected: `Almost max Max `,
+ },
+ {
+ name: "negative offset",
+ html: `Heading Subheading `,
+ amount: -2,
+ expected: `Heading Subheading `,
+ },
+ {
+ name: "no h tags",
+ html: ` Paragraph
`,
+ amount: 2,
+ expected: `Paragraph
`,
+ },
+ {
+ name: "mixed content",
+ html: `Title
Inner `,
+ amount: 1,
+ expected: `Title
Inner `,
+ },
+ {
+ name: "real one",
+ html: `Hello
+
+duration: 1h 43min
+
+
+adökfjaösldkjflaa
+
+ASDFADS
+
+adllglggl `,
+ amount: 3,
+ expected: `Hello
+
+duration: 1h 43min
+
+
+adökfjaösldkjflaa
+
+ASDFADS
+
+adllglggl `,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := offsetHTags(tt.amount, template.HTML(tt.html))
+ if string(got) != tt.expected {
+ t.Errorf("OffsetHTags() = %q, want %q", got, tt.expected)
+ }
+ })
+ }
+}
diff --git a/report/markdown.go b/report/markdown.go
new file mode 100644
index 0000000..5e3b623
--- /dev/null
+++ b/report/markdown.go
@@ -0,0 +1,49 @@
+package report
+
+import (
+ "fmt"
+ "html/template"
+ "regexp"
+ "strings"
+
+ "github.com/gomarkdown/markdown"
+ "github.com/gomarkdown/markdown/html"
+ "github.com/gomarkdown/markdown/parser"
+)
+
+func mdToHTML(md string) template.HTML {
+ md = markdownCheckboxesToHTML(md)
+
+ // create markdown parser with extensions
+ extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
+ p := parser.NewWithExtensions(extensions)
+ doc := p.Parse([]byte(md))
+
+ // create HTML renderer with extensions
+ htmlFlags := html.CommonFlags | html.HrefTargetBlank
+ opts := html.RendererOptions{Flags: htmlFlags}
+ renderer := html.NewRenderer(opts)
+
+ return template.HTML(markdown.Render(doc, renderer))
+}
+
+// markdownCheckboxesToHTML keeps a Markdown list but replaces checkboxes with .
+func markdownCheckboxesToHTML(markdown string) string {
+ lines := strings.Split(markdown, "\n")
+ checkboxPattern := regexp.MustCompile(`^(\s*[-*]\s+)\[( |x|X)\][\p{Zs}\s]*(.*)$`)
+
+ for i, line := range lines {
+ if matches := checkboxPattern.FindStringSubmatch(line); matches != nil {
+ prefix := matches[1] // "- " or "* "
+ checked := strings.ToLower(matches[2]) == "x"
+ content := matches[3] // task text
+ if checked {
+ lines[i] = fmt.Sprintf(`%s %s`, prefix, content)
+ } else {
+ lines[i] = fmt.Sprintf(`%s %s`, prefix, content)
+ }
+ }
+ }
+
+ return strings.Join(lines, "\n")
+}
diff --git a/report/markdown_test.go b/report/markdown_test.go
new file mode 100644
index 0000000..a6de701
--- /dev/null
+++ b/report/markdown_test.go
@@ -0,0 +1,27 @@
+package report
+
+import (
+ "testing"
+)
+
+func TestMarkdownCheckboxesToHTML(t *testing.T) {
+ input := `
+The application must support English and German.
+
+## TODO's
+ - [x] Add multiple languages to Pages
+ - [x] Add multiple languages to Events`
+
+ expected := `
+The application must support English and German.
+
+## TODO's
+ - Add multiple languages to Pages
+ - Add multiple languages to Events`
+
+ output := markdownCheckboxesToHTML(input)
+
+ if output != expected {
+ t.Errorf("unexpected output:\nGot:\n%s\n\nExpected:\n%s", output, expected)
+ }
+}
diff --git a/report/resource.go b/report/resource.go
new file mode 100644
index 0000000..a72d051
--- /dev/null
+++ b/report/resource.go
@@ -0,0 +1,15 @@
+package report
+
+import (
+ "html/template"
+ "time"
+
+ "git.schreifuchs.ch/lou-taylor/accounting/issue"
+)
+
+type Report struct {
+ Date time.Time
+ Issues []issue.Issue
+ Style template.CSS
+ Total string
+}
diff --git a/report/style.css b/report/style.css
new file mode 100644
index 0000000..faf3e66
--- /dev/null
+++ b/report/style.css
@@ -0,0 +1,89 @@
+/* body { */
+/* font-family: sans-serif; */
+/* font-size: 14px; */
+/* } */
+/**/
+/* table { */
+/* width: 100%; */
+/* border-collapse: collapse; */
+/* } */
+/**/
+/* th, */
+/* td { */
+/* border: 1px solid #ddd; */
+/* padding: 8px; */
+/* text-align: left; */
+/* } */
+/**/
+/* th { */
+/* background-color: #f4f4f4; */
+/* font-weight: bold; */
+/* } */
+/**/
+/* tr:nth-child(even) { */
+/* background-color: #f9f9f9; */
+/* } */
+body {
+ font-family: sans-serif;
+ margin: 40px;
+ color: #333;
+}
+header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 40px;
+}
+h1 {
+ margin: 0;
+ font-size: 2em;
+}
+.company,
+.client {
+ margin-bottom: 20px;
+}
+table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-bottom: 20px;
+}
+table,
+th,
+td {
+ border: 1px solid #ccc;
+}
+th,
+td {
+ padding: 10px;
+ text-align: left;
+}
+th {
+ background-color: #f2f2f2;
+}
+tfoot td {
+ font-weight: bold;
+}
+h2 {
+ margin-top: 40px;
+}
+article {
+ margin-bottom: 20px;
+}
+footer {
+ margin-top: 40px;
+ font-size: 0.9em;
+ text-align: center;
+ color: #666;
+}
+.totals {
+ float: right;
+ width: 300px;
+ margin-top: 20px;
+}
+.totals table {
+ border: none;
+}
+.totals td {
+ border: none;
+ padding: 5px 10px;
+ text-align: right;
+}
diff --git a/report/template.go b/report/template.go
new file mode 100644
index 0000000..dc7233f
--- /dev/null
+++ b/report/template.go
@@ -0,0 +1,45 @@
+package report
+
+import (
+ "bytes"
+ _ "embed"
+ "fmt"
+ "html/template"
+ "time"
+)
+
+//go:embed template.html
+var htmlTemplate string
+
+//go:embed style.css
+var style template.CSS
+
+func (r Report) ToHTML() string {
+ tmpl := template.Must(
+ template.New("report").Funcs(template.FuncMap{
+ "md": mdToHTML,
+ "oh": offsetHTags,
+ "price": applyRate,
+ "time": fmtTime,
+ "duration": fmtDuration,
+ }).Parse(htmlTemplate))
+
+ r.Style = style
+
+ buf := new(bytes.Buffer)
+ tmpl.Execute(buf, r)
+ return buf.String()
+}
+
+func applyRate(dur time.Duration) string {
+ cost := dur.Hours() * 16
+ return fmt.Sprintf("%.2f", cost)
+}
+
+func fmtTime(t time.Time) string {
+ return t.Format("02.08.2006 15:04")
+}
+
+func fmtDuration(d time.Duration) string {
+ return fmt.Sprintf("%.2f h", d.Hours())
+}
diff --git a/report/template.html b/report/template.html
new file mode 100644
index 0000000..23dc0ee
--- /dev/null
+++ b/report/template.html
@@ -0,0 +1,85 @@
+
+
+
+
+
+ Rechnung vom {{ .Date }}
+
+
+
+
+
+
+
.CompanyName
+ .CompanyAddress
+
.CompanyContact
+
+
+
Rechnung: .InvoiceNumber
+
Datum: .Date
+
Fällig am: .DueDate
+
+
+
+ Rechnung an:
+ .ClientName
+ .ClientAddress
+ .ClientContact
+
+
+
+
+ FID
+ Name
+ Zeitaufwand
+ Stundensatz
+ Preis CHF
+ Fertiggestellt
+
+
+
+ {{ range .Issues }}
+
+ {{ .Index }}
+ {{ .Title }}
+ {{ .Duration | duration }}
+ 16 CHF/h
+ {{ .Duration | price }} CHF
+
+ {{ .Closed | time }}
+
+ {{ end }}
+
+
+
+
+
+
+ Gesamtbetrag:
+ {{ .Total }} CHF
+
+
+
+
+
+
+
+ Details zu den Features
+
+ {{ range .Issues }}
+
+ {{ .Index }}: {{ .Title }}
+ {{ .Body | md | oh 3 }}
+
+ {{ end }}
+
+
+
+ Bitte überweisen Sie den Gesamtbetrag bis zum Fälligkeitsdatum auf folgendes Konto:
+ .BankDetails
+ Vielen Dank für Ihr Vertrauen!
+
+
+