update
This commit is contained in:
@@ -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) => (
|
||||
|
||||
Reference in New Issue
Block a user