diff --git a/backend/gestion/db/db_init.go b/backend/gestion/db/db_init.go index 6bd29940..980681fb 100644 --- a/backend/gestion/db/db_init.go +++ b/backend/gestion/db/db_init.go @@ -53,7 +53,7 @@ func InitDB() *Database { // Configuration du pool de connexions db.SetMaxOpenConns(50) db.SetMaxIdleConns(10) - db.SetConnMaxLifetime(5 * time.Minute) + db.SetConnMaxLifetime(30 * time.Minute) // Tester la connexion if err = db.Ping(); err != nil { @@ -66,7 +66,9 @@ func InitDB() *Database { gormDB, err := gorm.Open(postgres.New(postgres.Config{ Conn: db, }), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), + SkipDefaultTransaction: true, + PrepareStmt: true, + Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { log.Fatalf("❌ Erreur initialisation GORM: %v", err) diff --git a/backend/gestion/db/db_stat.go b/backend/gestion/db/db_stat.go index 194422ad..b0c9a9f4 100644 --- a/backend/gestion/db/db_stat.go +++ b/backend/gestion/db/db_stat.go @@ -57,15 +57,35 @@ type AdminStatsFilters struct { ResetDoses time.Time } -// LoadAdminStatsFilters lit toutes les dates de reset nécessaires au dashboard. +// LoadAdminStatsFilters lit toutes les dates de reset en une seule requête. func (d *Database) LoadAdminStatsFilters() AdminStatsFilters { + keys := []string{ + "stats_reset_commandes_at", + "stats_reset_revenus_at", + "stats_reset_produits_at", + "stats_reset_heures_at", + "stats_reset_jours_at", + "stats_reset_doses_at", + } + var rows []struct { + Key string `gorm:"column:key"` + Value string `gorm:"column:value"` + } + d.GDB.Table("app_settings").Select("key, value").Where("key IN ?", keys).Scan(&rows) + + m := make(map[string]time.Time, len(keys)) + for _, r := range rows { + if t, err := time.Parse(time.RFC3339, r.Value); err == nil { + m[r.Key] = t + } + } return AdminStatsFilters{ - ResetCommandes: d.ReadResetAt("stats_reset_commandes_at"), - ResetRevenus: d.ReadResetAt("stats_reset_revenus_at"), - ResetProduits: d.ReadResetAt("stats_reset_produits_at"), - ResetHeures: d.ReadResetAt("stats_reset_heures_at"), - ResetJours: d.ReadResetAt("stats_reset_jours_at"), - ResetDoses: d.ReadResetAt("stats_reset_doses_at"), + ResetCommandes: m["stats_reset_commandes_at"], + ResetRevenus: m["stats_reset_revenus_at"], + ResetProduits: m["stats_reset_produits_at"], + ResetHeures: m["stats_reset_heures_at"], + ResetJours: m["stats_reset_jours_at"], + ResetDoses: m["stats_reset_doses_at"], } } diff --git a/backend/gestion/handlers/panier.go b/backend/gestion/handlers/panier.go index bcc3175a..bf918923 100644 --- a/backend/gestion/handlers/panier.go +++ b/backend/gestion/handlers/panier.go @@ -477,7 +477,15 @@ func ValidateBasket(c *gin.Context) { priceAmt, _ := payResp.PriceAmount.Float64() payAmt, _ := payResp.PayAmount.Float64() - _, _ = database.CreateCryptoPayment(commandID, payResp.PaymentID.String(), payResp.Status, payResp.PriceCurrency, payResp.PayCurrency, payResp.PayAddress, priceAmt, payAmt) + if _, err := database.CreateCryptoPayment(commandID, payResp.PaymentID.String(), payResp.Status, payResp.PriceCurrency, payResp.PayCurrency, payResp.PayAddress, priceAmt, payAmt); err != nil { + log.Printf("❌ [CHECKOUT] Erreur enregistrement paiement crypto (commande %d, nowpayment %s): %v", commandID, payResp.PaymentID.String(), err) + _ = database.CancelCryptoCommand(commandID) + if referralUsed > 0 { + _ = database.CreditClientReferral(usernameStr, referralUsed) + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur interne lors de l'enregistrement du paiement"}) + return + } log.Printf("✅ [CHECKOUT] Commande %d en attente paiement crypto (%s)", commandID, req.PayCurrency) c.JSON(http.StatusCreated, gin.H{ diff --git a/backend/gestion/handlers/stats.go b/backend/gestion/handlers/stats.go index fc939360..cbfe6269 100644 --- a/backend/gestion/handlers/stats.go +++ b/backend/gestion/handlers/stats.go @@ -1,6 +1,7 @@ package handlers import ( + "context" "fmt" "gestion/db" "gestion/models" @@ -8,6 +9,7 @@ import ( "time" "github.com/gin-gonic/gin" + "golang.org/x/sync/errgroup" ) var weekdayNames = []string{"Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"} @@ -200,10 +202,62 @@ func GetAdminStats(c *gin.Context) { filters := database.LoadAdminStatsFilters() - // ── Commandes par jour de la semaine (non annulées) ──────────────────────── - var wdRows []models.WeekdayRow - database.OrderPerDaysPerWeeks(&wdRows, filters.ResetJours) + // Toutes les requêtes sont indépendantes — on les lance en parallèle. + var ( + wdRows []models.WeekdayRow + dayRows []models.DayRow + dayRevRows []models.DayRevenueRow + hourRows []models.HourRow + prodRows []models.ProductRow + qtyRows []models.QuantityBreakdownRow + dailyRows []models.DailyProductRow + totalOrders int64 + totalRevenue float64 + dailyTotalOrders int64 + activeDays int64 + last30Count int64 + ) + eg, _ := errgroup.WithContext(context.Background()) + eg.Go(func() error { return database.OrderPerDaysPerWeeks(&wdRows, filters.ResetJours) }) + eg.Go(func() error { return database.OrdersByDayLast30(&dayRows, filters.ResetCommandes) }) + eg.Go(func() error { return database.RevenueByDayLast30(&dayRevRows, filters.ResetRevenus) }) + eg.Go(func() error { return database.OrdersAndRevenueByHour(&hourRows, filters.ResetHeures) }) + eg.Go(func() error { return database.TopProducts(&prodRows, filters.ResetProduits, 15) }) + eg.Go(func() error { return database.QuantityBreakdown(&qtyRows, filters.ResetDoses) }) + eg.Go(func() error { return database.DailyProductDetail(&dailyRows) }) + eg.Go(func() error { + var err error + totalOrders, err = database.TotalOrders(filters.ResetCommandes) + return err + }) + eg.Go(func() error { + var err error + totalRevenue, err = database.TotalRevenue(filters.ResetRevenus) + return err + }) + eg.Go(func() error { + var err error + dailyTotalOrders, err = database.DailyOrdersCount() + return err + }) + eg.Go(func() error { + var err error + activeDays, err = database.ActiveDaysLast30(filters.ResetCommandes) + return err + }) + eg.Go(func() error { + var err error + last30Count, err = database.OrdersCountLast30(filters.ResetCommandes) + return err + }) + + if err := eg.Wait(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la récupération des statistiques"}) + return + } + + // ── Commandes par jour de la semaine ────────────────────────────────────── byWeekday := make([]gin.H, 7) wdMap := make(map[int]int, len(wdRows)) for _, r := range wdRows { @@ -220,9 +274,6 @@ func GetAdminStats(c *gin.Context) { } // ── Commandes par jour sur 30 jours ─────────────────────────────────────── - var dayRows []models.DayRow - database.OrdersByDayLast30(&dayRows, filters.ResetCommandes) - byDay := make([]gin.H, len(dayRows)) for i, r := range dayRows { byDay[i] = gin.H{ @@ -232,10 +283,7 @@ func GetAdminStats(c *gin.Context) { } } - // ── Revenus par jour sur 30 jours (commandes approuvées) ───────────────── - var dayRevRows []models.DayRevenueRow - database.RevenueByDayLast30(&dayRevRows, filters.ResetRevenus) - + // ── Revenus par jour sur 30 jours ───────────────────────────────────────── byDayRevenue := make([]gin.H, len(dayRevRows)) for i, r := range dayRevRows { byDayRevenue[i] = gin.H{ @@ -245,10 +293,7 @@ func GetAdminStats(c *gin.Context) { } } - // ── Commandes & revenus par heure (non annulées) ───────────────────────── - var hourRows []models.HourRow - database.OrdersAndRevenueByHour(&hourRows, filters.ResetHeures) - + // ── Commandes & revenus par heure ───────────────────────────────────────── hourMap := make(map[int]models.HourRow, len(hourRows)) for _, r := range hourRows { hourMap[r.Hour] = r @@ -264,10 +309,7 @@ func GetAdminStats(c *gin.Context) { } } - // ── Top produits (quantité vendue, commandes terminées) ─────────────────── - var prodRows []models.ProductRow - database.TopProducts(&prodRows, filters.ResetProduits, 15) - + // ── Top produits ────────────────────────────────────────────────────────── topProducts := make([]gin.H, len(prodRows)) topProductName := "" for i, r := range prodRows { @@ -285,10 +327,7 @@ func GetAdminStats(c *gin.Context) { } } - // ── Répartition des doses/quantités par produit ─────────────────────────── - var qtyRows []models.QuantityBreakdownRow - database.QuantityBreakdown(&qtyRows, filters.ResetDoses) - + // ── Répartition des doses/quantités ─────────────────────────────────────── type productGroup struct { ProductID int Name string @@ -317,8 +356,6 @@ func GetAdminStats(c *gin.Context) { "revenue": r.Revenue, }) } - - // Trier par total de commandes décroissant, garder 15 max for i := 0; i < len(groups)-1; i++ { for j := i + 1; j < len(groups); j++ { if groups[j].TotalOrders > groups[i].TotalOrders { @@ -329,22 +366,18 @@ func GetAdminStats(c *gin.Context) { if len(groups) > 15 { groups = groups[:15] } - byQuantity := make([]gin.H, len(groups)) - for i, g := range groups { + for i, grp := range groups { byQuantity[i] = gin.H{ - "product_id": g.ProductID, - "name": g.Name, - "category_color": g.CategoryColor, - "total_orders": g.TotalOrders, - "quantities": g.Quantities, + "product_id": grp.ProductID, + "name": grp.Name, + "category_color": grp.CategoryColor, + "total_orders": grp.TotalOrders, + "quantities": grp.Quantities, } } - // ── Détail du jour (catégorie → produits) ──────────────────────────────── - var dailyRows []models.DailyProductRow - database.DailyProductDetail(&dailyRows) - + // ── Détail du jour ──────────────────────────────────────────────────────── type dailyCatGroup struct { Category string CategoryColor string @@ -356,7 +389,6 @@ func GetAdminStats(c *gin.Context) { dailyCatIdx := map[string]int{} dailyTotalRevenue := 0.0 dailyTotalQty := 0.0 - for _, r := range dailyRows { dailyTotalRevenue += r.Revenue dailyTotalQty += r.TotalQuantity @@ -379,31 +411,21 @@ func GetAdminStats(c *gin.Context) { "revenue": r.Revenue, }) } - dailyCatsJSON := make([]gin.H, len(dailyCats)) - for i, g := range dailyCats { + for i, grp := range dailyCats { dailyCatsJSON[i] = gin.H{ - "category": g.Category, - "category_color": g.CategoryColor, - "total_quantity": g.TotalQuantity, - "total_revenue": g.TotalRevenue, - "products": g.Products, + "category": grp.Category, + "category_color": grp.CategoryColor, + "total_quantity": grp.TotalQuantity, + "total_revenue": grp.TotalRevenue, + "products": grp.Products, } } - dailyTotalOrders, _ := database.DailyOrdersCount() - // ── Résumé global ───────────────────────────────────────────────────────── - totalOrders, _ := database.TotalOrders(filters.ResetCommandes) - totalRevenue, _ := database.TotalRevenue(filters.ResetRevenus) - avgPerDay := 0.0 - if totalOrders > 0 { - activeDays, _ := database.ActiveDaysLast30(filters.ResetCommandes) - if activeDays > 0 { - last30Count, _ := database.OrdersCountLast30(filters.ResetCommandes) - avgPerDay = float64(last30Count) / float64(activeDays) - } + if totalOrders > 0 && activeDays > 0 { + avgPerDay = float64(last30Count) / float64(activeDays) } c.JSON(http.StatusOK, gin.H{