chore: fix ui
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,10 @@
|
|||||||
// ============================================
|
|
||||||
// pages/Cart/Cart.tsx - MÊME LOGIQUE QUE PRODUCTCARD
|
|
||||||
// ============================================
|
|
||||||
// ✅ Affiche quantity en grammes (5g, 10g, etc.)
|
|
||||||
// ✅ Affiche photos et vidéos SANS appels API supplémentaires
|
|
||||||
// ❌ Pas de boutons +/- (on ne modifie pas les grammes)
|
|
||||||
// ✅ Pour acheter 2× le même produit, l'ajouter 2 fois
|
|
||||||
// ✅ Vérification continue de l'authentification
|
|
||||||
|
|
||||||
import { useCart } from "../../context/useCart";
|
import { useCart } from "../../context/useCart";
|
||||||
import Navbar from "../../components/Navbar";
|
import Navbar from "../../components/Navbar";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { isUserAuthenticated, getProductById, getMediaUrl } from "../../api/api";
|
import { isUserAuthenticated, getProductById, getMediaUrl } from "../../api/api";
|
||||||
import type { Product } from "../../api/api";
|
import type { Product } from "../../api/api";
|
||||||
import { Trash2, ShoppingBag, AlertTriangle } from "lucide-react";
|
import { Trash2, ShoppingCart, AlertTriangle } from "lucide-react";
|
||||||
import "./Cart.css";
|
import "./Cart.css";
|
||||||
|
|
||||||
interface CartItemWithMedia {
|
interface CartItemWithMedia {
|
||||||
@@ -30,258 +21,89 @@ interface CartItemWithMedia {
|
|||||||
|
|
||||||
function Cart() {
|
function Cart() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { cartItems, removeFromCart, clearCart, loading, refreshCart } =
|
const { cartItems, removeFromCart, clearCart, loading, refreshCart } = useCart();
|
||||||
useCart();
|
|
||||||
const [showClearConfirm, setShowClearConfirm] = useState(false);
|
const [showClearConfirm, setShowClearConfirm] = useState(false);
|
||||||
const [showVideo, setShowVideo] = useState(false); // ✨ État pour afficher/masquer la vidéo
|
const [showVideo, setShowVideo] = useState(false);
|
||||||
const [currentVideoUrl, setCurrentVideoUrl] = useState<string>("");
|
const [currentVideoUrl, setCurrentVideoUrl] = useState<string>("");
|
||||||
const [enrichedItems, setEnrichedItems] = useState<CartItemWithMedia[]>([]);
|
const [enrichedItems, setEnrichedItems] = useState<CartItemWithMedia[]>([]);
|
||||||
const [loadingMedia, setLoadingMedia] = useState(false);
|
const [loadingMedia, setLoadingMedia] = useState(false);
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// HANDLERS VIDÉO (COMME PRODUCTCARD)
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
const handleVideoToggle = (videoUrl: string) => {
|
|
||||||
setCurrentVideoUrl(videoUrl);
|
|
||||||
setShowVideo(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseVideo = () => {
|
|
||||||
setShowVideo(false);
|
|
||||||
setCurrentVideoUrl("");
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// VÉRIFICATION AUTHENTIFICATION
|
|
||||||
// ============================================
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAuth = () => {
|
if (!isUserAuthenticated()) navigate("/login/client", { replace: true });
|
||||||
if (!isUserAuthenticated()) {
|
|
||||||
console.log(
|
|
||||||
"❌ [Cart] Utilisateur non authentifié, redirection vers /login/client",
|
|
||||||
);
|
|
||||||
navigate("/login/client", { replace: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkAuth();
|
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
// ✅ VÉRIFICATION CONTINUE (toutes les 5 secondes)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const authInterval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (!isUserAuthenticated()) {
|
if (!isUserAuthenticated()) navigate("/login/client", { replace: true });
|
||||||
console.log(
|
|
||||||
"❌ [Cart] Session expirée, redirection vers /login/client",
|
|
||||||
);
|
|
||||||
navigate("/login/client", { replace: true });
|
|
||||||
}
|
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
return () => clearInterval(authInterval);
|
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// ✅ Vérifier l'auth avant de rafraîchir le panier
|
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
|
||||||
if (!isUserAuthenticated()) {
|
|
||||||
console.log("❌ [refreshCart] Non authentifié");
|
|
||||||
navigate("/login/client", { replace: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshCart();
|
refreshCart();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// CALCULS
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
const total = cartItems.reduce((sum, item) => sum + item.price, 0);
|
const total = cartItems.reduce((sum, item) => sum + item.price, 0);
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// FONCTIONS POUR PHOTOS ET VIDÉOS (COMME PRODUCTCARD)
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
const getProductImage = (product: Product): string => {
|
const getProductImage = (product: Product): string => {
|
||||||
if (!product.media || product.media.length === 0) {
|
if (!product.media || product.media.length === 0)
|
||||||
return "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image";
|
return "https://via.placeholder.com/120x120/1a1a1a/ffffff?text=No+Image";
|
||||||
|
for (const m of product.media) {
|
||||||
|
if (m && m.type === "image" && m.url) return getMediaUrl(m.url);
|
||||||
}
|
}
|
||||||
|
return "https://via.placeholder.com/120x120/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;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// ENRICHIR LES ITEMS AVEC MÉDIAS
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const enrichCartItems = async () => {
|
const enrich = async () => {
|
||||||
if (cartItems.length === 0) {
|
if (cartItems.length === 0) { setEnrichedItems([]); return; }
|
||||||
console.log("🔄 [CART] Panier vide, pas d'enrichissement");
|
const same =
|
||||||
setEnrichedItems([]);
|
|
||||||
setLoadingMedia(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Guard: ne pas re-enrichir si déjà fait pour ces mêmes items
|
|
||||||
const sameItems =
|
|
||||||
enrichedItems.length === cartItems.length &&
|
enrichedItems.length === cartItems.length &&
|
||||||
enrichedItems.every(
|
enrichedItems.every((e, i) => e.id === cartItems[i]?.id && e.product_id === cartItems[i]?.product_id);
|
||||||
(enriched, index) =>
|
if (same) return;
|
||||||
enriched.id === cartItems[index]?.id &&
|
|
||||||
enriched.product_id === cartItems[index]?.product_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (sameItems) {
|
|
||||||
console.log("🔄 [CART] Items déjà enrichis, skip");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingMedia(true);
|
setLoadingMedia(true);
|
||||||
console.log(
|
|
||||||
"🔄 [CART] Enrichissement de",
|
|
||||||
cartItems.length,
|
|
||||||
"items...",
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const enrichedPromises = cartItems.map(async (item) => {
|
const enriched = await Promise.all(
|
||||||
|
cartItems.map(async (item) => {
|
||||||
try {
|
try {
|
||||||
console.log(
|
const res = await getProductById(item.product_id);
|
||||||
`📦 [CART] Récupération médias pour produit ${item.product_id}...`,
|
if (res.success && res.data) {
|
||||||
);
|
const p = res.data;
|
||||||
const productResponse = await getProductById(
|
const videoMedia = p.media?.find((m: any) => m && m.type === "video");
|
||||||
item.product_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (productResponse.success && productResponse.data) {
|
|
||||||
const product = productResponse.data;
|
|
||||||
const enriched = {
|
|
||||||
...item,
|
|
||||||
image: getProductImage(product),
|
|
||||||
hasVideo: hasProductVideo(product),
|
|
||||||
videoUrl: getProductVideoUrl(product),
|
|
||||||
};
|
|
||||||
console.log(`✅ [CART] Item enrichi:`, {
|
|
||||||
name: item.name_product,
|
|
||||||
image: enriched.image,
|
|
||||||
hasVideo: enriched.hasVideo,
|
|
||||||
});
|
|
||||||
return enriched;
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ [CART] Produit ${item.product_id} non trouvé`,
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
image: "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image",
|
image: getProductImage(p),
|
||||||
hasVideo: false,
|
hasVideo: !!videoMedia,
|
||||||
videoUrl: undefined,
|
videoUrl: videoMedia?.url ? getMediaUrl(videoMedia.url) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch { /* ignore */ }
|
||||||
console.error(
|
return { ...item, image: "https://via.placeholder.com/120x120/1a1a1a/ffffff?text=No+Image", hasVideo: false };
|
||||||
`❌ [CART] Erreur pour produit ${item.product_id}:`,
|
})
|
||||||
error,
|
|
||||||
);
|
);
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
image: "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image",
|
|
||||||
hasVideo: false,
|
|
||||||
videoUrl: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const enriched = await Promise.all(enrichedPromises);
|
|
||||||
setEnrichedItems(enriched);
|
setEnrichedItems(enriched);
|
||||||
console.log(
|
|
||||||
"✅ [CART] Enrichissement terminé:",
|
|
||||||
enriched.length,
|
|
||||||
"items",
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ [CART] Erreur enrichissement:", error);
|
|
||||||
setEnrichedItems(
|
|
||||||
cartItems.map((item) => ({
|
|
||||||
...item,
|
|
||||||
image: "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image",
|
|
||||||
hasVideo: false,
|
|
||||||
videoUrl: undefined,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingMedia(false);
|
setLoadingMedia(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
enrich();
|
||||||
enrichCartItems();
|
}, [cartItems]);
|
||||||
}, [cartItems]); // ✅ Dépendance sur cartItems complet
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// HANDLERS
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
const handleClearCart = async () => {
|
const handleClearCart = async () => {
|
||||||
// ✅ Vérifier l'auth avant de vider le panier
|
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
|
||||||
if (!isUserAuthenticated()) {
|
|
||||||
console.log("❌ [handleClearCart] Non authentifié");
|
|
||||||
navigate("/login/client", { replace: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowClearConfirm(false);
|
setShowClearConfirm(false);
|
||||||
await clearCart();
|
await clearCart();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveItem = async (itemId: number) => {
|
const handleRemoveItem = async (itemId: number) => {
|
||||||
// ✅ Vérifier l'auth avant de supprimer un item
|
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
|
||||||
if (!isUserAuthenticated()) {
|
|
||||||
console.log("❌ [handleRemoveItem] Non authentifié");
|
|
||||||
navigate("/login/client", { replace: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await removeFromCart(itemId);
|
await removeFromCart(itemId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCheckout = () => {
|
const handleCheckout = () => {
|
||||||
// ✅ Vérifier l'auth avant de passer commande
|
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
|
||||||
if (!isUserAuthenticated()) {
|
|
||||||
console.log("❌ [handleCheckout] Non authentifié");
|
|
||||||
navigate("/login/client", { replace: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate("/user/checkout");
|
navigate("/user/checkout");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -290,231 +112,145 @@ function Cart() {
|
|||||||
<>
|
<>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="cart-container">
|
<div className="cart-container">
|
||||||
<div className="cart-header">
|
<div className="loading-cart"><p>Chargement du panier...</p></div>
|
||||||
<h1>Votre Panier</h1>
|
|
||||||
</div>
|
|
||||||
<div className="loading-cart">
|
|
||||||
<p>Chargement de votre panier...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const displayItems = enrichedItems.length > 0 ? enrichedItems : cartItems.map(i => ({ ...i, image: "", hasVideo: false }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="cart-container">
|
<div className="cart-container">
|
||||||
<div className="cart-header">
|
|
||||||
<h1>Votre Panier</h1>
|
|
||||||
{cartItems.length > 0 && (
|
|
||||||
<button
|
|
||||||
className="clear-all-button"
|
|
||||||
onClick={() => setShowClearConfirm(true)}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<Trash2 size={16} className="trash-icon" />
|
|
||||||
Tout supprimer
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{cartItems.length === 0 ? (
|
{cartItems.length === 0 ? (
|
||||||
<div className="empty-cart">
|
<div className="empty-cart">
|
||||||
<ShoppingBag size={48} className="empty-cart-icon" />
|
<ShoppingCart size={72} className="empty-cart-icon" />
|
||||||
<p>Votre panier est vide</p>
|
<h2>Panier vide</h2>
|
||||||
<button
|
<p>Ajoutez des produits pour commencer</p>
|
||||||
className="continue-shopping"
|
<button className="continue-shopping" onClick={() => navigate("/user/nos-produits")}>
|
||||||
onClick={() => navigate("/user/nos-produits")}
|
Découvrir nos produits
|
||||||
>
|
|
||||||
Continuer mes achats
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{loadingMedia && enrichedItems.length === 0 ? (
|
{/* Header */}
|
||||||
<div className="loading-cart">
|
<div className="cart-header">
|
||||||
<p>Chargement des médias...</p>
|
<div className="cart-header-left">
|
||||||
|
<h1 className="cart-title">Mon panier</h1>
|
||||||
|
<span className="cart-header-count">
|
||||||
|
{cartItems.length} article{cartItems.length > 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button className="clear-all-button" onClick={() => setShowClearConfirm(true)} disabled={loading}>
|
||||||
|
Tout vider
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Liste des articles */}
|
||||||
|
<div className="cart-items-list">
|
||||||
|
{loadingMedia && enrichedItems.length === 0 ? (
|
||||||
|
<div className="loading-cart"><p>Chargement des médias...</p></div>
|
||||||
) : (
|
) : (
|
||||||
<div className="cart-items">
|
displayItems.map((item, index) => (
|
||||||
{enrichedItems.map((item, index) => (
|
<div key={`${item.id}-${index}`} className="cart-row">
|
||||||
<div
|
|
||||||
key={`${item.id}-${index}`}
|
{/* Vignette image */}
|
||||||
className="cart-item"
|
<div className="cart-row-thumb">
|
||||||
>
|
{item.image ? (
|
||||||
{/* ✨ MEDIA CONTAINER - Photo + Badge Vidéo */}
|
|
||||||
<div className="cart-item-media-container">
|
|
||||||
<img
|
<img
|
||||||
src={item.image}
|
src={item.image}
|
||||||
alt={item.name_product}
|
alt={item.name_product}
|
||||||
className="cart-item-image"
|
className="cart-row-img"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
onError={(e) => {
|
onError={(e) => { e.currentTarget.src = "https://via.placeholder.com/72x72/2d2d2d/ffffff?text=?"; }}
|
||||||
console.warn(
|
|
||||||
`❌ Erreur chargement image pour ${item.name_product}:`,
|
|
||||||
item.image,
|
|
||||||
);
|
|
||||||
e.currentTarget.src =
|
|
||||||
"https://via.placeholder.com/400x400/7c3aed/ffffff?text=" +
|
|
||||||
encodeURIComponent(
|
|
||||||
item.name_product.substring(
|
|
||||||
0,
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
{/* ✨ Badge vidéo si disponible */}
|
<div className="cart-row-img-placeholder">
|
||||||
{item.hasVideo && item.videoUrl && (
|
<i className="fas fa-leaf"></i>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(item as CartItemWithMedia).hasVideo && (item as CartItemWithMedia).videoUrl && (
|
||||||
<button
|
<button
|
||||||
className="cart-video-badge"
|
className="cart-row-video-badge"
|
||||||
onClick={(e) => {
|
onClick={(e) => { e.stopPropagation(); setCurrentVideoUrl((item as CartItemWithMedia).videoUrl!); setShowVideo(true); }}
|
||||||
e.stopPropagation();
|
aria-label="Voir la vidéo"
|
||||||
handleVideoToggle(
|
|
||||||
item.videoUrl!,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
aria-label="Voir la vidéo du produit"
|
|
||||||
>
|
>
|
||||||
<i className="fas fa-camera"></i>
|
<i className="fas fa-camera"></i>
|
||||||
<span>Vidéo</span>
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="cart-item-info">
|
{/* Infos */}
|
||||||
<h3>{item.name_product}</h3>
|
<div className="cart-row-info">
|
||||||
<p className="cart-item-category">
|
<p className="cart-row-name">{item.name_product}</p>
|
||||||
Catégorie: {item.category}
|
<p className="cart-row-qty">{item.quantity}g</p>
|
||||||
</p>
|
<p className="cart-row-price">{item.price.toFixed(2)} €</p>
|
||||||
|
|
||||||
{/* Quantité en grammes */}
|
|
||||||
<p className="cart-item-quantity">
|
|
||||||
Quantité:{" "}
|
|
||||||
<strong>
|
|
||||||
{item.quantity}g
|
|
||||||
</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="cart-item-price">
|
|
||||||
{item.price.toFixed(2)} €
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bouton supprimer */}
|
{/* Bouton supprimer */}
|
||||||
<div className="cart-item-controls">
|
|
||||||
<button
|
<button
|
||||||
className="remove-item-button"
|
className="cart-row-remove"
|
||||||
onClick={() =>
|
onClick={() => handleRemoveItem(item.id)}
|
||||||
handleRemoveItem(item.id)
|
|
||||||
}
|
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
title="Supprimer cet article"
|
|
||||||
aria-label={`Supprimer ${item.name_product}`}
|
aria-label={`Supprimer ${item.name_product}`}
|
||||||
>
|
>
|
||||||
<Trash2
|
<Trash2 size={18} />
|
||||||
size={18}
|
|
||||||
className="trash-icon"
|
|
||||||
/>
|
|
||||||
Supprimer
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="cart-summary">
|
|
||||||
<div className="summary-row">
|
|
||||||
<span>Nombre d'articles:</span>
|
|
||||||
<span>{cartItems.length}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="summary-row total-row">
|
{/* Footer fixe */}
|
||||||
<span>Total:</span>
|
<div className="cart-footer">
|
||||||
<span className="total-amount">
|
<div className="cart-footer-total">
|
||||||
{total.toFixed(2)} €
|
<span className="cart-footer-total-label">Total</span>
|
||||||
</span>
|
<span className="cart-footer-total-value">{total.toFixed(2)} €</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button className="checkout-button" onClick={handleCheckout} disabled={loading}>
|
||||||
<button
|
{loading ? "Chargement..." : "Commander"}
|
||||||
className="checkout-button"
|
|
||||||
onClick={handleCheckout}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading
|
|
||||||
? "Chargement..."
|
|
||||||
: "Valider la commande"}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal de confirmation vidage */}
|
{/* Modal vider le panier */}
|
||||||
{showClearConfirm && (
|
{showClearConfirm && (
|
||||||
<div
|
<div className="modal-overlay" onClick={() => setShowClearConfirm(false)}>
|
||||||
className="modal-overlay"
|
<div className="modal-content2" onClick={(e) => e.stopPropagation()}>
|
||||||
onClick={() => setShowClearConfirm(false)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="modal-content2"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div className="modal-icon-container">
|
<div className="modal-icon-container">
|
||||||
<AlertTriangle size={48} className="modal-icon" />
|
<AlertTriangle size={40} className="modal-icon" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="modal-title">Vider le panier</h2>
|
<h2 className="modal-title">Vider le panier</h2>
|
||||||
<p className="modal-message">
|
<p className="modal-message">
|
||||||
Êtes-vous sûr de vouloir supprimer tous les articles
|
Supprimer les {cartItems.length} article{cartItems.length > 1 ? "s" : ""} du panier ?
|
||||||
de votre panier ? Cette action est irréversible.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="modal-actions">
|
<div className="modal-actions">
|
||||||
<button
|
<button className="modal-button modal-cancel" onClick={() => setShowClearConfirm(false)}>
|
||||||
className="modal-button modal-cancel"
|
|
||||||
onClick={() => setShowClearConfirm(false)}
|
|
||||||
>
|
|
||||||
Annuler
|
Annuler
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button className="modal-button modal-confirm" onClick={handleClearCart} disabled={loading}>
|
||||||
className="modal-button modal-confirm"
|
<Trash2 size={16} />
|
||||||
onClick={handleClearCart}
|
Vider
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<Trash2 size={18} />
|
|
||||||
Tout supprimer
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ✨ Modal vidéo (COMME PRODUCTCARD) */}
|
{/* Modal vidéo */}
|
||||||
{showVideo && currentVideoUrl && (
|
{showVideo && currentVideoUrl && (
|
||||||
<div className="video-modal" onClick={handleCloseVideo}>
|
<div className="video-modal" onClick={() => { setShowVideo(false); setCurrentVideoUrl(""); }}>
|
||||||
<div
|
<div className="video-modal-content" onClick={(e) => e.stopPropagation()}>
|
||||||
className="video-modal-content"
|
<button className="video-close-btn" onClick={() => { setShowVideo(false); setCurrentVideoUrl(""); }}>
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="video-close-btn"
|
|
||||||
onClick={handleCloseVideo}
|
|
||||||
aria-label="Fermer la vidéo"
|
|
||||||
>
|
|
||||||
<i className="fas fa-times"></i>
|
<i className="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
<video
|
<video src={currentVideoUrl} controls autoPlay className="video-player">
|
||||||
src={currentVideoUrl}
|
Votre navigateur ne supporte pas la lecture de vidéos.
|
||||||
controls
|
|
||||||
autoPlay
|
|
||||||
className="video-player"
|
|
||||||
>
|
|
||||||
Votre navigateur ne supporte pas la lecture de
|
|
||||||
vidéos.
|
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -138,6 +138,15 @@
|
|||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summary-item-image--placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
object-fit: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.summary-item-info {
|
.summary-item-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useCart } from '../../context/useCart';
|
import { useCart } from '../../context/useCart';
|
||||||
import { createCheckout, clearCart, extractUsernameFromToken, isUserAuthenticated, getReferralBalance, getPublicSettings, getMyProfile, getCryptoPaymentStatus } from '../../api/api';
|
import { createCheckout, clearCart, extractUsernameFromToken, isUserAuthenticated, getReferralBalance, getPublicSettings, getMyProfile, getCryptoPaymentStatus, getProductById, getMediaUrl } from '../../api/api';
|
||||||
import type { CheckoutData, CryptoPaymentStatus } from '../../api/api';
|
import type { CheckoutData, CryptoPaymentStatus } from '../../api/api';
|
||||||
|
import type { Product } from '../../api/api';
|
||||||
import Navbar from '../../components/Navbar';
|
import Navbar from '../../components/Navbar';
|
||||||
import './Checkout.css';
|
import './Checkout.css';
|
||||||
|
|
||||||
@@ -55,6 +56,25 @@ function Checkout() {
|
|||||||
|
|
||||||
const total = cartTotal;
|
const total = cartTotal;
|
||||||
|
|
||||||
|
// Images enrichies
|
||||||
|
const [itemImages, setItemImages] = useState<Record<number, string>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cartItems.length === 0) return;
|
||||||
|
cartItems.forEach(async (item) => {
|
||||||
|
try {
|
||||||
|
const res = await getProductById(item.product_id);
|
||||||
|
if (res.success && res.data) {
|
||||||
|
const p: Product = res.data;
|
||||||
|
const img = p.media?.find((m: any) => m && m.type === 'image');
|
||||||
|
if (img?.url) {
|
||||||
|
setItemImages((prev) => ({ ...prev, [item.id]: getMediaUrl(img.url) }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
});
|
||||||
|
}, [cartItems]);
|
||||||
|
|
||||||
// Parrainage
|
// Parrainage
|
||||||
const [referralBalance, setReferralBalance] = useState(0);
|
const [referralBalance, setReferralBalance] = useState(0);
|
||||||
const [referralEnabled, setReferralEnabled] = useState(false);
|
const [referralEnabled, setReferralEnabled] = useState(false);
|
||||||
@@ -378,7 +398,13 @@ function Checkout() {
|
|||||||
<div className="summary-items">
|
<div className="summary-items">
|
||||||
{cartItems.map((item, index) => (
|
{cartItems.map((item, index) => (
|
||||||
<div key={`${item.id}-${index}`} className="summary-item">
|
<div key={`${item.id}-${index}`} className="summary-item">
|
||||||
<img src={item.image} alt={item.name_product || 'Produit'} className="summary-item-image" />
|
{itemImages[item.id] ? (
|
||||||
|
<img src={itemImages[item.id]} alt={item.name_product || 'Produit'} className="summary-item-image" />
|
||||||
|
) : (
|
||||||
|
<div className="summary-item-image summary-item-image--placeholder">
|
||||||
|
<i className="fas fa-leaf" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="summary-item-info">
|
<div className="summary-item-info">
|
||||||
<p className="summary-item-name">{item.name_product}</p>
|
<p className="summary-item-name">{item.name_product}</p>
|
||||||
<p className="summary-item-details">
|
<p className="summary-item-details">
|
||||||
|
|||||||
@@ -1,246 +1,269 @@
|
|||||||
/* ============================================
|
/* ============================================
|
||||||
ConsultationHistorique.css - VERSION VIOLET SOMBRE
|
ConsultationHistorique.css — style mobile
|
||||||
============================================
|
============================================ */
|
||||||
Styles pour 4 compteurs de points distincts */
|
|
||||||
|
|
||||||
.history-container {
|
.history-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: clamp(1rem, 3vw, 2rem);
|
padding: clamp(1rem, 3vw, 2rem);
|
||||||
padding-top: calc(60px + clamp(1rem, 3vw, 2rem));
|
padding-top: calc(60px + clamp(1rem, 3vw, 1.5rem));
|
||||||
max-width: 1400px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background: linear-gradient(to bottom, var(--bg), var(--surface));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
HEADER
|
TITRE PAGE
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
.history-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
margin-top: -65px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-title {
|
.history-title {
|
||||||
font-size: clamp(2rem, 6vw, 2.5rem);
|
font-size: 1.5rem;
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
margin: 0 0 1.25rem 0;
|
||||||
|
|
||||||
.history-subtitle {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: clamp(0.9rem, 2vw, 1.1rem);
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
STATISTIQUES - GRID ADAPTÉ POUR 6 CARTES
|
STATS GRID — cartes centrées (style mobile)
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 1.5rem;
|
gap: 0.875rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card2:last-child:nth-child(odd) {
|
||||||
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card2 {
|
.stat-card2 {
|
||||||
background: linear-gradient(135deg, var(--surface), var(--surface-2));
|
background: var(--surface);
|
||||||
border: 2px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 1.5rem;
|
padding: 1.25rem 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
text-align: center;
|
||||||
transition: all 0.3s ease;
|
gap: 0.35rem;
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
✅ HOVER VIOLET SOMBRE POUR TOUTES LES CARTES
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.stat-card2:hover {
|
.stat-card2:hover {
|
||||||
border-color: #6d28d9;
|
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 8px 25px rgba(109, 40, 217, 0.4);
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 6px 20px rgba(124, 58, 237, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card2.stat-card-danger {
|
||||||
|
border-color: rgba(239, 68, 68, 0.4);
|
||||||
|
background: rgba(239, 68, 68, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card2.stat-card-warning {
|
||||||
|
border-color: rgba(245, 158, 11, 0.4);
|
||||||
|
background: rgba(245, 158, 11, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon {
|
.stat-icon {
|
||||||
width: 48px;
|
width: 44px;
|
||||||
height: 48px;
|
height: 44px;
|
||||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
border-radius: 10px;
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: white;
|
font-size: 1.1rem;
|
||||||
flex-shrink: 0;
|
color: #fff;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
.stat-icon.icon-total-orders { background: linear-gradient(135deg, #7c3aed, #6d28d9); }
|
||||||
✅ ICÔNES POUR COMMANDES - VIOLET SOMBRE
|
.stat-icon.icon-total { background: linear-gradient(135deg, #f59e0b, #d97706); }
|
||||||
============================================ */
|
.stat-icon.icon-penalty-ok { background: rgba(100, 116, 139, 0.3); color: var(--text-muted); }
|
||||||
|
.stat-icon.icon-penalty-warning { background: linear-gradient(135deg, #f59e0b, #d97706); }
|
||||||
.stat-icon.icon-total-orders {
|
.stat-icon.icon-penalty-critical {
|
||||||
background: linear-gradient(135deg, #6d28d9, #5b21b6);
|
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||||
|
animation: pulse-danger 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon.icon-completed-orders {
|
@keyframes pulse-danger {
|
||||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.5); }
|
||||||
}
|
50% { box-shadow: 0 0 0 8px rgba(239, 68, 68, 0); }
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
✅ STYLES SPÉCIFIQUES POUR LES POINTS
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
/* Total Commandes - Violet Très Sombre */
|
|
||||||
.stat-card2.total-orders:hover {
|
|
||||||
border-color: #5b21b6;
|
|
||||||
box-shadow: 0 8px 25px rgba(91, 33, 182, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon.icon-total-orders {
|
|
||||||
background: linear-gradient(135deg, #6d28d9, #5b21b6);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Commandes Livrées - Violet Sombre Clair */
|
|
||||||
.stat-card2.completed-orders:hover {
|
|
||||||
border-color: #7c3aed;
|
|
||||||
box-shadow: 0 8px 25px rgba(124, 58, 237, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon.icon-completed-orders {
|
|
||||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Points Weed/Hash - Vert (couleur d'origine) */
|
|
||||||
.stat-card2.points-weed:hover {
|
|
||||||
border-color: #5b21b6;
|
|
||||||
box-shadow: 0 8px 25px rgba(91, 33, 182, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon.icon-weed {
|
|
||||||
background: linear-gradient(135deg, #10b981, #059669);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Points Zipette - Bleu (couleur d'origine) */
|
|
||||||
.stat-card2.points-zipette:hover {
|
|
||||||
border-color: #6d28d9;
|
|
||||||
box-shadow: 0 8px 25px rgba(109, 40, 217, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon.icon-zipette {
|
|
||||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Points Total - Orange (couleur d'origine) */
|
|
||||||
.stat-card2.points-total:hover {
|
|
||||||
border-color: #4c1d95;
|
|
||||||
box-shadow: 0 8px 25px rgba(76, 29, 149, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon.icon-total {
|
|
||||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
STAT CONTENT
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.stat-content {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 1.8rem;
|
font-size: 1.75rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
.stat-value.value-danger { color: #ef4444; }
|
||||||
PENALTY STAT CARD - VIOLET SOMBRE
|
.stat-value.value-warning { color: #f59e0b; }
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.penalty-stat {
|
.stat-label {
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.penalty-stat.has-penalty:hover {
|
|
||||||
border-color: #7c3aed;
|
|
||||||
box-shadow: 0 8px 25px rgba(124, 58, 237, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon.penalty-warning {
|
|
||||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon.penalty-critical {
|
|
||||||
background: linear-gradient(135deg, #6d28d9, #5b21b6);
|
|
||||||
animation: pulse-penalty 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-penalty {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 0 rgba(109, 40, 217, 0.7);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: 0 0 0 10px rgba(109, 40, 217, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.penalty-value {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.penalty-limit {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
margin: 0;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.penalty-warning-text {
|
/* ============================================
|
||||||
|
BOUTON PARRAINAGE
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.referral-btn-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid rgba(124, 58, 237, 0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-btn-row:hover {
|
||||||
|
background: rgba(124, 58, 237, 0.08);
|
||||||
|
border-color: rgba(124, 58, 237, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-btn-icon {
|
||||||
|
color: #7c3aed;
|
||||||
|
font-size: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-btn-text {
|
||||||
|
flex: 1;
|
||||||
color: #a78bfa;
|
color: #a78bfa;
|
||||||
font-size: 0.85rem;
|
font-size: 0.95rem;
|
||||||
margin: 0.5rem 0 0 0;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.referral-btn-chevron {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
PARRAINAGE STAT CARD
|
SECTION TITLE
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
.stat-card2.referral-stat:hover {
|
.section-title {
|
||||||
border-color: #8b5cf6;
|
color: var(--text-muted);
|
||||||
box-shadow: 0 8px 25px rgba(139, 92, 246, 0.4);
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin: 0 0 0.875rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon.icon-referral {
|
/* ============================================
|
||||||
background: linear-gradient(135deg, #8b5cf6, #6d28d9);
|
ORDER CARDS
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.orders-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.referral-value-active {
|
.order-card {
|
||||||
color: #8b5cf6 !important;
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem 1.125rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.referral-link-hint {
|
.order-card:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 4px 16px rgba(124, 58, 237, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card-number {
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.3rem 0.75rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: #a78bfa;
|
||||||
|
border: 1.5px solid #7c3aed;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card-row:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card-row-icon {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
margin: 0.25rem 0 0;
|
margin-top: 0.2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card-row-text {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.875rem;
|
||||||
|
padding-top: 0.875rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card-total {
|
||||||
|
color: #10b981;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card-chevron {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
@@ -257,28 +280,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
width: 60px;
|
width: 50px;
|
||||||
height: 60px;
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: 4px solid var(--border);
|
border: 3px solid var(--border);
|
||||||
border-top-color: #7c3aed;
|
border-top-color: #7c3aed;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 0.9s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to {
|
to { transform: rotate(360deg); }
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-text {
|
.loading-text {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 1.1rem;
|
font-size: 1rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,19 +311,15 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
background: rgba(109, 40, 217, 0.1);
|
background: rgba(109, 40, 217, 0.08);
|
||||||
border: 2px solid #7c3aed;
|
border: 1px solid rgba(124, 58, 237, 0.4);
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.25rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
color: #c4b5fd;
|
||||||
|
|
||||||
.error-banner span {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-banner p {
|
.error-banner p {
|
||||||
color: #c4b5fd;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
@@ -314,372 +331,63 @@
|
|||||||
.empty-history {
|
.empty-history {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 4rem 2rem;
|
padding: 4rem 2rem;
|
||||||
background: linear-gradient(135deg, var(--surface), var(--surface-2));
|
background: var(--surface);
|
||||||
border: 2px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
margin-top: 2rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-icon {
|
.empty-icon {
|
||||||
|
font-size: 4rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-bottom: 1.5rem;
|
opacity: 0.4;
|
||||||
opacity: 0.5;
|
margin-bottom: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-history h2 {
|
.empty-history h2 {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 1.5rem;
|
font-size: 1.4rem;
|
||||||
margin: 0 0 0.5rem 0;
|
margin: 0 0 0.5rem 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-history p {
|
.empty-history p {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 1.1rem;
|
font-size: 0.95rem;
|
||||||
margin: 0 0 2rem 0;
|
margin: 0 0 1.75rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.browse-button {
|
.browse-button {
|
||||||
background: linear-gradient(to right, #7c3aed, #6d28d9);
|
background: linear-gradient(to right, #7c3aed, #6d28d9);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0.75rem 2rem;
|
padding: 0.7rem 1.75rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
box-shadow: 0 4px 15px rgba(124, 58, 237, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.browse-button:hover {
|
.browse-button:hover {
|
||||||
background: linear-gradient(to right, #6d28d9, #5b21b6);
|
opacity: 0.9;
|
||||||
transform: translateY(-2px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 6px 20px rgba(109, 40, 217, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.browse-button:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
SUMMARY
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.orders-summary {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
background: rgba(124, 58, 237, 0.1);
|
|
||||||
border: 1px solid rgba(124, 58, 237, 0.3);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.orders-summary p {
|
|
||||||
color: #a78bfa;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
TABLE - HOVER VIOLET SOMBRE
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.table-wrapper {
|
|
||||||
overflow-x: auto;
|
|
||||||
background-color: var(--surface);
|
|
||||||
border: 2px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
min-width: 900px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-table thead {
|
|
||||||
background: linear-gradient(135deg, var(--bg), var(--surface));
|
|
||||||
border-bottom: 2px solid #7c3aed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-table th {
|
|
||||||
color: var(--text);
|
|
||||||
font-size: clamp(0.85rem, 2vw, 0.95rem);
|
|
||||||
font-weight: 700;
|
|
||||||
text-align: left;
|
|
||||||
padding: 1.25rem 1rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-table tbody tr {
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-table tbody tr:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ✅ HOVER VIOLET SOMBRE SUR LES LIGNES */
|
|
||||||
.history-table tbody tr:hover {
|
|
||||||
background: linear-gradient(90deg, rgba(109, 40, 217, 0.15), transparent);
|
|
||||||
border-left: 3px solid #6d28d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-table tbody tr:active {
|
|
||||||
background-color: var(--surface-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-table td {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: clamp(0.85rem, 2vw, 0.95rem);
|
|
||||||
padding: 1.25rem 1rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
TABLE CELLS - SPECIFIC STYLES
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.order-id {
|
|
||||||
color: var(--text) !important;
|
|
||||||
font-weight: 700;
|
|
||||||
font-family: "Courier New", monospace;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-cell {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-main {
|
|
||||||
color: var(--text);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-time {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-age {
|
|
||||||
color: #7c3aed;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address-cell {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address-icon {
|
|
||||||
color: #7c3aed;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address-text {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livreur-cell {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livreur-icon {
|
|
||||||
color: #7c3aed;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-livreur {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.products-count {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-total2 {
|
|
||||||
color: #7c3aed !important;
|
|
||||||
font-weight: 700 !important;
|
|
||||||
font-size: 1.1rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
STATUS BADGE - VIOLET SOMBRE
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.delivered {
|
|
||||||
background: transparent;
|
|
||||||
color: #a78bfa;
|
|
||||||
border: 2px solid #7c3aed;
|
|
||||||
box-shadow: 0 0 30px rgba(124, 58, 237, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.in-progress {
|
|
||||||
background: rgba(139, 92, 246, 0.15);
|
|
||||||
color: #a78bfa;
|
|
||||||
border: 2px solid #8b5cf6;
|
|
||||||
box-shadow: 0 0 10px rgba(139, 92, 246, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.pending {
|
|
||||||
background: rgba(124, 58, 237, 0.15);
|
|
||||||
color: #c4b5fd;
|
|
||||||
border: 2px solid #7c3aed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
RESPONSIVE
|
RESPONSIVE
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.history-table {
|
|
||||||
min-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card2 {
|
|
||||||
padding: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-table th,
|
|
||||||
.history-table td {
|
|
||||||
padding: 1rem 0.75rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-id {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-total {
|
|
||||||
font-size: 1rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address-text {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.history-container {
|
|
||||||
padding: 1rem;
|
|
||||||
padding-top: calc(60px + 1rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
grid-template-columns: 1fr;
|
gap: 0.625rem;
|
||||||
}
|
|
||||||
|
|
||||||
.table-wrapper {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-table {
|
|
||||||
min-width: 700px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-table th,
|
|
||||||
.history-table td {
|
|
||||||
padding: 0.875rem 0.625rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-history {
|
.order-card-number {
|
||||||
padding: 3rem 1.5rem;
|
font-size: 0.9rem;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
HOVER EFFECTS (Desktop only) - VIOLET SOMBRE
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
.clickable-row {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clickable-row::before {
|
|
||||||
content: "→";
|
|
||||||
position: absolute;
|
|
||||||
right: 1rem;
|
|
||||||
color: #a78bfa;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-10px);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clickable-row:hover::before {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
PRINT STYLES
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
.history-container {
|
|
||||||
background: white;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid,
|
|
||||||
.browse-button,
|
|
||||||
.status-badge {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-table {
|
|
||||||
border: 1px solid #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-table th,
|
|
||||||
.history-table td {
|
|
||||||
color: #000;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
// ============================================
|
|
||||||
// pages/ConsultationHistorique.tsx - VERSION AVEC FONT AWESOME
|
|
||||||
// ============================================
|
|
||||||
// Page d'historique avec 4 compteurs de points distincts
|
|
||||||
// ✅ AJOUT: Vérification continue de l'authentification
|
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import Navbar from '../../components/Navbar';
|
import Navbar from '../../components/Navbar';
|
||||||
@@ -19,9 +13,7 @@ import {
|
|||||||
} from '../../api/api';
|
} from '../../api/api';
|
||||||
import type { PublicSettings } from '../../api/api';
|
import type { PublicSettings } from '../../api/api';
|
||||||
import type { CompletedOrder, ClientStats, PenaltyInfo } from "../../api/api_types";
|
import type { CompletedOrder, ClientStats, PenaltyInfo } from "../../api/api_types";
|
||||||
import { Package, MapPin, User, TrendingUp } from 'lucide-react';
|
|
||||||
|
|
||||||
// ✅ Import Font Awesome
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import {
|
import {
|
||||||
faCannabis,
|
faCannabis,
|
||||||
@@ -31,8 +23,15 @@ import {
|
|||||||
faStar,
|
faStar,
|
||||||
faTrophy,
|
faTrophy,
|
||||||
faExclamationTriangle,
|
faExclamationTriangle,
|
||||||
faCheckCircle,
|
faShieldAlt,
|
||||||
faGift,
|
faGift,
|
||||||
|
faReceipt,
|
||||||
|
faMapMarkerAlt,
|
||||||
|
faClock,
|
||||||
|
faBicycle,
|
||||||
|
faChevronRight,
|
||||||
|
faCheckCircle,
|
||||||
|
faHistory,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
function ConsultationHistorique() {
|
function ConsultationHistorique() {
|
||||||
@@ -46,28 +45,15 @@ function ConsultationHistorique() {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAuth = () => {
|
if (!isUserAuthenticated()) navigate('/login/client', { replace: true });
|
||||||
if (!isUserAuthenticated()) {
|
|
||||||
console.log('❌ [ConsultationHistorique] Utilisateur non authentifié, redirection vers /login/client');
|
|
||||||
navigate('/login/client', { replace: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkAuth();
|
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
// ✅ VÉRIFICATION CONTINUE (toutes les 5 secondes)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const authInterval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (!isUserAuthenticated()) {
|
if (!isUserAuthenticated()) navigate('/login/client', { replace: true });
|
||||||
console.log('❌ [ConsultationHistorique] Session expirée, redirection vers /login/client');
|
|
||||||
navigate('/login/client', { replace: true });
|
|
||||||
}
|
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
return () => clearInterval(authInterval);
|
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -82,34 +68,18 @@ function ConsultationHistorique() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchHistory = async () => {
|
const fetchHistory = async () => {
|
||||||
// ✅ Vérifier l'auth avant de charger l'historique
|
if (!isUserAuthenticated()) { navigate('/login/client', { replace: true }); return; }
|
||||||
if (!isUserAuthenticated()) {
|
|
||||||
console.log('❌ [fetchHistory] Non authentifié');
|
|
||||||
navigate('/login/client', { replace: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('📚 [HISTORY] Chargement historique...');
|
|
||||||
const result = await getMyCompletedOrders();
|
const result = await getMyCompletedOrders();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('✅ [HISTORY] Historique chargé:', result.count, 'commandes');
|
|
||||||
|
|
||||||
console.log('🔍 [DEBUG] result.client_stats:', result.client_stats);
|
|
||||||
console.log('🔍 [DEBUG] points:', result.client_stats?.points);
|
|
||||||
|
|
||||||
setOrders(result.commands);
|
setOrders(result.commands);
|
||||||
setClientStats(result.client_stats || null);
|
setClientStats(result.client_stats || null);
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ [HISTORY] Erreur:', result.message);
|
|
||||||
setError(result.message || 'Erreur lors du chargement');
|
setError(result.message || 'Erreur lors du chargement');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {
|
||||||
console.error('❌ [HISTORY] Erreur catch:', err);
|
|
||||||
setError('Erreur de connexion');
|
setError('Erreur de connexion');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -117,39 +87,23 @@ function ConsultationHistorique() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchPenalties = async () => {
|
const fetchPenalties = async () => {
|
||||||
// ✅ Vérifier l'auth avant de charger les pénalités
|
if (!isUserAuthenticated()) return;
|
||||||
if (!isUserAuthenticated()) {
|
|
||||||
console.log('❌ [fetchPenalties] Non authentifié');
|
|
||||||
navigate('/login/client', { replace: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('🚨 [PENALTIES] Chargement pénalités...');
|
|
||||||
const result = await getMyPenalties();
|
const result = await getMyPenalties();
|
||||||
|
if (result.success && result.data) setPenalties(result.data);
|
||||||
if (result.success && result.data) {
|
} catch { /* ignore */ }
|
||||||
console.log('✅ [PENALTIES] Pénalités chargées:', result.data);
|
|
||||||
setPenalties(result.data);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('❌ [PENALTIES] Erreur:', err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProductCount = (totalPrix: number): number => {
|
const viewOrderDetails = (order: CompletedOrder) => {
|
||||||
return Math.max(1, Math.round(totalPrix / 25));
|
if (!isUserAuthenticated()) { navigate('/login/client', { replace: true }); return; }
|
||||||
|
navigate(`/user/commande/${order.client_order_number ?? order.id}`, {
|
||||||
|
state: { commandId: order.id },
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const viewOrderDetails = (orderId: number) => {
|
const formatDate = (dateStr: string) => {
|
||||||
// ✅ Vérifier l'auth avant de naviguer
|
const d = new Date(dateStr);
|
||||||
if (!isUserAuthenticated()) {
|
return d.toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||||
console.log('❌ [viewOrderDetails] Non authentifié');
|
|
||||||
navigate('/login/client', { replace: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate(`/user/commande/${orderId}`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -158,9 +112,7 @@ function ConsultationHistorique() {
|
|||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="history-container">
|
<div className="history-container">
|
||||||
<div className="loading-container">
|
<div className="loading-container">
|
||||||
<div className="loading-spinner">
|
<div className="loading-spinner"><div className="spinner"></div></div>
|
||||||
<div className="spinner"></div>
|
|
||||||
</div>
|
|
||||||
<p className="loading-text">Chargement de l'historique...</p>
|
<p className="loading-text">Chargement de l'historique...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,155 +120,86 @@ function ConsultationHistorique() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const poolNames = clientStats?.pool_names?.length ? clientStats.pool_names : appSettings.pool_names;
|
||||||
|
const poolPoints = clientStats?.pool_points ?? [clientStats?.points ?? 0];
|
||||||
|
const totalPoints = poolPoints.reduce((s, v) => s + (v || 0), 0);
|
||||||
|
const penaltyCount = penalties?.total_penalty || clientStats?.penalties || 0;
|
||||||
|
const poolIcons = [faCannabis, faPills, faFlask, faMortarPestle, faStar];
|
||||||
|
const poolIconColors = ['#10b981', '#e879f9', '#fb923c', '#38bdf8', '#7c3aed'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="history-container">
|
<div className="history-container">
|
||||||
|
|
||||||
<div className="history-header">
|
|
||||||
<h1 className="history-title">Historique des commandes</h1>
|
<h1 className="history-title">Historique des commandes</h1>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ✅ STATISTIQUES CLIENT - 6 CARTES AVEC ICÔNES FONT AWESOME */}
|
{/* Stats grid */}
|
||||||
{clientStats && (
|
|
||||||
<div className="stats-grid">
|
<div className="stats-grid">
|
||||||
{/* Carte 1: Total Commandes */}
|
|
||||||
<div className="stat-card2 total-orders">
|
{/* Total commandes */}
|
||||||
|
<div className="stat-card2">
|
||||||
<div className="stat-icon icon-total-orders">
|
<div className="stat-icon icon-total-orders">
|
||||||
<Package size={24} />
|
<FontAwesomeIcon icon={faReceipt} />
|
||||||
</div>
|
|
||||||
<div className="stat-content">
|
|
||||||
<p className="stat-label">Total commandes</p>
|
|
||||||
<p className="stat-value">{clientStats.total_commands}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="stat-value">{clientStats?.total_commands ?? orders.length}</p>
|
||||||
|
<p className="stat-label">Commandes</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cartes points - affichées uniquement si le système de points est activé */}
|
{/* Points */}
|
||||||
{appSettings.points_enabled && (() => {
|
{appSettings.points_enabled && (
|
||||||
const poolNames = clientStats.pool_names?.length ? clientStats.pool_names : appSettings.pool_names;
|
poolNames.length <= 1 ? (
|
||||||
const poolPoints = clientStats.pool_points ?? [clientStats.points];
|
<div className="stat-card2">
|
||||||
const poolIcons = [faCannabis, faPills, faFlask, faMortarPestle, faStar];
|
|
||||||
const poolClasses = poolNames.map((_, i) => `points-pool-${i}`);
|
|
||||||
const poolIconClasses = poolNames.map((_, i) => `icon-pool-${i}`);
|
|
||||||
|
|
||||||
if (poolNames.length <= 1) {
|
|
||||||
return (
|
|
||||||
<div className="stat-card2 points-total">
|
|
||||||
<div className="stat-icon icon-total">
|
<div className="stat-icon icon-total">
|
||||||
<FontAwesomeIcon icon={faTrophy} size="lg" />
|
<FontAwesomeIcon icon={faTrophy} />
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-content">
|
|
||||||
<p className="stat-label">
|
|
||||||
<FontAwesomeIcon icon={faTrophy} style={{ marginRight: '0.5rem' }} />
|
|
||||||
Points {poolNames[0] ?? 'Points'}
|
|
||||||
</p>
|
|
||||||
<p className="stat-value">{poolPoints[0] || 0}</p>
|
<p className="stat-value">{poolPoints[0] || 0}</p>
|
||||||
|
<p className="stat-label">Pts {poolNames[0] ?? 'Points'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const total = poolPoints.reduce((s, v) => s + (v || 0), 0);
|
|
||||||
return (
|
|
||||||
<>
|
<>
|
||||||
{poolNames.map((name, i) => (
|
{poolNames.map((name, i) => (
|
||||||
<div key={i} className={`stat-card2 ${poolClasses[i] ?? 'points-extra'}`}>
|
<div key={i} className="stat-card2">
|
||||||
<div className={`stat-icon ${poolIconClasses[i] ?? 'icon-total'}`}>
|
<div className="stat-icon" style={{ background: `linear-gradient(135deg, ${poolIconColors[i] ?? '#7c3aed'}cc, ${poolIconColors[i] ?? '#7c3aed'})` }}>
|
||||||
<FontAwesomeIcon icon={poolIcons[i] ?? faTrophy} size="lg" />
|
<FontAwesomeIcon icon={poolIcons[i] ?? faStar} />
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-content">
|
|
||||||
<p className="stat-label">
|
|
||||||
<FontAwesomeIcon icon={poolIcons[i] ?? faTrophy} style={{ marginRight: '0.5rem' }} />
|
|
||||||
Points {name}
|
|
||||||
</p>
|
|
||||||
<p className="stat-value">{poolPoints[i] || 0}</p>
|
<p className="stat-value">{poolPoints[i] || 0}</p>
|
||||||
</div>
|
<p className="stat-label">Pts {name}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{total > 0 && poolNames.length > 1 && (
|
{totalPoints > 0 && (
|
||||||
<div className="stat-card2 points-total">
|
<div className="stat-card2">
|
||||||
<div className="stat-icon icon-total">
|
<div className="stat-icon icon-total">
|
||||||
<FontAwesomeIcon icon={faTrophy} size="lg" />
|
<FontAwesomeIcon icon={faTrophy} />
|
||||||
</div>
|
|
||||||
<div className="stat-content">
|
|
||||||
<p className="stat-label">
|
|
||||||
<FontAwesomeIcon icon={faTrophy} style={{ marginRight: '0.5rem' }} />
|
|
||||||
Total Points
|
|
||||||
</p>
|
|
||||||
<p className="stat-value">{total}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="stat-value">{totalPoints}</p>
|
||||||
|
<p className="stat-label">Total Points</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
})()}
|
|
||||||
|
|
||||||
{/* Carte 5: Commandes Livrées */}
|
|
||||||
<div className="stat-card2 completed-orders">
|
|
||||||
<div className="stat-icon icon-completed-orders">
|
|
||||||
<TrendingUp size={24} />
|
|
||||||
</div>
|
|
||||||
<div className="stat-content">
|
|
||||||
<p className="stat-label">Commandes livrées</p>
|
|
||||||
<p className="stat-value">{orders.length}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Carte pénalités - affichée uniquement si le score amendes est activé */}
|
|
||||||
{appSettings.show_amende_score && penalties && (
|
|
||||||
<div className={`stat-card2 penalty-stat ${penalties.total_penalty > 0 ? 'has-penalty' : ''}`}>
|
|
||||||
<div
|
|
||||||
className={`stat-icon ${
|
|
||||||
penalties.total_penalty >= 100
|
|
||||||
? 'penalty-critical'
|
|
||||||
: penalties.total_penalty > 0
|
|
||||||
? 'penalty-warning'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faExclamationTriangle} size="lg" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="stat-content">
|
|
||||||
<p className="stat-label">Points de pénalité</p>
|
|
||||||
|
|
||||||
<p className="stat-value penalty-value">
|
|
||||||
{penalties.total_penalty}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{penalties.total_penalty > 0 && (
|
|
||||||
<p className="penalty-warning-text">
|
|
||||||
{penalties.total_penalty >= 100
|
|
||||||
? 'Commandes bloquées'
|
|
||||||
: `${penalties.cancellations_count} annulation${
|
|
||||||
penalties.cancellations_count > 1 ? 's' : ''
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Carte parrainage - affichée si le parrainage est activé */}
|
{/* Score amendes */}
|
||||||
|
{appSettings.show_amende_score && (
|
||||||
|
<div className={`stat-card2${penaltyCount >= 3 ? ' stat-card-danger' : penaltyCount > 0 ? ' stat-card-warning' : ''}`}>
|
||||||
|
<div className={`stat-icon ${penaltyCount >= 3 ? 'icon-penalty-critical' : penaltyCount > 0 ? 'icon-penalty-warning' : 'icon-penalty-ok'}`}>
|
||||||
|
<FontAwesomeIcon icon={penaltyCount > 0 ? faExclamationTriangle : faShieldAlt} />
|
||||||
|
</div>
|
||||||
|
<p className={`stat-value ${penaltyCount >= 3 ? 'value-danger' : penaltyCount > 0 ? 'value-warning' : ''}`}>{penaltyCount}</p>
|
||||||
|
<p className="stat-label">Score amendes</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bouton parrainage */}
|
||||||
{appSettings.referral_enabled && (
|
{appSettings.referral_enabled && (
|
||||||
<div
|
<div className="referral-btn-row" onClick={() => navigate('/user/parrainage')}>
|
||||||
className="stat-card2 referral-stat"
|
<FontAwesomeIcon icon={faGift} className="referral-btn-icon" />
|
||||||
onClick={() => navigate('/user/parrainage')}
|
<span className="referral-btn-text">
|
||||||
style={{ cursor: 'pointer' }}
|
Parrainage{referralBalance > 0 ? ` — ${referralBalance.toFixed(2)} €` : ''}
|
||||||
>
|
</span>
|
||||||
<div className="stat-icon icon-referral">
|
<FontAwesomeIcon icon={faChevronRight} className="referral-btn-chevron" />
|
||||||
<FontAwesomeIcon icon={faGift} size="lg" />
|
|
||||||
</div>
|
|
||||||
<div className="stat-content">
|
|
||||||
<p className="stat-label">Solde parrainage</p>
|
|
||||||
<p className={`stat-value ${referralBalance > 0 ? 'referral-value-active' : ''}`}>
|
|
||||||
{referralBalance.toFixed(2)} €
|
|
||||||
</p>
|
|
||||||
<p className="referral-link-hint">Voir le programme →</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -329,104 +212,64 @@ function ConsultationHistorique() {
|
|||||||
|
|
||||||
{orders.length === 0 ? (
|
{orders.length === 0 ? (
|
||||||
<div className="empty-history">
|
<div className="empty-history">
|
||||||
<Package className="empty-icon" size={64} />
|
<FontAwesomeIcon icon={faHistory} className="empty-icon" />
|
||||||
<h2>Aucune commande terminée</h2>
|
<h2>Aucun historique</h2>
|
||||||
<button
|
<p>Vos commandes terminées apparaîtront ici</p>
|
||||||
className="browse-button"
|
<button className="browse-button" onClick={() => navigate('/user/accueil')}>
|
||||||
onClick={() => navigate('/user/accueil')}
|
|
||||||
>
|
|
||||||
Découvrir nos produits
|
Découvrir nos produits
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="orders-summary">
|
<p className="section-title">Historique des commandes</p>
|
||||||
<p>{orders.length} commande{orders.length > 1 ? 's' : ''} terminée{orders.length > 1 ? 's' : ''}</p>
|
<div className="orders-list">
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="table-wrapper">
|
|
||||||
<table className="history-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>N° Commande</th>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Adresse</th>
|
|
||||||
<th>Livreur</th>
|
|
||||||
<th>Produits</th>
|
|
||||||
<th>Total</th>
|
|
||||||
<th>Statut</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{orders.map((order) => (
|
{orders.map((order) => (
|
||||||
<tr
|
<div
|
||||||
key={order.id}
|
key={order.id}
|
||||||
onClick={() => viewOrderDetails(order.id)}
|
className="order-card"
|
||||||
className="clickable-row"
|
onClick={() => viewOrderDetails(order)}
|
||||||
>
|
>
|
||||||
<td className="order-id">
|
<div className="order-card-header">
|
||||||
#{(order.client_order_number ?? 0).toString().padStart(4, '0')}
|
<span className="order-card-number">
|
||||||
</td>
|
Commande #{(order.client_order_number ?? 0).toString().padStart(4, '0')}
|
||||||
<td>
|
|
||||||
<div className="date-cell">
|
|
||||||
<span className="date-main">
|
|
||||||
{new Date(order.created_at).toLocaleDateString('fr-FR', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: 'short',
|
|
||||||
year: 'numeric'
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="date-time">
|
<span className="order-card-badge">
|
||||||
{new Date(order.created_at).toLocaleTimeString('fr-FR', {
|
<FontAwesomeIcon icon={faCheckCircle} style={{ marginRight: '0.35rem' }} />
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span className="date-age">{getOrderAge(order.created_at)}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="address-cell">
|
|
||||||
<MapPin size={14} className="address-icon" />
|
|
||||||
<span className="address-text">
|
|
||||||
{order.adresse.length > 40
|
|
||||||
? order.adresse.substring(0, 40) + '...'
|
|
||||||
: order.adresse
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="livreur-cell">
|
|
||||||
{order.livreur_assign ? (
|
|
||||||
<>
|
|
||||||
<User size={14} className="livreur-icon" />
|
|
||||||
<span>{order.livreur_assign}</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span className="no-livreur">Non assigné</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="products-count">
|
|
||||||
~{getProductCount(order.total_prix)} produit{getProductCount(order.total_prix) > 1 ? 's' : ''}
|
|
||||||
</td>
|
|
||||||
<td className="order-total2">
|
|
||||||
{formatPrice(order.total_prix)}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span className="status-badge delivered">
|
|
||||||
<FontAwesomeIcon icon={faCheckCircle} style={{ marginRight: '0.5rem' }} />
|
|
||||||
Livrée
|
Livrée
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
|
||||||
|
<div className="order-card-row">
|
||||||
|
<FontAwesomeIcon icon={faMapMarkerAlt} className="order-card-row-icon" />
|
||||||
|
<span className="order-card-row-text">
|
||||||
|
{order.adresse ? (order.adresse.length > 50 ? order.adresse.substring(0, 50) + '…' : order.adresse) : 'N/A'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="order-card-row">
|
||||||
|
<FontAwesomeIcon icon={faClock} className="order-card-row-icon" />
|
||||||
|
<span className="order-card-row-text">
|
||||||
|
{formatDate(order.created_at)} · {getOrderAge(order.created_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{order.livreur_assign && (
|
||||||
|
<div className="order-card-row">
|
||||||
|
<FontAwesomeIcon icon={faBicycle} className="order-card-row-icon" />
|
||||||
|
<span className="order-card-row-text">{order.livreur_assign}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="order-card-footer">
|
||||||
|
<span className="order-card-total">{formatPrice(order.total_prix || 0)}</span>
|
||||||
|
<FontAwesomeIcon icon={faChevronRight} className="order-card-chevron" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams, useNavigate, useLocation } from "react-router-dom";
|
||||||
import Navbar from "../../components/Navbar";
|
import Navbar from "../../components/Navbar";
|
||||||
import "./OrderDetails.css";
|
import "./OrderDetails.css";
|
||||||
import {
|
import {
|
||||||
@@ -55,6 +55,10 @@ interface OrderDetailsData extends CompletedOrder {
|
|||||||
function OrderDetails() {
|
function OrderDetails() {
|
||||||
const { orderId } = useParams<{ orderId: string }>();
|
const { orderId } = useParams<{ orderId: string }>();
|
||||||
const navigate = useNavigate();
|
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 [order, setOrder] = useState<OrderDetailsData | null>(null);
|
||||||
const [enrichedProducts, setEnrichedProducts] = useState<
|
const [enrichedProducts, setEnrichedProducts] = useState<
|
||||||
@@ -95,8 +99,8 @@ function OrderDetails() {
|
|||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (orderId) {
|
if (commandId) {
|
||||||
fetchOrderDetails(parseInt(orderId));
|
fetchOrderDetails(commandId);
|
||||||
}
|
}
|
||||||
}, [orderId]);
|
}, [orderId]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user