Redesign configurator UI with tabs and remove Excel export

- Add tab-based configurator (Base, Storage, PCI, Power, Accessories, Other)
- Base tab: single-select with autocomplete for MB, CPU, MEM
- Other tabs: multi-select with autocomplete and quantity input
- Table view with LOT, Description, Price, Quantity, Total columns
- Add configuration list page with create modal (opportunity number)
- Remove Excel export functionality and excelize dependency
- Increase component list limit from 100 to 5000
- Add web templates (base, index, configs, login, admin_pricing)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-01-26 15:57:15 +03:00
parent 44ccb01203
commit a93644131c
14 changed files with 2041 additions and 201 deletions

View File

@@ -11,14 +11,15 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/mchus/quoteforge/internal/config"
"github.com/mchus/quoteforge/internal/handlers"
"github.com/mchus/quoteforge/internal/middleware"
"github.com/mchus/quoteforge/internal/models"
"github.com/mchus/quoteforge/internal/repository"
"github.com/mchus/quoteforge/internal/services"
"github.com/mchus/quoteforge/internal/services/alerts"
"github.com/mchus/quoteforge/internal/services/pricing"
"git.mchus.pro/mchus/quoteforge/internal/config"
"git.mchus.pro/mchus/quoteforge/internal/handlers"
"git.mchus.pro/mchus/quoteforge/internal/middleware"
"git.mchus.pro/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/repository"
"git.mchus.pro/mchus/quoteforge/internal/services"
"git.mchus.pro/mchus/quoteforge/internal/services/alerts"
"git.mchus.pro/mchus/quoteforge/internal/services/pricing"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
@@ -59,11 +60,21 @@ func main() {
slog.Error("seeding categories failed", "error", err)
os.Exit(1)
}
// Create default admin user (admin / admin123)
adminHash, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
if err := models.SeedAdminUser(db, string(adminHash)); err != nil {
slog.Error("seeding admin user failed", "error", err)
os.Exit(1)
}
slog.Info("migrations completed")
}
gin.SetMode(cfg.Server.Mode)
router := setupRouter(db, cfg)
router, err := setupRouter(db, cfg)
if err != nil {
slog.Error("failed to setup router", "error", err)
os.Exit(1)
}
srv := &http.Server{
Addr: cfg.Address(),
@@ -143,7 +154,7 @@ func setupDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
return db, nil
}
func setupRouter(db *gorm.DB, cfg *config.Config) *gin.Engine {
func setupRouter(db *gorm.DB, cfg *config.Config) (*gin.Engine, error) {
// Repositories
userRepo := repository.NewUserRepository(db)
componentRepo := repository.NewComponentRepository(db)
@@ -168,7 +179,13 @@ func setupRouter(db *gorm.DB, cfg *config.Config) *gin.Engine {
quoteHandler := handlers.NewQuoteHandler(quoteService)
configHandler := handlers.NewConfigurationHandler(configService, exportService)
exportHandler := handlers.NewExportHandler(exportService, configService, componentService)
pricingHandler := handlers.NewPricingHandler(pricingService, alertService, componentRepo, statsRepo)
pricingHandler := handlers.NewPricingHandler(db, pricingService, alertService, componentRepo, priceRepo, statsRepo)
// Web handler (templates)
webHandler, err := handlers.NewWebHandler("web/templates", componentService)
if err != nil {
return nil, err
}
// Router
router := gin.New()
@@ -176,6 +193,9 @@ func setupRouter(db *gorm.DB, cfg *config.Config) *gin.Engine {
router.Use(requestLogger())
router.Use(middleware.CORS())
// Static files
router.Static("/static", "web/static")
// Health check
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
@@ -184,6 +204,47 @@ func setupRouter(db *gorm.DB, cfg *config.Config) *gin.Engine {
})
})
// DB status endpoint
router.GET("/api/db-status", func(c *gin.Context) {
var lotCount, lotLogCount, metadataCount int64
var dbOK bool = true
var dbError string
sqlDB, err := db.DB()
if err != nil {
dbOK = false
dbError = err.Error()
} else if err := sqlDB.Ping(); err != nil {
dbOK = false
dbError = err.Error()
}
db.Table("lot").Count(&lotCount)
db.Table("lot_log").Count(&lotLogCount)
db.Table("qt_lot_metadata").Count(&metadataCount)
c.JSON(http.StatusOK, gin.H{
"connected": dbOK,
"error": dbError,
"lot_count": lotCount,
"lot_log_count": lotLogCount,
"metadata_count": metadataCount,
})
})
// Web pages
router.GET("/", webHandler.Index)
router.GET("/login", webHandler.Login)
router.GET("/configs", webHandler.Configs)
router.GET("/configurator", webHandler.Configurator)
router.GET("/admin/pricing", webHandler.AdminPricing)
// htmx partials
partials := router.Group("/partials")
{
partials.GET("/components", webHandler.ComponentsPartial)
}
// API routes
api := router.Group("/api")
{
@@ -209,7 +270,6 @@ func setupRouter(db *gorm.DB, cfg *config.Config) *gin.Engine {
// Categories (public)
api.GET("/categories", componentHandler.GetCategories)
api.GET("/vendors", componentHandler.GetVendors)
// Quote (public, for anonymous quote building)
quote := api.Group("/quote")
@@ -222,7 +282,6 @@ func setupRouter(db *gorm.DB, cfg *config.Config) *gin.Engine {
export := api.Group("/export")
{
export.POST("/csv", exportHandler.ExportCSV)
export.POST("/xlsx", exportHandler.ExportXLSX)
}
// Configurations (requires auth)
@@ -237,7 +296,6 @@ func setupRouter(db *gorm.DB, cfg *config.Config) *gin.Engine {
configs.DELETE("/:uuid", configHandler.Delete)
configs.GET("/:uuid/export", configHandler.ExportJSON)
configs.GET("/:uuid/csv", exportHandler.ExportConfigCSV)
configs.GET("/:uuid/xlsx", exportHandler.ExportConfigXLSX)
configs.POST("/import", configHandler.ImportJSON)
}
}
@@ -254,6 +312,7 @@ func setupRouter(db *gorm.DB, cfg *config.Config) *gin.Engine {
pricingAdmin.GET("/components", pricingHandler.ListComponents)
pricingAdmin.GET("/components/:lot_name", pricingHandler.GetComponentPricing)
pricingAdmin.POST("/update", pricingHandler.UpdatePrice)
pricingAdmin.POST("/preview", pricingHandler.PreviewPrice)
pricingAdmin.POST("/recalculate-all", pricingHandler.RecalculateAll)
pricingAdmin.GET("/alerts", pricingHandler.ListAlerts)
@@ -263,7 +322,7 @@ func setupRouter(db *gorm.DB, cfg *config.Config) *gin.Engine {
}
}
return router
return router, nil
}
func requestLogger() gin.HandlerFunc {