package handlers import ( "fmt" "gestion/db" "gestion/models" "net/http" "time" "github.com/gin-gonic/gin" ) 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) upsert := `INSERT INTO app_settings (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value` if err := database.GDB.Exec(upsert, key, now).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "Erreur reset statistiques"}) return } c.JSON(http.StatusOK, gin.H{"success": true, "section": section, "reset_at": now}) } // readResetAt lit une date de reset depuis app_settings (zero value si absente). func readResetAt(database *db.Database, key string) time.Time { var row struct{ Value string } database.GDB.Table("app_settings").Select("value").Where("key = ?", key).Scan(&row) if row.Value != "" { if t, err := time.Parse(time.RFC3339, row.Value); err == nil { return t } } return time.Time{} } func dateFilter(t time.Time) string { if t.IsZero() { return "" } return t.Format(time.RFC3339) } // GetAdminStats returns aggregated order & product statistics for the admin dashboard. func GetAdminStats(c *gin.Context) { database := c.MustGet("database").(*db.Database) gdb := database.GDB resetCmd := readResetAt(database, "stats_reset_commandes_at") resetRev := readResetAt(database, "stats_reset_revenus_at") resetProd := readResetAt(database, "stats_reset_produits_at") resetHeure := readResetAt(database, "stats_reset_heures_at") resetJour := readResetAt(database, "stats_reset_jours_at") resetDoses := readResetAt(database, "stats_reset_doses_at") filterClause := func(base string, t time.Time) string { if !t.IsZero() { return base + " AND created_at >= '" + t.Format(time.RFC3339) + "'" } return base } whereCmd := func() string { return filterClause("status != 'cancelled'", resetCmd) } whereRev := func() string { return filterClause("status = 'approved'", resetRev) } whereProd := func() string { return filterClause("status != 'cancelled'", resetProd) } whereHeure := func() string { return filterClause("status != 'cancelled'", resetHeure) } whereJour := func() string { return filterClause("status != 'cancelled'", resetJour) } whereDoses := func() string { return filterClause("status != 'cancelled'", resetDoses) } whereBase := func(extra string) string { base := whereCmd() if extra != "" { return base + " AND " + extra } return base } whereApproved := whereRev // ── Commandes par jour de la semaine (non annulées) ──────────────────────── var wdRows []models.WeekdayRow gdb.Raw(` SELECT EXTRACT(DOW FROM created_at)::int AS dow, COUNT(*) AS count FROM commandes WHERE ` + whereJour() + ` GROUP BY dow ORDER BY dow `).Scan(&wdRows) 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 := 0; i < 7; i++ { 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 ─────────────────────────────────────── var dayRows []models.DayRow gdb.Raw(` SELECT DATE(created_at) AS day, COUNT(*) AS count FROM commandes WHERE created_at >= NOW() - INTERVAL '30 days' AND ` + whereBase("") + ` GROUP BY DATE(created_at) ORDER BY day `).Scan(&dayRows) 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 (commandes approuvées) ───────────────── var dayRevRows []models.DayRevenueRow gdb.Raw(` SELECT DATE(created_at) AS day, COALESCE(SUM(total_prix - COALESCE(referral_used, 0)), 0) AS revenue FROM commandes WHERE created_at >= NOW() - INTERVAL '30 days' AND ` + whereApproved() + ` GROUP BY DATE(created_at) ORDER BY day `).Scan(&dayRevRows) 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 (non annulées) ───────────────────────── var hourRows []models.HourRow gdb.Raw(` SELECT EXTRACT(HOUR FROM created_at)::int AS hour, COUNT(*) AS count, COALESCE(SUM(total_prix - COALESCE(referral_used, 0)), 0) AS revenue FROM commandes WHERE ` + whereHeure() + ` GROUP BY hour ORDER BY hour `).Scan(&hourRows) hourMap := make(map[int]models.HourRow, len(hourRows)) for _, r := range hourRows { hourMap[r.Hour] = r } byHour := make([]gin.H, 24) for h := 0; h < 24; h++ { r := hourMap[h] byHour[h] = gin.H{ "hour": h, "label": fmt.Sprintf("%02dh", h), "count": r.Count, "revenue": r.Revenue, } } // ── Top produits (quantité vendue, commandes terminées) ─────────────────── var prodRows []models.ProductRow gdb.Raw(` SELECT ci.product_id, ci.produit AS name, SUM(ci.quantite) AS total_quantity, COUNT(DISTINCT ci.command_id) AS order_count, SUM(ci.prix) AS revenue, COALESCE(p.category, '') AS category, COALESCE(cat.color, '#7c3aed') AS category_color FROM command_items ci JOIN commandes c ON c.id = ci.command_id LEFT JOIN products p ON p.id = ci.product_id LEFT JOIN categories cat ON cat.name = p.category WHERE c.` + whereProd() + ` GROUP BY ci.product_id, ci.produit, p.category, cat.color ORDER BY total_quantity DESC LIMIT 15 `).Scan(&prodRows) 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 par produit ─────────────────────────── var qtyRows []models.QuantityBreakdownRow gdb.Raw(` SELECT ci.product_id, ci.produit AS product_name, ci.quantite AS quantity, COUNT(DISTINCT ci.command_id) AS order_count, SUM(ci.quantite) AS total_sold, SUM(ci.prix) AS revenue, COALESCE(cat.color, '#7c3aed') AS category_color FROM command_items ci JOIN commandes c ON c.id = ci.command_id LEFT JOIN products p ON p.id = ci.product_id LEFT JOIN categories cat ON cat.name = p.category WHERE c.` + whereDoses() + ` GROUP BY ci.product_id, ci.produit, ci.quantite, cat.color ORDER BY ci.product_id, COUNT(DISTINCT ci.command_id) DESC `).Scan(&qtyRows) 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, }) } // 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 { groups[i], groups[j] = groups[j], groups[i] } } } if len(groups) > 15 { groups = groups[:15] } byQuantity := make([]gin.H, len(groups)) for i, g := range groups { byQuantity[i] = gin.H{ "product_id": g.ProductID, "name": g.Name, "category_color": g.CategoryColor, "total_orders": g.TotalOrders, "quantities": g.Quantities, } } // ── Résumé global ───────────────────────────────────────────────────────── var totalOrders int64 var totalRevenue float64 gdb.Raw(`SELECT COUNT(*) FROM commandes WHERE ` + whereBase("")).Scan(&totalOrders) gdb.Raw(`SELECT COALESCE(SUM(total_prix - COALESCE(referral_used, 0)), 0) FROM commandes WHERE ` + whereApproved()).Scan(&totalRevenue) avgPerDay := 0.0 if totalOrders > 0 { var activeDays int64 gdb.Raw(` SELECT COUNT(DISTINCT DATE(created_at)) FROM commandes WHERE created_at >= NOW() - INTERVAL '30 days' AND ` + whereBase("")).Scan(&activeDays) if activeDays > 0 { var last30Count int64 gdb.Raw(` SELECT COUNT(*) FROM commandes WHERE created_at >= NOW() - INTERVAL '30 days' AND ` + whereBase("")).Scan(&last30Count) 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(resetCmd), "reset_at_revenus": dateFilter(resetRev), "reset_at_produits": dateFilter(resetProd), "reset_at_heures": dateFilter(resetHeure), "reset_at_jours": dateFilter(resetJour), "reset_at_doses": dateFilter(resetDoses), "by_weekday": byWeekday, "by_day_30": byDay, "by_day_revenue": byDayRevenue, "by_hour": byHour, "top_products": topProducts, "by_quantity": byQuantity, }) }