1343 lines
52 KiB
TypeScript
1343 lines
52 KiB
TypeScript
import { useState, useEffect, useRef } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
|
|
import {
|
|
Package,
|
|
MapPin,
|
|
CheckCircle,
|
|
Navigation,
|
|
User,
|
|
AlertCircle,
|
|
MapPinned,
|
|
ShoppingBag,
|
|
BarChart3,
|
|
Shield,
|
|
} from "lucide-react";
|
|
import "./DeliveryDashboard.css";
|
|
import {
|
|
getDeliveryPersonDetails,
|
|
extractAdminUsernameFromToken,
|
|
} from "../../api/api_admin";
|
|
import {
|
|
getMyStatus,
|
|
updateMyStatus,
|
|
getMyQueue,
|
|
getMyDeliveries,
|
|
getDeliveryDetails,
|
|
startDelivery,
|
|
updateDeliveryStatus,
|
|
updateMyLocation,
|
|
isDeliveryAuthenticated,
|
|
triggerPoliceAlert,
|
|
endAlert,
|
|
getMyAlerts,
|
|
} from "../../api/api_delivery";
|
|
|
|
interface DeliveryStats {
|
|
todayDeliveries: number;
|
|
completedDeliveries: number;
|
|
pendingDeliveries: number;
|
|
approvedDeliveries: number;
|
|
totalDeliveries: number;
|
|
}
|
|
|
|
interface OrderItem {
|
|
produit: string;
|
|
prix: number;
|
|
quantite: number;
|
|
}
|
|
|
|
interface CurrentDelivery {
|
|
id: number;
|
|
orderNumber: string;
|
|
customerName: string;
|
|
customerPhone?: string;
|
|
deliveryAddress: string;
|
|
status: "assigned" | "picked_up" | "in_route";
|
|
distance: string;
|
|
items?: OrderItem[];
|
|
totalPrice?: number;
|
|
}
|
|
|
|
interface LocationState {
|
|
latitude: number | null;
|
|
longitude: number | null;
|
|
accuracy: number | null;
|
|
lastUpdate: Date | null;
|
|
permissionGranted: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
interface AlertConfig {
|
|
type: "success" | "error" | "warning" | "info";
|
|
title: string;
|
|
message: string;
|
|
onConfirm?: () => void;
|
|
}
|
|
|
|
function DeliveryDashboard() {
|
|
const navigate = useNavigate();
|
|
|
|
const [stats, setStats] = useState<DeliveryStats>({
|
|
todayDeliveries: 0,
|
|
completedDeliveries: 0,
|
|
pendingDeliveries: 0,
|
|
approvedDeliveries: 0,
|
|
totalDeliveries: 0,
|
|
});
|
|
|
|
const [currentDelivery, setCurrentDelivery] =
|
|
useState<CurrentDelivery | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [deliveryPersonName, setDeliveryPersonName] = useState("");
|
|
const [isAvailable, setIsAvailable] = useState(true);
|
|
const [currentStatus, setCurrentStatus] = useState<
|
|
"available" | "busy" | "offline"
|
|
>("offline");
|
|
|
|
const [location, setLocation] = useState<LocationState>({
|
|
latitude: null,
|
|
longitude: null,
|
|
accuracy: null,
|
|
lastUpdate: null,
|
|
permissionGranted: false,
|
|
error: null,
|
|
});
|
|
|
|
const [showLocationPrompt, setShowLocationPrompt] = useState(false);
|
|
const watchIdRef = useRef<number | null>(null);
|
|
|
|
const [showAlert, setShowAlert] = useState(false);
|
|
const [alertConfig, setAlertConfig] = useState<AlertConfig>({
|
|
type: "success",
|
|
title: "",
|
|
message: "",
|
|
onConfirm: undefined,
|
|
});
|
|
|
|
const [isAlertTriggered, setIsAlertTriggered] = useState(false);
|
|
const [policeAlertLoading, setPoliceAlertLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const checkAuth = () => {
|
|
if (!isDeliveryAuthenticated()) {
|
|
console.log(
|
|
"❌ [UserManagement] Cabine non authentifié, redirection vers /login-delivery/delivery",
|
|
);
|
|
navigate("/login-delivery/delivery", { replace: true });
|
|
}
|
|
};
|
|
|
|
checkAuth();
|
|
}, [navigate]);
|
|
|
|
useEffect(() => {
|
|
const authInterval = setInterval(() => {
|
|
if (!isDeliveryAuthenticated()) {
|
|
console.log(
|
|
"❌ [UserManagement] Session cabine expirée, redirection vers /login-delivery/delivery",
|
|
);
|
|
navigate("/login-delivery/delivery", { replace: true });
|
|
}
|
|
}, 5000);
|
|
|
|
return () => clearInterval(authInterval);
|
|
}, [navigate]);
|
|
|
|
const showStyledAlert = (
|
|
type: "success" | "error" | "warning" | "info",
|
|
title: string,
|
|
message: string,
|
|
onConfirm?: () => void,
|
|
) => {
|
|
setAlertConfig({ type, title, message, onConfirm });
|
|
setShowAlert(true);
|
|
};
|
|
|
|
const requestLocationPermission = async () => {
|
|
if (!navigator.geolocation) {
|
|
console.error(
|
|
"❌ [GEOLOCATION] Géolocalisation non supportée par ce navigateur",
|
|
);
|
|
setLocation((prev) => ({
|
|
...prev,
|
|
error: "Géolocalisation non supportée par votre navigateur",
|
|
}));
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
console.log("📍 [GEOLOCATION] Demande de permission...");
|
|
|
|
const position = await new Promise<GeolocationPosition>(
|
|
(resolve, reject) => {
|
|
navigator.geolocation.getCurrentPosition(resolve, reject, {
|
|
enableHighAccuracy: true,
|
|
timeout: 10000,
|
|
maximumAge: 0,
|
|
});
|
|
},
|
|
);
|
|
|
|
console.log("✅ [GEOLOCATION] Permission accordée!", {
|
|
lat: position.coords.latitude,
|
|
lng: position.coords.longitude,
|
|
accuracy: position.coords.accuracy,
|
|
});
|
|
|
|
setLocation({
|
|
latitude: position.coords.latitude,
|
|
longitude: position.coords.longitude,
|
|
accuracy: position.coords.accuracy,
|
|
lastUpdate: new Date(),
|
|
permissionGranted: true,
|
|
error: null,
|
|
});
|
|
|
|
setShowLocationPrompt(false);
|
|
|
|
await updateLocationOnServer(
|
|
position.coords.latitude,
|
|
position.coords.longitude,
|
|
);
|
|
startLocationTracking();
|
|
|
|
return true;
|
|
} catch (error: any) {
|
|
console.error("❌ [GEOLOCATION] Erreur permission:", error);
|
|
|
|
let errorMessage = "Impossible d'accéder à votre position";
|
|
|
|
if (error.code === 1) {
|
|
errorMessage = "Permission de géolocalisation refusée";
|
|
} else if (error.code === 2) {
|
|
errorMessage = "Position indisponible";
|
|
} else if (error.code === 3) {
|
|
errorMessage = "Délai d'attente dépassé";
|
|
}
|
|
|
|
setLocation((prev) => ({
|
|
...prev,
|
|
error: errorMessage,
|
|
permissionGranted: false,
|
|
}));
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const updateLocationOnServer = async (
|
|
latitude: number,
|
|
longitude: number,
|
|
) => {
|
|
try {
|
|
console.log("📡 [GEOLOCATION] Envoi position au serveur:", {
|
|
latitude,
|
|
longitude,
|
|
});
|
|
|
|
const result = await updateMyLocation(latitude, longitude);
|
|
|
|
if (!result.success) {
|
|
console.error("❌ [GEOLOCATION] Erreur:", result.error);
|
|
return;
|
|
}
|
|
|
|
console.log("✅ [GEOLOCATION] Position mise à jour sur le serveur");
|
|
|
|
if (result.status) {
|
|
setCurrentStatus(result.status as any);
|
|
setIsAvailable(result.status === "available");
|
|
}
|
|
} catch (error) {
|
|
console.error(
|
|
"❌ [GEOLOCATION] Erreur mise à jour serveur:",
|
|
error,
|
|
);
|
|
}
|
|
};
|
|
|
|
const startLocationTracking = () => {
|
|
if (!navigator.geolocation) return;
|
|
|
|
console.log("🎯 [GEOLOCATION] Démarrage du suivi continu...");
|
|
|
|
if (watchIdRef.current !== null) {
|
|
navigator.geolocation.clearWatch(watchIdRef.current);
|
|
}
|
|
|
|
watchIdRef.current = navigator.geolocation.watchPosition(
|
|
(position) => {
|
|
console.log("📍 [GEOLOCATION] Position mise à jour:", {
|
|
lat: position.coords.latitude,
|
|
lng: position.coords.longitude,
|
|
accuracy: position.coords.accuracy,
|
|
});
|
|
|
|
setLocation({
|
|
latitude: position.coords.latitude,
|
|
longitude: position.coords.longitude,
|
|
accuracy: position.coords.accuracy,
|
|
lastUpdate: new Date(),
|
|
permissionGranted: true,
|
|
error: null,
|
|
});
|
|
|
|
updateLocationOnServer(
|
|
position.coords.latitude,
|
|
position.coords.longitude,
|
|
);
|
|
},
|
|
(error) => {
|
|
console.error("❌ [GEOLOCATION] Erreur suivi:", error);
|
|
},
|
|
{
|
|
enableHighAccuracy: true,
|
|
timeout: 30000,
|
|
maximumAge: 0,
|
|
},
|
|
);
|
|
};
|
|
|
|
const stopLocationTracking = () => {
|
|
if (watchIdRef.current !== null) {
|
|
console.log("🛑 [GEOLOCATION] Arrêt du suivi");
|
|
navigator.geolocation.clearWatch(watchIdRef.current);
|
|
watchIdRef.current = null;
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const fetchDeliveryData = async () => {
|
|
try {
|
|
const username = extractAdminUsernameFromToken();
|
|
|
|
if (!username) {
|
|
console.error(
|
|
"❌ [DELIVERY_DASHBOARD] Username non trouvé",
|
|
);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
console.log(
|
|
"🔍 [DELIVERY_DASHBOARD] Chargement données pour:",
|
|
username,
|
|
);
|
|
|
|
const statusResult = await getMyStatus();
|
|
|
|
if (statusResult.success && statusResult.status) {
|
|
const status = statusResult.status.status;
|
|
setCurrentStatus(status);
|
|
setIsAvailable(status === "available");
|
|
console.log("✅ [DELIVERY_DASHBOARD] Statut:", status);
|
|
}
|
|
|
|
const queueResult = await getMyQueue();
|
|
|
|
// ✅ FIX: Vérification de queue_info avec optional chaining
|
|
if (queueResult.success && queueResult.queue_info) {
|
|
setStats((prev) => ({
|
|
...prev,
|
|
pendingDeliveries:
|
|
queueResult.queue_info?.queue_size || 0,
|
|
}));
|
|
console.log(
|
|
"✅ [DELIVERY_DASHBOARD] Queue:",
|
|
queueResult.queue_info.queue_size,
|
|
);
|
|
}
|
|
|
|
const deliveriesResult = await getMyDeliveries();
|
|
|
|
if (deliveriesResult.success && deliveriesResult.deliveries) {
|
|
const deliveries = deliveriesResult.deliveries;
|
|
console.log(
|
|
"✅ [DELIVERY_DASHBOARD] Livraisons:",
|
|
deliveries,
|
|
);
|
|
|
|
const completedToday = deliveries.filter((d: any) => {
|
|
const isToday =
|
|
new Date(d.updated_at).toDateString() ===
|
|
new Date().toDateString();
|
|
return d.status === "livre" && isToday;
|
|
}).length;
|
|
|
|
const totalCompleted = deliveries.filter(
|
|
(d: any) => d.status === "livre",
|
|
).length;
|
|
|
|
const approvedCount = deliveries.filter(
|
|
(d: any) => d.status === "approved",
|
|
).length;
|
|
console.log(
|
|
`📊 [DELIVERY_DASHBOARD] Commandes approved: ${approvedCount}`,
|
|
);
|
|
|
|
setStats((prev) => ({
|
|
...prev,
|
|
todayDeliveries: completedToday,
|
|
completedDeliveries: totalCompleted,
|
|
approvedDeliveries: approvedCount,
|
|
totalDeliveries: deliveries.length,
|
|
}));
|
|
|
|
let currentDeliveryData = deliveries.find(
|
|
(d: any) => d.status === "en_route",
|
|
);
|
|
|
|
if (!currentDeliveryData) {
|
|
currentDeliveryData = deliveries.find(
|
|
(d: any) => d.status === "assigned",
|
|
);
|
|
}
|
|
|
|
if (currentDeliveryData) {
|
|
console.log(
|
|
"📦 [DELIVERY_DASHBOARD] Livraison en cours:",
|
|
currentDeliveryData,
|
|
);
|
|
|
|
const detailsResult = await getDeliveryDetails(
|
|
currentDeliveryData.id,
|
|
);
|
|
|
|
if (detailsResult.success && detailsResult.delivery) {
|
|
const delivery = detailsResult.delivery.delivery;
|
|
const clientInfo =
|
|
detailsResult.delivery.client_info;
|
|
|
|
setCurrentDelivery({
|
|
id: delivery.id,
|
|
orderNumber: `CMD-${delivery.id}`,
|
|
customerName: clientInfo?.nom
|
|
? `${clientInfo.nom} ${clientInfo.prenom || ""}`.trim()
|
|
: clientInfo?.username || "Client",
|
|
customerPhone:
|
|
clientInfo?.telephone ||
|
|
"+33 X XX XX XX XX",
|
|
deliveryAddress:
|
|
delivery.adresse || "Adresse de livraison",
|
|
status:
|
|
delivery.status === "assigned"
|
|
? "assigned"
|
|
: "in_route",
|
|
distance: "N/A",
|
|
// ✅ FIX: Cast to any pour accéder à items qui peut exister mais n'est pas typé
|
|
items: (currentDeliveryData as any).items || [],
|
|
totalPrice: currentDeliveryData.total_prix || 0,
|
|
});
|
|
}
|
|
} else {
|
|
setCurrentDelivery(null);
|
|
}
|
|
}
|
|
|
|
const details = await getDeliveryPersonDetails(username);
|
|
// ✅ FIX: Cast to any car 'nom' n'existe pas dans le type mais peut exister dans la réponse
|
|
setDeliveryPersonName((details as any).nom || details.username);
|
|
|
|
// ✅ Vérifier les alertes actives au chargement
|
|
const alertsResult = await getMyAlerts();
|
|
if (alertsResult.success && alertsResult.alerts) {
|
|
const hasActiveAlert = alertsResult.alerts.some(
|
|
(alert) => alert.status === "true",
|
|
);
|
|
setIsAlertTriggered(hasActiveAlert);
|
|
console.log(
|
|
"📋 [DELIVERY_DASHBOARD] Alertes actives:",
|
|
hasActiveAlert,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error(
|
|
"❌ [DELIVERY_DASHBOARD] Erreur chargement données:",
|
|
error,
|
|
);
|
|
} finally {
|
|
setLoading(false);
|
|
|
|
setTimeout(() => {
|
|
setShowLocationPrompt(true);
|
|
}, 1000);
|
|
}
|
|
};
|
|
|
|
fetchDeliveryData();
|
|
|
|
const refreshInterval = setInterval(() => {
|
|
console.log(
|
|
"🔄 [DELIVERY_DASHBOARD] Rafraîchissement automatique...",
|
|
);
|
|
fetchDeliveryData();
|
|
}, 10000);
|
|
|
|
return () => {
|
|
stopLocationTracking();
|
|
clearInterval(refreshInterval);
|
|
};
|
|
}, []);
|
|
|
|
const handleStartDelivery = async () => {
|
|
if (!currentDelivery) return;
|
|
|
|
if (!location.latitude || !location.longitude) {
|
|
showStyledAlert(
|
|
"warning",
|
|
"Position GPS requise",
|
|
"Votre position GPS n'est pas disponible. Veuillez activer la géolocalisation pour démarrer la livraison.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log(
|
|
"📦 [DELIVERY_DASHBOARD] Démarrage de la livraison:",
|
|
currentDelivery.id,
|
|
);
|
|
|
|
const result = await startDelivery(
|
|
currentDelivery.id,
|
|
location.latitude,
|
|
location.longitude,
|
|
);
|
|
|
|
if (!result.success) {
|
|
console.error("❌ [DELIVERY_DASHBOARD] Erreur:", result.error);
|
|
showStyledAlert(
|
|
"error",
|
|
"Erreur",
|
|
result.error ||
|
|
"Une erreur est survenue lors du démarrage de la livraison.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
console.log("✅ [DELIVERY_DASHBOARD] Livraison démarrée");
|
|
showStyledAlert(
|
|
"success",
|
|
"Livraison démarrée",
|
|
"La livraison a été démarrée avec succès !",
|
|
() => window.location.reload(),
|
|
);
|
|
} catch (error) {
|
|
console.error(
|
|
"❌ [DELIVERY_DASHBOARD] Erreur démarrage livraison:",
|
|
error,
|
|
);
|
|
showStyledAlert(
|
|
"error",
|
|
"Erreur",
|
|
"Une erreur est survenue lors du démarrage de la livraison.",
|
|
);
|
|
}
|
|
};
|
|
|
|
const handleCompleteDelivery = async () => {
|
|
if (!currentDelivery) return;
|
|
|
|
if (!location.latitude || !location.longitude) {
|
|
showStyledAlert(
|
|
"warning",
|
|
"Position GPS requise",
|
|
"Votre position GPS n'est pas disponible. Veuillez activer la géolocalisation pour valider la livraison.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log(
|
|
"✅ [DELIVERY_DASHBOARD] Marquage livraison comme terminée:",
|
|
currentDelivery.id,
|
|
);
|
|
|
|
const result = await updateDeliveryStatus(
|
|
currentDelivery.id,
|
|
"livre",
|
|
location.latitude,
|
|
location.longitude,
|
|
);
|
|
|
|
if (!result.success) {
|
|
console.error("❌ [DELIVERY_DASHBOARD] Erreur:", result.error);
|
|
showStyledAlert(
|
|
"error",
|
|
"Erreur",
|
|
result.error ||
|
|
"Une erreur est survenue lors de la validation de la livraison.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
console.log("✅ [DELIVERY_DASHBOARD] Livraison terminée");
|
|
showStyledAlert(
|
|
"success",
|
|
"Livraison terminée",
|
|
"La livraison a été marquée comme terminée avec succès !",
|
|
() => window.location.reload(),
|
|
);
|
|
} catch (error) {
|
|
console.error(
|
|
"❌ [DELIVERY_DASHBOARD] Erreur complétion livraison:",
|
|
error,
|
|
);
|
|
showStyledAlert(
|
|
"error",
|
|
"Erreur",
|
|
"Une erreur est survenue lors de la validation de la livraison.",
|
|
);
|
|
}
|
|
};
|
|
|
|
const handleNavigate = () => {
|
|
if (currentDelivery) {
|
|
const address = encodeURIComponent(currentDelivery.deliveryAddress);
|
|
window.open(
|
|
`https://www.google.com/maps/search/?api=1&query=${address}`,
|
|
"_blank",
|
|
);
|
|
}
|
|
};
|
|
|
|
const toggleAvailability = async () => {
|
|
try {
|
|
const newStatus = isAvailable ? "offline" : "available";
|
|
|
|
console.log(
|
|
`🔄 [DELIVERY_DASHBOARD] Changement statut: ${currentStatus} -> ${newStatus}`,
|
|
);
|
|
|
|
const result = await updateMyStatus(newStatus);
|
|
|
|
if (!result.success) {
|
|
console.error("❌ [DELIVERY_DASHBOARD] Erreur:", result.error);
|
|
showStyledAlert(
|
|
"error",
|
|
"Erreur",
|
|
result.error ||
|
|
"Une erreur est survenue lors du changement de statut.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
console.log("✅ [DELIVERY_DASHBOARD] Statut mis à jour");
|
|
|
|
setIsAvailable(!isAvailable);
|
|
setCurrentStatus(newStatus);
|
|
} catch (error) {
|
|
console.error(
|
|
"❌ [DELIVERY_DASHBOARD] Erreur changement statut:",
|
|
error,
|
|
);
|
|
showStyledAlert(
|
|
"error",
|
|
"Erreur",
|
|
"Une erreur est survenue lors du changement de statut.",
|
|
);
|
|
}
|
|
};
|
|
|
|
const handlePoliceAlert = async () => {
|
|
if (policeAlertLoading) return;
|
|
|
|
setPoliceAlertLoading(true);
|
|
|
|
try {
|
|
console.log("🚨 [POLICE_ALERT] Déclenchement alerte police...");
|
|
|
|
const result = await triggerPoliceAlert();
|
|
|
|
if (!result.success) {
|
|
console.error("❌ [POLICE_ALERT] Erreur:", result.error);
|
|
showStyledAlert(
|
|
"error",
|
|
"Erreur",
|
|
result.error || "Impossible de déclencher l'alerte police.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
console.log("✅ [POLICE_ALERT] Alerte créée:", result);
|
|
setIsAlertTriggered(true);
|
|
|
|
showStyledAlert(
|
|
"success",
|
|
"Alerte Police Activée",
|
|
"Votre alerte police a été transmise avec succès. Les autorités et votre équipe ont été notifiées.",
|
|
);
|
|
|
|
// Vérifier les alertes actives pour mettre à jour l'état
|
|
const alertsResult = await getMyAlerts();
|
|
if (alertsResult.success && alertsResult.alerts) {
|
|
const hasActiveAlert = alertsResult.alerts.some(
|
|
(alert) => alert.status === "true",
|
|
);
|
|
setIsAlertTriggered(hasActiveAlert);
|
|
}
|
|
} catch (error) {
|
|
console.error("❌ [POLICE_ALERT] Erreur:", error);
|
|
showStyledAlert(
|
|
"error",
|
|
"Erreur",
|
|
"Une erreur est survenue lors du déclenchement de l'alerte police.",
|
|
);
|
|
} finally {
|
|
setPoliceAlertLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleEndPoliceAlert = async () => {
|
|
if (policeAlertLoading) return;
|
|
|
|
// Trouver l'alerte active
|
|
const alertsResult = await getMyAlerts();
|
|
if (!alertsResult.success || !alertsResult.alerts) {
|
|
showStyledAlert(
|
|
"error",
|
|
"Erreur",
|
|
"Impossible de récupérer les alertes actives.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
const activeAlert = alertsResult.alerts.find(
|
|
(alert) => alert.status === "true",
|
|
);
|
|
|
|
if (!activeAlert) {
|
|
showStyledAlert(
|
|
"warning",
|
|
"Aucune alerte active",
|
|
"Il n'y a pas d'alerte police active à terminer.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
setPoliceAlertLoading(true);
|
|
|
|
try {
|
|
console.log(
|
|
"🔚 [END_POLICE_ALERT] Fin d'alerte police:",
|
|
activeAlert.id,
|
|
);
|
|
|
|
const result = await endAlert(activeAlert.id);
|
|
|
|
if (!result.success) {
|
|
console.error("❌ [END_POLICE_ALERT] Erreur:", result.error);
|
|
showStyledAlert(
|
|
"error",
|
|
"Erreur",
|
|
result.error || "Impossible de terminer l'alerte police.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
console.log("✅ [END_POLICE_ALERT] Alerte terminée:", result);
|
|
setIsAlertTriggered(false);
|
|
|
|
showStyledAlert(
|
|
"success",
|
|
"Alerte Police Terminée",
|
|
"Votre alerte police a été désactivée avec succès.",
|
|
);
|
|
} catch (error) {
|
|
console.error("❌ [END_POLICE_ALERT] Erreur:", error);
|
|
showStyledAlert(
|
|
"error",
|
|
"Erreur",
|
|
"Une erreur est survenue lors de la désactivation de l'alerte police.",
|
|
);
|
|
} finally {
|
|
setPoliceAlertLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="delivery-container">
|
|
<div className="loading-delivery">
|
|
<p>Chargement de votre tableau de bord...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="delivery-container">
|
|
{/* Modal de demande de géolocalisation */}
|
|
{showLocationPrompt && !location.permissionGranted && (
|
|
<div className="location-modal-overlay">
|
|
<div className="location-modal">
|
|
<div className="location-modal-icon">
|
|
<MapPinned size={48} color="#10b981" />
|
|
</div>
|
|
<h2 className="location-modal-title">
|
|
Activer la localisation
|
|
</h2>
|
|
<p className="location-modal-text">
|
|
Pour vous assigner des livraisons et suivre vos
|
|
déplacements en temps réel, nous avons besoin
|
|
d'accéder à votre position GPS.
|
|
</p>
|
|
<div className="location-modal-buttons">
|
|
<button
|
|
className="location-button primary"
|
|
onClick={requestLocationPermission}
|
|
>
|
|
<MapPin size={20} />
|
|
Autoriser la localisation
|
|
</button>
|
|
<button
|
|
className="location-button secondary"
|
|
onClick={() => setShowLocationPrompt(false)}
|
|
>
|
|
Plus tard
|
|
</button>
|
|
</div>
|
|
{location.error && (
|
|
<p className="location-error">
|
|
⚠️ {location.error}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Modal d'alerte stylisée */}
|
|
{showAlert && (
|
|
<div
|
|
className="alert-modal-overlay"
|
|
onClick={() => setShowAlert(false)}
|
|
>
|
|
<div
|
|
className="alert-modal"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className={`alert-modal-icon ${alertConfig.type}`}>
|
|
{alertConfig.type === "success" && (
|
|
<CheckCircle size={48} />
|
|
)}
|
|
{alertConfig.type === "error" && (
|
|
<AlertCircle size={48} />
|
|
)}
|
|
{alertConfig.type === "warning" && (
|
|
<AlertCircle size={48} />
|
|
)}
|
|
{alertConfig.type === "info" && (
|
|
<Package size={48} />
|
|
)}
|
|
</div>
|
|
|
|
<h2 className="alert-modal-title">
|
|
{alertConfig.title}
|
|
</h2>
|
|
<p className="alert-modal-text">
|
|
{alertConfig.message}
|
|
</p>
|
|
|
|
<div className="alert-modal-buttons">
|
|
<button
|
|
className={`alert-button primary ${alertConfig.type}`}
|
|
onClick={() => {
|
|
setShowAlert(false);
|
|
if (alertConfig.onConfirm) {
|
|
alertConfig.onConfirm();
|
|
}
|
|
}}
|
|
>
|
|
OK
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Header */}
|
|
<div className="delivery-header">
|
|
<h1>Tableau de Bord Livreur</h1>
|
|
<p className="delivery-subtitle">
|
|
Gérez vos livraisons efficacement
|
|
</p>
|
|
<div className="welcome-message">
|
|
<User size={20} />
|
|
<span>Bonjour, {deliveryPersonName}!</span>
|
|
</div>
|
|
|
|
<div className="header-badges">
|
|
<div
|
|
className={`status-badge ${isAvailable ? "available" : "offline"}`}
|
|
onClick={toggleAvailability}
|
|
style={{ cursor: "pointer" }}
|
|
>
|
|
<div
|
|
style={{
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: "50%",
|
|
backgroundColor: isAvailable
|
|
? "#10b981"
|
|
: "#6b7280",
|
|
}}
|
|
/>
|
|
{isAvailable ? "Disponible" : "Hors ligne"}
|
|
</div>
|
|
|
|
{location.permissionGranted &&
|
|
location.latitude &&
|
|
location.longitude && (
|
|
<div
|
|
className="status-badge"
|
|
style={{
|
|
background:
|
|
"linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(37, 99, 235, 0.1))",
|
|
color: "#3b82f6",
|
|
border: "1px solid rgba(59, 130, 246, 0.3)",
|
|
cursor: "default",
|
|
}}
|
|
>
|
|
<MapPin size={14} />
|
|
GPS actif
|
|
</div>
|
|
)}
|
|
|
|
{!location.permissionGranted && (
|
|
<div
|
|
className="status-badge"
|
|
style={{
|
|
background:
|
|
"linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(220, 38, 38, 0.1))",
|
|
color: "#ef4444",
|
|
border: "1px solid rgba(239, 68, 68, 0.3)",
|
|
cursor: "pointer",
|
|
}}
|
|
onClick={() => setShowLocationPrompt(true)}
|
|
>
|
|
<MapPin size={14} />
|
|
GPS désactivé
|
|
</div>
|
|
)}
|
|
|
|
{/* Bouton Alerte Police - Un seul bouton à la fois */}
|
|
{!isAlertTriggered ? (
|
|
<button
|
|
className="police-alert-btn"
|
|
onClick={handlePoliceAlert}
|
|
disabled={policeAlertLoading}
|
|
style={{
|
|
background:
|
|
"linear-gradient(135deg, #dc2626, #991b1b)",
|
|
color: "white",
|
|
border: "none",
|
|
borderRadius: "12px",
|
|
padding: "0.75rem 1rem",
|
|
fontSize: "0.9rem",
|
|
fontWeight: "600",
|
|
cursor: policeAlertLoading
|
|
? "not-allowed"
|
|
: "pointer",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "0.5rem",
|
|
transition: "all 0.3s ease",
|
|
boxShadow: "0 4px 12px rgba(220, 38, 38, 0.3)",
|
|
opacity: policeAlertLoading ? 0.7 : 1,
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (!policeAlertLoading) {
|
|
e.currentTarget.style.transform =
|
|
"translateY(-2px)";
|
|
e.currentTarget.style.boxShadow =
|
|
"0 6px 16px rgba(220, 38, 38, 0.4)";
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (!policeAlertLoading) {
|
|
e.currentTarget.style.transform =
|
|
"translateY(0)";
|
|
e.currentTarget.style.boxShadow =
|
|
"0 4px 12px rgba(220, 38, 38, 0.3)";
|
|
}
|
|
}}
|
|
>
|
|
<Shield size={16} />
|
|
{policeAlertLoading ? "Envoi..." : "Alerte Police"}
|
|
</button>
|
|
) : (
|
|
<button
|
|
className="end-alert-btn"
|
|
onClick={handleEndPoliceAlert}
|
|
disabled={policeAlertLoading}
|
|
style={{
|
|
background:
|
|
"linear-gradient(135deg, #10b981, #059669)",
|
|
color: "white",
|
|
border: "none",
|
|
borderRadius: "12px",
|
|
padding: "0.75rem 1rem",
|
|
fontSize: "0.9rem",
|
|
fontWeight: "600",
|
|
cursor: policeAlertLoading
|
|
? "not-allowed"
|
|
: "pointer",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "0.5rem",
|
|
transition: "all 0.3s ease",
|
|
boxShadow: "0 4px 12px rgba(16, 185, 129, 0.3)",
|
|
opacity: policeAlertLoading ? 0.7 : 1,
|
|
animation: "pulse 2s infinite",
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (!policeAlertLoading) {
|
|
e.currentTarget.style.transform =
|
|
"translateY(-2px)";
|
|
e.currentTarget.style.boxShadow =
|
|
"0 6px 16px rgba(16, 185, 129, 0.4)";
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (!policeAlertLoading) {
|
|
e.currentTarget.style.transform =
|
|
"translateY(0)";
|
|
e.currentTarget.style.boxShadow =
|
|
"0 4px 12px rgba(16, 185, 129, 0.3)";
|
|
}
|
|
}}
|
|
>
|
|
<CheckCircle size={16} />
|
|
{policeAlertLoading ? "Arrêt..." : "Fin d'Alerte"}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Cards */}
|
|
<div className="delivery-stats-grid">
|
|
<div className="delivery-stat-card">
|
|
<div className="delivery-stat-icon blue">
|
|
<CheckCircle size={24} />
|
|
</div>
|
|
<div className="delivery-stat-content">
|
|
<p className="delivery-stat-label">Complétées</p>
|
|
<h3 className="delivery-stat-value">
|
|
{stats.completedDeliveries}
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="delivery-stat-card">
|
|
<div className="delivery-stat-icon orange">
|
|
<AlertCircle size={24} />
|
|
</div>
|
|
<div className="delivery-stat-content">
|
|
<p className="delivery-stat-label">Approved</p>
|
|
<h3 className="delivery-stat-value">
|
|
{stats.approvedDeliveries}
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className="delivery-stat-card stats-card"
|
|
onClick={() => navigate("/delivery/stats")}
|
|
style={{ cursor: "pointer" }}
|
|
>
|
|
<div className="delivery-stat-icon purple">
|
|
<BarChart3 size={24} />
|
|
</div>
|
|
<div className="delivery-stat-content">
|
|
<p className="delivery-stat-label">Voir Statistiques</p>
|
|
<h3 className="delivery-stat-value">Détails</h3>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className="delivery-stat-card stats-card"
|
|
onClick={() => navigate("/delivery/alerts")}
|
|
style={{ cursor: "pointer" }}
|
|
>
|
|
<div className="delivery-stat-icon red">
|
|
<AlertCircle size={24} />
|
|
</div>
|
|
<div className="delivery-stat-content">
|
|
<p className="delivery-stat-label">Alertes</p>
|
|
<h3 className="delivery-stat-value">Détails</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Current Delivery Section */}
|
|
<div className="current-delivery-section">
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
marginBottom: "1rem",
|
|
}}
|
|
>
|
|
<h2>Livraison en Cours</h2>
|
|
{stats.pendingDeliveries > 0 && (
|
|
<div
|
|
style={{
|
|
background:
|
|
"linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(37, 99, 235, 0.1))",
|
|
padding: "0.5rem 1rem",
|
|
borderRadius: "12px",
|
|
border: "1px solid rgba(59, 130, 246, 0.3)",
|
|
color: "#3b82f6",
|
|
fontSize: "0.9rem",
|
|
fontWeight: "600",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "0.5rem",
|
|
}}
|
|
>
|
|
<i
|
|
className="fas fa-box"
|
|
style={{ fontSize: "1rem" }}
|
|
></i>
|
|
{stats.pendingDeliveries} livraison
|
|
{stats.pendingDeliveries > 1 ? "s" : ""} en file
|
|
d'attente
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{currentDelivery ? (
|
|
<div className="current-delivery-card">
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
marginBottom: "1rem",
|
|
paddingBottom: "1rem",
|
|
borderBottom: "1px solid #e5e7eb",
|
|
}}
|
|
>
|
|
<h3 style={{ margin: 0, color: "#fff" }}>
|
|
Détails de la Livraison
|
|
</h3>
|
|
<span
|
|
style={{
|
|
padding: "0.5rem 1rem",
|
|
borderRadius: "20px",
|
|
fontSize: "0.875rem",
|
|
fontWeight: "600",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "0.5rem",
|
|
background:
|
|
currentDelivery.status === "assigned"
|
|
? "linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(37, 99, 235, 0.1))"
|
|
: "linear-gradient(135deg, rgba(251, 146, 60, 0.2), rgba(249, 115, 22, 0.1))",
|
|
color:
|
|
currentDelivery.status === "assigned"
|
|
? "#3b82f6"
|
|
: "#fb923c",
|
|
border: `1px solid ${
|
|
currentDelivery.status === "assigned"
|
|
? "#3b82f6"
|
|
: "#fb923c"
|
|
}`,
|
|
}}
|
|
>
|
|
{currentDelivery.status === "assigned" && (
|
|
<>
|
|
<i
|
|
className="fas fa-box"
|
|
style={{ fontSize: "0.875rem" }}
|
|
></i>
|
|
Assignée
|
|
</>
|
|
)}
|
|
{currentDelivery.status === "in_route" && (
|
|
<>
|
|
<i
|
|
className="fas fa-truck"
|
|
style={{ fontSize: "0.875rem" }}
|
|
></i>
|
|
En Route
|
|
</>
|
|
)}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="delivery-info-grid">
|
|
<div className="delivery-info-item">
|
|
<div className="delivery-info-icon">
|
|
<Package size={20} />
|
|
</div>
|
|
<div className="delivery-info-content">
|
|
<h4>N° Commande</h4>
|
|
<p>{currentDelivery.orderNumber}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="delivery-info-item">
|
|
<div className="delivery-info-icon">
|
|
<User size={20} />
|
|
</div>
|
|
<div className="delivery-info-content">
|
|
<h4>Client</h4>
|
|
<p>{currentDelivery.customerName}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="delivery-info-item">
|
|
<div className="delivery-info-icon">
|
|
<MapPin size={20} />
|
|
</div>
|
|
<div className="delivery-info-content">
|
|
<h4>Adresse</h4>
|
|
<p>{currentDelivery.deliveryAddress}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section Produits */}
|
|
{currentDelivery.items &&
|
|
currentDelivery.items.length > 0 && (
|
|
<div className="order-items-section">
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "0.5rem",
|
|
marginBottom: "1rem",
|
|
paddingTop: "1.5rem",
|
|
borderTop:
|
|
"1px solid rgba(255, 255, 255, 0.1)",
|
|
}}
|
|
>
|
|
<ShoppingBag
|
|
size={20}
|
|
color="#10b981"
|
|
/>
|
|
<h4
|
|
style={{
|
|
margin: 0,
|
|
color: "#fff",
|
|
fontSize: "1.1rem",
|
|
fontWeight: "600",
|
|
}}
|
|
>
|
|
Produits à livrer
|
|
</h4>
|
|
</div>
|
|
|
|
<div className="order-items-list">
|
|
{currentDelivery.items.map(
|
|
(item, index) => (
|
|
<div
|
|
key={index}
|
|
className="order-item"
|
|
>
|
|
<div style={{ flex: 1 }}>
|
|
<p className="order-item-name">
|
|
{item.produit}
|
|
</p>
|
|
<p className="order-item-quantity">
|
|
Quantité:{" "}
|
|
{item.quantite}
|
|
</p>
|
|
</div>
|
|
<p className="order-item-price">
|
|
{item.prix.toFixed(2)} €
|
|
</p>
|
|
</div>
|
|
),
|
|
)}
|
|
</div>
|
|
|
|
<div className="order-total">
|
|
<span>Total de la commande</span>
|
|
<span className="order-total-amount">
|
|
{currentDelivery.totalPrice?.toFixed(
|
|
2,
|
|
)}{" "}
|
|
€
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="delivery-actions">
|
|
{currentDelivery.status === "assigned" && (
|
|
<button
|
|
className="delivery-button primary"
|
|
onClick={handleStartDelivery}
|
|
>
|
|
<Package size={20} />
|
|
Démarrer la Livraison
|
|
</button>
|
|
)}
|
|
|
|
{currentDelivery.status === "in_route" && (
|
|
<button
|
|
className="delivery-button primary"
|
|
onClick={handleCompleteDelivery}
|
|
>
|
|
<CheckCircle size={20} />
|
|
Marquer comme Livrée
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
className="delivery-button secondary"
|
|
onClick={handleNavigate}
|
|
>
|
|
<Navigation size={20} />
|
|
Navigation GPS
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="current-delivery-card no-delivery-card">
|
|
<Package size={48} color="#888" />
|
|
<p>Aucune livraison en cours</p>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "0.5rem",
|
|
marginTop: "1rem",
|
|
fontSize: "0.9rem",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "0.5rem",
|
|
color: "#10b981",
|
|
marginLeft: "30px",
|
|
}}
|
|
></div>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "0.5rem",
|
|
color: "#6b7280",
|
|
}}
|
|
>
|
|
<span>
|
|
Les commandes vous seront assignées
|
|
automatiquement selon votre proximité
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default DeliveryDashboard;
|