chore: build

This commit is contained in:
2026-06-15 19:13:51 +02:00
parent c7d43e7641
commit 48e6cbcb4c
4 changed files with 656 additions and 142 deletions
+16 -3
View File
@@ -2,6 +2,7 @@ package handlers
import ( import (
"gestion/db" "gestion/db"
"gestion/models"
"gestion/utils" "gestion/utils"
"log" "log"
"net/http" "net/http"
@@ -166,7 +167,8 @@ func ClaimMyReward(c *gin.Context) {
} }
var req struct { 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 { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "pool_key requis"}) c.JSON(http.StatusBadRequest, gin.H{"error": "pool_key requis"})
@@ -215,11 +217,22 @@ func ClaimMyReward(c *gin.Context) {
return 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 // Ajouter les produits récompense au panier si configurés
productAdded := false productAdded := false
var productNames []string var productNames []string
if len(reward.RewardItems) > 0 { if len(itemsToAdd) > 0 {
if added, addErr := database.AddRewardsToBasket(username, reward.RewardItems, req.PoolKey); addErr == nil && len(added) > 0 { if added, addErr := database.AddRewardsToBasket(username, itemsToAdd, req.PoolKey); addErr == nil && len(added) > 0 {
productAdded = true productAdded = true
for _, item := range added { for _, item := range added {
productNames = append(productNames, item.ProductName) productNames = append(productNames, item.ProductName)
+4 -2
View File
@@ -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; success: boolean;
description?: string; description?: string;
remaining_rewards?: number; remaining_rewards?: number;
@@ -978,7 +978,9 @@ export const claimMyReward = async (poolKey: string): Promise<{
error?: string; error?: string;
}> => { }> => {
try { 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 { return {
success: true, success: true,
description: data.description, description: data.description,
+635 -136
View File
@@ -6,6 +6,8 @@ import {
TouchableOpacity, TouchableOpacity,
StyleSheet, StyleSheet,
RefreshControl, RefreshControl,
Modal,
ScrollView,
} from "react-native"; } from "react-native";
import { useNavigation, useFocusEffect } from "@react-navigation/native"; import { useNavigation, useFocusEffect } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
@@ -20,7 +22,12 @@ import {
formatOrderDate, formatOrderDate,
formatPrice, formatPrice,
} from "../../api/api"; } from "../../api/api";
import type { PublicSettings, PointsPoolInfo, PointsRewardConfig, RewardItemConfig } from "../../api/api"; import type {
PublicSettings,
PointsPoolInfo,
PointsRewardConfig,
RewardItemConfig,
} from "../../api/api";
import type { import type {
CompletedOrder, CompletedOrder,
ClientStats, ClientStats,
@@ -47,7 +54,20 @@ export default function OrderHistoryScreen() {
const [orders, setOrders] = useState<CompletedOrder[]>([]); const [orders, setOrders] = useState<CompletedOrder[]>([]);
const [stats, setStats] = useState<ClientStats | null>(null); const [stats, setStats] = useState<ClientStats | null>(null);
const [penalties, setPenalties] = useState<PenaltyInfo | null>(null); const [penalties, setPenalties] = useState<PenaltyInfo | null>(null);
const [appSettings, setAppSettings] = useState<PublicSettings>({ 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<PublicSettings>({
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 [referralBalance, setReferralBalance] = useState(0);
const [pointsRewards, setPointsRewards] = useState<{ const [pointsRewards, setPointsRewards] = useState<{
enabled: boolean; enabled: boolean;
@@ -55,19 +75,27 @@ export default function OrderHistoryScreen() {
reward: PointsRewardConfig | null; reward: PointsRewardConfig | null;
} | null>(null); } | null>(null);
const [claimingPool, setClaimingPool] = useState<string | null>(null); const [claimingPool, setClaimingPool] = useState<string | null>(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<string | null>(
null,
);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
try { try {
const [histRes, penRes, settings, refRes, rewardsRes] = await Promise.all([ const [histRes, penRes, settings, refRes, rewardsRes] =
getMyCompletedOrders(), await Promise.all([
getMyPenalties(), getMyCompletedOrders(),
getPublicSettings(), getMyPenalties(),
getReferralBalance(), getPublicSettings(),
getMyPointsRewards(), getReferralBalance(),
]); getMyPointsRewards(),
]);
if (histRes.success) { if (histRes.success) {
setOrders(histRes.commands || []); setOrders(histRes.commands || []);
setStats(histRes.client_stats || null); 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); setClaimingPool(poolKey);
setClaimFeedback(null); setClaimFeedback(null);
const res = await claimMyReward(poolKey); const res = await claimMyReward(poolKey, productId);
setClaimingPool(null); setClaimingPool(null);
if (res.success) { if (res.success) {
const text = res.product_added && res.product_name const text =
? `🎁 ${res.product_name} ajouté à votre panier ! Commandez au moins un produit pour en profiter.` res.product_added && res.product_name
: res.description || "Récompense réclamée !"; ? `🎁 ${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 }); setClaimFeedback({ pool: poolKey, type: "success", text });
getMyPointsRewards().then((r) => { if (r.success) setPointsRewards(r); }); getMyPointsRewards().then((r) => {
if (r.success) setPointsRewards(r);
});
} else { } 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, letterSpacing: 0.5,
marginLeft: 4, 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], [colors],
); );
@@ -363,7 +482,9 @@ export default function OrderHistoryScreen() {
if (loading && !refreshing) if (loading && !refreshing)
return <LoadingSpinner message="Chargement historique..." />; return <LoadingSpinner message="Chargement historique..." />;
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 poolPoints = stats?.pool_points ?? [stats?.points ?? 0];
const totalPoints = poolPoints.reduce((s, v) => s + (v || 0), 0); const totalPoints = poolPoints.reduce((s, v) => s + (v || 0), 0);
const penaltyCount = penalties?.total_penalty || stats?.penalties || 0; const penaltyCount = penalties?.total_penalty || stats?.penalties || 0;
@@ -395,38 +516,92 @@ export default function OrderHistoryScreen() {
</Text> </Text>
<Text style={styles.statLabel}>Commandes</Text> <Text style={styles.statLabel}>Commandes</Text>
</View> </View>
{appSettings.points_enabled && ( {appSettings.points_enabled &&
poolNames.length <= 1 ? ( (poolNames.length <= 1 ? (
<View style={[styles.statCard, shadows.sm]}> <View style={[styles.statCard, shadows.sm]}>
<Ionicons name="trophy-outline" size={24} color={colors.warning} /> <Ionicons
<Text style={styles.statValue}>{poolPoints[0] || 0}</Text> name="trophy-outline"
<Text style={styles.statLabel}>Pts {poolNames[0] ?? 'Points'}</Text> size={24}
color={colors.warning}
/>
<Text style={styles.statValue}>
{poolPoints[0] || 0}
</Text>
<Text style={styles.statLabel}>
Pts {poolNames[0] ?? "Points"}
</Text>
</View> </View>
) : ( ) : (
<> <>
{poolNames.map((name, i) => { {poolNames.map((name, i) => {
const poolIconNames = ["leaf-outline", "medical-outline", "flask-outline", "color-fill-outline", "star-outline"] as const; const poolIconNames = [
const poolIconColors = [colors.categoryWeedHash, "#e879f9", "#fb923c", "#38bdf8", colors.accent]; "leaf-outline",
const icon = poolIconNames[i] ?? "star-outline"; "medical-outline",
const color = poolIconColors[i] ?? colors.accent; "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 ( return (
<View key={i} style={[styles.statCard, shadows.sm]}> <View
<Ionicons name={icon} size={24} color={color} /> key={i}
<Text style={styles.statValue}>{poolPoints[i] || 0}</Text> style={[
<Text style={styles.statLabel}>Pts {name}</Text> styles.statCard,
</View> shadows.sm,
]}
>
<Ionicons
name={icon}
size={24}
color={color}
/>
<Text
style={styles.statValue}
>
{poolPoints[i] || 0}
</Text>
<Text
style={styles.statLabel}
>
Pts {name}
</Text>
</View>
); );
})} })}
{totalPoints > 0 && ( {totalPoints > 0 && (
<View style={[styles.statCard, shadows.sm]}> <View
<Ionicons name="trophy-outline" size={24} color={colors.warning} /> style={[
<Text style={styles.statValue}>{totalPoints}</Text> styles.statCard,
<Text style={styles.statLabel}>Total Points</Text> shadows.sm,
]}
>
<Ionicons
name="trophy-outline"
size={24}
color={colors.warning}
/>
<Text style={styles.statValue}>
{totalPoints}
</Text>
<Text style={styles.statLabel}>
Total Points
</Text>
</View> </View>
)} )}
</> </>
) ))}
)}
{appSettings.show_amende_score && ( {appSettings.show_amende_score && (
<View <View
style={[ style={[
@@ -446,124 +621,335 @@ export default function OrderHistoryScreen() {
]} ]}
> >
<Ionicons <Ionicons
name={penaltyCount > 0 ? "warning" : "shield-checkmark-outline"} name={
penaltyCount > 0
? "warning"
: "shield-checkmark-outline"
}
size={24} size={24}
color={ color={
penaltyCount >= 3 penaltyCount >= 3
? colors.danger ? colors.danger
: penaltyCount > 0 : penaltyCount > 0
? colors.warning ? colors.warning
: colors.textMuted : colors.textMuted
} }
/> />
<Text <Text
style={[ style={[
styles.statValue, styles.statValue,
penaltyCount >= 3 && { color: colors.danger }, penaltyCount >= 3 && {
penaltyCount > 0 && penaltyCount < 3 && { color: colors.warning }, color: colors.danger,
},
penaltyCount > 0 &&
penaltyCount < 3 && {
color: colors.warning,
},
]} ]}
> >
{penaltyCount} {penaltyCount}
</Text> </Text>
<Text style={styles.statLabel}> <Text style={styles.statLabel}>Amende</Text>
Amende
</Text>
</View> </View>
)} )}
</View> </View>
{/* Section récompenses */} {/* Section récompenses */}
{pointsRewards?.enabled && pointsRewards.reward && pointsRewards.pools.length > 0 && ( {pointsRewards?.enabled &&
<View style={styles.rewardsSection}> pointsRewards.reward &&
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs, marginBottom: spacing.xs }}> pointsRewards.pools.length > 0 && (
<Ionicons name="trophy-outline" size={14} color="#f59e0b" /> <View style={styles.rewardsSection}>
<Text style={styles.rewardsSectionTitle}>Récompenses</Text> <View
</View> style={{
{pointsRewards.reward.description !== "" && ( flexDirection: "row",
<Text style={styles.rewardsSectionDesc}>{pointsRewards.reward.description}</Text> alignItems: "center",
)} gap: spacing.xs,
{(pointsRewards.reward.reward_items ?? []).filter((it) => it.price > 0 || it.product_name).length > 0 && ( marginBottom: spacing.xs,
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 4, marginBottom: spacing.s }}> }}
{(pointsRewards.reward.reward_items ?? []).map((it: RewardItemConfig, idx: number) => ( >
<View key={idx} style={[styles.rewardAmountBadge, { flexDirection: "row", alignItems: "center", gap: 4 }]}> <Ionicons
<Ionicons name="gift-outline" size={11} color="#f59e0b" /> name="trophy-outline"
<Text style={styles.rewardAmountText}> size={14}
{it.product_name}{it.quantity > 0 && it.quantity !== 1 ? ` ×${it.quantity}` : ""}{it.price > 0 ? `${it.price}` : ""} color="#f59e0b"
</Text> />
</View> <Text
))} style={styles.rewardsSectionTitle}
>
Récompenses
</Text>
</View> </View>
)} {pointsRewards.reward.description !==
{pointsRewards.pools.map((pool) => { "" && (
const threshold = pointsRewards.reward!.threshold; <Text style={styles.rewardsSectionDesc}>
const progress = Math.min(1, (pool.points % threshold) / threshold); {pointsRewards.reward.description}
const remaining = threshold - (pool.points % threshold); </Text>
const isClaiming = claimingPool === pool.key; )}
const feedback = claimFeedback?.pool === pool.key ? claimFeedback : null; {(
return ( pointsRewards.reward.reward_items ?? []
<View key={pool.key} style={styles.rewardPoolCard}> ).filter(
<View style={{ flexDirection: "row", justifyContent: "space-between", marginBottom: spacing.xs }}> (it) => it.price > 0 || it.product_name,
<Text style={styles.rewardPoolName}>{pool.name}</Text> ).length > 0 && (
<Text style={styles.rewardPoolPts}>{pool.points} pts</Text> <View
</View> style={{
{pool.eligible_configs.length > 0 && ( flexDirection: "row",
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 4, marginBottom: spacing.xs }}> flexWrap: "wrap",
{pool.eligible_configs.flatMap((cfg) => gap: 4,
cfg.all_products marginBottom: spacing.s,
? [<View key={cfg.category} style={styles.rewardAmountBadge}> }}
<Text style={styles.rewardAmountText}>{cfg.category}</Text> >
</View>] {(
: (cfg.product_names ?? []).map((name) => ( pointsRewards.reward
<View key={`${cfg.category}-${name}`} style={styles.rewardAmountBadge}> .reward_items ?? []
<Text style={styles.rewardAmountText}>{name}</Text> ).map(
</View> (
)) it: RewardItemConfig,
)} idx: number,
</View> ) => (
)} <View
<View style={styles.progressBarBg}> key={idx}
<View style={[styles.progressBarFill, { width: `${Math.round(progress * 100)}%` as any }]} /> style={[
</View> styles.rewardAmountBadge,
<Text style={pool.rewards_available > 0 ? styles.rewardAvailable : styles.rewardRemaining}> {
{pool.rewards_available > 0 flexDirection:
? `${pool.rewards_available} récompense${pool.rewards_available > 1 ? "s" : ""} disponible${pool.rewards_available > 1 ? "s" : ""}` "row",
: `Encore ${remaining} pts pour une récompense`} alignItems:
</Text> "center",
{feedback && ( gap: 4,
<Text style={feedback.type === "success" ? styles.feedbackSuccess : styles.feedbackError}> },
{feedback.text} ]}
</Text> >
)} <Ionicons
{pool.rewards_available > 0 && ( name="gift-outline"
<TouchableOpacity size={11}
style={[styles.claimBtn, isClaiming && { opacity: 0.5 }]} color="#f59e0b"
onPress={() => handleClaim(pool.key)} />
disabled={isClaiming} <Text
> style={
<Ionicons name="gift-outline" size={14} color="#fff" /> styles.rewardAmountText
<Text style={styles.claimBtnText}> }
{isClaiming ? "..." : "Réclamer ma récompense"} >
</Text> {it.product_name}
</TouchableOpacity> {it.quantity > 0 &&
it.quantity !== 1
? ` ×${it.quantity}`
: ""}
{it.price > 0
? `${it.price}`
: ""}
</Text>
</View>
),
)} )}
</View> </View>
); )}
})} {pointsRewards.pools.map((pool) => {
</View> 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 (
<View
key={pool.key}
style={styles.rewardPoolCard}
>
<View
style={{
flexDirection: "row",
justifyContent:
"space-between",
marginBottom:
spacing.xs,
}}
>
<Text
style={
styles.rewardPoolName
}
>
{pool.name}
</Text>
<Text
style={
styles.rewardPoolPts
}
>
{pool.points} pts
</Text>
</View>
{pool.eligible_configs.length >
0 && (
<View
style={{
flexDirection:
"row",
flexWrap: "wrap",
gap: 4,
marginBottom:
spacing.xs,
}}
>
{pool.eligible_configs.flatMap(
(cfg) =>
cfg.all_products
? [
<View
key={
cfg.category
}
style={
styles.rewardAmountBadge
}
>
<Text
style={
styles.rewardAmountText
}
>
{
cfg.category
}
</Text>
</View>,
]
: (
cfg.product_names ??
[]
).map(
(
name,
) => (
<View
key={`${cfg.category}-${name}`}
style={
styles.rewardAmountBadge
}
>
<Text
style={
styles.rewardAmountText
}
>
{
name
}
</Text>
</View>
),
),
)}
</View>
)}
<View
style={styles.progressBarBg}
>
<View
style={[
styles.progressBarFill,
{
width: `${Math.round(progress * 100)}%` as any,
},
]}
/>
</View>
<Text
style={
pool.rewards_available >
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`}
</Text>
{feedback && (
<Text
style={
feedback.type ===
"success"
? styles.feedbackSuccess
: styles.feedbackError
}
>
{feedback.text}
</Text>
)}
{pool.rewards_available > 0 && (
<TouchableOpacity
style={[
styles.claimBtn,
isClaiming && {
opacity: 0.5,
},
]}
onPress={() =>
handleClaim(
pool.key,
)
}
disabled={isClaiming}
>
<Ionicons
name="gift-outline"
size={14}
color="#fff"
/>
<Text
style={
styles.claimBtnText
}
>
{isClaiming
? "..."
: "Réclamer ma récompense"}
</Text>
</TouchableOpacity>
)}
</View>
);
})}
</View>
)}
{/* Bouton parrainage — visible seulement si activé dans les settings */} {/* Bouton parrainage */}
{appSettings.referral_enabled && <TouchableOpacity {appSettings.referral_enabled && (
style={styles.referralBtn} <TouchableOpacity
onPress={() => navigation.navigate("Parrainage")} style={styles.referralBtn}
> onPress={() =>
<Ionicons name="gift-outline" size={18} color={colors.accent} /> navigation.navigate("Parrainage")
<Text style={styles.referralBtnText}> }
Parrainage >
{referralBalance > 0 ? `${referralBalance.toFixed(2)}` : ""} <Ionicons
</Text> name="gift-outline"
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} /> size={18}
</TouchableOpacity>} color={colors.accent}
/>
<Text style={styles.referralBtnText}>
Parrainage
{referralBalance > 0
? `${referralBalance.toFixed(2)}`
: ""}
</Text>
<Ionicons
name="chevron-forward"
size={16}
color={colors.textMuted}
/>
</TouchableOpacity>
)}
{orders.length > 0 && ( {orders.length > 0 && (
<Text style={styles.sectionTitle}> <Text style={styles.sectionTitle}>
@@ -603,8 +989,14 @@ export default function OrderHistoryScreen() {
<StatusBadge status={order.status} /> <StatusBadge status={order.status} />
) : ( ) : (
<View style={styles.deliveredBadge}> <View style={styles.deliveredBadge}>
<Ionicons name="checkmark-circle" size={12} color="#a78bfa" /> <Ionicons
<Text style={styles.deliveredBadgeText}>Livrée</Text> name="checkmark-circle"
size={12}
color="#a78bfa"
/>
<Text style={styles.deliveredBadgeText}>
Livrée
</Text>
</View> </View>
)} )}
</View> </View>
@@ -642,7 +1034,10 @@ export default function OrderHistoryScreen() {
)} )}
<View style={styles.orderFooter}> <View style={styles.orderFooter}>
<Text style={styles.orderTotal}> <Text style={styles.orderTotal}>
{formatPrice((order.total_prix || 0) - (order.referral_used || 0))} {formatPrice(
(order.total_prix || 0) -
(order.referral_used || 0),
)}
</Text> </Text>
<Ionicons <Ionicons
name="chevron-forward" name="chevron-forward"
@@ -653,6 +1048,110 @@ export default function OrderHistoryScreen() {
</TouchableOpacity> </TouchableOpacity>
)} )}
/> />
{/* Modal sélection produit récompense */}
<Modal
visible={rewardPickerPool !== null}
transparent
animationType="fade"
onRequestClose={() => setRewardPickerPool(null)}
>
<TouchableOpacity
style={styles.modalOverlay}
activeOpacity={1}
onPress={() => setRewardPickerPool(null)}
>
{/* Inner TouchableOpacity blocks tap propagation to the overlay */}
<TouchableOpacity activeOpacity={1} onPress={() => {}}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Ionicons
name="gift-outline"
size={18}
color="#f59e0b"
/>
<Text style={styles.modalTitle}>
Choisir votre récompense
</Text>
<TouchableOpacity
onPress={() => setRewardPickerPool(null)}
>
<Ionicons
name="close"
size={20}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
<Text style={styles.modalSubtitle}>
Sélectionnez le produit que vous souhaitez
recevoir
</Text>
<ScrollView showsVerticalScrollIndicator={false}>
{(
pointsRewards?.reward?.reward_items ?? []
).map((item: RewardItemConfig, idx: number) => (
<TouchableOpacity
key={idx}
style={styles.pickerItem}
onPress={() =>
doClaimReward(
rewardPickerPool!,
item.product_id,
)
}
activeOpacity={0.7}
>
<Ionicons
name="cube-outline"
size={22}
color="#f59e0b"
/>
<View style={{ flex: 1 }}>
<Text style={styles.pickerItemName}>
{item.product_name}
</Text>
{item.quantity > 0 &&
item.quantity !== 1 && (
<Text
style={
styles.pickerItemQty
}
>
×{item.quantity}
</Text>
)}
</View>
{item.price > 0 && (
<Text
style={styles.pickerItemPrice}
>
{item.price}
</Text>
)}
<Ionicons
name="chevron-forward"
size={16}
color={colors.textMuted}
/>
</TouchableOpacity>
))}
</ScrollView>
<TouchableOpacity
style={styles.modalCancelBtn}
onPress={() => setRewardPickerPool(null)}
>
<Text style={styles.modalCancelText}>
Annuler
</Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
</View> </View>
); );
} }
@@ -597,7 +597,7 @@ export default function ProductDetailScreen() {
"Aucune description disponible."} "Aucune description disponible."}
</Text> </Text>
</View> </View>
{hasValidPrices && ( {hasValidPrices && !isComingSoon && (
<View style={styles.stockSection}> <View style={styles.stockSection}>
<Text style={styles.selectorLabel}>Quantite:</Text> <Text style={styles.selectorLabel}>Quantite:</Text>
<TouchableOpacity <TouchableOpacity