This commit is contained in:
@@ -104,6 +104,28 @@ export interface ProductQuantityBreakdown {
|
|||||||
quantities: QuantityStat[];
|
quantities: QuantityStat[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DailyProductItem {
|
||||||
|
product_id: number;
|
||||||
|
name: string;
|
||||||
|
quantity: number;
|
||||||
|
order_count: number;
|
||||||
|
revenue: number;
|
||||||
|
}
|
||||||
|
export interface DailyCategoryDetail {
|
||||||
|
category: string;
|
||||||
|
category_color: string;
|
||||||
|
total_quantity: number;
|
||||||
|
total_revenue: number;
|
||||||
|
products: DailyProductItem[];
|
||||||
|
}
|
||||||
|
export interface DailyDetail {
|
||||||
|
date: string;
|
||||||
|
total_orders: number;
|
||||||
|
total_quantity: number;
|
||||||
|
total_revenue: number;
|
||||||
|
categories: DailyCategoryDetail[];
|
||||||
|
}
|
||||||
|
|
||||||
export type StatSection = "commandes" | "revenus" | "produits" | "heures" | "jours" | "doses";
|
export type StatSection = "commandes" | "revenus" | "produits" | "heures" | "jours" | "doses";
|
||||||
|
|
||||||
export interface AdminStats {
|
export interface AdminStats {
|
||||||
@@ -114,6 +136,7 @@ export interface AdminStats {
|
|||||||
by_hour: HourStat[];
|
by_hour: HourStat[];
|
||||||
top_products: ProductStat[];
|
top_products: ProductStat[];
|
||||||
by_quantity: ProductQuantityBreakdown[];
|
by_quantity: ProductQuantityBreakdown[];
|
||||||
|
daily_detail?: DailyDetail;
|
||||||
reset_at_commandes?: string;
|
reset_at_commandes?: string;
|
||||||
reset_at_revenus?: string;
|
reset_at_revenus?: string;
|
||||||
reset_at_produits?: string;
|
reset_at_produits?: string;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { spacing, fontSize, borderRadius } from "../../theme";
|
|||||||
import { shadows } from "../../theme/shadows";
|
import { shadows } from "../../theme/shadows";
|
||||||
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
||||||
import { getAdminStats, resetAdminStats } 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";
|
import type { AdminStats, WeekdayStat, DayStat, DayRevenueStat, HourStat, ProductStat, ProductQuantityBreakdown, StatSection, DailyDetail, DailyCategoryDetail } from "../../api/api_admin";
|
||||||
|
|
||||||
// ── Palette graphiques ────────────────────────────────────────────────────────
|
// ── Palette graphiques ────────────────────────────────────────────────────────
|
||||||
const CHART_ACCENT = "#6366f1";
|
const CHART_ACCENT = "#6366f1";
|
||||||
@@ -186,6 +186,123 @@ function SectionResetBtn({ onPress, loading, resetAt }: { onPress: () => void; l
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Section détail du jour ────────────────────────────────────────────────────
|
||||||
|
function DailyDetailSection({ daily }: { daily: DailyDetail }) {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const hasData = daily.categories.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[ddStyles.card, { backgroundColor: colors.bgCard, borderColor: colors.borderLight }]}>
|
||||||
|
{/* En-tête */}
|
||||||
|
<View style={ddStyles.header}>
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.s }}>
|
||||||
|
<Ionicons name="today-outline" size={16} color={CHART_ACCENT} />
|
||||||
|
<Text style={[ddStyles.title, { color: colors.textPrimary }]}>Activité du jour</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={[ddStyles.date, { color: colors.textMuted }]}>{daily.date}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Mini-résumé */}
|
||||||
|
<View style={ddStyles.chipRow}>
|
||||||
|
<View style={[ddStyles.chip, { backgroundColor: CHART_ACCENT + "18", borderColor: CHART_ACCENT + "44" }]}>
|
||||||
|
<Ionicons name="receipt-outline" size={13} color={CHART_ACCENT} />
|
||||||
|
<Text style={[ddStyles.chipVal, { color: colors.textPrimary }]}>{daily.total_orders}</Text>
|
||||||
|
<Text style={[ddStyles.chipLbl, { color: colors.textMuted }]}>commandes</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[ddStyles.chip, { backgroundColor: CHART_BLUE + "18", borderColor: CHART_BLUE + "44" }]}>
|
||||||
|
<Ionicons name="scale-outline" size={13} color={CHART_BLUE} />
|
||||||
|
<Text style={[ddStyles.chipVal, { color: colors.textPrimary }]}>{daily.total_quantity}g</Text>
|
||||||
|
<Text style={[ddStyles.chipLbl, { color: colors.textMuted }]}>vendues</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[ddStyles.chip, { backgroundColor: CHART_GREEN + "18", borderColor: CHART_GREEN + "44" }]}>
|
||||||
|
<Ionicons name="cash-outline" size={13} color={CHART_GREEN} />
|
||||||
|
<Text style={[ddStyles.chipVal, { color: colors.textPrimary }]}>{fmtEuro(daily.total_revenue)}</Text>
|
||||||
|
<Text style={[ddStyles.chipLbl, { color: colors.textMuted }]}>revenus</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{!hasData ? (
|
||||||
|
<Text style={[ddStyles.empty, { color: colors.textMuted }]}>Aucune commande aujourd'hui</Text>
|
||||||
|
) : (
|
||||||
|
daily.categories.map((cat: DailyCategoryDetail, ci: number) => {
|
||||||
|
const catMax = Math.max(...cat.products.map((p) => p.quantity), 1);
|
||||||
|
const isLast = ci === daily.categories.length - 1;
|
||||||
|
return (
|
||||||
|
<View key={cat.category} style={[ddStyles.catBlock, isLast && { marginBottom: 0, borderBottomWidth: 0 }]}>
|
||||||
|
{/* En-tête catégorie */}
|
||||||
|
<View style={ddStyles.catHeader}>
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.s }}>
|
||||||
|
<View style={[ddStyles.catDot, { backgroundColor: cat.category_color }]} />
|
||||||
|
<Text style={[ddStyles.catName, { color: colors.textPrimary }]}>{cat.category}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.m }}>
|
||||||
|
<Text style={[ddStyles.catMeta, { color: colors.textMuted }]}>{cat.total_quantity}g</Text>
|
||||||
|
<Text style={[ddStyles.catRevenue, { color: cat.category_color }]}>{fmtEuro(cat.total_revenue)}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Lignes produits */}
|
||||||
|
{cat.products.map((prod, pi) => {
|
||||||
|
const pct = catMax > 0 ? Math.max((prod.quantity / catMax) * 100, prod.quantity > 0 ? 3 : 0) : 0;
|
||||||
|
const isTopInCat = pi === 0;
|
||||||
|
return (
|
||||||
|
<View key={prod.product_id} style={[ddStyles.prodRow, pi === cat.products.length - 1 && { marginBottom: 0 }]}>
|
||||||
|
<View style={ddStyles.prodTopLine}>
|
||||||
|
<Text
|
||||||
|
style={[ddStyles.prodName, { color: isTopInCat ? colors.textPrimary : colors.textSecondary, fontWeight: isTopInCat ? "600" : "400" }]}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{prod.name}
|
||||||
|
</Text>
|
||||||
|
<View style={ddStyles.prodMeta}>
|
||||||
|
<Text style={[ddStyles.prodQty, { color: colors.textMuted }]}>
|
||||||
|
{prod.quantity}g · {prod.order_count} cmd
|
||||||
|
</Text>
|
||||||
|
<Text style={[ddStyles.prodRevenue, { color: isTopInCat ? cat.category_color : colors.textMuted }]}>
|
||||||
|
{fmtEuro(prod.revenue)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={[ddStyles.bar, { backgroundColor: colors.borderLight }]}>
|
||||||
|
<View style={[ddStyles.barFill, { width: `${pct}%`, backgroundColor: cat.category_color, opacity: isTopInCat ? 1 : 0.45 }]} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ddStyles = StyleSheet.create({
|
||||||
|
card: { borderRadius: borderRadius.md, padding: spacing.l, marginBottom: spacing.m, borderWidth: 1 },
|
||||||
|
header: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginBottom: spacing.m },
|
||||||
|
title: { fontSize: fontSize.md, fontWeight: "600" },
|
||||||
|
date: { fontSize: fontSize.xs, fontWeight: "500" },
|
||||||
|
chipRow: { flexDirection: "row", gap: spacing.s, marginBottom: spacing.l },
|
||||||
|
chip: { flex: 1, alignItems: "center", gap: 3, paddingVertical: spacing.s, borderRadius: borderRadius.sm, borderWidth: 1 },
|
||||||
|
chipVal: { fontSize: fontSize.sm, fontWeight: "700" },
|
||||||
|
chipLbl: { fontSize: 9, fontWeight: "500" },
|
||||||
|
empty: { textAlign: "center", fontSize: fontSize.sm, paddingVertical: spacing.l },
|
||||||
|
catBlock: { marginBottom: spacing.l, paddingBottom: spacing.l, borderBottomWidth: 1 },
|
||||||
|
catHeader: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginBottom: spacing.s, paddingBottom: spacing.xs },
|
||||||
|
catDot: { width: 10, height: 10, borderRadius: 5 },
|
||||||
|
catName: { fontSize: fontSize.sm, fontWeight: "700" },
|
||||||
|
catMeta: { fontSize: fontSize.xs },
|
||||||
|
catRevenue: { fontSize: fontSize.xs, fontWeight: "700" },
|
||||||
|
prodRow: { marginBottom: spacing.s },
|
||||||
|
prodTopLine:{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginBottom: 4 },
|
||||||
|
prodName: { flex: 1, fontSize: fontSize.xs, marginRight: spacing.s },
|
||||||
|
prodMeta: { flexDirection: "row", alignItems: "center", gap: spacing.m },
|
||||||
|
prodQty: { fontSize: fontSize.xs },
|
||||||
|
prodRevenue:{ fontSize: fontSize.xs, fontWeight: "600", minWidth: 52, textAlign: "right" },
|
||||||
|
bar: { height: 5, borderRadius: 3, overflow: "hidden" },
|
||||||
|
barFill: { height: "100%", borderRadius: 3 },
|
||||||
|
});
|
||||||
|
|
||||||
// ── Sélecteur de période top produits ────────────────────────────────────────
|
// ── Sélecteur de période top produits ────────────────────────────────────────
|
||||||
type ProdSort = "quantity" | "orders" | "revenue";
|
type ProdSort = "quantity" | "orders" | "revenue";
|
||||||
|
|
||||||
@@ -325,6 +442,11 @@ export default function StatsScreen() {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* ── Activité du jour ── */}
|
||||||
|
{stats?.daily_detail && (
|
||||||
|
<DailyDetailSection daily={stats.daily_detail} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ── Évolution 30 jours (commandes) ── */}
|
{/* ── Évolution 30 jours (commandes) ── */}
|
||||||
<Section title="30 derniers jours — commandes" icon="bar-chart-outline">
|
<Section title="30 derniers jours — commandes" icon="bar-chart-outline">
|
||||||
<SectionResetBtn
|
<SectionResetBtn
|
||||||
|
|||||||
Reference in New Issue
Block a user