Merge branch 'implement-pdf-generrating-and-download'

* implement-pdf-generrating-and-download:
  feat: add pdf generation
This commit is contained in:
Patryk Hegenberg 2025-11-08 12:07:44 +01:00
commit e931b97037
7 changed files with 245 additions and 5 deletions

View file

@ -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

View file

@ -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=

View file

@ -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)
}

View file

@ -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
View 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
}

View file

@ -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"
} }

View file

@ -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
}