package web import ( "fmt" "html/template" "net/http" "inventree/internal/assets" "inventree/internal/models" ) // LabelTemplate describes a sheet of adhesive labels: cell size, grid, and // page margins, all in inches, used to lay out printable asset tags. type LabelTemplate struct { Key string DisplayName string LabelWidthIn float64 LabelHeightIn float64 Cols int Rows int MarginTopIn float64 MarginLeftIn float64 ColGapIn float64 RowGapIn float64 QRSizeIn float64 // Approximate is true for templates whose margins/grid aren't from a // verified manufacturer spec sheet - print one test page on plain // paper and hold it up to a real label sheet before committing a // full (often expensive/waterproof) sheet to the printer. Approximate bool } var labelTemplates = map[string]LabelTemplate{ "5160": { Key: "5160", DisplayName: `Avery 5160 - 1" x 2-5/8" (30/sheet)`, LabelWidthIn: 2.625, LabelHeightIn: 1, Cols: 3, Rows: 10, MarginTopIn: 0.5, MarginLeftIn: 0.1875, ColGapIn: 0.125, RowGapIn: 0, QRSizeIn: 0.85, }, "61526": { Key: "61526", DisplayName: `Asset Tag Labels 61526 - 3/4" x 2" (~48/sheet)`, LabelWidthIn: 2, LabelHeightIn: 0.75, Cols: 4, Rows: 12, MarginTopIn: 1, MarginLeftIn: 0.25, ColGapIn: 0, RowGapIn: 0, QRSizeIn: 0.62, Approximate: true, }, } const defaultLabelTemplate = "5160" func (t LabelTemplate) css() template.CSS { return template.CSS(fmt.Sprintf(` .label-sheet { width: 8.5in; box-sizing: border-box; padding: %.4fin %.4fin; margin: 0 auto; display: grid; grid-template-columns: repeat(%d, %.4fin); column-gap: %.4fin; row-gap: %.4fin; justify-content: center; } .label { width: %.4fin; height: %.4fin; box-sizing: border-box; padding: 0.06in 0.08in; display: flex; align-items: center; gap: 0.08in; overflow: hidden; page-break-inside: avoid; } .label img { width: %.4fin; height: %.4fin; flex-shrink: 0; } `, t.MarginTopIn, t.MarginLeftIn, t.Cols, t.LabelWidthIn, t.ColGapIn, t.RowGapIn, t.LabelWidthIn, t.LabelHeightIn, t.QRSizeIn, t.QRSizeIn)) } type labelItem struct { Asset models.Asset // template.URL marks this as pre-vetted safe (it's server-generated, // never user input), so html/template's URL filter doesn't replace it // with the "#ZgotmplZ" safety placeholder the way it would a plain // string containing a data: URI. QRData template.URL } type labelsPageData struct { User *models.User Labels []labelItem SheetCSS template.CSS Templates map[string]LabelTemplate TemplateKeys []string Selected LabelTemplate AssetID string FolderID string Recursive bool } // handleLabelsPage renders a print-ready sheet of QR-coded asset labels: // either a single asset (?asset_id=) or every asset in a folder // (?folder_id=&recursive=1), or the whole inventory with neither param. // Each QR code encodes an absolute /scan/ URL so a phone camera (or a // USB barcode scanner focused on a browser address bar) opens straight to // that asset. func (a *App) handleLabelsPage(w http.ResponseWriter, r *http.Request) { ctx := r.Context() origin := requestOrigin(r) tmpl, ok := labelTemplates[r.URL.Query().Get("template")] if !ok { tmpl = labelTemplates[defaultLabelTemplate] } var list []models.Asset assetIDParam := r.URL.Query().Get("asset_id") folderIDParam := r.URL.Query().Get("folder_id") recursive := r.URL.Query().Get("recursive") == "1" if assetID, err := queryOptionalID(r, "asset_id"); err == nil && assetID != nil { asset, err := a.Assets.Get(ctx, *assetID) if err != nil { writeErr(w, http.StatusNotFound, err.Error()) return } list = []models.Asset{*asset} } else { filter := assets.ListFilter{Page: 1, PageSize: 2000} if folderID, err := queryOptionalID(r, "folder_id"); err == nil && folderID != nil { filter.FolderID = folderID filter.FolderSet = true filter.IncludeSub = recursive } batch, _, err := a.Assets.List(ctx, filter) if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } list = batch } labels := make([]labelItem, 0, len(list)) for _, as := range list { qr, err := qrDataURI(origin+"/scan/"+as.AssetTag, 220) if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } labels = append(labels, labelItem{Asset: as, QRData: template.URL(qr)}) } a.Render(w, http.StatusOK, "labels", labelsPageData{ User: currentUser(r), Labels: labels, SheetCSS: tmpl.css(), Templates: labelTemplates, TemplateKeys: []string{"5160", "61526"}, Selected: tmpl, AssetID: assetIDParam, FolderID: folderIDParam, Recursive: recursive, }) }