chore: add parrainage
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo } from "react";
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -6,12 +6,14 @@ import {
|
||||
StyleSheet,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
Switch,
|
||||
TouchableOpacity,
|
||||
} from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useCart } from "../../context/CartContext";
|
||||
import { checkoutCart } from "../../api/api";
|
||||
import { checkoutCart, getReferralBalance } from "../../api/api";
|
||||
import type { ClientStackParamList } from "../../navigation/types";
|
||||
import TextInput from "../../components/ui/TextInput";
|
||||
import Button from "../../components/ui/Button";
|
||||
@@ -36,6 +38,14 @@ export default function CheckoutScreen() {
|
||||
const [confirmationData, setConfirmationData] = useState<any>(null);
|
||||
const [invalidAddressModal, setInvalidAddressModal] = useState(false);
|
||||
const [suggestedAddress, setSuggestedAddress] = useState("");
|
||||
const [referralBalance, setReferralBalance] = useState(0);
|
||||
const [useReferral, setUseReferral] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getReferralBalance().then((res) => {
|
||||
if (res.success && res.balance > 0) setReferralBalance(res.balance);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleCheckout = async () => {
|
||||
if (!nom.trim()) {
|
||||
@@ -67,6 +77,7 @@ export default function CheckoutScreen() {
|
||||
nom.trim(),
|
||||
prenom.trim(),
|
||||
telephone.trim(),
|
||||
useReferral && referralBalance > 0,
|
||||
);
|
||||
if (res.success) {
|
||||
setConfirmationData(res);
|
||||
@@ -158,6 +169,36 @@ export default function CheckoutScreen() {
|
||||
fontSize: fontSize.sm,
|
||||
textAlign: "center",
|
||||
},
|
||||
referralCard: {
|
||||
backgroundColor: colors.bgCard,
|
||||
borderRadius: borderRadius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.accent + "44",
|
||||
padding: spacing.l,
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
referralRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
referralLeft: { flexDirection: "row", alignItems: "center", gap: spacing.s, flex: 1 },
|
||||
referralTitle: {
|
||||
color: colors.textPrimary,
|
||||
fontSize: fontSize.md,
|
||||
fontWeight: fontWeight.semibold,
|
||||
},
|
||||
referralAmount: {
|
||||
color: colors.accent,
|
||||
fontSize: fontSize.sm,
|
||||
marginTop: 2,
|
||||
},
|
||||
referralHint: {
|
||||
color: colors.textMuted,
|
||||
fontSize: fontSize.xs,
|
||||
marginTop: spacing.s,
|
||||
lineHeight: 16,
|
||||
},
|
||||
invalidAddrContent: { gap: spacing.m },
|
||||
invalidAddrIconContainer: { alignItems: "center" },
|
||||
invalidAddrIconCircle: {
|
||||
@@ -372,6 +413,34 @@ export default function CheckoutScreen() {
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Section parrainage — visible uniquement si solde > 0 */}
|
||||
{referralBalance > 0 && (
|
||||
<View style={styles.referralCard}>
|
||||
<View style={styles.referralRow}>
|
||||
<View style={styles.referralLeft}>
|
||||
<Ionicons name="gift-outline" size={22} color={colors.accent} />
|
||||
<View>
|
||||
<Text style={styles.referralTitle}>Credit parrainage</Text>
|
||||
<Text style={styles.referralAmount}>
|
||||
{referralBalance.toFixed(2)} € disponibles
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={useReferral}
|
||||
onValueChange={setUseReferral}
|
||||
trackColor={{ false: colors.border, true: colors.accent + "66" }}
|
||||
thumbColor={useReferral ? colors.accent : colors.textMuted}
|
||||
/>
|
||||
</View>
|
||||
{useReferral && (
|
||||
<Text style={styles.referralHint}>
|
||||
Le credit sera deduit de ta commande. Tu dois quand meme atteindre le minimum de ta zone + le credit utilise.
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{error &&
|
||||
nom.trim() &&
|
||||
prenom.trim() &&
|
||||
@@ -461,6 +530,21 @@ export default function CheckoutScreen() {
|
||||
Votre commande #{confirmationData?.command_id} a ete
|
||||
creee avec succes.
|
||||
</Text>
|
||||
{confirmationData?.referral_used > 0 && (
|
||||
<View style={styles.confirmInfo}>
|
||||
<Ionicons
|
||||
name="gift-outline"
|
||||
size={16}
|
||||
color={colors.accent}
|
||||
/>
|
||||
<Text style={styles.confirmInfoText}>
|
||||
{confirmationData.referral_used.toFixed(2)} € de credit parrainage utilises
|
||||
{confirmationData.referral_balance !== undefined
|
||||
? ` — Solde restant : ${confirmationData.referral_balance.toFixed(2)} €`
|
||||
: ""}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{confirmationData?.assigned_to && (
|
||||
<View style={styles.confirmInfo}>
|
||||
<Ionicons
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getMyCompletedOrders,
|
||||
getMyPenalties,
|
||||
getPublicSettings,
|
||||
getReferralBalance,
|
||||
formatOrderDate,
|
||||
formatPrice,
|
||||
} from "../../api/api";
|
||||
@@ -44,16 +45,18 @@ 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 });
|
||||
const [appSettings, setAppSettings] = useState<PublicSettings>({ penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: true });
|
||||
const [referralBalance, setReferralBalance] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const [histRes, penRes, settings] = await Promise.all([
|
||||
const [histRes, penRes, settings, refRes] = await Promise.all([
|
||||
getMyCompletedOrders(),
|
||||
getMyPenalties(),
|
||||
getPublicSettings(),
|
||||
getReferralBalance(),
|
||||
]);
|
||||
if (histRes.success) {
|
||||
setOrders(histRes.commands || []);
|
||||
@@ -62,6 +65,9 @@ export default function OrderHistoryScreen() {
|
||||
if (penRes.success) {
|
||||
setPenalties(penRes.data || null);
|
||||
}
|
||||
if (refRes.success) {
|
||||
setReferralBalance(refRes.balance);
|
||||
}
|
||||
setAppSettings(settings);
|
||||
} catch {
|
||||
/* ignore */
|
||||
@@ -118,28 +124,6 @@ export default function OrderHistoryScreen() {
|
||||
fontSize: fontSize.xs,
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
penaltyBanner: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: spacing.s,
|
||||
padding: spacing.m,
|
||||
borderRadius: borderRadius.sm,
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
penaltyWarning: {
|
||||
backgroundColor: colors.warning + "22",
|
||||
borderWidth: 1,
|
||||
borderColor: colors.warning + "44",
|
||||
},
|
||||
penaltyCritical: {
|
||||
backgroundColor: colors.danger + "22",
|
||||
borderWidth: 1,
|
||||
borderColor: colors.danger + "44",
|
||||
},
|
||||
penaltyText: {
|
||||
fontSize: fontSize.sm,
|
||||
fontWeight: fontWeight.semibold,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: colors.textSecondary,
|
||||
fontSize: fontSize.sm,
|
||||
@@ -148,6 +132,24 @@ export default function OrderHistoryScreen() {
|
||||
letterSpacing: 1,
|
||||
marginBottom: spacing.m,
|
||||
},
|
||||
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,
|
||||
@@ -303,42 +305,63 @@ export default function OrderHistoryScreen() {
|
||||
</View>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
|
||||
{appSettings.show_amende_score && penaltyCount > 0 && (
|
||||
<View
|
||||
style={[
|
||||
styles.penaltyBanner,
|
||||
penaltyCount >= 3
|
||||
? styles.penaltyCritical
|
||||
: styles.penaltyWarning,
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name="warning"
|
||||
size={20}
|
||||
color={
|
||||
penaltyCount >= 3
|
||||
? colors.danger
|
||||
: colors.warning
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
{appSettings.show_amende_score && (
|
||||
<View
|
||||
style={[
|
||||
styles.penaltyText,
|
||||
{
|
||||
color:
|
||||
styles.statCard,
|
||||
shadows.sm,
|
||||
penaltyCount > 0 && {
|
||||
borderWidth: 1,
|
||||
borderColor:
|
||||
penaltyCount >= 3
|
||||
? colors.danger
|
||||
: colors.warning,
|
||||
? colors.danger + "88"
|
||||
: colors.warning + "88",
|
||||
backgroundColor:
|
||||
penaltyCount >= 3
|
||||
? colors.danger + "18"
|
||||
: colors.warning + "18",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{penaltyCount} penalite
|
||||
{penaltyCount > 1 ? "s" : ""}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<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}>
|
||||
Score amendes
|
||||
</Text>
|
||||
</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>}
|
||||
|
||||
{orders.length > 0 && (
|
||||
<Text style={styles.sectionTitle}>
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Linking,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
import { spacing, borderRadius, fontSize, fontWeight } from "../../theme";
|
||||
import { getReferralBalance } from "../../api/api";
|
||||
|
||||
export default function ParrainageScreen() {
|
||||
const { colors } = useTheme();
|
||||
const [balance, setBalance] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
getReferralBalance().then((res) => {
|
||||
if (res.success) setBalance(res.balance);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
icon: "chatbubble-ellipses-outline" as const,
|
||||
title: "1. Contacte-nous sur Telegram",
|
||||
desc: "Envoie un message à @milieu_nantais en indiquant ton username et le username de la personne que tu as parrainée.",
|
||||
},
|
||||
{
|
||||
icon: "checkmark-circle-outline" as const,
|
||||
title: "2. Validation par l'admin",
|
||||
desc: "L'admin vérifie le parrainage et crédite manuellement un solde sur ton compte.",
|
||||
},
|
||||
{
|
||||
icon: "wallet-outline" as const,
|
||||
title: "3. Crédit disponible",
|
||||
desc: "Le solde apparaît dans ton profil et au moment du paiement. Tu choisis de l'utiliser ou de le cumuler.",
|
||||
},
|
||||
{
|
||||
icon: "cart-outline" as const,
|
||||
title: "4. Utilisation à la commande",
|
||||
desc: "Au checkout, active l'option \"Utiliser mon crédit parrainage\". Le montant sera déduit de ta commande.",
|
||||
},
|
||||
];
|
||||
|
||||
const styles = useMemo(
|
||||
() =>
|
||||
StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: colors.bgPrimary },
|
||||
content: { padding: spacing.xl, paddingBottom: spacing.xxxl },
|
||||
balanceCard: {
|
||||
backgroundColor: colors.accent + "18",
|
||||
borderRadius: borderRadius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.accent + "44",
|
||||
padding: spacing.xl,
|
||||
alignItems: "center",
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
balanceLabel: {
|
||||
color: colors.textMuted,
|
||||
fontSize: fontSize.sm,
|
||||
marginTop: spacing.s,
|
||||
},
|
||||
balanceAmount: {
|
||||
color: colors.accent,
|
||||
fontSize: 36,
|
||||
fontWeight: fontWeight.bold,
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: colors.textPrimary,
|
||||
fontSize: fontSize.lg,
|
||||
fontWeight: fontWeight.bold,
|
||||
marginBottom: spacing.l,
|
||||
},
|
||||
stepCard: {
|
||||
backgroundColor: colors.bgCard,
|
||||
borderRadius: borderRadius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.borderLight,
|
||||
padding: spacing.l,
|
||||
marginBottom: spacing.m,
|
||||
flexDirection: "row",
|
||||
gap: spacing.m,
|
||||
},
|
||||
stepIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.accent + "18",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
},
|
||||
stepContent: { flex: 1 },
|
||||
stepTitle: {
|
||||
color: colors.textPrimary,
|
||||
fontSize: fontSize.md,
|
||||
fontWeight: fontWeight.semibold,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
stepDesc: {
|
||||
color: colors.textSecondary,
|
||||
fontSize: fontSize.sm,
|
||||
lineHeight: 20,
|
||||
},
|
||||
ruleCard: {
|
||||
backgroundColor: colors.warning + "12",
|
||||
borderRadius: borderRadius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.warning + "44",
|
||||
padding: spacing.l,
|
||||
marginTop: spacing.m,
|
||||
marginBottom: spacing.l,
|
||||
},
|
||||
ruleTitle: {
|
||||
color: colors.warning,
|
||||
fontSize: fontSize.md,
|
||||
fontWeight: fontWeight.bold,
|
||||
marginBottom: spacing.s,
|
||||
},
|
||||
ruleText: {
|
||||
color: colors.textSecondary,
|
||||
fontSize: fontSize.sm,
|
||||
lineHeight: 20,
|
||||
},
|
||||
ruleExample: {
|
||||
color: colors.textPrimary,
|
||||
fontSize: fontSize.sm,
|
||||
fontWeight: fontWeight.semibold,
|
||||
marginTop: spacing.s,
|
||||
fontStyle: "italic",
|
||||
},
|
||||
telegramBtn: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: spacing.s,
|
||||
backgroundColor: "#229ED9",
|
||||
borderRadius: borderRadius.md,
|
||||
padding: spacing.l,
|
||||
marginTop: spacing.l,
|
||||
},
|
||||
telegramText: {
|
||||
color: "#fff",
|
||||
fontSize: fontSize.md,
|
||||
fontWeight: fontWeight.bold,
|
||||
},
|
||||
}),
|
||||
[colors],
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
{/* Solde actuel */}
|
||||
<View style={styles.balanceCard}>
|
||||
<Ionicons name="gift-outline" size={32} color={colors.accent} />
|
||||
<Text style={styles.balanceLabel}>Mon solde parrainage</Text>
|
||||
<Text style={styles.balanceAmount}>{balance.toFixed(2)} €</Text>
|
||||
</View>
|
||||
|
||||
{/* Comment ça marche */}
|
||||
<Text style={styles.sectionTitle}>Comment ca marche ?</Text>
|
||||
|
||||
{steps.map((step, i) => (
|
||||
<View key={i} style={styles.stepCard}>
|
||||
<View style={styles.stepIcon}>
|
||||
<Ionicons name={step.icon} size={20} color={colors.accent} />
|
||||
</View>
|
||||
<View style={styles.stepContent}>
|
||||
<Text style={styles.stepTitle}>{step.title}</Text>
|
||||
<Text style={styles.stepDesc}>{step.desc}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* Règle minimum de zone */}
|
||||
<View style={styles.ruleCard}>
|
||||
<Text style={styles.ruleTitle}>⚠️ Règle importante</Text>
|
||||
<Text style={styles.ruleText}>
|
||||
Même avec du crédit parrainage, tu dois toujours payer au minimum le seuil de ta zone de livraison.
|
||||
Le crédit est déduit en plus du montant minimum.
|
||||
</Text>
|
||||
<Text style={styles.ruleExample}>
|
||||
Exemple : crédit 50 € + zone 50 € = commande de 100 € minimum.
|
||||
Tu paies 50 € et le reste est couvert par ton crédit.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Bouton Telegram */}
|
||||
<View
|
||||
style={styles.telegramBtn}
|
||||
// Utiliser TouchableOpacity si besoin d'interaction
|
||||
>
|
||||
<Ionicons name="paper-plane-outline" size={20} color="#fff" />
|
||||
<Text
|
||||
style={styles.telegramText}
|
||||
onPress={() => Linking.openURL("https://t.me/milieu_nantais")}
|
||||
>
|
||||
Contacter @milieu_nantais
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -285,6 +285,11 @@ export default function ProductsScreen() {
|
||||
productId: item.id,
|
||||
})
|
||||
}
|
||||
categoryColor={
|
||||
categories.find(
|
||||
(c) => c.name.toLowerCase() === item.category?.toLowerCase(),
|
||||
)?.color
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user