chore: add ansible backend docker frontend-prep
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useCart } from '../../context/CartContext';
|
||||
import { createCheckout, clearCart, extractUsernameFromToken, isUserAuthenticated } from '../../api/api';
|
||||
import type { CheckoutData } from '../../api/api';
|
||||
import Navbar from '../../components/Navbar';
|
||||
import './Checkout.css';
|
||||
|
||||
// ============================================
|
||||
// Interface pour les données du modal
|
||||
// ============================================
|
||||
interface ConfirmationData {
|
||||
command_id: number;
|
||||
assigned_to?: {
|
||||
username: string;
|
||||
distance_km?: number;
|
||||
eta_minutes?: number;
|
||||
};
|
||||
queue_info?: {
|
||||
position: number;
|
||||
estimated_wait?: string;
|
||||
};
|
||||
delivery_address: string;
|
||||
arrivalTime: string;
|
||||
total: number;
|
||||
clientInfo: {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
phone: string;
|
||||
delivery_address: string;
|
||||
};
|
||||
}
|
||||
|
||||
function Checkout() {
|
||||
const navigate = useNavigate();
|
||||
const { cartItems, clearCart: clearCartContext, cartTotal } = useCart();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// État pour le modal de confirmation
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
const [confirmationData, setConfirmationData] = useState<ConfirmationData | null>(null);
|
||||
|
||||
// Informations personnelles
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
const [address, setAddress] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
|
||||
const total = cartTotal;
|
||||
|
||||
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log('❌ [Checkout] 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('❌ [Checkout] Session expirée, redirection vers /login/client');
|
||||
navigate('/login/client', { replace: true });
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(authInterval);
|
||||
}, [navigate]);
|
||||
|
||||
/**
|
||||
* ✅ Récupérer le username du JWT
|
||||
*/
|
||||
const getUsername = (): string | null => {
|
||||
const username = extractUsernameFromToken();
|
||||
|
||||
if (!username) {
|
||||
console.warn('⚠️ Impossible d\'extraire le username du JWT');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('✅ Username du JWT:', username);
|
||||
return username;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculer et formater l'heure d'arrivée estimée
|
||||
*/
|
||||
const calculateArrivalTime = (eta_minutes?: number, estimated_wait?: string): string => {
|
||||
const arrivalTime = new Date();
|
||||
let totalMinutes = 0;
|
||||
|
||||
if (eta_minutes) {
|
||||
totalMinutes = eta_minutes;
|
||||
} else if (estimated_wait) {
|
||||
const hourMatch = estimated_wait.match(/(\d+)\s*hour/i);
|
||||
const minuteMatch = estimated_wait.match(/(\d+)\s*minute/i);
|
||||
|
||||
if (hourMatch) {
|
||||
totalMinutes += parseInt(hourMatch[1]) * 60;
|
||||
}
|
||||
if (minuteMatch) {
|
||||
totalMinutes += parseInt(minuteMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
arrivalTime.setMinutes(arrivalTime.getMinutes() + totalMinutes);
|
||||
|
||||
return arrivalTime.toLocaleTimeString('fr-FR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* ✅ Gérer la soumission de la commande
|
||||
*/
|
||||
const handleSubmitOrder = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
// ✅ Vérifier l'auth avant de soumettre
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log('❌ [handleSubmitOrder] Non authentifié');
|
||||
navigate('/login/client', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation
|
||||
if (!firstName || !lastName || !address || !phone) {
|
||||
setError('Veuillez remplir tous les champs obligatoires');
|
||||
return;
|
||||
}
|
||||
|
||||
const username = getUsername();
|
||||
|
||||
if (!username) {
|
||||
setError('❌ Session expirée - Veuillez vous reconnecter');
|
||||
navigate('/login/client', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('👤 Username du JWT:', username);
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const checkoutData: CheckoutData = {
|
||||
username,
|
||||
delivery_address: address,
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
phone,
|
||||
payment_method: 'especes'
|
||||
};
|
||||
|
||||
console.log('📤 Envoi checkout avec JWT username:', checkoutData);
|
||||
|
||||
const response = await createCheckout(checkoutData);
|
||||
console.log('📥 Réponse checkout:', response);
|
||||
|
||||
if (response.success && response.command_id) {
|
||||
const { command_id, assigned_to, queue_info, delivery_address } = response;
|
||||
|
||||
const frontendTotal = total;
|
||||
|
||||
console.log(`💰 Total commande: ${frontendTotal.toFixed(2)}€`);
|
||||
|
||||
// Calculer l'heure d'arrivée
|
||||
const arrivalTime = calculateArrivalTime(
|
||||
assigned_to?.eta_minutes,
|
||||
queue_info?.estimated_wait
|
||||
);
|
||||
|
||||
// ✅ Vider le panier AVANT d'afficher la confirmation
|
||||
console.log('🗑️ [CHECKOUT] Vidage du panier après commande réussie...');
|
||||
|
||||
try {
|
||||
const clearResponse = await clearCart(username);
|
||||
console.log('📥 [CHECKOUT] Réponse clearCart:', clearResponse);
|
||||
|
||||
if (clearResponse.success) {
|
||||
await clearCartContext();
|
||||
console.log('✅ [CHECKOUT] Panier vidé avec succès');
|
||||
} else {
|
||||
console.warn('⚠️ [CHECKOUT] Erreur API clearCart (non bloquant):', clearResponse.message);
|
||||
await clearCartContext();
|
||||
}
|
||||
} catch (clearErr) {
|
||||
console.warn('⚠️ [CHECKOUT] Erreur au vidage du panier (non bloquant):', clearErr);
|
||||
|
||||
try {
|
||||
await clearCartContext();
|
||||
} catch (localClearErr) {
|
||||
console.error('❌ [CHECKOUT] Impossible de vider le panier local:', localClearErr);
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Préparer les données pour le modal
|
||||
setConfirmationData({
|
||||
command_id,
|
||||
assigned_to,
|
||||
queue_info,
|
||||
delivery_address: delivery_address || address,
|
||||
arrivalTime,
|
||||
total: frontendTotal,
|
||||
clientInfo: {
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
phone,
|
||||
delivery_address: address
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Afficher le modal
|
||||
setShowConfirmation(true);
|
||||
|
||||
} else {
|
||||
setError(response.message || '❌ Erreur lors de la validation de la commande');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('❌ Erreur checkout:', err);
|
||||
setError(err.message || '❌ Erreur serveur. Veuillez réessayer.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ✅ Fermer le modal et rediriger
|
||||
*/
|
||||
const handleCloseConfirmation = () => {
|
||||
setShowConfirmation(false);
|
||||
setConfirmationData(null);
|
||||
navigate('/user/suivi-livraison');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="checkout-container">
|
||||
<h1>Finaliser la commande</h1>
|
||||
|
||||
<div className="checkout-content">
|
||||
{/* Résumé de la commande */}
|
||||
<div className="order-summary-box">
|
||||
<h2>Résumé de la commande</h2>
|
||||
<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" />
|
||||
<div className="summary-item-info">
|
||||
<p className="summary-item-name">{item.name_product}</p>
|
||||
<p className="summary-item-details">
|
||||
{item.quantity}x - {item.price.toFixed(2)} €
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="summary-total">
|
||||
<span>Total:</span>
|
||||
<span className="total-price">{total.toFixed(2)} €</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Formulaire */}
|
||||
<form className="checkout-form" onSubmit={handleSubmitOrder}>
|
||||
{error && (
|
||||
<div className="error-alert">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-section">
|
||||
<h3>Informations personnelles</h3>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="firstName">Prénom *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="lastName">Nom *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="address">Adresse de livraison *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="address"
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
placeholder="Numéro, rue, ville, code postal"
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="phone">Téléphone *</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder="+33 6 12 34 56 78"
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="back-button"
|
||||
onClick={() => navigate('/user/panier')}
|
||||
disabled={loading}
|
||||
>
|
||||
← Retour au panier
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="submit-order-button"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Validation en cours...' : 'Passer la commande'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ============================================ */}
|
||||
{/* MODAL DE CONFIRMATION STYLISÉ */}
|
||||
{/* ============================================ */}
|
||||
{showConfirmation && confirmationData && (
|
||||
<div className="confirmation-modal-overlay" onClick={handleCloseConfirmation}>
|
||||
<div className="confirmation-modal" onClick={(e) => e.stopPropagation()}>
|
||||
{/* Header */}
|
||||
<div className="confirmation-modal-header">
|
||||
<i className="fas fa-check-circle confirmation-icon"></i>
|
||||
<h2>Commande Confirmée</h2>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="confirmation-modal-body">
|
||||
{/* Numéro de commande */}
|
||||
<div className="confirmation-section">
|
||||
<div className="confirmation-section-title">
|
||||
<i className="fas fa-receipt icon"></i>
|
||||
Détails de la commande
|
||||
</div>
|
||||
<div className="confirmation-detail">
|
||||
<strong>Numéro:</strong> #{confirmationData.command_id}
|
||||
</div>
|
||||
<div className="confirmation-detail">
|
||||
<strong>Date:</strong> {new Date().toLocaleString('fr-FR')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Informations client */}
|
||||
<div className="confirmation-section">
|
||||
<div className="confirmation-section-title">
|
||||
<i className="fas fa-user icon"></i>
|
||||
Informations client
|
||||
</div>
|
||||
<div className="confirmation-detail">
|
||||
<strong>Nom:</strong> {confirmationData.clientInfo.first_name} {confirmationData.clientInfo.last_name}
|
||||
</div>
|
||||
<div className="confirmation-detail">
|
||||
<strong>Téléphone:</strong> {confirmationData.clientInfo.phone}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Adresse de livraison */}
|
||||
<div className="confirmation-section">
|
||||
<div className="confirmation-section-title">
|
||||
<i className="fas fa-map-marker-alt icon"></i>
|
||||
Adresse de livraison
|
||||
</div>
|
||||
<div className="confirmation-detail">
|
||||
{confirmationData.delivery_address}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total */}
|
||||
<div className="confirmation-total">
|
||||
<div className="confirmation-total-label">
|
||||
<i className="fas fa-euro-sign"></i> TOTAL
|
||||
</div>
|
||||
<div className="confirmation-total-amount">{confirmationData.total.toFixed(2)} €</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="confirmation-modal-actions">
|
||||
<button className="confirmation-button" onClick={handleCloseConfirmation}>
|
||||
<i className="fas fa-location-arrow"></i> Suivre ma livraison
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Checkout;
|
||||
Reference in New Issue
Block a user