351 lines
13 KiB
TypeScript
351 lines
13 KiB
TypeScript
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
import {
|
|
TouchableOpacity,
|
|
View,
|
|
Text,
|
|
Modal,
|
|
ScrollView,
|
|
StyleSheet,
|
|
} from "react-native";
|
|
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { useAuth } from "../auth/AuthContext";
|
|
import { useTheme } from "../context/ThemeContext";
|
|
import { logoutAdmin } from "../api/api_admin";
|
|
import {
|
|
getLivreurNotifications,
|
|
markLivreurNotificationsRead,
|
|
} from "../api/api_delivery";
|
|
import type { LivreurNotification } from "../api/api_delivery";
|
|
import { fontSize, spacing } from "../theme";
|
|
import type { DeliveryTabParamList } from "./types";
|
|
|
|
import DashboardScreen from "../screens/delivery/DashboardScreen";
|
|
import StatsScreen from "../screens/delivery/StatsScreen";
|
|
import AlertsScreen from "../screens/delivery/AlertsScreen";
|
|
|
|
const Tab = createBottomTabNavigator<DeliveryTabParamList>();
|
|
|
|
function formatNotifTime(dateStr: string): string {
|
|
try {
|
|
const diffMs = Date.now() - new Date(dateStr).getTime();
|
|
const diffMin = Math.floor(diffMs / 60000);
|
|
if (diffMin < 1) return "À 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`;
|
|
return `Il y a ${Math.floor(diffH / 24)} jours`;
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
export default function DeliveryNavigator() {
|
|
const { logout } = useAuth();
|
|
const { colors, isDark, toggleTheme } = useTheme();
|
|
|
|
const [notifications, setNotifications] = useState<LivreurNotification[]>([]);
|
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
const [showModal, setShowModal] = useState(false);
|
|
const seenIdsRef = useRef<Set<string>>(new Set());
|
|
const isFirstLoad = useRef(true);
|
|
|
|
const fetchNotifications = useCallback(async () => {
|
|
const res = await getLivreurNotifications();
|
|
if (!res.success || !res.notifications) return;
|
|
setNotifications(res.notifications);
|
|
|
|
if (!isFirstLoad.current) {
|
|
let newUnread = 0;
|
|
for (const n of res.notifications) {
|
|
if (!n.read) {
|
|
const key = `${n.command_id}-${n.type}-${n.created_at}`;
|
|
if (!seenIdsRef.current.has(key)) {
|
|
seenIdsRef.current.add(key);
|
|
newUnread++;
|
|
}
|
|
}
|
|
}
|
|
if (newUnread > 0) {
|
|
setUnreadCount((prev) => prev + newUnread);
|
|
}
|
|
} else {
|
|
for (const n of res.notifications) {
|
|
const key = `${n.command_id}-${n.type}-${n.created_at}`;
|
|
seenIdsRef.current.add(key);
|
|
}
|
|
setUnreadCount(res.unread_count ?? 0);
|
|
isFirstLoad.current = false;
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetchNotifications();
|
|
const interval = setInterval(fetchNotifications, 15000);
|
|
return () => clearInterval(interval);
|
|
}, [fetchNotifications]);
|
|
|
|
const openModal = async () => {
|
|
setShowModal(true);
|
|
if (unreadCount > 0) {
|
|
await markLivreurNotificationsRead();
|
|
setUnreadCount(0);
|
|
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
|
|
}
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
await logoutAdmin();
|
|
await logout();
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Tab.Navigator
|
|
screenOptions={{
|
|
headerStyle: { backgroundColor: colors.bgSecondary },
|
|
headerTintColor: colors.textWhite,
|
|
headerRight: () => (
|
|
<View
|
|
style={{
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
marginRight: spacing.l,
|
|
gap: spacing.m,
|
|
}}
|
|
>
|
|
{/* Cloche notifications */}
|
|
<TouchableOpacity
|
|
onPress={openModal}
|
|
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.success,
|
|
tabBarInactiveTintColor: colors.textMuted,
|
|
tabBarLabelStyle: { fontSize: fontSize.xs },
|
|
}}
|
|
>
|
|
<Tab.Screen
|
|
name="Dashboard"
|
|
component={DashboardScreen}
|
|
options={{
|
|
title: "Livraisons",
|
|
tabBarIcon: ({ color, size }) => (
|
|
<Ionicons
|
|
name="navigate-outline"
|
|
size={size}
|
|
color={color}
|
|
/>
|
|
),
|
|
}}
|
|
/>
|
|
<Tab.Screen
|
|
name="Stats"
|
|
component={StatsScreen}
|
|
options={{
|
|
title: "Stats",
|
|
tabBarIcon: ({ color, size }) => (
|
|
<Ionicons
|
|
name="stats-chart-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.Navigator>
|
|
|
|
{/* Modal notifications */}
|
|
<Modal
|
|
visible={showModal}
|
|
animationType="slide"
|
|
transparent
|
|
onRequestClose={() => setShowModal(false)}
|
|
>
|
|
<View style={styles.overlay}>
|
|
<View
|
|
style={[
|
|
styles.modalContent,
|
|
{ backgroundColor: colors.bgSecondary },
|
|
]}
|
|
>
|
|
<View style={styles.modalHeader}>
|
|
<Text
|
|
style={[styles.modalTitle, { color: colors.textWhite }]}
|
|
>
|
|
Notifications
|
|
</Text>
|
|
<TouchableOpacity onPress={() => setShowModal(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.success,
|
|
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 },
|
|
]}
|
|
>
|
|
{formatNotifTime(n.created_at)}
|
|
</Text>
|
|
</View>
|
|
))
|
|
)}
|
|
</ScrollView>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
badge: {
|
|
position: "absolute",
|
|
top: -4,
|
|
right: -4,
|
|
backgroundColor: "#ef4444",
|
|
borderRadius: 10,
|
|
minWidth: 18,
|
|
height: 18,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
badgeText: {
|
|
color: "#fff",
|
|
fontSize: 10,
|
|
fontWeight: "bold",
|
|
},
|
|
overlay: {
|
|
flex: 1,
|
|
backgroundColor: "rgba(0,0,0,0.5)",
|
|
justifyContent: "flex-end",
|
|
},
|
|
modalContent: {
|
|
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,
|
|
},
|
|
emptyText: {
|
|
textAlign: "center",
|
|
marginTop: 32,
|
|
fontSize: 14,
|
|
},
|
|
notifItem: {
|
|
borderLeftWidth: 3,
|
|
paddingLeft: 12,
|
|
paddingVertical: 10,
|
|
marginBottom: 8,
|
|
borderRadius: 4,
|
|
paddingRight: 8,
|
|
},
|
|
notifMessage: {
|
|
fontSize: 14,
|
|
},
|
|
notifTime: {
|
|
fontSize: 12,
|
|
marginTop: 4,
|
|
},
|
|
});
|