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