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 Navbar from "../../components/Navbar";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { isUserAuthenticated, getProductById, getMediaUrl } 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";
|
||||
|
||||
interface CartItemWithMedia {
|
||||
@@ -30,258 +21,89 @@ interface CartItemWithMedia {
|
||||
|
||||
function Cart() {
|
||||
const navigate = useNavigate();
|
||||
const { cartItems, removeFromCart, clearCart, loading, refreshCart } =
|
||||
useCart();
|
||||
const { cartItems, removeFromCart, clearCart, loading, refreshCart } = useCart();
|
||||
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 [enrichedItems, setEnrichedItems] = useState<CartItemWithMedia[]>([]);
|
||||
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(() => {
|
||||
const checkAuth = () => {
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log(
|
||||
"❌ [Cart] Utilisateur non authentifié, redirection vers /login/client",
|
||||
);
|
||||
navigate("/login/client", { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
if (!isUserAuthenticated()) navigate("/login/client", { replace: true });
|
||||
}, [navigate]);
|
||||
|
||||
// ✅ VÉRIFICATION CONTINUE (toutes les 5 secondes)
|
||||
useEffect(() => {
|
||||
const authInterval = setInterval(() => {
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log(
|
||||
"❌ [Cart] Session expirée, redirection vers /login/client",
|
||||
);
|
||||
navigate("/login/client", { replace: true });
|
||||
}
|
||||
const interval = setInterval(() => {
|
||||
if (!isUserAuthenticated()) navigate("/login/client", { replace: true });
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(authInterval);
|
||||
return () => clearInterval(interval);
|
||||
}, [navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
// ✅ Vérifier l'auth avant de rafraîchir le panier
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log("❌ [refreshCart] Non authentifié");
|
||||
navigate("/login/client", { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
|
||||
refreshCart();
|
||||
}, []);
|
||||
|
||||
// ============================================
|
||||
// CALCULS
|
||||
// ============================================
|
||||
|
||||
const total = cartItems.reduce((sum, item) => sum + item.price, 0);
|
||||
|
||||
// ============================================
|
||||
// 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";
|
||||
if (!product.media || product.media.length === 0)
|
||||
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);
|
||||
}
|
||||
|
||||
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";
|
||||
return "https://via.placeholder.com/120x120/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(() => {
|
||||
const enrichCartItems = async () => {
|
||||
if (cartItems.length === 0) {
|
||||
console.log("🔄 [CART] Panier vide, pas d'enrichissement");
|
||||
setEnrichedItems([]);
|
||||
setLoadingMedia(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Guard: ne pas re-enrichir si déjà fait pour ces mêmes items
|
||||
const sameItems =
|
||||
const enrich = async () => {
|
||||
if (cartItems.length === 0) { setEnrichedItems([]); return; }
|
||||
const same =
|
||||
enrichedItems.length === cartItems.length &&
|
||||
enrichedItems.every(
|
||||
(enriched, index) =>
|
||||
enriched.id === cartItems[index]?.id &&
|
||||
enriched.product_id === cartItems[index]?.product_id,
|
||||
);
|
||||
|
||||
if (sameItems) {
|
||||
console.log("🔄 [CART] Items déjà enrichis, skip");
|
||||
return;
|
||||
}
|
||||
enrichedItems.every((e, i) => e.id === cartItems[i]?.id && e.product_id === cartItems[i]?.product_id);
|
||||
if (same) return;
|
||||
|
||||
setLoadingMedia(true);
|
||||
console.log(
|
||||
"🔄 [CART] Enrichissement de",
|
||||
cartItems.length,
|
||||
"items...",
|
||||
);
|
||||
|
||||
try {
|
||||
const enrichedPromises = cartItems.map(async (item) => {
|
||||
try {
|
||||
console.log(
|
||||
`📦 [CART] Récupération médias pour produit ${item.product_id}...`,
|
||||
);
|
||||
const productResponse = await getProductById(
|
||||
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 {
|
||||
...item,
|
||||
image: "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image",
|
||||
hasVideo: false,
|
||||
videoUrl: undefined,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`❌ [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);
|
||||
const enriched = await Promise.all(
|
||||
cartItems.map(async (item) => {
|
||||
try {
|
||||
const res = await getProductById(item.product_id);
|
||||
if (res.success && res.data) {
|
||||
const p = res.data;
|
||||
const videoMedia = p.media?.find((m: any) => m && m.type === "video");
|
||||
return {
|
||||
...item,
|
||||
image: getProductImage(p),
|
||||
hasVideo: !!videoMedia,
|
||||
videoUrl: videoMedia?.url ? getMediaUrl(videoMedia.url) : undefined,
|
||||
};
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return { ...item, image: "https://via.placeholder.com/120x120/1a1a1a/ffffff?text=No+Image", hasVideo: false };
|
||||
})
|
||||
);
|
||||
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 {
|
||||
setLoadingMedia(false);
|
||||
}
|
||||
};
|
||||
|
||||
enrichCartItems();
|
||||
}, [cartItems]); // ✅ Dépendance sur cartItems complet
|
||||
|
||||
// ============================================
|
||||
// HANDLERS
|
||||
// ============================================
|
||||
enrich();
|
||||
}, [cartItems]);
|
||||
|
||||
const handleClearCart = async () => {
|
||||
// ✅ Vérifier l'auth avant de vider le panier
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log("❌ [handleClearCart] Non authentifié");
|
||||
navigate("/login/client", { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
|
||||
setShowClearConfirm(false);
|
||||
await clearCart();
|
||||
};
|
||||
|
||||
const handleRemoveItem = async (itemId: number) => {
|
||||
// ✅ Vérifier l'auth avant de supprimer un item
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log("❌ [handleRemoveItem] Non authentifié");
|
||||
navigate("/login/client", { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
|
||||
await removeFromCart(itemId);
|
||||
};
|
||||
|
||||
const handleCheckout = () => {
|
||||
// ✅ Vérifier l'auth avant de passer commande
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log("❌ [handleCheckout] Non authentifié");
|
||||
navigate("/login/client", { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
|
||||
navigate("/user/checkout");
|
||||
};
|
||||
|
||||
@@ -290,231 +112,145 @@ function Cart() {
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="cart-container">
|
||||
<div className="cart-header">
|
||||
<h1>Votre Panier</h1>
|
||||
</div>
|
||||
<div className="loading-cart">
|
||||
<p>Chargement de votre panier...</p>
|
||||
</div>
|
||||
<div className="loading-cart"><p>Chargement du panier...</p></div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const displayItems = enrichedItems.length > 0 ? enrichedItems : cartItems.map(i => ({ ...i, image: "", hasVideo: false }));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<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 ? (
|
||||
<div className="empty-cart">
|
||||
<ShoppingBag size={48} className="empty-cart-icon" />
|
||||
<p>Votre panier est vide</p>
|
||||
<button
|
||||
className="continue-shopping"
|
||||
onClick={() => navigate("/user/nos-produits")}
|
||||
>
|
||||
Continuer mes achats
|
||||
<ShoppingCart size={72} className="empty-cart-icon" />
|
||||
<h2>Panier vide</h2>
|
||||
<p>Ajoutez des produits pour commencer</p>
|
||||
<button className="continue-shopping" onClick={() => navigate("/user/nos-produits")}>
|
||||
Découvrir nos produits
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{loadingMedia && enrichedItems.length === 0 ? (
|
||||
<div className="loading-cart">
|
||||
<p>Chargement des médias...</p>
|
||||
{/* Header */}
|
||||
<div className="cart-header">
|
||||
<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 className="cart-items">
|
||||
{enrichedItems.map((item, index) => (
|
||||
<div
|
||||
key={`${item.id}-${index}`}
|
||||
className="cart-item"
|
||||
>
|
||||
{/* ✨ MEDIA CONTAINER - Photo + Badge Vidéo */}
|
||||
<div className="cart-item-media-container">
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.name_product}
|
||||
className="cart-item-image"
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
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,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<button className="clear-all-button" onClick={() => setShowClearConfirm(true)} disabled={loading}>
|
||||
Tout vider
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ✨ Badge vidéo si disponible */}
|
||||
{item.hasVideo && item.videoUrl && (
|
||||
{/* Liste des articles */}
|
||||
<div className="cart-items-list">
|
||||
{loadingMedia && enrichedItems.length === 0 ? (
|
||||
<div className="loading-cart"><p>Chargement des médias...</p></div>
|
||||
) : (
|
||||
displayItems.map((item, index) => (
|
||||
<div key={`${item.id}-${index}`} className="cart-row">
|
||||
|
||||
{/* Vignette image */}
|
||||
<div className="cart-row-thumb">
|
||||
{item.image ? (
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.name_product}
|
||||
className="cart-row-img"
|
||||
loading="lazy"
|
||||
onError={(e) => { e.currentTarget.src = "https://via.placeholder.com/72x72/2d2d2d/ffffff?text=?"; }}
|
||||
/>
|
||||
) : (
|
||||
<div className="cart-row-img-placeholder">
|
||||
<i className="fas fa-leaf"></i>
|
||||
</div>
|
||||
)}
|
||||
{(item as CartItemWithMedia).hasVideo && (item as CartItemWithMedia).videoUrl && (
|
||||
<button
|
||||
className="cart-video-badge"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleVideoToggle(
|
||||
item.videoUrl!,
|
||||
);
|
||||
}}
|
||||
aria-label="Voir la vidéo du produit"
|
||||
className="cart-row-video-badge"
|
||||
onClick={(e) => { e.stopPropagation(); setCurrentVideoUrl((item as CartItemWithMedia).videoUrl!); setShowVideo(true); }}
|
||||
aria-label="Voir la vidéo"
|
||||
>
|
||||
<i className="fas fa-camera"></i>
|
||||
<span>Vidéo</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="cart-item-info">
|
||||
<h3>{item.name_product}</h3>
|
||||
<p className="cart-item-category">
|
||||
Catégorie: {item.category}
|
||||
</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>
|
||||
{/* Infos */}
|
||||
<div className="cart-row-info">
|
||||
<p className="cart-row-name">{item.name_product}</p>
|
||||
<p className="cart-row-qty">{item.quantity}g</p>
|
||||
<p className="cart-row-price">{item.price.toFixed(2)} €</p>
|
||||
</div>
|
||||
|
||||
{/* Bouton supprimer */}
|
||||
<div className="cart-item-controls">
|
||||
<button
|
||||
className="remove-item-button"
|
||||
onClick={() =>
|
||||
handleRemoveItem(item.id)
|
||||
}
|
||||
disabled={loading}
|
||||
title="Supprimer cet article"
|
||||
aria-label={`Supprimer ${item.name_product}`}
|
||||
>
|
||||
<Trash2
|
||||
size={18}
|
||||
className="trash-icon"
|
||||
/>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="cart-row-remove"
|
||||
onClick={() => handleRemoveItem(item.id)}
|
||||
disabled={loading}
|
||||
aria-label={`Supprimer ${item.name_product}`}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="cart-summary">
|
||||
<div className="summary-row">
|
||||
<span>Nombre d'articles:</span>
|
||||
<span>{cartItems.length}</span>
|
||||
{/* Footer fixe */}
|
||||
<div className="cart-footer">
|
||||
<div className="cart-footer-total">
|
||||
<span className="cart-footer-total-label">Total</span>
|
||||
<span className="cart-footer-total-value">{total.toFixed(2)} €</span>
|
||||
</div>
|
||||
|
||||
<div className="summary-row total-row">
|
||||
<span>Total:</span>
|
||||
<span className="total-amount">
|
||||
{total.toFixed(2)} €
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="checkout-button"
|
||||
onClick={handleCheckout}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading
|
||||
? "Chargement..."
|
||||
: "Valider la commande"}
|
||||
<button className="checkout-button" onClick={handleCheckout} disabled={loading}>
|
||||
{loading ? "Chargement..." : "Commander"}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal de confirmation vidage */}
|
||||
{/* Modal vider le panier */}
|
||||
{showClearConfirm && (
|
||||
<div
|
||||
className="modal-overlay"
|
||||
onClick={() => setShowClearConfirm(false)}
|
||||
>
|
||||
<div
|
||||
className="modal-content2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="modal-overlay" onClick={() => setShowClearConfirm(false)}>
|
||||
<div className="modal-content2" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-icon-container">
|
||||
<AlertTriangle size={48} className="modal-icon" />
|
||||
<AlertTriangle size={40} className="modal-icon" />
|
||||
</div>
|
||||
|
||||
<h2 className="modal-title">Vider le panier</h2>
|
||||
<p className="modal-message">
|
||||
Êtes-vous sûr de vouloir supprimer tous les articles
|
||||
de votre panier ? Cette action est irréversible.
|
||||
Supprimer les {cartItems.length} article{cartItems.length > 1 ? "s" : ""} du panier ?
|
||||
</p>
|
||||
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
className="modal-button modal-cancel"
|
||||
onClick={() => setShowClearConfirm(false)}
|
||||
>
|
||||
<button className="modal-button modal-cancel" onClick={() => setShowClearConfirm(false)}>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
className="modal-button modal-confirm"
|
||||
onClick={handleClearCart}
|
||||
disabled={loading}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
Tout supprimer
|
||||
<button className="modal-button modal-confirm" onClick={handleClearCart} disabled={loading}>
|
||||
<Trash2 size={16} />
|
||||
Vider
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ✨ Modal vidéo (COMME PRODUCTCARD) */}
|
||||
{/* 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"
|
||||
>
|
||||
<div className="video-modal" onClick={() => { setShowVideo(false); setCurrentVideoUrl(""); }}>
|
||||
<div className="video-modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="video-close-btn" onClick={() => { setShowVideo(false); setCurrentVideoUrl(""); }}>
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
<video
|
||||
src={currentVideoUrl}
|
||||
controls
|
||||
autoPlay
|
||||
className="video-player"
|
||||
>
|
||||
Votre navigateur ne supporte pas la lecture de
|
||||
vidéos.
|
||||
<video src={currentVideoUrl} controls autoPlay className="video-player">
|
||||
Votre navigateur ne supporte pas la lecture de vidéos.
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -138,6 +138,15 @@
|
||||
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 {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
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 { Product } from '../../api/api';
|
||||
import Navbar from '../../components/Navbar';
|
||||
import './Checkout.css';
|
||||
|
||||
@@ -55,6 +56,25 @@ function Checkout() {
|
||||
|
||||
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
|
||||
const [referralBalance, setReferralBalance] = useState(0);
|
||||
const [referralEnabled, setReferralEnabled] = useState(false);
|
||||
@@ -378,7 +398,13 @@ function Checkout() {
|
||||
<div className="summary-items">
|
||||
{cartItems.map((item, index) => (
|
||||
<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">
|
||||
<p className="summary-item-name">{item.name_product}</p>
|
||||
<p className="summary-item-details">
|
||||
|
||||
@@ -1,246 +1,269 @@
|
||||
/* ============================================
|
||||
ConsultationHistorique.css - VERSION VIOLET SOMBRE
|
||||
============================================
|
||||
Styles pour 4 compteurs de points distincts */
|
||||
ConsultationHistorique.css — style mobile
|
||||
============================================ */
|
||||
|
||||
.history-container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
padding: clamp(1rem, 3vw, 2rem);
|
||||
padding-top: calc(60px + clamp(1rem, 3vw, 2rem));
|
||||
max-width: 1400px;
|
||||
padding-top: calc(60px + clamp(1rem, 3vw, 1.5rem));
|
||||
max-width: 900px;
|
||||
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 {
|
||||
font-size: clamp(2rem, 6vw, 2.5rem);
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.history-subtitle {
|
||||
color: var(--text-muted);
|
||||
font-size: clamp(0.9rem, 2vw, 1.1rem);
|
||||
margin: 0;
|
||||
margin: 0 0 1.25rem 0;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
STATISTIQUES - GRID ADAPTÉ POUR 6 CARTES
|
||||
STATS GRID — cartes centrées (style mobile)
|
||||
============================================ */
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stat-card2:last-child:nth-child(odd) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.stat-card2 {
|
||||
background: linear-gradient(135deg, var(--surface), var(--surface-2));
|
||||
border: 2px solid var(--border);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
padding: 1.25rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||
text-align: center;
|
||||
gap: 0.35rem;
|
||||
transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
✅ HOVER VIOLET SOMBRE POUR TOUTES LES CARTES
|
||||
============================================ */
|
||||
|
||||
.stat-card2:hover {
|
||||
border-color: #6d28d9;
|
||||
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 {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
border-radius: 12px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
font-size: 1.1rem;
|
||||
color: #fff;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
✅ ICÔNES POUR COMMANDES - VIOLET SOMBRE
|
||||
============================================ */
|
||||
|
||||
.stat-icon.icon-total-orders {
|
||||
background: linear-gradient(135deg, #6d28d9, #5b21b6);
|
||||
.stat-icon.icon-total-orders { background: linear-gradient(135deg, #7c3aed, #6d28d9); }
|
||||
.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-penalty-critical {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
animation: pulse-danger 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.stat-icon.icon-completed-orders {
|
||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
✅ 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;
|
||||
@keyframes pulse-danger {
|
||||
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); }
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: var(--text);
|
||||
font-size: 1.8rem;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
PENALTY STAT CARD - VIOLET SOMBRE
|
||||
============================================ */
|
||||
.stat-value.value-danger { color: #ef4444; }
|
||||
.stat-value.value-warning { color: #f59e0b; }
|
||||
|
||||
.penalty-stat {
|
||||
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;
|
||||
.stat-label {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.78rem;
|
||||
margin: 0;
|
||||
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;
|
||||
font-size: 0.85rem;
|
||||
margin: 0.5rem 0 0 0;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.referral-btn-chevron {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
PARRAINAGE STAT CARD
|
||||
SECTION TITLE
|
||||
============================================ */
|
||||
|
||||
.stat-card2.referral-stat:hover {
|
||||
border-color: #8b5cf6;
|
||||
box-shadow: 0 8px 25px rgba(139, 92, 246, 0.4);
|
||||
.section-title {
|
||||
color: var(--text-muted);
|
||||
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 {
|
||||
color: #8b5cf6 !important;
|
||||
.order-card {
|
||||
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);
|
||||
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 {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 4px solid var(--border);
|
||||
border: 3px solid var(--border);
|
||||
border-top-color: #7c3aed;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
animation: spin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: var(--text-muted);
|
||||
font-size: 1.1rem;
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -290,19 +311,15 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
background: rgba(109, 40, 217, 0.1);
|
||||
border: 2px solid #7c3aed;
|
||||
border-radius: 12px;
|
||||
padding: 1rem 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.error-banner span {
|
||||
font-size: 1.5rem;
|
||||
background: rgba(109, 40, 217, 0.08);
|
||||
border: 1px solid rgba(124, 58, 237, 0.4);
|
||||
border-radius: 10px;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #c4b5fd;
|
||||
}
|
||||
|
||||
.error-banner p {
|
||||
color: #c4b5fd;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -314,372 +331,63 @@
|
||||
.empty-history {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
background: linear-gradient(135deg, var(--surface), var(--surface-2));
|
||||
border: 2px solid var(--border);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
margin-top: 2rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1.5rem;
|
||||
opacity: 0.5;
|
||||
opacity: 0.4;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.empty-history h2 {
|
||||
color: var(--text);
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.4rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-history p {
|
||||
color: var(--text-muted);
|
||||
font-size: 1.1rem;
|
||||
margin: 0 0 2rem 0;
|
||||
font-size: 0.95rem;
|
||||
margin: 0 0 1.75rem 0;
|
||||
}
|
||||
|
||||
.browse-button {
|
||||
background: linear-gradient(to right, #7c3aed, #6d28d9);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 2rem;
|
||||
padding: 0.7rem 1.75rem;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 4px 15px rgba(124, 58, 237, 0.3);
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.browse-button:hover {
|
||||
background: linear-gradient(to right, #6d28d9, #5b21b6);
|
||||
transform: translateY(-2px);
|
||||
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;
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
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) {
|
||||
.history-container {
|
||||
padding: 1rem;
|
||||
padding-top: calc(60px + 1rem);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.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;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.empty-history {
|
||||
padding: 3rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
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;
|
||||
.order-card-number {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 { useNavigate } from 'react-router-dom';
|
||||
import Navbar from '../../components/Navbar';
|
||||
@@ -19,9 +13,7 @@ import {
|
||||
} from '../../api/api';
|
||||
import type { PublicSettings } from '../../api/api';
|
||||
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 {
|
||||
faCannabis,
|
||||
@@ -31,8 +23,15 @@ import {
|
||||
faStar,
|
||||
faTrophy,
|
||||
faExclamationTriangle,
|
||||
faCheckCircle,
|
||||
faShieldAlt,
|
||||
faGift,
|
||||
faReceipt,
|
||||
faMapMarkerAlt,
|
||||
faClock,
|
||||
faBicycle,
|
||||
faChevronRight,
|
||||
faCheckCircle,
|
||||
faHistory,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
function ConsultationHistorique() {
|
||||
@@ -46,28 +45,15 @@ function ConsultationHistorique() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log('❌ [ConsultationHistorique] Utilisateur non authentifié, redirection vers /login/client');
|
||||
navigate('/login/client', { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
if (!isUserAuthenticated()) navigate('/login/client', { replace: true });
|
||||
}, [navigate]);
|
||||
|
||||
// ✅ VÉRIFICATION CONTINUE (toutes les 5 secondes)
|
||||
useEffect(() => {
|
||||
const authInterval = setInterval(() => {
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log('❌ [ConsultationHistorique] Session expirée, redirection vers /login/client');
|
||||
navigate('/login/client', { replace: true });
|
||||
}
|
||||
const interval = setInterval(() => {
|
||||
if (!isUserAuthenticated()) navigate('/login/client', { replace: true });
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(authInterval);
|
||||
return () => clearInterval(interval);
|
||||
}, [navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -82,34 +68,18 @@ function ConsultationHistorique() {
|
||||
}, []);
|
||||
|
||||
const fetchHistory = async () => {
|
||||
// ✅ Vérifier l'auth avant de charger l'historique
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log('❌ [fetchHistory] Non authentifié');
|
||||
navigate('/login/client', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isUserAuthenticated()) { navigate('/login/client', { replace: true }); return; }
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
console.log('📚 [HISTORY] Chargement historique...');
|
||||
const result = await getMyCompletedOrders();
|
||||
|
||||
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);
|
||||
setClientStats(result.client_stats || null);
|
||||
} else {
|
||||
console.error('❌ [HISTORY] Erreur:', result.message);
|
||||
setError(result.message || 'Erreur lors du chargement');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ [HISTORY] Erreur catch:', err);
|
||||
} catch {
|
||||
setError('Erreur de connexion');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -117,39 +87,23 @@ function ConsultationHistorique() {
|
||||
};
|
||||
|
||||
const fetchPenalties = async () => {
|
||||
// ✅ Vérifier l'auth avant de charger les pénalités
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log('❌ [fetchPenalties] Non authentifié');
|
||||
navigate('/login/client', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isUserAuthenticated()) return;
|
||||
try {
|
||||
console.log('🚨 [PENALTIES] Chargement pénalités...');
|
||||
const result = await getMyPenalties();
|
||||
|
||||
if (result.success && result.data) {
|
||||
console.log('✅ [PENALTIES] Pénalités chargées:', result.data);
|
||||
setPenalties(result.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ [PENALTIES] Erreur:', err);
|
||||
}
|
||||
if (result.success && result.data) setPenalties(result.data);
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
||||
const getProductCount = (totalPrix: number): number => {
|
||||
return Math.max(1, Math.round(totalPrix / 25));
|
||||
const viewOrderDetails = (order: CompletedOrder) => {
|
||||
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) => {
|
||||
// ✅ Vérifier l'auth avant de naviguer
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log('❌ [viewOrderDetails] Non authentifié');
|
||||
navigate('/login/client', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(`/user/commande/${orderId}`);
|
||||
const formatDate = (dateStr: string) => {
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
@@ -158,9 +112,7 @@ function ConsultationHistorique() {
|
||||
<Navbar />
|
||||
<div className="history-container">
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner">
|
||||
<div className="spinner"></div>
|
||||
</div>
|
||||
<div className="loading-spinner"><div className="spinner"></div></div>
|
||||
<p className="loading-text">Chargement de l'historique...</p>
|
||||
</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 (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="history-container">
|
||||
|
||||
<div className="history-header">
|
||||
<h1 className="history-title">Historique des commandes</h1>
|
||||
<h1 className="history-title">Historique des commandes</h1>
|
||||
|
||||
{/* Stats grid */}
|
||||
<div className="stats-grid">
|
||||
|
||||
{/* Total commandes */}
|
||||
<div className="stat-card2">
|
||||
<div className="stat-icon icon-total-orders">
|
||||
<FontAwesomeIcon icon={faReceipt} />
|
||||
</div>
|
||||
<p className="stat-value">{clientStats?.total_commands ?? orders.length}</p>
|
||||
<p className="stat-label">Commandes</p>
|
||||
</div>
|
||||
|
||||
{/* Points */}
|
||||
{appSettings.points_enabled && (
|
||||
poolNames.length <= 1 ? (
|
||||
<div className="stat-card2">
|
||||
<div className="stat-icon icon-total">
|
||||
<FontAwesomeIcon icon={faTrophy} />
|
||||
</div>
|
||||
<p className="stat-value">{poolPoints[0] || 0}</p>
|
||||
<p className="stat-label">Pts {poolNames[0] ?? 'Points'}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{poolNames.map((name, i) => (
|
||||
<div key={i} className="stat-card2">
|
||||
<div className="stat-icon" style={{ background: `linear-gradient(135deg, ${poolIconColors[i] ?? '#7c3aed'}cc, ${poolIconColors[i] ?? '#7c3aed'})` }}>
|
||||
<FontAwesomeIcon icon={poolIcons[i] ?? faStar} />
|
||||
</div>
|
||||
<p className="stat-value">{poolPoints[i] || 0}</p>
|
||||
<p className="stat-label">Pts {name}</p>
|
||||
</div>
|
||||
))}
|
||||
{totalPoints > 0 && (
|
||||
<div className="stat-card2">
|
||||
<div className="stat-icon icon-total">
|
||||
<FontAwesomeIcon icon={faTrophy} />
|
||||
</div>
|
||||
<p className="stat-value">{totalPoints}</p>
|
||||
<p className="stat-label">Total Points</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* ✅ STATISTIQUES CLIENT - 6 CARTES AVEC ICÔNES FONT AWESOME */}
|
||||
{clientStats && (
|
||||
<div className="stats-grid">
|
||||
{/* Carte 1: Total Commandes */}
|
||||
<div className="stat-card2 total-orders">
|
||||
<div className="stat-icon icon-total-orders">
|
||||
<Package size={24} />
|
||||
</div>
|
||||
<div className="stat-content">
|
||||
<p className="stat-label">Total commandes</p>
|
||||
<p className="stat-value">{clientStats.total_commands}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cartes points - affichées uniquement si le système de points est activé */}
|
||||
{appSettings.points_enabled && (() => {
|
||||
const poolNames = clientStats.pool_names?.length ? clientStats.pool_names : appSettings.pool_names;
|
||||
const poolPoints = clientStats.pool_points ?? [clientStats.points];
|
||||
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">
|
||||
<FontAwesomeIcon icon={faTrophy} size="lg" />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const total = poolPoints.reduce((s, v) => s + (v || 0), 0);
|
||||
return (
|
||||
<>
|
||||
{poolNames.map((name, i) => (
|
||||
<div key={i} className={`stat-card2 ${poolClasses[i] ?? 'points-extra'}`}>
|
||||
<div className={`stat-icon ${poolIconClasses[i] ?? 'icon-total'}`}>
|
||||
<FontAwesomeIcon icon={poolIcons[i] ?? faTrophy} size="lg" />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{total > 0 && poolNames.length > 1 && (
|
||||
<div className="stat-card2 points-total">
|
||||
<div className="stat-icon icon-total">
|
||||
<FontAwesomeIcon icon={faTrophy} size="lg" />
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 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é */}
|
||||
{appSettings.referral_enabled && (
|
||||
<div
|
||||
className="stat-card2 referral-stat"
|
||||
onClick={() => navigate('/user/parrainage')}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div className="stat-icon icon-referral">
|
||||
<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>
|
||||
)}
|
||||
{/* Bouton parrainage */}
|
||||
{appSettings.referral_enabled && (
|
||||
<div className="referral-btn-row" onClick={() => navigate('/user/parrainage')}>
|
||||
<FontAwesomeIcon icon={faGift} className="referral-btn-icon" />
|
||||
<span className="referral-btn-text">
|
||||
Parrainage{referralBalance > 0 ? ` — ${referralBalance.toFixed(2)} €` : ''}
|
||||
</span>
|
||||
<FontAwesomeIcon icon={faChevronRight} className="referral-btn-chevron" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -329,104 +212,64 @@ function ConsultationHistorique() {
|
||||
|
||||
{orders.length === 0 ? (
|
||||
<div className="empty-history">
|
||||
<Package className="empty-icon" size={64} />
|
||||
<h2>Aucune commande terminée</h2>
|
||||
<button
|
||||
className="browse-button"
|
||||
onClick={() => navigate('/user/accueil')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faHistory} className="empty-icon" />
|
||||
<h2>Aucun historique</h2>
|
||||
<p>Vos commandes terminées apparaîtront ici</p>
|
||||
<button className="browse-button" onClick={() => navigate('/user/accueil')}>
|
||||
Découvrir nos produits
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="orders-summary">
|
||||
<p>{orders.length} commande{orders.length > 1 ? 's' : ''} terminée{orders.length > 1 ? 's' : ''}</p>
|
||||
</div>
|
||||
<p className="section-title">Historique des commandes</p>
|
||||
<div className="orders-list">
|
||||
{orders.map((order) => (
|
||||
<div
|
||||
key={order.id}
|
||||
className="order-card"
|
||||
onClick={() => viewOrderDetails(order)}
|
||||
>
|
||||
<div className="order-card-header">
|
||||
<span className="order-card-number">
|
||||
Commande #{(order.client_order_number ?? 0).toString().padStart(4, '0')}
|
||||
</span>
|
||||
<span className="order-card-badge">
|
||||
<FontAwesomeIcon icon={faCheckCircle} style={{ marginRight: '0.35rem' }} />
|
||||
Livrée
|
||||
</span>
|
||||
</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) => (
|
||||
<tr
|
||||
key={order.id}
|
||||
onClick={() => viewOrderDetails(order.id)}
|
||||
className="clickable-row"
|
||||
>
|
||||
<td className="order-id">
|
||||
#{(order.client_order_number ?? 0).toString().padStart(4, '0')}
|
||||
</td>
|
||||
<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 className="date-time">
|
||||
{new Date(order.created_at).toLocaleTimeString('fr-FR', {
|
||||
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
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 "./OrderDetails.css";
|
||||
import {
|
||||
@@ -55,6 +55,10 @@ interface OrderDetailsData extends CompletedOrder {
|
||||
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<
|
||||
@@ -95,8 +99,8 @@ function OrderDetails() {
|
||||
}, [navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (orderId) {
|
||||
fetchOrderDetails(parseInt(orderId));
|
||||
if (commandId) {
|
||||
fetchOrderDetails(commandId);
|
||||
}
|
||||
}, [orderId]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user