chore: add parrainage
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import React from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
Modal,
|
||||
Text,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
} from "react-native";
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
@@ -7,7 +14,12 @@ import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { useTheme } from "../context/ThemeContext";
|
||||
import { logoutAdmin } from "../api/api_admin";
|
||||
import {
|
||||
logoutAdmin,
|
||||
getAdminNotifications,
|
||||
markAdminNotificationsRead,
|
||||
} from "../api/api_admin";
|
||||
import type { AppNotification } from "../api/api_admin";
|
||||
import { fontSize, spacing } from "../theme";
|
||||
import type { AdminTabParamList, AdminStackParamList } from "./types";
|
||||
|
||||
@@ -30,176 +42,288 @@ function AdminTabs() {
|
||||
const { colors, isDark, toggleTheme } = useTheme();
|
||||
const navigation = useNavigation<NativeStackNavigationProp<AdminStackParamList>>();
|
||||
|
||||
const [notifications, setNotifications] = useState<AppNotification[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const fetchNotifications = useCallback(async () => {
|
||||
try {
|
||||
const res = await getAdminNotifications();
|
||||
setNotifications(res.notifications ?? []);
|
||||
setUnreadCount(res.unread_count ?? 0);
|
||||
} catch { /* ignore */ }
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchNotifications();
|
||||
intervalRef.current = setInterval(fetchNotifications, 15000);
|
||||
return () => {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
};
|
||||
}, [fetchNotifications]);
|
||||
|
||||
const openModal = async () => {
|
||||
setModalVisible(true);
|
||||
if (unreadCount > 0) {
|
||||
try {
|
||||
await markAdminNotificationsRead();
|
||||
setUnreadCount(0);
|
||||
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logoutAdmin();
|
||||
await logout();
|
||||
};
|
||||
|
||||
const formatTime = (iso: string) => {
|
||||
const diff = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
|
||||
if (diff < 1) return "à l'instant";
|
||||
if (diff < 60) return `il y a ${diff} min`;
|
||||
const h = Math.floor(diff / 60);
|
||||
if (h < 24) return `il y a ${h}h`;
|
||||
return `il y a ${Math.floor(h / 24)}j`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: colors.bgSecondary },
|
||||
headerTintColor: colors.textWhite,
|
||||
headerRight: () => (
|
||||
<>
|
||||
<Tab.Navigator
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: colors.bgSecondary },
|
||||
headerTintColor: colors.textWhite,
|
||||
headerRight: () => (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginRight: spacing.l,
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={openModal}
|
||||
style={{ marginRight: spacing.m }}
|
||||
>
|
||||
<View>
|
||||
<Ionicons
|
||||
name="notifications-outline"
|
||||
size={22}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
{unreadCount > 0 && (
|
||||
<View
|
||||
style={[
|
||||
styles.badge,
|
||||
{ backgroundColor: colors.error ?? "#e74c3c" },
|
||||
]}
|
||||
>
|
||||
<Text style={styles.badgeText}>
|
||||
{unreadCount > 9 ? "9+" : unreadCount}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={toggleTheme}
|
||||
style={{ marginRight: spacing.m }}
|
||||
>
|
||||
<Ionicons
|
||||
name={isDark ? "sunny-outline" : "moon-outline"}
|
||||
size={22}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.navigate("Settings")}
|
||||
style={{ marginRight: spacing.m }}
|
||||
>
|
||||
<Ionicons
|
||||
name="settings-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="Dashboard"
|
||||
component={DashboardScreen}
|
||||
options={{
|
||||
title: "Dashboard",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="grid-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Orders"
|
||||
component={OrdersScreen}
|
||||
options={{
|
||||
title: "Commandes",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="receipt-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Users"
|
||||
component={UsersScreen}
|
||||
options={{
|
||||
title: "Clients",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="people-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Products"
|
||||
component={ProductsScreen}
|
||||
options={{
|
||||
title: "Produits",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="cube-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Categories"
|
||||
component={CategoriesScreen}
|
||||
options={{
|
||||
title: "Catégories",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="pricetag-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Delivery"
|
||||
component={DeliveryScreen}
|
||||
options={{
|
||||
title: "Livreurs",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="bicycle-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Alerts"
|
||||
component={AlertsScreen}
|
||||
options={{
|
||||
title: "Alertes",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="alert-circle-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Addresses"
|
||||
component={AddressScreen}
|
||||
options={{
|
||||
title: "Adresses",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="map-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
transparent
|
||||
animationType="slide"
|
||||
onRequestClose={() => setModalVisible(false)}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginRight: spacing.l,
|
||||
}}
|
||||
style={[
|
||||
styles.modalContainer,
|
||||
{ backgroundColor: colors.bgSecondary },
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={toggleTheme}
|
||||
style={{ marginRight: spacing.m }}
|
||||
>
|
||||
<Ionicons
|
||||
name={isDark ? "sunny-outline" : "moon-outline"}
|
||||
size={22}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.navigate("Settings")}
|
||||
style={{ marginRight: spacing.m }}
|
||||
>
|
||||
<Ionicons
|
||||
name="settings-outline"
|
||||
size={22}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={handleLogout}>
|
||||
<Ionicons
|
||||
name="log-out-outline"
|
||||
size={24}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text
|
||||
style={[styles.modalTitle, { color: colors.textWhite }]}
|
||||
>
|
||||
Notifications
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
||||
<Ionicons
|
||||
name="close"
|
||||
size={24}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<ScrollView style={styles.notifList}>
|
||||
{notifications.length === 0 ? (
|
||||
<Text
|
||||
style={[
|
||||
styles.emptyText,
|
||||
{ color: colors.textMuted },
|
||||
]}
|
||||
>
|
||||
Aucune notification
|
||||
</Text>
|
||||
) : (
|
||||
notifications.map((n, i) => (
|
||||
<View
|
||||
key={i}
|
||||
style={[
|
||||
styles.notifItem,
|
||||
{
|
||||
borderLeftColor: n.read
|
||||
? colors.border
|
||||
: colors.accent,
|
||||
backgroundColor: n.read
|
||||
? "transparent"
|
||||
: colors.bgPrimary ?? colors.bgSecondary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.notifMessage,
|
||||
{ color: colors.textWhite },
|
||||
]}
|
||||
>
|
||||
{n.message}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.notifTime,
|
||||
{ color: colors.textMuted },
|
||||
]}
|
||||
>
|
||||
{formatTime(n.created_at)}
|
||||
</Text>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
),
|
||||
tabBarStyle: {
|
||||
backgroundColor: colors.bgSecondary,
|
||||
borderTopColor: colors.border,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
tabBarActiveTintColor: colors.accent,
|
||||
tabBarInactiveTintColor: colors.textMuted,
|
||||
tabBarLabelStyle: { fontSize: fontSize.xs },
|
||||
}}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="Dashboard"
|
||||
component={DashboardScreen}
|
||||
options={{
|
||||
title: "Dashboard",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons
|
||||
name="grid-outline"
|
||||
size={size}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Orders"
|
||||
component={OrdersScreen}
|
||||
options={{
|
||||
title: "Commandes",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons
|
||||
name="receipt-outline"
|
||||
size={size}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Users"
|
||||
component={UsersScreen}
|
||||
options={{
|
||||
title: "Clients",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons
|
||||
name="people-outline"
|
||||
size={size}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Products"
|
||||
component={ProductsScreen}
|
||||
options={{
|
||||
title: "Produits",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons
|
||||
name="cube-outline"
|
||||
size={size}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Categories"
|
||||
component={CategoriesScreen}
|
||||
options={{
|
||||
title: "Catégories",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons
|
||||
name="pricetag-outline"
|
||||
size={size}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Delivery"
|
||||
component={DeliveryScreen}
|
||||
options={{
|
||||
title: "Livreurs",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons
|
||||
name="bicycle-outline"
|
||||
size={size}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Alerts"
|
||||
component={AlertsScreen}
|
||||
options={{
|
||||
title: "Alertes",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons
|
||||
name="alert-circle-outline"
|
||||
size={size}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Addresses"
|
||||
component={AddressScreen}
|
||||
options={{
|
||||
title: "Adresses",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons
|
||||
name="map-outline"
|
||||
size={size}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
</View>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -231,3 +355,68 @@ export default function AdminNavigator() {
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
badge: {
|
||||
position: "absolute",
|
||||
top: -4,
|
||||
right: -6,
|
||||
minWidth: 16,
|
||||
height: 16,
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 2,
|
||||
},
|
||||
badgeText: {
|
||||
color: "#fff",
|
||||
fontSize: 9,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0,0,0,0.5)",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
modalContainer: {
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
maxHeight: "70%",
|
||||
paddingBottom: 32,
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "rgba(255,255,255,0.1)",
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
notifList: {
|
||||
padding: 12,
|
||||
},
|
||||
notifItem: {
|
||||
borderLeftWidth: 3,
|
||||
paddingLeft: 12,
|
||||
paddingVertical: 10,
|
||||
marginBottom: 8,
|
||||
borderRadius: 4,
|
||||
paddingRight: 8,
|
||||
},
|
||||
notifMessage: {
|
||||
fontSize: 14,
|
||||
},
|
||||
notifTime: {
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
emptyText: {
|
||||
textAlign: "center",
|
||||
marginTop: 32,
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user