460 lines
14 KiB
Go
460 lines
14 KiB
Go
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,
|
|
},
|
|
})
|
|
}
|