chore: update
This commit is contained in:
@@ -591,6 +591,31 @@ export const getMyPenalties = async (): Promise<PenaltiesResponse> => {
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 👤 PROFIL CLIENT
|
||||
// ============================================
|
||||
|
||||
export const getMyProfile = async (): Promise<{ success: boolean; client?: any; message?: string }> => {
|
||||
try {
|
||||
const { data } = await apiClient.get(`${V1}/profile`);
|
||||
return { success: true, client: data.client };
|
||||
} catch {
|
||||
return { success: false, message: "Erreur récupération profil" };
|
||||
}
|
||||
};
|
||||
|
||||
export const updateMyProfile = async (fields: { nom?: string; prenom?: string; telephone?: string }): Promise<{ success: boolean; message?: string; client?: any }> => {
|
||||
try {
|
||||
const { data } = await apiClient.put(`${V1}/profile/update`, fields);
|
||||
return { success: true, client: data.client, message: data.message };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.response?.data?.error || error.response?.data?.message || "Erreur mise à jour profil",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// ORDER DETAILS
|
||||
// ============================================
|
||||
|
||||
@@ -30,6 +30,7 @@ import ProductDetailScreen from "../screens/client/ProductDetailScreen";
|
||||
import CheckoutScreen from "../screens/client/CheckoutScreen";
|
||||
import OrderDetailsScreen from "../screens/client/OrderDetailsScreen";
|
||||
import ParrainageScreen from "../screens/client/ParrainageScreen";
|
||||
import ProfileScreen from "../screens/client/ProfileScreen";
|
||||
|
||||
const Tab = createBottomTabNavigator<ClientTabParamList>();
|
||||
const Stack = createNativeStackNavigator<ClientStackParamList>();
|
||||
@@ -238,6 +239,20 @@ function ClientTabs() {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Profile"
|
||||
component={ProfileScreen}
|
||||
options={{
|
||||
title: "Profil",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons
|
||||
name="person-outline"
|
||||
size={size}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
|
||||
<Modal
|
||||
|
||||
@@ -23,6 +23,7 @@ export type ClientTabParamList = {
|
||||
Cart: undefined;
|
||||
Tracking: undefined;
|
||||
History: undefined;
|
||||
Profile: undefined;
|
||||
};
|
||||
|
||||
export type ClientStackParamList = {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { useCart } from "../../context/CartContext";
|
||||
import { checkoutCart, getReferralBalance } from "../../api/api";
|
||||
import type { ClientStackParamList } from "../../navigation/types";
|
||||
@@ -45,6 +46,15 @@ export default function CheckoutScreen() {
|
||||
getReferralBalance().then((res) => {
|
||||
if (res.success && res.balance > 0) setReferralBalance(res.balance);
|
||||
});
|
||||
AsyncStorage.multiGet([
|
||||
"profile_default_address",
|
||||
"profile_default_phone",
|
||||
]).then((pairs) => {
|
||||
const savedAddress = pairs[0][1];
|
||||
const savedPhone = pairs[1][1];
|
||||
if (savedAddress) setAddress(savedAddress);
|
||||
if (savedPhone) setTelephone(savedPhone);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleCheckout = async () => {
|
||||
@@ -199,6 +209,12 @@ export default function CheckoutScreen() {
|
||||
marginTop: spacing.s,
|
||||
lineHeight: 16,
|
||||
},
|
||||
prefillHint: {
|
||||
color: "#6ee7b7",
|
||||
fontSize: fontSize.xs,
|
||||
marginTop: 4,
|
||||
opacity: 0.85,
|
||||
},
|
||||
invalidAddrContent: { gap: spacing.m },
|
||||
invalidAddrIconContainer: { alignItems: "center" },
|
||||
invalidAddrIconCircle: {
|
||||
@@ -378,6 +394,11 @@ export default function CheckoutScreen() {
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{!!telephone && (
|
||||
<Text style={styles.prefillHint}>
|
||||
Pre-rempli depuis votre profil
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -411,6 +432,11 @@ export default function CheckoutScreen() {
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
/>
|
||||
{!!address && (
|
||||
<Text style={styles.prefillHint}>
|
||||
Pre-rempli depuis votre profil — modifiez si vous etes ailleurs
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Section parrainage — visible uniquement si solde > 0 */}
|
||||
|
||||
@@ -257,17 +257,19 @@ export default function OrderHistoryScreen() {
|
||||
</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 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={i === 0 ? "leaf-outline" : i === 1 ? "flash-outline" : "star-outline"}
|
||||
size={24}
|
||||
color={i === 0 ? colors.categoryWeedHash : i === 1 ? colors.info : colors.accent}
|
||||
/>
|
||||
<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} />
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
import React, { useState, useCallback, useMemo } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { getMyProfile, updateMyProfile } from "../../api/api";
|
||||
import TextInput from "../../components/ui/TextInput";
|
||||
import Button from "../../components/ui/Button";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
import { spacing, borderRadius, fontSize, fontWeight } from "../../theme";
|
||||
|
||||
const STORAGE_ADDRESS = "profile_default_address";
|
||||
const STORAGE_PHONE = "profile_default_phone";
|
||||
const STORAGE_SIGNAL = "profile_signal_pseudo";
|
||||
|
||||
export default function ProfileScreen() {
|
||||
const { colors } = useTheme();
|
||||
|
||||
// Données compte (backend)
|
||||
const [nom, setNom] = useState("");
|
||||
const [prenom, setPrenom] = useState("");
|
||||
const [telephone, setTelephone] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
|
||||
// Données locales (AsyncStorage)
|
||||
const [defaultAddress, setDefaultAddress] = useState("");
|
||||
const [defaultPhone, setDefaultPhone] = useState("");
|
||||
const [signalPseudo, setSignalPseudo] = useState("");
|
||||
|
||||
const [loadingProfile, setLoadingProfile] = useState(true);
|
||||
const [savingContact, setSavingContact] = useState(false);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoadingProfile(true);
|
||||
const [savedAddress, savedPhone, savedSignal, profileRes] = await Promise.all([
|
||||
AsyncStorage.getItem(STORAGE_ADDRESS),
|
||||
AsyncStorage.getItem(STORAGE_PHONE),
|
||||
AsyncStorage.getItem(STORAGE_SIGNAL),
|
||||
getMyProfile(),
|
||||
]);
|
||||
|
||||
if (savedAddress !== null) setDefaultAddress(savedAddress);
|
||||
if (savedPhone !== null) setDefaultPhone(savedPhone);
|
||||
if (savedSignal !== null) setSignalPseudo(savedSignal);
|
||||
|
||||
if (profileRes.success && profileRes.client) {
|
||||
const c = profileRes.client;
|
||||
setNom(c.nom ?? "");
|
||||
setPrenom(c.prenom ?? "");
|
||||
setTelephone(c.telephone ?? "");
|
||||
setUsername(c.username ?? "");
|
||||
// Init téléphone par défaut si pas encore sauvegardé
|
||||
if (savedPhone === null && c.telephone) {
|
||||
setDefaultPhone(c.telephone);
|
||||
}
|
||||
}
|
||||
setLoadingProfile(false);
|
||||
}, []);
|
||||
|
||||
useFocusEffect(useCallback(() => { loadData(); }, [loadData]));
|
||||
|
||||
const saveLocal = async () => {
|
||||
await Promise.all([
|
||||
AsyncStorage.setItem(STORAGE_ADDRESS, defaultAddress.trim()),
|
||||
AsyncStorage.setItem(STORAGE_PHONE, defaultPhone.trim()),
|
||||
AsyncStorage.setItem(STORAGE_SIGNAL, signalPseudo.trim()),
|
||||
]);
|
||||
Alert.alert("Enregistré", "Informations par défaut sauvegardées");
|
||||
};
|
||||
|
||||
const saveContact = async () => {
|
||||
setSavingContact(true);
|
||||
const res = await updateMyProfile({ nom: nom.trim(), prenom: prenom.trim(), telephone: telephone.trim() });
|
||||
setSavingContact(false);
|
||||
if (res.success) {
|
||||
Alert.alert("Succès", res.message ?? "Profil mis à jour");
|
||||
} else {
|
||||
Alert.alert("Erreur", res.message ?? "Erreur lors de la mise à jour");
|
||||
}
|
||||
};
|
||||
|
||||
const styles = useMemo(() => StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: colors.bgPrimary },
|
||||
content: { padding: spacing.l, paddingBottom: spacing.xxxl },
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: spacing.m,
|
||||
marginBottom: spacing.xl,
|
||||
paddingTop: spacing.m,
|
||||
},
|
||||
avatar: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
backgroundColor: colors.accent,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
username: { color: colors.textPrimary, fontSize: fontSize.lg, fontWeight: fontWeight.bold },
|
||||
usernameLabel: { color: colors.textMuted, fontSize: fontSize.sm },
|
||||
card: {
|
||||
backgroundColor: colors.bgCard,
|
||||
borderRadius: borderRadius.md,
|
||||
padding: spacing.l,
|
||||
marginBottom: spacing.m,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.borderLight,
|
||||
},
|
||||
cardTitle: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: spacing.s,
|
||||
marginBottom: spacing.m,
|
||||
},
|
||||
cardTitleText: {
|
||||
color: colors.textPrimary,
|
||||
fontSize: fontSize.md,
|
||||
fontWeight: fontWeight.semibold,
|
||||
},
|
||||
hint: {
|
||||
color: colors.textMuted,
|
||||
fontSize: fontSize.xs,
|
||||
marginBottom: spacing.m,
|
||||
lineHeight: 18,
|
||||
},
|
||||
row: { flexDirection: "row", gap: spacing.m },
|
||||
half: { flex: 1 },
|
||||
fieldSpacing: { marginTop: spacing.m },
|
||||
saveBtn: {
|
||||
marginTop: spacing.m,
|
||||
backgroundColor: colors.accent,
|
||||
borderRadius: borderRadius.sm,
|
||||
paddingVertical: spacing.m,
|
||||
paddingHorizontal: spacing.l,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: spacing.s,
|
||||
},
|
||||
saveBtnSecondary: {
|
||||
backgroundColor: "transparent",
|
||||
borderWidth: 1,
|
||||
borderColor: colors.accent + "66",
|
||||
},
|
||||
saveBtnText: { color: "#fff", fontSize: fontSize.sm, fontWeight: fontWeight.semibold },
|
||||
saveBtnTextSecondary: { color: colors.accent },
|
||||
}), [colors]);
|
||||
|
||||
if (loadingProfile) {
|
||||
return (
|
||||
<View style={[styles.container, { justifyContent: "center", alignItems: "center" }]}>
|
||||
<Text style={{ color: colors.textMuted }}>Chargement...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView style={styles.container} behavior={Platform.OS === "ios" ? "padding" : undefined}>
|
||||
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
|
||||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.avatar}>
|
||||
<Ionicons name="person" size={28} color="#fff" />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.username}>@{username || "..."}</Text>
|
||||
<Text style={styles.usernameLabel}>Mon compte</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Carte compte */}
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardTitle}>
|
||||
<Ionicons name="person-outline" size={18} color={colors.accent} />
|
||||
<Text style={styles.cardTitleText}>Mon compte</Text>
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<View style={styles.half}>
|
||||
<TextInput
|
||||
placeholder="Prénom"
|
||||
value={prenom}
|
||||
onChangeText={setPrenom}
|
||||
icon={<Ionicons name="person-outline" size={18} color={colors.textMuted} />}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.half}>
|
||||
<TextInput
|
||||
placeholder="Nom"
|
||||
value={nom}
|
||||
onChangeText={setNom}
|
||||
icon={<Ionicons name="person-outline" size={18} color={colors.textMuted} />}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.fieldSpacing}>
|
||||
<TextInput
|
||||
placeholder="Téléphone (compte)"
|
||||
value={telephone}
|
||||
onChangeText={setTelephone}
|
||||
icon={<Ionicons name="call-outline" size={18} color={colors.textMuted} />}
|
||||
keyboardType="phone-pad"
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.saveBtn}
|
||||
onPress={saveContact}
|
||||
disabled={savingContact}
|
||||
>
|
||||
<Ionicons name="save-outline" size={16} color="#fff" />
|
||||
<Text style={styles.saveBtnText}>
|
||||
{savingContact ? "Enregistrement..." : "Enregistrer le compte"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Carte adresse par défaut */}
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardTitle}>
|
||||
<Ionicons name="location-outline" size={18} color="#10b981" />
|
||||
<Text style={styles.cardTitleText}>Adresse par défaut</Text>
|
||||
</View>
|
||||
<Text style={styles.hint}>
|
||||
Sera pré-remplie à la commande. Modifiable si vous n'êtes pas à cette adresse.
|
||||
</Text>
|
||||
<TextInput
|
||||
placeholder="Numéro, rue, ville, code postal"
|
||||
value={defaultAddress}
|
||||
onChangeText={setDefaultAddress}
|
||||
icon={<Ionicons name="location-outline" size={18} color={colors.textMuted} />}
|
||||
multiline
|
||||
numberOfLines={2}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Carte contact livraison */}
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardTitle}>
|
||||
<Ionicons name="call-outline" size={18} color="#3b82f6" />
|
||||
<Text style={styles.cardTitleText}>Contact livraison</Text>
|
||||
</View>
|
||||
<Text style={styles.hint}>
|
||||
Numéro et pseudo Signal utilisés lors de la livraison.
|
||||
</Text>
|
||||
<TextInput
|
||||
placeholder="Téléphone par défaut"
|
||||
value={defaultPhone}
|
||||
onChangeText={setDefaultPhone}
|
||||
icon={<Ionicons name="call-outline" size={18} color={colors.textMuted} />}
|
||||
keyboardType="phone-pad"
|
||||
/>
|
||||
<View style={styles.fieldSpacing}>
|
||||
<TextInput
|
||||
placeholder="Pseudo Signal (optionnel)"
|
||||
value={signalPseudo}
|
||||
onChangeText={setSignalPseudo}
|
||||
icon={<Ionicons name="chatbubble-outline" size={18} color={colors.textMuted} />}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.saveBtn, styles.saveBtnSecondary]}
|
||||
onPress={saveLocal}
|
||||
>
|
||||
<Ionicons name="save-outline" size={16} color={colors.accent} />
|
||||
<Text style={[styles.saveBtnText, styles.saveBtnTextSecondary]}>
|
||||
Enregistrer les infos par défaut
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user