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
@@ -0,0 +1,960 @@
// ============================================
// pages/SuiviLivraison.tsx - VERSION FINALE CORRIGÉE
// ============================================
// ✅ Calcul total identique à Checkout (somme des prix)
// ✅ FIX: Gestion correcte du code 409 Conflict
// ✅ FIX: Suppression des useState non utilisés
// ✅ AJOUT: Vérification continue de l'authentification
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
getMyOrders,
getOrderTracking,
getOrderETA,
confirmReception,
cancelCommand,
isUserAuthenticated
} from '../../api/api';
import type {
ETAResponse,
TrackingResponse,
OrderDetail,
CancelCommandResponse
} from '../../api/api_types';
import Navbar from '../../components/Navbar';
import Toast from '../../components/Toast';
import './SuiviLivraison.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faHourglassHalf,
faTruck,
faBox,
faCheckCircle,
faTimesCircle,
faQuestionCircle,
faMapMarkerAlt,
faBiking,
faClock,
faShoppingCart,
faMoneyBillWave,
faCalendarAlt,
faSync,
faGift,
faLightbulb,
faCheck,
faExclamationTriangle,
faLeaf,
faWind
} from '@fortawesome/free-solid-svg-icons';
interface OrderWithTracking extends OrderDetail {
tracking?: TrackingResponse;
eta?: ETAResponse;
}
interface ToastMessage {
id: string;
message: string;
type: 'success' | 'error' | 'warning' | 'info';
}
// ============================================
// ✅ HELPERS - Calcul total identique à Checkout
// ============================================
/**
* ✅ Calculer le total EXACTEMENT comme dans Checkout.tsx
* Somme des prix individuels (pas de multiplication)
*/
const getTotalAmount = (order: OrderWithTracking): number => {
// 1. Priorité: champ total stocké en DB
if (typeof order.total === 'number' && order.total > 0) {
return order.total;
}
// 2. Fallback: total_prix
if (typeof order.total_prix === 'number' && order.total_prix > 0) {
return order.total_prix;
}
// 3. Calcul depuis items (comme dans Checkout: somme des prix)
if (order.items && order.items.length > 0) {
const calculatedTotal = order.items.reduce((sum, item) => {
const itemPrice = item.prix || item.price || 0;
return sum + itemPrice; // ✅ Somme simple (pas de × quantity)
}, 0);
console.log(`💰 [getTotalAmount] Commande ${order.id}: ${calculatedTotal.toFixed(2)}`);
return calculatedTotal;
}
return 0;
};
const getClientInfo = (order: OrderWithTracking) => {
const firstName = order.first_name || order.client_prenom || '';
const lastName = order.last_name || order.client_nom || '';
const phone = order.phone || order.client_telephone || '';
return { firstName, lastName, phone };
};
const getDeliveryAddress = (order: OrderWithTracking): string => {
return order.delivery_address || order.adresse || 'Non disponible';
};
const formatOrderItem = (item: any) => {
return {
name: item.produit || item.product_name || item.name_product || 'Produit',
quantity: item.quantite || item.quantity || 0, // Grammes
price: item.prix || item.price || 0
};
};
const getStatusColor = (status: string): string => {
switch (status?.toLowerCase()) {
case 'pending':
return 'linear-gradient(135deg, #ddd6fe 0%, #a78bfa 50%, #7c3aed 100%)';
case 'assigned':
return 'linear-gradient(135deg, #fef3c7 0%, #fbbf24 50%, #f59e0b 100%)';
case 'support':
return 'linear-gradient(135deg, #e9d5ff 0%, #c084fc 50%, #9333ea 100%)';
case 'en_route':
return 'linear-gradient(135deg, #bfdbfe 0%, #60a5fa 50%, #3b82f6 100%)';
case 'arrived':
return 'linear-gradient(135deg, #bbf7d0 0%, #4ade80 50%, #22c55e 100%)';
case 'livre':
return 'linear-gradient(135deg, #f3e8ff 0%, #d8b4fe 50%, #a855f7 100%)';
case 'approved':
return 'linear-gradient(135deg, #c7d2fe 0%, #a5b4fc 50%, #6366f1 100%)';
case 'cancelled':
return 'linear-gradient(135deg, #fae8ff 0%, #f0abfc 50%, #c026d3 100%)';
default:
return 'linear-gradient(135deg, #e5e7eb 0%, #9ca3af 50%, #6b7280 100%)';
}
};
const getStatusLabel = (status: string): string => {
const statusMap: Record<string, string> = {
'pending': 'En attente d\'assignation',
'assigned': 'Livreur assigné',
'support': 'Pris en charge par le livreur',
'en_route': 'En route vers vous',
'arrived': 'Livreur arrivé',
'livre': 'Livré - À confirmer',
'approved': 'Livraison confirmée',
'cancelled': 'Annulée'
};
return statusMap[status?.toLowerCase()] || 'Statut inconnu';
};
const getStatusIcon = (status: string): any => {
const iconMap: Record<string, any> = {
'pending': faHourglassHalf,
'assigned': faBiking,
'support': faTruck,
'en_route': faTruck,
'arrived': faMapMarkerAlt,
'livre': faBox,
'approved': faCheckCircle,
'cancelled': faTimesCircle
};
return iconMap[status?.toLowerCase()] || faQuestionCircle;
};
const getStatusProgress = (status: string): number => {
const progressMap: Record<string, number> = {
'pending': 0,
'assigned': 10,
'support': 25,
'en_route': 50,
'arrived': 80,
'livre': 90,
'approved': 100,
'cancelled': 0
};
return progressMap[status?.toLowerCase()] || 0;
};
/**
* ✅ Calculer les points avec le TOTAL (pas item par item)
*/
const calculateOrderPoints = (order: OrderWithTracking): {
points: number;
category: string;
categoryDisplay: string;
categoryIcon: any;
categoryColor: string;
} => {
let zipetteTotal = 0;
let weedTotal = 0;
let grosSemiTotal = 0;
// ✅ Calculer les totaux par catégorie
if (order.items && order.items.length > 0) {
order.items.forEach((item: any) => {
const category = (item.category || '').toLowerCase();
const itemPrice = item.prix || item.price || 0;
if (category.includes('zipette')) {
zipetteTotal += itemPrice;
} else if (category.includes('gros') || category.includes('semi')) {
grosSemiTotal += itemPrice;
} else {
weedTotal += itemPrice;
}
});
}
console.log(`💰 [CALC_POINTS] Cmd ${order.id} - Zipette: ${zipetteTotal.toFixed(2)}€, Weed: ${weedTotal.toFixed(2)}€, GrosSemi: ${grosSemiTotal.toFixed(2)}`);
let points = 0;
let category = '';
let categoryDisplay = '';
let categoryIcon = faGift;
let categoryColor = '#7c3aed';
// ✅ Gros&Semi = 0 points
if (grosSemiTotal > 0 && zipetteTotal === 0 && weedTotal === 0) {
return {
points: 0,
category: 'gros&semi',
categoryDisplay: 'Gros&Semi',
categoryIcon: faBox,
categoryColor: '#9ca3af'
};
}
// ✅ Zipette > Weed → Barème Zipette
if (zipetteTotal > weedTotal) {
category = 'zipette&co';
categoryDisplay = 'Zipette&Co';
categoryIcon = faWind;
categoryColor = '#3b82f6';
if (zipetteTotal >= 30 && zipetteTotal <= 100) {
points = 1;
} else if (zipetteTotal >= 110 && zipetteTotal <= 200) {
points = 2;
} else if (zipetteTotal >= 210) {
points = 3;
}
// ✅ Weed > Zipette → Barème Weed
} else if (weedTotal > 0) {
category = 'weed&hash';
categoryDisplay = 'Weed&Hash';
categoryIcon = faLeaf;
categoryColor = '#10b981';
if (weedTotal >= 30 && weedTotal <= 50) {
points = 1;
} else if (weedTotal >= 60 && weedTotal <= 150) {
points = 2;
} else if (weedTotal >= 160 && weedTotal <= 300) {
points = 3;
} else if (weedTotal >= 310 && weedTotal <= 400) {
points = 5;
} else if (weedTotal >= 400) {
points = 10;
}
}
console.log(`🎁 [CALC_POINTS] Cmd ${order.id} - ${category} (${zipetteTotal + weedTotal}€) → ${points} points`);
return { points, category, categoryDisplay, categoryIcon, categoryColor };
};
// ============================================
// COMPONENT PRINCIPAL
// ============================================
function SuiviLivraison() {
const navigate = useNavigate();
const [orders, setOrders] = useState<OrderWithTracking[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [expandedOrder, setExpandedOrder] = useState<number | null>(null);
const [confirming, setConfirming] = useState<number | null>(null);
const [toasts, setToasts] = useState<ToastMessage[]>([]);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [orderToConfirm, setOrderToConfirm] = useState<number | null>(null);
const [selectedOrderPoints, setSelectedOrderPoints] = useState<number>(0);
const [selectedOrderCategoryDisplay, setSelectedOrderCategoryDisplay] = useState<string>('');
const [cancellingOrder, setCancellingOrder] = useState<number | null>(null);
const [showCancelDialog, setShowCancelDialog] = useState(false);
const [orderToCancel, setOrderToCancel] = useState<number | null>(null);
const [cancelReason, setCancelReason] = useState('');
const [showPenaltyWarning, setShowPenaltyWarning] = useState(false);
const [penaltyWarningData, setPenaltyWarningData] = useState<CancelCommandResponse | null>(null);
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
useEffect(() => {
const checkAuth = () => {
if (!isUserAuthenticated()) {
console.log('❌ [SuiviLivraison] 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('❌ [SuiviLivraison] Session expirée, redirection vers /login/client');
navigate('/login/client', { replace: true });
}
}, 5000);
return () => clearInterval(authInterval);
}, [navigate]);
useEffect(() => {
loadOrders();
const interval = setInterval(loadOrders, 10000);
return () => clearInterval(interval);
}, []);
const loadOrders = async () => {
// ✅ Vérifier l'auth avant de charger les commandes
if (!isUserAuthenticated()) {
console.log('❌ [loadOrders] Non authentifié');
navigate('/login/client', { replace: true });
return;
}
try {
setLoading(true);
const response = await getMyOrders();
if (response.success && response.commands) {
const ordersWithTracking = await Promise.all(
response.commands.map(async (order: OrderDetail) => {
const normalizedOrder = {
...order,
total: getTotalAmount(order)
};
let tracking;
let eta;
try {
tracking = await getOrderTracking(order.id);
} catch (err) {
console.warn(`Tracking non disponible pour commande ${order.id}`);
tracking = undefined;
}
try {
eta = await getOrderETA(order.id);
} catch (err) {
console.warn(`ETA non disponible pour commande ${order.id}`);
eta = undefined;
}
return {
...normalizedOrder,
tracking,
eta
};
})
);
setOrders(ordersWithTracking);
setError('');
} else {
setError('Impossible de charger les commandes');
showToast('Impossible de charger les commandes', 'error');
}
} catch (err: any) {
console.error('Erreur loadOrders:', err);
setError(err.message || 'Erreur lors du chargement');
showToast(err.message || 'Erreur lors du chargement', 'error');
} finally {
setLoading(false);
}
};
const showToast = (message: string, type: 'success' | 'error' | 'warning' | 'info') => {
const id = Date.now().toString();
setToasts(prev => [...prev, { id, message, type }]);
};
const removeToast = (id: string) => {
setToasts(prev => prev.filter(toast => toast.id !== id));
};
const openConfirmDialog = (orderId: number) => {
// ✅ Vérifier l'auth avant d'ouvrir le dialog
if (!isUserAuthenticated()) {
console.log('❌ [openConfirmDialog] Non authentifié');
navigate('/login/client', { replace: true });
return;
}
const order = orders.find(o => o.id === orderId);
if (order) {
const { points, categoryDisplay } = calculateOrderPoints(order);
setSelectedOrderPoints(points);
setSelectedOrderCategoryDisplay(categoryDisplay);
} else {
setSelectedOrderPoints(0);
setSelectedOrderCategoryDisplay('');
}
setOrderToConfirm(orderId);
setShowConfirmDialog(true);
};
const closeConfirmDialog = () => {
setShowConfirmDialog(false);
setOrderToConfirm(null);
setSelectedOrderPoints(0);
setSelectedOrderCategoryDisplay('');
};
const handleConfirmReception = async () => {
if (!orderToConfirm || confirming) return;
// ✅ Vérifier l'auth avant de confirmer
if (!isUserAuthenticated()) {
console.log('❌ [handleConfirmReception] Non authentifié');
navigate('/login/client', { replace: true });
return;
}
try {
setConfirming(orderToConfirm);
closeConfirmDialog();
showToast('Confirmation en cours...', 'info');
const response = await confirmReception(orderToConfirm);
if (response.success) {
const pointsEarned = response.points_earned || selectedOrderPoints;
const responseData = (response as any).data || {};
const apiCategory = responseData.category || '';
let displayCategory = selectedOrderCategoryDisplay;
if (apiCategory.toLowerCase().includes('zipette')) {
displayCategory = '💨 Zipette&Co';
} else if (apiCategory.toLowerCase().includes('weed') || apiCategory.toLowerCase().includes('hash')) {
displayCategory = '🌿 Weeds&Hash';
}
showToast(
`Commande confirmée! +${pointsEarned} point${pointsEarned > 1 ? 's' : ''} ${displayCategory}`,
'success'
);
setConfirming(null);
loadOrders();
} else {
setError(response.message || 'Erreur lors de la confirmation');
showToast(response.message || 'Erreur lors de la confirmation', 'error');
setConfirming(null);
}
} catch (err: any) {
setError(err.message || 'Erreur serveur');
showToast(err.message || 'Erreur serveur', 'error');
setConfirming(null);
}
};
const openCancelDialog = (orderId: number) => {
// ✅ Vérifier l'auth avant d'ouvrir le dialog
if (!isUserAuthenticated()) {
console.log('❌ [openCancelDialog] Non authentifié');
navigate('/login/client', { replace: true });
return;
}
setOrderToCancel(orderId);
setCancelReason('');
setShowPenaltyWarning(false);
setPenaltyWarningData(null);
setShowCancelDialog(true);
};
const closeCancelDialog = () => {
setShowCancelDialog(false);
setOrderToCancel(null);
setCancelReason('');
setShowPenaltyWarning(false);
setPenaltyWarningData(null);
};
const handleCancelOrder = async (force: boolean = false) => {
if (!orderToCancel) return;
// ✅ Vérifier l'auth avant d'annuler
if (!isUserAuthenticated()) {
console.log('❌ [handleCancelOrder] Non authentifié');
navigate('/login/client', { replace: true });
return;
}
try {
setCancellingOrder(orderToCancel);
if (!force) {
showToast('Vérification en cours...', 'info');
}
const response = await cancelCommand(orderToCancel, cancelReason, force);
if (response.warning && response.penalty_warning && !force) {
console.log('⚠️ [CANCEL] Avertissement reçu:', response.penalty_warning);
setPenaltyWarningData(response);
setShowPenaltyWarning(true);
setCancellingOrder(null);
return;
}
if (response.success) {
let message = 'Commande annulée avec succès';
if (response.penalty) {
message += ` (Pénalité: ${response.penalty.points} points)`;
showToast(message, 'warning');
} else if (response.info) {
showToast(message + ' - ' + response.info, 'success');
} else {
showToast(message, 'success');
}
closeCancelDialog();
loadOrders();
} else {
showToast(response.message || 'Erreur lors de l\'annulation', 'error');
}
} catch (error: any) {
console.error('❌ [CANCEL] Erreur:', error);
showToast(error.message || 'Erreur lors de l\'annulation', 'error');
} finally {
setCancellingOrder(null);
}
};
const confirmCancelWithPenalty = () => {
setShowPenaltyWarning(false);
handleCancelOrder(true);
};
if (loading && orders.length === 0) {
return (
<>
<Navbar />
<div className="suivi-container">
<div className="loading-state">
<div className="spinner"></div>
<p>Chargement de vos commandes...</p>
</div>
</div>
</>
);
}
return (
<>
<Navbar />
<div className="suivi-container">
<div className="suivi-header">
<h1>Suivi de vos commandes</h1>
</div>
{error && (
<div className="error-banner">
<FontAwesomeIcon icon={faExclamationTriangle} /> {error}
<button onClick={() => setError('')}></button>
</div>
)}
{orders.length === 0 ? (
<div className="empty-state">
<h2>Aucune commande trouvée</h2>
<p>Vous n'avez pas encore passé de commande.</p>
<button
className="action-button"
onClick={() => navigate('/user/nos-produits')}
>
Découvrir nos produits
</button>
</div>
) : (
<div className="orders-list">
{orders.map((order) => (
<div key={order.id} className="order-card">
<div
className="order-header"
onClick={() => setExpandedOrder(expandedOrder === order.id ? null : order.id)}
role="button"
tabIndex={0}
>
<div className="order-header-left">
<h3>Commande #{order.id}</h3>
<div
className="status-badge"
style={{ backgroundImage: getStatusColor(order.status) }}
>
<FontAwesomeIcon icon={getStatusIcon(order.status)} /> {getStatusLabel(order.status)}
</div>
</div>
<div className="order-header-right">
<span className="order-total2">
{getTotalAmount(order).toFixed(2)} €
</span>
<span className="expand-icon">
{expandedOrder === order.id ? '' : ''}
</span>
</div>
</div>
{expandedOrder === order.id && (
<div className="order-details">
<div className="status-progress">
<div
className="status-progress-bar"
style={{
width: `${getStatusProgress(order.status)}%`,
backgroundImage: getStatusColor(order.status)
}}
/>
</div>
<div className="detail-section">
<h4><FontAwesomeIcon icon={faMapMarkerAlt} /> Adresse de livraison</h4>
<p className="address">
{getDeliveryAddress(order)}
</p>
{(() => {
const clientInfo = getClientInfo(order);
if (clientInfo.firstName || clientInfo.lastName) {
return (
<p className="contact">
{clientInfo.firstName} {clientInfo.lastName}
{clientInfo.phone && ` • ${clientInfo.phone}`}
</p>
);
}
return null;
})()}
</div>
{order.livreur_assign && (
<div className="detail-section">
<h4><FontAwesomeIcon icon={faBiking} /> Livreur assigné</h4>
<p>{order.livreur_assign}</p>
</div>
)}
{order.eta && (
<div className="detail-section">
<h4><FontAwesomeIcon icon={faClock} /> Heure estimée d'arrivée</h4>
<p>{order.eta.estimated_arrival || `${order.eta.eta_minutes} minutes`}</p>
{order.eta.updated_at && (
<small>Mise à jour: {new Date(order.eta.updated_at * 1000).toLocaleTimeString()}</small>
)}
</div>
)}
{order.items && order.items.length > 0 && (
<div className="detail-section">
<h4><FontAwesomeIcon icon={faShoppingCart} /> Produits</h4>
<div className="items-list">
{order.items.map((item: any, idx: number) => {
const formatted = formatOrderItem(item);
return (
<div key={idx} className="item">
<span className="item-name">
{formatted.name} ({formatted.quantity}g)
</span>
<span className="price">
{formatted.price.toFixed(2)}
</span>
</div>
);
})}
</div>
</div>
)}
<div className="detail-section">
<h4><FontAwesomeIcon icon={faMoneyBillWave} /> Montant total</h4>
<p className="total-amount">
<strong>{getTotalAmount(order).toFixed(2)} </strong>
</p>
</div>
<div className="detail-section">
<h4><FontAwesomeIcon icon={faCalendarAlt} /> Date de commande</h4>
<p>{new Date(order.created_at).toLocaleDateString('fr-FR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}</p>
</div>
<div className="order-actions">
{order.status?.toLowerCase() === 'livre' ? (
<div className="confirmation-section">
<div className="delivery-notice">
<p className="notice-title">
<FontAwesomeIcon icon={faBox} /> Votre colis a é livré
</p>
<p className="notice-subtitle">
Confirmez la réception pour valider la livraison et gagner des points
</p>
</div>
<button
className="btn-confirm-delivery"
onClick={() => openConfirmDialog(order.id)}
disabled={confirming === order.id}
>
{confirming === order.id ? (
<>
<FontAwesomeIcon icon={faClock} spin /> Confirmation...
</>
) : (
<>
<FontAwesomeIcon icon={faCheck} /> J'ai bien reçu ma commande
</>
)}
</button>
</div>
) : order.status?.toLowerCase() === 'approved' ? (
<div className="status-message success">
<FontAwesomeIcon icon={faCheckCircle} /> Commande validée et confirmée
</div>
) : order.status?.toLowerCase() === 'cancelled' ? (
<div className="status-message cancelled">
<FontAwesomeIcon icon={faTimesCircle} /> Cette commande a été annulée
</div>
) : (
<div className="action-buttons">
<button
className="btn-cancel-order"
onClick={() => openCancelDialog(order.id)}
disabled={cancellingOrder === order.id}
>
{cancellingOrder === order.id ? (
<>
<FontAwesomeIcon icon={faClock} spin /> Annulation...
</>
) : (
<>
<FontAwesomeIcon icon={faTimesCircle} /> Annuler la commande
</>
)}
</button>
<button
className="btn-secondary"
onClick={() => {
loadOrders();
showToast('Actualisation en cours...', 'info');
}}
>
<FontAwesomeIcon icon={faSync} /> Actualiser
</button>
</div>
)}
</div>
</div>
)}
</div>
))}
</div>
)}
<div className="footer-info">
<p><FontAwesomeIcon icon={faLightbulb} /> Les informations se mettent à jour automatiquement chaque 10 secondes</p>
<p><FontAwesomeIcon icon={faGift} /> Points de fidélité selon votre achat : 💨 Zipette&Co (1-3 pts) • 🌿 Weed&Hash (1-10 pts)</p>
</div>
</div>
{/* Dialog de confirmation */}
{showConfirmDialog && (
<div className="confirm-dialog-overlay" onClick={closeConfirmDialog}>
<div className="confirm-dialog" onClick={(e) => e.stopPropagation()}>
<div className="confirm-dialog-header">
<h3><FontAwesomeIcon icon={faCheckCircle} /> Confirmer la réception</h3>
</div>
<div className="confirm-dialog-body">
<p>Confirmez-vous avoir bien reçu votre commande ?</p>
<div className="confirm-dialog-reward">
<FontAwesomeIcon icon={faGift} />
<strong>
{selectedOrderPoints > 0
? `+${selectedOrderPoints} point${selectedOrderPoints > 1 ? 's' : ''} de fidélité`
: 'Commande éligible aux points de fidélité'
}
</strong>
</div>
{selectedOrderPoints > 0 && (
<div className="points-info">
<p>Accumulez des points pour obtenir des récompenses !</p>
</div>
)}
</div>
<div className="confirm-dialog-actions">
<button className="btn-secondary" onClick={closeConfirmDialog}>
Retour
</button>
<button className="btn-confirm" onClick={handleConfirmReception}>
<FontAwesomeIcon icon={faCheck} /> Confirmer
</button>
</div>
</div>
</div>
)}
{/* Dialog d'annulation */}
{showCancelDialog && (
<div className="confirm-dialog-overlay" onClick={closeCancelDialog}>
<div className="confirm-dialog cancel-dialog" onClick={(e) => e.stopPropagation()}>
<div className="confirm-dialog-header">
<h3>
<FontAwesomeIcon icon={faTimesCircle} />
{showPenaltyWarning ? ' Confirmation requise' : ' Annuler la commande'}
</h3>
</div>
<div className="confirm-dialog-body">
{showPenaltyWarning && penaltyWarningData ? (
<div className="penalty-warning">
<div className="warning-icon">
<FontAwesomeIcon icon={faExclamationTriangle} />
</div>
<p className="warning-title">{penaltyWarningData.message}</p>
{penaltyWarningData.details && (
<div className="warning-details">
<p><strong>Livreur assigné:</strong> {penaltyWarningData.details.livreur}</p>
<p><strong>Statut:</strong> {getStatusLabel(penaltyWarningData.details.status || '')}</p>
{penaltyWarningData.details.position_in_queue && (
<p><strong>Position dans la queue:</strong> {penaltyWarningData.details.position_in_queue}</p>
)}
</div>
)}
{penaltyWarningData.penalty_warning && (
<div className="penalty-info">
{penaltyWarningData.penalty_warning.will_apply ? (
<>
<p className="penalty-amount">
<strong> Pénalité: {penaltyWarningData.penalty_warning.penalty_amount} points</strong>
</p>
<p className="penalty-message">{penaltyWarningData.penalty_warning.message}</p>
<div className="penalty-scale">
<p><strong>Barème des pénalités:</strong></p>
<ul>
<li>1ère annulation: {penaltyWarningData.penalty_warning.scale['1st_cancel']}</li>
<li>2ème annulation: {penaltyWarningData.penalty_warning.scale['2nd_cancel']}</li>
<li>3ème annulation: {penaltyWarningData.penalty_warning.scale['3rd_cancel']}</li>
<li>4ème+ annulation: {penaltyWarningData.penalty_warning.scale['4th+_cancel']}</li>
</ul>
</div>
</>
) : (
<div className="info-message">
<FontAwesomeIcon icon={faLightbulb} />
<span>{penaltyWarningData.penalty_warning.message}</span>
</div>
)}
</div>
)}
<p className="warning-question">
{penaltyWarningData.penalty_warning?.will_apply
? 'Voulez-vous vraiment annuler cette commande et accepter la pénalité ?'
: 'Voulez-vous vraiment annuler cette commande ?'
}
</p>
</div>
) : (
<div className="cancel-form">
<p>Êtes-vous sûr de vouloir annuler cette commande ?</p>
<div className="form-group">
<label htmlFor="cancel-reason">Raison de l'annulation (optionnelle)</label>
<textarea
id="cancel-reason"
value={cancelReason}
onChange={(e) => setCancelReason(e.target.value)}
placeholder="Expliquez pourquoi vous annulez cette commande..."
rows={4}
maxLength={500}
/>
<small>{cancelReason.length}/500 caractères</small>
</div>
<div className="info-message">
<FontAwesomeIcon icon={faLightbulb} />
<span>Si aucun livreur n'est assigné, l'annulation est gratuite. Sinon, une confirmation sera demandée.</span>
</div>
</div>
)}
</div>
<div className="confirm-dialog-actions">
<button
className="btn-secondary"
onClick={closeCancelDialog}
disabled={cancellingOrder !== null}
>
Retour
</button>
<button
className="btn-danger"
onClick={() => showPenaltyWarning ? confirmCancelWithPenalty() : handleCancelOrder(false)}
disabled={cancellingOrder !== null}
>
{showPenaltyWarning ? (
<>
<FontAwesomeIcon icon={faCheck} /> Confirmer l'annulation
</>
) : (
<>
<FontAwesomeIcon icon={faTimesCircle} /> Annuler la commande
</>
)}
</button>
</div>
</div>
</div>
)}
{/* Toasts */}
{toasts.map((toast) => (
<Toast
key={toast.id}
message={toast.message}
type={toast.type}
duration={3000}
onClose={() => removeToast(toast.id)}
/>
))}
</>
);
}
export default SuiviLivraison;