package handlers import ( "html/template" "strconv" "strings" qfassets "git.mchus.pro/mchus/quoteforge" "git.mchus.pro/mchus/quoteforge/internal/localdb" "git.mchus.pro/mchus/quoteforge/internal/models" "github.com/gin-gonic/gin" ) type WebHandler struct { templates map[string]*template.Template localDB *localdb.LocalDB } func NewWebHandler(_ string, localDB *localdb.LocalDB) (*WebHandler, error) { funcMap := template.FuncMap{ "sub": func(a, b int) int { return a - b }, "add": func(a, b int) int { return a + b }, "mul": func(a, b int) int { return a * b }, "div": func(a, b int) int { if b == 0 { return 0 } return (a + b - 1) / b }, "deref": func(f *float64) float64 { if f == nil { return 0 } return *f }, "jsesc": func(s string) string { // Escape string for safe use in JavaScript result := "" for _, r := range s { switch r { case '\\': result += "\\\\" case '\'': result += "\\'" case '"': result += "\\\"" case '\n': result += "\\n" case '\r': result += "\\r" case '\t': result += "\\t" default: result += string(r) } } return result }, } templates := make(map[string]*template.Template) // Load each page template with base simplePages := []string{"configs.html", "projects.html", "project_detail.html", "pricelists.html", "pricelist_detail.html", "config_revisions.html", "partnumber_books.html"} for _, page := range simplePages { var tmpl *template.Template var err error tmpl, err = template.New("").Funcs(funcMap).ParseFS( qfassets.TemplatesFS, "web/templates/base.html", "web/templates/"+page, ) if err != nil { return nil, err } templates[page] = tmpl } // Index page needs components_list.html as well var indexTmpl *template.Template var err error indexTmpl, err = template.New("").Funcs(funcMap).ParseFS( qfassets.TemplatesFS, "web/templates/base.html", "web/templates/index.html", "web/templates/components_list.html", ) if err != nil { return nil, err } templates["index.html"] = indexTmpl // Load partial templates (no base needed) partials := []string{"components_list.html"} for _, partial := range partials { var tmpl *template.Template var err error tmpl, err = template.New("").Funcs(funcMap).ParseFS( qfassets.TemplatesFS, "web/templates/"+partial, ) if err != nil { return nil, err } templates[partial] = tmpl } return &WebHandler{ templates: templates, localDB: localDB, }, nil } func (h *WebHandler) render(c *gin.Context, name string, data gin.H) { c.Header("Content-Type", "text/html; charset=utf-8") tmpl, ok := h.templates[name] if !ok { c.String(500, "Template not found: %s", name) return } // Execute the page template which will use base if err := tmpl.ExecuteTemplate(c.Writer, name, data); err != nil { c.String(500, "Template error: %v", err) } } func (h *WebHandler) Index(c *gin.Context) { // Redirect to projects page - configurator is accessed via /configurator?uuid=... c.Redirect(302, "/projects") } func (h *WebHandler) Configurator(c *gin.Context) { uuid := c.Query("uuid") categories, _ := h.localCategories() components, total, err := h.localDB.ListComponents(localdb.ComponentFilter{}, 0, 20) data := gin.H{ "ActivePage": "configurator", "Categories": categories, "Components": []localComponentView{}, "Total": int64(0), "Page": 1, "PerPage": 20, "ConfigUUID": uuid, } if err == nil { data["Components"] = toLocalComponentViews(components) data["Total"] = total } h.render(c, "index.html", data) } func (h *WebHandler) Configs(c *gin.Context) { h.render(c, "configs.html", gin.H{"ActivePage": "configs"}) } func (h *WebHandler) Projects(c *gin.Context) { h.render(c, "projects.html", gin.H{"ActivePage": "projects"}) } func (h *WebHandler) ProjectDetail(c *gin.Context) { h.render(c, "project_detail.html", gin.H{ "ActivePage": "projects", "ProjectUUID": c.Param("uuid"), }) } func (h *WebHandler) ConfigRevisions(c *gin.Context) { h.render(c, "config_revisions.html", gin.H{ "ActivePage": "configs", "ConfigUUID": c.Param("uuid"), }) } func (h *WebHandler) Pricelists(c *gin.Context) { h.render(c, "pricelists.html", gin.H{"ActivePage": "pricelists"}) } func (h *WebHandler) PricelistDetail(c *gin.Context) { h.render(c, "pricelist_detail.html", gin.H{"ActivePage": "pricelists"}) } func (h *WebHandler) PartnumberBooks(c *gin.Context) { h.render(c, "partnumber_books.html", gin.H{"ActivePage": "partnumber-books"}) } // Partials for htmx func (h *WebHandler) ComponentsPartial(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) if page < 1 { page = 1 } filter := localdb.ComponentFilter{ Category: c.Query("category"), Search: c.Query("search"), } if c.Query("has_price") == "true" { filter.HasPrice = true } offset := (page - 1) * 20 data := gin.H{ "Components": []localComponentView{}, "Total": int64(0), "Page": page, "PerPage": 20, } components, total, err := h.localDB.ListComponents(filter, offset, 20) if err == nil { data["Components"] = toLocalComponentViews(components) data["Total"] = total } c.Header("Content-Type", "text/html; charset=utf-8") if tmpl, ok := h.templates["components_list.html"]; ok { tmpl.ExecuteTemplate(c.Writer, "components_list.html", data) } } type localComponentView struct { LotName string Description string Category string CategoryName string Model string CurrentPrice *float64 } func toLocalComponentViews(items []localdb.LocalComponent) []localComponentView { result := make([]localComponentView, 0, len(items)) for _, item := range items { result = append(result, localComponentView{ LotName: item.LotName, Description: item.LotDescription, Category: item.Category, CategoryName: item.Category, Model: item.Model, }) } return result } func (h *WebHandler) localCategories() ([]models.Category, error) { codes, err := h.localDB.GetLocalComponentCategories() if err != nil || len(codes) == 0 { return []models.Category{}, err } categories := make([]models.Category, 0, len(codes)) for _, code := range codes { trimmed := strings.TrimSpace(code) if trimmed == "" { continue } categories = append(categories, models.Category{ Code: trimmed, Name: trimmed, }) } return categories, nil }