This commit is contained in:
2026-05-15 21:44:46 +02:00
parent cefc7e9f32
commit afbe310dd7
4 changed files with 179 additions and 8 deletions
@@ -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 }) {
/>
))}
</View>
{/* Labels début / fin */}
<View style={{ flexDirection: "row", justifyContent: "space-between", marginTop: 4 }}>
<Text style={{ color: colors.textMuted, fontSize: 9 }}>{data[0]?.label}</Text>
<Text style={{ color: colors.textMuted, fontSize: 9 }}>{data[data.length - 1]?.label}</Text>
</View>
</View>
);
}
// ── 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 (
<View>
<View style={{ flexDirection: "row", alignItems: "flex-end", height: BAR_H, gap: 2 }}>
{data.map((d, i) => (
<View
key={i}
style={{
flex: 1,
height: max > 0 ? Math.max((d.revenue / max) * BAR_H, d.revenue > 0 ? 3 : 0) : 0,
backgroundColor: color,
borderRadius: 2,
opacity: 0.85,
}}
/>
))}
</View>
<View style={{ flexDirection: "row", justifyContent: "space-between", marginTop: 4 }}>
<Text style={{ color: colors.textMuted, fontSize: 9 }}>{data[0]?.label}</Text>
<Text style={{ color: colors.textMuted, fontSize: 9 }}>{data[data.length - 1]?.label}</Text>
@@ -230,8 +259,8 @@ export default function StatsScreen() {
/>
</View>
{/* ── Évolution 30 jours ── */}
<Section title="30 derniers jours" icon="bar-chart-outline">
{/* ── Évolution 30 jours (commandes) ── */}
<Section title="30 derniers jours — commandes" icon="bar-chart-outline">
{stats?.by_day_30?.length ? (
<SparkLine data={stats.by_day_30} color={CHART_ACCENT} />
) : (
@@ -239,6 +268,73 @@ export default function StatsScreen() {
)}
</Section>
{/* ── Revenus par jour (30 jours) ── */}
<Section title="Revenus par jour (30j)" icon="trending-up-outline">
{stats?.by_day_revenue?.length ? (
<>
<SparkLineRevenue data={stats.by_day_revenue} color={CHART_GREEN} />
{(() => {
const best = [...(stats.by_day_revenue ?? [])].sort((a, b) => b.revenue - a.revenue)[0];
if (!best) return null;
return (
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs, marginTop: spacing.s }}>
<Ionicons name="star-outline" size={13} color={CHART_GREEN} />
<Text style={{ color: colors.textMuted, fontSize: fontSize.xs }}>
Meilleure journée :{" "}
<Text style={{ color: CHART_GREEN, fontWeight: "600" }}>
{best.label} · {fmtEuro(best.revenue)}
</Text>
</Text>
</View>
);
})()}
</>
) : (
<Text style={styles.emptyText}>Aucune donnée de revenu</Text>
)}
</Section>
{/* ── Heures d'affluence ── */}
{(() => {
const hourData: HourStat[] = stats?.by_hour ?? [];
const peakHour = hourData.reduce<HourStat | null>(
(best, h) => (!best || h.count > best.count ? h : best),
null,
);
const hourMax = maxOf(hourData.map((h) => h.count));
return (
<Section title="Heures d'affluence" icon="time-outline">
{hourData.some((h) => h.count > 0) ? (
<>
{hourData.map((h) => (
<HBar
key={h.hour}
label={h.label}
value={h.count}
max={hourMax}
color={h.count === hourMax && hourMax > 0 ? CHART_AMBER : CHART_BLUE}
right={h.count > 0 ? String(h.count) : "—"}
/>
))}
{peakHour && peakHour.count > 0 && (
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs, marginTop: spacing.s }}>
<Ionicons name="flame-outline" size={13} color={CHART_AMBER} />
<Text style={{ color: colors.textMuted, fontSize: fontSize.xs }}>
Heure de pointe :{" "}
<Text style={{ color: CHART_AMBER, fontWeight: "600" }}>
{peakHour.label} · {peakHour.count} cmd · {fmtEuro(peakHour.revenue)}
</Text>
</Text>
</View>
)}
</>
) : (
<Text style={styles.emptyText}>Aucune donnée horaire</Text>
)}
</Section>
);
})()}
{/* ── Commandes par jour de la semaine ── */}
<Section title="Jours d'affluence" icon="calendar-outline">
{(stats?.by_weekday ?? []).map((w: WeekdayStat) => (