Compare commits
42
Commits
1.1.0
..
bc17f7256b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc17f7256b
|
||
|
|
d2f3eea8a5
|
||
|
|
f902c61490
|
||
|
|
11e8cc5b1d
|
||
|
|
84155b7404
|
||
|
|
45b37197ec
|
||
|
|
f65848924c
|
||
|
|
98d584867e
|
||
|
|
376e8de1a4
|
||
|
|
2911391fb9
|
||
|
|
6d2e0241c9
|
||
|
|
afc5b1f0c6
|
||
|
|
4a76ee469b
|
||
|
|
b58bf700df
|
||
|
|
efd3a35802
|
||
|
|
0f7e44a42a
|
||
|
|
f90e5d75fa
|
||
|
|
31d4ec5f27
|
||
|
|
d61d4d6e7e
|
||
|
|
606ce6b940
|
||
|
|
750fa70332
|
||
|
|
7d503edbc9
|
||
|
|
5c9235df8d
|
||
|
|
11ea0858bb
|
||
|
|
4d57cf827d
|
||
|
|
df9f7fdc13
|
||
|
|
cdd2b5e250
|
||
|
|
94b766f106
|
||
|
|
a2e94f715b
|
||
|
|
f64daaf817
|
||
|
|
b4bb732303
|
||
|
|
3dee3e72af
|
||
|
|
f5914e5c38
|
||
|
|
5a5a7179e9
|
||
|
|
6c57d63891
|
||
|
|
b502e2fbd5
|
||
|
|
c9475d0093
|
||
|
|
1cc19e0085
|
||
|
|
b792806481
|
||
|
|
ea6a4a7080
|
||
|
|
de6fe4991c
|
||
|
|
1d068b2655
|
No files matched your search
@@ -1,3 +1,4 @@
|
||||
LOGLEVEL=debug
|
||||
PORT=3000
|
||||
PRODUCION=false
|
||||
APIKEY=lfk
|
||||
@@ -10,9 +11,13 @@ CARD_SUBTITLE=Kaya ist cool
|
||||
CARD_BARCODEFORMAT=ean13
|
||||
# CARD_BARCODEPREFIX=
|
||||
|
||||
SPONSOING_RECEIPTMINIMUM=10
|
||||
SPONSORING_RECEIPTMINIMUM=40
|
||||
SPONSORING_DISCLAIMER=Kaya ist cool, aber pass auf, dass du nicht zu viel Geld sammelst!
|
||||
SPONSORING_BARCODEFORMAT=code128
|
||||
# SPONSORING_BARCODEPREFIX=
|
||||
|
||||
CERTIFICATE_FOOTER=Kaya ist cool, danke für deine Unterstützung!
|
||||
CERTIFICATE_FOOTER=Kaya ist cool, danke für deine Unterstützung!
|
||||
|
||||
SEPA_BIC=FNOMDEB2
|
||||
SEPA_NAME=ODIT.Services
|
||||
SEPA_IBAN=DE25100180000690238989
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Build latest image
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: registry.odit.services
|
||||
username: ${{ vars.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
tags: |
|
||||
${{ vars.REGISTRY }}/lfk/document-server:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Build release images
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*.*.*"
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: registry.odit.services
|
||||
username: ${{ vars.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
tags: |
|
||||
${{ vars.REGISTRY }}/lfk/document-server:${{ github.ref_name }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -1,18 +0,0 @@
|
||||
steps:
|
||||
- name: build latest
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
repo: registry.odit.services/lfk/document-server
|
||||
tags:
|
||||
- latest
|
||||
registry: registry.odit.services
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache_from: registry.odit.services/lfk/document-server:latest
|
||||
username:
|
||||
from_secret: odit-registry-builder-username
|
||||
password:
|
||||
from_secret: odit-registry-builder-password
|
||||
when:
|
||||
branch: main
|
||||
when:
|
||||
event: push
|
||||
@@ -1,17 +0,0 @@
|
||||
steps:
|
||||
- name: build tag
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
repo: registry.odit.services/lfk/document-server
|
||||
tags:
|
||||
- "${CI_COMMIT_TAG}"
|
||||
registry: registry.odit.services
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache_from: registry.odit.services/lfk/document-server:latest
|
||||
username:
|
||||
from_secret: odit-registry-builder-username
|
||||
password:
|
||||
from_secret: odit-registry-builder-password
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
+18
-5
@@ -70,6 +70,15 @@ const docTemplate = `{
|
||||
"description": "Barcode height",
|
||||
"name": "height",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"maximum": 100,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"default": 10,
|
||||
"description": "Padding around the barcode (included in image size)",
|
||||
"name": "padding",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {}
|
||||
@@ -312,18 +321,22 @@ const docTemplate = `{
|
||||
"models.Group": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent_group": {
|
||||
"$ref": "#/definitions/models.Group"
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+18
-5
@@ -61,6 +61,15 @@
|
||||
"description": "Barcode height",
|
||||
"name": "height",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"maximum": 100,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"default": 10,
|
||||
"description": "Padding around the barcode (included in image size)",
|
||||
"name": "padding",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {}
|
||||
@@ -303,18 +312,22 @@
|
||||
"models.Group": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent_group": {
|
||||
"$ref": "#/definitions/models.Group"
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+13
-4
@@ -94,14 +94,16 @@ definitions:
|
||||
type: object
|
||||
models.Group:
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
parent_group:
|
||||
$ref: '#/definitions/models.Group'
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
type: object
|
||||
models.Runner:
|
||||
@@ -195,6 +197,13 @@ paths:
|
||||
minimum: 1
|
||||
name: height
|
||||
type: integer
|
||||
- default: 10
|
||||
description: Padding around the barcode (included in image size)
|
||||
in: query
|
||||
maximum: 100
|
||||
minimum: 0
|
||||
name: padding
|
||||
type: integer
|
||||
produces:
|
||||
- image/png
|
||||
responses: {}
|
||||
|
||||
@@ -49,7 +49,8 @@ require (
|
||||
github.com/valyala/fasthttp v1.57.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
|
||||
@@ -107,6 +107,10 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
|
||||
+24
-6
@@ -11,14 +11,17 @@ import (
|
||||
// @Summary Generate barcodes
|
||||
// @Description Generate barcodes based on the provided data
|
||||
// @Tags barcodes
|
||||
// @Param type path string true "Barcode type" Enums(ean13, code128)
|
||||
// @Param content path string true "Barcode content" MinLength(1)
|
||||
// @Param width query int false "Barcode width" Minimum(1) Maximum(10000) default(1000)
|
||||
// @Param height query int false "Barcode height" Minimum(1) Maximum(10000) default(1000)
|
||||
// @Param type path string true "Barcode type" Enums(ean13, code128)
|
||||
// @Param content path string true "Barcode content" MinLength(1)
|
||||
// @Param width query int false "Barcode width" Minimum(1) Maximum(10000) default(1000)
|
||||
// @Param height query int false "Barcode height" Minimum(1) Maximum(10000) default(1000)
|
||||
// @Param padding query int false "Padding around the barcode (included in image size)" Minimum(0) Maximum(100) default(10)
|
||||
// @Produce image/png
|
||||
// @Router /v1/barcodes/{type}/{content} [get]
|
||||
func (h *DefaultHandler) GenerateBarcode(c *fiber.Ctx) error {
|
||||
|
||||
logger := h.Logger.Named("GenerateBarcode")
|
||||
|
||||
// Get the type and content from the URL
|
||||
barcodeType := c.Params("type")
|
||||
barcodeContent := c.Params("content")
|
||||
@@ -26,10 +29,12 @@ func (h *DefaultHandler) GenerateBarcode(c *fiber.Ctx) error {
|
||||
// Get the width and height from the query parameters
|
||||
widthStr := c.Query("width", "1000")
|
||||
heightStr := c.Query("height", "1000")
|
||||
paddingStr := c.Query("padding", "10")
|
||||
|
||||
// Convert width and height to integers
|
||||
width, err := strconv.Atoi(widthStr)
|
||||
if err != nil {
|
||||
logger.Errorw("Invalid width parameter", "width", widthStr, "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid width parameter",
|
||||
})
|
||||
@@ -37,18 +42,31 @@ func (h *DefaultHandler) GenerateBarcode(c *fiber.Ctx) error {
|
||||
|
||||
height, err := strconv.Atoi(heightStr)
|
||||
if err != nil {
|
||||
logger.Errorw("Invalid height parameter", "height", heightStr, "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid height parameter",
|
||||
})
|
||||
}
|
||||
|
||||
// Generate the barcode
|
||||
barcode, err := h.BarcodeService.GenerateBarcode(barcodeType, barcodeContent, width, height)
|
||||
padding, err := strconv.Atoi(paddingStr)
|
||||
if err != nil {
|
||||
logger.Errorw("Invalid padding parameter", "padding", paddingStr, "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid padding parameter",
|
||||
})
|
||||
}
|
||||
logger = logger.With("type", barcodeType, "content", barcodeContent, "width", width, "height", height, "padding", padding)
|
||||
|
||||
// Generate the barcode
|
||||
logger.Info("Generating barcode")
|
||||
barcode, err := h.BarcodeService.GenerateBarcode(barcodeType, barcodeContent, width, height, padding)
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to generate barcode", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
logger.Info("Barcode generated")
|
||||
|
||||
c.Set(fiber.HeaderContentType, "image/png")
|
||||
return c.Send(barcode.Bytes())
|
||||
|
||||
+15
-2
@@ -1,7 +1,6 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"slices"
|
||||
|
||||
"git.odit.services/lfk/document-server/models"
|
||||
@@ -19,27 +18,35 @@ import (
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /v1/pdfs/cards [post]
|
||||
func (h *DefaultHandler) GenerateCard(c *fiber.Ctx) error {
|
||||
|
||||
logger := h.Logger.Named("GenerateCard")
|
||||
|
||||
cardRequest := new(models.CardRequest)
|
||||
if err := c.BodyParser(cardRequest); err != nil {
|
||||
logger.Errorw("Invalid request", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
if !slices.Contains([]string{"en", "de"}, cardRequest.Locale) {
|
||||
logger.Errorw("Invalid locale", "locale", cardRequest.Locale)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid locale",
|
||||
})
|
||||
}
|
||||
|
||||
logger = logger.With("locale", cardRequest.Locale)
|
||||
|
||||
templateString, err := h.StaticService.GetTemplate(cardRequest.Locale, "card")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
logger.Errorw("Template not found", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Template not found",
|
||||
})
|
||||
}
|
||||
template, err := h.Templater.StringToTemplate(templateString)
|
||||
if err != nil {
|
||||
logger.Errorw("Error parsing template", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
@@ -53,20 +60,26 @@ func (h *DefaultHandler) GenerateCard(c *fiber.Ctx) error {
|
||||
BarcodePrefix: h.Config.CardBarcodePrefix,
|
||||
}
|
||||
|
||||
logger.Info("Generating card html")
|
||||
result, err := h.Templater.Execute(template, genConfig)
|
||||
if err != nil {
|
||||
logger.Errorw("Error executing template", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
logger.Info("Generated card html")
|
||||
c.Set(fiber.HeaderContentType, "text/html")
|
||||
|
||||
logger.Info("Converting html to pdf")
|
||||
pdf, err := h.Converter.ToPdf(result, "a4", false)
|
||||
if err != nil {
|
||||
logger.Errorw("Error converting html to pdf", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
logger.Info("Converted html to pdf")
|
||||
|
||||
c.Set(fiber.HeaderContentType, "application/pdf")
|
||||
c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-cards.pdf")
|
||||
|
||||
+26
-2
@@ -1,7 +1,6 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"slices"
|
||||
|
||||
"git.odit.services/lfk/document-server/models"
|
||||
@@ -19,27 +18,35 @@ import (
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /v1/pdfs/certificates [post]
|
||||
func (h *DefaultHandler) GenerateCertificate(c *fiber.Ctx) error {
|
||||
|
||||
logger := h.Logger.Named("GenerateCertificate")
|
||||
|
||||
certificateRequest := new(models.CertificateRequest)
|
||||
if err := c.BodyParser(certificateRequest); err != nil {
|
||||
logger.Errorw("Invalid request", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
if !slices.Contains([]string{"en", "de"}, certificateRequest.Locale) {
|
||||
logger.Errorw("Invalid locale", "locale", certificateRequest.Locale)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid locale",
|
||||
})
|
||||
}
|
||||
|
||||
logger = logger.With("locale", certificateRequest.Locale)
|
||||
|
||||
templateString, err := h.StaticService.GetTemplate(certificateRequest.Locale, "certificate")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
logger.Errorw("Template not found", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Template not found",
|
||||
})
|
||||
}
|
||||
template, err := h.Templater.StringToTemplate(templateString)
|
||||
if err != nil {
|
||||
logger.Errorw("Error parsing template", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
@@ -51,22 +58,32 @@ func (h *DefaultHandler) GenerateCertificate(c *fiber.Ctx) error {
|
||||
Footer: h.Config.CertificateFooter,
|
||||
CurrencySymbol: h.Config.CurrencySymbol,
|
||||
Locale: certificateRequest.Locale,
|
||||
SepaConfig: &models.SepaConfig{
|
||||
BIC: h.Config.SepaBic,
|
||||
HolderName: h.Config.SepaName,
|
||||
IBAN: h.Config.SepaIban,
|
||||
CurrencyIdentifier: h.Config.CurrencyIdentifier,
|
||||
},
|
||||
}
|
||||
|
||||
logger.Info("Generating certificate html")
|
||||
result, err := h.Templater.Execute(template, genConfig)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
logger.Info("Generated card html")
|
||||
c.Set(fiber.HeaderContentType, "text/html")
|
||||
|
||||
logger.Info("Converting html to pdf")
|
||||
pdf, err := h.Converter.ToPdf(result, "a4", false)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
logger.Info("Converted html to pdf")
|
||||
|
||||
c.Set(fiber.HeaderContentType, "application/pdf")
|
||||
c.Set(fiber.HeaderContentDisposition, "attachment; filename=certificate.pdf")
|
||||
@@ -79,6 +96,13 @@ func addUpRunnerDonations(runners []models.RunnerWithDonations) []models.RunnerW
|
||||
runners[i].TotalDonations += runners[i].DistanceDonations[j].Amount
|
||||
runners[i].TotalPerDistance += runners[i].DistanceDonations[j].AmountPerDistance
|
||||
}
|
||||
if runners[i].Group.ParentGroup.Name != "" {
|
||||
runners[i].CombinedGroupName = runners[i].Group.ParentGroup.Name + " - " + runners[i].Group.Name
|
||||
} else {
|
||||
runners[i].CombinedGroupName = runners[i].Group.Name
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
return runners
|
||||
}
|
||||
+13
-4
@@ -1,7 +1,6 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"slices"
|
||||
|
||||
"git.odit.services/lfk/document-server/models"
|
||||
@@ -19,36 +18,42 @@ import (
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /v1/pdfs/contracts [post]
|
||||
func (h *DefaultHandler) GenerateContract(c *fiber.Ctx) error {
|
||||
|
||||
logger := h.Logger.Named("GenerateContract")
|
||||
|
||||
contract := new(models.ContractRequest)
|
||||
if err := c.BodyParser(contract); err != nil {
|
||||
logger.Errorw("Invalid request", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
if !slices.Contains([]string{"en", "de"}, contract.Locale) {
|
||||
logger.Errorw("Invalid locale", "locale", contract.Locale)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid locale",
|
||||
})
|
||||
}
|
||||
|
||||
contract.Runners = repeatRunnerArrayItems(contract.Runners, 2)
|
||||
logger = logger.With("locale", contract.Locale)
|
||||
|
||||
templateString, err := h.StaticService.GetTemplate(contract.Locale, "contract")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
logger.Errorw("Template not found", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Template not found",
|
||||
})
|
||||
}
|
||||
template, err := h.Templater.StringToTemplate(templateString)
|
||||
if err != nil {
|
||||
logger.Errorw("Error parsing template", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
genConfig := &models.ContractTemplateOptions{
|
||||
Runners: contract.Runners,
|
||||
Runners: repeatRunnerArrayItems(contract.Runners, 2),
|
||||
CurrencySymbol: h.Config.CurrencySymbol,
|
||||
Disclaimer: h.Config.SponosringDisclaimer,
|
||||
ReceiptMinimumAmount: h.Config.SponsoringReceiptMinimum,
|
||||
@@ -57,19 +62,23 @@ func (h *DefaultHandler) GenerateContract(c *fiber.Ctx) error {
|
||||
BarcodePrefix: h.Config.SponsoringBarcodePrefix,
|
||||
}
|
||||
|
||||
logger.Info("Generating contract html")
|
||||
result, err := h.Templater.Execute(template, genConfig)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
logger.Info("Generated contract html")
|
||||
|
||||
logger.Info("Converting html to pdf")
|
||||
pdf, err := h.Converter.ToPdf(result, "a5", true)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
logger.Info("Converted html to pdf")
|
||||
|
||||
c.Set(fiber.HeaderContentType, "application/pdf")
|
||||
c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-contracts.pdf")
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"git.odit.services/lfk/document-server/models"
|
||||
"git.odit.services/lfk/document-server/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
@@ -19,4 +20,5 @@ type DefaultHandler struct {
|
||||
Templater services.Templater
|
||||
Converter services.Converter
|
||||
StaticService services.StaticService
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
@@ -3,21 +3,27 @@ package main
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.odit.services/lfk/document-server/docs" // Correct import path for docs
|
||||
"git.odit.services/lfk/document-server/handlers"
|
||||
"git.odit.services/lfk/document-server/models"
|
||||
"git.odit.services/lfk/document-server/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/keyauth"
|
||||
"github.com/gofiber/fiber/v2/middleware/requestid"
|
||||
"github.com/gofiber/swagger"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
var (
|
||||
config *models.Config
|
||||
logger *zap.SugaredLogger
|
||||
)
|
||||
|
||||
func validateAPIKey(c *fiber.Ctx, key string) (bool, error) {
|
||||
@@ -32,11 +38,26 @@ func validateAPIKey(c *fiber.Ctx, key string) (bool, error) {
|
||||
|
||||
func loadEnv() error {
|
||||
|
||||
viper.SetDefault("LOGLEVEL", "INFO")
|
||||
viper.SetDefault("PRODUCION", false)
|
||||
viper.SetDefault("PORT", "3000")
|
||||
viper.SetDefault("APIKEY", "lfk")
|
||||
viper.SetDefault("EVENTNAME", "Demo Event")
|
||||
viper.SetDefault("CURRENCYSYMBOL", "€")
|
||||
viper.SetDefault("CURRENCYIDENTIFIER", "EUR")
|
||||
viper.SetDefault("CARD_SUBTITLE", "Runner Card")
|
||||
viper.SetDefault("CARD_BARCODEFORMAT", "ean13")
|
||||
viper.SetDefault("CARD_BARCODEPREFIX", "")
|
||||
viper.SetDefault("SPONSORING_RECEIPTMINIMUM", 0)
|
||||
viper.SetDefault("SPONSORING_DISCLAIMER", "Disclaimer")
|
||||
viper.SetDefault("SPONSORING_BARCODEFORMAT", "code128")
|
||||
viper.SetDefault("SPONSORING_BARCODEPREFIX", "")
|
||||
viper.SetDefault("CERTIFICATE_FOOTER", "Footer")
|
||||
viper.SetDefault("GOTENBERG_BASEURL", "")
|
||||
viper.SetDefault("REDIS_ADDR", "")
|
||||
viper.SetDefault("SEPA_BIC", "")
|
||||
viper.SetDefault("SEPA_NAME", "")
|
||||
viper.SetDefault("SEPA_IBAN", "")
|
||||
|
||||
// Load .env file
|
||||
viper.SetConfigFile(".env")
|
||||
@@ -45,7 +66,7 @@ func loadEnv() error {
|
||||
viper.AutomaticEnv()
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
log.Println("No .env file found")
|
||||
logger.Warn("No .env file found")
|
||||
}
|
||||
|
||||
// Unmarshal the config from file and env into the config struct
|
||||
@@ -54,29 +75,65 @@ func loadEnv() error {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infow("Loaded config", "config", &config)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initLogger() error {
|
||||
logLevel := os.Getenv("LOGLEVEL")
|
||||
if logLevel == "" {
|
||||
logLevel = "INFO"
|
||||
}
|
||||
|
||||
var zapLogLevel zapcore.Level
|
||||
err := zapLogLevel.UnmarshalText([]byte(strings.ToLower(logLevel)))
|
||||
if err != nil {
|
||||
zapLogLevel = zapcore.InfoLevel
|
||||
}
|
||||
|
||||
zapConfig := zap.NewProductionConfig()
|
||||
zapConfig.Level = zap.NewAtomicLevelAt(zapLogLevel)
|
||||
zapLogger, err := zapConfig.Build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer zapLogger.Sync()
|
||||
logger = zapLogger.Sugar()
|
||||
|
||||
logger.Debug("Initialized logger")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @title LfK Document Server API
|
||||
// @description This is the API documentation for the LfK Document Server - a tool for pdf generation.
|
||||
// @description This is the API documentation for the LfK Document Server - a tool for pdf generation.
|
||||
// @license.name CC BY-NC-SA 4.0
|
||||
// @termsOfService https://lauf-fuer-kaya.de/datenschutz
|
||||
// @contact.name ODIT.Services UG (haftungsbeschränkt)
|
||||
// @contact.url https://odit.services
|
||||
// @contact.url https://odit.services
|
||||
// @contact.email info@odit.services
|
||||
// @securityDefinitions.apiKey ApiKeyAuth
|
||||
// @in query
|
||||
// @name key
|
||||
func main() {
|
||||
|
||||
err := loadEnv()
|
||||
// Init the logger
|
||||
err := initLogger()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = loadEnv()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
var redisClient *redis.Client
|
||||
if config.RedisAddr != "" {
|
||||
log.Println("Using redis at", config.RedisAddr)
|
||||
logger.Infow("Using redis", "redisAddr", config.RedisAddr)
|
||||
redisClient = redis.NewClient(&redis.Options{
|
||||
Addr: config.RedisAddr,
|
||||
})
|
||||
@@ -84,9 +141,11 @@ func main() {
|
||||
|
||||
barcodeGenerator := &services.DefaultBarcodeService{
|
||||
RedisClient: redisClient,
|
||||
Logger: logger.Named("DefaultBarcodeService"),
|
||||
}
|
||||
staticService := &services.DefaultStaticService{
|
||||
Cache: make(map[string]string),
|
||||
Cache: make(map[string]string),
|
||||
Logger: logger.Named("DefaultStaticService"),
|
||||
}
|
||||
handler := handlers.DefaultHandler{
|
||||
Config: config,
|
||||
@@ -95,17 +154,24 @@ func main() {
|
||||
Templater: &services.DefaultTemplater{
|
||||
BarcodeService: barcodeGenerator,
|
||||
StaticService: staticService,
|
||||
Logger: logger.Named("DefaultTemplater"),
|
||||
},
|
||||
Converter: &services.GotenbergConverter{
|
||||
BaseUrl: config.GotenbergBaseUrl,
|
||||
Logger: logger.Named("GotenbergConverter"),
|
||||
},
|
||||
Logger: logger.Named("DefaultHandler"),
|
||||
}
|
||||
logger.Debug("Initialized services")
|
||||
|
||||
// Create a new Fiber instance
|
||||
app := fiber.New(fiber.Config{
|
||||
Prefork: config.Prod,
|
||||
})
|
||||
|
||||
app.Use(cors.New())
|
||||
app.Use(requestid.New())
|
||||
|
||||
// Swagger documentation route
|
||||
app.Get("/swagger/*", swagger.HandlerDefault)
|
||||
|
||||
@@ -122,9 +188,11 @@ func main() {
|
||||
pdfv1.Post("/certificates", handler.GenerateCertificate)
|
||||
|
||||
v1.Get("/barcodes/:type/:content", handler.GenerateBarcode)
|
||||
logger.Debug("Initialized routes")
|
||||
|
||||
app.Use(handler.NotFoundHandler)
|
||||
docs.SwaggerInfo.BasePath = "/"
|
||||
|
||||
log.Fatal(app.Listen("0.0.0.0:" + config.Port))
|
||||
logger.Infow("Starting server", "port", config.Port)
|
||||
logger.Error(app.Listen("0.0.0.0:" + config.Port))
|
||||
}
|
||||
@@ -11,10 +11,12 @@ type RunnerWithDonations struct {
|
||||
MiddleName string `json:"middle_name" validate:"optional"`
|
||||
LastName string `json:"last_name" validate:"required"`
|
||||
Group Group `json:"group" validate:"required"`
|
||||
CombinedGroupName string `json:"combined_group_name" validate:"optional"`
|
||||
Distance int `json:"distance" validate:"required"`
|
||||
DistanceDonations []DistanceDonation `json:"distance_donations" validate:"optional"`
|
||||
TotalPerDistance int `json:"total_per_distance" validate:"optional"`
|
||||
TotalDonations int `json:"total_donations" validate:"optional"`
|
||||
SelfServiceLink string `json:"self_service_link" validate:"required"`
|
||||
}
|
||||
|
||||
type DistanceDonation struct {
|
||||
@@ -38,4 +40,12 @@ type CertificateTemplateOptions struct {
|
||||
Footer string `json:"footer"`
|
||||
CurrencySymbol string `json:"currency_symbol"`
|
||||
Locale string `json:"locale"`
|
||||
SepaConfig *SepaConfig `json:"sepa_config"`
|
||||
}
|
||||
|
||||
type SepaConfig struct {
|
||||
IBAN string `json:"iban" validate:"required"`
|
||||
HolderName string `json:"holder_name" validate:"required"`
|
||||
BIC string `json:"bic" validate:"required"`
|
||||
CurrencyIdentifier string `json:"currency_identifier" validate:"required"`
|
||||
}
|
||||
+6
-1
@@ -1,19 +1,24 @@
|
||||
package models
|
||||
|
||||
type Config struct {
|
||||
LogLevel string `mapstructure:"LOGLEVEL"`
|
||||
Prod bool `mapstructure:"PRODUCION"`
|
||||
Port string `mapstructure:"PORT"`
|
||||
APIKey string `mapstructure:"APIKEY"`
|
||||
EventName string `mapstructure:"EVENTNAME"`
|
||||
CurrencySymbol string `mapstructure:"CURRENCYSYMBOL"`
|
||||
CurrencyIdentifier string `mapstructure:"CURRENCYIDENTIFIER"`
|
||||
CardSubtitle string `mapstructure:"CARD_SUBTITLE"`
|
||||
CardBarcodeFormat string `mapstructure:"CARD_BARCODEFORMAT"`
|
||||
CardBarcodePrefix string `mapstructure:"CARD_BARCODEPREFIX"`
|
||||
SponsoringReceiptMinimum int `mapstructure:"SPONSOING_RECEIPTMINIMUM"`
|
||||
SponsoringReceiptMinimum string `mapstructure:"SPONSORING_RECEIPTMINIMUM"`
|
||||
SponosringDisclaimer string `mapstructure:"SPONSORING_DISCLAIMER"`
|
||||
SponsoringBarcodeFormat string `mapstructure:"SPONSORING_BARCODEFORMAT"`
|
||||
SponsoringBarcodePrefix string `mapstructure:"SPONSORING_BARCODEPREFIX"`
|
||||
CertificateFooter string `mapstructure:"CERTIFICATE_FOOTER"`
|
||||
GotenbergBaseUrl string `mapstructure:"GOTENBERG_BASEURL"`
|
||||
RedisAddr string `mapstructure:"REDIS_ADDR"`
|
||||
SepaBic string `mapstructure:"SEPA_BIC"`
|
||||
SepaName string `mapstructure:"SEPA_NAME"`
|
||||
SepaIban string `mapstructure:"SEPA_IBAN"`
|
||||
}
|
||||
+5
-2
@@ -14,14 +14,17 @@ type Runner struct {
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
ParentGroup struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
} `json:"parent_group" validate:"optional"`
|
||||
}
|
||||
|
||||
type ContractTemplateOptions struct {
|
||||
Runners []Runner `json:"runners"`
|
||||
CurrencySymbol string `json:"currency_symbol"`
|
||||
Disclaimer string `json:"disclaimer"`
|
||||
ReceiptMinimumAmount int `json:"receipt_minimum_amount"`
|
||||
ReceiptMinimumAmount string `json:"receipt_minimum_amount"`
|
||||
EventName string `json:"event_name"`
|
||||
BarcodeFormat string `json:"barcode_format"`
|
||||
BarcodePrefix string `json:"barcode_prefix"`
|
||||
|
||||
+45
-9
@@ -4,8 +4,10 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"log"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
@@ -14,28 +16,36 @@ import (
|
||||
"github.com/boombuler/barcode/ean"
|
||||
"github.com/boombuler/barcode/qr"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type BarcodeService interface {
|
||||
GenerateBarcode(format string, content string, width int, height int) (bytes.Buffer, error)
|
||||
GenerateBarcode(format string, content string, width int, height int, padding int) (bytes.Buffer, error)
|
||||
IsTypeSupported(format string) bool
|
||||
}
|
||||
|
||||
type DefaultBarcodeService struct {
|
||||
RedisClient *redis.Client
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func (b *DefaultBarcodeService) GenerateBarcode(format string, content string, width int, height int) (bytes.Buffer, error) {
|
||||
func (b *DefaultBarcodeService) GenerateBarcode(format string, content string, width int, height int, padding int) (bytes.Buffer, error) {
|
||||
ctx := context.Background()
|
||||
logger := b.Logger.Named("GenerateBarcode")
|
||||
|
||||
if !b.IsTypeSupported(format) {
|
||||
logger.Errorw("Unsupported barcode type", "type", format)
|
||||
return bytes.Buffer{}, fmt.Errorf("unsupported barcode type: %s", format)
|
||||
}
|
||||
|
||||
logger = logger.With("type", format, "content", content, "width", width, "height", height, "padding", padding)
|
||||
cacheKey := fmt.Sprintf("barcode:%s:%s:%d:%d:%d", format, content, width, height, padding)
|
||||
|
||||
if b.RedisClient != nil {
|
||||
cachedBarcode, err := b.RedisClient.Get(ctx, fmt.Sprintf("barcode:%s:%s:%d:%d", format, content, width, height)).Result()
|
||||
logger.Debugw("Checking cache for barcode", "key", cacheKey)
|
||||
cachedBarcode, err := b.RedisClient.Get(ctx, cacheKey).Result()
|
||||
if err == nil {
|
||||
log.Printf("Cache hit for barcode:%s:%s:%d:%d", format, content, width, height)
|
||||
logger.Infow("Barcode found in cache", "key", cacheKey)
|
||||
buf := bytes.Buffer{}
|
||||
buf.Write([]byte(cachedBarcode))
|
||||
return buf, nil
|
||||
@@ -59,30 +69,56 @@ func (b *DefaultBarcodeService) GenerateBarcode(format string, content string, w
|
||||
}
|
||||
break
|
||||
case "qr":
|
||||
generatedCode, err = qr.Encode(content, qr.M, qr.AlphaNumeric)
|
||||
// Always use qr.Auto encoding to support all characters in the content
|
||||
encoding := qr.Auto
|
||||
|
||||
// QR code generation with error correction level M and auto encoding
|
||||
generatedCode, err = qr.Encode(content, qr.M, encoding)
|
||||
if err != nil {
|
||||
return bytes.Buffer{}, err
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
scaledCode, err := barcode.Scale(generatedCode, width, height)
|
||||
// Create a white background image
|
||||
bg := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
white := color.RGBA{255, 255, 255, 255}
|
||||
draw.Draw(bg, bg.Bounds(), &image.Uniform{white}, image.Point{}, draw.Src)
|
||||
logger.Debug("Created white background")
|
||||
|
||||
// Calculate the new size for the barcode to fit within the padding
|
||||
newWidth := width - 2*padding
|
||||
newHeight := height - 2*padding
|
||||
|
||||
// Scale the barcode to the new size
|
||||
scaledCode, err := barcode.Scale(generatedCode, newWidth, newHeight)
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to scale barcode", "error", err)
|
||||
return bytes.Buffer{}, err
|
||||
}
|
||||
logger.Debug("Scaled barcode")
|
||||
|
||||
// Draw the barcode on top of the white background with padding
|
||||
draw.Draw(bg, scaledCode.Bounds().Add(image.Point{padding, padding}), scaledCode, image.Point{}, draw.Over)
|
||||
logger.Debug("Drew barcode on background")
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = png.Encode(&buf, scaledCode)
|
||||
err = png.Encode(&buf, bg)
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to encode barcode to PNG", "error", err)
|
||||
return bytes.Buffer{}, err
|
||||
}
|
||||
logger.Debug("Encoded barcode to PNG")
|
||||
|
||||
if b.RedisClient != nil {
|
||||
err = b.RedisClient.Set(ctx, fmt.Sprintf("barcode:%s:%s:%d:%d", format, content, width, height), buf.String(), 10*time.Minute).Err()
|
||||
err = b.RedisClient.Set(ctx, cacheKey, buf.String(), 10*time.Minute).Err()
|
||||
logger.Debugw("Cached barcode", "key", cacheKey)
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to cache barcode", "error", err)
|
||||
return bytes.Buffer{}, err
|
||||
}
|
||||
}
|
||||
logger.Info("Generated barcode")
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/oxplot/papersizes"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Converter interface {
|
||||
@@ -16,92 +17,115 @@ type Converter interface {
|
||||
|
||||
type GotenbergConverter struct {
|
||||
BaseUrl string
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func (g *GotenbergConverter) ToPdf(html string, pageSize string, landscape bool) ([]byte, error) {
|
||||
logger := g.Logger.Named("ToPdf").With("page_size", pageSize, "landscape", landscape, "base_url", g.BaseUrl)
|
||||
|
||||
client := &http.Client{}
|
||||
defer client.CloseIdleConnections()
|
||||
logger.Debug("Created HTTP client")
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part, err := writer.CreateFormFile("files", "index.html")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to create form file", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = part.Write([]byte(html))
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write to form file", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
size := papersizes.FromName(pageSize)
|
||||
if size == nil {
|
||||
logger.Errorw("Invalid page size", "size", pageSize)
|
||||
return nil, fmt.Errorf("invalid page size: %s", pageSize)
|
||||
}
|
||||
|
||||
err = writer.WriteField("paperWidth", strconv.Itoa(size.Width)+"mm")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write paper width", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.WriteField("paperHeight", strconv.Itoa(size.Height)+"mm")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write paper height", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.WriteField("landscape", strconv.FormatBool(landscape))
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write landscape", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.WriteField("marginTop", "0")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write margin top", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.WriteField("marginBottom", "0")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write margin bottom", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.WriteField("marginLeft", "0")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write margin left", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.WriteField("marginRight", "0")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write margin right", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.WriteField("preferCssPageSize", "true")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write prefer css page size", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to close writer", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
logger.Debug("Created form data")
|
||||
|
||||
logger.Debug("Creating HTTP request")
|
||||
req, err := http.NewRequest("POST", g.BaseUrl+"/forms/chromium/convert/html", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
logger.Debug("Sending HTTP request")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to send request", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
logger.Debug("Received HTTP response")
|
||||
|
||||
defer resp.Body.Close()
|
||||
data := new(bytes.Buffer)
|
||||
_, err = data.ReadFrom(resp.Body)
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to read response body", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Debug("Returning PDF data")
|
||||
return data.Bytes(), nil
|
||||
}
|
||||
+30
-1
@@ -7,6 +7,8 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Templater interface {
|
||||
@@ -17,6 +19,7 @@ type Templater interface {
|
||||
type DefaultTemplater struct {
|
||||
BarcodeService BarcodeService
|
||||
StaticService StaticService
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func idToEan13(id string, prefix string) (string, error) {
|
||||
@@ -32,6 +35,28 @@ func idToEan13(id string, prefix string) (string, error) {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (t *DefaultTemplater) GenerateEPC(iban string, bic string, name string, title string, amount int, currency string) (string, error) {
|
||||
var err error
|
||||
|
||||
code := fmt.Sprintf(`BCD
|
||||
002
|
||||
1
|
||||
INST
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s%.2f
|
||||
|
||||
%s
|
||||
|
||||
`, bic, name, iban, currency, float64(amount)/100, title,
|
||||
)
|
||||
|
||||
buf, err := t.BarcodeService.GenerateBarcode("qr", code, 500, 500, 0)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(buf.Bytes()), err
|
||||
}
|
||||
|
||||
func (t *DefaultTemplater) GenerateBarcode(code string, format string, prefix string) (string, error) {
|
||||
var err error
|
||||
|
||||
@@ -42,16 +67,19 @@ func (t *DefaultTemplater) GenerateBarcode(code string, format string, prefix st
|
||||
}
|
||||
}
|
||||
|
||||
buf, err := t.BarcodeService.GenerateBarcode(format, code, 1000, 500)
|
||||
buf, err := t.BarcodeService.GenerateBarcode(format, code, 1000, 500, 0)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(buf.Bytes()), err
|
||||
}
|
||||
|
||||
func (t *DefaultTemplater) SelectSponsorImage(id int) (string, error) {
|
||||
logger := t.Logger.Named("SelectSponsorImage")
|
||||
sponsors, err := t.StaticService.ListFilesInStaticSubFolder("images/sponsors")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to list sponsors", "error", err)
|
||||
return "", err
|
||||
}
|
||||
logger.Debugw("Selected sponsor", "sponsors", sponsors, "id", id, "selected", sponsors[id%len(sponsors)])
|
||||
return t.StaticService.GetImage("sponsors/" + strings.TrimSuffix(sponsors[id%len(sponsors)], ".base64")), nil
|
||||
}
|
||||
|
||||
@@ -82,6 +110,7 @@ func (t *DefaultTemplater) StringToTemplate(templateString string) (*template.Te
|
||||
"sponsorLogo": t.SelectSponsorImage,
|
||||
"formatUnit": t.FormatUnit,
|
||||
"loadImage": t.LoadImage,
|
||||
"epcCode": t.GenerateEPC,
|
||||
}).Parse(templateString)
|
||||
}
|
||||
|
||||
|
||||
+16
-7
@@ -3,8 +3,9 @@ package services
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type StaticService interface {
|
||||
@@ -14,47 +15,55 @@ type StaticService interface {
|
||||
}
|
||||
|
||||
type DefaultStaticService struct {
|
||||
Cache map[string]string
|
||||
Cache map[string]string
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func (s *DefaultStaticService) GetTemplate(locale, templateName string) (string, error) {
|
||||
logger := s.Logger.Named("GetTemplate").With("locale", locale, "template_name", templateName)
|
||||
|
||||
if s.Cache[locale+templateName] != "" {
|
||||
log.Printf("returning cached template %s with locale %s", templateName, locale)
|
||||
logger.Debugw("Template found in cache", "key", locale+templateName)
|
||||
return s.Cache[locale+templateName], nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(fmt.Sprintf("static/templates/%s/%s.html", templateName, locale))
|
||||
if content == nil || err != nil {
|
||||
log.Printf("error reading template %s with locale %s: %v", templateName, locale, err)
|
||||
logger.Errorw("Failed to read template", "error", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
s.Cache[locale+templateName] = string(content)
|
||||
logger.Debugw("Saved template to cache", "key", locale+templateName)
|
||||
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
func (s *DefaultStaticService) ListFilesInStaticSubFolder(folderName string) ([]string, error) {
|
||||
logger := s.Logger.Named("ListFilesInStaticSubFolder").With("folder_name", folderName)
|
||||
|
||||
files, err := os.ReadDir(fmt.Sprintf("static/%s", folderName))
|
||||
if err != nil {
|
||||
log.Printf("error reading files from folder %s: %v", folderName, err)
|
||||
logger.Errorw("Failed to list files", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
var images []string
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
logger.Debugw("Skipping directory", "file", file.Name())
|
||||
}
|
||||
images = append(images, file.Name())
|
||||
}
|
||||
logger.Debugw("Listed files", "files", images)
|
||||
return images, nil
|
||||
}
|
||||
|
||||
func (s *DefaultStaticService) GetImage(imageName string) string {
|
||||
logger := s.Logger.Named("GetImage").With("image_name", imageName)
|
||||
|
||||
content, err := os.ReadFile("static/images/" + imageName + ".base64")
|
||||
if content == nil || err != nil {
|
||||
log.Printf("error reading image %s: %v", imageName, err)
|
||||
logger.Errorw("Failed to read image", "error", err)
|
||||
return ImageErrorBase64
|
||||
}
|
||||
return string(content)
|
||||
|
||||
@@ -1 +1 @@
|
||||
iVBORw0KGgoAAAANSUhEUgAAApQAAACWCAYAAABgmBgeAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAFHGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIxLTAzLTMxVDE0OjEyOjMzKzAyOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMS0wMy0zMVQxNDoxMzozMyswMjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMS0wMy0zMVQxNDoxMzozMyswMjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpmZDhjZTFlZi1lMTAy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 truncated
|
||||
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 truncated
|
||||
@@ -0,0 +1 @@
|
||||
iVBORw0KGgoAAAANSUhEUgAAA+gAAAJvCAYAAADoYfswAAAACXBIWXMAAC4jAAAuIwF4pT92AAAQ32lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDIgNzkuYTZhNjM5NjhhLCAyMDI0LzAzLzA2LTExOjUyOjA1ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnBkZj0iaHR0cDovL25zLmFkb2JlLmNvbS9wZGYvMS4zLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczp4bXBUUGc9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC90L3BnLyIgeG1sbnM6c3REaW09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9EaW1lbnNpb25zIyIgeG1sbnM6eG1wRz0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL2cvIiB4bWxuczppbGx1c3RyYXRvcj0iaHR0cDovL25zLmFkb2JlLmNvbS9pbGx1c3RyYXRvci8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1wOkNyZWF0ZURhdGU9IjIwMjQtMDctMTVUMTI6NTA6MDgrMDI6MDAiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgSWxsdXN0cmF0b3IgMjguNSAoTWFjaW50b3NoKSIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNC0wNy0xN1QxMjo1NDo1NCswMjowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjQtMDctMTdUMTI6NTQ6NTQrMDI6MDAiIHBkZjpQcm9kdWNlcj0iQWRvYmUgUERGIGxpYnJhcnkgMTcuMDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiB4bXBUUGc6TlBhZ2VzPSIxIiB4bXBUUGc6SGFzVmlzaWJsZVRyYW5zcGFyZW5jeT0iRmFsc2UiIHhtcFRQZzpIYXNWaXNpYmxlT3ZlcnByaW50PSJUcnVlIiBpbGx1c3RyYXRvcjpDcmVhdG9yU3ViVG9vbD0iQWRvYmUgSWxsdXN0cmF0b3IiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo5ZjNmZDFmMi0zNWVmLTkxNDUtOTA1Zi03ZmMzNGExMmEwNDEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NzMxNjE2YTctZWJhNy00MjRiLTlhZjgtZjY5MTRlNDBjMDRkIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MjAwMTFiN2QtZTdiYy00OGQ4LWE2MDQtOWE5ZDlkMGJjOTU5IiB4bXBNTTpSZW5kaXRpb25DbGFzcz0iZGVmYXVsdCIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyI+IDxkYzp0aXRsZT4gPHJkZjpBbHQ+IDxyZGY6bGkgeG1sOmxhbmc9IngtZGVmYXVsdCI+QXRsYW50aXNfTG9nb180Y19SZWxhdW5jaDIwMjRfY2xhaW1fdHVlcmtpczwvcmRmOmxpPiA8L3JkZjpBbHQ+IDwvZGM6dGl0bGU+IDx4bXBUUGc6TWF4UGFnZVNpemUgc3REaW06dz0iMjk3LjAwMDAwMiIgc3REaW06aD0iMTg2LjM1OTUzMiIgc3REaW06dW5pdD0iTWlsbGltZXRlcnMiLz4gPHhtcFRQZzpQbGF0ZU5hbWVzPiA8cmRmOlNlcT4gPHJkZjpsaT5DeWFuPC9yZGY6bGk+IDxyZGY6bGk+TWFnZW50YTwvcmRmOmxpPiA8cmRmOmxpPlllbGxvdzwvcmRmOmxpPiA8cmRmOmxpPkJsYWNrPC9yZGY6bGk+IDwvcmRmOlNlcT4gPC94bXBUUGc6UGxhdGVOYW1lcz4gPHhtcFRQZzpTd2F0Y2hHcm91cHM+IDxyZGY6U2VxPiA8cmRmOmxpPiA8cmRmOkRlc2NyaXB0aW9uIHhtcEc6Z3JvdXBOYW1lPSJTdGFuZGFyZC1GYXJiZmVsZGdydXBwZSIgeG1wRzpncm91cFR5cGU9IjAiPiA8eG1wRzpDb2xvcmFudHM+IDxyZGY6U2VxPiA8cmRmOmxpIHhtcEc6c3dhdGNoTmFtZT0iQmxhdSBBVExBTlRJUyIgeG1wRzp0eXBlPSJQUk9DRVNTIiB4bXBHOnRpbnQ9IjEwMC4wMDAwMDAiIHhtcEc6bW9kZT0iQ01ZSyIgeG1wRzpjeWFuPSIxMDAuMDAwMDAwIiB4bXBHOm1hZ2VudGE9IjY5Ljk5OTk5OSIgeG1wRzp5ZWxsb3c9IjEwLjAwMDAwMCIgeG1wRzpibGFjaz0iMC4wMDAwMDAiLz4gPHJkZjpsaSB4bXBHOnN3YXRjaE5hbWU9IkM9MCBNPTAgWT0wIEs9MCAxIiB4bXBHOm1vZGU9IkNNWUsiIHhtcEc6dHlwZT0iUFJPQ0VTUyIgeG1wRzpjeWFuPSIwLjAwMDAwMCIgeG1wRzptYWdlbnRhPSIwLjAwMDAwMCIgeG1wRzp5ZWxsb3c9IjAuMDAwMDAwIiB4bXBHOmJsYWNrPSIwLjAwMDAwMCIvPiA8cmRmOmxpIHhtcEc6c3dhdGNoTmFtZT0iVHVlcmtpcyIgeG1wRzp0eXBlPSJQUk9DRVNTIiB4bXBHOnRpbnQ9IjEwMC4wMDAwMDAiIHhtcEc6bW9kZT0iQ01ZSyIgeG1wRzpjeWFuPSI4OS45OTk5OTgiIHhtcEc6bWFnZW50YT0iNS4wMDAwMDAiIHhtcEc6eWVsbG93PSI0MC4wMDAwMDEiIHhtcEc6YmxhY2s9IjAuMDAwMDAwIi8+IDxyZGY6bGkgeG1wRzpzd2F0Y2hOYW1lPSJDPTAgTT0wIFk9MCBLPTEwMCIgeG1wRzptb2RlPSJDTVlLIiB4bXBHOnR5cGU9IlBST0NFU1MiIHhtcEc6Y3lhbj0iMC4wMDAwMDAiIHhtcEc6bWFnZW50YT0iMC4wMDAwMDAiIHhtcEc6eWVsbG93PSIwLjAwMDAwMCIgeG1wRzpibGFjaz0iMTAwLjAwMDAwMCIvPiA8L3JkZjpTZXE+IDwveG1wRzpDb2xvcmFudHM+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpsaT4gPC9yZGY6U2VxPiA8L3htcFRQZzpTd2F0Y2hHcm91cHM+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjExYmMwNDFlLTVhYjEtNDEwMC04ZDM3LTYyMGVjOWJkM2MyYyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxMWJjMDQxZS01YWIxLTQxMDAtOGQzNy02MjBlYzliZDNjMmMiIHN0UmVmOm9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoyMDAxMWI3ZC1lN2JjLTQ4ZDgtYTYwNC05YTlkOWQwYmM5NTkiIHN0UmVmOnJlbmRpdGlvbkNsYXNzPSJkZWZhdWx0Ii8+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjIwMDExYjdkLWU3YmMtNDhkOC1hNjA0LTlhOWQ5ZDBiYzk1OSIgc3RFdnQ6d2hlbj0iMjAyMy0wOS0yNVQxMjoyNTozNyswMjowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgSWxsdXN0cmF0b3IgMjcuOSAoTWFjaW50b3NoKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6ZjQwNWEyNjQtNTFkMS00OWY0LTg0NjQtM2U5ZTMyMmExOGRjIiBzdEV2dDp3aGVuPSIyMDI0LTA3LTE1VDEyOjUwOjA4KzAyOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBJbGx1c3RyYXRvciAyOC41IChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjb252ZXJ0ZWQiIHN0RXZ0OnBhcmFtZXRlcnM9ImZyb20gYXBwbGljYXRpb24vcG9zdHNjcmlwdCB0byBpbWFnZS9lcHNmIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoxMWJLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
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 truncated
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Sponsoring contract</title>
|
||||
<title>Läuferkarten</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
||||
<style>
|
||||
.sheet {
|
||||
@@ -50,8 +50,12 @@
|
||||
<p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ if ne .Runner.FirstName "" }}
|
||||
<p>{{ .Runner.LastName }}, {{ .Runner.FirstName }} {{ .Runner.MiddleName }}</p>
|
||||
<p>{{ .Runner.Group.Name }}</p>
|
||||
<p>{{ if ne .Runner.Group.ParentGroup.Name "" -}}{{ .Runner.Group.ParentGroup.Name }}/{{end -}}{{ .Runner.Group.Name }}</p>
|
||||
{{ else }}
|
||||
<p>Kein Läufer zugewiesen</p>
|
||||
{{ end}}
|
||||
{{ end}}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Sponsoring contract</title>
|
||||
<title>Runner cards</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
||||
<style>
|
||||
.sheet {
|
||||
@@ -49,8 +49,12 @@
|
||||
<p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ if ne .Runner.FirstName ""}}
|
||||
<p>{{ .Runner.LastName }}, {{ .Runner.FirstName }} {{ .Runner.MiddleName }}</p>
|
||||
<p>{{ .Runner.Group.Name }}</p>
|
||||
<p>{{ if ne .Runner.Group.ParentGroup.Name "" -}}{{ .Runner.Group.ParentGroup.Name }}/{{end -}}{{ .Runner.Group.Name }}</p>
|
||||
{{ else }}
|
||||
<p>Blank card</p>
|
||||
{{ end}}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Sponsoring contract</title>
|
||||
<title>Läuferurkunde</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
||||
<style>
|
||||
.sheet {
|
||||
@@ -87,13 +87,30 @@
|
||||
<td>Gesamt</td>
|
||||
<td>{{ formatUnit "euro" $.Locale .TotalPerDistance }} {{ $.CurrencySymbol }}</td>
|
||||
<td>{{ formatUnit "euro" $.Locale .TotalDonations }} {{ $.CurrencySymbol }}</td>
|
||||
</tfoot>
|
||||
</table>
|
||||
</main>
|
||||
<footer class="certificate-footer">
|
||||
<p>
|
||||
{{ $.Footer }}
|
||||
</p>
|
||||
</table>
|
||||
</main>
|
||||
<footer class="certificate-footer">
|
||||
<table style="border-collapse: collapse; border: none; width: 17cm;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="border: none; width: 50%; text-align: center;">Link zu deinen Rundenzeiten</th>
|
||||
<th style="border: none; width: 50%; text-align: center;">Spende überweisen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border: none; text-align: center;">
|
||||
<img src="data:image/png;base64,{{ barcode .SelfServiceLink "qr" "" }}" style="height: 2.5cm; padding: 0.2cm">
|
||||
</td>
|
||||
<td style="border: none; text-align: center;">
|
||||
<img src="data:image/png;base64,{{ epcCode $.SepaConfig.IBAN $.SepaConfig.BIC $.SepaConfig.HolderName (print "Sponsoring für " .FirstName " " .LastName ", " .CombinedGroupName) .TotalDonations $.SepaConfig.CurrencyIdentifier}}" style="height: 2.5cm; padding: 0.2cm">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="width: 17cm; text-align: center;">
|
||||
{{ $.Footer }}
|
||||
</p>
|
||||
</footer>
|
||||
</article>
|
||||
{{ end }}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Sponsoring contract</title>
|
||||
<title>Runner certificate</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
||||
<style>
|
||||
.sheet {
|
||||
@@ -91,7 +91,25 @@
|
||||
</table>
|
||||
</main>
|
||||
<footer class="certificate-footer">
|
||||
<p>
|
||||
<table style="border-collapse: collapse; border: none; width: 17cm;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="border: none; width: 50%; text-align: center;">Link to your lap times</th>
|
||||
<th style="border: none; width: 50%; text-align: center;">Transfer donation via SEPA</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border: none; text-align: center;">
|
||||
<img src="data:image/png;base64,{{ barcode .SelfServiceLink "qr" "" }}" style="height: 2.5cm; padding: 0.2cm">
|
||||
</td>
|
||||
<td style="border: none; text-align: center;">
|
||||
<img src="data:image/png;base64,{{ epcCode $.SepaConfig.IBAN $.SepaConfig.BIC $.SepaConfig.HolderName (print "Sponsoring for " .FirstName " " .LastName ", " .CombinedGroupName) .TotalDonations $.SepaConfig.CurrencyIdentifier}}" style="height: 2.5cm; padding: 0.2cm">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="width: 17cm; text-align: center;">
|
||||
{{ $.Footer }}
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
@@ -65,8 +65,8 @@
|
||||
<p style="font-size: x-small; display: block;">Nachname</p>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<span style="border-bottom: 1px solid; width: 100%; display: block;">{{ .Group.Name }}</span>
|
||||
<p style="font-size: x-small; display: block;">Team/ Klasse</p>
|
||||
<span style="border-bottom: 1px solid; width: 100%; display: block;"><p>{{ if ne .Group.ParentGroup.Name "" -}}{{ .Group.ParentGroup.Name }}/ {{end -}}{{ .Group.Name }}</p></span>
|
||||
<p style="font-size: x-small; display: block;">Team/Klasse</p>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin-top: -0.5rem">mit einem Betrag von _____ {{ $.CurrencySymbol }} pro gelaufenem Kilometer zu
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<p style="font-size: x-small; display: block;">Last Name</p>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<span style="border-bottom: 1px solid; width: 100%; display: block;">{{ .Group.Name}}</span>
|
||||
<span style="border-bottom: 1px solid; width: 100%; display: block;"><p>{{ if ne .Group.ParentGroup.Name "" -}}{{ .Group.ParentGroup.Name }}/ {{end -}}{{ .Group.Name }}</p></span>
|
||||
<p style="font-size: x-small; display: block;">Team/class</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user