chore: add ansible backend docker frontend-prep

This commit is contained in:
2026-01-21 13:05:13 +01:00
parent 5a280b6b01
commit 943fe4de7d
14930 changed files with 2341433 additions and 0 deletions
+433
View File
@@ -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;