Files
projet_gestion_commande/frontend-prep/src/pages/User/SuiviLivraison.tsx
T
2026-06-14 17:50:35 +02:00

1414 lines
71 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,
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[]>([]);
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);
};
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>
{(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 é
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;