- Go module with Gin, GORM, JWT, excelize dependencies - Configuration loading from YAML with all settings - GORM models for users, categories, components, configurations, alerts - Repository layer for all entities - Services: auth (JWT), pricing (median/average/weighted), components, quotes, configurations, export (CSV/XLSX), alerts - Middleware: JWT auth, role-based access, CORS - HTTP handlers for all API endpoints - Main server with dependency injection and graceful shutdown Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
211 lines
5.5 KiB
Go
211 lines
5.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/mchus/quoteforge/internal/middleware"
|
|
"github.com/mchus/quoteforge/internal/models"
|
|
"github.com/mchus/quoteforge/internal/repository"
|
|
"github.com/mchus/quoteforge/internal/services/alerts"
|
|
"github.com/mchus/quoteforge/internal/services/pricing"
|
|
)
|
|
|
|
type PricingHandler struct {
|
|
pricingService *pricing.Service
|
|
alertService *alerts.Service
|
|
componentRepo *repository.ComponentRepository
|
|
statsRepo *repository.StatsRepository
|
|
}
|
|
|
|
func NewPricingHandler(
|
|
pricingService *pricing.Service,
|
|
alertService *alerts.Service,
|
|
componentRepo *repository.ComponentRepository,
|
|
statsRepo *repository.StatsRepository,
|
|
) *PricingHandler {
|
|
return &PricingHandler{
|
|
pricingService: pricingService,
|
|
alertService: alertService,
|
|
componentRepo: componentRepo,
|
|
statsRepo: statsRepo,
|
|
}
|
|
}
|
|
|
|
func (h *PricingHandler) GetStats(c *gin.Context) {
|
|
newAlerts, _ := h.alertService.GetNewAlertsCount()
|
|
topComponents, _ := h.statsRepo.GetTopComponents(10)
|
|
trendingComponents, _ := h.statsRepo.GetTrendingComponents(10)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"new_alerts_count": newAlerts,
|
|
"top_components": topComponents,
|
|
"trending_components": trendingComponents,
|
|
})
|
|
}
|
|
|
|
func (h *PricingHandler) ListComponents(c *gin.Context) {
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))
|
|
|
|
filter := repository.ComponentFilter{
|
|
Category: c.Query("category"),
|
|
Vendor: c.Query("vendor"),
|
|
Search: c.Query("search"),
|
|
}
|
|
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if perPage < 1 || perPage > 100 {
|
|
perPage = 20
|
|
}
|
|
offset := (page - 1) * perPage
|
|
|
|
components, total, err := h.componentRepo.List(filter, offset, perPage)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"components": components,
|
|
"total": total,
|
|
"page": page,
|
|
"per_page": perPage,
|
|
})
|
|
}
|
|
|
|
func (h *PricingHandler) GetComponentPricing(c *gin.Context) {
|
|
lotName := c.Param("lot_name")
|
|
|
|
component, err := h.componentRepo.GetByLotName(lotName)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "component not found"})
|
|
return
|
|
}
|
|
|
|
stats, err := h.pricingService.GetPriceStats(lotName, 0)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"component": component,
|
|
"price_stats": stats,
|
|
})
|
|
}
|
|
|
|
type UpdatePriceRequest struct {
|
|
LotName string `json:"lot_name" binding:"required"`
|
|
Method models.PriceMethod `json:"method"`
|
|
PeriodDays int `json:"period_days"`
|
|
ManualPrice *float64 `json:"manual_price"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
func (h *PricingHandler) UpdatePrice(c *gin.Context) {
|
|
userID := middleware.GetUserID(c)
|
|
|
|
var req UpdatePriceRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if req.ManualPrice != nil && *req.ManualPrice > 0 {
|
|
err := h.pricingService.SetManualPrice(req.LotName, *req.ManualPrice, req.Reason, userID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
|
|
if req.Method != "" {
|
|
err := h.pricingService.UpdatePriceMethod(req.LotName, req.Method, req.PeriodDays)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "price updated"})
|
|
}
|
|
|
|
func (h *PricingHandler) RecalculateAll(c *gin.Context) {
|
|
// This would be better as a background job
|
|
c.JSON(http.StatusAccepted, gin.H{"message": "recalculation started"})
|
|
}
|
|
|
|
func (h *PricingHandler) ListAlerts(c *gin.Context) {
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))
|
|
|
|
filter := repository.AlertFilter{
|
|
Status: models.AlertStatus(c.Query("status")),
|
|
Severity: models.AlertSeverity(c.Query("severity")),
|
|
Type: models.AlertType(c.Query("type")),
|
|
LotName: c.Query("lot_name"),
|
|
}
|
|
|
|
alertsList, total, err := h.alertService.List(filter, page, perPage)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"alerts": alertsList,
|
|
"total": total,
|
|
"page": page,
|
|
"per_page": perPage,
|
|
})
|
|
}
|
|
|
|
func (h *PricingHandler) AcknowledgeAlert(c *gin.Context) {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid alert id"})
|
|
return
|
|
}
|
|
|
|
if err := h.alertService.Acknowledge(uint(id)); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "acknowledged"})
|
|
}
|
|
|
|
func (h *PricingHandler) ResolveAlert(c *gin.Context) {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid alert id"})
|
|
return
|
|
}
|
|
|
|
if err := h.alertService.Resolve(uint(id)); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "resolved"})
|
|
}
|
|
|
|
func (h *PricingHandler) IgnoreAlert(c *gin.Context) {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid alert id"})
|
|
return
|
|
}
|
|
|
|
if err := h.alertService.Ignore(uint(id)); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "ignored"})
|
|
}
|