diff --git a/app/screens/auth/SignIn-Screen.tsx b/app/screens/auth/SignIn-Screen.tsx index 60542a4..f1d970a 100644 --- a/app/screens/auth/SignIn-Screen.tsx +++ b/app/screens/auth/SignIn-Screen.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import {View,Text,TextInput,Pressable,StyleSheet,Image,Alert,TouchableOpacity,} from "react-native"; +import {View,Text,TextInput,Pressable,StyleSheet,Image,Alert,TouchableOpacity,ActivityIndicator} from "react-native"; import { Eye, EyeOff,ArrowLeft,Check } from "lucide-react-native"; import { router } from "expo-router"; import { signIn } from "../../../firebase/auth"; // Assure-toi que le chemin est correct @@ -10,6 +10,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; const SignInScreen = () => { + const [loading, setLoading] = useState(false); const [form, setForm] = useState({ email: "", password: "", @@ -43,7 +44,7 @@ const SignInScreen = () => { const handleLogin = async () => { if (!validateForm()) return; - + setLoading(true); try { const { user } = await signIn(form.email, form.password); @@ -57,7 +58,9 @@ const SignInScreen = () => { router.replace("/screens/user/UserHomeScreen"); } catch (error: any) { - Alert.alert("Erreur", error.message); + Alert.alert("Erreur", "Utilisateur Introuvable"); + } finally{ + setLoading(false); } }; @@ -159,9 +162,17 @@ const SignInScreen = () => { - - Connexion - + + {loading ? ( + + ) : ( + Connexion + )} + Nouveau ici? diff --git a/app/screens/user/CartScreen.tsx b/app/screens/user/CartScreen.tsx index ce22018..f3ddd88 100644 --- a/app/screens/user/CartScreen.tsx +++ b/app/screens/user/CartScreen.tsx @@ -7,12 +7,14 @@ import { FlatList, Platform, TouchableOpacity, - Button, + Button } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useCart } from '@/context/cartContext'; import { getAuth } from 'firebase/auth'; import { collection, addDoc } from 'firebase/firestore'; +import { query, orderBy, limit, getDocs } from 'firebase/firestore'; + import { db } from '@/firebase/config'; import CartItem from '@/components/CartItem'; import { ArrowLeft } from 'lucide-react-native'; @@ -44,39 +46,61 @@ export default function CartScreen() { })); const handleConfirmOrder = useCallback(async () => { - - if (totalAmount <= 0) { Alert.alert('Panier vide', 'Veuillez ajouter des produits à votre panier'); return; } - + setIsProcessingOrder(true); buttonScale.value = withSequence( withTiming(0.95, { duration: 100 }), withTiming(1, { duration: 150 }) ); - + if (Platform.OS !== 'web') { Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); } - + try { + const user = auth.currentUser; + if (!user) { + Alert.alert('Utilisateur non connecté', 'Veuillez vous connecter pour passer une commande.'); + return; + } + + // Utilisation de query pour appliquer orderBy et limit + const orderRef = collection(db, 'orders'); + const q = query(orderRef, orderBy('createdAt', 'desc'), limit(1)); + const orderSnapshot = await getDocs(q); + let lastOrderId = 0; + + if (!orderSnapshot.empty) { + const lastOrderDoc = orderSnapshot.docs[0].data(); + lastOrderId = parseInt(lastOrderDoc.orderId.replace(/^0+/, '')) || 0; // Remove leading zeros + } + + const newOrderId = String(lastOrderId + 1).padStart(3, '0'); // Generate the new order ID + const orderData: any = { + orderId: newOrderId, // Use the generated ID + userId: user.uid, status: 'En attente', createdAt: new Date().toISOString(), - totalAmount: totalAmount + totalAmount: totalAmount, }; - + + // Add each item in the cart to the orderData cart.items.forEach((item, index) => { orderData[`item${index + 1}`] = { name: item.name, quantity: item.quantity, }; }); - + + // Add the order to Firestore await addDoc(collection(db, 'orders'), orderData); Alert.alert('Commande confirmée', `Montant Total : ${totalAmount}DT`); + clearCart(); } catch (error) { console.error('Erreur commande :', error); @@ -84,7 +108,8 @@ export default function CartScreen() { } finally { setIsProcessingOrder(false); } - }, [cart, totalAmount]); + }, [cart, totalAmount, auth]); + const renderItem = useCallback(({ item }: { item: CartItemType }) => ( + + + router.back()}> + + + Historique d'achat + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + header: { + height: 60, + justifyContent: 'center', + alignItems: 'center', + position: 'relative', + backgroundColor: '#fff', + borderBottomWidth: 1, + borderBottomColor: '#eee', + marginTop: 5, + }, + backButton: { + position: 'absolute', + left: 20, + top: '50%', + transform: [{ translateY: -12 }], + }, + headerTitle: { + fontSize: 20, + fontWeight: '600', + color: '#333', + }, + content: { + padding: 15, + backgroundColor: '#fff', + paddingBottom: 10, + }, +}); \ No newline at end of file diff --git a/app/screens/user/ProfileScreen.tsx b/app/screens/user/ProfileScreen.tsx index 786f741..e639547 100644 --- a/app/screens/user/ProfileScreen.tsx +++ b/app/screens/user/ProfileScreen.tsx @@ -1,49 +1,165 @@ -import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import React, { useEffect, useState } from 'react'; +import { StyleSheet, View, Text, TouchableOpacity, ScrollView, Alert, ActivityIndicator } from 'react-native'; +import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; +import { Feather } from '@expo/vector-icons'; import { ArrowLeft } from 'lucide-react-native'; import { router } from "expo-router"; -import { getAuth, signOut } from "firebase/auth"; +import MenuItem from '@/components/MenuItem'; -export default function ProfileScreen() { - const handleSignOut = () => { - Alert.alert( - "Déconnexion", - "Êtes-vous sûr de vouloir vous déconnecter ?", - [ - { text: "Annuler", style: "cancel" }, - { - text: "Se déconnecter", - style: "destructive", - onPress: async () => { - try { - const auth = getAuth(); - await signOut(auth); - router.replace('/'); // Replace with your actual sign-in screen route - } catch (error) { - Alert.alert("Erreur", "Impossible de se déconnecter."); - console.error("Sign out error:", error); - } +import { getAuth, signOut } from "firebase/auth"; +import { getFirestore, doc, getDoc, collection, query, where, getDocs } from "firebase/firestore"; + +const ProfileCard = ({ cafeName, cafeAddress, city, phone }: { cafeName: string, cafeAddress: string, city: string, phone: string }) => ( + + + + + {cafeName || "Nom De Café"} + + + Validé + + + + + {cafeAddress || "Adresse Du Café"}, {city} + + + + +216 {phone || "+216 00 000 000"} + + +); + +const handleSignOut = () => { + Alert.alert( + "Déconnexion", + "Êtes-vous sûr de vouloir vous déconnecter ?", + [ + { text: "Annuler", style: "cancel" }, + { + text: "Déconnexion", + style: "destructive", + onPress: async () => { + try { + const auth = getAuth(); + await signOut(auth); + router.replace('/'); + } catch (error) { + Alert.alert("Erreur", "Impossible de se déconnecter."); } } - ] + } + ] + ); +}; + +export default function ProfileScreen() { + const [cafeName, setCafeName] = useState(''); + const [cafeAddress, setCafeAddress] = useState(''); + const [city, setCity] = useState(''); + const [phone, setPhone] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + const fetchProfileData = async () => { + try { + setLoading(true); + const auth = getAuth(); + const db = getFirestore(); + const user = auth.currentUser; + + if (!user) { + setError("Utilisateur non connecté."); + return; + } + + // Fetch téléphone + const userDoc = await getDoc(doc(db, 'users', user.uid)); + if (userDoc.exists()) { + const userData = userDoc.data(); + setPhone(userData.phone || ''); + } else { + setError("Données utilisateur non trouvées."); + } + + // Fetch café + const q = query(collection(db, 'coffee_shops'), where('ownerId', '==', user.uid)); + const querySnapshot = await getDocs(q); + if (!querySnapshot.empty) { + const cafeData = querySnapshot.docs[0].data(); + setCafeName(cafeData.cafeName || ''); + setCafeAddress(cafeData.cafeAddress || ''); + setCity(cafeData.city || ''); + } else { + setError("Aucune information de café trouvée."); + } + + } catch (error) { + setError("Erreur lors du chargement des données."); + } finally { + setLoading(false); + } + }; + + fetchProfileData(); + }, []); + + if (loading) { + return ( + + + + Chargement des données... + + ); - }; + } + + if (error) { + return ( + + + {error} + router.back()} style={{ marginTop: 20 }}> + Retour + + + + ); + } return ( - - - router.back()}> - - - Espace Personnel - + + + + + router.back()}> + + + Espace Personnel + - - - Se déconnecter - - - + + + + Commandes + router.push('/screens/user/OrderHistoryScreen')} /> + + + + Paramètres + + + + + + Déconnexion + + + + ); } @@ -57,7 +173,6 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', position: 'relative', - backgroundColor: '#fff', borderBottomWidth: 1, borderBottomColor: '#eee', marginTop: 5, @@ -73,20 +188,71 @@ const styles = StyleSheet.create({ fontWeight: '600', color: '#333', }, - body: { - flex: 1, - justifyContent: 'center', + profileCard: { + backgroundColor: '#f8f3e9', + borderRadius: 12, + padding: 20, + marginBottom: 32, + marginVertical: 25, + marginHorizontal: 15, + }, + profileHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: 15, + }, + profileTitle: { + flexDirection: 'row', alignItems: 'center', + gap: 8, }, - signOutButton: { - backgroundColor: '#B07B4F', - paddingVertical: 12, - paddingHorizontal: 24, - borderRadius: 10, - }, - signOutButtonText: { - color: '#fff', + cafeName: { fontSize: 16, - fontWeight: '600', + fontWeight: '700', + color: '#1F2937', + }, + badge: { + backgroundColor: '#10B981', + paddingHorizontal: 12, + paddingVertical: 4, + borderRadius: 9999, + }, + badgeText: { + color: 'white', + fontSize: 12, + fontWeight: '500', + }, + profileInfo: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + marginBottom: 8, + }, + infoText: { + fontSize: 14, + color: '#4B5563', + }, + section: { + marginBottom: 16, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '700', + color: '#1F2937', + marginBottom: 8, + marginHorizontal: 15 + }, + logoutButton: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + paddingVertical: 16, + marginHorizontal: 15 + }, + logoutText: { + color: '#EF4444', + fontSize: 15, + fontWeight: '500', }, }); diff --git a/app/screens/user/_layout.tsx b/app/screens/user/_layout.tsx index d299b39..414a52d 100644 --- a/app/screens/user/_layout.tsx +++ b/app/screens/user/_layout.tsx @@ -52,6 +52,12 @@ export default function UserLayout() { href: null, }} /> + ); diff --git a/components/MenuItem.tsx b/components/MenuItem.tsx new file mode 100644 index 0000000..a68b603 --- /dev/null +++ b/components/MenuItem.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; +import { Feather } from '@expo/vector-icons'; + +type MenuItemProps = { + icon: string; + label: string; + badge?: number; + onPress?: () => void; +}; + +const MenuItem = ({ icon, label, badge, onPress }: MenuItemProps) => { + return ( + + + + {label} + + + {badge ? ( + + {badge} + + ) : null} + + + + ); +}; + +export default MenuItem; + +const styles = StyleSheet.create({ + menuItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 16, + borderBottomWidth: 1, + borderBottomColor: '#E5E7EB', + marginHorizontal: 15 + }, + menuItemLeft: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + menuItemText: { + fontSize: 15, + color: '#374151', + }, + menuItemRight: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + menuBadge: { + backgroundColor: '#F59E0B', + width: 20, + height: 20, + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + }, + menuBadgeText: { + color: 'white', + fontSize: 12, + fontWeight: '500', + }, +}); diff --git a/components/OrderItem.tsx b/components/OrderItem.tsx new file mode 100644 index 0000000..479ac21 --- /dev/null +++ b/components/OrderItem.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { Order } from '@/constants/types'; +import Animated, { + useAnimatedStyle, + useSharedValue, + withTiming, + Easing, +} from 'react-native-reanimated'; +import COLORS from '@/constants/colors'; // Make sure you have COLORS imported if not already + +interface OrderItemProps { + order: Order; +} + +export default function OrderItem({ order }: OrderItemProps) { + const scale = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => { + return { + transform: [{ scale: scale.value }], + }; + }); + + const handlePressIn = () => { + scale.value = withTiming(0.98, { + duration: 100, + easing: Easing.bezier(0.25, 0.1, 0.25, 1), + }); + }; + + const handlePressOut = () => { + scale.value = withTiming(1, { + duration: 200, + easing: Easing.bezier(0.25, 0.1, 0.25, 1), + }); + }; + + // Conditional status logic + const statusBackgroundColor = order.status === "Confirmée" ? "#4C7C54" : COLORS.primary; + const statusText = order.status === "Confirmée" ? "Terminée" : order.status; + + return ( + + + + {/* Show the orderId */} + Commande #{order.orderId} + + {statusText} + + + + {order.items.map((item, index) => ( + + {item.name} x {item.quantity} + + ))} + + {order.timeAgo} + + + ); +} + +const styles = StyleSheet.create({ + container: { + backgroundColor: '#F2F2F2', + borderRadius: 12, + padding: 25, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 1, + }, + shadowOpacity: 0.05, + shadowRadius: 3, + elevation: 2, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + orderId: { + fontSize: 16, + fontWeight: '600', + color: '#333333', + }, + statusContainer: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 4, + }, + statusText: { + color: '#FFFFFF', + fontSize: 12, + fontWeight: '500', + }, + itemsContainer: { + marginBottom: 8, + }, + item: { + fontSize: 14, + color: '#666666', + lineHeight: 20, + }, + time: { + fontSize: 12, + color: '#999999', + alignSelf: 'flex-end', + }, +}); diff --git a/components/OrderList.tsx b/components/OrderList.tsx new file mode 100644 index 0000000..634fa1d --- /dev/null +++ b/components/OrderList.tsx @@ -0,0 +1,120 @@ +import React, { useState, useEffect } from 'react'; +import { FlatList, StyleSheet, View, Text, Alert, RefreshControl } from 'react-native'; +import Animated, { FadeInUp } from 'react-native-reanimated'; +import OrderItem from '@/components/OrderItem'; +import { getAuth } from 'firebase/auth'; +import { collection, query, where, getDocs } from 'firebase/firestore'; +import { db } from '@/firebase/config'; +import { Order } from '@/constants/types'; +import COLORS from '@/constants/colors'; + +interface OrderListProps { + // Accept a trigger or prop to refresh data +} + +export default function OrderList() { + const [orders, setOrders] = useState([]); + const [refreshing, setRefreshing] = useState(false); + + const fetchOrders = async () => { + const user = getAuth().currentUser; + if (!user) { + Alert.alert('Erreur', 'Utilisateur non connecté'); + return; + } + + try { + const ordersQuery = query( + collection(db, 'orders'), + where('userId', '==', user.uid) + ); + + const querySnapshot = await getDocs(ordersQuery); + + const ordersData = querySnapshot.docs.map((doc) => { + const data = doc.data(); + const order: Order = { + id: doc.id, + orderId: data.orderId || '', + items: Object.keys(data) + .filter(key => key.startsWith('item')) + .map((key) => ({ + name: data[key].name, + quantity: data[key].quantity, + })), + status: data.status || '', + timeAgo: data.createdAt ? new Date(data.createdAt).toLocaleString() : '', + userId: data.userId || '', + createdAt: data.createdAt ? new Date(data.createdAt.seconds * 1000) : new Date(), // Ensure it is a Date object + }; + return order; + }); + + // Sort by orderId in descending order + const sortedOrders = ordersData.sort((a, b) => + b.orderId.localeCompare(a.orderId) // reverse of localeCompare + ); + + setOrders(sortedOrders); + } catch (error) { + console.error('Erreur lors de la récupération des commandes:', error); + Alert.alert('Erreur', "Impossible de récupérer les commandes."); + } finally { + setRefreshing(false); // stop the refreshing indicator after fetch + } + }; + + const onRefresh = () => { + setRefreshing(true); + fetchOrders(); + }; + + useEffect(() => { + fetchOrders(); + }, []); + + return ( + item.id} + renderItem={({ item, index }) => ( + + + + )} + contentContainerStyle={styles.listContent} + showsVerticalScrollIndicator={false} + ListEmptyComponent={ + + Aucune commande trouvée + + } + refreshControl={ + + } + /> + ); +} + +const styles = StyleSheet.create({ + listContent: { + paddingTop: 16, + paddingBottom: 24, + }, + itemContainer: { + marginBottom: 12, + }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingVertical: 10, + }, + emptyText: { + color: COLORS.text, + fontSize: 18, + }, +}); diff --git a/components/ProductCard.tsx b/components/ProductCard.tsx index fe7dda1..27b41cd 100644 --- a/components/ProductCard.tsx +++ b/components/ProductCard.tsx @@ -22,7 +22,6 @@ export default function ProductCard({ inStock = true, }: ProductCardProps) { const router = useRouter(); - console.log(image); // Log the image URL to ensure it's correct return ( diff --git a/constants/types.ts b/constants/types.ts index 10974ce..d7c6c93 100644 --- a/constants/types.ts +++ b/constants/types.ts @@ -17,4 +17,19 @@ export interface Product { export interface CartState { items: CartItem[]; total: number; - } \ No newline at end of file + } + + export interface Order { + id: string; + orderId:string, + items: OrderItem[]; + status: string; + timeAgo: string; + userId: string; + createdAt: Date; + } + export interface OrderItem { + name: string; + quantity: number; + } + \ No newline at end of file