1163 lines
55 KiB
TypeScript
1163 lines
55 KiB
TypeScript
import React, { useState, useCallback, useMemo } from "react";
|
||
import {
|
||
View,
|
||
Text,
|
||
FlatList,
|
||
TouchableOpacity,
|
||
StyleSheet,
|
||
RefreshControl,
|
||
Modal,
|
||
ScrollView,
|
||
} from "react-native";
|
||
import { useNavigation, useFocusEffect } from "@react-navigation/native";
|
||
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||
import { Ionicons } from "@expo/vector-icons";
|
||
import {
|
||
getMyCompletedOrders,
|
||
getMyPenalties,
|
||
getPublicSettings,
|
||
getReferralBalance,
|
||
getMyPointsRewards,
|
||
claimMyReward,
|
||
formatOrderDate,
|
||
formatPrice,
|
||
} from "../../api/api";
|
||
import type {
|
||
PublicSettings,
|
||
PointsPoolInfo,
|
||
PointsRewardConfig,
|
||
RewardItemConfig,
|
||
} from "../../api/api";
|
||
import type {
|
||
CompletedOrder,
|
||
ClientStats,
|
||
PenaltyInfo,
|
||
} from "../../api/api_types";
|
||
import type { ClientStackParamList } from "../../navigation/types";
|
||
import StatusBadge from "../../components/StatusBadge";
|
||
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
||
import Card from "../../components/ui/Card";
|
||
import { useTheme } from "../../context/ThemeContext";
|
||
import {
|
||
spacing,
|
||
borderRadius,
|
||
fontSize,
|
||
fontWeight,
|
||
shadows,
|
||
} from "../../theme";
|
||
|
||
type Nav = NativeStackNavigationProp<ClientStackParamList>;
|
||
|
||
export default function OrderHistoryScreen() {
|
||
const navigation = useNavigation<Nav>();
|
||
const { colors } = useTheme();
|
||
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: "",
|
||
client_color_primary: "",
|
||
client_color_secondary: "",
|
||
client_color_success: "",
|
||
client_color_danger: "",
|
||
client_color_warning: "",
|
||
});
|
||
const [referralBalance, setReferralBalance] = useState(0);
|
||
const [pointsRewards, setPointsRewards] = useState<{
|
||
enabled: boolean;
|
||
pools: PointsPoolInfo[];
|
||
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 [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(),
|
||
]);
|
||
if (histRes.success) {
|
||
setOrders(histRes.commands || []);
|
||
setStats(histRes.client_stats || null);
|
||
}
|
||
if (penRes.success) {
|
||
setPenalties(penRes.data || null);
|
||
}
|
||
if (refRes.success) {
|
||
setReferralBalance(refRes.balance);
|
||
}
|
||
setAppSettings(settings);
|
||
if (rewardsRes.success && rewardsRes.enabled) {
|
||
setPointsRewards(rewardsRes);
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
} finally {
|
||
setLoading(false);
|
||
setRefreshing(false);
|
||
}
|
||
}, []);
|
||
|
||
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, 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 !";
|
||
setClaimFeedback({ pool: poolKey, type: "success", text });
|
||
getMyPointsRewards().then((r) => {
|
||
if (r.success) setPointsRewards(r);
|
||
});
|
||
} else {
|
||
setClaimFeedback({
|
||
pool: poolKey,
|
||
type: "error",
|
||
text: res.error || "Erreur",
|
||
});
|
||
}
|
||
};
|
||
|
||
useFocusEffect(
|
||
useCallback(() => {
|
||
setLoading(true);
|
||
fetchData();
|
||
}, [fetchData]),
|
||
);
|
||
|
||
const onRefresh = () => {
|
||
setRefreshing(true);
|
||
fetchData();
|
||
};
|
||
|
||
const styles = useMemo(
|
||
() =>
|
||
StyleSheet.create({
|
||
container: { flex: 1, backgroundColor: colors.bgPrimary },
|
||
listContent: {
|
||
padding: spacing.l,
|
||
paddingBottom: spacing.xxxl,
|
||
},
|
||
statsGrid: {
|
||
flexDirection: "row",
|
||
flexWrap: "wrap",
|
||
gap: spacing.m,
|
||
marginBottom: spacing.xl,
|
||
},
|
||
statCard: {
|
||
backgroundColor: colors.bgCard,
|
||
borderRadius: borderRadius.md,
|
||
padding: spacing.l,
|
||
alignItems: "center",
|
||
flex: 1,
|
||
minWidth: "45%",
|
||
borderWidth: 1,
|
||
borderColor: colors.borderLight,
|
||
},
|
||
statValue: {
|
||
color: colors.textWhite,
|
||
fontSize: fontSize.xxl,
|
||
fontWeight: fontWeight.bold,
|
||
marginTop: spacing.s,
|
||
},
|
||
statLabel: {
|
||
color: colors.textMuted,
|
||
fontSize: fontSize.xs,
|
||
marginTop: spacing.xs,
|
||
},
|
||
sectionTitle: {
|
||
color: colors.textSecondary,
|
||
fontSize: fontSize.sm,
|
||
fontWeight: fontWeight.medium,
|
||
textTransform: "uppercase",
|
||
letterSpacing: 1,
|
||
marginBottom: spacing.m,
|
||
},
|
||
rewardsSection: {
|
||
backgroundColor: colors.bgCard,
|
||
borderRadius: borderRadius.md,
|
||
borderWidth: 1,
|
||
borderColor: "#f59e0b44",
|
||
padding: spacing.m,
|
||
marginBottom: spacing.l,
|
||
},
|
||
rewardsSectionTitle: {
|
||
color: "#f59e0b",
|
||
fontSize: fontSize.xs,
|
||
fontWeight: fontWeight.bold,
|
||
textTransform: "uppercase",
|
||
letterSpacing: 0.8,
|
||
},
|
||
rewardsSectionDesc: {
|
||
color: colors.textMuted,
|
||
fontSize: fontSize.xs,
|
||
fontStyle: "italic",
|
||
marginBottom: spacing.m,
|
||
},
|
||
rewardPoolCard: {
|
||
backgroundColor: "rgba(245,158,11,0.07)",
|
||
borderRadius: borderRadius.sm,
|
||
borderWidth: 1,
|
||
borderColor: "rgba(245,158,11,0.2)",
|
||
padding: spacing.m,
|
||
marginTop: spacing.s,
|
||
},
|
||
rewardPoolName: {
|
||
color: colors.textPrimary,
|
||
fontSize: fontSize.sm,
|
||
fontWeight: fontWeight.semibold,
|
||
},
|
||
rewardPoolPts: {
|
||
color: "#f59e0b",
|
||
fontSize: fontSize.sm,
|
||
fontWeight: fontWeight.bold,
|
||
},
|
||
rewardAmountBadge: {
|
||
backgroundColor: "rgba(16,185,129,0.15)",
|
||
borderRadius: borderRadius.sm,
|
||
paddingHorizontal: spacing.s,
|
||
paddingVertical: 2,
|
||
flexDirection: "row" as const,
|
||
},
|
||
rewardAmountText: {
|
||
color: "#10b981",
|
||
fontSize: fontSize.xs,
|
||
fontWeight: fontWeight.bold,
|
||
},
|
||
progressBarBg: {
|
||
height: 5,
|
||
backgroundColor: "rgba(245,158,11,0.15)",
|
||
borderRadius: 3,
|
||
overflow: "hidden",
|
||
marginVertical: spacing.xs,
|
||
},
|
||
progressBarFill: {
|
||
height: "100%",
|
||
backgroundColor: "#f59e0b",
|
||
borderRadius: 3,
|
||
},
|
||
rewardAvailable: {
|
||
color: "#10b981",
|
||
fontSize: fontSize.xs,
|
||
fontWeight: fontWeight.semibold,
|
||
marginBottom: spacing.s,
|
||
},
|
||
rewardRemaining: {
|
||
color: colors.textMuted,
|
||
fontSize: fontSize.xs,
|
||
marginBottom: spacing.s,
|
||
},
|
||
feedbackSuccess: {
|
||
color: "#10b981",
|
||
fontSize: fontSize.xs,
|
||
fontStyle: "italic",
|
||
marginBottom: spacing.xs,
|
||
},
|
||
feedbackError: {
|
||
color: "#ef4444",
|
||
fontSize: fontSize.xs,
|
||
fontStyle: "italic",
|
||
marginBottom: spacing.xs,
|
||
},
|
||
claimBtn: {
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
gap: spacing.xs,
|
||
backgroundColor: "#f59e0b",
|
||
borderRadius: borderRadius.sm,
|
||
paddingVertical: spacing.s,
|
||
paddingHorizontal: spacing.m,
|
||
},
|
||
claimBtnText: {
|
||
color: "#fff",
|
||
fontSize: fontSize.sm,
|
||
fontWeight: fontWeight.bold,
|
||
},
|
||
referralBtn: {
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
gap: spacing.s,
|
||
backgroundColor: colors.bgCard,
|
||
borderRadius: borderRadius.sm,
|
||
borderWidth: 1,
|
||
borderColor: colors.accent + "44",
|
||
paddingVertical: spacing.m,
|
||
paddingHorizontal: spacing.l,
|
||
marginBottom: spacing.l,
|
||
},
|
||
referralBtnText: {
|
||
color: colors.accent,
|
||
fontSize: fontSize.sm,
|
||
fontWeight: fontWeight.semibold,
|
||
flex: 1,
|
||
},
|
||
emptyContainer: {
|
||
alignItems: "center",
|
||
paddingTop: spacing.xxxl,
|
||
},
|
||
emptyTitle: {
|
||
color: colors.textPrimary,
|
||
fontSize: fontSize.xl,
|
||
fontWeight: fontWeight.bold,
|
||
marginTop: spacing.l,
|
||
},
|
||
emptySubtitle: {
|
||
color: colors.textMuted,
|
||
fontSize: fontSize.md,
|
||
marginTop: spacing.s,
|
||
},
|
||
orderCard: {
|
||
backgroundColor: colors.bgCard,
|
||
borderRadius: borderRadius.md,
|
||
padding: spacing.l,
|
||
marginBottom: spacing.m,
|
||
borderWidth: 1,
|
||
borderColor: colors.borderLight,
|
||
},
|
||
orderHeader: {
|
||
flexDirection: "row",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
marginBottom: spacing.m,
|
||
},
|
||
orderIdText: {
|
||
color: colors.textWhite,
|
||
fontSize: fontSize.md,
|
||
fontWeight: fontWeight.semibold,
|
||
},
|
||
orderRow: {
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
marginBottom: spacing.xs,
|
||
},
|
||
orderDetail: {
|
||
color: colors.textSecondary,
|
||
fontSize: fontSize.sm,
|
||
marginLeft: spacing.s,
|
||
flex: 1,
|
||
},
|
||
orderFooter: {
|
||
flexDirection: "row",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
marginTop: spacing.m,
|
||
paddingTop: spacing.m,
|
||
borderTopWidth: 1,
|
||
borderTopColor: colors.borderLight,
|
||
},
|
||
orderTotal: {
|
||
color: colors.success,
|
||
fontSize: fontSize.lg,
|
||
fontWeight: fontWeight.bold,
|
||
},
|
||
deliveredBadge: {
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
paddingHorizontal: spacing.m,
|
||
paddingVertical: spacing.xs,
|
||
borderRadius: borderRadius.xl,
|
||
borderWidth: 1.5,
|
||
borderColor: "#7c3aed",
|
||
},
|
||
deliveredBadgeText: {
|
||
color: "#a78bfa",
|
||
fontSize: fontSize.xs,
|
||
fontWeight: fontWeight.bold,
|
||
textTransform: "uppercase",
|
||
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],
|
||
);
|
||
|
||
if (loading && !refreshing)
|
||
return <LoadingSpinner message="Chargement historique..." />;
|
||
|
||
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;
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
<FlatList
|
||
data={orders}
|
||
keyExtractor={(item) => String(item.id)}
|
||
refreshControl={
|
||
<RefreshControl
|
||
refreshing={refreshing}
|
||
onRefresh={onRefresh}
|
||
tintColor={colors.accent}
|
||
/>
|
||
}
|
||
contentContainerStyle={styles.listContent}
|
||
ListHeaderComponent={
|
||
<View>
|
||
<View style={styles.statsGrid}>
|
||
<View style={[styles.statCard, shadows.sm]}>
|
||
<Ionicons
|
||
name="receipt-outline"
|
||
size={24}
|
||
color={colors.accent}
|
||
/>
|
||
<Text style={styles.statValue}>
|
||
{stats?.total_commands || orders.length}
|
||
</Text>
|
||
<Text style={styles.statLabel}>Commandes</Text>
|
||
</View>
|
||
{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>
|
||
</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;
|
||
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>
|
||
);
|
||
})}
|
||
{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>
|
||
)}
|
||
</>
|
||
))}
|
||
{appSettings.show_amende_score && (
|
||
<View
|
||
style={[
|
||
styles.statCard,
|
||
shadows.sm,
|
||
penaltyCount > 0 && {
|
||
borderWidth: 1,
|
||
borderColor:
|
||
penaltyCount >= 3
|
||
? colors.danger + "88"
|
||
: colors.warning + "88",
|
||
backgroundColor:
|
||
penaltyCount >= 3
|
||
? colors.danger + "18"
|
||
: colors.warning + "18",
|
||
},
|
||
]}
|
||
>
|
||
<Ionicons
|
||
name={
|
||
penaltyCount > 0
|
||
? "warning"
|
||
: "shield-checkmark-outline"
|
||
}
|
||
size={24}
|
||
color={
|
||
penaltyCount >= 3
|
||
? colors.danger
|
||
: penaltyCount > 0
|
||
? colors.warning
|
||
: colors.textMuted
|
||
}
|
||
/>
|
||
<Text
|
||
style={[
|
||
styles.statValue,
|
||
penaltyCount >= 3 && {
|
||
color: colors.danger,
|
||
},
|
||
penaltyCount > 0 &&
|
||
penaltyCount < 3 && {
|
||
color: colors.warning,
|
||
},
|
||
]}
|
||
>
|
||
{penaltyCount} €
|
||
</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>
|
||
),
|
||
)}
|
||
</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 */}
|
||
{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}>
|
||
Historique des commandes
|
||
</Text>
|
||
)}
|
||
</View>
|
||
}
|
||
ListEmptyComponent={
|
||
<View style={styles.emptyContainer}>
|
||
<Ionicons
|
||
name="time-outline"
|
||
size={80}
|
||
color={colors.textMuted}
|
||
/>
|
||
<Text style={styles.emptyTitle}>Aucun historique</Text>
|
||
<Text style={styles.emptySubtitle}>
|
||
Vos commandes terminees apparaitront ici
|
||
</Text>
|
||
</View>
|
||
}
|
||
renderItem={({ item: order }) => (
|
||
<TouchableOpacity
|
||
activeOpacity={0.7}
|
||
onPress={() =>
|
||
navigation.navigate("OrderDetails", {
|
||
orderId: order.id,
|
||
})
|
||
}
|
||
style={[styles.orderCard, shadows.sm]}
|
||
>
|
||
<View style={styles.orderHeader}>
|
||
<Text style={styles.orderIdText}>
|
||
Commande #{order.client_order_number}
|
||
</Text>
|
||
{order.status === "cancelled" ? (
|
||
<StatusBadge status={order.status} />
|
||
) : (
|
||
<View style={styles.deliveredBadge}>
|
||
<Ionicons
|
||
name="checkmark-circle"
|
||
size={12}
|
||
color="#a78bfa"
|
||
/>
|
||
<Text style={styles.deliveredBadgeText}>
|
||
Livrée
|
||
</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
<View style={styles.orderRow}>
|
||
<Ionicons
|
||
name="location-outline"
|
||
size={14}
|
||
color={colors.textMuted}
|
||
/>
|
||
<Text style={styles.orderDetail} numberOfLines={1}>
|
||
{order.adresse || "N/A"}
|
||
</Text>
|
||
</View>
|
||
<View style={styles.orderRow}>
|
||
<Ionicons
|
||
name="time-outline"
|
||
size={14}
|
||
color={colors.textMuted}
|
||
/>
|
||
<Text style={styles.orderDetail}>
|
||
{formatOrderDate(order.created_at)}
|
||
</Text>
|
||
</View>
|
||
{order.livreur_assign && (
|
||
<View style={styles.orderRow}>
|
||
<Ionicons
|
||
name="bicycle-outline"
|
||
size={14}
|
||
color={colors.textMuted}
|
||
/>
|
||
<Text style={styles.orderDetail}>
|
||
{order.livreur_assign}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
<View style={styles.orderFooter}>
|
||
<Text style={styles.orderTotal}>
|
||
{formatPrice(
|
||
(order.total_prix || 0) -
|
||
(order.referral_used || 0),
|
||
)}
|
||
</Text>
|
||
<Ionicons
|
||
name="chevron-forward"
|
||
size={18}
|
||
color={colors.textMuted}
|
||
/>
|
||
</View>
|
||
</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>
|
||
);
|
||
}
|