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) => (