Files
projet_gestion_commande/frontend-prep/src/pages/User/SuiviLivraison.tsx
T
2026-05-03 19:44:35 +02:00

1430 lines
71 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================
// 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,
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 => {
// 1. Priorité: champ total stocké en DB
if (typeof order.total === "number" && order.total > 0) {
return order.total;
}
// 2. Fallback: total_prix
if (typeof order.total_prix === "number" && order.total_prix > 0) {
return order.total_prix;
}
// 3. Calcul depuis items (comme dans Checkout: somme des prix)
if (order.items && order.items.length > 0) {
const calculatedTotal = order.items.reduce((sum, item) => {
const itemPrice = item.prix || item.price || 0;
return sum + itemPrice; // ✅ Somme simple (pas de × quantity)
}, 0);
console.log(
`💰 [getTotalAmount] Commande ${order.id}: ${calculatedTotal.toFixed(2)}€`,
);
return calculatedTotal;
}
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[]>([]);
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 () => {
// ✅ Vérifier l'auth avant de charger les commandes
if (!isUserAuthenticated()) {
console.log("❌ [loadOrders] Non authentifié");
navigate("/login/client", { replace: true });
return;
}
try {
setLoading(true);
const response = await getMyOrders();
if (response.success && response.commands) {
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 {
console.warn(
`Tracking non disponible pour commande ${order.id}`,
);
tracking = undefined;
}
try {
eta = await getOrderETA(order.id);
} catch {
console.warn(
`ETA non disponible pour commande ${order.id}`,
);
eta = undefined;
}
return {
...normalizedOrder,
tracking,
eta,
};
}),
);
setOrders(ordersWithTracking);
setError("");
} else {
setError("Impossible de charger les commandes");
showToast("Impossible de charger les commandes", "error");
}
} 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);
};
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>
{(showEta || (statusLow === "en_route" && computedArrival)) && (
<div className="eta-badge eta-badge--enRoute">
<FontAwesomeIcon icon={faClock} />
{` Vers ${computedArrival}`}
</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>
<p className="total-amount">
<strong>
{getTotalAmount(
order,
).toFixed(2)}{" "}
</strong>
</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 é
annulée
</div>
) : (
<div className="action-buttons">
<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 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;