import React, { useState, useEffect, useCallback, useRef, useMemo, } from "react"; import { View, Text, StyleSheet, FlatList, RefreshControl, TouchableOpacity, Modal, StatusBar, useWindowDimensions, ScrollView, } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { spacing, fontSize, borderRadius } from "../../theme"; import { useTheme } from "../../context/ThemeContext"; import { getAllDeliveryPersonsWithDetails, getCommandByID, getLivreurRatings, getLivreurLoginHistory, } from "../../api/api_admin"; import type { LoginHistoryWeek } from "../../api/api_admin"; import { geocodeAddress, calculateRoute } from "../../api/tomtom"; import type { RouteInfo } from "../../api/tomtom"; import type { DeliveryPerson } from "../../api/types"; import { STATUS_LABELS, getStatusColors } from "../../utils/constants"; import LoadingSpinner from "../../components/ui/LoadingSpinner"; import Card from "../../components/ui/Card"; import Badge from "../../components/ui/Badge"; import TomTomMap, { TomTomMapRef, TomTomMarker, } from "../../components/TomTomMap"; export default function DeliveryScreen() { const { colors } = useTheme(); const { width: screenWidth, height: screenHeight } = useWindowDimensions(); const MAP_HEIGHT = screenHeight < 700 ? 200 : screenWidth < 380 ? 220 : 280; const statusColors = getStatusColors(colors); const [livreurs, setLivreurs] = useState([]); const [stats, setStats] = useState({ total: 0, available: 0, busy: 0, offline: 0, }); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); // TomTomMap refs const mapRef = useRef(null); const fullscreenMapRef = useRef(null); const [mapFullscreen, setMapFullscreen] = useState(false); // Selected livreur route tracking const [selectedLivreur, setSelectedLivreur] = useState(null); const [routeInfo, setRouteInfo] = useState(null); const [routeLoading, setRouteLoading] = useState(false); // Avis livreur const [ratingsModal, setRatingsModal] = useState<{ username: string; ratings: { id: number; order_id: number; client_username: string; rating: number; comment: string; created_at: string }[]; average: number; count: number; } | null>(null); const [ratingsLoading, setRatingsLoading] = useState(false); const openRatings = async (username: string) => { setRatingsLoading(true); const data = await getLivreurRatings(username); setRatingsModal({ username, ...data }); setRatingsLoading(false); }; // Historique de connexion livreur const [loginHistoryModal, setLoginHistoryModal] = useState<{ username: string; year: number; month: number; weeks: LoginHistoryWeek[]; } | null>(null); const [loginHistoryLoading, setLoginHistoryLoading] = useState(false); const loginHistoryRequestRef = useRef(0); const fetchLoginHistory = async ( username: string, year: number, month: number, ) => { const requestId = ++loginHistoryRequestRef.current; setLoginHistoryLoading(true); const res = await getLivreurLoginHistory(username, year, month); if (requestId !== loginHistoryRequestRef.current) return; setLoginHistoryModal({ username, year: res.year, month: res.month, weeks: res.weeks, }); setLoginHistoryLoading(false); }; const openLoginHistory = (username: string) => { const now = new Date(); fetchLoginHistory(username, now.getFullYear(), now.getMonth() + 1); }; const changeLoginHistoryMonth = (delta: number) => { if (!loginHistoryModal || loginHistoryLoading) return; let year = loginHistoryModal.year; let month = loginHistoryModal.month + delta; if (month < 1) { month = 12; year -= 1; } else if (month > 12) { month = 1; year += 1; } const now = new Date(); if ( year > now.getFullYear() || (year === now.getFullYear() && month > now.getMonth() + 1) ) { return; } fetchLoginHistory(loginHistoryModal.username, year, month); }; const loadData = useCallback(async () => { try { const result = await getAllDeliveryPersonsWithDetails(); setLivreurs(result.deliveryPersons); setStats(result.stats); } catch { /* ignore */ } setLoading(false); }, []); useEffect(() => { loadData(); const interval = setInterval(loadData, 15000); return () => clearInterval(interval); }, [loadData]); const onRefresh = async () => { setRefreshing(true); await loadData(); setRefreshing(false); }; const livreursWithGPS = livreurs.filter( (l) => l.location.latitude !== 0 && l.location.longitude !== 0, ); // -------------------------------------------------- // Construire les markers TomTom pour tous les livreurs // -------------------------------------------------- const tomtomMarkers: TomTomMarker[] = useMemo(() => { return livreursWithGPS.map((l) => ({ id: l.username, latitude: l.location.latitude, longitude: l.location.longitude, color: statusColors[l.status] || colors.textMuted, label: l.username, description: `${STATUS_LABELS[l.status] || l.status}${ l.stats.current_command ? ` · Cmd #${l.stats.current_command}` : "" }`, isSelected: selectedLivreur?.username === l.username, })); }, [livreursWithGPS, selectedLivreur, statusColors, colors.textMuted]); // -------------------------------------------------- // Track livreur — calcul de route via ref TomTomMap // -------------------------------------------------- const trackLivreur = useCallback( async (livreur: DeliveryPerson) => { setSelectedLivreur(livreur); setRouteInfo(null); if (!livreur.stats.current_command) return; setRouteLoading(true); try { const cmdRes = await getCommandByID( livreur.stats.current_command, ); const cmd = cmdRes.command; if (!cmd?.adresse) { setRouteLoading(false); return; } const dest = await geocodeAddress(cmd.adresse); if (!dest) { setRouteLoading(false); return; } const origin = { latitude: livreur.location.latitude, longitude: livreur.location.longitude, }; const result = await calculateRoute(origin, dest); if (result) { setRouteInfo(result.route); const activeRef = mapFullscreen ? fullscreenMapRef : mapRef; activeRef.current?.calcRoute(origin, dest); } } catch { /* silent */ } setRouteLoading(false); }, [mapFullscreen], ); const clearRoute = () => { setSelectedLivreur(null); setRouteInfo(null); }; // -------------------------------------------------- // onMarkerPress depuis TomTomMap // -------------------------------------------------- const handleMarkerPress = useCallback( (markerId: string) => { const livreur = livreursWithGPS.find( (l) => l.username === markerId, ); if (livreur) { if (selectedLivreur?.username === markerId) { clearRoute(); } else { trackLivreur(livreur); } } }, [livreursWithGPS, selectedLivreur, trackLivreur], ); // -------------------------------------------------- // Styles // -------------------------------------------------- const styles = useMemo( () => StyleSheet.create({ container: { flex: 1, backgroundColor: colors.bgPrimary }, summaryRow: { flexDirection: "row", padding: screenWidth < 380 ? spacing.m : spacing.l, gap: screenWidth < 380 ? spacing.xs : spacing.s, }, summaryCard: { flex: 1, backgroundColor: colors.bgCard, borderRadius: borderRadius.sm, padding: spacing.m, borderLeftWidth: 3, alignItems: "center", }, summaryValue: { fontSize: screenWidth < 380 ? fontSize.lg : fontSize.xl, fontWeight: "bold", color: colors.textWhite, }, summaryLabel: { fontSize: screenWidth < 380 ? 10 : fontSize.xs, color: colors.textMuted, textAlign: "center", }, mapContainer: { borderRadius: borderRadius.md, overflow: "hidden", marginBottom: spacing.m, position: "relative", height: MAP_HEIGHT, }, map: { flex: 1 }, routeOverlay: { position: "absolute", top: spacing.s, left: spacing.s, backgroundColor: "rgba(0,0,0,0.75)", borderRadius: borderRadius.sm, padding: spacing.s, paddingHorizontal: spacing.m, }, routeOverlayUser: { color: colors.white, fontSize: fontSize.sm, fontWeight: "700", }, routeChips: { flexDirection: "row", gap: spacing.s, marginTop: 4, }, routeChip: { flexDirection: "row", alignItems: "center", gap: 3, }, routeChipText: { color: colors.accent, fontSize: fontSize.xs, fontWeight: "600", }, routeLoadingOverlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "rgba(0,0,0,0.3)", justifyContent: "center", alignItems: "center", }, routeLoadingText: { color: colors.white, fontSize: fontSize.sm, fontWeight: "600", }, mapBtns: { position: "absolute", top: spacing.s, right: spacing.s, gap: spacing.xs, }, mapBtn: { width: 36, height: 36, borderRadius: 18, backgroundColor: "rgba(0,0,0,0.6)", justifyContent: "center", alignItems: "center", }, noMapBox: { alignItems: "center", paddingVertical: spacing.xl, marginBottom: spacing.m, backgroundColor: colors.bgSecondary, borderRadius: borderRadius.md, }, noMapText: { color: colors.textMuted, fontSize: fontSize.sm, marginTop: spacing.s, }, sectionTitle: { color: colors.textWhite, fontSize: fontSize.lg, fontWeight: "700", marginBottom: spacing.m, }, row: { flexDirection: "row", alignItems: "center", gap: spacing.s, marginBottom: spacing.s, }, statusDot: { width: 10, height: 10, borderRadius: 5 }, username: { flex: 1, fontSize: fontSize.lg, fontWeight: "600", color: colors.textWhite, }, statsRow: { flexDirection: "row", flexWrap: "wrap", gap: screenWidth < 380 ? spacing.s : spacing.l, marginTop: spacing.s, }, stat: { flexDirection: "row", alignItems: "center", gap: spacing.xs, }, statText: { color: colors.textSecondary, fontSize: fontSize.sm, }, location: { color: colors.textMuted, fontSize: fontSize.xs, marginTop: spacing.s, }, currentCmd: { color: colors.info, fontSize: fontSize.sm, marginTop: spacing.xs, fontWeight: "500", }, ratingsBtn: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: spacing.s, marginTop: spacing.m, paddingVertical: spacing.s, paddingHorizontal: spacing.m, borderRadius: borderRadius.sm, borderWidth: 1, borderColor: "#f59e0b", }, ratingsBtnText: { color: "#f59e0b", fontSize: fontSize.sm, fontWeight: "600", }, historyBtn: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: spacing.s, marginTop: spacing.m, paddingVertical: spacing.s, paddingHorizontal: spacing.m, borderRadius: borderRadius.sm, borderWidth: 1, borderColor: colors.accent, }, historyBtnText: { color: colors.accent, fontSize: fontSize.sm, fontWeight: "600", }, historyWeekLabel: { color: colors.textMuted, fontSize: fontSize.xs, fontWeight: "700", textTransform: "uppercase", marginBottom: spacing.xs, marginTop: spacing.m, }, historyEntryRow: { flexDirection: "row", justifyContent: "space-between", paddingVertical: spacing.xs, borderTopWidth: 1, borderTopColor: colors.borderLight, }, historyEntryDate: { color: colors.textPrimary, fontSize: fontSize.sm, }, historyEntryTime: { color: colors.textMuted, fontSize: fontSize.sm, }, trackBtn: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: spacing.s, marginTop: spacing.m, paddingVertical: spacing.s, paddingHorizontal: spacing.m, borderRadius: borderRadius.sm, borderWidth: 1, borderColor: colors.accent, }, trackBtnText: { color: colors.accent, fontSize: fontSize.sm, fontWeight: "600", }, ratingsOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.7)", justifyContent: "flex-end", }, ratingsSheet: { backgroundColor: colors.bgCard, borderTopLeftRadius: borderRadius.xl, borderTopRightRadius: borderRadius.xl, padding: spacing.l, maxHeight: "80%", }, ratingsList: { padding: spacing.s, }, ratingsHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: spacing.s, }, ratingsTitle: { color: colors.textWhite, fontSize: fontSize.lg, fontWeight: "700", }, ratingsAvg: { flexDirection: "row", alignItems: "center", gap: spacing.xs, marginBottom: spacing.l, }, ratingsAvgText: { color: "#f59e0b", fontSize: fontSize.md, fontWeight: "700", }, ratingsCount: { color: colors.textMuted, fontSize: fontSize.sm, }, ratingItem: { borderTopWidth: 1, borderTopColor: colors.borderLight, paddingVertical: spacing.m, }, ratingItemHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: spacing.xs, }, ratingItemClient: { color: colors.textPrimary, fontSize: fontSize.sm, fontWeight: "600", }, ratingItemDate: { color: colors.textMuted, fontSize: fontSize.xs, }, ratingStarsRow: { flexDirection: "row", gap: 2, marginBottom: spacing.xs, }, ratingItemComment: { color: colors.textSecondary, fontSize: fontSize.sm, fontStyle: "italic", }, ratingsEmpty: { color: colors.textMuted, textAlign: "center", paddingVertical: spacing.xl, }, empty: { color: colors.textMuted, textAlign: "center", marginTop: spacing.xxl, }, // Fullscreen fullscreenContainer: { flex: 1, backgroundColor: colors.bgPrimary, }, fullscreenMap: { ...StyleSheet.absoluteFillObject }, fullscreenTopBar: { position: "absolute", top: 0, left: 0, right: 0, flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingTop: 50, paddingHorizontal: spacing.l, paddingBottom: spacing.m, backgroundColor: "rgba(0,0,0,0.5)", }, closeBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: "rgba(255,255,255,0.15)", justifyContent: "center", alignItems: "center", }, fullscreenTitle: { color: colors.white, fontSize: fontSize.lg, fontWeight: "700", }, fullscreenRouteBar: { position: "absolute", top: 110, left: spacing.m, right: spacing.m, flexDirection: "row", alignItems: "center", backgroundColor: "rgba(0,0,0,0.8)", padding: spacing.m, borderRadius: borderRadius.md, }, fullscreenRouteUser: { color: colors.white, fontSize: fontSize.md, fontWeight: "700", }, fullscreenRouteInfo: { color: colors.accent, fontSize: fontSize.sm, marginTop: 2, }, clearRouteBtn: { padding: spacing.xs }, fullscreenBottomBar: { position: "absolute", bottom: 0, left: 0, right: 0, backgroundColor: "rgba(0,0,0,0.7)", paddingHorizontal: spacing.l, paddingTop: spacing.m, paddingBottom: 40, }, legendRow: { flexDirection: "row", flexWrap: "wrap", justifyContent: "center", gap: screenWidth < 380 ? spacing.m : spacing.l, }, legendItem: { flexDirection: "row", alignItems: "center", gap: 6, }, legendDot: { width: 10, height: 10, borderRadius: 5 }, legendText: { color: colors.white, fontSize: fontSize.sm }, }), [colors, screenWidth, screenHeight, MAP_HEIGHT], ); // -------------------------------------------------- // Render livreur card // -------------------------------------------------- const renderLivreur = ({ item }: { item: DeliveryPerson }) => { const isSelected = selectedLivreur?.username === item.username; const hasGPS = item.location.latitude !== 0; return ( {item.username} {item.stats.queue_size} en queue {item.stats.completed_today} aujourd'hui {item.stats.total_deliveries} total {hasGPS && ( GPS: {item.location.latitude.toFixed(4)},{" "} {item.location.longitude.toFixed(4)} {item.location.is_recent ? " (récent)" : " (ancien)"} )} {item.stats.current_command && ( Commande en cours: #{item.stats.current_command} )} openRatings(item.username)} activeOpacity={0.7} > Voir les avis openLoginHistory(item.username)} activeOpacity={0.7} > Historique de connexion {hasGPS && ( isSelected ? clearRoute() : trackLivreur(item) } activeOpacity={0.7} > {isSelected ? "Arrêter le suivi" : item.stats.current_command ? "Suivre l'itinéraire" : "Voir sur la carte"} )} ); }; // -------------------------------------------------- // Header avec TomTomMap // -------------------------------------------------- const renderHeader = () => ( {livreursWithGPS.length > 0 ? ( {/* Overlay info route sélectionnée */} {routeInfo && selectedLivreur && ( {selectedLivreur.username} {routeInfo.distance} {routeInfo.duration} )} {routeLoading && ( Calcul itinéraire... )} mapRef.current?.fitAllMarkers()} > setMapFullscreen(true)} > ) : !loading ? ( Aucun livreur avec GPS actif ) : null} Livreurs ({livreurs.length}) ); if (loading) return ; return ( {/* ── Modal plein écran TomTomMap ── */} setMapFullscreen(false)} statusBarTranslucent > {/* ── Résumé stats ── */} {stats.available} Dispo {stats.busy} Occupés {stats.offline} Hors ligne {/* ── Liste livreurs ── */} item.username} renderItem={renderLivreur} refreshControl={ } contentContainerStyle={{ padding: spacing.l, paddingTop: 0 }} ListHeaderComponent={renderHeader()} ListEmptyComponent={ Aucun livreur } /> {/* ── Modal avis livreur ── */} setRatingsModal(null)} > Avis — {ratingsModal?.username} setRatingsModal(null)}> {ratingsLoading ? ( Chargement... ) : ratingsModal && ratingsModal.count > 0 ? ( <> {[1,2,3,4,5].map((s) => ( ))} {ratingsModal.average.toFixed(1)} ({ratingsModal.count} avis) {ratingsModal.ratings.map((r) => ( {r.client_username} {new Date(r.created_at).toLocaleDateString("fr-FR")} {[1,2,3,4,5].map((s) => ( ))} {r.comment !== "" && ( "{r.comment}" )} ))} ) : ( Aucun avis pour ce livreur )} {/* ── Modal historique de connexion livreur ── */} setLoginHistoryModal(null)} > Connexions — {loginHistoryModal?.username} setLoginHistoryModal(null)} > {(() => { const now = new Date(); const isCurrentMonth = !!loginHistoryModal && loginHistoryModal.year === now.getFullYear() && loginHistoryModal.month === now.getMonth() + 1; const canGoBack = !loginHistoryLoading; const canGoForward = !loginHistoryLoading && !isCurrentMonth; return ( changeLoginHistoryMonth(-1) } hitSlop={8} > {loginHistoryModal && new Date( loginHistoryModal.year, loginHistoryModal.month - 1, 1, ) .toLocaleDateString( "fr-FR", { month: "long", year: "numeric", }, ) .replace(/^./, (c) => c.toUpperCase(), )} changeLoginHistoryMonth(1) } hitSlop={8} > ); })()} {loginHistoryLoading ? ( Chargement... ) : loginHistoryModal && loginHistoryModal.weeks.length > 0 ? ( {loginHistoryModal.weeks.map((week) => ( Semaine {week.week} {week.entries.map((entry) => ( {new Date( entry.created_at, ).toLocaleDateString( "fr-FR", { weekday: "short", day: "2-digit", month: "2-digit", }, )} {new Date( entry.created_at, ).toLocaleTimeString( "fr-FR", { hour: "2-digit", minute: "2-digit", }, )} ))} ))} ) : ( Aucune connexion ce mois-ci )} ); }