diff --git a/backend/gestion/handlers/stats.go b/backend/gestion/handlers/stats.go index 585b40b0..ccdd1966 100644 --- a/backend/gestion/handlers/stats.go +++ b/backend/gestion/handlers/stats.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "gestion/db" "gestion/models" "net/http" @@ -60,6 +61,54 @@ func GetAdminStats(c *gin.Context) { } } + // ── 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), 0) AS revenue + FROM commandes + WHERE created_at >= NOW() - INTERVAL '30 days' + AND status = 'approved' + 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 (all time, 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), 0) AS revenue + FROM commandes + WHERE status != 'cancelled' + 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(` @@ -125,8 +174,10 @@ func GetAdminStats(c *gin.Context) { "top_product": topProductName, "avg_per_day": avgPerDay, }, - "by_weekday": byWeekday, - "by_day_30": byDay, - "top_products": topProducts, + "by_weekday": byWeekday, + "by_day_30": byDay, + "by_day_revenue": byDayRevenue, + "by_hour": byHour, + "top_products": topProducts, }) } diff --git a/backend/gestion/models/stats.go b/backend/gestion/models/stats.go index 57da85d6..2a1a260a 100644 --- a/backend/gestion/models/stats.go +++ b/backend/gestion/models/stats.go @@ -19,3 +19,14 @@ type ProductRow struct { OrderCount int `gorm:"column:order_count"` Revenue float64 `gorm:"column:revenue"` } + +type HourRow struct { + Hour int `gorm:"column:hour"` + Count int `gorm:"column:count"` + Revenue float64 `gorm:"column:revenue"` +} + +type DayRevenueRow struct { + Day time.Time `gorm:"column:day"` + Revenue float64 `gorm:"column:revenue"` +} diff --git a/frontend-admin/src/api/api_admin.ts b/frontend-admin/src/api/api_admin.ts index 697e3b48..faa8f4fb 100644 --- a/frontend-admin/src/api/api_admin.ts +++ b/frontend-admin/src/api/api_admin.ts @@ -69,6 +69,17 @@ export interface DayStat { label: string; count: number; } +export interface DayRevenueStat { + day: string; + label: string; + revenue: number; +} +export interface HourStat { + hour: number; + label: string; + count: number; + revenue: number; +} export interface ProductStat { product_id: number; name: string; @@ -81,6 +92,8 @@ export interface AdminStats { summary: StatsSummary; by_weekday: WeekdayStat[]; by_day_30: DayStat[]; + by_day_revenue: DayRevenueStat[]; + by_hour: HourStat[]; top_products: ProductStat[]; } diff --git a/frontend-admin/src/screens/admin/StatsScreen.tsx b/frontend-admin/src/screens/admin/StatsScreen.tsx index 119a5426..6ba2a505 100644 --- a/frontend-admin/src/screens/admin/StatsScreen.tsx +++ b/frontend-admin/src/screens/admin/StatsScreen.tsx @@ -14,7 +14,7 @@ import { spacing, fontSize, borderRadius } from "../../theme"; import { shadows } from "../../theme/shadows"; import LoadingSpinner from "../../components/ui/LoadingSpinner"; import { getAdminStats } from "../../api/api_admin"; -import type { AdminStats, WeekdayStat, DayStat, ProductStat } from "../../api/api_admin"; +import type { AdminStats, WeekdayStat, DayStat, DayRevenueStat, HourStat, ProductStat } from "../../api/api_admin"; // ── Palette graphiques ──────────────────────────────────────────────────────── const CHART_ACCENT = "#6366f1"; @@ -60,7 +60,7 @@ const hBarStyles = StyleSheet.create({ value: { width: 46, fontSize: fontSize.xs, textAlign: "right" }, }); -// ── Barres verticales (sparkline 30 jours) ──────────────────────────────────── +// ── Barres verticales (sparkline 30 jours — commandes) ─────────────────────── function SparkLine({ data, color }: { data: DayStat[]; color: string }) { const { colors } = useTheme(); if (!data.length) return null; @@ -82,7 +82,36 @@ function SparkLine({ data, color }: { data: DayStat[]; color: string }) { /> ))} - {/* Labels début / fin */} + + {data[0]?.label} + {data[data.length - 1]?.label} + + + ); +} + +// ── Sparkline revenus 30 jours ──────────────────────────────────────────────── +function SparkLineRevenue({ data, color }: { data: DayRevenueStat[]; color: string }) { + const { colors } = useTheme(); + if (!data.length) return null; + const max = Math.max(...data.map((d) => d.revenue), 1); + const BAR_H = 56; + return ( + + + {data.map((d, i) => ( + 0 ? Math.max((d.revenue / max) * BAR_H, d.revenue > 0 ? 3 : 0) : 0, + backgroundColor: color, + borderRadius: 2, + opacity: 0.85, + }} + /> + ))} + {data[0]?.label} {data[data.length - 1]?.label} @@ -230,8 +259,8 @@ export default function StatsScreen() { /> - {/* ── Évolution 30 jours ── */} -
+ {/* ── Évolution 30 jours (commandes) ── */} +
{stats?.by_day_30?.length ? ( ) : ( @@ -239,6 +268,73 @@ export default function StatsScreen() { )}
+ {/* ── Revenus par jour (30 jours) ── */} +
+ {stats?.by_day_revenue?.length ? ( + <> + + {(() => { + const best = [...(stats.by_day_revenue ?? [])].sort((a, b) => b.revenue - a.revenue)[0]; + if (!best) return null; + return ( + + + + Meilleure journée :{" "} + + {best.label} · {fmtEuro(best.revenue)} + + + + ); + })()} + + ) : ( + Aucune donnée de revenu + )} +
+ + {/* ── Heures d'affluence ── */} + {(() => { + const hourData: HourStat[] = stats?.by_hour ?? []; + const peakHour = hourData.reduce( + (best, h) => (!best || h.count > best.count ? h : best), + null, + ); + const hourMax = maxOf(hourData.map((h) => h.count)); + return ( +
+ {hourData.some((h) => h.count > 0) ? ( + <> + {hourData.map((h) => ( + 0 ? CHART_AMBER : CHART_BLUE} + right={h.count > 0 ? String(h.count) : "—"} + /> + ))} + {peakHour && peakHour.count > 0 && ( + + + + Heure de pointe :{" "} + + {peakHour.label} · {peakHour.count} cmd · {fmtEuro(peakHour.revenue)} + + + + )} + + ) : ( + Aucune donnée horaire + )} +
+ ); + })()} + {/* ── Commandes par jour de la semaine ── */}
{(stats?.by_weekday ?? []).map((w: WeekdayStat) => (