Merge branch 'implement-pdf-generrating-and-download'
* implement-pdf-generrating-and-download: feat: add pdf generation
This commit is contained in:
commit
e931b97037
7 changed files with 245 additions and 5 deletions
|
|
@ -3,6 +3,7 @@ module school-timetracker
|
||||||
go 1.25.3
|
go 1.25.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/jung-kurt/gofpdf v1.16.2
|
||||||
github.com/labstack/echo/v4 v4.13.4
|
github.com/labstack/echo/v4 v4.13.4
|
||||||
golang.org/x/crypto v0.43.0
|
golang.org/x/crypto v0.43.0
|
||||||
golang.org/x/time v0.11.0
|
golang.org/x/time v0.11.0
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
|
@ -6,6 +8,9 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||||
|
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
|
||||||
|
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
|
||||||
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
|
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
|
||||||
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
|
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
|
||||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
|
|
@ -16,10 +21,14 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
|
@ -30,6 +39,7 @@ golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||||
|
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||||
|
|
@ -39,6 +49,7 @@ golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -494,3 +495,31 @@ func (app *App) GetActiveSchoolYearHandler(c echo.Context) error {
|
||||||
}
|
}
|
||||||
return c.JSON(http.StatusOK, year)
|
return c.JSON(http.StatusOK, year)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *App) GenerateYearlySummaryPDFHandler(c echo.Context) error {
|
||||||
|
isAdmin, _ := c.Get("is_admin").(bool)
|
||||||
|
if !isAdmin {
|
||||||
|
return echo.NewHTTPError(http.StatusForbidden, "Only admins can generate PDFs")
|
||||||
|
}
|
||||||
|
|
||||||
|
schoolYear, err := GetActiveSchoolYear(app.DB)
|
||||||
|
if err != nil || schoolYear == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "No active school year found")
|
||||||
|
}
|
||||||
|
|
||||||
|
summary, err := GetYearlyHoursSummary(app.DB)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfBytes, err := GenerateYearlySummaryPDF(schoolYear, summary)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate PDF: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("Jahresuebersicht_%s.pdf", schoolYear.Name)
|
||||||
|
c.Response().Header().Set("Content-Type", "application/pdf")
|
||||||
|
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
||||||
|
|
||||||
|
return c.Blob(http.StatusOK, "application/pdf", pdfBytes)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ func main() {
|
||||||
admin.GET("/school-years", app.GetSchoolYearsHandler)
|
admin.GET("/school-years", app.GetSchoolYearsHandler)
|
||||||
admin.POST("/school-years", app.CreateSchoolYearHandler)
|
admin.POST("/school-years", app.CreateSchoolYearHandler)
|
||||||
admin.PUT("/school-years/:id/activate", app.SetActiveSchoolYearHandler)
|
admin.PUT("/school-years/:id/activate", app.SetActiveSchoolYearHandler)
|
||||||
|
admin.GET("/yearly-summary/pdf", app.GenerateYearlySummaryPDFHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Static("/", "./static")
|
e.Static("/", "./static")
|
||||||
|
|
|
||||||
110
backend/pdf.go
Normal file
110
backend/pdf.go
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jung-kurt/gofpdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateYearlySummaryPDF(schoolYear *SchoolYear, summary []WeeklyHours) ([]byte, error) {
|
||||||
|
pdf := gofpdf.New("P", "mm", "A4", "")
|
||||||
|
pdf.AddPage()
|
||||||
|
|
||||||
|
pdf.SetFont("Arial", "B", 20)
|
||||||
|
|
||||||
|
title := fmt.Sprintf("Stundenjahresübersicht für Schuljahr %s", schoolYear.Name)
|
||||||
|
pdf.Cell(0, 15, title)
|
||||||
|
pdf.Ln(10)
|
||||||
|
|
||||||
|
pdf.SetFont("Arial", "", 12)
|
||||||
|
subtitle := fmt.Sprintf("%s bis %s", schoolYear.StartDate, schoolYear.EndDate)
|
||||||
|
pdf.Cell(0, 10, subtitle)
|
||||||
|
pdf.Ln(15)
|
||||||
|
|
||||||
|
pdf.SetFont("Arial", "B", 10)
|
||||||
|
pdf.SetFillColor(52, 152, 219)
|
||||||
|
pdf.SetTextColor(255, 255, 255)
|
||||||
|
|
||||||
|
colWidths := []float64{60, 40, 40, 40}
|
||||||
|
headers := []string{"Mitarbeiter", "Soll (Std.)", "Ist (Std.)", "Differenz (Std.)"}
|
||||||
|
|
||||||
|
for i, header := range headers {
|
||||||
|
pdf.CellFormat(colWidths[i], 10, header, "1", 0, "C", true, 0, "")
|
||||||
|
}
|
||||||
|
pdf.Ln(-1)
|
||||||
|
|
||||||
|
pdf.SetFont("Arial", "", 10)
|
||||||
|
pdf.SetTextColor(0, 0, 0)
|
||||||
|
fill := false
|
||||||
|
|
||||||
|
for _, entry := range summary {
|
||||||
|
if fill {
|
||||||
|
pdf.SetFillColor(240, 240, 240)
|
||||||
|
} else {
|
||||||
|
pdf.SetFillColor(255, 255, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
pdf.CellFormat(colWidths[0], 8, entry.Username, "1", 0, "L", true, 0, "")
|
||||||
|
|
||||||
|
pdf.CellFormat(colWidths[1], 8, fmt.Sprintf("%.1f", entry.YearlyTarget), "1", 0, "R", true, 0, "")
|
||||||
|
|
||||||
|
pdf.CellFormat(colWidths[2], 8, fmt.Sprintf("%.1f", entry.YearlyActual), "1", 0, "R", true, 0, "")
|
||||||
|
|
||||||
|
diffStr := fmt.Sprintf("%.1f", entry.RemainingYearly)
|
||||||
|
if entry.RemainingYearly > 0 {
|
||||||
|
pdf.SetTextColor(220, 53, 69)
|
||||||
|
} else {
|
||||||
|
pdf.SetTextColor(40, 167, 69)
|
||||||
|
}
|
||||||
|
pdf.CellFormat(colWidths[3], 8, diffStr, "1", 0, "R", true, 0, "")
|
||||||
|
pdf.SetTextColor(0, 0, 0)
|
||||||
|
|
||||||
|
pdf.Ln(-1)
|
||||||
|
fill = !fill
|
||||||
|
}
|
||||||
|
|
||||||
|
pdf.Ln(5)
|
||||||
|
pdf.SetFont("Arial", "B", 10)
|
||||||
|
|
||||||
|
totalTarget := 0.0
|
||||||
|
totalActual := 0.0
|
||||||
|
totalRemaining := 0.0
|
||||||
|
|
||||||
|
for _, entry := range summary {
|
||||||
|
totalTarget += entry.YearlyTarget
|
||||||
|
totalActual += entry.YearlyActual
|
||||||
|
totalRemaining += entry.RemainingYearly
|
||||||
|
}
|
||||||
|
|
||||||
|
pdf.SetFillColor(52, 152, 219)
|
||||||
|
pdf.SetTextColor(255, 255, 255)
|
||||||
|
|
||||||
|
pdf.CellFormat(colWidths[0], 10, "GESAMT", "1", 0, "L", true, 0, "")
|
||||||
|
pdf.CellFormat(colWidths[1], 10, fmt.Sprintf("%.1f", totalTarget), "1", 0, "R", true, 0, "")
|
||||||
|
pdf.CellFormat(colWidths[2], 10, fmt.Sprintf("%.1f", totalActual), "1", 0, "R", true, 0, "")
|
||||||
|
pdf.CellFormat(colWidths[3], 10, fmt.Sprintf("%.1f", totalRemaining), "1", 0, "R", true, 0, "")
|
||||||
|
|
||||||
|
pdf.Ln(15)
|
||||||
|
pdf.SetFont("Arial", "I", 8)
|
||||||
|
pdf.SetTextColor(128, 128, 128)
|
||||||
|
pdf.Cell(0, 10, fmt.Sprintf("Erstellt am: %s", time.Now().Format("02.01.2006 15:04")))
|
||||||
|
|
||||||
|
var buf []byte
|
||||||
|
w := &pdfWriter{buf: &buf}
|
||||||
|
err := pdf.Output(w)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type pdfWriter struct {
|
||||||
|
buf *[]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *pdfWriter) Write(p []byte) (n int, err error) {
|
||||||
|
*w.buf = append(*w.buf, p...)
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
{
|
{
|
||||||
"type": "application",
|
"type": "application",
|
||||||
"source-directories": ["src"],
|
"source-directories": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
"elm-version": "0.19.1",
|
"elm-version": "0.19.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"direct": {
|
"direct": {
|
||||||
"elm/browser": "1.0.2",
|
"elm/browser": "1.0.2",
|
||||||
|
"elm/bytes": "1.0.8",
|
||||||
"elm/core": "1.0.5",
|
"elm/core": "1.0.5",
|
||||||
|
"elm/file": "1.0.5",
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
"elm/http": "2.0.0",
|
"elm/http": "2.0.0",
|
||||||
"elm/json": "1.1.3",
|
"elm/json": "1.1.3",
|
||||||
"elm/time": "1.0.0"
|
"elm/time": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/bytes": "1.0.8",
|
|
||||||
"elm/file": "1.0.5",
|
|
||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
"elm/virtual-dom": "1.0.3"
|
"elm/virtual-dom": "1.0.3"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
port module Main exposing (..)
|
port module Main exposing (..)
|
||||||
|
|
||||||
import Browser
|
import Browser
|
||||||
|
import Bytes exposing (Bytes)
|
||||||
import Dict exposing (Dict)
|
import Dict exposing (Dict)
|
||||||
|
import File.Download
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
import Html.Events exposing (..)
|
import Html.Events exposing (..)
|
||||||
|
|
@ -430,6 +432,8 @@ type Msg
|
||||||
| SchoolYearActivated (Result Http.Error ())
|
| SchoolYearActivated (Result Http.Error ())
|
||||||
| DeleteSchoolYear Int
|
| DeleteSchoolYear Int
|
||||||
| SchoolYearDeleted (Result Http.Error ())
|
| SchoolYearDeleted (Result Http.Error ())
|
||||||
|
| DownloadYearlySummaryPDF
|
||||||
|
| YearlySummaryPDFReceived (Result Http.Error Bytes.Bytes)
|
||||||
|
|
||||||
|
|
||||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
|
|
@ -1631,6 +1635,29 @@ update msg model =
|
||||||
SchoolYearDeleted (Err _) ->
|
SchoolYearDeleted (Err _) ->
|
||||||
( { model | error = Just "Fehler beim Löschen" }, Cmd.none )
|
( { model | error = Just "Fehler beim Löschen" }, Cmd.none )
|
||||||
|
|
||||||
|
DownloadYearlySummaryPDF ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model | isProcessing = True }, downloadYearlySummaryPDF token )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
YearlySummaryPDFReceived (Ok pdfBytes) ->
|
||||||
|
let
|
||||||
|
filename =
|
||||||
|
"Jahresuebersicht_" ++ String.fromInt model.currentYear ++ ".pdf"
|
||||||
|
in
|
||||||
|
( { model | isProcessing = False }, File.Download.bytes filename "application/pdf" pdfBytes )
|
||||||
|
|
||||||
|
YearlySummaryPDFReceived (Err _) ->
|
||||||
|
( { model
|
||||||
|
| error = Just "Fehler beim Herunterladen der PDF"
|
||||||
|
, isProcessing = False
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- SUBSCRIPTIONS
|
-- SUBSCRIPTIONS
|
||||||
|
|
@ -2606,7 +2633,35 @@ viewTimeEntriesTab model =
|
||||||
viewYearlyHoursSummary : Model -> Html Msg
|
viewYearlyHoursSummary : Model -> Html Msg
|
||||||
viewYearlyHoursSummary model =
|
viewYearlyHoursSummary model =
|
||||||
div [ class "box" ]
|
div [ class "box" ]
|
||||||
[ if List.isEmpty model.yearlyHoursSummary then
|
[ div [ class "level mb-4" ]
|
||||||
|
[ div [ class "level-left" ]
|
||||||
|
[ div [ class "level-item" ]
|
||||||
|
[ h3 [ class "subtitle is-5 mb-0" ] [ text "Jahresübersicht" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "level-right" ]
|
||||||
|
[ div [ class "level-item" ]
|
||||||
|
[ a
|
||||||
|
[ class "button is-info"
|
||||||
|
, onClick DownloadYearlySummaryPDF
|
||||||
|
, disabled model.isProcessing
|
||||||
|
]
|
||||||
|
[ span [ class "icon" ]
|
||||||
|
[ i [ class "fas fa-file-pdf" ] [] ]
|
||||||
|
, span []
|
||||||
|
[ text
|
||||||
|
(if model.isProcessing then
|
||||||
|
"Wird erstellt..."
|
||||||
|
|
||||||
|
else
|
||||||
|
"PDF exportieren"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, if List.isEmpty model.yearlyHoursSummary then
|
||||||
p [ class "has-text-centered" ] [ text "Keine Daten vorhanden" ]
|
p [ class "has-text-centered" ] [ text "Keine Daten vorhanden" ]
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|
@ -2950,7 +3005,7 @@ viewWeekNavigation model =
|
||||||
, style "flex-direction" "column"
|
, style "flex-direction" "column"
|
||||||
, style "align-items" "center"
|
, style "align-items" "center"
|
||||||
, style "gap" "0.5rem"
|
, style "gap" "0.5rem"
|
||||||
, style "min-width" "250px" -- Verhindert Kompression
|
, style "min-width" "250px"
|
||||||
]
|
]
|
||||||
[ p
|
[ p
|
||||||
[ class "heading"
|
[ class "heading"
|
||||||
|
|
@ -4225,3 +4280,34 @@ schoolYearDecoder =
|
||||||
(field "start_date" string)
|
(field "start_date" string)
|
||||||
(field "end_date" string)
|
(field "end_date" string)
|
||||||
(field "is_active" bool)
|
(field "is_active" bool)
|
||||||
|
|
||||||
|
|
||||||
|
downloadYearlySummaryPDF : String -> Cmd Msg
|
||||||
|
downloadYearlySummaryPDF token =
|
||||||
|
Http.request
|
||||||
|
{ method = "GET"
|
||||||
|
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||||||
|
, url = "/api/admin/yearly-summary/pdf"
|
||||||
|
, body = Http.emptyBody
|
||||||
|
, expect =
|
||||||
|
Http.expectBytesResponse YearlySummaryPDFReceived
|
||||||
|
(\response ->
|
||||||
|
case response of
|
||||||
|
Http.GoodStatus_ _ body ->
|
||||||
|
Ok body
|
||||||
|
|
||||||
|
Http.BadUrl_ url ->
|
||||||
|
Err (Http.BadUrl url)
|
||||||
|
|
||||||
|
Http.Timeout_ ->
|
||||||
|
Err Http.Timeout
|
||||||
|
|
||||||
|
Http.NetworkError_ ->
|
||||||
|
Err Http.NetworkError
|
||||||
|
|
||||||
|
Http.BadStatus_ metadata _ ->
|
||||||
|
Err (Http.BadStatus metadata.statusCode)
|
||||||
|
)
|
||||||
|
, timeout = Nothing
|
||||||
|
, tracker = Nothing
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue