1660 lines
83 KiB
TypeScript
1660 lines
83 KiB
TypeScript
// ============================================
|
|
// pages/SuiviLivraison.tsx - VERSION FINALE CORRIGÉE
|
|
// ============================================
|
|
// ✅ Calcul total identique à Checkout (somme des prix)
|
|
// ✅ FIX: Gestion correcte du code 409 Conflict
|
|
// ✅ FIX: Suppression des useState non utilisés
|
|
// ✅ AJOUT: Vérification continue de l'authentification
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import {
|
|
getMyOrders,
|
|
getOrderTracking,
|
|
getOrderETA,
|
|
confirmReception,
|
|
cancelCommand,
|
|
updateOwnCommandAddress,
|
|
isUserAuthenticated,
|
|
getPublicSettings,
|
|
} from "../../api/api";
|
|
import type {
|
|
ETAResponse,
|
|
TrackingResponse,
|
|
OrderDetail,
|
|
OrderItem,
|
|
CancelCommandResponse,
|
|
} from "../../api/api_types";
|
|
import Navbar from "../../components/Navbar";
|
|
import Toast from "../../components/Toast";
|
|
import "./SuiviLivraison.css";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
|
import {
|
|
faHourglassHalf,
|
|
faTruck,
|
|
faBox,
|
|
faCheckCircle,
|
|
faTimesCircle,
|
|
faQuestionCircle,
|
|
faMapMarkerAlt,
|
|
faBiking,
|
|
faClock,
|
|
faShoppingCart,
|
|
faMoneyBillWave,
|
|
faCalendarAlt,
|
|
faSync,
|
|
faGift,
|
|
faLightbulb,
|
|
faCheck,
|
|
faExclamationTriangle,
|
|
faLeaf,
|
|
faWind,
|
|
faChevronUp,
|
|
faChevronDown,
|
|
} from "@fortawesome/free-solid-svg-icons";
|
|
|
|
interface OrderWithTracking extends OrderDetail {
|
|
tracking?: TrackingResponse;
|
|
eta?: ETAResponse;
|
|
}
|
|
|
|
interface ToastMessage {
|
|
id: string;
|
|
message: string;
|
|
type: "success" | "error" | "warning" | "info";
|
|
}
|
|
|
|
// ============================================
|
|
// ✅ HELPERS - Calcul total identique à Checkout
|
|
// ============================================
|
|
|
|
/**
|
|
* ✅ Calculer le total EXACTEMENT comme dans Checkout.tsx
|
|
* Somme des prix individuels (pas de multiplication)
|
|
*/
|
|
const getTotalAmount = (order: OrderWithTracking): number => {
|
|
if (typeof order.total_prix === "number" && order.total_prix > 0) {
|
|
return order.total_prix;
|
|
}
|
|
if (typeof order.total === "number" && order.total > 0) {
|
|
return order.total;
|
|
}
|
|
if (order.items && order.items.length > 0) {
|
|
return order.items.reduce((sum, item) => {
|
|
const itemPrice = item.prix || item.price || 0;
|
|
return sum + itemPrice;
|
|
}, 0);
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
const getClientInfo = (order: OrderWithTracking) => {
|
|
const firstName = order.first_name || order.client_prenom || "";
|
|
const lastName = order.last_name || order.client_nom || "";
|
|
const phone = order.phone || order.client_telephone || "";
|
|
|
|
return { firstName, lastName, phone };
|
|
};
|
|
|
|
const getDeliveryAddress = (order: OrderWithTracking): string => {
|
|
return order.delivery_address || order.adresse || "Non disponible";
|
|
};
|
|
|
|
const formatOrderItem = (item: OrderItem) => {
|
|
return {
|
|
name: String(
|
|
item.produit || item.product_name || item.name_product || "Produit",
|
|
),
|
|
quantity: Number(item.quantite || item.quantity || 0),
|
|
price: Number(item.prix || item.price || 0),
|
|
};
|
|
};
|
|
|
|
const getStatusColor = (status: string): string => {
|
|
switch (status?.toLowerCase()) {
|
|
case "pending":
|
|
return "linear-gradient(135deg, #ddd6fe 0%, #a78bfa 50%, #7c3aed 100%)";
|
|
case "assigned":
|
|
return "linear-gradient(135deg, #fef3c7 0%, #fbbf24 50%, #f59e0b 100%)";
|
|
case "en_route":
|
|
return "linear-gradient(135deg, #bfdbfe 0%, #60a5fa 50%, #3b82f6 100%)";
|
|
case "arrived":
|
|
return "linear-gradient(135deg, #bbf7d0 0%, #4ade80 50%, #22c55e 100%)";
|
|
case "livre":
|
|
return "linear-gradient(135deg, #f3e8ff 0%, #d8b4fe 50%, #a855f7 100%)";
|
|
case "approved":
|
|
return "linear-gradient(135deg, #c7d2fe 0%, #a5b4fc 50%, #6366f1 100%)";
|
|
case "cancelled":
|
|
return "linear-gradient(135deg, #fae8ff 0%, #f0abfc 50%, #c026d3 100%)";
|
|
default:
|
|
return "linear-gradient(135deg, #e5e7eb 0%, #9ca3af 50%, #6b7280 100%)";
|
|
}
|
|
};
|
|
|
|
const getStatusLabel = (status: string): string => {
|
|
const statusMap: Record<string, string> = {
|
|
pending: "En attente d'assignation",
|
|
assigned: "Livreur assigné",
|
|
en_route: "En route vers vous",
|
|
arrived: "Livreur arrivé",
|
|
livre: "Livré - À confirmer",
|
|
approved: "Livraison confirmée",
|
|
cancelled: "Annulée",
|
|
};
|
|
|
|
return statusMap[status?.toLowerCase()] || "Statut inconnu";
|
|
};
|
|
|
|
const getStatusIcon = (status: string): IconDefinition => {
|
|
const iconMap: Record<string, IconDefinition> = {
|
|
pending: faHourglassHalf,
|
|
assigned: faBiking,
|
|
en_route: faTruck,
|
|
arrived: faMapMarkerAlt,
|
|
livre: faBox,
|
|
approved: faCheckCircle,
|
|
cancelled: faTimesCircle,
|
|
};
|
|
|
|
return iconMap[status?.toLowerCase()] || faQuestionCircle;
|
|
};
|
|
|
|
/**
|
|
* ✅ Calculer les points avec le TOTAL (pas item par item)
|
|
*/
|
|
const calculateOrderPoints = (
|
|
order: OrderWithTracking,
|
|
poolNames: string[] = [],
|
|
): {
|
|
points: number;
|
|
category: string;
|
|
categoryDisplay: string;
|
|
categoryIcon: IconDefinition;
|
|
categoryColor: string;
|
|
} => {
|
|
// Totaux indexés par pool (+ index spécial pour "gros&semi" exclu des points)
|
|
const poolTotals: number[] = poolNames.map(() => 0);
|
|
let excludedTotal = 0;
|
|
|
|
if (order.items && order.items.length > 0) {
|
|
order.items.forEach((item: OrderItem) => {
|
|
const cat = String(item.category || "").toLowerCase();
|
|
const itemPrice = Number(item.prix || item.price || 0);
|
|
|
|
if (cat.includes("gros") || cat.includes("semi")) {
|
|
excludedTotal += itemPrice;
|
|
return;
|
|
}
|
|
|
|
// Trouver le pool correspondant par nom (insensible à la casse)
|
|
const poolIdx = poolNames.findIndex((name) =>
|
|
cat.includes(name.toLowerCase().replace(/[&\s]/g, "")),
|
|
);
|
|
if (poolIdx >= 0) {
|
|
poolTotals[poolIdx] += itemPrice;
|
|
} else if (poolTotals.length > 0) {
|
|
// Fallback : pool 0 si aucun match
|
|
poolTotals[0] += itemPrice;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Gros&Semi uniquement → 0 points
|
|
if (excludedTotal > 0 && poolTotals.every((t) => t === 0)) {
|
|
return {
|
|
points: 0,
|
|
category: "excluded",
|
|
categoryDisplay: "Gros&Semi",
|
|
categoryIcon: faBox,
|
|
categoryColor: "#9ca3af",
|
|
};
|
|
}
|
|
|
|
// Pool dominant = celui avec le plus grand total
|
|
const dominantIdx = poolTotals.reduce(
|
|
(best, val, i) => (val > poolTotals[best] ? i : best),
|
|
0,
|
|
);
|
|
const dominantTotal = poolTotals[dominantIdx];
|
|
const categoryName = poolNames[dominantIdx] ?? "";
|
|
const poolIcons = [faWind, faLeaf, faGift, faBox];
|
|
const poolColors = ["#3b82f6", "#10b981", "#7c3aed", "#f59e0b"];
|
|
|
|
let points = 0;
|
|
if (dominantTotal >= 30 && dominantTotal <= 100) points = 1;
|
|
else if (dominantTotal >= 110 && dominantTotal <= 200) points = 2;
|
|
else if (dominantTotal >= 210 && dominantTotal <= 300) points = 3;
|
|
else if (dominantTotal >= 310 && dominantTotal <= 400) points = 5;
|
|
else if (dominantTotal > 400) points = 10;
|
|
|
|
return {
|
|
points,
|
|
category: categoryName.toLowerCase().replace(/\s/g, "_"),
|
|
categoryDisplay: categoryName,
|
|
categoryIcon: poolIcons[dominantIdx] ?? faGift,
|
|
categoryColor: poolColors[dominantIdx] ?? "#7c3aed",
|
|
};
|
|
};
|
|
|
|
// ============================================
|
|
// TIMELINE STEPS
|
|
// ============================================
|
|
|
|
const TIMELINE_STEPS = [
|
|
{ key: "pending", label: "En attente", icon: faHourglassHalf },
|
|
{ key: "assigned", label: "Assigné", icon: faBiking },
|
|
{ key: "en_route", label: "En route", icon: faTruck },
|
|
{ key: "arrived", label: "Arrivé", icon: faMapMarkerAlt },
|
|
{ key: "livre", label: "Livré", icon: faBox },
|
|
{ key: "approved", label: "Confirmé", icon: faCheckCircle },
|
|
];
|
|
|
|
const STATUS_ORDER: Record<string, number> = {
|
|
pending: 0,
|
|
assigned: 1,
|
|
en_route: 2,
|
|
arrived: 3,
|
|
livre: 4,
|
|
approved: 5,
|
|
};
|
|
|
|
// ============================================
|
|
// COMPONENT PRINCIPAL
|
|
// ============================================
|
|
|
|
function SuiviLivraison() {
|
|
const navigate = useNavigate();
|
|
const [orders, setOrders] = useState<OrderWithTracking[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState("");
|
|
const [expandedOrder, setExpandedOrder] = useState<number | null>(null);
|
|
const [confirming, setConfirming] = useState<number | null>(null);
|
|
|
|
const [toasts, setToasts] = useState<ToastMessage[]>([]);
|
|
|
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
|
const [orderToConfirm, setOrderToConfirm] = useState<number | null>(null);
|
|
const [selectedOrderPoints, setSelectedOrderPoints] = useState<number>(0);
|
|
const [selectedOrderCategoryDisplay, setSelectedOrderCategoryDisplay] =
|
|
useState<string>("");
|
|
|
|
const [cancellingOrder, setCancellingOrder] = useState<number | null>(null);
|
|
const [showCancelDialog, setShowCancelDialog] = useState(false);
|
|
const [orderToCancel, setOrderToCancel] = useState<number | null>(null);
|
|
const [cancelReason, setCancelReason] = useState("");
|
|
const [showPenaltyWarning, setShowPenaltyWarning] = useState(false);
|
|
const [penaltyWarningData, setPenaltyWarningData] =
|
|
useState<CancelCommandResponse | null>(null);
|
|
const [poolNames, setPoolNames] = useState<string[]>([]);
|
|
|
|
const [editingAddressOrder, setEditingAddressOrder] = useState<
|
|
number | null
|
|
>(null);
|
|
const [newAddress, setNewAddress] = useState("");
|
|
const [editAddressLoading, setEditAddressLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
getPublicSettings().then((s) => setPoolNames(s.pool_names ?? []));
|
|
}, []);
|
|
|
|
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
|
|
useEffect(() => {
|
|
const checkAuth = () => {
|
|
if (!isUserAuthenticated()) {
|
|
console.log(
|
|
"❌ [SuiviLivraison] Utilisateur non authentifié, redirection vers /login/client",
|
|
);
|
|
navigate("/login/client", { replace: true });
|
|
}
|
|
};
|
|
|
|
checkAuth();
|
|
}, [navigate]);
|
|
|
|
// ✅ VÉRIFICATION CONTINUE (toutes les 5 secondes)
|
|
useEffect(() => {
|
|
const authInterval = setInterval(() => {
|
|
if (!isUserAuthenticated()) {
|
|
console.log(
|
|
"❌ [SuiviLivraison] Session expirée, redirection vers /login/client",
|
|
);
|
|
navigate("/login/client", { replace: true });
|
|
}
|
|
}, 5000);
|
|
|
|
return () => clearInterval(authInterval);
|
|
}, [navigate]);
|
|
|
|
useEffect(() => {
|
|
loadOrders();
|
|
const interval = setInterval(loadOrders, 10000);
|
|
return () => clearInterval(interval);
|
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const loadOrders = async () => {
|
|
if (!isUserAuthenticated()) {
|
|
navigate("/login/client", { replace: true });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await getMyOrders();
|
|
|
|
if (response.success) {
|
|
const ordersWithTracking = await Promise.all(
|
|
(response.commands || []).map(
|
|
async (order: OrderDetail) => {
|
|
const normalizedOrder = {
|
|
...order,
|
|
total: getTotalAmount(order),
|
|
};
|
|
|
|
let tracking;
|
|
let eta;
|
|
|
|
try {
|
|
tracking = await getOrderTracking(order.id);
|
|
} catch {
|
|
tracking = undefined;
|
|
}
|
|
|
|
try {
|
|
eta = await getOrderETA(order.id);
|
|
} catch {
|
|
eta = undefined;
|
|
}
|
|
|
|
return {
|
|
...normalizedOrder,
|
|
tracking,
|
|
eta,
|
|
};
|
|
},
|
|
),
|
|
);
|
|
|
|
setOrders(ordersWithTracking);
|
|
setError("");
|
|
}
|
|
} catch (err: unknown) {
|
|
console.error("Erreur loadOrders:", err);
|
|
const msg =
|
|
err instanceof Error
|
|
? err.message
|
|
: "Erreur lors du chargement";
|
|
setError(msg);
|
|
showToast(msg, "error");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const showToast = (
|
|
message: string,
|
|
type: "success" | "error" | "warning" | "info",
|
|
) => {
|
|
const id = Date.now().toString();
|
|
setToasts((prev) => [...prev, { id, message, type }]);
|
|
};
|
|
|
|
const removeToast = (id: string) => {
|
|
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
|
};
|
|
|
|
const openConfirmDialog = (orderId: number) => {
|
|
// ✅ Vérifier l'auth avant d'ouvrir le dialog
|
|
if (!isUserAuthenticated()) {
|
|
console.log("❌ [openConfirmDialog] Non authentifié");
|
|
navigate("/login/client", { replace: true });
|
|
return;
|
|
}
|
|
|
|
const order = orders.find((o) => o.id === orderId);
|
|
|
|
if (order) {
|
|
const { points, categoryDisplay } = calculateOrderPoints(
|
|
order,
|
|
poolNames,
|
|
);
|
|
setSelectedOrderPoints(points);
|
|
setSelectedOrderCategoryDisplay(categoryDisplay);
|
|
} else {
|
|
setSelectedOrderPoints(0);
|
|
setSelectedOrderCategoryDisplay("");
|
|
}
|
|
|
|
setOrderToConfirm(orderId);
|
|
setShowConfirmDialog(true);
|
|
};
|
|
|
|
const closeConfirmDialog = () => {
|
|
setShowConfirmDialog(false);
|
|
setOrderToConfirm(null);
|
|
setSelectedOrderPoints(0);
|
|
setSelectedOrderCategoryDisplay("");
|
|
};
|
|
|
|
const handleConfirmReception = async () => {
|
|
if (!orderToConfirm || confirming) return;
|
|
|
|
// ✅ Vérifier l'auth avant de confirmer
|
|
if (!isUserAuthenticated()) {
|
|
console.log("❌ [handleConfirmReception] Non authentifié");
|
|
navigate("/login/client", { replace: true });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setConfirming(orderToConfirm);
|
|
closeConfirmDialog();
|
|
|
|
showToast("Confirmation en cours...", "info");
|
|
|
|
const response = await confirmReception(orderToConfirm);
|
|
|
|
if (response.success) {
|
|
const pointsEarned =
|
|
response.points_earned || selectedOrderPoints;
|
|
const apiCategory =
|
|
response.category || response.data?.category || "";
|
|
|
|
const displayCategory =
|
|
apiCategory && apiCategory !== "total"
|
|
? apiCategory
|
|
: selectedOrderCategoryDisplay;
|
|
|
|
showToast(
|
|
`Commande confirmée! +${pointsEarned} point${pointsEarned > 1 ? "s" : ""} ${displayCategory}`,
|
|
"success",
|
|
);
|
|
setConfirming(null);
|
|
loadOrders();
|
|
} else {
|
|
setError(response.message || "Erreur lors de la confirmation");
|
|
showToast(
|
|
response.message || "Erreur lors de la confirmation",
|
|
"error",
|
|
);
|
|
setConfirming(null);
|
|
}
|
|
} catch (err: unknown) {
|
|
const msg = err instanceof Error ? err.message : "Erreur serveur";
|
|
setError(msg);
|
|
showToast(msg, "error");
|
|
setConfirming(null);
|
|
}
|
|
};
|
|
|
|
const openCancelDialog = (orderId: number) => {
|
|
// ✅ Vérifier l'auth avant d'ouvrir le dialog
|
|
if (!isUserAuthenticated()) {
|
|
console.log("❌ [openCancelDialog] Non authentifié");
|
|
navigate("/login/client", { replace: true });
|
|
return;
|
|
}
|
|
|
|
setOrderToCancel(orderId);
|
|
setCancelReason("");
|
|
setShowPenaltyWarning(false);
|
|
setPenaltyWarningData(null);
|
|
setShowCancelDialog(true);
|
|
};
|
|
|
|
const closeCancelDialog = () => {
|
|
setShowCancelDialog(false);
|
|
setOrderToCancel(null);
|
|
setCancelReason("");
|
|
setShowPenaltyWarning(false);
|
|
setPenaltyWarningData(null);
|
|
};
|
|
|
|
const handleCancelOrder = async (force: boolean = false) => {
|
|
if (!orderToCancel) return;
|
|
|
|
// ✅ Vérifier l'auth avant d'annuler
|
|
if (!isUserAuthenticated()) {
|
|
console.log("❌ [handleCancelOrder] Non authentifié");
|
|
navigate("/login/client", { replace: true });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setCancellingOrder(orderToCancel);
|
|
|
|
if (!force) {
|
|
showToast("Vérification en cours...", "info");
|
|
}
|
|
|
|
const response = await cancelCommand(
|
|
orderToCancel,
|
|
cancelReason,
|
|
force,
|
|
);
|
|
|
|
if (response.warning && response.penalty_warning && !force) {
|
|
console.log(
|
|
"⚠️ [CANCEL] Avertissement reçu:",
|
|
response.penalty_warning,
|
|
);
|
|
setPenaltyWarningData(response);
|
|
setShowPenaltyWarning(true);
|
|
setCancellingOrder(null);
|
|
return;
|
|
}
|
|
|
|
if (response.success) {
|
|
let message = "Commande annulée avec succès";
|
|
|
|
if (response.penalty) {
|
|
message += ` (Pénalité: ${response.penalty.points} points)`;
|
|
showToast(message, "warning");
|
|
} else if (response.info) {
|
|
showToast(message + " - " + response.info, "success");
|
|
} else {
|
|
showToast(message, "success");
|
|
}
|
|
|
|
closeCancelDialog();
|
|
loadOrders();
|
|
} else {
|
|
showToast(
|
|
response.message || "Erreur lors de l'annulation",
|
|
"error",
|
|
);
|
|
}
|
|
} catch (error: unknown) {
|
|
console.error("❌ [CANCEL] Erreur:", error);
|
|
showToast(
|
|
error instanceof Error
|
|
? error.message
|
|
: "Erreur lors de l'annulation",
|
|
"error",
|
|
);
|
|
} finally {
|
|
setCancellingOrder(null);
|
|
}
|
|
};
|
|
|
|
const confirmCancelWithPenalty = () => {
|
|
setShowPenaltyWarning(false);
|
|
handleCancelOrder(true);
|
|
};
|
|
|
|
const openEditAddressDialog = (orderId: number) => {
|
|
if (!isUserAuthenticated()) {
|
|
navigate("/login/client", { replace: true });
|
|
return;
|
|
}
|
|
const order = orders.find((o) => o.id === orderId);
|
|
setNewAddress(order ? getDeliveryAddress(order) : "");
|
|
setEditingAddressOrder(orderId);
|
|
};
|
|
|
|
const closeEditAddressDialog = () => {
|
|
setEditingAddressOrder(null);
|
|
setNewAddress("");
|
|
};
|
|
|
|
const handleUpdateAddress = async () => {
|
|
if (!editingAddressOrder || !newAddress.trim()) return;
|
|
|
|
try {
|
|
setEditAddressLoading(true);
|
|
const response = await updateOwnCommandAddress(
|
|
editingAddressOrder,
|
|
newAddress.trim(),
|
|
);
|
|
|
|
if (response.success) {
|
|
showToast("Adresse mise à jour", "success");
|
|
closeEditAddressDialog();
|
|
loadOrders();
|
|
} else {
|
|
showToast(
|
|
response.message || "Erreur lors de la mise à jour",
|
|
"error",
|
|
);
|
|
}
|
|
} catch (error: unknown) {
|
|
showToast(
|
|
error instanceof Error
|
|
? error.message
|
|
: "Erreur lors de la mise à jour de l'adresse",
|
|
"error",
|
|
);
|
|
} finally {
|
|
setEditAddressLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loading && orders.length === 0) {
|
|
return (
|
|
<>
|
|
<Navbar />
|
|
<div className="suivi-container">
|
|
<div className="loading-state">
|
|
<div className="spinner"></div>
|
|
<p>Chargement de vos commandes...</p>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Navbar />
|
|
<div className="suivi-container">
|
|
<div className="suivi-header">
|
|
<h1>Suivi de vos commandes</h1>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="error-banner">
|
|
<FontAwesomeIcon icon={faExclamationTriangle} /> {error}
|
|
<button onClick={() => setError("")}>✕</button>
|
|
</div>
|
|
)}
|
|
|
|
{orders.length === 0 ? (
|
|
<div className="empty-state">
|
|
<div className="empty-state-icon">
|
|
<FontAwesomeIcon icon={faShoppingCart} />
|
|
</div>
|
|
<h2>Aucune commande trouvée</h2>
|
|
<p>Vous n'avez pas encore passé de commande.</p>
|
|
<button
|
|
className="action-button"
|
|
onClick={() => navigate("/user/nos-produits")}
|
|
>
|
|
Découvrir nos produits
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="orders-list">
|
|
{orders.map((order) => {
|
|
const statusLow = order.status?.toLowerCase() ?? "";
|
|
const currentIdx = STATUS_ORDER[statusLow] ?? -1;
|
|
const isCancelled = statusLow === "cancelled";
|
|
const showEta =
|
|
order.eta?.eta_available &&
|
|
order.eta.eta_minutes > 0 &&
|
|
statusLow === "en_route";
|
|
|
|
// Heure d'arrivée estimée : depuis le backend ou calculée côté client
|
|
const computedArrival = (() => {
|
|
if (statusLow !== "en_route") return null;
|
|
if (order.eta?.estimated_arrival)
|
|
return order.eta.estimated_arrival;
|
|
if (
|
|
order.eta?.eta_minutes &&
|
|
order.eta.eta_minutes > 0
|
|
) {
|
|
return new Date(
|
|
Date.now() +
|
|
order.eta.eta_minutes * 60000,
|
|
).toLocaleTimeString("fr-FR", {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
}
|
|
return null;
|
|
})();
|
|
|
|
const showArrivedEta = statusLow === "arrived";
|
|
|
|
return (
|
|
<div key={order.id} className="order-card">
|
|
{/* Colored strip */}
|
|
<div
|
|
className="order-status-strip"
|
|
style={{
|
|
backgroundImage: getStatusColor(
|
|
order.status,
|
|
),
|
|
}}
|
|
/>
|
|
|
|
{/* Clickable header */}
|
|
<div
|
|
className="order-header"
|
|
onClick={() =>
|
|
setExpandedOrder(
|
|
expandedOrder === order.id
|
|
? null
|
|
: order.id,
|
|
)
|
|
}
|
|
role="button"
|
|
tabIndex={0}
|
|
>
|
|
<div className="order-header-left">
|
|
<div className="order-id-row">
|
|
<span className="order-id">
|
|
Commande #
|
|
{order.client_order_number}
|
|
</span>
|
|
<div
|
|
className="status-badge"
|
|
style={{
|
|
backgroundImage:
|
|
getStatusColor(
|
|
order.status,
|
|
),
|
|
}}
|
|
>
|
|
<FontAwesomeIcon
|
|
icon={getStatusIcon(
|
|
order.status,
|
|
)}
|
|
/>{" "}
|
|
{getStatusLabel(
|
|
order.status,
|
|
)}
|
|
</div>
|
|
{statusLow === "en_route" && (
|
|
<div className="eta-badge eta-badge--enRoute">
|
|
<FontAwesomeIcon
|
|
icon={faClock}
|
|
/>
|
|
{computedArrival
|
|
? ` Vers ${computedArrival}`
|
|
: " Aucune heure disponible"}
|
|
</div>
|
|
)}
|
|
{showArrivedEta && (
|
|
<div className="eta-badge eta-badge--arrived">
|
|
<FontAwesomeIcon
|
|
icon={faClock}
|
|
/>
|
|
{" ~5 min"}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<span className="order-date">
|
|
{new Date(
|
|
order.created_at,
|
|
).toLocaleDateString("fr-FR", {
|
|
day: "numeric",
|
|
month: "short",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
})}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="order-header-right">
|
|
<span className="expand-btn">
|
|
<FontAwesomeIcon
|
|
icon={
|
|
expandedOrder ===
|
|
order.id
|
|
? faChevronUp
|
|
: faChevronDown
|
|
}
|
|
/>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Expanded details */}
|
|
{expandedOrder === order.id && (
|
|
<div className="order-details">
|
|
{/* Timeline */}
|
|
{!isCancelled && (
|
|
<div className="status-timeline">
|
|
{TIMELINE_STEPS.map(
|
|
(step, idx) => {
|
|
const isDone =
|
|
currentIdx >
|
|
idx;
|
|
const isActive =
|
|
currentIdx ===
|
|
idx;
|
|
return (
|
|
<div
|
|
key={
|
|
step.key
|
|
}
|
|
className={`timeline-step${isDone ? " done" : ""}${isActive ? " active" : ""}`}
|
|
>
|
|
<div className="timeline-dot">
|
|
<FontAwesomeIcon
|
|
icon={
|
|
isDone ||
|
|
isActive
|
|
? step.icon
|
|
: faQuestionCircle
|
|
}
|
|
/>
|
|
</div>
|
|
<span className="timeline-label">
|
|
{
|
|
step.label
|
|
}
|
|
</span>
|
|
</div>
|
|
);
|
|
},
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* ETA card - arrived */}
|
|
{showArrivedEta && (
|
|
<div className="eta-card eta-card--arrived">
|
|
<div className="eta-card-icon">
|
|
<FontAwesomeIcon
|
|
icon={
|
|
faMapMarkerAlt
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="eta-card-body">
|
|
<p className="eta-card-title">
|
|
Livreur sur place
|
|
</p>
|
|
<p className="eta-card-value">
|
|
~5 min
|
|
</p>
|
|
<p className="eta-card-sub">
|
|
Préparez-vous à
|
|
réceptionner votre
|
|
commande
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ETA card - en_route */}
|
|
{(showEta ||
|
|
(statusLow === "en_route" &&
|
|
computedArrival)) && (
|
|
<div className="eta-card eta-card--enRoute">
|
|
<div className="eta-card-icon">
|
|
<FontAwesomeIcon
|
|
icon={faClock}
|
|
/>
|
|
</div>
|
|
<div className="eta-card-body">
|
|
<p className="eta-card-title">
|
|
Heure d'arrivée
|
|
estimée
|
|
</p>
|
|
{computedArrival && (
|
|
<p className="eta-card-value">
|
|
{
|
|
computedArrival
|
|
}
|
|
</p>
|
|
)}
|
|
{order.eta
|
|
?.eta_minutes &&
|
|
order.eta
|
|
.eta_minutes >
|
|
0 && (
|
|
<p className="eta-card-sub">
|
|
~
|
|
{
|
|
order
|
|
.eta
|
|
.eta_minutes
|
|
}{" "}
|
|
min
|
|
restantes
|
|
</p>
|
|
)}
|
|
{order.eta
|
|
?.livreur_distance !=
|
|
null && (
|
|
<p className="eta-card-sub">
|
|
Distance :{" "}
|
|
{typeof order
|
|
.eta
|
|
.livreur_distance ===
|
|
"number"
|
|
? order.eta.livreur_distance.toFixed(
|
|
1,
|
|
)
|
|
: order.eta
|
|
.livreur_distance}{" "}
|
|
km
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Detail sections */}
|
|
<div className="details-grid">
|
|
<div className="detail-section">
|
|
<h4>
|
|
<FontAwesomeIcon
|
|
icon={
|
|
faMapMarkerAlt
|
|
}
|
|
/>{" "}
|
|
Adresse de livraison
|
|
</h4>
|
|
<p className="address">
|
|
{getDeliveryAddress(
|
|
order,
|
|
)}
|
|
</p>
|
|
{(() => {
|
|
const info =
|
|
getClientInfo(
|
|
order,
|
|
);
|
|
if (
|
|
info.firstName ||
|
|
info.lastName
|
|
) {
|
|
return (
|
|
<p className="contact">
|
|
{
|
|
info.firstName
|
|
}{" "}
|
|
{
|
|
info.lastName
|
|
}
|
|
{info.phone &&
|
|
` • ${info.phone}`}
|
|
</p>
|
|
);
|
|
}
|
|
return null;
|
|
})()}
|
|
</div>
|
|
|
|
{order.livreur_assign && (
|
|
<div className="detail-section">
|
|
<h4>
|
|
<FontAwesomeIcon
|
|
icon={faBiking}
|
|
/>{" "}
|
|
Livreur assigné
|
|
</h4>
|
|
<p>
|
|
{
|
|
order.livreur_assign
|
|
}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{order.items &&
|
|
order.items.length > 0 && (
|
|
<div className="detail-section">
|
|
<h4>
|
|
<FontAwesomeIcon
|
|
icon={
|
|
faShoppingCart
|
|
}
|
|
/>{" "}
|
|
Produits
|
|
</h4>
|
|
<div className="items-list">
|
|
{order.items.map(
|
|
(
|
|
item: OrderItem,
|
|
idx: number,
|
|
) => {
|
|
const formatted =
|
|
formatOrderItem(
|
|
item,
|
|
);
|
|
return (
|
|
<div
|
|
key={
|
|
idx
|
|
}
|
|
className="item"
|
|
>
|
|
<span className="item-name">
|
|
{
|
|
formatted.name
|
|
}{" "}
|
|
(
|
|
{
|
|
formatted.quantity
|
|
}
|
|
g)
|
|
</span>
|
|
<span className="price">
|
|
{formatted.price.toFixed(
|
|
2,
|
|
)}{" "}
|
|
€
|
|
</span>
|
|
</div>
|
|
);
|
|
},
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="detail-section">
|
|
<h4>
|
|
<FontAwesomeIcon
|
|
icon={
|
|
faMoneyBillWave
|
|
}
|
|
/>{" "}
|
|
Montant total
|
|
</h4>
|
|
{(order.referral_used ??
|
|
0) > 0 && (
|
|
<p
|
|
style={{
|
|
margin: "0 0 2px",
|
|
fontSize:
|
|
"0.85rem",
|
|
color: "var(--text-muted)",
|
|
}}
|
|
>
|
|
Brut :{" "}
|
|
{(
|
|
order.total_prix ??
|
|
0
|
|
).toFixed(2)}{" "}
|
|
€
|
|
</p>
|
|
)}
|
|
<p className="total-amount">
|
|
<strong>
|
|
{Math.max(
|
|
0,
|
|
getTotalAmount(
|
|
order,
|
|
) -
|
|
(order.referral_used ??
|
|
0),
|
|
).toFixed(2)}{" "}
|
|
€
|
|
</strong>
|
|
</p>
|
|
{(order.referral_used ??
|
|
0) > 0 && (
|
|
<p
|
|
style={{
|
|
margin: "4px 0 0",
|
|
fontSize:
|
|
"0.82rem",
|
|
color: "#10b981",
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
— dont{" "}
|
|
{order.referral_used!.toFixed(
|
|
2,
|
|
)}{" "}
|
|
€ parrainage déduit
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="detail-section">
|
|
<h4>
|
|
<FontAwesomeIcon
|
|
icon={faCalendarAlt}
|
|
/>{" "}
|
|
Date de commande
|
|
</h4>
|
|
<p>
|
|
{new Date(
|
|
order.created_at,
|
|
).toLocaleDateString(
|
|
"fr-FR",
|
|
{
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
},
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="order-actions">
|
|
{statusLow === "livre" ? (
|
|
<div className="confirmation-section">
|
|
<div className="delivery-notice">
|
|
<p className="notice-title">
|
|
<FontAwesomeIcon
|
|
icon={faBox}
|
|
/>{" "}
|
|
Votre colis a
|
|
été livré
|
|
</p>
|
|
<p className="notice-subtitle">
|
|
Confirmez la
|
|
réception pour
|
|
valider la
|
|
livraison et
|
|
gagner des
|
|
points
|
|
</p>
|
|
</div>
|
|
<button
|
|
className="btn-confirm-delivery"
|
|
onClick={() =>
|
|
openConfirmDialog(
|
|
order.id,
|
|
)
|
|
}
|
|
disabled={
|
|
confirming ===
|
|
order.id
|
|
}
|
|
>
|
|
{confirming ===
|
|
order.id ? (
|
|
<>
|
|
<FontAwesomeIcon
|
|
icon={
|
|
faClock
|
|
}
|
|
spin
|
|
/>{" "}
|
|
Confirmation...
|
|
</>
|
|
) : (
|
|
<>
|
|
<FontAwesomeIcon
|
|
icon={
|
|
faCheck
|
|
}
|
|
/>{" "}
|
|
J'ai bien
|
|
reçu ma
|
|
commande
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
) : statusLow === "approved" ? (
|
|
<div className="status-message success">
|
|
<FontAwesomeIcon
|
|
icon={faCheckCircle}
|
|
/>{" "}
|
|
Commande validée et
|
|
confirmée
|
|
</div>
|
|
) : isCancelled ? (
|
|
<div className="status-message cancelled">
|
|
<FontAwesomeIcon
|
|
icon={faTimesCircle}
|
|
/>{" "}
|
|
Cette commande a été
|
|
annulée
|
|
</div>
|
|
) : (
|
|
<div className="action-buttons">
|
|
{(statusLow ===
|
|
"pending" ||
|
|
statusLow ===
|
|
"assigned") && (
|
|
<button
|
|
className="btn-secondary"
|
|
onClick={() =>
|
|
openEditAddressDialog(
|
|
order.id,
|
|
)
|
|
}
|
|
>
|
|
<FontAwesomeIcon
|
|
icon={
|
|
faMapMarkerAlt
|
|
}
|
|
/>{" "}
|
|
Modifier
|
|
l'adresse
|
|
</button>
|
|
)}
|
|
<button
|
|
className="btn-cancel-order"
|
|
onClick={() =>
|
|
openCancelDialog(
|
|
order.id,
|
|
)
|
|
}
|
|
disabled={
|
|
cancellingOrder ===
|
|
order.id
|
|
}
|
|
>
|
|
{cancellingOrder ===
|
|
order.id ? (
|
|
<>
|
|
<FontAwesomeIcon
|
|
icon={
|
|
faClock
|
|
}
|
|
spin
|
|
/>{" "}
|
|
Annulation...
|
|
</>
|
|
) : (
|
|
<>
|
|
<FontAwesomeIcon
|
|
icon={
|
|
faTimesCircle
|
|
}
|
|
/>{" "}
|
|
Annuler la
|
|
commande
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
<button
|
|
className="btn-secondary"
|
|
onClick={() => {
|
|
loadOrders();
|
|
showToast(
|
|
"Actualisation en cours...",
|
|
"info",
|
|
);
|
|
}}
|
|
>
|
|
<FontAwesomeIcon
|
|
icon={faSync}
|
|
/>{" "}
|
|
Actualiser
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Dialog de confirmation */}
|
|
{showConfirmDialog && (
|
|
<div
|
|
className="confirm-dialog-overlay"
|
|
onClick={closeConfirmDialog}
|
|
>
|
|
<div
|
|
className="confirm-dialog"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="confirm-dialog-header">
|
|
<h3>
|
|
<FontAwesomeIcon icon={faCheckCircle} />{" "}
|
|
Confirmer la réception
|
|
</h3>
|
|
</div>
|
|
<div className="confirm-dialog-body">
|
|
<p>
|
|
Confirmez-vous avoir bien reçu votre commande ?
|
|
</p>
|
|
|
|
<div className="confirm-dialog-reward">
|
|
<FontAwesomeIcon icon={faGift} />
|
|
<strong>
|
|
{selectedOrderPoints > 0
|
|
? `+${selectedOrderPoints} point${selectedOrderPoints > 1 ? "s" : ""} de fidélité`
|
|
: "Commande éligible aux points de fidélité"}
|
|
</strong>
|
|
</div>
|
|
|
|
{selectedOrderPoints > 0 && (
|
|
<div className="points-info">
|
|
<p>
|
|
Accumulez des points pour obtenir des
|
|
récompenses !
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="confirm-dialog-actions">
|
|
<button
|
|
className="btn-secondary"
|
|
onClick={closeConfirmDialog}
|
|
>
|
|
Retour
|
|
</button>
|
|
<button
|
|
className="btn-confirm"
|
|
onClick={handleConfirmReception}
|
|
>
|
|
<FontAwesomeIcon icon={faCheck} /> Confirmer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Dialog de modification d'adresse */}
|
|
{editingAddressOrder !== null && (
|
|
<div
|
|
className="confirm-dialog-overlay"
|
|
onClick={closeEditAddressDialog}
|
|
>
|
|
<div
|
|
className="confirm-dialog"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="confirm-dialog-header">
|
|
<h3>
|
|
<FontAwesomeIcon icon={faMapMarkerAlt} />{" "}
|
|
Modifier l'adresse de livraison
|
|
</h3>
|
|
</div>
|
|
<div className="confirm-dialog-body">
|
|
<div className="form-group">
|
|
<label htmlFor="new-address">
|
|
Nouvelle adresse de livraison
|
|
</label>
|
|
<textarea
|
|
id="new-address"
|
|
value={newAddress}
|
|
onChange={(e) =>
|
|
setNewAddress(e.target.value)
|
|
}
|
|
placeholder="Adresse complète"
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="confirm-dialog-actions">
|
|
<button
|
|
className="btn-secondary"
|
|
onClick={closeEditAddressDialog}
|
|
disabled={editAddressLoading}
|
|
>
|
|
Retour
|
|
</button>
|
|
<button
|
|
className="btn-confirm"
|
|
onClick={handleUpdateAddress}
|
|
disabled={
|
|
editAddressLoading || !newAddress.trim()
|
|
}
|
|
>
|
|
{editAddressLoading ? (
|
|
<>
|
|
<FontAwesomeIcon
|
|
icon={faClock}
|
|
spin
|
|
/>{" "}
|
|
Enregistrement...
|
|
</>
|
|
) : (
|
|
<>
|
|
<FontAwesomeIcon icon={faCheck} />{" "}
|
|
Enregistrer
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Dialog d'annulation */}
|
|
{showCancelDialog && (
|
|
<div
|
|
className="confirm-dialog-overlay"
|
|
onClick={closeCancelDialog}
|
|
>
|
|
<div
|
|
className="confirm-dialog cancel-dialog"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="confirm-dialog-header">
|
|
<h3>
|
|
<FontAwesomeIcon icon={faTimesCircle} />
|
|
{showPenaltyWarning
|
|
? " Confirmation requise"
|
|
: " Annuler la commande"}
|
|
</h3>
|
|
</div>
|
|
|
|
<div className="confirm-dialog-body">
|
|
{showPenaltyWarning && penaltyWarningData ? (
|
|
<div className="penalty-warning">
|
|
<div className="warning-icon">
|
|
<FontAwesomeIcon
|
|
icon={faExclamationTriangle}
|
|
/>
|
|
</div>
|
|
<p className="warning-title">
|
|
{penaltyWarningData.message}
|
|
</p>
|
|
|
|
{penaltyWarningData.details && (
|
|
<div className="warning-details">
|
|
<p>
|
|
<strong>
|
|
Livreur assigné:
|
|
</strong>{" "}
|
|
{
|
|
penaltyWarningData.details
|
|
.livreur
|
|
}
|
|
</p>
|
|
<p>
|
|
<strong>Statut:</strong>{" "}
|
|
{getStatusLabel(
|
|
penaltyWarningData.details
|
|
.status || "",
|
|
)}
|
|
</p>
|
|
{penaltyWarningData.details
|
|
.position_in_queue && (
|
|
<p>
|
|
<strong>
|
|
Position dans la queue:
|
|
</strong>{" "}
|
|
{
|
|
penaltyWarningData
|
|
.details
|
|
.position_in_queue
|
|
}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{penaltyWarningData.penalty_warning && (
|
|
<div className="penalty-info">
|
|
{penaltyWarningData.penalty_warning
|
|
.will_apply ? (
|
|
<>
|
|
<p className="penalty-amount">
|
|
<strong>
|
|
⚠️ Pénalité:{" "}
|
|
{
|
|
penaltyWarningData
|
|
.penalty_warning
|
|
.penalty_amount
|
|
}{" "}
|
|
points
|
|
</strong>
|
|
</p>
|
|
<p className="penalty-message">
|
|
{
|
|
penaltyWarningData
|
|
.penalty_warning
|
|
.message
|
|
}
|
|
</p>
|
|
<div className="penalty-scale">
|
|
<p>
|
|
<strong>
|
|
Barème des
|
|
pénalités:
|
|
</strong>
|
|
</p>
|
|
<ul>
|
|
<li>
|
|
1ère annulation:{" "}
|
|
{
|
|
penaltyWarningData
|
|
.penalty_warning
|
|
.scale[
|
|
"1st_cancel"
|
|
]
|
|
}
|
|
</li>
|
|
<li>
|
|
2ème annulation:{" "}
|
|
{
|
|
penaltyWarningData
|
|
.penalty_warning
|
|
.scale[
|
|
"2nd_cancel"
|
|
]
|
|
}
|
|
</li>
|
|
<li>
|
|
3ème annulation:{" "}
|
|
{
|
|
penaltyWarningData
|
|
.penalty_warning
|
|
.scale[
|
|
"3rd_cancel"
|
|
]
|
|
}
|
|
</li>
|
|
<li>
|
|
4ème+
|
|
annulation:{" "}
|
|
{
|
|
penaltyWarningData
|
|
.penalty_warning
|
|
.scale[
|
|
"4th+_cancel"
|
|
]
|
|
}
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="info-message">
|
|
<FontAwesomeIcon
|
|
icon={faLightbulb}
|
|
/>
|
|
<span>
|
|
{
|
|
penaltyWarningData
|
|
.penalty_warning
|
|
.message
|
|
}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<p className="warning-question">
|
|
{penaltyWarningData.penalty_warning
|
|
?.will_apply
|
|
? "Voulez-vous vraiment annuler cette commande et accepter la pénalité ?"
|
|
: "Voulez-vous vraiment annuler cette commande ?"}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="cancel-form">
|
|
<p>
|
|
Êtes-vous sûr de vouloir annuler cette
|
|
commande ?
|
|
</p>
|
|
|
|
<div className="form-group">
|
|
<label htmlFor="cancel-reason">
|
|
Raison de l'annulation (optionnelle)
|
|
</label>
|
|
<textarea
|
|
id="cancel-reason"
|
|
value={cancelReason}
|
|
onChange={(e) =>
|
|
setCancelReason(e.target.value)
|
|
}
|
|
placeholder="Expliquez pourquoi vous annulez cette commande..."
|
|
rows={4}
|
|
maxLength={500}
|
|
/>
|
|
<small>
|
|
{cancelReason.length}/500 caractères
|
|
</small>
|
|
</div>
|
|
|
|
<div className="info-message">
|
|
<FontAwesomeIcon icon={faLightbulb} />
|
|
<span>
|
|
Si aucun livreur n'est assigné,
|
|
l'annulation est gratuite. Sinon,
|
|
une confirmation sera demandée.
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="confirm-dialog-actions">
|
|
<button
|
|
className="btn-secondary"
|
|
onClick={closeCancelDialog}
|
|
disabled={cancellingOrder !== null}
|
|
>
|
|
Retour
|
|
</button>
|
|
|
|
<button
|
|
className="btn-danger"
|
|
onClick={() =>
|
|
showPenaltyWarning
|
|
? confirmCancelWithPenalty()
|
|
: handleCancelOrder(false)
|
|
}
|
|
disabled={cancellingOrder !== null}
|
|
>
|
|
{showPenaltyWarning ? (
|
|
<>
|
|
<FontAwesomeIcon icon={faCheck} />{" "}
|
|
Confirmer l'annulation
|
|
</>
|
|
) : (
|
|
<>
|
|
<FontAwesomeIcon icon={faTimesCircle} />{" "}
|
|
Annuler la commande
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Toasts */}
|
|
{toasts.map((toast) => (
|
|
<Toast
|
|
key={toast.id}
|
|
message={toast.message}
|
|
type={toast.type}
|
|
duration={3000}
|
|
onClose={() => removeToast(toast.id)}
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default SuiviLivraison;
|