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 (
"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)
+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;
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,
+635 -136
View File
@@ -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<CompletedOrder[]>([]);
const [stats, setStats] = useState<ClientStats | 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 [pointsRewards, setPointsRewards] = useState<{
enabled: boolean;
@@ -55,19 +75,27 @@ export default function OrderHistoryScreen() {
reward: PointsRewardConfig | null;
} | 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 [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 <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 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() {
</Text>
<Text style={styles.statLabel}>Commandes</Text>
</View>
{appSettings.points_enabled && (
poolNames.length <= 1 ? (
{appSettings.points_enabled &&
(poolNames.length <= 1 ? (
<View style={[styles.statCard, shadows.sm]}>
<Ionicons name="trophy-outline" size={24} color={colors.warning} />
<Text style={styles.statValue}>{poolPoints[0] || 0}</Text>
<Text style={styles.statLabel}>Pts {poolNames[0] ?? 'Points'}</Text>
<Ionicons
name="trophy-outline"
size={24}
color={colors.warning}
/>
<Text style={styles.statValue}>
{poolPoints[0] || 0}
</Text>
<Text style={styles.statLabel}>
Pts {poolNames[0] ?? "Points"}
</Text>
</View>
) : (
<>
{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 (
<View key={i} style={[styles.statCard, 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>
<View
key={i}
style={[
styles.statCard,
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 && (
<View style={[styles.statCard, 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
style={[
styles.statCard,
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>
)}
</>
)
)}
))}
{appSettings.show_amende_score && (
<View
style={[
@@ -446,124 +621,335 @@ export default function OrderHistoryScreen() {
]}
>
<Ionicons
name={penaltyCount > 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
}
/>
<Text
style={[
styles.statValue,
penaltyCount >= 3 && { color: colors.danger },
penaltyCount > 0 && penaltyCount < 3 && { color: colors.warning },
penaltyCount >= 3 && {
color: colors.danger,
},
penaltyCount > 0 &&
penaltyCount < 3 && {
color: colors.warning,
},
]}
>
{penaltyCount}
</Text>
<Text style={styles.statLabel}>
Amende
</Text>
<Text style={styles.statLabel}>Amende</Text>
</View>
)}
</View>
{/* Section récompenses */}
{pointsRewards?.enabled && pointsRewards.reward && pointsRewards.pools.length > 0 && (
<View style={styles.rewardsSection}>
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs, marginBottom: spacing.xs }}>
<Ionicons name="trophy-outline" size={14} color="#f59e0b" />
<Text style={styles.rewardsSectionTitle}>Récompenses</Text>
</View>
{pointsRewards.reward.description !== "" && (
<Text style={styles.rewardsSectionDesc}>{pointsRewards.reward.description}</Text>
)}
{(pointsRewards.reward.reward_items ?? []).filter((it) => it.price > 0 || it.product_name).length > 0 && (
<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 name="gift-outline" size={11} color="#f59e0b" />
<Text style={styles.rewardAmountText}>
{it.product_name}{it.quantity > 0 && it.quantity !== 1 ? ` ×${it.quantity}` : ""}{it.price > 0 ? `${it.price}` : ""}
</Text>
</View>
))}
{pointsRewards?.enabled &&
pointsRewards.reward &&
pointsRewards.pools.length > 0 && (
<View style={styles.rewardsSection}>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: spacing.xs,
marginBottom: spacing.xs,
}}
>
<Ionicons
name="trophy-outline"
size={14}
color="#f59e0b"
/>
<Text
style={styles.rewardsSectionTitle}
>
Récompenses
</Text>
</View>
)}
{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 (
<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>
{pointsRewards.reward.description !==
"" && (
<Text style={styles.rewardsSectionDesc}>
{pointsRewards.reward.description}
</Text>
)}
{(
pointsRewards.reward.reward_items ?? []
).filter(
(it) => it.price > 0 || it.product_name,
).length > 0 && (
<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
name="gift-outline"
size={11}
color="#f59e0b"
/>
<Text
style={
styles.rewardAmountText
}
>
{it.product_name}
{it.quantity > 0 &&
it.quantity !== 1
? ` ×${it.quantity}`
: ""}
{it.price > 0
? `${it.price}`
: ""}
</Text>
</View>
),
)}
</View>
);
})}
</View>
)}
)}
{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 (
<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 */}
{appSettings.referral_enabled && <TouchableOpacity
style={styles.referralBtn}
onPress={() => navigation.navigate("Parrainage")}
>
<Ionicons name="gift-outline" size={18} color={colors.accent} />
<Text style={styles.referralBtnText}>
Parrainage
{referralBalance > 0 ? `${referralBalance.toFixed(2)}` : ""}
</Text>
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
</TouchableOpacity>}
{/* Bouton parrainage */}
{appSettings.referral_enabled && (
<TouchableOpacity
style={styles.referralBtn}
onPress={() =>
navigation.navigate("Parrainage")
}
>
<Ionicons
name="gift-outline"
size={18}
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 && (
<Text style={styles.sectionTitle}>
@@ -603,8 +989,14 @@ export default function OrderHistoryScreen() {
<StatusBadge status={order.status} />
) : (
<View style={styles.deliveredBadge}>
<Ionicons name="checkmark-circle" size={12} color="#a78bfa" />
<Text style={styles.deliveredBadgeText}>Livrée</Text>
<Ionicons
name="checkmark-circle"
size={12}
color="#a78bfa"
/>
<Text style={styles.deliveredBadgeText}>
Livrée
</Text>
</View>
)}
</View>
@@ -642,7 +1034,10 @@ export default function OrderHistoryScreen() {
)}
<View style={styles.orderFooter}>
<Text style={styles.orderTotal}>
{formatPrice((order.total_prix || 0) - (order.referral_used || 0))}
{formatPrice(
(order.total_prix || 0) -
(order.referral_used || 0),
)}
</Text>
<Ionicons
name="chevron-forward"
@@ -653,6 +1048,110 @@ export default function OrderHistoryScreen() {
</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>
);
}
@@ -597,7 +597,7 @@ export default function ProductDetailScreen() {
"Aucune description disponible."}
</Text>
</View>
{hasValidPrices && (
{hasValidPrices && !isComingSoon && (
<View style={styles.stockSection}>
<Text style={styles.selectorLabel}>Quantite:</Text>
<TouchableOpacity