chore: build
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user