From 9361997a42a4c6383bde1ac7a3cafe600927f88e Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Mon, 13 Feb 2023 10:00:29 -0900 Subject: [PATCH] feat(reporting): bill of materials (#275) * new reporting service * API route * code gen * get tsv export from tools page * fix naming --- .../app/api/handlers/v1/v1_ctrl_reporting.go | 32 ++++++ backend/app/api/routes.go | 3 + backend/app/api/static/docs/docs.go | 24 ++++ backend/app/api/static/docs/swagger.json | 24 ++++ backend/app/api/static/docs/swagger.yaml | 14 +++ backend/go.mod | 1 + backend/go.sum | 37 +----- .../{testdata => .testdata}/import.csv | 0 .../{testdata => .testdata}/import.tsv | 0 backend/internal/core/services/all.go | 15 ++- .../core/services/reporting/reporting.go | 85 ++++++++++++++ .../core/services/service_items_csv_test.go | 4 +- backend/internal/data/repo/repo_items.go | 7 +- frontend/lib/api/classes/reports.ts | 27 +++++ frontend/lib/api/user.ts | 3 + frontend/pages/tools.vue | 106 ++++++++++-------- 16 files changed, 291 insertions(+), 91 deletions(-) create mode 100644 backend/app/api/handlers/v1/v1_ctrl_reporting.go rename backend/internal/core/services/{testdata => .testdata}/import.csv (100%) rename backend/internal/core/services/{testdata => .testdata}/import.tsv (100%) create mode 100644 backend/internal/core/services/reporting/reporting.go create mode 100644 frontend/lib/api/classes/reports.ts diff --git a/backend/app/api/handlers/v1/v1_ctrl_reporting.go b/backend/app/api/handlers/v1/v1_ctrl_reporting.go new file mode 100644 index 0000000..09f2ae6 --- /dev/null +++ b/backend/app/api/handlers/v1/v1_ctrl_reporting.go @@ -0,0 +1,32 @@ +package v1 + +import ( + "net/http" + + "github.com/hay-kot/homebox/backend/internal/core/services" + "github.com/hay-kot/homebox/backend/pkgs/server" +) + +// HandleBillOfMaterialsExport godoc +// +// @Summary Generates a Bill of Materials CSV +// @Tags Reporting +// @Produce json +// @Success 200 {string} string "text/csv" +// @Router /v1/reporting/bill-of-materials [GET] +// @Security Bearer +func (ctrl *V1Controller) HandleBillOfMaterialsExport() server.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) error { + actor := services.UseUserCtx(r.Context()) + + csv, err := ctrl.svc.Reporting.BillOfMaterialsTSV(r.Context(), actor.GroupID) + if err != nil { + return err + } + + w.Header().Set("Content-Type", "text/csv") + w.Header().Set("Content-Disposition", "attachment; filename=bom.csv") + _, err = w.Write(csv) + return err + } +} diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index 820de7d..e995fa4 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -136,6 +136,9 @@ func (a *app) mountRoutes(repos *repo.AllRepos) { a.mwAuthToken, a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()), ) + // Reporting Services + a.server.Get(v1Base("/reporting/bill-of-materials"), v1Ctrl.HandleBillOfMaterialsExport(), userMW...) + a.server.NotFound(notFoundHandler()) } diff --git a/backend/app/api/static/docs/docs.go b/backend/app/api/static/docs/docs.go index 5ea1b82..71c9840 100644 --- a/backend/app/api/static/docs/docs.go +++ b/backend/app/api/static/docs/docs.go @@ -1303,6 +1303,30 @@ const docTemplate = `{ } } }, + "/v1/reporting/bill-of-materials": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Reporting" + ], + "summary": "Generates a Bill of Materials CSV", + "responses": { + "200": { + "description": "text/csv", + "schema": { + "type": "string" + } + } + } + } + }, "/v1/status": { "get": { "produces": [ diff --git a/backend/app/api/static/docs/swagger.json b/backend/app/api/static/docs/swagger.json index d39650d..37d87b2 100644 --- a/backend/app/api/static/docs/swagger.json +++ b/backend/app/api/static/docs/swagger.json @@ -1295,6 +1295,30 @@ } } }, + "/v1/reporting/bill-of-materials": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Reporting" + ], + "summary": "Generates a Bill of Materials CSV", + "responses": { + "200": { + "description": "text/csv", + "schema": { + "type": "string" + } + } + } + } + }, "/v1/status": { "get": { "produces": [ diff --git a/backend/app/api/static/docs/swagger.yaml b/backend/app/api/static/docs/swagger.yaml index be985fd..d087658 100644 --- a/backend/app/api/static/docs/swagger.yaml +++ b/backend/app/api/static/docs/swagger.yaml @@ -1408,6 +1408,20 @@ paths: summary: Encode data into QRCode tags: - Items + /v1/reporting/bill-of-materials: + get: + produces: + - application/json + responses: + "200": + description: text/csv + schema: + type: string + security: + - Bearer: [] + summary: Generates a Bill of Materials CSV + tags: + - Reporting /v1/status: get: produces: diff --git a/backend/go.mod b/backend/go.mod index c19b9ff..43bfaf9 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -8,6 +8,7 @@ require ( github.com/ardanlabs/conf/v3 v3.1.3 github.com/go-chi/chi/v5 v5.0.8 github.com/go-playground/validator/v10 v10.11.2 + github.com/gocarina/gocsv v0.0.0-20230123225133-763e25b40669 github.com/google/uuid v1.3.0 github.com/mattn/go-sqlite3 v1.14.16 github.com/rs/zerolog v1.29.0 diff --git a/backend/go.sum b/backend/go.sum index f4ef428..d308f71 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,9 +1,5 @@ -ariga.io/atlas v0.9.0 h1:q0JMtqyA3X1YWtPcn+E/kVPwLDslb+jAC8Ejl/vW6d0= -ariga.io/atlas v0.9.0/go.mod h1:T230JFcENj4ZZzMkZrXFDSkv+2kXkUgpJ5FQQ5hMcKU= ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb h1:mbsFtavDqGdYwdDpP50LGOOZ2hgyGoJcZeOpbgKMyu4= ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb/go.mod h1:T230JFcENj4ZZzMkZrXFDSkv+2kXkUgpJ5FQQ5hMcKU= -entgo.io/ent v0.11.5 h1:V2qhG91C4PMQTa82Q4StoESMQ4dzkMNeStCzszxi0jQ= -entgo.io/ent v0.11.5/go.mod h1:u7eKwNWAo/VlHIKxgwbmsFy3J7cKDxwi3jyF5TW/okY= entgo.io/ent v0.11.7 h1:V+wKFh0jhAbY/FoU+PPbdMOf2Ma5vh07R/IdF+N/nFg= entgo.io/ent v0.11.7/go.mod h1:ericBi6Q8l3wBH1wEIDfKxw7rcQEuRPyBfbIzjtxJ18= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= @@ -37,21 +33,16 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= -github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/gocarina/gocsv v0.0.0-20230123225133-763e25b40669 h1:MvZzCA/mduVWoBSVKJeMdv+AqXQmZZ8i6p8889ejt/Y= +github.com/gocarina/gocsv v0.0.0-20230123225133-763e25b40669/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -65,9 +56,7 @@ github.com/hashicorp/hcl/v2 v2.15.0/go.mod h1:JRmR89jycNkrrqnMmvPDMd56n1rQJ2Q6Ko github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -94,14 +83,11 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= @@ -113,7 +99,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= @@ -135,9 +120,6 @@ github.com/zclconf/go-cty v1.12.1 h1:PcupnljUm9EIvbgSHQnHhUr3fO6oFmkOrvs2BAFNXXY github.com/zclconf/go-cty v1.12.1/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= @@ -147,28 +129,21 @@ golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -176,11 +151,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -193,13 +165,10 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/internal/core/services/testdata/import.csv b/backend/internal/core/services/.testdata/import.csv similarity index 100% rename from backend/internal/core/services/testdata/import.csv rename to backend/internal/core/services/.testdata/import.csv diff --git a/backend/internal/core/services/testdata/import.tsv b/backend/internal/core/services/.testdata/import.tsv similarity index 100% rename from backend/internal/core/services/testdata/import.tsv rename to backend/internal/core/services/.testdata/import.tsv diff --git a/backend/internal/core/services/all.go b/backend/internal/core/services/all.go index 43deb52..2997095 100644 --- a/backend/internal/core/services/all.go +++ b/backend/internal/core/services/all.go @@ -1,11 +1,16 @@ package services -import "github.com/hay-kot/homebox/backend/internal/data/repo" +import ( + "github.com/hay-kot/homebox/backend/internal/core/services/reporting" + "github.com/hay-kot/homebox/backend/internal/data/repo" + "github.com/rs/zerolog/log" +) type AllServices struct { - User *UserService - Group *GroupService - Items *ItemService + User *UserService + Group *GroupService + Items *ItemService + Reporting *reporting.ReportingService } type OptionsFunc func(*options) @@ -40,5 +45,7 @@ func New(repos *repo.AllRepos, opts ...OptionsFunc) *AllServices { repo: repos, autoIncrementAssetID: options.autoIncrementAssetID, }, + // TODO: don't use global logger + Reporting: reporting.NewReportingService(repos, &log.Logger), } } diff --git a/backend/internal/core/services/reporting/reporting.go b/backend/internal/core/services/reporting/reporting.go new file mode 100644 index 0000000..4ba408b --- /dev/null +++ b/backend/internal/core/services/reporting/reporting.go @@ -0,0 +1,85 @@ +package reporting + +import ( + "context" + "encoding/csv" + "io" + "time" + + "github.com/gocarina/gocsv" + "github.com/google/uuid" + "github.com/hay-kot/homebox/backend/internal/data/repo" + "github.com/rs/zerolog" +) + +type ReportingService struct { + repos *repo.AllRepos + l *zerolog.Logger +} + +func NewReportingService(repos *repo.AllRepos, l *zerolog.Logger) *ReportingService { + gocsv.SetCSVWriter(func(out io.Writer) *gocsv.SafeCSVWriter { + writer := csv.NewWriter(out) + writer.Comma = '\t' + return gocsv.NewSafeCSVWriter(writer) + }) + + return &ReportingService{ + repos: repos, + l: l, + } +} + +// ================================================================================================= + +// NullableTime is a custom type that implements the MarshalCSV interface +// to allow for nullable time.Time fields in the CSV output to be empty +// and not "0001-01-01". It also overrides the default CSV output format +type NullableTime time.Time + +func (t NullableTime) MarshalCSV() (string, error) { + if time.Time(t).IsZero() { + return "", nil + } + // YYYY-MM-DD + return time.Time(t).Format("2006-01-02"), nil +} + +type BillOfMaterialsEntry struct { + PurchaseDate NullableTime `csv:"Purchase Date"` + Name string `csv:"Name"` + Description string `csv:"Description"` + Manufacturer string `csv:"Manufacturer"` + SerialNumber string `csv:"Serial Number"` + ModelNumber string `csv:"Model Number"` + Quantity int `csv:"Quantity"` + Price float64 `csv:"Price"` + TotalPrice float64 `csv:"Total Price"` +} + +// BillOfMaterialsTSV returns a byte slice of the Bill of Materials for a given GID in TSV format +// See BillOfMaterialsEntry for the format of the output +func (rs *ReportingService) BillOfMaterialsTSV(ctx context.Context, GID uuid.UUID) ([]byte, error) { + entities, err := rs.repos.Items.GetAll(ctx, GID) + if err != nil { + rs.l.Debug().Err(err).Msg("failed to get all items for BOM Csv Reporting") + return nil, err + } + + bomEntries := make([]BillOfMaterialsEntry, len(entities)) + for i, entity := range entities { + bomEntries[i] = BillOfMaterialsEntry{ + PurchaseDate: NullableTime(entity.PurchaseTime), + Name: entity.Name, + Description: entity.Description, + Manufacturer: entity.Manufacturer, + SerialNumber: entity.SerialNumber, + ModelNumber: entity.ModelNumber, + Quantity: entity.Quantity, + Price: entity.PurchasePrice, + TotalPrice: entity.PurchasePrice * float64(entity.Quantity), + } + } + + return gocsv.MarshalBytes(&bomEntries) +} diff --git a/backend/internal/core/services/service_items_csv_test.go b/backend/internal/core/services/service_items_csv_test.go index 675a1a9..5338979 100644 --- a/backend/internal/core/services/service_items_csv_test.go +++ b/backend/internal/core/services/service_items_csv_test.go @@ -12,10 +12,10 @@ import ( "github.com/stretchr/testify/assert" ) -//go:embed testdata/import.csv +//go:embed .testdata/import.csv var CSVData_Comma []byte -//go:embed testdata/import.tsv +//go:embed .testdata/import.tsv var CSVData_Tab []byte func loadcsv() [][]string { diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index 91dd2b3..0f644f0 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -185,7 +185,8 @@ func mapItemSummary(item *ent.Item) ItemSummary { } var ( - mapItemOutErr = mapTErrFunc(mapItemOut) + mapItemOutErr = mapTErrFunc(mapItemOut) + mapItemsOutErr = mapTEachErrFunc(mapItemOut) ) func mapFields(fields []*ent.ItemField) []ItemField { @@ -434,8 +435,8 @@ func (e *ItemsRepository) QueryByAssetID(ctx context.Context, gid uuid.UUID, ass } // GetAll returns all the items in the database with the Labels and Locations eager loaded. -func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemSummary, error) { - return mapItemsSummaryErr(e.db.Item.Query(). +func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemOut, error) { + return mapItemsOutErr(e.db.Item.Query(). Where(item.HasGroupWith(group.ID(gid))). WithLabel(). WithLocation(). diff --git a/frontend/lib/api/classes/reports.ts b/frontend/lib/api/classes/reports.ts new file mode 100644 index 0000000..03890cb --- /dev/null +++ b/frontend/lib/api/classes/reports.ts @@ -0,0 +1,27 @@ +import { BaseAPI, route } from "../base"; + +export class ReportsAPI extends BaseAPI { + async billOfMaterials(): Promise { + const { data: stream, error } = await this.http.get({ url: route("/reporting/bill-of-materials") }); + + if (error) { + return; + } + + const reader = stream.getReader(); + let data = ""; + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + data += new TextDecoder("utf-8").decode(value); + } + + const blob = new Blob([data], { type: "text/tsv" }); + const link = document.createElement("a"); + link.href = window.URL.createObjectURL(blob); + link.download = "bill-of-materials.tsv"; + link.click(); + } +} diff --git a/frontend/lib/api/user.ts b/frontend/lib/api/user.ts index a0fd413..fa08258 100644 --- a/frontend/lib/api/user.ts +++ b/frontend/lib/api/user.ts @@ -7,6 +7,7 @@ import { UserApi } from "./classes/users"; import { ActionsAPI } from "./classes/actions"; import { StatsAPI } from "./classes/stats"; import { AssetsApi } from "./classes/assets"; +import { ReportsAPI } from "./classes/reports"; import { Requests } from "~~/lib/requests"; export class UserClient extends BaseAPI { @@ -18,6 +19,7 @@ export class UserClient extends BaseAPI { actions: ActionsAPI; stats: StatsAPI; assets: AssetsApi; + reports: ReportsAPI; constructor(requests: Requests, attachmentToken: string) { super(requests, attachmentToken); @@ -30,6 +32,7 @@ export class UserClient extends BaseAPI { this.actions = new ActionsAPI(requests); this.stats = new StatsAPI(requests); this.assets = new AssetsApi(requests); + this.reports = new ReportsAPI(requests); Object.freeze(this); } diff --git a/frontend/pages/tools.vue b/frontend/pages/tools.vue index e1ba350..181c308 100644 --- a/frontend/pages/tools.vue +++ b/frontend/pages/tools.vue @@ -2,6 +2,32 @@
+ + +
+ + + Generates a printable PDF of labels for a range of Asset ID. These are not specific to your inventory so + your are able to print labels ahead of time and apply them to your inventory when you receive them. + + + + + Generates a TSV (Tab Separated Values) file that can be imported into a spreadsheet program. This is a + summary of your inventory with basic item and pricing information + + +
+
-
- - - Imports the standard CSV format for Homebox. This will not overwrite any existing items in your - inventory. It will only add new items. - - -
- -
- - +
+ + + Imports the standard CSV format for Homebox. This will not overwrite any existing items in your + inventory. It will only add new items. + + +
-
- - - Ensures that all items in your inventory have a valid asset_id field. This is done by finding the highest - current asset_id field in the database and applying the next value to each item that has an unset asset_id - field. This is done in order of the created_at field. - - - - Resets the time value for all date time fields in your inventory to the beginning of the date. This is to - fix a bug that was introduced early on in the development of the site that caused the time value to be - stored with the time which caused issues with date fields displaying accurate values. - - See Github Issue #236 for more details - - -
+
+ + + Ensures that all items in your inventory have a valid asset_id field. This is done by finding the highest + current asset_id field in the database and applying the next value to each item that has an unset asset_id + field. This is done in order of the created_at field. + + + + Resets the time value for all date time fields in your inventory to the beginning of the date. This is to + fix a bug that was introduced early on in the development of the site that caused the time value to be + stored with the time which caused issues with date fields displaying accurate values. + + See Github Issue #236 for more details + + +
@@ -96,6 +102,10 @@ const confirm = useConfirm(); const notify = useNotifier(); + async function getBillOfMaterials() { + await api.reports.billOfMaterials(); + } + async function ensureAssetIDs() { const { isCanceled } = await confirm.open( "Are you sure you want to ensure all assets have an ID? This can take a while and cannot be undone."