From 59d4f89a14a1d2ffc21320cfbcbfd9f785c3f96c Mon Sep 17 00:00:00 2001 From: Xor290 Date: Sat, 13 Jun 2026 15:22:51 +0200 Subject: [PATCH] chore: build --- frontend-admin/src/api/api_admin.ts | 7 +- .../src/screens/admin/OrderDetailScreen.tsx | 8 +- .../src/screens/admin/OrdersScreen.tsx | 83 ++++++++++++++++--- .../src/screens/admin/SettingsScreen.tsx | 72 ++++++++++++++++ .../src/screens/delivery/DashboardScreen.tsx | 30 ++++--- .../src/screens/delivery/StatsScreen.tsx | 2 +- mobile/src/api/api.ts | 16 ++-- .../src/screens/client/OrderHistoryScreen.tsx | 5 +- .../screens/client/OrderTrackingScreen.tsx | 16 +++- 9 files changed, 198 insertions(+), 41 deletions(-) diff --git a/frontend-admin/src/api/api_admin.ts b/frontend-admin/src/api/api_admin.ts index 1d55ee3f..e3ddca5c 100644 --- a/frontend-admin/src/api/api_admin.ts +++ b/frontend-admin/src/api/api_admin.ts @@ -235,8 +235,11 @@ export const updateCommandAddress = async ( export const validateCommand = async (commandId: number) => { const { data } = await apiClient.post( `${V2}/admin/protected/orders/${commandId}/force-validate`, + { command_id: commandId }, ); - return { success: true, message: data.message }; + const validated = (data.validated ?? []) as { command_id: number; points_awarded: number }[]; + const points = validated.find((v) => v.command_id === commandId)?.points_awarded ?? 0; + return { success: true, points_awarded: points, validated_count: data.validated_count ?? 0 }; }; export const proposeAddressChangeAdmin = async ( @@ -937,6 +940,8 @@ export interface PointsReward { type: "free_product" | "half_price_product" | "custom"; description: string; category_configs: RewardCategoryConfig[]; + reward_product_id?: number; + reward_quantity?: number; } export interface PointsPool { diff --git a/frontend-admin/src/screens/admin/OrderDetailScreen.tsx b/frontend-admin/src/screens/admin/OrderDetailScreen.tsx index 559173eb..5a01986b 100644 --- a/frontend-admin/src/screens/admin/OrderDetailScreen.tsx +++ b/frontend-admin/src/screens/admin/OrderDetailScreen.tsx @@ -72,7 +72,7 @@ export default function OrderDetailScreen() { getCommandItems(orderId), ]); setCommand(cmdRes.command); - setItems(itemsRes.items); + setItems(itemsRes.items ?? []); // If livreur assigned, fetch their location and calc route const cmd = cmdRes.command; @@ -205,7 +205,7 @@ export default function OrderDetailScreen() { try { await deleteCommandItem(orderId, itemId); const itemsRes = await getCommandItems(orderId); - setItems(itemsRes.items); + setItems(itemsRes.items ?? []); const cmdRes = await getCommandByID(orderId); setCommand(cmdRes.command); } catch (e: any) { @@ -552,7 +552,7 @@ export default function OrderDetailScreen() { ); // Groupement des items par product_id - const productGroups = items.reduce>((acc, item) => { + const productGroups = (items ?? []).reduce>((acc, item) => { const key = String(item.product_id || item.produit); if (!acc[key]) acc[key] = []; acc[key].push(item); @@ -561,7 +561,7 @@ export default function OrderDetailScreen() { const groupedList = Object.values(productGroups); // Récapitulatif par catégorie - const categoryTotals = items.reduce>((acc, item) => { + const categoryTotals = (items ?? []).reduce>((acc, item) => { const cat = item.category || "Autre"; if (!acc[cat]) acc[cat] = { qty: 0, total: 0 }; acc[cat].qty += item.quantite ?? 0; diff --git a/frontend-admin/src/screens/admin/OrdersScreen.tsx b/frontend-admin/src/screens/admin/OrdersScreen.tsx index 7ca2b0a3..c6a4183f 100644 --- a/frontend-admin/src/screens/admin/OrdersScreen.tsx +++ b/frontend-admin/src/screens/admin/OrdersScreen.tsx @@ -24,6 +24,7 @@ import { getCommandItems, notifyClientToDescend, confirmReceptionAdmin, + validateCommand, deleteCommand, deleteCommandItem, proposeAddressChangeAdmin, @@ -240,6 +241,26 @@ export default function OrdersScreen() { ); }; + const handleForceValidate = (commandId: number) => { + showConfirm( + "Finaliser la commande", + `Finaliser définitivement la commande #${commandId} et attribuer les points au client ?`, + async () => { + try { + const res = await validateCommand(commandId); + showSuccess( + "Commande finalisée", + `${res.points_awarded} point(s) attribués au client`, + ); + await loadData(); + } catch (e: any) { + showError("Erreur", e.message); + } + }, + "Finaliser", + ); + }; + const handleProposeAddress = async () => { if (!addressModal.commandId || !addressModal.input.trim()) return; try { @@ -647,14 +668,21 @@ export default function OrdersScreen() { {/* Total + parrainage */} - Total : + + {(item.referral_used ?? 0) > 0 ? "Total brut : " : "Total : "} + {item.total_prix.toFixed(2)} € - {(item.referral_used ?? 0) > 0 && ( - - {" "}(parrainage -{item.referral_used!.toFixed(2)} €) - - )} + {(item.referral_used ?? 0) > 0 && ( + <> + + Parrainage : -{item.referral_used!.toFixed(2)} € + + + Net : {(item.total_prix - item.referral_used!).toFixed(2)} € + + + )} {/* Livreur */} {!!item.livreur_assign && ( @@ -775,6 +803,15 @@ export default function OrdersScreen() { }, condition: item.status === "livre", }, + { + label: "Finaliser commande", + icon: "trophy-outline" as keyof typeof Ionicons.glyphMap, + onPress: () => { + setOpenMenuId(null); + handleForceValidate(item.id); + }, + condition: !isDone && item.status !== "livre", + }, { label: "Supprimer", icon: "trash-outline" as keyof typeof Ionicons.glyphMap, @@ -802,8 +839,18 @@ export default function OrdersScreen() { Client: {item.username} Adresse: {item.adresse} - Total: {item.total_prix.toFixed(2)} € + {(item.referral_used ?? 0) > 0 ? "Total brut" : "Total"}: {item.total_prix.toFixed(2)} € + {(item.referral_used ?? 0) > 0 && ( + <> + + Parrainage: -{(item.referral_used ?? 0).toFixed(2)} € + + + Net: {(item.total_prix - (item.referral_used ?? 0)).toFixed(2)} € + + + )} {item.livreur_assign && ( Livreur: {item.livreur_assign} @@ -1111,12 +1158,22 @@ export default function OrdersScreen() { )} {itemsModal.commandInfo?.total_prix != null && ( - - {Number( - itemsModal.commandInfo.total_prix, - ).toFixed(2)}{" "} - € - + <> + + {(itemsModal.commandInfo.referral_used ?? 0) > 0 ? "Brut : " : ""} + {Number(itemsModal.commandInfo.total_prix).toFixed(2)} € + + {(itemsModal.commandInfo.referral_used ?? 0) > 0 && ( + <> + + Parrainage : -{Number(itemsModal.commandInfo.referral_used).toFixed(2)} € + + + Net : {(Number(itemsModal.commandInfo.total_prix) - Number(itemsModal.commandInfo.referral_used)).toFixed(2)} € + + + )} + )} )} diff --git a/frontend-admin/src/screens/admin/SettingsScreen.tsx b/frontend-admin/src/screens/admin/SettingsScreen.tsx index 9fe50c1c..90a54bc0 100644 --- a/frontend-admin/src/screens/admin/SettingsScreen.tsx +++ b/frontend-admin/src/screens/admin/SettingsScreen.tsx @@ -657,6 +657,8 @@ const EMPTY_REWARD: PointsReward = { type: "free_product", description: "", category_configs: [], + reward_product_id: 0, + reward_quantity: 0, }; // ────────────────────────────────────────────────────────────── @@ -918,6 +920,76 @@ function CentralRewardSection({ /> + {/* Produit récompense */} + + Produit ajouté au panier + + Quand le client réclame sa récompense, ce produit est automatiquement ajouté à son panier (gratuit). Il doit commander au moins un produit normal. + + {/* Sélecteur produit */} + + {Object.values(productsByCategory).flat().length === 0 ? ( + Aucun produit disponible + ) : ( + + update({ reward_product_id: 0, reward_quantity: 0 })} + style={{ + flexDirection: "row", alignItems: "center", gap: spacing.xs, + paddingHorizontal: spacing.m, paddingVertical: spacing.s, + borderRadius: borderRadius.sm, borderWidth: 1.5, + borderColor: !r.reward_product_id ? colors.border : "transparent", + backgroundColor: !r.reward_product_id ? colors.border + "30" : "transparent", + }} + > + + Aucun (désactivé) + + + {Object.values(productsByCategory).flat().map((prod) => { + const sel = r.reward_product_id === prod.id; + return ( + update({ reward_product_id: prod.id, reward_quantity: r.reward_quantity && r.reward_quantity > 0 ? r.reward_quantity : 1 })} + style={{ + flexDirection: "row", alignItems: "center", gap: spacing.xs, + paddingHorizontal: spacing.m, paddingVertical: spacing.s, + borderRadius: borderRadius.sm, borderWidth: 1.5, + borderColor: sel ? REWARD_ACCENT : colors.border, + backgroundColor: sel ? REWARD_ACCENT + "18" : "transparent", + }} + > + + + {prod.name} ({prod.category}) + + {sel && } + + ); + })} + + )} + {/* Quantité */} + {!!r.reward_product_id && ( + + Quantité offerte + { + const n = parseFloat(v); + if (!isNaN(n) && n > 0) update({ reward_quantity: n }); + }} + placeholder="1" + placeholderTextColor={colors.textMuted} + /> + + )} + + + {/* Catégories éligibles */} Catégories éligibles diff --git a/frontend-admin/src/screens/delivery/DashboardScreen.tsx b/frontend-admin/src/screens/delivery/DashboardScreen.tsx index fcc3ba85..1be784ff 100644 --- a/frontend-admin/src/screens/delivery/DashboardScreen.tsx +++ b/frontend-admin/src/screens/delivery/DashboardScreen.tsx @@ -756,18 +756,28 @@ export default function DashboardScreen() { {(!item.items || item.items.length === 0) && ( <> - {item.total_prix ?? 0}€ + {(item.referral_used ?? 0) > 0 ? "Brut : " : ""} + {(item.total_prix ?? 0)}€ {(item.referral_used ?? 0) > 0 && ( - - Parrainage: - - {(item.referral_used ?? 0).toFixed(2)}€ - + <> + + Parrainage: -{(item.referral_used ?? 0).toFixed(2)}€ + + + Net à encaisser: {((item.total_prix ?? 0) - (item.referral_used ?? 0)).toFixed(2)}€ + + )} )} diff --git a/frontend-admin/src/screens/delivery/StatsScreen.tsx b/frontend-admin/src/screens/delivery/StatsScreen.tsx index c34348cc..7c302a87 100644 --- a/frontend-admin/src/screens/delivery/StatsScreen.tsx +++ b/frontend-admin/src/screens/delivery/StatsScreen.tsx @@ -139,7 +139,7 @@ export default function StatsScreen() { const pending = deliveries.filter((d) => d.status === "assigned").length; const totalRevenue = deliveries .filter((d) => d.status === "livre" || d.status === "approved") - .reduce((s, d) => s + d.total_prix, 0); + .reduce((s, d) => s + (d.total_prix - (d.referral_used ?? 0)), 0); const chartData = period === "day" ? byDay : period === "week" ? byWeek : byMonth; diff --git a/mobile/src/api/api.ts b/mobile/src/api/api.ts index 3857ebe0..67921c6b 100644 --- a/mobile/src/api/api.ts +++ b/mobile/src/api/api.ts @@ -965,6 +965,8 @@ export const claimMyReward = async (poolKey: string): Promise<{ success: boolean; description?: string; remaining_rewards?: number; + product_added?: boolean; + product_name?: string; error?: string; }> => { try { @@ -973,6 +975,8 @@ export const claimMyReward = async (poolKey: string): Promise<{ success: true, description: data.description, remaining_rewards: data.remaining_rewards, + product_added: data.product_added, + product_name: data.product_name, }; } catch (e: any) { return { success: false, error: e?.response?.data?.error || "Erreur" }; @@ -980,14 +984,12 @@ export const claimMyReward = async (poolKey: string): Promise<{ }; export const calculateOrderTotal = (order: any): number => { - let gross = 0; - if (typeof order.total === "number" && order.total > 0) gross = order.total; - else if (typeof order.total_prix === "number" && order.total_prix > 0) gross = order.total_prix; - else if (Array.isArray(order.items) && order.items.length > 0) { - gross = order.items.reduce((sum: number, item: any) => { + if (typeof order.total_prix === "number" && order.total_prix > 0) return order.total_prix; + if (typeof order.total === "number" && order.total > 0) return order.total; + if (Array.isArray(order.items) && order.items.length > 0) { + return order.items.reduce((sum: number, item: any) => { return sum + (item.prix || item.price || 0) * (item.quantite || item.quantity || 1); }, 0); } - const referralUsed = typeof order.referral_used === "number" ? order.referral_used : 0; - return Math.max(0, gross - referralUsed); + return 0; }; diff --git a/mobile/src/screens/client/OrderHistoryScreen.tsx b/mobile/src/screens/client/OrderHistoryScreen.tsx index c08a8d65..493c07e6 100644 --- a/mobile/src/screens/client/OrderHistoryScreen.tsx +++ b/mobile/src/screens/client/OrderHistoryScreen.tsx @@ -96,7 +96,10 @@ export default function OrderHistoryScreen() { const res = await claimMyReward(poolKey); setClaimingPool(null); if (res.success) { - setClaimFeedback({ pool: poolKey, type: "success", text: res.description || "Récompense réclamée !" }); + const text = res.product_added && res.product_name + ? `🎁 ${res.product_name} ajouté à votre panier ! Commandez au moins un produit pour en profiter.` + : res.description || "Récompense réclamée !"; + setClaimFeedback({ pool: poolKey, type: "success", text }); getMyPointsRewards().then((r) => { if (r.success) setPointsRewards(r); }); } else { setClaimFeedback({ pool: poolKey, type: "error", text: res.error || "Erreur" }); diff --git a/mobile/src/screens/client/OrderTrackingScreen.tsx b/mobile/src/screens/client/OrderTrackingScreen.tsx index dfad20cf..3f936eb4 100644 --- a/mobile/src/screens/client/OrderTrackingScreen.tsx +++ b/mobile/src/screens/client/OrderTrackingScreen.tsx @@ -385,7 +385,8 @@ export default function OrderTrackingScreen() { contentContainerStyle={styles.list} renderItem={({ item: order }) => { const expanded = expandedId === order.id; - const total = calculateOrderTotal(order); + const gross = calculateOrderTotal(order); + const total = Math.max(0, gross - (order.referral_used ?? 0)); const progress = STATUS_PROGRESS[order.status] || 0; const track = tracking[order.id]; const eta = etas[order.id]; @@ -444,9 +445,16 @@ export default function OrderTrackingScreen() { - - {formatPrice(total)} - + + + {formatPrice(total)} + + {(order.referral_used ?? 0) > 0 && ( + + dont -{(order.referral_used as number).toFixed(2)} € parrainage + + )} +