271 lines
6.5 KiB
Go
271 lines
6.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"strconv"
|
|
"strings"
|
|
|
|
qfassets "git.mchus.pro/mchus/quoteforge"
|
|
"git.mchus.pro/mchus/quoteforge/internal/appmeta"
|
|
"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) {
|
|
data["AppVersion"] = appmeta.Version()
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
tmpl, ok := h.templates[name]
|
|
if !ok {
|
|
_ = c.Error(fmt.Errorf("template %q not found", name))
|
|
c.String(500, "Template error")
|
|
return
|
|
}
|
|
// Execute the page template which will use base
|
|
if err := tmpl.ExecuteTemplate(c.Writer, name, data); err != nil {
|
|
_ = c.Error(err)
|
|
c.String(500, "Template error")
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|