This commit is contained in:
@@ -104,6 +104,28 @@ export interface ProductQuantityBreakdown {
|
||||
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 interface AdminStats {
|
||||
@@ -114,6 +136,7 @@ export interface AdminStats {
|
||||
by_hour: HourStat[];
|
||||
top_products: ProductStat[];
|
||||
by_quantity: ProductQuantityBreakdown[];
|
||||
daily_detail?: DailyDetail;
|
||||
reset_at_commandes?: string;
|
||||
reset_at_revenus?: string;
|
||||
reset_at_produits?: string;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { spacing, fontSize, borderRadius } from "../../theme";
|
||||
import { shadows } from "../../theme/shadows";
|
||||
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
||||
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 ────────────────────────────────────────────────────────
|
||||
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 ────────────────────────────────────────
|
||||
type ProdSort = "quantity" | "orders" | "revenue";
|
||||
|
||||
@@ -325,6 +442,11 @@ export default function StatsScreen() {
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* ── Activité du jour ── */}
|
||||
{stats?.daily_detail && (
|
||||
<DailyDetailSection daily={stats.daily_detail} />
|
||||
)}
|
||||
|
||||
{/* ── Évolution 30 jours (commandes) ── */}
|
||||
<Section title="30 derniers jours — commandes" icon="bar-chart-outline">
|
||||
<SectionResetBtn
|
||||
|
||||
Reference in New Issue
Block a user