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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user