From f72313299dc76e96702f2c5944750095429d16d7 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Mon, 15 Jun 2026 19:24:33 +0200 Subject: [PATCH] chore: build --- backend/gestion/db/db_cancel_command.go | 5 - backend/gestion/handlers/points.go | 19 +- mobile/src/api/api.ts | 6 +- mobile/src/api/api_types.ts | 2 - .../src/screens/client/OrderHistoryScreen.tsx | 771 +++++++++++++++--- .../screens/client/ProductDetailScreen.tsx | 2 +- 6 files changed, 656 insertions(+), 149 deletions(-) diff --git a/backend/gestion/db/db_cancel_command.go b/backend/gestion/db/db_cancel_command.go index 40c23c03..9b608313 100644 --- a/backend/gestion/db/db_cancel_command.go +++ b/backend/gestion/db/db_cancel_command.go @@ -1,8 +1,3 @@ -// ============================================ -// db/cancel_commands_db.go -// FONCTIONS DB ATOMIQUES POUR L'ANNULATION -// ============================================ - package db import ( diff --git a/backend/gestion/handlers/points.go b/backend/gestion/handlers/points.go index 526cbb8a..aa5678e3 100644 --- a/backend/gestion/handlers/points.go +++ b/backend/gestion/handlers/points.go @@ -2,6 +2,7 @@ package handlers import ( "gestion/db" + "gestion/models" "gestion/utils" "log" "net/http" @@ -166,7 +167,8 @@ func ClaimMyReward(c *gin.Context) { } var req struct { - PoolKey string `json:"pool_key" binding:"required"` + PoolKey string `json:"pool_key" binding:"required"` + ProductID int `json:"product_id"` // optionnel : 0 = automatique (1 seul item) } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "pool_key requis"}) @@ -215,11 +217,22 @@ func ClaimMyReward(c *gin.Context) { return } + // Si le client a sélectionné un produit spécifique parmi plusieurs, ne donner que celui-là + itemsToAdd := reward.RewardItems + if req.ProductID > 0 && len(reward.RewardItems) > 1 { + for _, item := range reward.RewardItems { + if item.ProductID == req.ProductID { + itemsToAdd = []models.RewardItem{item} + break + } + } + } + // Ajouter les produits récompense au panier si configurés productAdded := false var productNames []string - if len(reward.RewardItems) > 0 { - if added, addErr := database.AddRewardsToBasket(username, reward.RewardItems, req.PoolKey); addErr == nil && len(added) > 0 { + if len(itemsToAdd) > 0 { + if added, addErr := database.AddRewardsToBasket(username, itemsToAdd, req.PoolKey); addErr == nil && len(added) > 0 { productAdded = true for _, item := range added { productNames = append(productNames, item.ProductName) diff --git a/mobile/src/api/api.ts b/mobile/src/api/api.ts index 941313d7..ef508423 100644 --- a/mobile/src/api/api.ts +++ b/mobile/src/api/api.ts @@ -969,7 +969,7 @@ export const getMyPointsRewards = async (): Promise<{ } }; -export const claimMyReward = async (poolKey: string): Promise<{ +export const claimMyReward = async (poolKey: string, productId?: number): Promise<{ success: boolean; description?: string; remaining_rewards?: number; @@ -978,7 +978,9 @@ export const claimMyReward = async (poolKey: string): Promise<{ error?: string; }> => { try { - const { data } = await apiClient.post(`${V1}/points/claim`, { pool_key: poolKey }); + const body: { pool_key: string; product_id?: number } = { pool_key: poolKey }; + if (productId !== undefined) body.product_id = productId; + const { data } = await apiClient.post(`${V1}/points/claim`, body); return { success: true, description: data.description, diff --git a/mobile/src/api/api_types.ts b/mobile/src/api/api_types.ts index 88a2e41a..b66244cb 100644 --- a/mobile/src/api/api_types.ts +++ b/mobile/src/api/api_types.ts @@ -1,8 +1,6 @@ // ============================================ // api/api_TYPES.ts - TOUTES LES INTERFACES // ============================================ -// Interfaces complètes pour le frontend -// À importer dans les composants export interface ApiResponse { success: boolean; diff --git a/mobile/src/screens/client/OrderHistoryScreen.tsx b/mobile/src/screens/client/OrderHistoryScreen.tsx index 41daaf5d..a06e6c36 100644 --- a/mobile/src/screens/client/OrderHistoryScreen.tsx +++ b/mobile/src/screens/client/OrderHistoryScreen.tsx @@ -6,6 +6,8 @@ import { TouchableOpacity, StyleSheet, RefreshControl, + Modal, + ScrollView, } from "react-native"; import { useNavigation, useFocusEffect } from "@react-navigation/native"; import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; @@ -20,7 +22,12 @@ import { formatOrderDate, formatPrice, } from "../../api/api"; -import type { PublicSettings, PointsPoolInfo, PointsRewardConfig, RewardItemConfig } from "../../api/api"; +import type { + PublicSettings, + PointsPoolInfo, + PointsRewardConfig, + RewardItemConfig, +} from "../../api/api"; import type { CompletedOrder, ClientStats, @@ -47,7 +54,20 @@ export default function OrderHistoryScreen() { const [orders, setOrders] = useState([]); const [stats, setStats] = useState(null); const [penalties, setPenalties] = useState(null); - const [appSettings, setAppSettings] = useState({ penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: false, pool_names: ['Pool 1', 'Pool 2'], crypto_payment_enabled: false, nowpayments_currencies: [], crypto_only: false, telegram_notifications_enabled: false, two_fa_enabled: false, contact_telegram: "" }); + const [appSettings, setAppSettings] = useState({ + penalties_enabled: true, + show_amende_score: true, + points_enabled: true, + points_separated: true, + referral_enabled: false, + pool_names: ["Pool 1", "Pool 2"], + crypto_payment_enabled: false, + nowpayments_currencies: [], + crypto_only: false, + telegram_notifications_enabled: false, + two_fa_enabled: false, + contact_telegram: "", + }); const [referralBalance, setReferralBalance] = useState(0); const [pointsRewards, setPointsRewards] = useState<{ enabled: boolean; @@ -55,19 +75,27 @@ export default function OrderHistoryScreen() { reward: PointsRewardConfig | null; } | null>(null); const [claimingPool, setClaimingPool] = useState(null); - const [claimFeedback, setClaimFeedback] = useState<{ pool: string; type: "success" | "error"; text: string } | null>(null); + const [claimFeedback, setClaimFeedback] = useState<{ + pool: string; + type: "success" | "error"; + text: string; + } | null>(null); + const [rewardPickerPool, setRewardPickerPool] = useState( + null, + ); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const fetchData = useCallback(async () => { try { - const [histRes, penRes, settings, refRes, rewardsRes] = await Promise.all([ - getMyCompletedOrders(), - getMyPenalties(), - getPublicSettings(), - getReferralBalance(), - getMyPointsRewards(), - ]); + const [histRes, penRes, settings, refRes, rewardsRes] = + await Promise.all([ + getMyCompletedOrders(), + getMyPenalties(), + getPublicSettings(), + getReferralBalance(), + getMyPointsRewards(), + ]); if (histRes.success) { setOrders(histRes.commands || []); setStats(histRes.client_stats || null); @@ -90,19 +118,40 @@ export default function OrderHistoryScreen() { } }, []); - const handleClaim = async (poolKey: string) => { + const handleClaim = (poolKey: string) => { + const items = pointsRewards?.reward?.reward_items ?? []; + if (items.length > 1) { + setClaimFeedback(null); + setRewardPickerPool(poolKey); + return; + } + doClaimReward(poolKey, undefined); + }; + + const doClaimReward = async ( + poolKey: string, + productId: number | undefined, + ) => { + setRewardPickerPool(null); setClaimingPool(poolKey); setClaimFeedback(null); - const res = await claimMyReward(poolKey); + const res = await claimMyReward(poolKey, productId); setClaimingPool(null); if (res.success) { - 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 !"; + 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); }); + getMyPointsRewards().then((r) => { + if (r.success) setPointsRewards(r); + }); } else { - setClaimFeedback({ pool: poolKey, type: "error", text: res.error || "Erreur" }); + setClaimFeedback({ + pool: poolKey, + type: "error", + text: res.error || "Erreur", + }); } }; @@ -356,6 +405,76 @@ export default function OrderHistoryScreen() { letterSpacing: 0.5, marginLeft: 4, }, + // Modal styles + modalOverlay: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.65)", + justifyContent: "center", + padding: spacing.l, + }, + modalContent: { + backgroundColor: colors.bgCard, + borderRadius: borderRadius.md, + padding: spacing.l, + borderWidth: 1, + borderColor: "#f59e0b44", + maxHeight: "80%", + }, + modalHeader: { + flexDirection: "row", + alignItems: "center", + gap: spacing.s, + marginBottom: spacing.m, + paddingBottom: spacing.m, + borderBottomWidth: 1, + borderBottomColor: colors.borderLight, + }, + modalTitle: { + color: colors.textWhite, + fontSize: fontSize.md, + fontWeight: fontWeight.bold, + flex: 1, + }, + modalSubtitle: { + color: colors.textMuted, + fontSize: fontSize.xs, + marginBottom: spacing.m, + }, + pickerItem: { + flexDirection: "row", + alignItems: "center", + gap: spacing.m, + backgroundColor: "rgba(245,158,11,0.07)", + borderRadius: borderRadius.sm, + borderWidth: 1, + borderColor: "rgba(245,158,11,0.2)", + padding: spacing.m, + marginBottom: spacing.s, + }, + pickerItemName: { + color: colors.textWhite, + fontSize: fontSize.sm, + fontWeight: fontWeight.semibold, + }, + pickerItemQty: { + color: colors.textMuted, + fontSize: fontSize.xs, + marginTop: 2, + }, + pickerItemPrice: { + color: "#10b981", + fontSize: fontSize.sm, + fontWeight: fontWeight.bold, + }, + modalCancelBtn: { + marginTop: spacing.s, + alignItems: "center", + paddingVertical: spacing.m, + }, + modalCancelText: { + color: colors.textMuted, + fontSize: fontSize.sm, + }, }), [colors], ); @@ -363,7 +482,9 @@ export default function OrderHistoryScreen() { if (loading && !refreshing) return ; - const poolNames = stats?.pool_names?.length ? stats.pool_names : appSettings.pool_names; + const poolNames = stats?.pool_names?.length + ? stats.pool_names + : appSettings.pool_names; const poolPoints = stats?.pool_points ?? [stats?.points ?? 0]; const totalPoints = poolPoints.reduce((s, v) => s + (v || 0), 0); const penaltyCount = penalties?.total_penalty || stats?.penalties || 0; @@ -395,38 +516,92 @@ export default function OrderHistoryScreen() { Commandes - {appSettings.points_enabled && ( - poolNames.length <= 1 ? ( + {appSettings.points_enabled && + (poolNames.length <= 1 ? ( - - {poolPoints[0] || 0} - Pts {poolNames[0] ?? 'Points'} + + + {poolPoints[0] || 0} + + + Pts {poolNames[0] ?? "Points"} + ) : ( <> {poolNames.map((name, i) => { - const poolIconNames = ["leaf-outline", "medical-outline", "flask-outline", "color-fill-outline", "star-outline"] as const; - const poolIconColors = [colors.categoryWeedHash, "#e879f9", "#fb923c", "#38bdf8", colors.accent]; - const icon = poolIconNames[i] ?? "star-outline"; - const color = poolIconColors[i] ?? colors.accent; + const poolIconNames = [ + "leaf-outline", + "medical-outline", + "flask-outline", + "color-fill-outline", + "star-outline", + ] as const; + const poolIconColors = [ + colors.categoryWeedHash, + "#e879f9", + "#fb923c", + "#38bdf8", + colors.accent, + ]; + const icon = + poolIconNames[i] ?? + "star-outline"; + const color = + poolIconColors[i] ?? + colors.accent; return ( - - - {poolPoints[i] || 0} - Pts {name} - + + + + {poolPoints[i] || 0} + + + Pts {name} + + ); })} {totalPoints > 0 && ( - - - {totalPoints} - Total Points + + + + {totalPoints} + + + Total Points + )} - ) - )} + ))} {appSettings.show_amende_score && ( 0 ? "warning" : "shield-checkmark-outline"} + name={ + penaltyCount > 0 + ? "warning" + : "shield-checkmark-outline" + } size={24} color={ penaltyCount >= 3 ? colors.danger : penaltyCount > 0 - ? colors.warning - : colors.textMuted + ? colors.warning + : colors.textMuted } /> = 3 && { color: colors.danger }, - penaltyCount > 0 && penaltyCount < 3 && { color: colors.warning }, + penaltyCount >= 3 && { + color: colors.danger, + }, + penaltyCount > 0 && + penaltyCount < 3 && { + color: colors.warning, + }, ]} > {penaltyCount} € - - Amende - + Amende )} {/* Section récompenses */} - {pointsRewards?.enabled && pointsRewards.reward && pointsRewards.pools.length > 0 && ( - - - - Récompenses - - {pointsRewards.reward.description !== "" && ( - {pointsRewards.reward.description} - )} - {(pointsRewards.reward.reward_items ?? []).filter((it) => it.price > 0 || it.product_name).length > 0 && ( - - {(pointsRewards.reward.reward_items ?? []).map((it: RewardItemConfig, idx: number) => ( - - - - {it.product_name}{it.quantity > 0 && it.quantity !== 1 ? ` ×${it.quantity}` : ""}{it.price > 0 ? ` — ${it.price}€` : ""} - - - ))} + {pointsRewards?.enabled && + pointsRewards.reward && + pointsRewards.pools.length > 0 && ( + + + + + Récompenses + - )} - {pointsRewards.pools.map((pool) => { - const threshold = pointsRewards.reward!.threshold; - const progress = Math.min(1, (pool.points % threshold) / threshold); - const remaining = threshold - (pool.points % threshold); - const isClaiming = claimingPool === pool.key; - const feedback = claimFeedback?.pool === pool.key ? claimFeedback : null; - return ( - - - {pool.name} - {pool.points} pts - - {pool.eligible_configs.length > 0 && ( - - {pool.eligible_configs.flatMap((cfg) => - cfg.all_products - ? [ - {cfg.category} - ] - : (cfg.product_names ?? []).map((name) => ( - - {name} - - )) - )} - - )} - - - - 0 ? styles.rewardAvailable : styles.rewardRemaining}> - {pool.rewards_available > 0 - ? `${pool.rewards_available} récompense${pool.rewards_available > 1 ? "s" : ""} disponible${pool.rewards_available > 1 ? "s" : ""}` - : `Encore ${remaining} pts pour une récompense`} - - {feedback && ( - - {feedback.text} - - )} - {pool.rewards_available > 0 && ( - handleClaim(pool.key)} - disabled={isClaiming} - > - - - {isClaiming ? "..." : "Réclamer ma récompense"} - - + {pointsRewards.reward.description !== + "" && ( + + {pointsRewards.reward.description} + + )} + {( + pointsRewards.reward.reward_items ?? [] + ).filter( + (it) => it.price > 0 || it.product_name, + ).length > 0 && ( + + {( + pointsRewards.reward + .reward_items ?? [] + ).map( + ( + it: RewardItemConfig, + idx: number, + ) => ( + + + + {it.product_name} + {it.quantity > 0 && + it.quantity !== 1 + ? ` ×${it.quantity}` + : ""} + {it.price > 0 + ? ` — ${it.price}€` + : ""} + + + ), )} - ); - })} - - )} + )} + {pointsRewards.pools.map((pool) => { + const threshold = + pointsRewards.reward!.threshold; + const progress = Math.min( + 1, + (pool.points % threshold) / + threshold, + ); + const remaining = + threshold - + (pool.points % threshold); + const isClaiming = + claimingPool === pool.key; + const feedback = + claimFeedback?.pool === pool.key + ? claimFeedback + : null; + return ( + + + + {pool.name} + + + {pool.points} pts + + + {pool.eligible_configs.length > + 0 && ( + + {pool.eligible_configs.flatMap( + (cfg) => + cfg.all_products + ? [ + + + { + cfg.category + } + + , + ] + : ( + cfg.product_names ?? + [] + ).map( + ( + name, + ) => ( + + + { + name + } + + + ), + ), + )} + + )} + + + + + 0 + ? styles.rewardAvailable + : styles.rewardRemaining + } + > + {pool.rewards_available > 0 + ? `${pool.rewards_available} récompense${pool.rewards_available > 1 ? "s" : ""} disponible${pool.rewards_available > 1 ? "s" : ""}` + : `Encore ${remaining} pts pour une récompense`} + + {feedback && ( + + {feedback.text} + + )} + {pool.rewards_available > 0 && ( + + handleClaim( + pool.key, + ) + } + disabled={isClaiming} + > + + + {isClaiming + ? "..." + : "Réclamer ma récompense"} + + + )} + + ); + })} + + )} - {/* Bouton parrainage — visible seulement si activé dans les settings */} - {appSettings.referral_enabled && navigation.navigate("Parrainage")} - > - - - Parrainage - {referralBalance > 0 ? ` — ${referralBalance.toFixed(2)} €` : ""} - - - } + {/* Bouton parrainage */} + {appSettings.referral_enabled && ( + + navigation.navigate("Parrainage") + } + > + + + Parrainage + {referralBalance > 0 + ? ` — ${referralBalance.toFixed(2)} €` + : ""} + + + + )} {orders.length > 0 && ( @@ -603,8 +989,14 @@ export default function OrderHistoryScreen() { ) : ( - - Livrée + + + Livrée + )} @@ -642,7 +1034,10 @@ export default function OrderHistoryScreen() { )} - {formatPrice((order.total_prix || 0) - (order.referral_used || 0))} + {formatPrice( + (order.total_prix || 0) - + (order.referral_used || 0), + )} )} /> + + {/* Modal sélection produit récompense */} + setRewardPickerPool(null)} + > + setRewardPickerPool(null)} + > + {/* Inner TouchableOpacity blocks tap propagation to the overlay */} + {}}> + + + + + Choisir votre récompense + + setRewardPickerPool(null)} + > + + + + + + Sélectionnez le produit que vous souhaitez + recevoir + + + + {( + pointsRewards?.reward?.reward_items ?? [] + ).map((item: RewardItemConfig, idx: number) => ( + + doClaimReward( + rewardPickerPool!, + item.product_id, + ) + } + activeOpacity={0.7} + > + + + + {item.product_name} + + {item.quantity > 0 && + item.quantity !== 1 && ( + + ×{item.quantity} + + )} + + {item.price > 0 && ( + + {item.price}€ + + )} + + + ))} + + + setRewardPickerPool(null)} + > + + Annuler + + + + + + ); } diff --git a/mobile/src/screens/client/ProductDetailScreen.tsx b/mobile/src/screens/client/ProductDetailScreen.tsx index 4d14e1bc..75382375 100644 --- a/mobile/src/screens/client/ProductDetailScreen.tsx +++ b/mobile/src/screens/client/ProductDetailScreen.tsx @@ -597,7 +597,7 @@ export default function ProductDetailScreen() { "Aucune description disponible."} - {hasValidPrices && ( + {hasValidPrices && !isComingSoon && ( Quantite: