This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
StyleSheet,
|
||||
RefreshControl,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
@@ -13,8 +14,8 @@ import { useTheme } from "../../context/ThemeContext";
|
||||
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, DayRevenueStat, HourStat, ProductStat, ProductQuantityBreakdown } from "../../api/api_admin";
|
||||
import { getAdminStats, resetAdminStats } from "../../api/api_admin";
|
||||
import type { AdminStats, WeekdayStat, DayStat, DayRevenueStat, HourStat, ProductStat, ProductQuantityBreakdown, StatSection } from "../../api/api_admin";
|
||||
|
||||
// ── Palette graphiques ────────────────────────────────────────────────────────
|
||||
const CHART_ACCENT = "#6366f1";
|
||||
@@ -28,7 +29,7 @@ const maxOf = (arr: number[]) => arr.length ? Math.max(...arr) : 1;
|
||||
const fmtNum = (n: number) =>
|
||||
n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(Math.round(n));
|
||||
const fmtEuro = (n: number) =>
|
||||
n >= 1000 ? `${(n / 1000).toFixed(1)}k€` : `${Math.round(n)}€`;
|
||||
n >= 1000 ? `${(n / 1000).toFixed(2)}k€` : `${n.toFixed(2)}€`;
|
||||
|
||||
// ── Barre horizontale ─────────────────────────────────────────────────────────
|
||||
function HBar({
|
||||
@@ -163,6 +164,28 @@ const secStyles = StyleSheet.create({
|
||||
title: { fontSize: fontSize.md, fontWeight: "600" },
|
||||
});
|
||||
|
||||
// ── Bouton reset de section ───────────────────────────────────────────────────
|
||||
function SectionResetBtn({ onPress, loading, resetAt }: { onPress: () => void; loading: boolean; resetAt?: string }) {
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<View style={{ alignItems: "flex-end", marginBottom: 6 }}>
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
disabled={loading}
|
||||
style={{ flexDirection: "row", alignItems: "center", gap: 4, paddingHorizontal: 8, paddingVertical: 3, borderRadius: 6, borderWidth: 1, borderColor: CHART_RED + "66", backgroundColor: CHART_RED + "11" }}
|
||||
>
|
||||
<Ionicons name="refresh-outline" size={11} color={CHART_RED} />
|
||||
<Text style={{ fontSize: 10, fontWeight: "600", color: CHART_RED }}>{loading ? "..." : "Réinitialiser"}</Text>
|
||||
</TouchableOpacity>
|
||||
{resetAt ? (
|
||||
<Text style={{ fontSize: 9, color: colors.textMuted, marginTop: 2 }}>
|
||||
Depuis le {new Date(resetAt).toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric" })}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Sélecteur de période top produits ────────────────────────────────────────
|
||||
type ProdSort = "quantity" | "orders" | "revenue";
|
||||
|
||||
@@ -172,6 +195,7 @@ export default function StatsScreen() {
|
||||
const [stats, setStats] = useState<AdminStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [resetting, setResetting] = useState<StatSection | null>(null);
|
||||
const [prodSort, setProdSort] = useState<ProdSort>("quantity");
|
||||
|
||||
const loadStats = useCallback(async () => {
|
||||
@@ -190,6 +214,28 @@ export default function StatsScreen() {
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
const handleReset = (section: StatSection, label: string) => {
|
||||
Alert.alert(
|
||||
`Réinitialiser — ${label}`,
|
||||
"Les données de cette section repartiront de zéro. Les commandes restent en base.\n\nConfirmer ?",
|
||||
[
|
||||
{ text: "Annuler", style: "cancel" },
|
||||
{
|
||||
text: "Réinitialiser",
|
||||
style: "destructive",
|
||||
onPress: async () => {
|
||||
setResetting(section);
|
||||
try {
|
||||
await resetAdminStats(section);
|
||||
await loadStats();
|
||||
} catch { /* ignore */ }
|
||||
setResetting(null);
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
// Produits triés selon le sélecteur
|
||||
const sortedProducts = useMemo<ProductStat[]>(() => {
|
||||
if (!stats?.top_products) return [];
|
||||
@@ -210,6 +256,10 @@ export default function StatsScreen() {
|
||||
sortBtn: { paddingHorizontal: spacing.m, paddingVertical: spacing.xs, borderRadius: borderRadius.sm, borderWidth: 1 },
|
||||
sortBtnText:{ fontSize: fontSize.xs, fontWeight: "600" },
|
||||
emptyText: { color: colors.textMuted, fontSize: fontSize.sm, textAlign: "center", paddingVertical: spacing.l },
|
||||
headerRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginBottom: spacing.xs },
|
||||
resetBtn: { flexDirection: "row", alignItems: "center", gap: 4, paddingHorizontal: spacing.s, paddingVertical: 3, borderRadius: borderRadius.sm, borderWidth: 1, borderColor: CHART_RED + "66", backgroundColor: CHART_RED + "11" },
|
||||
resetBtnText:{ fontSize: 10, fontWeight: "600", color: CHART_RED },
|
||||
resetDate: { fontSize: fontSize.xs, color: colors.textMuted, marginBottom: spacing.l },
|
||||
}), [colors]);
|
||||
|
||||
if (loading) return <LoadingSpinner message="Chargement des statistiques..." />;
|
||||
@@ -261,6 +311,11 @@ export default function StatsScreen() {
|
||||
|
||||
{/* ── Évolution 30 jours (commandes) ── */}
|
||||
<Section title="30 derniers jours — commandes" icon="bar-chart-outline">
|
||||
<SectionResetBtn
|
||||
onPress={() => handleReset("commandes", "Commandes")}
|
||||
loading={resetting === "commandes"}
|
||||
resetAt={stats?.reset_at_commandes}
|
||||
/>
|
||||
{stats?.by_day_30?.length ? (
|
||||
<SparkLine data={stats.by_day_30} color={CHART_ACCENT} />
|
||||
) : (
|
||||
@@ -275,6 +330,11 @@ export default function StatsScreen() {
|
||||
const best = [...revData].sort((a, b) => b.revenue - a.revenue)[0];
|
||||
return (
|
||||
<Section title="Revenus par jour (30j)" icon="trending-up-outline">
|
||||
<SectionResetBtn
|
||||
onPress={() => handleReset("revenus", "Revenus")}
|
||||
loading={resetting === "revenus"}
|
||||
resetAt={stats?.reset_at_revenus}
|
||||
/>
|
||||
{revData.length ? (
|
||||
<>
|
||||
<SparkLineRevenue data={revData} color={CHART_GREEN} />
|
||||
@@ -432,6 +492,11 @@ export default function StatsScreen() {
|
||||
|
||||
{/* ── Top produits ── */}
|
||||
<Section title="Top produits" icon="cube-outline">
|
||||
<SectionResetBtn
|
||||
onPress={() => handleReset("produits", "Produits")}
|
||||
loading={resetting === "produits"}
|
||||
resetAt={stats?.reset_at_produits}
|
||||
/>
|
||||
{/* Sélecteur tri */}
|
||||
<View style={styles.sortRow}>
|
||||
{(["quantity", "orders", "revenue"] as ProdSort[]).map((key) => {
|
||||
|
||||
Reference in New Issue
Block a user