chore: add ansible backend docker frontend-prep
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
// ============================================
|
||||
// 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';
|
||||
import './ConsultationHistorique.css';
|
||||
import {
|
||||
getMyCompletedOrders,
|
||||
formatPrice,
|
||||
getOrderAge,
|
||||
getMyPenalties,
|
||||
isUserAuthenticated
|
||||
} 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,
|
||||
faWind,
|
||||
faTrophy,
|
||||
faExclamationTriangle,
|
||||
faCheckCircle
|
||||
} 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);
|
||||
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();
|
||||
}, [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 });
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(authInterval);
|
||||
}, [navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchHistory();
|
||||
fetchPenalties();
|
||||
}, []);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
console.log('🔍 [DEBUG] points_zipette:', result.client_stats?.points_zipette);
|
||||
|
||||
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);
|
||||
setError('Erreur de connexion');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const getProductCount = (totalPrix: number): number => {
|
||||
return Math.max(1, Math.round(totalPrix / 25));
|
||||
};
|
||||
|
||||
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}`);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="history-container">
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner">
|
||||
<div className="spinner"></div>
|
||||
</div>
|
||||
<p className="loading-text">Chargement de l'historique...</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="history-container">
|
||||
|
||||
<div className="history-header">
|
||||
<h1 className="history-title">Historique des commandes</h1>
|
||||
</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>
|
||||
|
||||
{/* ✅ Carte 2: Points Weed/Hash - ICÔNE CANNABIS */}
|
||||
<div className="stat-card2 points-weed">
|
||||
<div className="stat-icon icon-weed">
|
||||
<FontAwesomeIcon icon={faCannabis} size="lg" />
|
||||
</div>
|
||||
<div className="stat-content">
|
||||
<p className="stat-label">
|
||||
<FontAwesomeIcon icon={faCannabis} style={{ marginRight: '0.5rem' }} />
|
||||
Points Weed/Hash
|
||||
</p>
|
||||
<p className="stat-value">{clientStats.points || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ✅ Carte 3: Points Zipette - ICÔNE VENT */}
|
||||
<div className="stat-card2 points-zipette">
|
||||
<div className="stat-icon icon-zipette">
|
||||
<FontAwesomeIcon icon={faWind} size="lg" />
|
||||
</div>
|
||||
<div className="stat-content">
|
||||
<p className="stat-label">
|
||||
<FontAwesomeIcon icon={faWind} style={{ marginRight: '0.5rem' }} />
|
||||
Points Zipette
|
||||
</p>
|
||||
<p className="stat-value">{clientStats.points_zipette || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ✅ Carte 4: Total Points - ICÔNE TROPHÉE */}
|
||||
<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">
|
||||
{(clientStats.points || 0) + (clientStats.points_zipette || 0)}
|
||||
</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 6: Pénalités - ICÔNE AVERTISSEMENT */}
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="error-banner">
|
||||
<FontAwesomeIcon icon={faExclamationTriangle} size="2x" />
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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')}
|
||||
>
|
||||
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>
|
||||
|
||||
<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.id.toString().padStart(5, '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>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConsultationHistorique;
|
||||
Reference in New Issue
Block a user