346 lines
10 KiB
Go
346 lines
10 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",
|
|
}
|
|
|
|
// 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")
|
|
|
|
// whereCmd : filtre commandes non annulées depuis resetCmd
|
|
whereCmd := func() string {
|
|
base := "status != 'cancelled'"
|
|
if !resetCmd.IsZero() {
|
|
base += " AND created_at >= '" + resetCmd.Format(time.RFC3339) + "'"
|
|
}
|
|
return base
|
|
}
|
|
// whereRev : filtre commandes approuvées depuis resetRev
|
|
whereRev := func() string {
|
|
base := "status = 'approved'"
|
|
if !resetRev.IsZero() {
|
|
base += " AND created_at >= '" + resetRev.Format(time.RFC3339) + "'"
|
|
}
|
|
return base
|
|
}
|
|
// whereProd : filtre commandes non annulées depuis resetProd (pour les jointures sur c.)
|
|
whereProd := func() string {
|
|
base := "status != 'cancelled'"
|
|
if !resetProd.IsZero() {
|
|
base += " AND created_at >= '" + resetProd.Format(time.RFC3339) + "'"
|
|
}
|
|
return base
|
|
}
|
|
|
|
// compatibilité : les anciennes clauses whereBase/whereApproved pointent sur commandes/revenus
|
|
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 ` + whereBase("") + `
|
|
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 ` + whereBase("") + `
|
|
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.` + whereProd() + `
|
|
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),
|
|
"by_weekday": byWeekday,
|
|
"by_day_30": byDay,
|
|
"by_day_revenue": byDayRevenue,
|
|
"by_hour": byHour,
|
|
"top_products": topProducts,
|
|
"by_quantity": byQuantity,
|
|
})
|
|
}
|