chore: add quantity stat

This commit is contained in:
2026-05-17 14:54:59 +02:00
parent 94fa9de6a7
commit a8696b12fa
4 changed files with 158 additions and 4 deletions
+15
View File
@@ -90,6 +90,20 @@ export interface ProductStat {
category_color: string;
}
export interface QuantityStat {
quantity: number;
order_count: number;
total_sold: number;
revenue: number;
}
export interface ProductQuantityBreakdown {
product_id: number;
name: string;
category_color: string;
total_orders: number;
quantities: QuantityStat[];
}
export interface AdminStats {
summary: StatsSummary;
by_weekday: WeekdayStat[];
@@ -97,6 +111,7 @@ export interface AdminStats {
by_day_revenue: DayRevenueStat[];
by_hour: HourStat[];
top_products: ProductStat[];
by_quantity: ProductQuantityBreakdown[];
}
export const getAdminStats = async (): Promise<AdminStats> => {
@@ -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, DayRevenueStat, HourStat, ProductStat } from "../../api/api_admin";
import type { AdminStats, WeekdayStat, DayStat, DayRevenueStat, HourStat, ProductStat, ProductQuantityBreakdown } from "../../api/api_admin";
// ── Palette graphiques ────────────────────────────────────────────────────────
const CHART_ACCENT = "#6366f1";
@@ -372,6 +372,64 @@ export default function StatsScreen() {
)}
</Section>
{/* ── Doses les plus populaires par produit ── */}
{(() => {
const qtyData: ProductQuantityBreakdown[] = (stats?.by_quantity ?? []).filter(
(p) => p.quantities.length >= 1,
);
if (!qtyData.length) return null;
return (
<Section title="Doses populaires par produit" icon="flask-outline">
{qtyData.map((product) => {
const peakCount = product.quantities[0]?.order_count ?? 1;
return (
<View
key={product.product_id}
style={{
marginBottom: spacing.m,
paddingBottom: spacing.m,
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
}}
>
{/* Nom du produit avec pastille couleur */}
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs, marginBottom: spacing.s }}>
<View style={{ width: 10, height: 10, borderRadius: 5, backgroundColor: product.category_color }} />
<Text style={{ color: colors.textPrimary, fontSize: fontSize.sm, fontWeight: "600" }} numberOfLines={1}>
{product.name}
</Text>
</View>
{/* Barre par dose */}
{product.quantities.map((q, i) => {
const pct = peakCount > 0 ? Math.max((q.order_count / peakCount) * 100, q.order_count > 0 ? 2 : 0) : 0;
const isPeak = i === 0;
const label = Number.isInteger(q.quantity)
? `${q.quantity}g`
: `${q.quantity}g`;
return (
<View key={q.quantity} style={{ flexDirection: "row", alignItems: "center", marginBottom: 4, gap: spacing.s }}>
<Text style={{ width: 44, fontSize: fontSize.xs, color: isPeak ? product.category_color : colors.textMuted, fontWeight: isPeak ? "700" : "400" }}>
{label}
</Text>
<View style={{ flex: 1, height: 14, borderRadius: 3, backgroundColor: colors.borderLight, overflow: "hidden" }}>
<View style={{ width: `${pct}%`, height: "100%", borderRadius: 3, backgroundColor: isPeak ? product.category_color : product.category_color + "66" }} />
</View>
<Text style={{ width: 54, fontSize: fontSize.xs, textAlign: "right", color: isPeak ? product.category_color : colors.textMuted, fontWeight: isPeak ? "700" : "400" }}>
{q.order_count} cmd
</Text>
{isPeak && (
<Ionicons name="flame" size={11} color={CHART_AMBER} />
)}
</View>
);
})}
</View>
);
})}
</Section>
);
})()}
{/* ── Top produits ── */}
<Section title="Top produits" icon="cube-outline">
{/* Sélecteur tri */}