chore: add ansible backend docker frontend-prep
This commit is contained in:
@@ -0,0 +1,788 @@
|
||||
// ============================================
|
||||
// pages/OrderDetails.tsx
|
||||
// ============================================
|
||||
// Page de détails d'une commande spécifique
|
||||
// ✅ AJOUT: Vérification continue de l'authentification
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import Navbar from "../../components/Navbar";
|
||||
import "./OrderDetails.css";
|
||||
import {
|
||||
formatPrice,
|
||||
getOrderAge,
|
||||
getCommandItemsWithDetails,
|
||||
isUserAuthenticated,
|
||||
getProductById,
|
||||
} from "../../api/api";
|
||||
import type { CompletedOrder, Product } from "../../api/api_types";
|
||||
import {
|
||||
Package,
|
||||
MapPin,
|
||||
User,
|
||||
Calendar,
|
||||
Clock,
|
||||
ArrowLeft,
|
||||
CheckCircle,
|
||||
Truck,
|
||||
Phone,
|
||||
Mail,
|
||||
CreditCard,
|
||||
ShoppingBag,
|
||||
} from "lucide-react";
|
||||
|
||||
interface OrderProduct {
|
||||
id: number;
|
||||
product_id: number;
|
||||
name_product: string;
|
||||
category: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
interface OrderProductWithMedia extends OrderProduct {
|
||||
hasVideo: boolean;
|
||||
videoUrl?: string;
|
||||
}
|
||||
|
||||
interface OrderDetailsData extends CompletedOrder {
|
||||
products?: OrderProduct[];
|
||||
phone?: string;
|
||||
email?: string;
|
||||
nom?: string;
|
||||
prenom?: string;
|
||||
payment_method?: string;
|
||||
delivery_time?: string;
|
||||
delivery_notes?: string;
|
||||
}
|
||||
|
||||
function OrderDetails() {
|
||||
const { orderId } = useParams<{ orderId: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [order, setOrder] = useState<OrderDetailsData | null>(null);
|
||||
const [enrichedProducts, setEnrichedProducts] = useState<
|
||||
OrderProductWithMedia[]
|
||||
>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [loadingMedia, setLoadingMedia] = useState(false);
|
||||
const [error, setError] = useState<string>("");
|
||||
const [showVideo, setShowVideo] = useState(false);
|
||||
const [currentVideoUrl, setCurrentVideoUrl] = useState<string>("");
|
||||
|
||||
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log(
|
||||
"❌ [OrderDetails] 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(
|
||||
"❌ [OrderDetails] Session expirée, redirection vers /login/client",
|
||||
);
|
||||
navigate("/login/client", { replace: true });
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(authInterval);
|
||||
}, [navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (orderId) {
|
||||
fetchOrderDetails(parseInt(orderId));
|
||||
}
|
||||
}, [orderId]);
|
||||
|
||||
// ============================================
|
||||
// FONCTIONS POUR PHOTOS ET VIDÉOS (COMME PRODUCTCARD)
|
||||
// ============================================
|
||||
|
||||
const getProductImage = (product: Product): string => {
|
||||
if (!product.media || product.media.length === 0) {
|
||||
return "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image";
|
||||
}
|
||||
|
||||
for (let i = 0; i < product.media.length; i++) {
|
||||
const mediaItem = product.media[i];
|
||||
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
|
||||
return `http://localhost:8080${mediaItem.url}`;
|
||||
}
|
||||
}
|
||||
|
||||
return "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image";
|
||||
};
|
||||
|
||||
const hasProductVideo = (product: Product): boolean => {
|
||||
if (!product.media || product.media.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return product.media.some(
|
||||
(mediaItem) => mediaItem && mediaItem.type === "video",
|
||||
);
|
||||
};
|
||||
|
||||
const getProductVideoUrl = (product: Product): string | undefined => {
|
||||
if (!product.media || product.media.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const videoMedia = product.media.find(
|
||||
(mediaItem) => mediaItem && mediaItem.type === "video",
|
||||
);
|
||||
|
||||
return videoMedia?.url
|
||||
? `http://localhost:8080${videoMedia.url}`
|
||||
: undefined;
|
||||
};
|
||||
|
||||
// HANDLERS VIDÉO
|
||||
const handleVideoToggle = (videoUrl: string) => {
|
||||
setCurrentVideoUrl(videoUrl);
|
||||
setShowVideo(true);
|
||||
};
|
||||
|
||||
const handleCloseVideo = () => {
|
||||
setShowVideo(false);
|
||||
setCurrentVideoUrl("");
|
||||
};
|
||||
|
||||
// ENRICHIR LES PRODUITS AVEC MÉDIAS
|
||||
useEffect(() => {
|
||||
const enrichOrderProducts = async () => {
|
||||
if (!order?.products || order.products.length === 0) {
|
||||
setEnrichedProducts([]);
|
||||
setLoadingMedia(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingMedia(true);
|
||||
console.log(
|
||||
"🔄 [ORDER DETAILS] Enrichissement de",
|
||||
order.products.length,
|
||||
"produits...",
|
||||
);
|
||||
|
||||
try {
|
||||
const enrichedPromises = order.products.map(async (product) => {
|
||||
try {
|
||||
console.log(
|
||||
`📦 [ORDER DETAILS] Récupération médias pour produit ${product.product_id}...`,
|
||||
);
|
||||
const productResponse = await getProductById(
|
||||
product.product_id,
|
||||
);
|
||||
|
||||
if (productResponse.success && productResponse.data) {
|
||||
const fullProduct = productResponse.data;
|
||||
const enriched: OrderProductWithMedia = {
|
||||
...product,
|
||||
image: getProductImage(fullProduct),
|
||||
hasVideo: hasProductVideo(fullProduct),
|
||||
videoUrl: getProductVideoUrl(fullProduct),
|
||||
};
|
||||
console.log(`✅ [ORDER DETAILS] Produit enrichi:`, {
|
||||
name: product.name_product,
|
||||
image: enriched.image,
|
||||
hasVideo: enriched.hasVideo,
|
||||
});
|
||||
return enriched;
|
||||
} else {
|
||||
console.warn(
|
||||
`⚠️ [ORDER DETAILS] Produit ${product.product_id} non trouvé`,
|
||||
);
|
||||
return {
|
||||
...product,
|
||||
image: "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image",
|
||||
hasVideo: false,
|
||||
videoUrl: undefined,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`❌ [ORDER DETAILS] Erreur pour produit ${product.product_id}:`,
|
||||
error,
|
||||
);
|
||||
return {
|
||||
...product,
|
||||
image: "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image",
|
||||
hasVideo: false,
|
||||
videoUrl: undefined,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const enriched = await Promise.all(enrichedPromises);
|
||||
setEnrichedProducts(enriched);
|
||||
console.log(
|
||||
"✅ [ORDER DETAILS] Enrichissement terminé:",
|
||||
enriched.length,
|
||||
"produits",
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ [ORDER DETAILS] Erreur enrichissement:",
|
||||
error,
|
||||
);
|
||||
setEnrichedProducts(
|
||||
order.products.map((product) => ({
|
||||
...product,
|
||||
image: "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image",
|
||||
hasVideo: false,
|
||||
videoUrl: undefined,
|
||||
})),
|
||||
);
|
||||
} finally {
|
||||
setLoadingMedia(false);
|
||||
}
|
||||
};
|
||||
|
||||
enrichOrderProducts();
|
||||
}, [order?.products]);
|
||||
|
||||
const fetchOrderDetails = async (id: number) => {
|
||||
// ✅ Vérifier l'auth avant de charger les détails
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log("❌ [fetchOrderDetails] Non authentifié");
|
||||
navigate("/login/client", { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
console.log("📦 [ORDER DETAILS] Chargement commande:", id);
|
||||
|
||||
// ✅ Appeler getCommandItemsWithDetails pour récupérer les items
|
||||
const result = await getCommandItemsWithDetails(id);
|
||||
|
||||
if (result.success && result.data) {
|
||||
console.log("✅ [ORDER DETAILS] Données reçues:", result.data);
|
||||
|
||||
const apiData = result.data;
|
||||
|
||||
// ✅ Mapper les données depuis la structure de GetCommandItemsWithDetails
|
||||
const orderData: OrderDetailsData = {
|
||||
// Infos de command_info
|
||||
id: apiData.command_info?.id || id,
|
||||
username: apiData.client_info?.username || "",
|
||||
status: apiData.command_info?.command_status || "unknown",
|
||||
adresse: apiData.command_info?.command_address || "",
|
||||
total_prix:
|
||||
apiData.command_info?.total_prix ||
|
||||
apiData.total_price ||
|
||||
0,
|
||||
livreur_assign: apiData.command_info?.livreur_assign || "",
|
||||
created_at:
|
||||
apiData.command_info?.command_created_at ||
|
||||
new Date().toISOString(),
|
||||
updated_at:
|
||||
apiData.command_info?.command_created_at ||
|
||||
new Date().toISOString(),
|
||||
|
||||
// ✅ Infos client depuis client_info
|
||||
nom: apiData.client_info?.nom || "",
|
||||
prenom: apiData.client_info?.prenom || "",
|
||||
phone: apiData.client_info?.telephone || "",
|
||||
|
||||
// ✅ Produits depuis items (command_items)
|
||||
products:
|
||||
apiData.items?.map((item: any) => ({
|
||||
id: item.id,
|
||||
product_id: item.product_id,
|
||||
name_product: item.produit,
|
||||
category: "Non spécifié", // Pas de catégorie dans command_items
|
||||
price: item.prix,
|
||||
quantity: item.quantite,
|
||||
status: item.status,
|
||||
image: item.image, // Si vous avez des images
|
||||
})) || [],
|
||||
};
|
||||
|
||||
console.log("✅ [ORDER DETAILS] Données mappées:", orderData);
|
||||
setOrder(orderData);
|
||||
} else {
|
||||
console.error("❌ [ORDER DETAILS] Erreur:", result.message);
|
||||
setError(result.message || "Erreur lors du chargement");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ [ORDER DETAILS] Erreur catch:", err);
|
||||
setError("Erreur de connexion");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getProductCount = (totalPrix: number): number => {
|
||||
return Math.max(1, Math.round(totalPrix / 25));
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="order-details-container">
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner">
|
||||
<div className="spinner"></div>
|
||||
</div>
|
||||
<p className="loading-text">
|
||||
Chargement des détails...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !order) {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="order-details-container">
|
||||
<div className="error-container">
|
||||
<Package size={64} className="error-icon" />
|
||||
<h2>Commande introuvable</h2>
|
||||
<p>
|
||||
{error ||
|
||||
"Cette commande n'existe pas ou vous n'y avez pas accès"}
|
||||
</p>
|
||||
<button
|
||||
className="back-button"
|
||||
onClick={() =>
|
||||
navigate("/user/consultation-historique")
|
||||
}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Retour à l'historique
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="order-details-container">
|
||||
{/* Header avec bouton retour */}
|
||||
<div className="details-header">
|
||||
<button
|
||||
className="back-button"
|
||||
onClick={() =>
|
||||
navigate("/user/consultation-historique")
|
||||
}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Retour
|
||||
</button>
|
||||
<div className="header-info">
|
||||
<h1 className="order-number">
|
||||
Commande #{order.id.toString().padStart(5, "0")}
|
||||
</h1>
|
||||
<div className="status-container">
|
||||
<span className="status-badge delivered">
|
||||
<CheckCircle size={16} />
|
||||
Livrée
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grille principale */}
|
||||
<div className="details-grid">
|
||||
{/* Section Informations de livraison */}
|
||||
<div className="details-card">
|
||||
<div className="card-header">
|
||||
<Truck size={24} />
|
||||
<h2>Informations de livraison</h2>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<div className="info-row">
|
||||
<div className="info-icon">
|
||||
<MapPin size={18} />
|
||||
</div>
|
||||
<div className="info-content">
|
||||
<span className="info-label">
|
||||
Adresse de livraison
|
||||
</span>
|
||||
<span className="info-value">
|
||||
{order.adresse}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{order.livreur_assign && (
|
||||
<div className="info-row">
|
||||
<div className="info-icon">
|
||||
<User size={18} />
|
||||
</div>
|
||||
<div className="info-content">
|
||||
<span className="info-label">
|
||||
Livreur
|
||||
</span>
|
||||
<span className="info-value">
|
||||
{order.livreur_assign}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="info-row">
|
||||
<div className="info-icon">
|
||||
<Calendar size={18} />
|
||||
</div>
|
||||
<div className="info-content">
|
||||
<span className="info-label">
|
||||
Date de commande
|
||||
</span>
|
||||
<span className="info-value">
|
||||
{new Date(
|
||||
order.created_at,
|
||||
).toLocaleDateString("fr-FR", {
|
||||
weekday: "long",
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="info-row">
|
||||
<div className="info-icon">
|
||||
<Clock size={18} />
|
||||
</div>
|
||||
<div className="info-content">
|
||||
<span className="info-label">Heure</span>
|
||||
<span className="info-value">
|
||||
{new Date(
|
||||
order.created_at,
|
||||
).toLocaleTimeString("fr-FR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
<span className="info-age">
|
||||
{getOrderAge(order.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{order.delivery_time && (
|
||||
<div className="info-row">
|
||||
<div className="info-icon">
|
||||
<CheckCircle size={18} />
|
||||
</div>
|
||||
<div className="info-content">
|
||||
<span className="info-label">
|
||||
Livrée le
|
||||
</span>
|
||||
<span className="info-value">
|
||||
{order.delivery_time}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section Contact Client */}
|
||||
<div className="details-card">
|
||||
<div className="card-header">
|
||||
<Phone size={24} />
|
||||
<h2>Informations de contact</h2>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<div className="info-row">
|
||||
<div className="info-icon">
|
||||
<User size={18} />
|
||||
</div>
|
||||
<div className="info-content">
|
||||
<span className="info-label">Client</span>
|
||||
<span className="info-value">
|
||||
{order.prenom && order.nom
|
||||
? `${order.prenom} ${order.nom}`
|
||||
: order.username}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{order.phone && (
|
||||
<div className="info-row">
|
||||
<div className="info-icon">
|
||||
<Phone size={18} />
|
||||
</div>
|
||||
<div className="info-content">
|
||||
<span className="info-label">
|
||||
Téléphone
|
||||
</span>
|
||||
<span className="info-value">
|
||||
{order.phone}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{order.email && (
|
||||
<div className="info-row">
|
||||
<div className="info-icon">
|
||||
<Mail size={18} />
|
||||
</div>
|
||||
<div className="info-content">
|
||||
<span className="info-label">
|
||||
Email
|
||||
</span>
|
||||
<span className="info-value">
|
||||
{order.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="info-row">
|
||||
<div className="info-icon">
|
||||
<User size={18} />
|
||||
</div>
|
||||
<div className="info-content">
|
||||
<span className="info-label">
|
||||
Nom d'utilisateur
|
||||
</span>
|
||||
<span className="info-value">
|
||||
{order.username}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section Produits */}
|
||||
<div className="details-card products-card">
|
||||
<div className="card-header">
|
||||
<ShoppingBag size={24} />
|
||||
<h2>Produits commandés</h2>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
{loadingMedia && enrichedProducts.length === 0 ? (
|
||||
<div className="loading-container">
|
||||
<p>Chargement des médias...</p>
|
||||
</div>
|
||||
) : enrichedProducts.length > 0 ? (
|
||||
<div className="products-list">
|
||||
{enrichedProducts.map((product, index) => (
|
||||
<div
|
||||
key={`${product.id}-${index}`}
|
||||
className="product-item"
|
||||
>
|
||||
<div className="product-media-container">
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name_product}
|
||||
className="product-image2"
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
console.warn(
|
||||
`❌ Erreur chargement image pour ${product.name_product}:`,
|
||||
product.image,
|
||||
);
|
||||
e.currentTarget.src =
|
||||
"https://via.placeholder.com/400x400/7c3aed/ffffff?text=" +
|
||||
encodeURIComponent(
|
||||
product.name_product.substring(
|
||||
0,
|
||||
10,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* Badge vidéo si disponible */}
|
||||
{product.hasVideo &&
|
||||
product.videoUrl && (
|
||||
<button
|
||||
className="product-video-badge"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleVideoToggle(
|
||||
product.videoUrl!,
|
||||
);
|
||||
}}
|
||||
aria-label="Voir la vidéo du produit"
|
||||
>
|
||||
<i className="fas fa-camera"></i>
|
||||
<span>Vidéo</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="product-info">
|
||||
<h3 className="product-name">
|
||||
{product.name_product}
|
||||
</h3>
|
||||
<p className="product-category">
|
||||
{product.category}
|
||||
</p>
|
||||
<div className="product-details">
|
||||
<span className="product-quantity">
|
||||
Qté: {product.quantity}g
|
||||
</span>
|
||||
<span className="product-price">
|
||||
{formatPrice(
|
||||
product.price,
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* ✅ FIX: Ne pas multiplier, price contient déjà le total */}
|
||||
<div className="product-total">
|
||||
{formatPrice(product.price)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="products-placeholder">
|
||||
<Package
|
||||
size={32}
|
||||
className="placeholder-icon"
|
||||
/>
|
||||
<p>
|
||||
~{getProductCount(order.total_prix)}{" "}
|
||||
produit
|
||||
{getProductCount(order.total_prix) > 1
|
||||
? "s"
|
||||
: ""}
|
||||
</p>
|
||||
<span className="placeholder-note">
|
||||
Détails non disponibles
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section Récapitulatif */}
|
||||
<div className="details-card summary-card">
|
||||
<div className="card-header">
|
||||
<CreditCard size={24} />
|
||||
<h2>Récapitulatif</h2>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<div className="summary-row">
|
||||
<span>Sous-total</span>
|
||||
<span>{formatPrice(order.total_prix)}</span>
|
||||
</div>
|
||||
<div className="summary-row total-row">
|
||||
<span>Total</span>
|
||||
<span className="total-amount">
|
||||
{formatPrice(order.total_prix)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notes de livraison */}
|
||||
{order.delivery_notes && (
|
||||
<div className="details-card notes-card">
|
||||
<div className="card-header">
|
||||
<Package size={24} />
|
||||
<h2>Notes de livraison</h2>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<p className="delivery-notes">
|
||||
{order.delivery_notes}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Timeline de livraison */}
|
||||
<div className="delivery-timeline">
|
||||
<h2 className="timeline-title">Suivi de la commande</h2>
|
||||
<div className="timeline">
|
||||
<div className="timeline-item completed">
|
||||
<div className="timeline-marker"></div>
|
||||
<div className="timeline-content">
|
||||
<h3>Commande confirmée</h3>
|
||||
<p>
|
||||
{new Date(order.created_at).toLocaleString(
|
||||
"fr-FR",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="timeline-item completed">
|
||||
<div className="timeline-marker"></div>
|
||||
<div className="timeline-content">
|
||||
<h3>En préparation</h3>
|
||||
<p>Votre commande a été préparée</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="timeline-item completed">
|
||||
<div className="timeline-marker"></div>
|
||||
<div className="timeline-content">
|
||||
<h3>En cours de livraison</h3>
|
||||
<p>
|
||||
{order.livreur_assign
|
||||
? `Livrée par ${order.livreur_assign}`
|
||||
: "En route"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="timeline-item completed">
|
||||
<div className="timeline-marker"></div>
|
||||
<div className="timeline-content">
|
||||
<h3>Livrée</h3>
|
||||
<p>
|
||||
{order.delivery_time ||
|
||||
"Commande livrée avec succès"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal vidéo */}
|
||||
{showVideo && currentVideoUrl && (
|
||||
<div className="video-modal" onClick={handleCloseVideo}>
|
||||
<div
|
||||
className="video-modal-content"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
className="video-close-btn"
|
||||
onClick={handleCloseVideo}
|
||||
aria-label="Fermer la vidéo"
|
||||
>
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
<video
|
||||
src={currentVideoUrl}
|
||||
controls
|
||||
autoPlay
|
||||
className="video-player"
|
||||
>
|
||||
Votre navigateur ne supporte pas la lecture de
|
||||
vidéos.
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default OrderDetails;
|
||||
Reference in New Issue
Block a user