Files
projet_gestion_commande/backend/gestion/handlers/stats.go
T
Xor290 b670bc3108
Backend - Build & Lint / build (push) Has been cancelled
Frontend Client - EAS Build / build (push) Failing after 56m32s
Frontend Web - Build & Lint / build (push) Failing after 14m33s
chore: build
2026-06-23 22:02:04 +02:00

422 lines
13 KiB
Go

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,
}
}
// ── Détail du jour (catégorie → produits) ────────────────────────────────
var dailyRows []models.DailyProductRow
gdb.Raw(`
SELECT
ci.product_id,
ci.produit AS product_name,
COALESCE(p.category, 'Sans catégorie') AS category,
COALESCE(cat.color, '#7c3aed') AS category_color,
SUM(ci.quantite) AS total_quantity,
COUNT(DISTINCT ci.command_id) AS order_count,
SUM(ci.prix) AS revenue
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 DATE(c.created_at) = CURRENT_DATE
AND c.status != 'cancelled'
GROUP BY ci.product_id, ci.produit, p.category, cat.color
ORDER BY p.category, SUM(ci.quantite) DESC
`).Scan(&dailyRows)
type dailyCatGroup struct {
Category string
CategoryColor string
TotalQuantity float64
TotalRevenue float64
Products []gin.H
}
var dailyCats []dailyCatGroup
dailyCatIdx := map[string]int{}
var dailyTotalOrders int64
dailyTotalRevenue := 0.0
dailyTotalQty := 0.0
gdb.Raw(`
SELECT COUNT(DISTINCT id) FROM commandes
WHERE DATE(created_at) = CURRENT_DATE AND status != 'cancelled'
`).Scan(&dailyTotalOrders)
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,
}
}
// ── 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,
"daily_detail": gin.H{
"date": time.Now().Format("02/01/2006"),
"total_orders": dailyTotalOrders,
"total_quantity": dailyTotalQty,
"total_revenue": dailyTotalRevenue,
"categories": dailyCatsJSON,
},
})
}