package handlers import ( "context" "fmt" "gestion/db" "gestion/models" "net/http" "time" "github.com/gin-gonic/gin" "golang.org/x/sync/errgroup" ) var weekdayNames = []string{"Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"} // sections valides pour le reset var validStatsSections = map[string]string{ "commandes": "stats_reset_commandes_at", "revenus": "stats_reset_revenus_at", "produits": "stats_reset_produits_at", "heures": "stats_reset_heures_at", "jours": "stats_reset_jours_at", "doses": "stats_reset_doses_at", } // ResetAdminStats réinitialise une section précise des statistiques. func ResetAdminStats(c *gin.Context) { section := c.Param("section") key, ok := validStatsSections[section] if !ok { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("section invalide : %s", section)}) return } database := c.MustGet("database").(*db.Database) now := time.Now().UTC().Format(time.RFC3339) if err := database.ResetAdminStat(key); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Erreur lors de la suppresion de la section statistique: %s", err)}) return } c.JSON(http.StatusOK, gin.H{"success": true, "section": section, "reset_at": now}) } func dateFilter(t time.Time) string { if t.IsZero() { return "" } return t.Format(time.RFC3339) } // GetAdminStatsByMonth renvoie, pour chaque jour du mois demandé (paramètre // de query "month" au format YYYY-MM, mois courant par défaut), le nombre de // commandes, le revenu et la quantité vendue. Les jours sans commande sont // inclus avec des valeurs à zéro. func GetAdminStatsByMonth(c *gin.Context) { database := c.MustGet("database").(*db.Database) monthParam := c.Query("month") monthStart := time.Now() if monthParam != "" { parsed, err := time.Parse("2006-01", monthParam) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("paramètre month invalide (attendu YYYY-MM) : %s", monthParam)}) return } monthStart = parsed } monthStart = time.Date(monthStart.Year(), monthStart.Month(), 1, 0, 0, 0, 0, monthStart.Location()) resetCmd := database.ReadResetAt("stats_reset_commandes_at") var rows []db.DailyMonthStatRow if err := database.StatsByDayForMonth(&rows, monthStart, resetCmd); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Erreur lors de la récupération des statistiques mensuelles: %s", err)}) return } rowByDay := make(map[string]db.DailyMonthStatRow, len(rows)) for _, r := range rows { rowByDay[r.Day.Format("2006-01-02")] = r } daysInMonth := monthStart.AddDate(0, 1, -1).Day() byDay := make([]gin.H, daysInMonth) var totalOrders int var totalRevenue float64 var totalQuantity float64 for i := range daysInMonth { day := monthStart.AddDate(0, 0, i) key := day.Format("2006-01-02") r, ok := rowByDay[key] if !ok { r = db.DailyMonthStatRow{Day: day} } byDay[i] = gin.H{ "day": key, "label": day.Format("02/01"), "count": r.Count, "revenue": r.Revenue, "quantity": r.Quantity, } totalOrders += r.Count totalRevenue += r.Revenue totalQuantity += r.Quantity } c.JSON(http.StatusOK, gin.H{ "month": monthStart.Format("2006-01"), "summary": gin.H{ "total_orders": totalOrders, "total_revenue": totalRevenue, "total_quantity": totalQuantity, }, "by_day": byDay, }) } func GetAdminDailyDetail(c *gin.Context) { database := c.MustGet("database").(*db.Database) dateParam := c.Query("date") date := time.Now() if dateParam != "" { parsed, err := time.Parse("2006-01-02", dateParam) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("paramètre date invalide (attendu YYYY-MM-DD) : %s", dateParam)}) return } date = parsed } var dailyRows []models.DailyProductRow if err := database.DailyProductDetailForDate(&dailyRows, date); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Erreur lors de la récupération du détail du jour: %s", err)}) return } type dailyCatGroup struct { Category string CategoryColor string TotalQuantity float64 TotalRevenue float64 Products []gin.H } var dailyCats []dailyCatGroup dailyCatIdx := map[string]int{} dailyTotalRevenue := 0.0 dailyTotalQty := 0.0 for _, r := range dailyRows { dailyTotalRevenue += r.Revenue dailyTotalQty += r.TotalQuantity idx, ok := dailyCatIdx[r.Category] if !ok { idx = len(dailyCats) dailyCats = append(dailyCats, dailyCatGroup{ Category: r.Category, CategoryColor: r.CategoryColor, }) dailyCatIdx[r.Category] = idx } dailyCats[idx].TotalQuantity += r.TotalQuantity dailyCats[idx].TotalRevenue += r.Revenue dailyCats[idx].Products = append(dailyCats[idx].Products, gin.H{ "product_id": r.ProductID, "name": r.ProductName, "quantity": r.TotalQuantity, "order_count": r.OrderCount, "revenue": r.Revenue, }) } dailyCatsJSON := make([]gin.H, len(dailyCats)) for i, g := range dailyCats { dailyCatsJSON[i] = gin.H{ "category": g.Category, "category_color": g.CategoryColor, "total_quantity": g.TotalQuantity, "total_revenue": g.TotalRevenue, "products": g.Products, } } dailyTotalOrders, _ := database.DailyOrdersCountForDate(date) c.JSON(http.StatusOK, gin.H{ "date": date.Format("02/01/2006"), "total_orders": dailyTotalOrders, "total_quantity": dailyTotalQty, "total_revenue": dailyTotalRevenue, "categories": dailyCatsJSON, }) } // GetAdminStats returns aggregated order & product statistics for the admin dashboard. func GetAdminStats(c *gin.Context) { database := c.MustGet("database").(*db.Database) filters := database.LoadAdminStatsFilters() // 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 { wdMap[r.DOW] = r.Count } peakCount, peakWeekday := 0, "" for i := range 7 { cnt := wdMap[i] byWeekday[i] = gin.H{"weekday": weekdayNames[i], "count": cnt} if cnt > peakCount { peakCount = cnt peakWeekday = weekdayNames[i] } } // ── Commandes par jour sur 30 jours ─────────────────────────────────────── byDay := make([]gin.H, len(dayRows)) for i, r := range dayRows { byDay[i] = gin.H{ "day": r.Day.Format("2006-01-02"), "label": r.Day.Format("02/01"), "count": r.Count, } } // ── Revenus par jour sur 30 jours ───────────────────────────────────────── byDayRevenue := make([]gin.H, len(dayRevRows)) for i, r := range dayRevRows { byDayRevenue[i] = gin.H{ "day": r.Day.Format("2006-01-02"), "label": r.Day.Format("02/01"), "revenue": r.Revenue, } } // ── Commandes & revenus par heure ───────────────────────────────────────── hourMap := make(map[int]models.HourRow, len(hourRows)) for _, r := range hourRows { hourMap[r.Hour] = r } byHour := make([]gin.H, 24) for h := range 24 { r := hourMap[h] byHour[h] = gin.H{ "hour": h, "label": fmt.Sprintf("%02dh", h), "count": r.Count, "revenue": r.Revenue, } } // ── Top produits ────────────────────────────────────────────────────────── topProducts := make([]gin.H, len(prodRows)) topProductName := "" for i, r := range prodRows { topProducts[i] = gin.H{ "product_id": r.ProductID, "name": r.Name, "quantity": r.Quantity, "order_count": r.OrderCount, "revenue": r.Revenue, "category": r.Category, "category_color": r.CategoryColor, } if i == 0 { topProductName = r.Name } } // ── Répartition des doses/quantités ─────────────────────────────────────── type productGroup struct { ProductID int Name string CategoryColor string TotalOrders int Quantities []gin.H } var groups []productGroup groupIdx := map[int]int{} for _, r := range qtyRows { idx, ok := groupIdx[r.ProductID] if !ok { idx = len(groups) groups = append(groups, productGroup{ ProductID: r.ProductID, Name: r.ProductName, CategoryColor: r.CategoryColor, }) groupIdx[r.ProductID] = idx } groups[idx].TotalOrders += r.OrderCount groups[idx].Quantities = append(groups[idx].Quantities, gin.H{ "quantity": r.Quantity, "order_count": r.OrderCount, "total_sold": r.TotalSold, "revenue": r.Revenue, }) } for i := 0; i < len(groups)-1; i++ { for j := i + 1; j < len(groups); j++ { if groups[j].TotalOrders > groups[i].TotalOrders { groups[i], groups[j] = groups[j], groups[i] } } } if len(groups) > 15 { groups = groups[:15] } byQuantity := make([]gin.H, len(groups)) for i, grp := range groups { byQuantity[i] = gin.H{ "product_id": grp.ProductID, "name": grp.Name, "category_color": grp.CategoryColor, "total_orders": grp.TotalOrders, "quantities": grp.Quantities, } } // ── Détail du jour ──────────────────────────────────────────────────────── type dailyCatGroup struct { Category string CategoryColor string TotalQuantity float64 TotalRevenue float64 Products []gin.H } var dailyCats []dailyCatGroup dailyCatIdx := map[string]int{} dailyTotalRevenue := 0.0 dailyTotalQty := 0.0 for _, r := range dailyRows { dailyTotalRevenue += r.Revenue dailyTotalQty += r.TotalQuantity idx, ok := dailyCatIdx[r.Category] if !ok { idx = len(dailyCats) dailyCats = append(dailyCats, dailyCatGroup{ Category: r.Category, CategoryColor: r.CategoryColor, }) dailyCatIdx[r.Category] = idx } dailyCats[idx].TotalQuantity += r.TotalQuantity dailyCats[idx].TotalRevenue += r.Revenue dailyCats[idx].Products = append(dailyCats[idx].Products, gin.H{ "product_id": r.ProductID, "name": r.ProductName, "quantity": r.TotalQuantity, "order_count": r.OrderCount, "revenue": r.Revenue, }) } dailyCatsJSON := make([]gin.H, len(dailyCats)) for i, grp := range dailyCats { dailyCatsJSON[i] = gin.H{ "category": grp.Category, "category_color": grp.CategoryColor, "total_quantity": grp.TotalQuantity, "total_revenue": grp.TotalRevenue, "products": grp.Products, } } // ── Résumé global ───────────────────────────────────────────────────────── avgPerDay := 0.0 if totalOrders > 0 && activeDays > 0 { avgPerDay = float64(last30Count) / float64(activeDays) } c.JSON(http.StatusOK, gin.H{ "summary": gin.H{ "total_orders": totalOrders, "total_revenue": totalRevenue, "peak_weekday": peakWeekday, "top_product": topProductName, "avg_per_day": avgPerDay, }, "reset_at_commandes": dateFilter(filters.ResetCommandes), "reset_at_revenus": dateFilter(filters.ResetRevenus), "reset_at_produits": dateFilter(filters.ResetProduits), "reset_at_heures": dateFilter(filters.ResetHeures), "reset_at_jours": dateFilter(filters.ResetJours), "reset_at_doses": dateFilter(filters.ResetDoses), "by_weekday": byWeekday, "by_day_30": byDay, "by_day_revenue": byDayRevenue, "by_hour": byHour, "top_products": topProducts, "by_quantity": byQuantity, "daily_detail": gin.H{ "date": time.Now().Format("02/01/2006"), "total_orders": dailyTotalOrders, "total_quantity": dailyTotalQty, "total_revenue": dailyTotalRevenue, "categories": dailyCatsJSON, }, }) }