chore: fix ui

This commit is contained in:
2026-03-31 19:50:44 +02:00
parent ff75c9811a
commit 8fc1335588
7 changed files with 860 additions and 1745 deletions
File diff suppressed because it is too large Load Diff
+125 -389
View File
@@ -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;
+28 -2
View File
@@ -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,13 +23,20 @@ import {
faStar,
faTrophy,
faExclamationTriangle,
faCheckCircle,
faShieldAlt,
faGift,
faReceipt,
faMapMarkerAlt,
faClock,
faBicycle,
faChevronRight,
faCheckCircle,
faHistory,
} from '@fortawesome/free-solid-svg-icons';
function ConsultationHistorique() {
const navigate = useNavigate();
const [orders, setOrders] = useState<CompletedOrder[]>([]);
const [clientStats, setClientStats] = useState<ClientStats | null>(null);
const [penalties, setPenalties] = useState<PenaltyInfo | null>(null);
@@ -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,107 +212,67 @@ 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>
</>
);
}
export default ConsultationHistorique;
export default ConsultationHistorique;
@@ -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]);