import React, { useState, useEffect, useRef, useMemo } from "react"; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Modal, Alert, useWindowDimensions, } from "react-native"; import { useRoute, useNavigation, type RouteProp, } from "@react-navigation/native"; import { Ionicons } from "@expo/vector-icons"; import TomTomMap, { type TomTomMapRef, type TomTomMarker } from "../../components/TomTomMap"; import { spacing, fontSize, borderRadius } from "../../theme"; import { useTheme } from "../../context/ThemeContext"; import { getCommandByID, getCommandItems, updateCommandStatus, validateCommand, notifyClientToDescend, getDeliveryPersonDetails, deleteCommandItem, deleteCommand, } from "../../api/api_admin"; import { calculateRoute, geocodeAddress } 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; export default function OrderDetailScreen() { const { colors } = useTheme(); const { height: screenHeight } = useWindowDimensions(); const MAP_HEIGHT = screenHeight < 700 ? 180 : 240; 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 [livreurMarkers, setLivreurMarkers] = useState([]); 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]); // Rejoue la route sur la carte fullscreen quand elle s'ouvre useEffect(() => { if (mapFullscreen && livreurCoords && destCoords) { setTimeout(() => { fullscreenMapRef.current?.calcRoute(livreurCoords, destCoords); }, 600); } }, [mapFullscreen]); const loadLivreurRoute = async ( livreurUsername: string, deliveryAddress: string, ) => { setMapLoading(true); try { 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); setLivreurMarkers([{ id: livreurUsername, latitude: loc.latitude, longitude: loc.longitude, color: "#22c55e", label: livreurUsername, description: "Livreur", }]); // Dessine la route sur la carte et récupère les infos (distance/durée) const dest = await geocodeAddress(deliveryAddress); if (dest) { setDestCoords(dest); const result = await calculateRoute(origin, dest); if (result) { setRouteInfo(result.route); mapRef.current?.calcRoute(origin, dest); } } } catch { /* silent */ } setMapLoading(false); }; 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 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 }, 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, }, // Grouped items subItemRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingVertical: spacing.xs, paddingLeft: spacing.m, borderLeftWidth: 2, borderLeftColor: colors.border, marginLeft: spacing.xs, marginBottom: 2, }, subItemText: { color: colors.textSecondary, fontSize: fontSize.sm, flex: 1, }, groupTotalRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginTop: spacing.s, paddingTop: spacing.s, borderTopWidth: 1, borderTopColor: colors.border, }, groupTotalQty: { color: colors.textWhite, fontSize: fontSize.sm, fontWeight: "700", }, groupTotalPrice: { color: colors.accent, fontSize: fontSize.md, fontWeight: "700", }, categoryBadge: { fontSize: fontSize.xs, color: colors.accent, fontWeight: "600", marginRight: spacing.s, textTransform: "uppercase", }, // Category summary categorySummaryCard: { marginTop: spacing.s, }, categoryRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingVertical: spacing.s, borderBottomWidth: 1, borderBottomColor: colors.border, }, categoryName: { color: colors.textSecondary, fontSize: fontSize.sm, fontWeight: "600", flex: 1, }, categoryQty: { color: colors.textWhite, fontSize: fontSize.sm, fontWeight: "700", marginRight: spacing.l, }, categoryTotal: { color: colors.accent, fontSize: fontSize.sm, fontWeight: "700", minWidth: 70, textAlign: "right", }, // 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, MAP_HEIGHT], ); // Groupement des items par product_id const productGroups = (items ?? []).reduce>( (acc, item) => { const key = String(item.product_id || item.produit); if (!acc[key]) acc[key] = []; acc[key].push(item); return acc; }, {}, ); const groupedList = Object.values(productGroups); // Récapitulatif par catégorie const categoryTotals = (items ?? []).reduce< Record >((acc, item) => { const cat = item.category || "Autre"; if (!acc[cat]) acc[cat] = { qty: 0, total: 0 }; acc[cat].qty += item.quantite ?? 0; acc[cat].total += item.prix ?? 0; return acc; }, {}); const categoryEntries = Object.entries(categoryTotals); if (loading) return ; if (!command) return ( Commande introuvable ); const hasMap = livreurCoords !== null; return ( {/* Fullscreen map modal */} setMapFullscreen(false)} > setMapFullscreen(false)} > {routeInfo ? `${routeInfo.distance} · ${routeInfo.duration}` : `Livreur: ${command.livreur_assign}`} {/* Order info */} Commande #{command.id} Client: {command.username} Adresse: {command.adresse} Total brut: {command.total_prix?.toFixed(2)} € {command.referral_used > 0 && ( <> Parrainage utilisé: - {command.referral_used?.toFixed(2)} € Net:{" "} {( command.total_prix - command.referral_used ).toFixed(2)}{" "} € )} {command.livreur_assign && ( Livreur: {command.livreur_assign} )} {new Date(command.created_at).toLocaleString("fr-FR")} {command.status === "cancelled" && command.cancel_reason ? ( Motif d'annulation {command.cancel_reason} ) : command.status === "cancelled" ? ( Aucun motif fourni ) : null} {/* Livreur tracking map */} {command.livreur_assign && ( <> Suivi du livreur {hasMap ? ( {routeInfo && ( {command.livreur_assign} {routeInfo.distance} · {routeInfo.duration} )} {mapLoading && ( Chargement... )} setMapFullscreen(true)} > ) : mapLoading ? ( Chargement position du livreur... ) : ( Position GPS du livreur non disponible )} )} {/* Items groupés par produit */} Articles ({items.length}) {groupedList.map((group, gi) => { const rep = group[0]; const name = rep.produit ?? rep.product_name; const unit = rep.unit || ""; const totalQty = group.reduce( (s: number, it: any) => s + (it.quantite ?? 0), 0, ); const totalPrice = group.reduce( (s: number, it: any) => s + (it.prix ?? 0), 0, ); const isMultiple = group.length > 1; return ( {name} {rep.category ? ( {rep.category} ) : null} handleDeleteItem(rep.id, name)} > {isMultiple && group.map((item: any, i: number) => ( {item.quantite} {unit} — {item.prix?.toFixed(2)} € handleDeleteItem(item.id, name) } > ))} {isMultiple ? `Total: ${totalQty}${unit}` : `${totalQty}${unit}`} {totalPrice.toFixed(2)} € ); })} {/* Récapitulatif par catégorie */} {categoryEntries.length > 0 && ( <> Par catégorie {categoryEntries.map(([cat, data]) => ( {cat} {data.qty.toFixed( data.qty % 1 === 0 ? 0 : 2, )} {data.total.toFixed(2)} € ))} )} {/* Actions */} Actions