chore: fix map dashboard admin
This commit is contained in:
@@ -14,10 +14,8 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Modal,
|
Modal,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Dimensions,
|
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import MapView, { Marker, Polyline, PROVIDER_DEFAULT } from "react-native-maps";
|
|
||||||
import { spacing, fontSize, borderRadius } from "../../theme";
|
import { spacing, fontSize, borderRadius } from "../../theme";
|
||||||
import { useTheme } from "../../context/ThemeContext";
|
import { useTheme } from "../../context/ThemeContext";
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +29,12 @@ import { STATUS_LABELS, getStatusColors } from "../../utils/constants";
|
|||||||
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
||||||
import Card from "../../components/ui/Card";
|
import Card from "../../components/ui/Card";
|
||||||
import Badge from "../../components/ui/Badge";
|
import Badge from "../../components/ui/Badge";
|
||||||
|
import TomTomMap, {
|
||||||
|
TomTomMapRef,
|
||||||
|
TomTomMarker,
|
||||||
|
TomTomRoute,
|
||||||
|
TomTomDestination,
|
||||||
|
} from "../../components/TomTomMap";
|
||||||
|
|
||||||
const MAP_HEIGHT = 280;
|
const MAP_HEIGHT = 280;
|
||||||
|
|
||||||
@@ -47,9 +51,9 @@ export default function DeliveryScreen() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
// Map
|
// TomTomMap refs
|
||||||
const mapRef = useRef<MapView | null>(null);
|
const mapRef = useRef<TomTomMapRef>(null);
|
||||||
const fullscreenMapRef = useRef<MapView | null>(null);
|
const fullscreenMapRef = useRef<TomTomMapRef>(null);
|
||||||
const [mapFullscreen, setMapFullscreen] = useState(false);
|
const [mapFullscreen, setMapFullscreen] = useState(false);
|
||||||
|
|
||||||
// Selected livreur route tracking
|
// Selected livreur route tracking
|
||||||
@@ -88,6 +92,44 @@ export default function DeliveryScreen() {
|
|||||||
(l) => l.location.latitude !== 0 && l.location.longitude !== 0,
|
(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]);
|
||||||
|
|
||||||
|
// Route TomTom
|
||||||
|
const tomtomRoute: TomTomRoute | null = useMemo(() => {
|
||||||
|
if (!routeInfo || routeInfo.coordinates.length === 0) return null;
|
||||||
|
return { coordinates: routeInfo.coordinates, color: "#4285F4" };
|
||||||
|
}, [routeInfo]);
|
||||||
|
|
||||||
|
// Destination TomTom
|
||||||
|
const tomtomDestination: TomTomDestination | null = useMemo(() => {
|
||||||
|
if (!destinationCoords) return null;
|
||||||
|
return {
|
||||||
|
latitude: destinationCoords.latitude,
|
||||||
|
longitude: destinationCoords.longitude,
|
||||||
|
color: colors.danger,
|
||||||
|
};
|
||||||
|
}, [destinationCoords, colors.danger]);
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Track livreur — calcul de route
|
||||||
|
// --------------------------------------------------
|
||||||
const trackLivreur = useCallback(
|
const trackLivreur = useCallback(
|
||||||
async (livreur: DeliveryPerson) => {
|
async (livreur: DeliveryPerson) => {
|
||||||
setSelectedLivreur(livreur);
|
setSelectedLivreur(livreur);
|
||||||
@@ -121,30 +163,15 @@ export default function DeliveryScreen() {
|
|||||||
const result = await calculateRoute(origin, dest);
|
const result = await calculateRoute(origin, dest);
|
||||||
if (result) {
|
if (result) {
|
||||||
setRouteInfo(result.route);
|
setRouteInfo(result.route);
|
||||||
|
// Fit la map sur les deux points
|
||||||
const ref = mapFullscreen ? fullscreenMapRef : mapRef;
|
const ref = mapFullscreen ? fullscreenMapRef : mapRef;
|
||||||
if (ref.current) {
|
ref.current?.fitToCoordinates([
|
||||||
ref.current.fitToCoordinates(
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
latitude: origin.latitude,
|
latitude: origin.latitude,
|
||||||
longitude: origin.longitude,
|
longitude: origin.longitude,
|
||||||
},
|
},
|
||||||
{
|
{ latitude: dest.latitude, longitude: dest.longitude },
|
||||||
latitude: dest.latitude,
|
]);
|
||||||
longitude: dest.longitude,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
edgePadding: {
|
|
||||||
top: 80,
|
|
||||||
right: 60,
|
|
||||||
bottom: 80,
|
|
||||||
left: 60,
|
|
||||||
},
|
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* silent */
|
/* silent */
|
||||||
@@ -160,21 +187,28 @@ export default function DeliveryScreen() {
|
|||||||
setDestinationCoords(null);
|
setDestinationCoords(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fitAllMarkers = (ref: React.RefObject<MapView | null>) => {
|
// --------------------------------------------------
|
||||||
if (ref.current && livreursWithGPS.length > 0) {
|
// onMarkerPress depuis TomTomMap
|
||||||
ref.current.fitToCoordinates(
|
// --------------------------------------------------
|
||||||
livreursWithGPS.map((l) => ({
|
const handleMarkerPress = useCallback(
|
||||||
latitude: l.location.latitude,
|
(markerId: string) => {
|
||||||
longitude: l.location.longitude,
|
const livreur = livreursWithGPS.find(
|
||||||
})),
|
(l) => l.username === markerId,
|
||||||
{
|
|
||||||
edgePadding: { top: 60, right: 60, bottom: 60, left: 60 },
|
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
if (livreur) {
|
||||||
|
if (selectedLivreur?.username === markerId) {
|
||||||
|
clearRoute();
|
||||||
|
} else {
|
||||||
|
trackLivreur(livreur);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
[livreursWithGPS, selectedLivreur, trackLivreur],
|
||||||
|
);
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Styles
|
||||||
|
// --------------------------------------------------
|
||||||
const styles = useMemo(
|
const styles = useMemo(
|
||||||
() =>
|
() =>
|
||||||
StyleSheet.create({
|
StyleSheet.create({
|
||||||
@@ -208,46 +242,9 @@ export default function DeliveryScreen() {
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
marginBottom: spacing.m,
|
marginBottom: spacing.m,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
height: MAP_HEIGHT,
|
||||||
},
|
},
|
||||||
map: { width: "100%", height: MAP_HEIGHT },
|
map: { flex: 1 },
|
||||||
|
|
||||||
markerOuter: {
|
|
||||||
width: 34,
|
|
||||||
height: 34,
|
|
||||||
borderRadius: 17,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
markerSelected: {
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: colors.accent,
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
borderRadius: 20,
|
|
||||||
},
|
|
||||||
markerInner: {
|
|
||||||
width: 26,
|
|
||||||
height: 26,
|
|
||||||
borderRadius: 13,
|
|
||||||
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: {
|
routeOverlay: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -393,6 +390,7 @@ export default function DeliveryScreen() {
|
|||||||
marginTop: spacing.xxl,
|
marginTop: spacing.xxl,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Fullscreen
|
||||||
fullscreenContainer: {
|
fullscreenContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.bgPrimary,
|
backgroundColor: colors.bgPrimary,
|
||||||
@@ -472,77 +470,9 @@ export default function DeliveryScreen() {
|
|||||||
[colors],
|
[colors],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderMapMarkers = () =>
|
// --------------------------------------------------
|
||||||
livreursWithGPS.map((l) => {
|
// Render livreur card
|
||||||
const isSelected = selectedLivreur?.username === l.username;
|
// --------------------------------------------------
|
||||||
const markerColor = statusColors[l.status] || colors.textMuted;
|
|
||||||
return (
|
|
||||||
<Marker
|
|
||||||
key={l.username}
|
|
||||||
coordinate={{
|
|
||||||
latitude: l.location.latitude,
|
|
||||||
longitude: l.location.longitude,
|
|
||||||
}}
|
|
||||||
title={l.username}
|
|
||||||
description={`${STATUS_LABELS[l.status] || l.status}${l.stats.current_command ? ` · Cmd #${l.stats.current_command}` : ""}`}
|
|
||||||
onPress={() => trackLivreur(l)}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.markerOuter,
|
|
||||||
{ backgroundColor: markerColor + "40" },
|
|
||||||
isSelected && styles.markerSelected,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.markerInner,
|
|
||||||
{ backgroundColor: markerColor },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name="bicycle"
|
|
||||||
size={14}
|
|
||||||
color={colors.white}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Marker>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderRouteOverlay = () => {
|
|
||||||
if (!destinationCoords) return null;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Marker
|
|
||||||
coordinate={{
|
|
||||||
latitude: destinationCoords.latitude,
|
|
||||||
longitude: destinationCoords.longitude,
|
|
||||||
}}
|
|
||||||
title="Destination"
|
|
||||||
>
|
|
||||||
<View style={styles.destMarkerOuter}>
|
|
||||||
<View style={styles.destMarkerInner}>
|
|
||||||
<Ionicons
|
|
||||||
name="flag"
|
|
||||||
size={12}
|
|
||||||
color={colors.white}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Marker>
|
|
||||||
{routeInfo && routeInfo.coordinates.length > 0 && (
|
|
||||||
<Polyline
|
|
||||||
coordinates={routeInfo.coordinates}
|
|
||||||
strokeColor="#4285F4"
|
|
||||||
strokeWidth={4}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderLivreur = ({ item }: { item: DeliveryPerson }) => {
|
const renderLivreur = ({ item }: { item: DeliveryPerson }) => {
|
||||||
const isSelected = selectedLivreur?.username === item.username;
|
const isSelected = selectedLivreur?.username === item.username;
|
||||||
const hasGPS = item.location.latitude !== 0;
|
const hasGPS = item.location.latitude !== 0;
|
||||||
@@ -658,27 +588,28 @@ export default function DeliveryScreen() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Header avec TomTomMap
|
||||||
|
// --------------------------------------------------
|
||||||
const renderHeader = () => (
|
const renderHeader = () => (
|
||||||
<View>
|
<View>
|
||||||
{livreursWithGPS.length > 0 && (
|
{livreursWithGPS.length > 0 ? (
|
||||||
<View style={styles.mapContainer}>
|
<View style={styles.mapContainer}>
|
||||||
<MapView
|
<TomTomMap
|
||||||
ref={mapRef}
|
ref={mapRef}
|
||||||
provider={PROVIDER_DEFAULT}
|
|
||||||
style={styles.map}
|
style={styles.map}
|
||||||
initialRegion={{
|
markers={tomtomMarkers}
|
||||||
|
route={tomtomRoute}
|
||||||
|
destination={tomtomDestination}
|
||||||
|
initialCenter={{
|
||||||
latitude: livreursWithGPS[0].location.latitude,
|
latitude: livreursWithGPS[0].location.latitude,
|
||||||
longitude: livreursWithGPS[0].location.longitude,
|
longitude: livreursWithGPS[0].location.longitude,
|
||||||
latitudeDelta: 0.05,
|
|
||||||
longitudeDelta: 0.05,
|
|
||||||
}}
|
}}
|
||||||
onMapReady={() => fitAllMarkers(mapRef)}
|
initialZoom={12}
|
||||||
showsUserLocation={false}
|
onMarkerPress={handleMarkerPress}
|
||||||
>
|
/>
|
||||||
{renderMapMarkers()}
|
|
||||||
{renderRouteOverlay()}
|
|
||||||
</MapView>
|
|
||||||
|
|
||||||
|
{/* Overlay info route sélectionnée */}
|
||||||
{routeInfo && selectedLivreur && (
|
{routeInfo && selectedLivreur && (
|
||||||
<View style={styles.routeOverlay}>
|
<View style={styles.routeOverlay}>
|
||||||
<Text style={styles.routeOverlayUser}>
|
<Text style={styles.routeOverlayUser}>
|
||||||
@@ -720,7 +651,7 @@ export default function DeliveryScreen() {
|
|||||||
<View style={styles.mapBtns}>
|
<View style={styles.mapBtns}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.mapBtn}
|
style={styles.mapBtn}
|
||||||
onPress={() => fitAllMarkers(mapRef)}
|
onPress={() => mapRef.current?.fitAllMarkers()}
|
||||||
>
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="locate-outline"
|
name="locate-outline"
|
||||||
@@ -740,9 +671,7 @@ export default function DeliveryScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
) : !loading ? (
|
||||||
|
|
||||||
{livreursWithGPS.length === 0 && !loading && (
|
|
||||||
<View style={styles.noMapBox}>
|
<View style={styles.noMapBox}>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="location-outline"
|
name="location-outline"
|
||||||
@@ -753,7 +682,7 @@ export default function DeliveryScreen() {
|
|||||||
Aucun livreur avec GPS actif
|
Aucun livreur avec GPS actif
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
<Text style={styles.sectionTitle}>
|
<Text style={styles.sectionTitle}>
|
||||||
Livreurs ({livreurs.length})
|
Livreurs ({livreurs.length})
|
||||||
@@ -765,7 +694,7 @@ export default function DeliveryScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{/* Fullscreen map modal */}
|
{/* ── Modal plein écran TomTomMap ── */}
|
||||||
<Modal
|
<Modal
|
||||||
visible={mapFullscreen}
|
visible={mapFullscreen}
|
||||||
animationType="fade"
|
animationType="fade"
|
||||||
@@ -775,27 +704,23 @@ export default function DeliveryScreen() {
|
|||||||
<StatusBar hidden={mapFullscreen} />
|
<StatusBar hidden={mapFullscreen} />
|
||||||
<View style={styles.fullscreenContainer}>
|
<View style={styles.fullscreenContainer}>
|
||||||
{livreursWithGPS.length > 0 && (
|
{livreursWithGPS.length > 0 && (
|
||||||
<MapView
|
<TomTomMap
|
||||||
ref={fullscreenMapRef}
|
ref={fullscreenMapRef}
|
||||||
provider={PROVIDER_DEFAULT}
|
|
||||||
style={styles.fullscreenMap}
|
style={styles.fullscreenMap}
|
||||||
initialRegion={{
|
markers={tomtomMarkers}
|
||||||
|
route={tomtomRoute}
|
||||||
|
destination={tomtomDestination}
|
||||||
|
initialCenter={{
|
||||||
latitude: livreursWithGPS[0].location.latitude,
|
latitude: livreursWithGPS[0].location.latitude,
|
||||||
longitude:
|
longitude:
|
||||||
livreursWithGPS[0].location.longitude,
|
livreursWithGPS[0].location.longitude,
|
||||||
latitudeDelta: 0.05,
|
|
||||||
longitudeDelta: 0.05,
|
|
||||||
}}
|
}}
|
||||||
onMapReady={() => fitAllMarkers(fullscreenMapRef)}
|
initialZoom={12}
|
||||||
showsUserLocation={false}
|
onMarkerPress={handleMarkerPress}
|
||||||
showsCompass
|
/>
|
||||||
showsScale
|
|
||||||
>
|
|
||||||
{renderMapMarkers()}
|
|
||||||
{renderRouteOverlay()}
|
|
||||||
</MapView>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Top bar */}
|
||||||
<View style={styles.fullscreenTopBar}>
|
<View style={styles.fullscreenTopBar}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.closeBtn}
|
style={styles.closeBtn}
|
||||||
@@ -812,7 +737,9 @@ export default function DeliveryScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.closeBtn}
|
style={styles.closeBtn}
|
||||||
onPress={() => fitAllMarkers(fullscreenMapRef)}
|
onPress={() =>
|
||||||
|
fullscreenMapRef.current?.fitAllMarkers()
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="locate-outline"
|
name="locate-outline"
|
||||||
@@ -822,6 +749,7 @@ export default function DeliveryScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Route bar plein écran */}
|
||||||
{routeInfo && selectedLivreur && (
|
{routeInfo && selectedLivreur && (
|
||||||
<View style={styles.fullscreenRouteBar}>
|
<View style={styles.fullscreenRouteBar}>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
@@ -848,6 +776,7 @@ export default function DeliveryScreen() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Bottom legend */}
|
||||||
<View style={styles.fullscreenBottomBar}>
|
<View style={styles.fullscreenBottomBar}>
|
||||||
<View style={styles.legendRow}>
|
<View style={styles.legendRow}>
|
||||||
<View style={styles.legendItem}>
|
<View style={styles.legendItem}>
|
||||||
@@ -888,7 +817,7 @@ export default function DeliveryScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Summary */}
|
{/* ── Résumé stats ── */}
|
||||||
<View style={styles.summaryRow}>
|
<View style={styles.summaryRow}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -919,6 +848,7 @@ export default function DeliveryScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* ── Liste livreurs ── */}
|
||||||
<FlatList
|
<FlatList
|
||||||
data={livreurs}
|
data={livreurs}
|
||||||
keyExtractor={(item) => item.username}
|
keyExtractor={(item) => item.username}
|
||||||
|
|||||||
Reference in New Issue
Block a user