807 lines
30 KiB
TypeScript
807 lines
30 KiB
TypeScript
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, 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';
|
|
|
|
// ============================================
|
|
// Interface pour les données du modal
|
|
// ============================================
|
|
interface ConfirmationData {
|
|
command_id: number;
|
|
client_order_number?: 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;
|
|
referral_used?: 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);
|
|
|
|
// État pour le modal zone non desservie
|
|
const [showZoneModal, setShowZoneModal] = useState(false);
|
|
const [zoneErrorMsg, setZoneErrorMsg] = useState('');
|
|
|
|
// Informations personnelles
|
|
const [firstName, setFirstName] = useState('');
|
|
const [lastName, setLastName] = useState('');
|
|
const [address, setAddress] = useState('');
|
|
const [phone, setPhone] = useState('');
|
|
|
|
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) => 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);
|
|
const [useReferral, setUseReferral] = useState(false);
|
|
|
|
// Crypto
|
|
const [cryptoEnabled, setCryptoEnabled] = useState(false);
|
|
const [cryptoOnly, setCryptoOnly] = useState(false);
|
|
const [cryptoCurrencies, setCryptoCurrencies] = useState<string[]>([]);
|
|
const [paymentMethod, setPaymentMethod] = useState<'especes' | 'crypto'>('especes');
|
|
const [payCurrency, setPayCurrency] = useState('');
|
|
const [cryptoPaymentData, setCryptoPaymentData] = useState<CryptoPaymentStatus | null>(null);
|
|
const [showCryptoModal, setShowCryptoModal] = useState(false);
|
|
const [cryptoPolling, setCryptoPolling] = useState(false);
|
|
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
// ✅ 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]);
|
|
|
|
// Charger les infos par défaut depuis le profil
|
|
useEffect(() => {
|
|
const savedAddress = localStorage.getItem('profile_default_address');
|
|
const savedPhone = localStorage.getItem('profile_default_phone');
|
|
if (savedAddress) setAddress(savedAddress);
|
|
if (savedPhone) setPhone(savedPhone);
|
|
|
|
// Pré-remplir nom/prénom depuis le backend
|
|
getMyProfile().then((res) => {
|
|
if (res.success && res.client) {
|
|
if (res.client.nom) setLastName(res.client.nom);
|
|
if (res.client.prenom) setFirstName(res.client.prenom);
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
// Charger settings publics (parrainage + crypto)
|
|
useEffect(() => {
|
|
getPublicSettings().then((settings) => {
|
|
if (settings.referral_enabled) {
|
|
setReferralEnabled(true);
|
|
getReferralBalance().then((res) => {
|
|
if (res.success) setReferralBalance(res.balance);
|
|
});
|
|
}
|
|
if (settings.crypto_payment_enabled && settings.nowpayments_currencies.length > 0) {
|
|
setCryptoEnabled(true);
|
|
setCryptoCurrencies(settings.nowpayments_currencies);
|
|
setPayCurrency(settings.nowpayments_currencies[0]);
|
|
if (settings.crypto_only) {
|
|
setCryptoOnly(true);
|
|
setPaymentMethod('crypto');
|
|
}
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
// Nettoyage du polling au démontage
|
|
useEffect(() => {
|
|
return () => {
|
|
if (pollIntervalRef.current) clearInterval(pollIntervalRef.current);
|
|
};
|
|
}, []);
|
|
|
|
/**
|
|
* ✅ 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'
|
|
});
|
|
};
|
|
|
|
const startCryptoPolling = (commandId: number) => {
|
|
setCryptoPolling(true);
|
|
pollIntervalRef.current = setInterval(async () => {
|
|
const status = await getCryptoPaymentStatus(commandId);
|
|
if (!status) return;
|
|
setCryptoPaymentData(status);
|
|
if (status.payment_status === 'finished' || status.payment_status === 'confirmed') {
|
|
stopCryptoPolling();
|
|
const username = extractUsernameFromToken();
|
|
if (username) {
|
|
await clearCart(username);
|
|
await clearCartContext();
|
|
}
|
|
} else if (status.payment_status === 'failed' || status.payment_status === 'expired') {
|
|
stopCryptoPolling();
|
|
}
|
|
}, 10000);
|
|
};
|
|
|
|
const stopCryptoPolling = () => {
|
|
setCryptoPolling(false);
|
|
if (pollIntervalRef.current) {
|
|
clearInterval(pollIntervalRef.current);
|
|
pollIntervalRef.current = null;
|
|
}
|
|
};
|
|
|
|
const handleCryptoModalClose = () => {
|
|
stopCryptoPolling();
|
|
setShowCryptoModal(false);
|
|
navigate('/user/suivi-livraison');
|
|
};
|
|
|
|
/**
|
|
* ✅ 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: paymentMethod === 'crypto' ? 'crypto' : 'especes',
|
|
pay_currency: paymentMethod === 'crypto' ? payCurrency : undefined,
|
|
use_referral_balance: useReferral && referralBalance > 0,
|
|
};
|
|
|
|
console.log('📤 Envoi checkout avec JWT username:', checkoutData);
|
|
|
|
const response = await createCheckout(checkoutData);
|
|
console.log('📥 Réponse checkout:', response);
|
|
|
|
// Paiement crypto : afficher le modal avec l'adresse wallet
|
|
if (response.success && response.payment_method === 'crypto') {
|
|
setCryptoPaymentData({
|
|
command_id: response.command_id!,
|
|
client_order_number: (response as Record<string, unknown>).client_order_number as number | undefined,
|
|
payment_status: response.payment_status!,
|
|
pay_address: response.pay_address!,
|
|
pay_amount: response.pay_amount!,
|
|
pay_currency: response.pay_currency!,
|
|
price_amount: response.price_amount!,
|
|
price_currency: response.price_currency!,
|
|
});
|
|
setShowCryptoModal(true);
|
|
startCryptoPolling(response.command_id);
|
|
return;
|
|
}
|
|
|
|
if (!response.success && response.zone_error) {
|
|
setZoneErrorMsg(response.message);
|
|
setShowZoneModal(true);
|
|
return;
|
|
}
|
|
|
|
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,
|
|
client_order_number: (response as Record<string, unknown>).client_order_number as number | undefined,
|
|
assigned_to,
|
|
queue_info,
|
|
delivery_address: delivery_address || address,
|
|
arrivalTime,
|
|
total: frontendTotal,
|
|
referral_used: (response as Record<string, unknown>).referral_used as number | undefined,
|
|
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: unknown) {
|
|
console.error('❌ Erreur checkout:', err);
|
|
setError(err instanceof Error ? 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">
|
|
{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">
|
|
{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
|
|
/>
|
|
{localStorage.getItem('profile_default_address') && (
|
|
<p className="form-prefill-hint"><i className="fas fa-map-marker-alt" /> Pré-rempli depuis votre profil — modifiez si vous êtes ailleurs</p>
|
|
)}
|
|
</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
|
|
/>
|
|
{localStorage.getItem('profile_default_phone') && (
|
|
<p className="form-prefill-hint"><i className="fas fa-phone" /> Pré-rempli depuis votre profil</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Méthode de paiement */}
|
|
{cryptoEnabled && (
|
|
<div className="form-section">
|
|
<h3>Méthode de paiement</h3>
|
|
|
|
{cryptoOnly ? (
|
|
<div className="crypto-only-badge">
|
|
<i className="fas fa-coins" /> Paiement uniquement en cryptomonnaie
|
|
</div>
|
|
) : (
|
|
<div className="payment-method-selector">
|
|
<button
|
|
type="button"
|
|
className={`payment-method-btn ${paymentMethod === 'especes' ? 'active' : ''}`}
|
|
onClick={() => setPaymentMethod('especes')}
|
|
disabled={loading}
|
|
>
|
|
<i className="fas fa-money-bill-wave" /> Espèces
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`payment-method-btn ${paymentMethod === 'crypto' ? 'active' : ''}`}
|
|
onClick={() => setPaymentMethod('crypto')}
|
|
disabled={loading}
|
|
>
|
|
<i className="fas fa-coins" /> Crypto
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{(paymentMethod === 'crypto') && (
|
|
<div className="crypto-currency-selector">
|
|
<label>Cryptomonnaie</label>
|
|
<div className="crypto-currency-grid">
|
|
{cryptoCurrencies.map((currency) => (
|
|
<button
|
|
key={currency}
|
|
type="button"
|
|
className={`crypto-currency-btn ${payCurrency === currency ? 'active' : ''}`}
|
|
onClick={() => setPayCurrency(currency)}
|
|
disabled={loading}
|
|
>
|
|
{currency.toUpperCase()}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<p className="crypto-info-hint">
|
|
<i className="fas fa-info-circle" /> Vous recevrez l'adresse de paiement après validation
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Toggle parrainage */}
|
|
{referralEnabled && referralBalance > 0 && (
|
|
<div className="referral-toggle-box">
|
|
<div className="referral-toggle-info">
|
|
<span className="referral-toggle-icon"><i className="fas fa-gift"></i></span>
|
|
<div>
|
|
<p className="referral-toggle-label">Solde parrainage</p>
|
|
<p className="referral-toggle-balance">{referralBalance.toFixed(2)} € disponible</p>
|
|
</div>
|
|
</div>
|
|
<label className="referral-switch">
|
|
<input
|
|
type="checkbox"
|
|
checked={useReferral}
|
|
onChange={(e) => setUseReferral(e.target.checked)}
|
|
disabled={loading}
|
|
/>
|
|
<span className="referral-switch-slider" />
|
|
</label>
|
|
</div>
|
|
)}
|
|
|
|
<div className="checkout-telegram-note">
|
|
<i className="fab fa-telegram" />
|
|
<div>
|
|
<strong>Compte Telegram requis</strong>
|
|
<p>
|
|
Votre commande ne pourra être validée que si votre compte Telegram est lié.
|
|
Rendez-vous dans votre <a href="/user/profil">profil</a> pour le lier avant de confirmer.
|
|
</p>
|
|
</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 PAIEMENT CRYPTO */}
|
|
{/* ============================================ */}
|
|
{showCryptoModal && cryptoPaymentData && (
|
|
<div className="confirmation-modal-overlay">
|
|
<div className="confirmation-modal">
|
|
<div className="confirmation-modal-header confirmation-modal-header--crypto">
|
|
<i className="fas fa-coins confirmation-icon" />
|
|
<h2>Paiement Crypto</h2>
|
|
</div>
|
|
<div className="confirmation-modal-body">
|
|
<div className="confirmation-section">
|
|
<div className="confirmation-section-title">
|
|
<i className="fas fa-receipt icon" /> Commande #{cryptoPaymentData.client_order_number ?? cryptoPaymentData.command_id}
|
|
</div>
|
|
<div className="confirmation-detail">
|
|
<strong>Statut :</strong>{' '}
|
|
<span className={`crypto-status crypto-status--${cryptoPaymentData.payment_status}`}>
|
|
{cryptoPaymentData.payment_status === 'waiting' && '⏳ En attente de paiement'}
|
|
{cryptoPaymentData.payment_status === 'confirming' && '🔄 Confirmation en cours...'}
|
|
{cryptoPaymentData.payment_status === 'confirmed' && '✅ Confirmé'}
|
|
{cryptoPaymentData.payment_status === 'finished' && '✅ Paiement reçu !'}
|
|
{cryptoPaymentData.payment_status === 'failed' && '❌ Paiement échoué'}
|
|
{cryptoPaymentData.payment_status === 'expired' && '⌛ Expiré'}
|
|
{!['waiting','confirming','confirmed','finished','failed','expired'].includes(cryptoPaymentData.payment_status) && cryptoPaymentData.payment_status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="confirmation-section">
|
|
<div className="confirmation-section-title">
|
|
<i className="fas fa-wallet icon" /> Adresse de paiement
|
|
</div>
|
|
<div className="crypto-address-box">
|
|
<code className="crypto-address">{cryptoPaymentData.pay_address}</code>
|
|
<button
|
|
type="button"
|
|
className="crypto-copy-btn"
|
|
onClick={() => navigator.clipboard.writeText(cryptoPaymentData.pay_address)}
|
|
>
|
|
<i className="fas fa-copy" /> Copier
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="confirmation-section">
|
|
<div className="confirmation-section-title">
|
|
<i className="fas fa-coins icon" /> Montant à envoyer
|
|
</div>
|
|
<div className="confirmation-detail">
|
|
<strong>{cryptoPaymentData.pay_amount} {cryptoPaymentData.pay_currency.toUpperCase()}</strong>
|
|
<span className="crypto-equiv"> ≈ {cryptoPaymentData.price_amount.toFixed(2)} {cryptoPaymentData.price_currency.toUpperCase()}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{cryptoPolling && (
|
|
<div className="crypto-polling-info">
|
|
<i className="fas fa-spinner fa-spin" /> Vérification automatique toutes les 10 secondes...
|
|
</div>
|
|
)}
|
|
|
|
{(cryptoPaymentData.payment_status === 'finished' || cryptoPaymentData.payment_status === 'confirmed') && (
|
|
<div className="crypto-success-msg">
|
|
<i className="fas fa-check-circle" /> Paiement confirmé ! Votre commande est en cours de traitement.
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="confirmation-modal-actions">
|
|
<button className="confirmation-button confirmation-button--neutral" onClick={handleCryptoModalClose}>
|
|
<i className="fas fa-location-arrow" /> Suivre ma commande
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ============================================ */}
|
|
{/* MODAL ZONE NON DESSERVIE */}
|
|
{/* ============================================ */}
|
|
{showZoneModal && (
|
|
<div className="confirmation-modal-overlay" onClick={() => setShowZoneModal(false)}>
|
|
<div className="confirmation-modal" onClick={(e) => e.stopPropagation()}>
|
|
<div className="confirmation-modal-header confirmation-modal-header--error">
|
|
<i className="fas fa-map-marker-alt confirmation-icon"></i>
|
|
<h2>Zone non desservie</h2>
|
|
</div>
|
|
<div className="confirmation-modal-body">
|
|
<div className="confirmation-section">
|
|
<div className="confirmation-detail confirmation-detail--centered">
|
|
<p style={{ marginBottom: '0.75rem' }}>{zoneErrorMsg}</p>
|
|
<p className="confirmation-detail--muted">
|
|
Vérifiez l'adresse saisie ou contactez-nous pour connaître les zones de livraison disponibles.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="confirmation-modal-actions">
|
|
<button className="confirmation-button confirmation-button--neutral" onClick={() => setShowZoneModal(false)}>
|
|
<i className="fas fa-arrow-left"></i> Modifier l'adresse
|
|
</button>
|
|
</div>
|
|
</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.client_order_number ?? 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>
|
|
|
|
{/* Parrainage utilisé */}
|
|
{confirmationData.referral_used != null && confirmationData.referral_used > 0 && (
|
|
<div className="confirmation-section">
|
|
<div className="confirmation-section-title">
|
|
<i className="fas fa-gift icon"></i>
|
|
Parrainage appliqué
|
|
</div>
|
|
<div className="confirmation-detail">
|
|
<strong>Crédit utilisé:</strong> -{confirmationData.referral_used.toFixed(2)} €
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Suivi */}
|
|
<div className="confirmation-section">
|
|
<div className="confirmation-section-title">
|
|
<i className="fas fa-map-marker-alt icon"></i>
|
|
Suivi de livraison
|
|
</div>
|
|
<div className="confirmation-detail">
|
|
Suivez votre livraison en temps réel depuis la page Suivi
|
|
</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; |