Files
projet_gestion_commande/frontend-prep/src/pages/User/OrderDetails.tsx
T
2026-05-03 18:25:33 +02:00

792 lines
34 KiB
TypeScript

import { useState, useEffect } from "react";
import { useParams, useNavigate, useLocation } from "react-router-dom";
import Navbar from "../../components/Navbar";
import "./OrderDetails.css";
import {
formatPrice,
getOrderAge,
getCommandItemsWithDetails,
isUserAuthenticated,
getProductById,
getMediaUrl,
} from "../../api/api";
import type { CompletedOrder, Product } from "../../api/api_types";
import {
Package,
MapPin,
User,
Calendar,
Clock,
ArrowLeft,
CheckCircle,
Truck,
Phone,
Mail,
CreditCard,
ShoppingBag,
Camera,
X,
} 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 location = useLocation();
// commandId = ID global pour l'API (passé en state depuis l'historique)
// fallback sur orderId si navigation directe via URL
const commandId = (location.state as { commandId?: number } | null)?.commandId ?? parseInt(orderId ?? "0");
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 (commandId) {
fetchOrderDetails(commandId);
}
}, [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 getMediaUrl(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 ? getMediaUrl(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) => {
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,
client_order_number:
apiData.command_info?.client_order_number || 0,
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é",
price: item.prix,
quantity: item.quantite,
status: item.status,
image: item.image,
})) || [],
};
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.client_order_number ?? 0)
.toString()
.padStart(4, "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"
>
<Camera size={14} />
<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 é 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"
>
<X size={18} />
</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;