import React, { useState, useEffect, useRef, useMemo } from "react"; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Modal, StatusBar, Alert, } from "react-native"; import { useRoute, useNavigation, type RouteProp, } from "@react-navigation/native"; import { Ionicons } from "@expo/vector-icons"; import MapView, { Marker, Polyline, PROVIDER_DEFAULT } from "react-native-maps"; import { spacing, fontSize, borderRadius } from "../../theme"; import { useTheme } from "../../context/ThemeContext"; import { getCommandByID, getCommandItems, updateCommandStatus, validateCommand, confirmReceptionAdmin, notifyClientToDescend, getDeliveryPersonDetails, deleteCommandItem, deleteCommand, } from "../../api/api_admin"; import { geocodeAddress, calculateRoute } from "../../api/tomtom"; import type { RouteInfo, LatLng } from "../../api/tomtom"; import type { AdminStackParamList } from "../../navigation/types"; import StatusBadge from "../../components/StatusBadge"; import LoadingSpinner from "../../components/ui/LoadingSpinner"; import Button from "../../components/ui/Button"; import Card from "../../components/ui/Card"; import AlertModal from "../../components/ui/AlertModal"; import { useAlert } from "../../hooks/useAlert"; type Route = RouteProp; const MAP_HEIGHT = 240; export default function OrderDetailScreen() { const { colors } = useTheme(); const route = useRoute(); const navigation = useNavigation(); const { orderId } = route.params; const [command, setCommand] = useState(null); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [showItemsModal, setShowItemsModal] = useState(false); // Map / tracking const mapRef = useRef(null); const fullscreenMapRef = useRef(null); const [mapFullscreen, setMapFullscreen] = useState(false); const [livreurCoords, setLivreurCoords] = useState(null); const [destCoords, setDestCoords] = useState(null); const [routeInfo, setRouteInfo] = useState(null); const [mapLoading, setMapLoading] = useState(false); const { alert, showError, showSuccess, hideAlert } = useAlert(); useEffect(() => { const load = async () => { try { const [cmdRes, itemsRes] = await Promise.all([ getCommandByID(orderId), getCommandItems(orderId), ]); setCommand(cmdRes.command); setItems(itemsRes.items); // If livreur assigned, fetch their location and calc route const cmd = cmdRes.command; if (cmd?.livreur_assign && cmd?.adresse) { loadLivreurRoute(cmd.livreur_assign, cmd.adresse); } } catch { /* ignore */ } setLoading(false); }; load(); }, [orderId]); useEffect(() => { const interval = setInterval(async () => { try { const res = await getCommandByID(orderId); setCommand(res.command); } catch { /* ignore */ } }, 20000); return () => clearInterval(interval); }, [orderId]); const loadLivreurRoute = async ( livreurUsername: string, deliveryAddress: string, ) => { setMapLoading(true); try { // Get livreur location const details = await getDeliveryPersonDetails(livreurUsername); const loc = details?.location; if (!loc?.latitude || !loc?.longitude) { setMapLoading(false); return; } const origin: LatLng = { latitude: loc.latitude, longitude: loc.longitude, }; setLivreurCoords(origin); // Geocode destination const dest = await geocodeAddress(deliveryAddress); if (!dest) { setMapLoading(false); return; } setDestCoords(dest); // Calculate route const result = await calculateRoute(origin, dest); if (result) { setRouteInfo(result.route); } } catch { /* silent */ } setMapLoading(false); }; const fitMapToRoute = (ref: React.RefObject) => { if (ref.current && livreurCoords && destCoords) { ref.current.fitToCoordinates( [ { latitude: livreurCoords.latitude, longitude: livreurCoords.longitude, }, { latitude: destCoords.latitude, longitude: destCoords.longitude, }, ], { edgePadding: { top: 80, right: 60, bottom: 80, left: 60 }, animated: true, }, ); } }; const handleValidate = async () => { try { await validateCommand(orderId); showSuccess("Succès", "Commande validée"); const res = await getCommandByID(orderId); setCommand(res.command); } catch (e: any) { showError("Erreur", e.message); } }; const handleStatusChange = async (status: string) => { try { await updateCommandStatus(orderId, status); const res = await getCommandByID(orderId); setCommand(res.command); } catch (e: any) { showError("Erreur", e.message); } }; const handleNotifyClient = async () => { try { await notifyClientToDescend(orderId); showSuccess( "Notification envoyée", "Le client a été prévenu de descendre", ); } catch (e: any) { showError("Erreur", e.message); } }; const handleDeleteItem = (itemId: number, itemName: string) => { Alert.alert( "Supprimer l'article", `Supprimer "${itemName}" de la commande ?`, [ { text: "Annuler", style: "cancel" }, { text: "Supprimer", style: "destructive", onPress: async () => { try { await deleteCommandItem(orderId, itemId); const itemsRes = await getCommandItems(orderId); setItems(itemsRes.items); const cmdRes = await getCommandByID(orderId); setCommand(cmdRes.command); } catch (e: any) { showError("Erreur", e.message); } }, }, ], ); }; const handleConfirmReception = async () => { try { const res = await confirmReceptionAdmin(orderId); showSuccess( "Réception confirmée", `${res.points_earned} point(s) attribués au client ${res.client_username}`, ); const updated = await getCommandByID(orderId); setCommand(updated.command); } catch (e: any) { showError("Erreur", e.message); } }; const handleDeleteCommand = () => { Alert.alert( "Supprimer la commande", `Supprimer définitivement la commande #${orderId} ?`, [ { text: "Annuler", style: "cancel" }, { text: "Supprimer", style: "destructive", onPress: async () => { try { await deleteCommand(orderId); navigation.goBack(); } catch (e: any) { showError("Erreur", e.message); } }, }, ], ); }; const styles = useMemo( () => StyleSheet.create({ container: { flex: 1, backgroundColor: colors.bgPrimary }, row: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: spacing.s, }, title: { fontSize: fontSize.xl, fontWeight: "bold", color: colors.textWhite, }, info: { color: colors.textSecondary, fontSize: fontSize.sm, marginTop: 2, }, date: { color: colors.textMuted, fontSize: fontSize.xs, marginTop: spacing.s, }, sectionTitle: { fontSize: fontSize.lg, fontWeight: "600", color: colors.textWhite, marginTop: spacing.xl, marginBottom: spacing.m, }, itemName: { color: colors.textWhite, fontSize: fontSize.md, fontWeight: "600", marginBottom: spacing.xs, }, itemDetails: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", }, itemDeleteBtn: { padding: spacing.xs, marginLeft: spacing.s, }, actions: { marginTop: spacing.m, marginBottom: spacing.xxl }, empty: { color: colors.textMuted, textAlign: "center", marginTop: spacing.xxl, fontSize: fontSize.md, }, // Map mapContainer: { borderRadius: borderRadius.md, overflow: "hidden", position: "relative", }, map: { width: "100%", height: MAP_HEIGHT }, driverMarkerOuter: { width: 34, height: 34, borderRadius: 17, backgroundColor: colors.success + "40", justifyContent: "center", alignItems: "center", }, driverMarkerInner: { width: 26, height: 26, borderRadius: 13, backgroundColor: colors.success, justifyContent: "center", alignItems: "center", }, destMarkerOuter: { width: 30, height: 30, borderRadius: 15, backgroundColor: colors.danger + "40", justifyContent: "center", alignItems: "center", }, destMarkerInner: { width: 22, height: 22, borderRadius: 11, backgroundColor: colors.danger, justifyContent: "center", alignItems: "center", }, 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", }, routeOverlayInfo: { color: colors.accent, fontSize: fontSize.xs, marginTop: 2, }, mapLoadingOverlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "rgba(0,0,0,0.3)", justifyContent: "center", alignItems: "center", }, mapLoadingText: { color: colors.white, fontSize: fontSize.sm, fontWeight: "600", }, expandBtn: { position: "absolute", top: spacing.s, right: spacing.s, width: 36, height: 36, borderRadius: 18, backgroundColor: "rgba(0,0,0,0.6)", justifyContent: "center", alignItems: "center", }, noMapBox: { alignItems: "center", paddingVertical: spacing.l, backgroundColor: colors.bgSecondary, borderRadius: borderRadius.md, }, noMapText: { color: colors.textMuted, fontSize: fontSize.sm, marginTop: spacing.xs, }, // Items modal itemsModalOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.6)", justifyContent: "flex-end", }, itemsModalContainer: { backgroundColor: colors.bgPrimary, borderTopLeftRadius: borderRadius.lg, borderTopRightRadius: borderRadius.lg, maxHeight: "75%", padding: spacing.l, }, itemsModalHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: spacing.m, }, itemsModalTitle: { fontSize: fontSize.lg, fontWeight: "700", color: colors.textWhite, }, itemsModalCard: { backgroundColor: colors.bgCard, borderRadius: borderRadius.sm, padding: spacing.m, marginBottom: spacing.s, }, // 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", }, }), [colors], ); if (loading) return ; if (!command) return ( Commande introuvable ); const hasMap = livreurCoords !== null; return ( {/* Fullscreen map modal */} setMapFullscreen(false)} statusBarTranslucent > {/* Order info */} Commande #{command.id} Client: {command.username} Adresse: {command.adresse} Total: {command.total_prix?.toFixed(2)} € {command.referral_used > 0 && ( Parrainage utilisé: -{command.referral_used?.toFixed(2)} € )} {command.livreur_assign && ( Livreur: {command.livreur_assign} )} {new Date(command.created_at).toLocaleString("fr-FR")} {/* Livreur tracking map */} {command.livreur_assign && ( <> Suivi du livreur {hasMap ? ( fitMapToRoute(mapRef)} > {destCoords && ( )} {routeInfo && routeInfo.coordinates.length > 0 && ( )} {/* Route info overlay */} {routeInfo && ( {command.livreur_assign} {routeInfo.distance} ·{" "} {routeInfo.duration} )} {mapLoading && ( Chargement... )} setMapFullscreen(true)} > ) : mapLoading ? ( Chargement position du livreur... ) : ( Position GPS du livreur non disponible )} )} {/* Items */} Articles ({items.length}) {items.map((item: any) => ( {item.produit ?? item.product_name} handleDeleteItem( item.id, item.produit ?? item.product_name, ) } > Quantité: {item.quantite} Prix: {item.prix?.toFixed(2)} € ))} {/* Actions */} Actions