Files
projet_gestion_commande/mobile/src/navigation/ClientNavigator.tsx
T
2026-03-08 13:57:25 +01:00

418 lines
15 KiB
TypeScript

import React, { useState, useEffect } from "react";
import {
TouchableOpacity,
View,
Text,
Modal,
FlatList,
StyleSheet,
Pressable,
} from "react-native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { useNavigation } from "@react-navigation/native";
import { Ionicons } from "@expo/vector-icons";
import { useCart } from "../context/CartContext";
import { useNotifications } from "../context/NotificationContext";
import { useAuth } from "../auth/AuthContext";
import { useTheme } from "../context/ThemeContext";
import { logoutUser } from "../api/api";
import type { ClientNotification } from "../api/api";
import { fontSize, spacing, borderRadius } from "../theme";
import Toast from "../components/ui/Toast";
import type { ClientTabParamList, ClientStackParamList } from "./types";
import ProductsScreen from "../screens/client/ProductsScreen";
import CartScreen from "../screens/client/CartScreen";
import OrderTrackingScreen from "../screens/client/OrderTrackingScreen";
import OrderHistoryScreen from "../screens/client/OrderHistoryScreen";
import ProductDetailScreen from "../screens/client/ProductDetailScreen";
import CheckoutScreen from "../screens/client/CheckoutScreen";
import OrderDetailsScreen from "../screens/client/OrderDetailsScreen";
import ParrainageScreen from "../screens/client/ParrainageScreen";
const Tab = createBottomTabNavigator<ClientTabParamList>();
const Stack = createNativeStackNavigator<ClientStackParamList>();
function formatNotifDate(dateStr: string): string {
try {
const diffMs = Date.now() - new Date(dateStr).getTime();
const diffMin = Math.floor(diffMs / 60000);
if (diffMin < 1) return "A l'instant";
if (diffMin < 60) return `Il y a ${diffMin} min`;
const diffH = Math.floor(diffMin / 60);
if (diffH < 24) return `Il y a ${diffH}h`;
const diffD = Math.floor(diffH / 24);
if (diffD === 1) return "Hier";
return `Il y a ${diffD} jours`;
} catch {
return "";
}
}
function ClientTabs() {
const { cartCount } = useCart();
const {
notifications,
unreadCount,
markAllRead,
toast,
clearToast,
navigateToOrder,
clearNavigateToOrder,
} = useNotifications();
const { logout } = useAuth();
const { colors, isDark, toggleTheme } = useTheme();
const navigation = useNavigation<any>();
const [notifModalVisible, setNotifModalVisible] = useState(false);
useEffect(() => {
if (navigateToOrder) {
setNotifModalVisible(false);
navigation.navigate("Tracking");
clearNavigateToOrder();
}
}, [navigateToOrder, navigation, clearNavigateToOrder]);
const handleLogout = async () => {
await logoutUser();
await logout();
};
const openNotifModal = async () => {
setNotifModalVisible(true);
if (unreadCount > 0) {
await markAllRead();
}
};
const renderNotifItem = ({ item }: { item: ClientNotification }) => {
if (!item) return null;
return (
<View
style={[
styles.notifItem,
{
borderBottomColor: colors.border,
borderLeftColor: item.read
? "transparent"
: colors.accent,
backgroundColor: item.read
? "transparent"
: colors.accent + "10",
},
]}
>
<Text
style={[styles.notifMessage, { color: colors.textPrimary }]}
>
{item.message || "Notification"}
</Text>
<Text style={[styles.notifDate, { color: colors.textMuted }]}>
{item.created_at ? formatNotifDate(item.created_at) : ""}
</Text>
</View>
);
};
return (
<>
<Tab.Navigator
screenOptions={{
headerStyle: { backgroundColor: colors.bgSecondary },
headerTintColor: colors.textWhite,
headerRight: () => (
<View
style={{
flexDirection: "row",
alignItems: "center",
marginRight: spacing.l,
gap: spacing.m,
}}
>
<TouchableOpacity
onPress={openNotifModal}
style={{ position: "relative" }}
>
<Ionicons
name="notifications-outline"
size={24}
color={colors.textSecondary}
/>
{unreadCount > 0 && (
<View style={styles.badge}>
<Text style={styles.badgeText}>
{unreadCount > 9
? "9+"
: unreadCount}
</Text>
</View>
)}
</TouchableOpacity>
<TouchableOpacity onPress={toggleTheme}>
<Ionicons
name={
isDark
? "sunny-outline"
: "moon-outline"
}
size={22}
color={colors.textSecondary}
/>
</TouchableOpacity>
<TouchableOpacity onPress={handleLogout}>
<Ionicons
name="log-out-outline"
size={24}
color={colors.textSecondary}
/>
</TouchableOpacity>
</View>
),
tabBarStyle: {
backgroundColor: colors.bgSecondary,
borderTopColor: colors.border,
borderTopWidth: 1,
},
tabBarActiveTintColor: colors.accent,
tabBarInactiveTintColor: colors.textMuted,
tabBarLabelStyle: { fontSize: fontSize.xs },
}}
>
<Tab.Screen
name="Products"
component={ProductsScreen}
options={{
title: "Produits",
tabBarIcon: ({ color, size }) => (
<Ionicons
name="leaf-outline"
size={size}
color={color}
/>
),
}}
/>
<Tab.Screen
name="Cart"
component={CartScreen}
options={{
title: "Panier",
tabBarIcon: ({ color, size }) => (
<Ionicons
name="cart-outline"
size={size}
color={color}
/>
),
tabBarBadge: cartCount > 0 ? cartCount : undefined,
tabBarBadgeStyle: { backgroundColor: colors.accent },
}}
/>
<Tab.Screen
name="Tracking"
component={OrderTrackingScreen}
options={{
title: "Suivi",
tabBarIcon: ({ color, size }) => (
<Ionicons
name="navigate-outline"
size={size}
color={color}
/>
),
}}
/>
<Tab.Screen
name="History"
component={OrderHistoryScreen}
options={{
title: "Historique",
tabBarIcon: ({ color, size }) => (
<Ionicons
name="time-outline"
size={size}
color={color}
/>
),
}}
/>
</Tab.Navigator>
<Modal
visible={notifModalVisible}
animationType="slide"
transparent={true}
onRequestClose={() => setNotifModalVisible(false)}
>
<View
style={[
styles.modalOverlay,
{ backgroundColor: "rgba(0,0,0,0.5)" },
]}
>
<View
style={[
styles.modalContent,
{ backgroundColor: colors.bgPrimary },
]}
>
<View
style={[
styles.modalHeader,
{ borderBottomColor: colors.border },
]}
>
<Text
style={[
styles.modalTitle,
{ color: colors.textPrimary },
]}
>
Notifications
</Text>
<Pressable
onPress={() => setNotifModalVisible(false)}
>
<Ionicons
name="close"
size={24}
color={colors.textSecondary}
/>
</Pressable>
</View>
<FlatList
data={notifications || []}
keyExtractor={(item, index) => {
if (
item?.command_id &&
item?.type &&
item?.created_at
) {
return `${item.command_id}-${item.type}-${item.created_at}`;
}
return `notif-${index}`;
}}
renderItem={renderNotifItem}
ListEmptyComponent={
<Text
style={[
styles.emptyText,
{ color: colors.textMuted },
]}
>
Aucune notification
</Text>
}
/>
</View>
</View>
</Modal>
<Toast
message={toast?.message || ""}
type={toast?.type || "info"}
visible={!!toast}
onHide={clearToast}
duration={5000}
/>
</>
);
}
export default function ClientNavigator() {
const { colors } = useTheme();
return (
<Stack.Navigator
screenOptions={{
headerStyle: { backgroundColor: colors.bgSecondary },
headerTintColor: colors.textWhite,
}}
>
<Stack.Screen
name="ClientTabs"
component={ClientTabs}
options={{ headerShown: false }}
/>
<Stack.Screen
name="ProductDetail"
component={ProductDetailScreen}
options={{ title: "Detail produit" }}
/>
<Stack.Screen
name="Checkout"
component={CheckoutScreen}
options={{ title: "Validation commande" }}
/>
<Stack.Screen
name="OrderDetails"
component={OrderDetailsScreen}
options={{ title: "Detail commande" }}
/>
<Stack.Screen
name="Parrainage"
component={ParrainageScreen}
options={{ title: "Parrainage" }}
/>
</Stack.Navigator>
);
}
const styles = StyleSheet.create({
notifItem: {
padding: spacing.m,
borderBottomWidth: 1,
borderLeftWidth: 3,
},
notifMessage: {
fontSize: fontSize.sm,
marginBottom: spacing.xs,
},
notifDate: {
fontSize: fontSize.xs,
},
badge: {
position: "absolute",
top: -4,
right: -4,
backgroundColor: "#FF3B30",
borderRadius: 10,
minWidth: 18,
height: 18,
justifyContent: "center",
alignItems: "center",
},
badgeText: {
color: "#fff",
fontSize: 10,
fontWeight: "bold",
},
modalOverlay: {
flex: 1,
justifyContent: "flex-end",
},
modalContent: {
height: "70%",
borderTopLeftRadius: borderRadius.lg,
borderTopRightRadius: borderRadius.lg,
},
modalHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: spacing.l,
borderBottomWidth: 1,
},
modalTitle: {
fontSize: fontSize.lg,
fontWeight: "bold",
},
emptyText: {
textAlign: "center",
marginTop: spacing.xl,
fontSize: fontSize.sm,
},
});