client v1/notifs

This commit is contained in:
Med Kamel 2025-04-25 16:29:39 +01:00
parent bb4befc189
commit 3b8e19b211
10 changed files with 659 additions and 69 deletions

View File

@ -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 = () => {
</View>
</View>
<Pressable style={styles.loginButton} onPress={handleLogin}>
<TouchableOpacity
style={[styles.loginButton, loading && { opacity: 0.7 }]}
onPress={handleLogin}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.loginButtonText}>Connexion</Text>
</Pressable>
)}
</TouchableOpacity>
<View style={styles.signupContainer}>
<Text style={styles.signupText}>Nouveau ici? </Text>

View File

@ -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,8 +46,6 @@ export default function CartScreen() {
}));
const handleConfirmOrder = useCallback(async () => {
if (totalAmount <= 0) {
Alert.alert('Panier vide', 'Veuillez ajouter des produits à votre panier');
return;
@ -62,12 +62,34 @@ export default function CartScreen() {
}
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,
@ -75,8 +97,10 @@ export default function CartScreen() {
};
});
// 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 }) => (
<CartItem
@ -169,7 +194,8 @@ export default function CartScreen() {
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#fff' },
container: { flex: 1,
backgroundColor: '#fff' },
header: {
height: 60,
justifyContent: 'center',

View File

@ -0,0 +1,59 @@
import React from 'react';
import { View, StyleSheet,Text,TouchableOpacity } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { router } from "expo-router";
import { ArrowLeft } from 'lucide-react-native';
import OrderList from '@/components/OrderList';
export default function OrderHistoryScreen() {
return (
<SafeAreaProvider>
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
<ArrowLeft size={24} color="#666" />
</TouchableOpacity>
<Text style={styles.headerTitle}>Historique d'achat</Text>
</View>
<View style={styles.content}>
<OrderList />
</View>
</SafeAreaView>
</SafeAreaProvider>
);
}
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,
},
});

View File

@ -1,36 +1,139 @@
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 = () => {
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 }) => (
<View style={styles.profileCard}>
<View style={styles.profileHeader}>
<View style={styles.profileTitle}>
<Feather name="coffee" size={20} color="#B45309" />
<Text style={styles.cafeName}>{cafeName || "Nom De Café"}</Text>
</View>
<View style={styles.badge}>
<Text style={styles.badgeText}>Validé</Text>
</View>
</View>
<View style={styles.profileInfo}>
<Feather name="map-pin" size={16} color="#4B5563" />
<Text style={styles.infoText}>{cafeAddress || "Adresse Du Café"}, {city}</Text>
</View>
<View style={styles.profileInfo}>
<Feather name="phone" size={16} color="#4B5563" />
<Text style={styles.infoText}>+216 {phone || "+216 00 000 000"}</Text>
</View>
</View>
);
const handleSignOut = () => {
Alert.alert(
"Déconnexion",
"Êtes-vous sûr de vouloir vous déconnecter ?",
[
{ text: "Annuler", style: "cancel" },
{
text: "Se déconnecter",
text: "Déconnexion",
style: "destructive",
onPress: async () => {
try {
const auth = getAuth();
await signOut(auth);
router.replace('/'); // Replace with your actual sign-in screen route
router.replace('/');
} catch (error) {
Alert.alert("Erreur", "Impossible de se déconnecter.");
console.error("Sign out error:", error);
}
}
}
]
);
};
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 (
<SafeAreaView style={styles.container}>
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" color="#B45309" />
<Text style={{ marginTop: 12 }}>Chargement des données...</Text>
</View>
</SafeAreaView>
);
}
if (error) {
return (
<SafeAreaView style={styles.container}>
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }}>
<Text style={{ color: 'red', fontSize: 16, textAlign: 'center' }}>{error}</Text>
<TouchableOpacity onPress={() => router.back()} style={{ marginTop: 20 }}>
<Text style={{ color: '#2563EB' }}>Retour</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaProvider>
<SafeAreaView style={styles.container}>
<ScrollView>
<View style={styles.header}>
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
<ArrowLeft size={24} color="#666" />
@ -38,12 +141,25 @@ export default function ProfileScreen() {
<Text style={styles.headerTitle}>Espace Personnel</Text>
</View>
<View style={styles.body}>
<TouchableOpacity style={styles.signOutButton} onPress={handleSignOut}>
<Text style={styles.signOutButtonText}>Se déconnecter</Text>
</TouchableOpacity>
<ProfileCard cafeName={cafeName} cafeAddress={cafeAddress} city={city} phone={phone} />
<View style={styles.section}>
<Text style={styles.sectionTitle}>Commandes</Text>
<MenuItem icon="clock" label="Historique d'achats" onPress={() => router.push('/screens/user/OrderHistoryScreen')} />
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Paramètres</Text>
<MenuItem icon="bell" label="Notifications" badge={2}/>
</View>
<TouchableOpacity style={styles.logoutButton} onPress={handleSignOut}>
<Feather name="log-out" size={20} color="#EF4444" />
<Text style={styles.logoutText}>Déconnexion</Text>
</TouchableOpacity>
</ScrollView>
</SafeAreaView>
</SafeAreaProvider>
);
}
@ -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',
},
});

View File

@ -52,6 +52,12 @@ export default function UserLayout() {
href: null,
}}
/>
<Tabs.Screen
name="OrderHistoryScreen"
options={{
href: null,
}}
/>
</Tabs>
);

70
components/MenuItem.tsx Normal file
View File

@ -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 (
<TouchableOpacity style={styles.menuItem} onPress={onPress}>
<View style={styles.menuItemLeft}>
<Feather name={icon as any} size={20} color="#374151" />
<Text style={styles.menuItemText}>{label}</Text>
</View>
<View style={styles.menuItemRight}>
{badge ? (
<View style={styles.menuBadge}>
<Text style={styles.menuBadgeText}>{badge}</Text>
</View>
) : null}
<Feather name="chevron-right" size={18} color="#9CA3AF" />
</View>
</TouchableOpacity>
);
};
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',
},
});

118
components/OrderItem.tsx Normal file
View File

@ -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 (
<TouchableOpacity
activeOpacity={0.9}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
>
<Animated.View style={[styles.container, animatedStyle]}>
<View style={styles.header}>
{/* Show the orderId */}
<Text style={styles.orderId}>Commande #{order.orderId}</Text>
<View style={[styles.statusContainer, { backgroundColor: statusBackgroundColor }]}>
<Text style={styles.statusText}>{statusText}</Text>
</View>
</View>
<View style={styles.itemsContainer}>
{order.items.map((item, index) => (
<Text key={index} style={styles.item}>
{item.name} x {item.quantity}
</Text>
))}
</View>
<Text style={styles.time}>{order.timeAgo}</Text>
</Animated.View>
</TouchableOpacity>
);
}
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',
},
});

120
components/OrderList.tsx Normal file
View File

@ -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<Order[]>([]);
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 (
<FlatList
data={orders}
keyExtractor={(item) => item.id}
renderItem={({ item, index }) => (
<Animated.View
entering={FadeInUp.delay(index * 100).springify()}
style={styles.itemContainer}
>
<OrderItem order={item} />
</Animated.View>
)}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>Aucune commande trouvée</Text>
</View>
}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
/>
);
}
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,
},
});

View File

@ -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 (

View File

@ -18,3 +18,18 @@ export interface Product {
items: CartItem[];
total: number;
}
export interface Order {
id: string;
orderId:string,
items: OrderItem[];
status: string;
timeAgo: string;
userId: string;
createdAt: Date;
}
export interface OrderItem {
name: string;
quantity: number;
}