From d18052e749c172977cf2b94ae8fca30eea3563b5 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Fri, 12 Jun 2026 16:11:41 +0200 Subject: [PATCH] chore: update --- frontend-admin/src/api/api_delivery.ts | 22 ++ .../src/screens/cabine/OrdersScreen.tsx | 2 +- .../src/screens/delivery/DashboardScreen.tsx | 6 +- .../src/screens/delivery/StatsScreen.tsx | 342 ++++++++++++++---- mobile/src/api/api.ts | 1 + .../src/screens/client/OrderHistoryScreen.tsx | 20 +- 6 files changed, 321 insertions(+), 72 deletions(-) diff --git a/frontend-admin/src/api/api_delivery.ts b/frontend-admin/src/api/api_delivery.ts index 047a7ff8..427cd367 100644 --- a/frontend-admin/src/api/api_delivery.ts +++ b/frontend-admin/src/api/api_delivery.ts @@ -372,6 +372,28 @@ export const ISSUE_LABELS: Record = { other: "Autre", }; +export type StatPoint = { label: string; count: number; revenue: number }; + +export const getMyStats = async (): Promise<{ + success: boolean; + by_day?: StatPoint[]; + by_week?: StatPoint[]; + by_month?: StatPoint[]; + error?: string; +}> => { + try { + const { data } = await apiClient.get(`${API}/stats`); + return { + success: true, + by_day: data.by_day || [], + by_week: data.by_week || [], + by_month: data.by_month || [], + }; + } catch (error: any) { + return { success: false, error: error.response?.data?.error || "Erreur réseau" }; + } +}; + export const reportDeliveryIssue = async ( deliveryId: number, issueType: IssueType, diff --git a/frontend-admin/src/screens/cabine/OrdersScreen.tsx b/frontend-admin/src/screens/cabine/OrdersScreen.tsx index 402eebd9..bbe7a40b 100644 --- a/frontend-admin/src/screens/cabine/OrdersScreen.tsx +++ b/frontend-admin/src/screens/cabine/OrdersScreen.tsx @@ -672,7 +672,7 @@ export default function OrdersScreen() { Qté:{" "} - {item.quantite ?? item.quantity} ·{" "} + {item.quantite ?? item.quantity}{item.unit || ""} ·{" "} {(item.prix ?? item.price)?.toFixed( 2, )}{" "} diff --git a/frontend-admin/src/screens/delivery/DashboardScreen.tsx b/frontend-admin/src/screens/delivery/DashboardScreen.tsx index bafe8c43..1b664051 100644 --- a/frontend-admin/src/screens/delivery/DashboardScreen.tsx +++ b/frontend-admin/src/screens/delivery/DashboardScreen.tsx @@ -75,7 +75,7 @@ interface EnrichedDelivery extends DeliveryItem { clientUsername?: string; clientNom?: string; clientPrenom?: string; - items?: Array<{ produit: string; quantite: number; prix: number }>; + items?: Array<{ produit: string; quantite: number; prix: number; unit?: string }>; } export default function DashboardScreen() { @@ -706,7 +706,7 @@ export default function DashboardScreen() { {prod.produit} - Quantité: {prod.quantite} + Quantité: {prod.quantite}{prod.unit || ""} @@ -1974,7 +1974,7 @@ export default function DashboardScreen() { {prod.produit} - Quantité : {prod.quantite} + Quantité : {prod.quantite}{prod.unit || ""} diff --git a/frontend-admin/src/screens/delivery/StatsScreen.tsx b/frontend-admin/src/screens/delivery/StatsScreen.tsx index 05b9111a..c34348cc 100644 --- a/frontend-admin/src/screens/delivery/StatsScreen.tsx +++ b/frontend-admin/src/screens/delivery/StatsScreen.tsx @@ -5,34 +5,128 @@ import { StyleSheet, ScrollView, RefreshControl, + TouchableOpacity, + useWindowDimensions, } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { spacing, fontSize } from "../../theme"; import { useTheme } from "../../context/ThemeContext"; -import { getMyDeliveries, getMyStatus } from "../../api/api_delivery"; -import type { DeliveryItem } from "../../api/types"; +import { getMyDeliveries, getMyStats } from "../../api/api_delivery"; +import type { DeliveryItem, } from "../../api/types"; +import type { StatPoint } from "../../api/api_delivery"; import LoadingSpinner from "../../components/ui/LoadingSpinner"; import Card from "../../components/ui/Card"; +type Period = "day" | "week" | "month"; +type Metric = "count" | "revenue"; + +const BAR_MAX_HEIGHT = 110; +const BAR_WIDTH = 36; +const BAR_GAP = 8; + +function BarChart({ + data, + metric, + colors, +}: { + data: StatPoint[]; + metric: Metric; + colors: any; +}) { + const values = data.map((d) => (metric === "count" ? d.count : d.revenue)); + const maxVal = Math.max(...values, 1); + + if (data.length === 0) { + return ( + + + + Aucune donnée sur cette période + + + ); + } + + return ( + + + {data.map((point, i) => { + const val = metric === "count" ? point.count : point.revenue; + const barH = Math.max(4, (val / maxVal) * BAR_MAX_HEIGHT); + const isLast = i === data.length - 1; + return ( + + + {metric === "count" + ? val > 0 ? String(val) : "" + : val > 0 ? (val >= 1000 ? `${(val / 1000).toFixed(1)}k` : `${Math.round(val)}`) : ""} + + 0 ? colors.accent : colors.border, + borderRadius: 5, + opacity: val > 0 ? 1 : 0.3, + }} + /> + + {point.label} + + + ); + })} + + + ); +} + export default function StatsScreen() { const { colors } = useTheme(); const [deliveries, setDeliveries] = useState([]); + const [byDay, setByDay] = useState([]); + const [byWeek, setByWeek] = useState([]); + const [byMonth, setByMonth] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); + const [period, setPeriod] = useState("week"); + const [metric, setMetric] = useState("count"); const loadData = useCallback(async () => { try { - const res = await getMyDeliveries(); - if (res.success && res.deliveries) setDeliveries(res.deliveries); + const [delivRes, statsRes] = await Promise.all([ + getMyDeliveries(), + getMyStats(), + ]); + if (delivRes.success && delivRes.deliveries) setDeliveries(delivRes.deliveries); + if (statsRes.success) { + setByDay(statsRes.by_day ?? []); + setByWeek(statsRes.by_week ?? []); + setByMonth(statsRes.by_month ?? []); + } } catch { /* ignore */ } setLoading(false); }, []); - useEffect(() => { - loadData(); - }, [loadData]); + useEffect(() => { loadData(); }, [loadData]); + const onRefresh = async () => { setRefreshing(true); await loadData(); @@ -40,39 +134,27 @@ export default function StatsScreen() { }; const total = deliveries.length; - const completed = deliveries.filter((d) => d.status === "livre").length; + const completed = deliveries.filter((d) => d.status === "livre" || d.status === "approved").length; const inProgress = deliveries.filter((d) => d.status === "en_route").length; const pending = deliveries.filter((d) => d.status === "assigned").length; const totalRevenue = deliveries - .filter((d) => d.status === "livre") + .filter((d) => d.status === "livre" || d.status === "approved") .reduce((s, d) => s + d.total_prix, 0); - const stats = [ - { - label: "Total livraisons", - value: total.toString(), - icon: "cube-outline" as const, - color: colors.accent, - }, - { - label: "Complétées", - value: completed.toString(), - icon: "checkmark-circle-outline" as const, - color: colors.success, - }, - { - label: "En cours", - value: inProgress.toString(), - icon: "time-outline" as const, - color: colors.warning, - }, - { - label: "En attente", - value: pending.toString(), - icon: "hourglass-outline" as const, - color: colors.info, - }, - ]; + const chartData = period === "day" ? byDay : period === "week" ? byWeek : byMonth; + + const periodTotal = useMemo(() => { + return chartData.reduce( + (acc, p) => ({ count: acc.count + p.count, revenue: acc.revenue + p.revenue }), + { count: 0, revenue: 0 }, + ); + }, [chartData]); + + const periodLabels: Record = { + day: "30 derniers jours", + week: "12 dernières semaines", + month: "12 derniers mois", + }; const styles = useMemo( () => @@ -84,57 +166,195 @@ export default function StatsScreen() { fontWeight: "700", marginBottom: spacing.l, }, - grid: { - flexDirection: "row", - flexWrap: "wrap", - gap: spacing.m, - }, - statCard: { - width: "47%", - alignItems: "center", - paddingVertical: spacing.l, - }, - statValue: { - fontSize: fontSize.xxl, - fontWeight: "700", - marginTop: spacing.s, - }, + grid: { flexDirection: "row", flexWrap: "wrap", gap: spacing.m }, + statCard: { width: "47%", alignItems: "center", paddingVertical: spacing.l }, + statValue: { fontSize: fontSize.xxl, fontWeight: "700", marginTop: spacing.s }, statLabel: { color: colors.textMuted, fontSize: fontSize.xs, marginTop: spacing.xs, textAlign: "center", }, + revenueCard: { + alignItems: "center", + paddingVertical: spacing.l, + marginTop: spacing.m, + }, + revenueValue: { + fontSize: 28, + fontWeight: "800", + color: colors.success, + marginTop: spacing.s, + }, + revenueLabel: { + color: colors.textMuted, + fontSize: fontSize.sm, + marginTop: spacing.xs, + }, + sectionTitle: { + color: colors.textWhite, + fontSize: fontSize.lg, + fontWeight: "700", + marginBottom: spacing.m, + marginTop: spacing.xl, + }, + periodRow: { + flexDirection: "row", + gap: spacing.s, + marginBottom: spacing.m, + }, + periodBtn: { + flex: 1, + paddingVertical: spacing.s, + borderRadius: 8, + alignItems: "center", + backgroundColor: colors.bgCard, + borderWidth: 1, + borderColor: colors.border, + }, + periodBtnActive: { + backgroundColor: colors.accent + "22", + borderColor: colors.accent, + }, + periodBtnText: { + fontSize: fontSize.sm, + fontWeight: "600", + color: colors.textMuted, + }, + periodBtnTextActive: { color: colors.accent }, + metricRow: { + flexDirection: "row", + gap: spacing.s, + marginBottom: spacing.s, + }, + metricBtn: { + flex: 1, + paddingVertical: spacing.xs, + borderRadius: 6, + alignItems: "center", + backgroundColor: colors.bgCard, + borderWidth: 1, + borderColor: colors.border, + }, + metricBtnActive: { + backgroundColor: colors.success + "22", + borderColor: colors.success, + }, + metricBtnText: { fontSize: fontSize.xs, fontWeight: "600", color: colors.textMuted }, + metricBtnTextActive: { color: colors.success }, + chartCard: { paddingBottom: spacing.s }, + summaryRow: { + flexDirection: "row", + justifyContent: "space-between", + marginTop: spacing.s, + paddingTop: spacing.s, + borderTopWidth: 1, + borderTopColor: colors.border, + }, + summaryItem: { alignItems: "center", flex: 1 }, + summaryValue: { color: colors.textWhite, fontSize: fontSize.lg, fontWeight: "700" }, + summaryLabel: { color: colors.textMuted, fontSize: fontSize.xs, marginTop: 2 }, + periodHint: { color: colors.textMuted, fontSize: fontSize.xs, marginBottom: spacing.s }, }), [colors], ); + const summaryCards = [ + { label: "Total livraisons", value: total.toString(), icon: "cube-outline" as const, color: colors.accent }, + { label: "Complétées", value: completed.toString(), icon: "checkmark-circle-outline" as const, color: colors.success }, + { label: "En cours", value: inProgress.toString(), icon: "time-outline" as const, color: colors.warning }, + { label: "En attente", value: pending.toString(), icon: "hourglass-outline" as const, color: colors.info }, + ]; + if (loading) return ; return ( - } + refreshControl={} > Mes performances + + {/* Cartes résumé */} - {stats.map((s, i) => ( + {summaryCards.map((s, i) => ( - - {s.value} - + {s.value} {s.label} ))} + + {/* Revenu total all-time */} + + + + {totalRevenue.toLocaleString("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} € + + Revenu total généré + + + {/* Section graphiques */} + Évolution + + {/* Sélecteur période */} + + {(["day", "week", "month"] as Period[]).map((p) => ( + setPeriod(p)} + > + + {p === "day" ? "Jour" : p === "week" ? "Semaine" : "Mois"} + + + ))} + + + {/* Sélecteur métrique */} + + setMetric("count")} + > + + Commandes + + + setMetric("revenue")} + > + + Revenus (€) + + + + + + {periodLabels[period]} + + + + {/* Totaux de la période */} + {chartData.length > 0 && ( + + + {periodTotal.count} + livraisons + + + + {periodTotal.revenue.toLocaleString("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} € + + revenus + + + )} + ); } diff --git a/mobile/src/api/api.ts b/mobile/src/api/api.ts index 17f36d0a..25f09140 100644 --- a/mobile/src/api/api.ts +++ b/mobile/src/api/api.ts @@ -922,6 +922,7 @@ export type RewardCategoryConfig = { category: string; all_products: boolean; product_ids: number[]; + product_names: string[]; amount: number; }; diff --git a/mobile/src/screens/client/OrderHistoryScreen.tsx b/mobile/src/screens/client/OrderHistoryScreen.tsx index 9a3841e9..c08a8d65 100644 --- a/mobile/src/screens/client/OrderHistoryScreen.tsx +++ b/mobile/src/screens/client/OrderHistoryScreen.tsx @@ -493,13 +493,19 @@ export default function OrderHistoryScreen() { {pool.eligible_configs.length > 0 && ( - {pool.eligible_configs.map((cfg) => ( - - - {cfg.category}{cfg.amount > 0 ? ` — ${cfg.amount}€` : ""} - - - ))} + {pool.eligible_configs.flatMap((cfg) => + cfg.all_products + ? [ + + {cfg.category}{cfg.amount > 0 ? ` — ${cfg.amount}€` : ""} + + ] + : (cfg.product_names ?? []).map((name) => ( + + {name} + + )) + )} )}