This commit is contained in:
@@ -53,7 +53,7 @@ func InitDB() *Database {
|
|||||||
// Configuration du pool de connexions
|
// Configuration du pool de connexions
|
||||||
db.SetMaxOpenConns(50)
|
db.SetMaxOpenConns(50)
|
||||||
db.SetMaxIdleConns(10)
|
db.SetMaxIdleConns(10)
|
||||||
db.SetConnMaxLifetime(5 * time.Minute)
|
db.SetConnMaxLifetime(30 * time.Minute)
|
||||||
|
|
||||||
// Tester la connexion
|
// Tester la connexion
|
||||||
if err = db.Ping(); err != nil {
|
if err = db.Ping(); err != nil {
|
||||||
@@ -66,7 +66,9 @@ func InitDB() *Database {
|
|||||||
gormDB, err := gorm.Open(postgres.New(postgres.Config{
|
gormDB, err := gorm.Open(postgres.New(postgres.Config{
|
||||||
Conn: db,
|
Conn: db,
|
||||||
}), &gorm.Config{
|
}), &gorm.Config{
|
||||||
Logger: logger.Default.LogMode(logger.Silent),
|
SkipDefaultTransaction: true,
|
||||||
|
PrepareStmt: true,
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("❌ Erreur initialisation GORM: %v", err)
|
log.Fatalf("❌ Erreur initialisation GORM: %v", err)
|
||||||
|
|||||||
@@ -57,15 +57,35 @@ type AdminStatsFilters struct {
|
|||||||
ResetDoses time.Time
|
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 {
|
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{
|
return AdminStatsFilters{
|
||||||
ResetCommandes: d.ReadResetAt("stats_reset_commandes_at"),
|
ResetCommandes: m["stats_reset_commandes_at"],
|
||||||
ResetRevenus: d.ReadResetAt("stats_reset_revenus_at"),
|
ResetRevenus: m["stats_reset_revenus_at"],
|
||||||
ResetProduits: d.ReadResetAt("stats_reset_produits_at"),
|
ResetProduits: m["stats_reset_produits_at"],
|
||||||
ResetHeures: d.ReadResetAt("stats_reset_heures_at"),
|
ResetHeures: m["stats_reset_heures_at"],
|
||||||
ResetJours: d.ReadResetAt("stats_reset_jours_at"),
|
ResetJours: m["stats_reset_jours_at"],
|
||||||
ResetDoses: d.ReadResetAt("stats_reset_doses_at"),
|
ResetDoses: m["stats_reset_doses_at"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -477,7 +477,15 @@ func ValidateBasket(c *gin.Context) {
|
|||||||
|
|
||||||
priceAmt, _ := payResp.PriceAmount.Float64()
|
priceAmt, _ := payResp.PriceAmount.Float64()
|
||||||
payAmt, _ := payResp.PayAmount.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)
|
log.Printf("✅ [CHECKOUT] Commande %d en attente paiement crypto (%s)", commandID, req.PayCurrency)
|
||||||
c.JSON(http.StatusCreated, gin.H{
|
c.JSON(http.StatusCreated, gin.H{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gestion/db"
|
"gestion/db"
|
||||||
"gestion/models"
|
"gestion/models"
|
||||||
@@ -8,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
var weekdayNames = []string{"Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"}
|
var weekdayNames = []string{"Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"}
|
||||||
@@ -200,10 +202,62 @@ func GetAdminStats(c *gin.Context) {
|
|||||||
|
|
||||||
filters := database.LoadAdminStatsFilters()
|
filters := database.LoadAdminStatsFilters()
|
||||||
|
|
||||||
// ── Commandes par jour de la semaine (non annulées) ────────────────────────
|
// Toutes les requêtes sont indépendantes — on les lance en parallèle.
|
||||||
var wdRows []models.WeekdayRow
|
var (
|
||||||
database.OrderPerDaysPerWeeks(&wdRows, filters.ResetJours)
|
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)
|
byWeekday := make([]gin.H, 7)
|
||||||
wdMap := make(map[int]int, len(wdRows))
|
wdMap := make(map[int]int, len(wdRows))
|
||||||
for _, r := range wdRows {
|
for _, r := range wdRows {
|
||||||
@@ -220,9 +274,6 @@ func GetAdminStats(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Commandes par jour sur 30 jours ───────────────────────────────────────
|
// ── Commandes par jour sur 30 jours ───────────────────────────────────────
|
||||||
var dayRows []models.DayRow
|
|
||||||
database.OrdersByDayLast30(&dayRows, filters.ResetCommandes)
|
|
||||||
|
|
||||||
byDay := make([]gin.H, len(dayRows))
|
byDay := make([]gin.H, len(dayRows))
|
||||||
for i, r := range dayRows {
|
for i, r := range dayRows {
|
||||||
byDay[i] = gin.H{
|
byDay[i] = gin.H{
|
||||||
@@ -232,10 +283,7 @@ func GetAdminStats(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Revenus par jour sur 30 jours (commandes approuvées) ─────────────────
|
// ── Revenus par jour sur 30 jours ─────────────────────────────────────────
|
||||||
var dayRevRows []models.DayRevenueRow
|
|
||||||
database.RevenueByDayLast30(&dayRevRows, filters.ResetRevenus)
|
|
||||||
|
|
||||||
byDayRevenue := make([]gin.H, len(dayRevRows))
|
byDayRevenue := make([]gin.H, len(dayRevRows))
|
||||||
for i, r := range dayRevRows {
|
for i, r := range dayRevRows {
|
||||||
byDayRevenue[i] = gin.H{
|
byDayRevenue[i] = gin.H{
|
||||||
@@ -245,10 +293,7 @@ func GetAdminStats(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Commandes & revenus par heure (non annulées) ─────────────────────────
|
// ── Commandes & revenus par heure ─────────────────────────────────────────
|
||||||
var hourRows []models.HourRow
|
|
||||||
database.OrdersAndRevenueByHour(&hourRows, filters.ResetHeures)
|
|
||||||
|
|
||||||
hourMap := make(map[int]models.HourRow, len(hourRows))
|
hourMap := make(map[int]models.HourRow, len(hourRows))
|
||||||
for _, r := range hourRows {
|
for _, r := range hourRows {
|
||||||
hourMap[r.Hour] = r
|
hourMap[r.Hour] = r
|
||||||
@@ -264,10 +309,7 @@ func GetAdminStats(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Top produits (quantité vendue, commandes terminées) ───────────────────
|
// ── Top produits ──────────────────────────────────────────────────────────
|
||||||
var prodRows []models.ProductRow
|
|
||||||
database.TopProducts(&prodRows, filters.ResetProduits, 15)
|
|
||||||
|
|
||||||
topProducts := make([]gin.H, len(prodRows))
|
topProducts := make([]gin.H, len(prodRows))
|
||||||
topProductName := ""
|
topProductName := ""
|
||||||
for i, r := range prodRows {
|
for i, r := range prodRows {
|
||||||
@@ -285,10 +327,7 @@ func GetAdminStats(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Répartition des doses/quantités par produit ───────────────────────────
|
// ── Répartition des doses/quantités ───────────────────────────────────────
|
||||||
var qtyRows []models.QuantityBreakdownRow
|
|
||||||
database.QuantityBreakdown(&qtyRows, filters.ResetDoses)
|
|
||||||
|
|
||||||
type productGroup struct {
|
type productGroup struct {
|
||||||
ProductID int
|
ProductID int
|
||||||
Name string
|
Name string
|
||||||
@@ -317,8 +356,6 @@ func GetAdminStats(c *gin.Context) {
|
|||||||
"revenue": r.Revenue,
|
"revenue": r.Revenue,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trier par total de commandes décroissant, garder 15 max
|
|
||||||
for i := 0; i < len(groups)-1; i++ {
|
for i := 0; i < len(groups)-1; i++ {
|
||||||
for j := i + 1; j < len(groups); j++ {
|
for j := i + 1; j < len(groups); j++ {
|
||||||
if groups[j].TotalOrders > groups[i].TotalOrders {
|
if groups[j].TotalOrders > groups[i].TotalOrders {
|
||||||
@@ -329,22 +366,18 @@ func GetAdminStats(c *gin.Context) {
|
|||||||
if len(groups) > 15 {
|
if len(groups) > 15 {
|
||||||
groups = groups[:15]
|
groups = groups[:15]
|
||||||
}
|
}
|
||||||
|
|
||||||
byQuantity := make([]gin.H, len(groups))
|
byQuantity := make([]gin.H, len(groups))
|
||||||
for i, g := range groups {
|
for i, grp := range groups {
|
||||||
byQuantity[i] = gin.H{
|
byQuantity[i] = gin.H{
|
||||||
"product_id": g.ProductID,
|
"product_id": grp.ProductID,
|
||||||
"name": g.Name,
|
"name": grp.Name,
|
||||||
"category_color": g.CategoryColor,
|
"category_color": grp.CategoryColor,
|
||||||
"total_orders": g.TotalOrders,
|
"total_orders": grp.TotalOrders,
|
||||||
"quantities": g.Quantities,
|
"quantities": grp.Quantities,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Détail du jour (catégorie → produits) ────────────────────────────────
|
// ── Détail du jour ────────────────────────────────────────────────────────
|
||||||
var dailyRows []models.DailyProductRow
|
|
||||||
database.DailyProductDetail(&dailyRows)
|
|
||||||
|
|
||||||
type dailyCatGroup struct {
|
type dailyCatGroup struct {
|
||||||
Category string
|
Category string
|
||||||
CategoryColor string
|
CategoryColor string
|
||||||
@@ -356,7 +389,6 @@ func GetAdminStats(c *gin.Context) {
|
|||||||
dailyCatIdx := map[string]int{}
|
dailyCatIdx := map[string]int{}
|
||||||
dailyTotalRevenue := 0.0
|
dailyTotalRevenue := 0.0
|
||||||
dailyTotalQty := 0.0
|
dailyTotalQty := 0.0
|
||||||
|
|
||||||
for _, r := range dailyRows {
|
for _, r := range dailyRows {
|
||||||
dailyTotalRevenue += r.Revenue
|
dailyTotalRevenue += r.Revenue
|
||||||
dailyTotalQty += r.TotalQuantity
|
dailyTotalQty += r.TotalQuantity
|
||||||
@@ -379,31 +411,21 @@ func GetAdminStats(c *gin.Context) {
|
|||||||
"revenue": r.Revenue,
|
"revenue": r.Revenue,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
dailyCatsJSON := make([]gin.H, len(dailyCats))
|
dailyCatsJSON := make([]gin.H, len(dailyCats))
|
||||||
for i, g := range dailyCats {
|
for i, grp := range dailyCats {
|
||||||
dailyCatsJSON[i] = gin.H{
|
dailyCatsJSON[i] = gin.H{
|
||||||
"category": g.Category,
|
"category": grp.Category,
|
||||||
"category_color": g.CategoryColor,
|
"category_color": grp.CategoryColor,
|
||||||
"total_quantity": g.TotalQuantity,
|
"total_quantity": grp.TotalQuantity,
|
||||||
"total_revenue": g.TotalRevenue,
|
"total_revenue": grp.TotalRevenue,
|
||||||
"products": g.Products,
|
"products": grp.Products,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dailyTotalOrders, _ := database.DailyOrdersCount()
|
|
||||||
|
|
||||||
// ── Résumé global ─────────────────────────────────────────────────────────
|
// ── Résumé global ─────────────────────────────────────────────────────────
|
||||||
totalOrders, _ := database.TotalOrders(filters.ResetCommandes)
|
|
||||||
totalRevenue, _ := database.TotalRevenue(filters.ResetRevenus)
|
|
||||||
|
|
||||||
avgPerDay := 0.0
|
avgPerDay := 0.0
|
||||||
if totalOrders > 0 {
|
if totalOrders > 0 && activeDays > 0 {
|
||||||
activeDays, _ := database.ActiveDaysLast30(filters.ResetCommandes)
|
avgPerDay = float64(last30Count) / float64(activeDays)
|
||||||
if activeDays > 0 {
|
|
||||||
last30Count, _ := database.OrdersCountLast30(filters.ResetCommandes)
|
|
||||||
avgPerDay = float64(last30Count) / float64(activeDays)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
|||||||
Reference in New Issue
Block a user