diff --git a/backend/gestion/db/db_delivery.go b/backend/gestion/db/db_delivery.go index 2409c80f..bcb1eade 100644 --- a/backend/gestion/db/db_delivery.go +++ b/backend/gestion/db/db_delivery.go @@ -104,14 +104,8 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e // ÉTAPE 3: Vérifier que la commande est assignable // ============================================ - // ✅ Vérifier si déjà assignée à un autre livreur - if currentLivreur.Valid && currentLivreur.String != "" && currentLivreur.String != livreurUsername { - log.Printf("❌ Commande déjà assignée à: %s", currentLivreur.String) - return fmt.Errorf("commande déjà assignée au livreur '%s'", currentLivreur.String) - } - - // ✅ Vérifier le statut - validStatusesForAssignment := []string{"pending"} + // ✅ Vérifier le statut (pending ou assigned pour permettre la réassignation) + validStatusesForAssignment := []string{"pending", "assigned"} isValidStatus := false for _, vs := range validStatusesForAssignment { if currentStatus == vs { @@ -135,8 +129,7 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e status = 'assigned', updated_at = CURRENT_TIMESTAMP WHERE id = $2 - AND status IN ('pending') - AND (livreur_assign IS NULL OR livreur_assign = '' OR livreur_assign = $1)` + AND status IN ('pending', 'assigned')` result, err := tx.Exec(updateQuery, livreurUsername, commandID) if err != nil { diff --git a/backend/gestion/db/db_notifications.go b/backend/gestion/db/db_notifications.go index 9665d988..758a1f37 100644 --- a/backend/gestion/db/db_notifications.go +++ b/backend/gestion/db/db_notifications.go @@ -172,6 +172,48 @@ func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryA log.Printf("📬 [ADMIN_NOTIF] Notif Redis + push (%d tokens) pour commande #%d", sent, commandID) } +// NotifyAllAdminCabineAlert envoie une notification Redis + push à tous les admins/cabines +// lors du déclenchement d'une alerte par un livreur. +func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alertMessage string) { + rows, err := d.Query( + `SELECT username, COALESCE(push_token, '') FROM users WHERE role IN ('admin','cabine')`, + ) + if err != nil { + log.Printf("❌ [ALERT_NOTIF] Erreur lecture users admin/cabine: %v", err) + return + } + defer rows.Close() + + title := "🚨 Alerte livreur" + body := fmt.Sprintf("%s — livreur : %s", alertMessage, livreurUsername) + + notification := map[string]interface{}{ + "alert_id": alertID, + "type": "alert", + "message": body, + "created_at": time.Now().Format(time.RFC3339), + "read": false, + } + notifJSON, _ := json.Marshal(notification) + + sent := 0 + for rows.Next() { + var username, token string + if err := rows.Scan(&username, &token); err != nil { + continue + } + notifKey := fmt.Sprintf("notifications:%s", username) + Redis.LPush(RedisCtx, notifKey, notifJSON) + Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) + + if token != "" { + go sendExpoPushWithChannel(token, title, body, alertID, "alert", "orders") + sent++ + } + } + log.Printf("🚨 [ALERT_NOTIF] Notif Redis + push (%d tokens) pour alerte #%d de %s", sent, alertID, livreurUsername) +} + // AddDeliveryRating ajoute une note pour un livreur func (d *Database) AddDeliveryRating(livreurUsername string, commandID, rating int, comment string) error { query := ` diff --git a/backend/gestion/db/db_settings.go b/backend/gestion/db/db_settings.go index ad92a4bd..4ab836d3 100644 --- a/backend/gestion/db/db_settings.go +++ b/backend/gestion/db/db_settings.go @@ -74,6 +74,7 @@ type AppSettings struct { PointsPools []PointsPool `json:"points_pools"` // types de points personnalisés ReferralEnabled bool `json:"referral_enabled"` // activer/désactiver le système de parrainage CryptoPaymentEnabled bool `json:"crypto_payment_enabled"` // activer/désactiver le paiement crypto + CryptoOnly bool `json:"crypto_only"` // forcer le paiement crypto uniquement (pas d'espèces) NowPaymentsAPIKey string `json:"nowpayments_api_key"` // clé API NowPayments NowPaymentsIPNSecret string `json:"nowpayments_ipn_secret"` // secret IPN NowPayments NowPaymentsCurrencies []string `json:"nowpayments_currencies"` // cryptos acceptées (ex: ["btc","eth","ltc"]) @@ -157,6 +158,8 @@ func (d *Database) GetSettings() (AppSettings, error) { settings.ReferralEnabled = value == "true" case "crypto_payment_enabled": settings.CryptoPaymentEnabled = value == "true" + case "crypto_only": + settings.CryptoOnly = value == "true" case "nowpayments_api_key": settings.NowPaymentsAPIKey = value case "nowpayments_ipn_secret": @@ -232,6 +235,7 @@ func (d *Database) UpdateSettings(s AppSettings) error { {"points_pools", string(poolsJSON)}, {"referral_enabled", boolStr(s.ReferralEnabled)}, {"crypto_payment_enabled", boolStr(s.CryptoPaymentEnabled)}, + {"crypto_only", boolStr(s.CryptoOnly)}, {"nowpayments_api_key", s.NowPaymentsAPIKey}, {"nowpayments_ipn_secret", s.NowPaymentsIPNSecret}, {"nowpayments_currencies", string(currenciesJSON)}, diff --git a/backend/gestion/handlers/alert.go b/backend/gestion/handlers/alert.go index b892e86f..ab9f46ee 100644 --- a/backend/gestion/handlers/alert.go +++ b/backend/gestion/handlers/alert.go @@ -36,6 +36,9 @@ func AlertPolice(c *gin.Context) { return } + // Notifier tous les admins/cabines en temps réel + go database.NotifyAllAdminCabineAlert(alert.ID, usernameStr, req.Message) + c.JSON(200, gin.H{ "success": true, "message": "Police alert created", diff --git a/backend/gestion/handlers/settings.go b/backend/gestion/handlers/settings.go index 3a9ee9e8..ffc68446 100644 --- a/backend/gestion/handlers/settings.go +++ b/backend/gestion/handlers/settings.go @@ -37,6 +37,7 @@ func GetPublicSettings(c *gin.Context) { "referral_enabled": settings.ReferralEnabled, "delivery_schedule": settings.DeliverySchedule, "crypto_payment_enabled": settings.CryptoPaymentEnabled, + "crypto_only": settings.CryptoOnly, "nowpayments_currencies": settings.NowPaymentsCurrencies, }) } diff --git a/frontend-admin/src/screens/delivery/AlertsScreen.tsx b/frontend-admin/src/screens/delivery/AlertsScreen.tsx index 1834bf06..7ed9a284 100644 --- a/frontend-admin/src/screens/delivery/AlertsScreen.tsx +++ b/frontend-admin/src/screens/delivery/AlertsScreen.tsx @@ -30,6 +30,19 @@ const ALERT_PHRASES = [ { label: "Guet-apens", icon: "warning-outline" as const }, ]; +const ALERT_CONFIG: Record = { + "Contrôle de police": { + title: "Alerte — Contrôle de police", + message: "Vous signalez un contrôle de police. Restez calme, soyez coopératif et ne résistez pas. L'administration sera immédiatement notifiée.", + successHint: "L'administration a été alertée. Restez calme, coopérez avec les forces de l'ordre et attendez les instructions.", + }, + "Guet-apens": { + title: "Alerte — Guet-apens", + message: "Vous signalez un guet-apens. Si possible, éloignez-vous de la zone immédiatement. L'administration sera immédiatement notifiée.", + successHint: "L'administration a été alertée. Éloignez-vous du danger si possible et attendez les instructions de l'équipe.", + }, +}; + export default function AlertsScreen() { const { colors } = useTheme(); const [alerts, setAlerts] = useState([]); @@ -412,17 +425,12 @@ export default function AlertsScreen() { - Alerte Police + + {ALERT_CONFIG[selectedPhrase]?.title ?? "Alerte"} + - Vous êtes sur le point de déclencher une alerte - police. Cette action notifiera immédiatement - l'administration. + {ALERT_CONFIG[selectedPhrase]?.message ?? "Vous êtes sur le point de déclencher une alerte. L'administration sera immédiatement notifiée."} - {selectedPhrase ? ( - - {selectedPhrase} - - ) : null} Confirmez-vous le déclenchement ? @@ -480,8 +488,7 @@ export default function AlertsScreen() { {successMessage} - L'administration a été notifiée. Vous pourrez - terminer l'alerte quand la situation sera résolue. + {ALERT_CONFIG[selectedPhrase]?.successHint ?? "L'administration a été notifiée. Vous pourrez terminer l'alerte quand la situation sera résolue."} } /> + } + /> } diff --git a/frontend-prep/src/api/api.ts b/frontend-prep/src/api/api.ts index c4bf5a57..ad71aa2a 100644 --- a/frontend-prep/src/api/api.ts +++ b/frontend-prep/src/api/api.ts @@ -4,15 +4,21 @@ // ✅ AuthResponse inclut access_token // ✅ loginUser et registerUser retournent AuthResponse // ✅ sessionStorage (pas localStorage) - -const API_URL = "http://5.181.0.112/api/v1"; -const BACKEND_URL = "http://5.181.0.112"; - +const API_URL = "/api/v1"; +const BACKEND_URL = ""; export function getMediaUrl(url: string): string { if (!url) return ""; if (url.startsWith("http")) return url; return `${BACKEND_URL}${url}`; } + +async function safeJson(response: Response) { + const ct = response.headers.get("content-type") || ""; + if (!response.ok && !ct.includes("application/json")) { + throw new Error(`HTTP ${response.status}`); + } + return response.json(); +} import type { ConfirmReceptionResponse, CheckoutCartResponse, @@ -184,7 +190,7 @@ export const loginUser = async ( if (!response.ok) { let errorMessage = "Erreur de connexion"; try { - const errorData = await response.json(); + const errorData = await safeJson(response); errorMessage = errorData.error || errorData.message || errorMessage; } catch { @@ -201,7 +207,7 @@ export const loginUser = async ( }; } - const data = await response.json(); + const data = await safeJson(response); console.log("📋 [LOGIN] Réponse:", data); // ✅ Vérifier access_token @@ -294,7 +300,7 @@ export const changePassword = async ( new_password: newPassword, }), }); - const data = await response.json(); + const data = await safeJson(response); if (!response.ok) { return { success: false, @@ -375,16 +381,15 @@ export const getCart = async (username: string): Promise => { }, }); - const responseData = await response.json(); - if (!response.ok) { return { success: false, - message: responseData.error || "Erreur récupération", + message: "Erreur récupération", panier: [], }; } + const responseData = await safeJson(response); return { success: true, panier: responseData.panier || [], @@ -445,7 +450,7 @@ export const addToCart = async (cartItem: { body: JSON.stringify(cartItem), }); - const data = await response.json(); + const data = await safeJson(response); if (!response.ok) { return { @@ -502,7 +507,7 @@ export const removeFromCart = async (id: number, username: string) => { }), }); - const data = await response.json(); + const data = await safeJson(response); if (!response.ok) { return { @@ -558,7 +563,7 @@ export const clearCart = async (username: string) => { // ✅ Gérer les erreurs HTTP avant de parser JSON if (!response.ok) { try { - const errorData = await response.json(); + const errorData = await safeJson(response); console.error("❌ [CLEAR] Erreur API:", errorData); return { success: false, @@ -575,7 +580,7 @@ export const clearCart = async (username: string) => { } // ✅ Parser JSON seulement si response.ok - const data = await response.json(); + const data = await safeJson(response); console.log("✅ [CLEAR] Panier vidé:", { stock_released: data.stock_released, @@ -620,7 +625,7 @@ export const getMyOrders = async () => { }, }); - const data = await response.json(); + const data = await safeJson(response); if (!response.ok) { return { @@ -651,6 +656,8 @@ export interface CheckoutData { last_name?: string; phone?: string; payment_method?: string; + pay_currency?: string; + use_referral_balance?: boolean; } export const createCheckout = async (checkoutData: CheckoutData) => { @@ -685,16 +692,24 @@ export const createCheckout = async (checkoutData: CheckoutData) => { }, body: JSON.stringify({ ...checkoutData, - use_referral_balance: checkoutData.use_referral_balance ?? false, + use_referral_balance: + checkoutData.use_referral_balance ?? false, }), }); - const data = await response.json(); + const data = await safeJson(response); if (!response.ok) { + const isZoneError = + !!data.postal_code || + (typeof data.error === "string" && + (data.error.includes("code postal") || + data.error.includes("hors zone"))); return { success: false, message: data.error || "Erreur création", + postal_code: data.postal_code as string | undefined, + zone_error: isZoneError as boolean, }; } @@ -706,6 +721,13 @@ export const createCheckout = async (checkoutData: CheckoutData) => { delivery_address: data.delivery_address, assigned_to: data.assigned_to, queue_info: data.queue_info, + payment_method: data.payment_method as string | undefined, + payment_status: data.payment_status as string | undefined, + pay_address: data.pay_address as string | undefined, + pay_amount: data.pay_amount as number | undefined, + pay_currency: data.pay_currency as string | undefined, + price_amount: data.price_amount as number | undefined, + price_currency: data.price_currency as string | undefined, }; } catch (error) { console.error("❌ [CHECKOUT] Erreur:", error); @@ -743,7 +765,7 @@ export const approveDelivery = async ( }, ); - const responseData = await response.json(); + const responseData = await safeJson(response); if (!response.ok) { return { @@ -797,7 +819,7 @@ export interface Category { export const getCategories = async (): Promise => { try { const response = await fetch(`${API_URL}/categories`); - const data = await response.json(); + const data = await safeJson(response); return data.categories || []; } catch { return []; @@ -807,7 +829,7 @@ export const getCategories = async (): Promise => { export const getAllProducts = async () => { try { const response = await fetch(`${API_URL}/products`); - return await response.json(); + return await safeJson(response); } catch (error) { console.error("❌ [PRODUCTS] Erreur:", error); return { success: false, data: [] }; @@ -820,11 +842,10 @@ export const getAllProducts = async () => { */ export const getProductsByCategory = async (category: string) => { try { - // ✅ CHANGÉ: De /products?category=X à /products/category/X const response = await fetch( `${API_URL}/products/category/${category}`, ); - return await response.json(); + return await safeJson(response); } catch (error) { console.error("❌ [PRODUCTS] Erreur:", error); return { success: false, data: [] }; @@ -837,7 +858,7 @@ export const getProductsByCategory = async (category: string) => { export const getProductById = async (id: number) => { try { const response = await fetch(`${API_URL}/products/${id}`); - return await response.json(); + return await safeJson(response); } catch (error) { console.error("❌ [PRODUCT] Erreur:", error); return { success: false, data: null }; @@ -874,15 +895,18 @@ export const getOrderTracking = async ( commandId, ); - const response = await fetch(`${API_URL}/commands/${commandId}/track`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, + const response = await fetch( + `${API_URL}/commands/${commandId}/tracking`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, }, - }); + ); - const data = await response.json(); + const data = await safeJson(response); if (!response.ok) { console.error("❌ [TRACKING] Erreur API:", data); @@ -965,7 +989,7 @@ export const getOrderETA = async (commandId: number): Promise => { }, }); - const data = await response.json(); + const data = await safeJson(response); if (!response.ok) { console.error("❌ [ETA] Erreur API:", data); @@ -1040,7 +1064,7 @@ export const getOrdersWithTracking = async () => { }, }); - const data = await response.json(); + const data = await safeJson(response); if (!response.ok) { console.error("❌ [ORDERS_TRACKING] Erreur API:", data); @@ -1112,7 +1136,7 @@ export const confirmReception = async ( }, ); - const responseData = await response.json(); + const responseData = await safeJson(response); if (!response.ok) { console.error("❌ [CONFIRM] Erreur API:", responseData); @@ -1165,7 +1189,7 @@ export const getOrderTotal = async (commandId: number): Promise => { }, }); - const data = await response.json(); + const data = await safeJson(response); if (!response.ok) { console.error("❌ [TOTAL] Erreur API:", data); @@ -1237,7 +1261,7 @@ export const getMyCompletedOrders = async (): Promise => { }, }); - const data = await response.json(); + const data = await safeJson(response); if (!response.ok) { console.error("❌ [HISTORY] Erreur API:", data); @@ -1366,7 +1390,7 @@ export const cancelCommand = async ( }, ); - const data = await response.json(); + const data = await safeJson(response); // ⚠️ AVERTISSEMENT (409 Conflict) - Livreur assigné if (response.status === 409 && data.warning) { @@ -1468,7 +1492,7 @@ export const getMyCancellationHistory = }, }); - const data = await response.json(); + const data = await safeJson(response); if (!response.ok) { console.error("❌ [CANCEL_HISTORY] Erreur API:", data); @@ -1522,7 +1546,7 @@ export const getOrderDetails = async (commandId: number) => { }, }); - const data = await response.json(); + const data = await safeJson(response); if (!response.ok) { console.error("❌ [ORDER DETAILS] Erreur API:", data); @@ -1587,7 +1611,7 @@ export const getCommandItemsWithDetails = async (commandId: number) => { }, }); - const data = await response.json(); + const data = await safeJson(response); if (!response.ok) { console.error("❌ [COMMAND ITEMS] Erreur API:", data); @@ -1638,7 +1662,7 @@ export const getMyPenalties = async (): Promise => { }, }); - const data = await response.json(); + const data = await safeJson(response); if (!response.ok) { console.error("❌ [PENALTIES] Erreur API:", data); @@ -1718,7 +1742,7 @@ export const checkoutCart = async ( }), }); - const data = await response.json(); + const data = await safeJson(response); if (!response.ok) { console.error("❌ [CHECKOUT] Erreur API:", data); @@ -1795,7 +1819,7 @@ export const getClientNotifications = unread_count: 0, total: 0, }; - const data = await response.json(); + const data = await safeJson(response); return { success: true, notifications: data.notifications || [], @@ -1834,43 +1858,146 @@ export interface PublicSettings { points_enabled: boolean; points_separated: boolean; referral_enabled: boolean; + pool_names: string[]; + crypto_payment_enabled: boolean; + crypto_only: boolean; + nowpayments_currencies: string[]; } export const getPublicSettings = async (): Promise => { - const defaults: PublicSettings = { penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: true }; + const defaults: PublicSettings = { + penalties_enabled: true, + show_amende_score: true, + points_enabled: true, + points_separated: true, + referral_enabled: true, + pool_names: ["Pool 1", "Pool 2"], + crypto_payment_enabled: false, + crypto_only: false, + nowpayments_currencies: [], + }; try { const response = await fetch(`${API_URL}/app-settings`); if (!response.ok) return defaults; - const data = await response.json(); + const data = await safeJson(response); return { penalties_enabled: data.penalties_enabled ?? true, show_amende_score: data.show_amende_score ?? true, points_enabled: data.points_enabled ?? true, points_separated: data.points_separated ?? true, referral_enabled: data.referral_enabled ?? true, + pool_names: + Array.isArray(data.pool_names) && data.pool_names.length > 0 + ? data.pool_names + : defaults.pool_names, + crypto_payment_enabled: data.crypto_payment_enabled ?? false, + crypto_only: data.crypto_only ?? false, + nowpayments_currencies: Array.isArray(data.nowpayments_currencies) + ? data.nowpayments_currencies + : [], }; } catch { return defaults; } }; +export interface CryptoPaymentStatus { + command_id: number; + payment_status: string; + pay_address: string; + pay_amount: number; + pay_currency: string; + price_amount: number; + price_currency: string; +} + +export const getCryptoPaymentStatus = async ( + commandId: number, +): Promise => { + const token = getAuthToken(); + if (!token) return null; + try { + const response = await fetch( + `${API_URL}/commands/${commandId}/payment-status`, + { + headers: { Authorization: `Bearer ${token}` }, + }, + ); + if (!response.ok) return null; + return await safeJson(response); + } catch { + return null; + } +}; + export interface ReferralBalanceResponse { success: boolean; balance: number; referral_enabled?: boolean; } -export const getReferralBalance = async (): Promise => { +export const getReferralBalance = + async (): Promise => { + const token = getAuthToken(); + if (!token) return { success: false, balance: 0 }; + try { + const response = await fetch(`${API_URL}/referral/balance`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!response.ok) return { success: false, balance: 0 }; + const data = await safeJson(response); + return { + success: true, + balance: data.balance ?? 0, + referral_enabled: data.referral_enabled, + }; + } catch { + return { success: false, balance: 0 }; + } + }; + +export const getMyProfile = async (): Promise<{ + success: boolean; + client?: { + nom: string; + prenom: string; + telephone: string; + username: string; + }; + message?: string; +}> => { const token = getAuthToken(); - if (!token) return { success: false, balance: 0 }; + if (!token) return { success: false, message: "Non authentifié" }; try { - const response = await fetch(`${API_URL}/referral/balance`, { + const response = await fetch(`${API_URL}/profile`, { headers: { Authorization: `Bearer ${token}` }, }); - if (!response.ok) return { success: false, balance: 0 }; - const data = await response.json(); - return { success: true, balance: data.balance ?? 0, referral_enabled: data.referral_enabled }; + const data = await safeJson(response); + return data; } catch { - return { success: false, balance: 0 }; + return { success: false, message: "Erreur de connexion" }; + } +}; + +export const updateMyProfile = async (fields: { + nom?: string; + prenom?: string; + telephone?: string; +}): Promise<{ success: boolean; message?: string }> => { + const token = getAuthToken(); + if (!token) return { success: false, message: "Non authentifié" }; + try { + const response = await fetch(`${API_URL}/profile/update`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(fields), + }); + const data = await safeJson(response); + return data; + } catch { + return { success: false, message: "Erreur de connexion" }; } }; diff --git a/frontend-prep/src/api/api_types.ts b/frontend-prep/src/api/api_types.ts index e4ac3a82..eede2320 100644 --- a/frontend-prep/src/api/api_types.ts +++ b/frontend-prep/src/api/api_types.ts @@ -35,7 +35,7 @@ export interface UserResponse { session_id?: string; command?: number; point?: number; - point_zipette?: number; // ✅ AJOUTER CETTE LIGNE + pool_points?: number[]; amende?: number; } @@ -581,7 +581,8 @@ export interface ClientStats { telephone?: string; total_commands: number; points: number; - points_zipette: number; // ✅ AJOUTER CETTE LIGNE + pool_points: number[]; + pool_names: string[]; penalties: number; } @@ -709,8 +710,8 @@ export interface PenaltyInfo { total_penalty: number; cancellations_count: number; has_penalties: boolean; - points?: number; // ✅ AJOUTER CETTE LIGNE (points weed/hash) - points_zipette?: number; // ✅ AJOUTER CETTE LIGNE (points zipette) + pool_points?: number[]; + pool_names?: string[]; cancellation_history?: { current_amende: number; next_penalty: number; diff --git a/frontend-prep/src/components/Navbar.tsx b/frontend-prep/src/components/Navbar.tsx index 177e4dcd..8e568ab9 100644 --- a/frontend-prep/src/components/Navbar.tsx +++ b/frontend-prep/src/components/Navbar.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { useNavigate, useLocation } from "react-router-dom"; -import { useCart } from "../context/CartContext"; +import { useCart } from "../context/useCart"; import "./Navbar.css"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { @@ -14,10 +14,11 @@ import { faTimes, faBell, faGift, + faUserCircle, } from "@fortawesome/free-solid-svg-icons"; import { faTelegram } from "@fortawesome/free-brands-svg-icons"; import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; -import { getClientNotifications, markNotificationsRead } from "../api/api"; +import { getClientNotifications, markNotificationsRead, getPublicSettings } from "../api/api"; import type { ClientNotification } from "../api/api"; interface MenuItem { @@ -33,6 +34,7 @@ function Navbar() { const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [showNotifPanel, setShowNotifPanel] = useState(false); + const [referralEnabled, setReferralEnabled] = useState(true); const seenKeysRef = useRef>(new Set()); const isFirstLoadRef = useRef(true); const notifPanelRef = useRef(null); @@ -67,6 +69,10 @@ function Navbar() { return () => clearInterval(interval); }, [fetchNotifications]); + useEffect(() => { + getPublicSettings().then((s) => setReferralEnabled(s.referral_enabled)); + }, []); + useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (notifPanelRef.current && !notifPanelRef.current.contains(e.target as Node)) { @@ -92,7 +98,8 @@ function Navbar() { { id: "panier", label: "Mon Panier", icon: faShoppingCart, path: "/user/panier" }, { id: "suivi", label: "Suivi Livraison", icon: faTruck, path: "/user/suivi-livraison" }, { id: "historique", label: "Historique", icon: faClockRotateLeft, path: "/user/consultation-historique" }, - { id: "parrainage", label: "Parrainage", icon: faGift, path: "/user/parrainage" }, + ...(referralEnabled ? [{ id: "parrainage", label: "Parrainage", icon: faGift, path: "/user/parrainage" } as MenuItem] : []), + { id: "profil", label: "Mon Profil", icon: faUserCircle, path: "/user/profil" }, ]; const toggleMenu = () => setIsMenuOpen((v) => !v); diff --git a/frontend-prep/src/components/ProductCard.css b/frontend-prep/src/components/ProductCard.css index 620c8664..2d78db71 100644 --- a/frontend-prep/src/components/ProductCard.css +++ b/frontend-prep/src/components/ProductCard.css @@ -1,6 +1,6 @@ .product-card { background-color: #1a1a1a; - border: 2px solid white; + border: 2px solid var(--category-color, white); border-radius: 15px; overflow: hidden; display: flex; @@ -14,7 +14,7 @@ .product-card:hover { transform: scale(1.08); - box-shadow: 0 8px 25px rgba(255, 255, 255, 0.2); + box-shadow: 0 8px 25px color-mix(in srgb, var(--category-color, white) 40%, transparent); z-index: 10; } diff --git a/frontend-prep/src/components/ProductCard.tsx b/frontend-prep/src/components/ProductCard.tsx index b963d87e..e9398d23 100644 --- a/frontend-prep/src/components/ProductCard.tsx +++ b/frontend-prep/src/components/ProductCard.tsx @@ -1,8 +1,17 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { useCart } from "../context/CartContext"; +import { useCart } from "../context/useCart"; import "./ProductCard.css"; +function getTextColor(hex: string): string { + const h = hex.replace("#", ""); + const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h; + const r = parseInt(full.slice(0, 2), 16); + const g = parseInt(full.slice(2, 4), 16); + const b = parseInt(full.slice(4, 6), 16); + return (r * 299 + g * 587 + b * 114) / 1000 > 128 ? "#000000" : "#ffffff"; +} + interface ProductCardProps { id: number; name: string; @@ -108,8 +117,13 @@ function ProductCard({ } }; + const cardColor = categoryColor || "#ffffff"; + return ( -
+
diff --git a/frontend-prep/src/context/CartContext.tsx b/frontend-prep/src/context/CartContext.tsx index 2d6539d4..84201d43 100644 --- a/frontend-prep/src/context/CartContext.tsx +++ b/frontend-prep/src/context/CartContext.tsx @@ -7,7 +7,6 @@ import { createContext, - useContext, useState, useEffect, type ReactNode, @@ -55,7 +54,7 @@ interface ToastMessage { type: "success" | "error" | "warning" | "info"; } -const CartContext = createContext(undefined); +export const CartContext = createContext(undefined); export function CartProvider({ children }: { children: ReactNode }) { const [cartItems, setCartItems] = useState([]); @@ -201,9 +200,10 @@ export function CartProvider({ children }: { children: ReactNode }) { const requestData = { username, + product_id: item.product_id || 0, name_product: cleanName, category: category, - quantity: Number(item.quantity), // ✨ Grammes (5, 10, 25) + quantity: Number(item.quantity), price: Number(item.price) || 0, }; @@ -345,10 +345,3 @@ export function CartProvider({ children }: { children: ReactNode }) { ); } -export function useCart() { - const context = useContext(CartContext); - if (!context) { - throw new Error("useCart must be used within a CartProvider"); - } - return context; -} diff --git a/frontend-prep/src/context/useCart.ts b/frontend-prep/src/context/useCart.ts new file mode 100644 index 00000000..e0aee636 --- /dev/null +++ b/frontend-prep/src/context/useCart.ts @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import { CartContext } from "./CartContext"; + +export function useCart() { + const context = useContext(CartContext); + if (!context) { + throw new Error("useCart must be used within a CartProvider"); + } + return context; +} diff --git a/frontend-prep/src/pages/User/Accueil.tsx b/frontend-prep/src/pages/User/Accueil.tsx index 6a4d004c..9cd40c60 100644 --- a/frontend-prep/src/pages/User/Accueil.tsx +++ b/frontend-prep/src/pages/User/Accueil.tsx @@ -1,10 +1,24 @@ import { useState, useEffect } from "react"; import ProductCard from "../../components/ProductCard"; import Navbar from "../../components/Navbar"; -import { getAllProducts, getProductsByCategory, getMediaUrl, getCategories } from "../../api/api"; +import { + getAllProducts, + getProductsByCategory, + getMediaUrl, + getCategories, +} from "../../api/api"; import type { Product, Category } from "../../api/api"; import "./UserAccueil.css"; +function getTextColor(hex: string): string { + const h = hex.replace("#", ""); + const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h; + const r = parseInt(full.slice(0, 2), 16); + const g = parseInt(full.slice(2, 4), 16); + const b = parseInt(full.slice(4, 6), 16); + return (r * 299 + g * 587 + b * 114) / 1000 > 128 ? "#000000" : "#ffffff"; +} + function UserAccueil() { const [selectedCategory, setSelectedCategory] = useState("tous"); const [categories, setCategories] = useState([]); @@ -42,10 +56,7 @@ function UserAccueil() { if (response.success && response.data) { setProducts(response.data); } else { - setError( - response.message || - "Erreur lors du chargement des produits", - ); + setProducts([]); } } catch (err: any) { setError(err.message || "Erreur lors du chargement des produits"); @@ -104,13 +115,29 @@ function UserAccueil() { return "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image"; }; - const selectedCategoryObj = categories.find((c) => c.name === selectedCategory); + const selectedCategoryObj = categories.find( + (c) => c.name === selectedCategory, + ); const isSelectedComingSoon = selectedCategoryObj?.is_coming_soon ?? false; +
+

+ {selectedCategory === "tous" + ? "Tous les produits" + : selectedCategory} +

+
; return ( <>
+
+

+ {selectedCategory === "tous" + ? "Tous les produits" + : selectedCategory} +

+
+ {categories.map((category) => { const isActive = selectedCategory === category.name; const catColor = category.color || "#7c3aed"; @@ -131,11 +159,13 @@ function UserAccueil() { ? { backgroundColor: catColor, borderColor: catColor, - color: "#ffffff", + color: getTextColor(catColor), } : { borderColor: `${catColor}66` } } - onClick={() => handleCategoryChange(category.name)} + onClick={() => + handleCategoryChange(category.name) + } > {category.name} @@ -143,12 +173,6 @@ function UserAccueil() { })}
-
-

- {selectedCategory === "tous" ? "Tous les produits" : selectedCategory} -

-
- {isSelectedComingSoon ? (
Prochainement @@ -156,43 +180,50 @@ function UserAccueil() { Les produits de cette catégorie arrivent bientôt !

+ ) : loading ? ( +
+

Chargement des produits...

+
+ ) : error ? ( +
+

{error}

+ +
+ ) : products.length === 0 ? ( +
+

+ Il n'y a pas de produit disponible pour l'instant. +

+
) : ( - <> - {loading && ( -
-

Chargement des produits...

+
+ {products.map((product) => ( +
+ + c.name.toLowerCase() === + product.category?.toLowerCase(), + )?.color + } + />
- )} - - {!loading && !error && products.length > 0 && ( -
- {products.map((product) => ( -
- c.name.toLowerCase() === product.category?.toLowerCase(), - )?.color - } - /> -
- ))} -
- )} - + ))} +
)}
diff --git a/frontend-prep/src/pages/User/Cart.css b/frontend-prep/src/pages/User/Cart.css index 59bdf9b8..7e0cd6ef 100644 --- a/frontend-prep/src/pages/User/Cart.css +++ b/frontend-prep/src/pages/User/Cart.css @@ -19,6 +19,7 @@ margin-bottom: clamp(1.5rem, 4vw, 2rem); flex-wrap: wrap; gap: 1rem; + margin-top: -65px; } .cart-header h1 { diff --git a/frontend-prep/src/pages/User/Cart.tsx b/frontend-prep/src/pages/User/Cart.tsx index 2374bb7b..1bd6437c 100644 --- a/frontend-prep/src/pages/User/Cart.tsx +++ b/frontend-prep/src/pages/User/Cart.tsx @@ -7,7 +7,7 @@ // ✅ Pour acheter 2× le même produit, l'ajouter 2 fois // ✅ Vérification continue de l'authentification -import { useCart } from "../../context/CartContext"; +import { useCart } from "../../context/useCart"; import Navbar from "../../components/Navbar"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; diff --git a/frontend-prep/src/pages/User/Checkout.css b/frontend-prep/src/pages/User/Checkout.css index 1f4d6802..51fa8ce2 100644 --- a/frontend-prep/src/pages/User/Checkout.css +++ b/frontend-prep/src/pages/User/Checkout.css @@ -1,3 +1,10 @@ +.form-prefill-hint { + font-size: 0.75rem; + color: #6ee7b7; + margin: 0.3rem 0 0; + opacity: 0.85; +} + .checkout-container { width: 100%; min-height: 100vh; @@ -327,11 +334,11 @@ background: rgba(0, 0, 0, 0.92); backdrop-filter: blur(10px); display: flex; - align-items: center; + align-items: flex-start; justify-content: center; z-index: 10000; animation: fadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); - padding: 1rem; + padding: 2rem 1rem; overflow-y: auto; -webkit-overflow-scrolling: touch; } @@ -351,8 +358,6 @@ border-radius: 20px; max-width: 600px; width: 100%; - max-height: 90vh; - overflow-y: auto; box-shadow: 0 25px 70px rgba(91, 33, 182, 0.5), 0 0 120px rgba(91, 33, 182, 0.3); animation: slideUp 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); position: relative; @@ -639,13 +644,12 @@ /* Modal responsive compacte */ .confirmation-modal-overlay { - padding: 0.75rem; - align-items: center; + padding: 1rem 0.75rem; + align-items: flex-start; } .confirmation-modal { max-width: 100%; - max-height: 80vh; border-radius: 12px; border-width: 1px; } @@ -727,11 +731,10 @@ /* Modal encore plus compacte sur mobile */ .confirmation-modal-overlay { - padding: 0.5rem; + padding: 0.75rem 0.5rem; } .confirmation-modal { - max-height: 85vh; border-radius: 10px; } @@ -972,3 +975,172 @@ .referral-switch input:checked + .referral-switch-slider::before { transform: translateX(22px); } + +/* ============================================ */ +/* PAIEMENT - Sélecteur de méthode */ +/* ============================================ */ +.payment-method-selector { + display: flex; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.payment-method-btn { + flex: 1; + padding: 0.75rem 1rem; + border: 2px solid #2d2d2d; + border-radius: 10px; + background: #1a1a1a; + color: #9ca3af; + font-size: 0.95rem; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.payment-method-btn:hover { + border-color: #4b5563; + color: #fff; +} + +.payment-method-btn.active { + border-color: #f7931a; + color: #f7931a; + background: rgba(247, 147, 26, 0.08); +} + +/* ============================================ */ +/* CRYPTO - Sélecteur de monnaie */ +/* ============================================ */ +.crypto-currency-selector { + margin-top: 1rem; +} + +.crypto-currency-selector label { + display: block; + font-size: 0.85rem; + color: #9ca3af; + margin-bottom: 0.5rem; +} + +.crypto-currency-grid { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.crypto-currency-btn { + padding: 0.4rem 0.9rem; + border: 2px solid #2d2d2d; + border-radius: 8px; + background: #1a1a1a; + color: #9ca3af; + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + letter-spacing: 0.05em; +} + +.crypto-currency-btn:hover { + border-color: #4b5563; + color: #fff; +} + +.crypto-currency-btn.active { + border-color: #f7931a; + color: #f7931a; + background: rgba(247, 147, 26, 0.08); +} + +.crypto-info-hint { + font-size: 0.78rem; + color: #6b7280; + margin-top: 0.6rem; +} + +/* ============================================ */ +/* CRYPTO - Modal de paiement */ +/* ============================================ */ +.crypto-address-box { + display: flex; + align-items: center; + gap: 0.75rem; + background: #111; + border: 1px solid #2d2d2d; + border-radius: 8px; + padding: 0.75rem 1rem; + margin-top: 0.5rem; + flex-wrap: wrap; +} + +.crypto-address { + font-family: monospace; + font-size: 0.78rem; + color: #d1d5db; + word-break: break-all; + flex: 1; +} + +.crypto-copy-btn { + padding: 0.35rem 0.75rem; + border: 1px solid #374151; + border-radius: 6px; + background: #1f2937; + color: #9ca3af; + font-size: 0.8rem; + cursor: pointer; + white-space: nowrap; + transition: all 0.2s; +} + +.crypto-copy-btn:hover { + background: #374151; + color: #fff; +} + +.crypto-equiv { + color: #6b7280; + font-size: 0.85rem; + margin-left: 0.5rem; +} + +.crypto-status--waiting { color: #fbbf24; } +.crypto-status--confirming { color: #60a5fa; } +.crypto-status--confirmed, +.crypto-status--finished { color: #34d399; } +.crypto-status--failed, +.crypto-status--expired { color: #f87171; } + +.crypto-polling-info { + text-align: center; + color: #6b7280; + font-size: 0.82rem; + margin-top: 0.5rem; +} + +.crypto-success-msg { + text-align: center; + color: #34d399; + font-size: 0.9rem; + padding: 0.75rem; + background: rgba(52, 211, 153, 0.08); + border-radius: 8px; + margin-top: 0.5rem; +} + +.crypto-only-badge { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + background: rgba(247, 147, 26, 0.08); + border: 1px solid rgba(247, 147, 26, 0.3); + border-radius: 10px; + color: #f7931a; + font-size: 0.9rem; + font-weight: 500; +} diff --git a/frontend-prep/src/pages/User/Checkout.tsx b/frontend-prep/src/pages/User/Checkout.tsx index fbc06907..c43a30f6 100644 --- a/frontend-prep/src/pages/User/Checkout.tsx +++ b/frontend-prep/src/pages/User/Checkout.tsx @@ -1,8 +1,8 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useCart } from '../../context/CartContext'; -import { createCheckout, clearCart, extractUsernameFromToken, isUserAuthenticated, getReferralBalance, getPublicSettings } from '../../api/api'; -import type { CheckoutData } from '../../api/api'; +import { useCart } from '../../context/useCart'; +import { createCheckout, clearCart, extractUsernameFromToken, isUserAuthenticated, getReferralBalance, getPublicSettings, getMyProfile, getCryptoPaymentStatus } from '../../api/api'; +import type { CheckoutData, CryptoPaymentStatus } from '../../api/api'; import Navbar from '../../components/Navbar'; import './Checkout.css'; @@ -42,6 +42,10 @@ function Checkout() { const [showConfirmation, setShowConfirmation] = useState(false); const [confirmationData, setConfirmationData] = useState(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(''); @@ -55,6 +59,17 @@ function Checkout() { 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([]); + const [paymentMethod, setPaymentMethod] = useState<'especes' | 'crypto'>('especes'); + const [payCurrency, setPayCurrency] = useState(''); + const [cryptoPaymentData, setCryptoPaymentData] = useState(null); + const [showCryptoModal, setShowCryptoModal] = useState(false); + const [cryptoPolling, setCryptoPolling] = useState(false); + const pollIntervalRef = useRef | null>(null); + // ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION useEffect(() => { const checkAuth = () => { @@ -79,7 +94,22 @@ function Checkout() { return () => clearInterval(authInterval); }, [navigate]); - // Charger solde parrainage si activé + // 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 depuis le backend + getMyProfile().then((res) => { + if (res.success && res.client) { + if (res.client.nom) setLastName(res.client.nom); + } + }); + }, []); + + // Charger settings publics (parrainage + crypto) useEffect(() => { getPublicSettings().then((settings) => { if (settings.referral_enabled) { @@ -88,9 +118,25 @@ function Checkout() { 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 */ @@ -135,6 +181,39 @@ function Checkout() { }); }; + 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 */ @@ -174,7 +253,8 @@ function Checkout() { first_name: firstName, last_name: lastName, phone, - payment_method: 'especes', + payment_method: paymentMethod === 'crypto' ? 'crypto' : 'especes', + pay_currency: paymentMethod === 'crypto' ? payCurrency : undefined, use_referral_balance: useReferral && referralBalance > 0, }; @@ -183,6 +263,28 @@ function Checkout() { 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!, + 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; @@ -337,6 +439,9 @@ function Checkout() { disabled={loading} required /> + {localStorage.getItem('profile_default_address') && ( +

Pré-rempli depuis votre profil — modifiez si vous êtes ailleurs

+ )}
@@ -350,14 +455,71 @@ function Checkout() { disabled={loading} required /> + {localStorage.getItem('profile_default_phone') && ( +

Pré-rempli depuis votre profil

+ )}
+ {/* Méthode de paiement */} + {cryptoEnabled && ( +
+

Méthode de paiement

+ + {cryptoOnly ? ( +
+ Paiement uniquement en cryptomonnaie +
+ ) : ( +
+ + +
+ )} + + {(paymentMethod === 'crypto') && ( +
+ +
+ {cryptoCurrencies.map((currency) => ( + + ))} +
+

+ Vous recevrez l'adresse de paiement après validation +

+
+ )} +
+ )} + {/* Toggle parrainage */} {referralEnabled && referralBalance > 0 && (
- 🎁 +

Solde parrainage

{referralBalance.toFixed(2)} € disponible

@@ -397,6 +559,111 @@ function Checkout() {
+ {/* ============================================ */} + {/* MODAL PAIEMENT CRYPTO */} + {/* ============================================ */} + {showCryptoModal && cryptoPaymentData && ( +
+
+
+ +

Paiement Crypto

+
+
+
+
+ Commande #{cryptoPaymentData.command_id} +
+
+ Statut :{' '} + + {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} + +
+
+ +
+
+ Adresse de paiement +
+
+ {cryptoPaymentData.pay_address} + +
+
+ +
+
+ Montant à envoyer +
+
+ {cryptoPaymentData.pay_amount} {cryptoPaymentData.pay_currency.toUpperCase()} + ≈ {cryptoPaymentData.price_amount.toFixed(2)} {cryptoPaymentData.price_currency.toUpperCase()} +
+
+ + {cryptoPolling && ( +
+ Vérification automatique toutes les 10 secondes... +
+ )} + + {(cryptoPaymentData.payment_status === 'finished' || cryptoPaymentData.payment_status === 'confirmed') && ( +
+ Paiement confirmé ! Votre commande est en cours de traitement. +
+ )} +
+
+ +
+
+
+ )} + + {/* ============================================ */} + {/* MODAL ZONE NON DESSERVIE */} + {/* ============================================ */} + {showZoneModal && ( +
setShowZoneModal(false)}> +
e.stopPropagation()}> +
+ +

Zone non desservie

+
+
+
+
+

{zoneErrorMsg}

+

+ Vérifiez l'adresse saisie ou contactez-nous pour connaître les zones de livraison disponibles. +

+
+
+
+
+ +
+
+
+ )} + {/* ============================================ */} {/* MODAL DE CONFIRMATION STYLISÉ */} {/* ============================================ */} @@ -463,6 +730,17 @@ function Checkout() {
)} + {/* Suivi */} +
+
+ + Suivi de livraison +
+
+ Suivez votre livraison en temps réel depuis la page Suivi +
+
+ {/* Total */}
diff --git a/frontend-prep/src/pages/User/ConsultationHistorique.css b/frontend-prep/src/pages/User/ConsultationHistorique.css index cf42eac5..f9e86837 100644 --- a/frontend-prep/src/pages/User/ConsultationHistorique.css +++ b/frontend-prep/src/pages/User/ConsultationHistorique.css @@ -4,13 +4,13 @@ Styles pour 4 compteurs de points distincts */ .history-container { - width: 100%; - min-height: 100vh; - padding: clamp(1rem, 3vw, 2rem); - padding-top: calc(60px + clamp(1rem, 3vw, 2rem)); - max-width: 1400px; - margin: 0 auto; - background: linear-gradient(to bottom, #0a0a0a, #1a1a1a); + width: 100%; + min-height: 100vh; + padding: clamp(1rem, 3vw, 2rem); + padding-top: calc(60px + clamp(1rem, 3vw, 2rem)); + max-width: 1400px; + margin: 0 auto; + background: linear-gradient(to bottom, #0a0a0a, #1a1a1a); } /* ============================================ @@ -18,25 +18,26 @@ ============================================ */ .history-header { - text-align: center; - margin-bottom: 2rem; + text-align: center; + margin-bottom: 2rem; + margin-top: -65px; } .history-title { - color: white; - font-size: clamp(2rem, 6vw, 2.5rem); - margin: 0 0 0.5rem 0; - font-weight: 700; - background: #ffffff; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + color: white; + font-size: clamp(2rem, 6vw, 2.5rem); + margin: 0 0 0.5rem 0; + font-weight: 700; + background: #ffffff; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } .history-subtitle { - color: #9ca3af; - font-size: clamp(0.9rem, 2vw, 1.1rem); - margin: 0; + color: #9ca3af; + font-size: clamp(0.9rem, 2vw, 1.1rem); + margin: 0; } /* ============================================ @@ -44,22 +45,22 @@ ============================================ */ .stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 1.5rem; - margin-bottom: 2rem; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; } .stat-card2 { - background: linear-gradient(135deg, #1a1a1a, #2a2a2a); - border: 2px solid #333; - border-radius: 12px; - padding: 1.5rem; - display: flex; - align-items: center; - gap: 1rem; - transition: all 0.3s ease; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); + background: linear-gradient(135deg, #1a1a1a, #2a2a2a); + border: 2px solid #333; + border-radius: 12px; + padding: 1.5rem; + display: flex; + align-items: center; + gap: 1rem; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); } /* ============================================ @@ -67,21 +68,21 @@ ============================================ */ .stat-card2:hover { - border-color: #6d28d9; - transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(109, 40, 217, 0.4); + border-color: #6d28d9; + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(109, 40, 217, 0.4); } .stat-icon { - width: 48px; - height: 48px; - background: linear-gradient(135deg, #7c3aed, #6d28d9); - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - color: white; - flex-shrink: 0; + width: 48px; + height: 48px; + background: linear-gradient(135deg, #7c3aed, #6d28d9); + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + color: white; + flex-shrink: 0; } /* ============================================ @@ -89,11 +90,11 @@ ============================================ */ .stat-icon.icon-total-orders { - background: linear-gradient(135deg, #6d28d9, #5b21b6); + background: linear-gradient(135deg, #6d28d9, #5b21b6); } .stat-icon.icon-completed-orders { - background: linear-gradient(135deg, #8b5cf6, #7c3aed); + background: linear-gradient(135deg, #8b5cf6, #7c3aed); } /* ============================================ @@ -102,52 +103,52 @@ /* Total Commandes - Violet Très Sombre */ .stat-card2.total-orders:hover { - border-color: #5b21b6; - box-shadow: 0 8px 25px rgba(91, 33, 182, 0.5); + border-color: #5b21b6; + box-shadow: 0 8px 25px rgba(91, 33, 182, 0.5); } .stat-icon.icon-total-orders { - background: linear-gradient(135deg, #6d28d9, #5b21b6); + background: linear-gradient(135deg, #6d28d9, #5b21b6); } /* Commandes Livrées - Violet Sombre Clair */ .stat-card2.completed-orders:hover { - border-color: #7c3aed; - box-shadow: 0 8px 25px rgba(124, 58, 237, 0.5); + border-color: #7c3aed; + box-shadow: 0 8px 25px rgba(124, 58, 237, 0.5); } .stat-icon.icon-completed-orders { - background: linear-gradient(135deg, #8b5cf6, #7c3aed); + background: linear-gradient(135deg, #8b5cf6, #7c3aed); } /* Points Weed/Hash - Vert (couleur d'origine) */ .stat-card2.points-weed:hover { - border-color: #5b21b6; - box-shadow: 0 8px 25px rgba(91, 33, 182, 0.5); + border-color: #5b21b6; + box-shadow: 0 8px 25px rgba(91, 33, 182, 0.5); } .stat-icon.icon-weed { - background: linear-gradient(135deg, #10b981, #059669); + background: linear-gradient(135deg, #10b981, #059669); } /* Points Zipette - Bleu (couleur d'origine) */ .stat-card2.points-zipette:hover { - border-color: #6d28d9; - box-shadow: 0 8px 25px rgba(109, 40, 217, 0.5); + border-color: #6d28d9; + box-shadow: 0 8px 25px rgba(109, 40, 217, 0.5); } .stat-icon.icon-zipette { - background: linear-gradient(135deg, #3b82f6, #2563eb); + background: linear-gradient(135deg, #3b82f6, #2563eb); } /* Points Total - Orange (couleur d'origine) */ .stat-card2.points-total:hover { - border-color: #4c1d95; - box-shadow: 0 8px 25px rgba(76, 29, 149, 0.5); + border-color: #4c1d95; + box-shadow: 0 8px 25px rgba(76, 29, 149, 0.5); } .stat-icon.icon-total { - background: linear-gradient(135deg, #f59e0b, #d97706); + background: linear-gradient(135deg, #f59e0b, #d97706); } /* ============================================ @@ -155,21 +156,21 @@ ============================================ */ .stat-content { - flex: 1; + flex: 1; } .stat-label { - color: #9ca3af; - font-size: 0.9rem; - margin: 0 0 0.5rem 0; - font-weight: 500; + color: #9ca3af; + font-size: 0.9rem; + margin: 0 0 0.5rem 0; + font-weight: 500; } .stat-value { - color: white; - font-size: 1.8rem; - font-weight: 700; - margin: 0; + color: white; + font-size: 1.8rem; + font-weight: 700; + margin: 0; } /* ============================================ @@ -177,49 +178,50 @@ ============================================ */ .penalty-stat { - position: relative; + position: relative; } .penalty-stat.has-penalty:hover { - border-color: #7c3aed; - box-shadow: 0 8px 25px rgba(124, 58, 237, 0.5); + border-color: #7c3aed; + box-shadow: 0 8px 25px rgba(124, 58, 237, 0.5); } .stat-icon.penalty-warning { - background: linear-gradient(135deg, #8b5cf6, #7c3aed); + background: linear-gradient(135deg, #8b5cf6, #7c3aed); } .stat-icon.penalty-critical { - background: linear-gradient(135deg, #6d28d9, #5b21b6); - animation: pulse-penalty 2s ease-in-out infinite; + background: linear-gradient(135deg, #6d28d9, #5b21b6); + animation: pulse-penalty 2s ease-in-out infinite; } @keyframes pulse-penalty { - 0%, 100% { - box-shadow: 0 0 0 0 rgba(109, 40, 217, 0.7); - } - 50% { - box-shadow: 0 0 0 10px rgba(109, 40, 217, 0); - } + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(109, 40, 217, 0.7); + } + 50% { + box-shadow: 0 0 0 10px rgba(109, 40, 217, 0); + } } .penalty-value { - display: inline-flex; - align-items: baseline; - gap: 0.25rem; + display: inline-flex; + align-items: baseline; + gap: 0.25rem; } .penalty-limit { - font-size: 1rem; - color: #6b7280; - font-weight: 500; + font-size: 1rem; + color: #6b7280; + font-weight: 500; } .penalty-warning-text { - color: #a78bfa; - font-size: 0.85rem; - margin: 0.5rem 0 0 0; - font-weight: 600; + color: #a78bfa; + font-size: 0.85rem; + margin: 0.5rem 0 0 0; + font-weight: 600; } /* ============================================ @@ -227,22 +229,22 @@ ============================================ */ .stat-card2.referral-stat:hover { - border-color: #8b5cf6; - box-shadow: 0 8px 25px rgba(139, 92, 246, 0.4); + border-color: #8b5cf6; + box-shadow: 0 8px 25px rgba(139, 92, 246, 0.4); } .stat-icon.icon-referral { - background: linear-gradient(135deg, #8b5cf6, #6d28d9); + background: linear-gradient(135deg, #8b5cf6, #6d28d9); } .referral-value-active { - color: #8b5cf6 !important; + color: #8b5cf6 !important; } .referral-link-hint { - color: #6b7280; - font-size: 0.8rem; - margin: 0.25rem 0 0; + color: #6b7280; + font-size: 0.8rem; + margin: 0.25rem 0 0; } /* ============================================ @@ -250,38 +252,38 @@ ============================================ */ .loading-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 60vh; - gap: 1rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 60vh; + gap: 1rem; } .loading-spinner { - width: 60px; - height: 60px; + width: 60px; + height: 60px; } .spinner { - width: 100%; - height: 100%; - border: 4px solid #333; - border-top-color: #7c3aed; - border-radius: 50%; - animation: spin 1s linear infinite; + width: 100%; + height: 100%; + border: 4px solid #333; + border-top-color: #7c3aed; + border-radius: 50%; + animation: spin 1s linear infinite; } @keyframes spin { - to { - transform: rotate(360deg); - } + to { + transform: rotate(360deg); + } } .loading-text { - color: #9ca3af; - font-size: 1.1rem; - margin: 0; + color: #9ca3af; + font-size: 1.1rem; + margin: 0; } /* ============================================ @@ -289,24 +291,24 @@ ============================================ */ .error-banner { - display: flex; - align-items: center; - gap: 1rem; - background: rgba(109, 40, 217, 0.1); - border: 2px solid #7c3aed; - border-radius: 12px; - padding: 1rem 1.5rem; - margin-bottom: 2rem; + display: flex; + align-items: center; + gap: 1rem; + background: rgba(109, 40, 217, 0.1); + border: 2px solid #7c3aed; + border-radius: 12px; + padding: 1rem 1.5rem; + margin-bottom: 2rem; } .error-banner span { - font-size: 1.5rem; + font-size: 1.5rem; } .error-banner p { - color: #c4b5fd; - margin: 0; - font-weight: 500; + color: #c4b5fd; + margin: 0; + font-weight: 500; } /* ============================================ @@ -314,54 +316,54 @@ ============================================ */ .empty-history { - text-align: center; - padding: 4rem 2rem; - background: linear-gradient(135deg, #1a1a1a, #2a2a2a); - border: 2px solid #333; - border-radius: 12px; - margin-top: 2rem; + text-align: center; + padding: 4rem 2rem; + background: linear-gradient(135deg, #1a1a1a, #2a2a2a); + border: 2px solid #333; + border-radius: 12px; + margin-top: 2rem; } .empty-icon { - color: #4b5563; - margin-bottom: 1.5rem; - opacity: 0.5; + color: #4b5563; + margin-bottom: 1.5rem; + opacity: 0.5; } .empty-history h2 { - color: white; - font-size: 1.5rem; - margin: 0 0 0.5rem 0; - font-weight: 600; + color: white; + font-size: 1.5rem; + margin: 0 0 0.5rem 0; + font-weight: 600; } .empty-history p { - color: #9ca3af; - font-size: 1.1rem; - margin: 0 0 2rem 0; + color: #9ca3af; + font-size: 1.1rem; + margin: 0 0 2rem 0; } .browse-button { - background: linear-gradient(to right, #7c3aed, #6d28d9); - color: white; - border: none; - padding: 0.75rem 2rem; - border-radius: 8px; - font-size: 1rem; - font-weight: 600; - cursor: pointer; - transition: all 0.2s; - box-shadow: 0 4px 15px rgba(124, 58, 237, 0.3); + background: linear-gradient(to right, #7c3aed, #6d28d9); + color: white; + border: none; + padding: 0.75rem 2rem; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + box-shadow: 0 4px 15px rgba(124, 58, 237, 0.3); } .browse-button:hover { - background: linear-gradient(to right, #6d28d9, #5b21b6); - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(109, 40, 217, 0.5); + background: linear-gradient(to right, #6d28d9, #5b21b6); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(109, 40, 217, 0.5); } .browse-button:active { - transform: translateY(0); + transform: translateY(0); } /* ============================================ @@ -369,18 +371,18 @@ ============================================ */ .orders-summary { - margin-bottom: 1rem; - padding: 0.75rem 1rem; - background: rgba(124, 58, 237, 0.1); - border: 1px solid rgba(124, 58, 237, 0.3); - border-radius: 8px; + margin-bottom: 1rem; + padding: 0.75rem 1rem; + background: rgba(124, 58, 237, 0.1); + border: 1px solid rgba(124, 58, 237, 0.3); + border-radius: 8px; } .orders-summary p { - color: #a78bfa; - margin: 0; - font-weight: 600; - font-size: 0.95rem; + color: #a78bfa; + margin: 0; + font-weight: 600; + font-size: 0.95rem; } /* ============================================ @@ -388,59 +390,59 @@ ============================================ */ .table-wrapper { - overflow-x: auto; - background-color: #1a1a1a; - border: 2px solid #333; - border-radius: 12px; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); + overflow-x: auto; + background-color: #1a1a1a; + border: 2px solid #333; + border-radius: 12px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); } .history-table { - width: 100%; - border-collapse: collapse; - min-width: 900px; + width: 100%; + border-collapse: collapse; + min-width: 900px; } .history-table thead { - background: linear-gradient(135deg, #0a0a0a, #1a1a1a); - border-bottom: 2px solid #7c3aed; + background: linear-gradient(135deg, #0a0a0a, #1a1a1a); + border-bottom: 2px solid #7c3aed; } .history-table th { - color: white; - font-size: clamp(0.85rem, 2vw, 0.95rem); - font-weight: 700; - text-align: left; - padding: 1.25rem 1rem; - text-transform: uppercase; - letter-spacing: 0.5px; + color: white; + font-size: clamp(0.85rem, 2vw, 0.95rem); + font-weight: 700; + text-align: left; + padding: 1.25rem 1rem; + text-transform: uppercase; + letter-spacing: 0.5px; } .history-table tbody tr { - border-bottom: 1px solid #333; - transition: all 0.2s ease; - cursor: pointer; + border-bottom: 1px solid #333; + transition: all 0.2s ease; + cursor: pointer; } .history-table tbody tr:last-child { - border-bottom: none; + border-bottom: none; } /* ✅ HOVER VIOLET SOMBRE SUR LES LIGNES */ .history-table tbody tr:hover { - background: linear-gradient(90deg, rgba(109, 40, 217, 0.15), transparent); - border-left: 3px solid #6d28d9; + background: linear-gradient(90deg, rgba(109, 40, 217, 0.15), transparent); + border-left: 3px solid #6d28d9; } .history-table tbody tr:active { - background-color: #2a2a2a; + background-color: #2a2a2a; } .history-table td { - color: #cccccc; - font-size: clamp(0.85rem, 2vw, 0.95rem); - padding: 1.25rem 1rem; - vertical-align: middle; + color: #cccccc; + font-size: clamp(0.85rem, 2vw, 0.95rem); + padding: 1.25rem 1rem; + vertical-align: middle; } /* ============================================ @@ -448,76 +450,76 @@ ============================================ */ .order-id { - color: white !important; - font-weight: 700; - font-family: 'Courier New', monospace; - font-size: 1rem; + color: white !important; + font-weight: 700; + font-family: "Courier New", monospace; + font-size: 1rem; } .date-cell { - display: flex; - flex-direction: column; - gap: 0.25rem; + display: flex; + flex-direction: column; + gap: 0.25rem; } .date-main { - color: white; - font-weight: 600; - font-size: 0.95rem; + color: white; + font-weight: 600; + font-size: 0.95rem; } .date-time { - color: #9ca3af; - font-size: 0.85rem; + color: #9ca3af; + font-size: 0.85rem; } .date-age { - color: #7c3aed; - font-size: 0.8rem; - font-weight: 500; + color: #7c3aed; + font-size: 0.8rem; + font-weight: 500; } .address-cell { - display: flex; - align-items: center; - gap: 0.5rem; + display: flex; + align-items: center; + gap: 0.5rem; } .address-icon { - color: #7c3aed; - flex-shrink: 0; + color: #7c3aed; + flex-shrink: 0; } .address-text { - color: #d1d5db; - font-size: 0.9rem; + color: #d1d5db; + font-size: 0.9rem; } .livreur-cell { - display: flex; - align-items: center; - gap: 0.5rem; + display: flex; + align-items: center; + gap: 0.5rem; } .livreur-icon { - color: #7c3aed; - flex-shrink: 0; + color: #7c3aed; + flex-shrink: 0; } .no-livreur { - color: #6b7280; - font-style: italic; + color: #6b7280; + font-style: italic; } .products-count { - color: #9ca3af; - font-size: 0.9rem; + color: #9ca3af; + font-size: 0.9rem; } .order-total2 { - color: #7c3aed !important; - font-weight: 700 !important; - font-size: 1.1rem !important; + color: #7c3aed !important; + font-weight: 700 !important; + font-size: 1.1rem !important; } /* ============================================ @@ -525,36 +527,36 @@ ============================================ */ .status-badge { - display: inline-flex; - align-items: center; - gap: 0.4rem; - padding: 0.5rem 1rem; - border-radius: 20px; - font-size: 0.85rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.5px; - white-space: nowrap; + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.5rem 1rem; + border-radius: 20px; + font-size: 0.85rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + white-space: nowrap; } .status-badge.delivered { - background: transparent; - color: #a78bfa; - border: 2px solid #7c3aed; - box-shadow: 0 0 30px rgba(124, 58, 237, 0.5); + background: transparent; + color: #a78bfa; + border: 2px solid #7c3aed; + box-shadow: 0 0 30px rgba(124, 58, 237, 0.5); } .status-badge.in-progress { - background: rgba(139, 92, 246, 0.15); - color: #a78bfa; - border: 2px solid #8b5cf6; - box-shadow: 0 0 10px rgba(139, 92, 246, 0.3); + background: rgba(139, 92, 246, 0.15); + color: #a78bfa; + border: 2px solid #8b5cf6; + box-shadow: 0 0 10px rgba(139, 92, 246, 0.3); } .status-badge.pending { - background: rgba(124, 58, 237, 0.15); - color: #c4b5fd; - border: 2px solid #7c3aed; + background: rgba(124, 58, 237, 0.15); + color: #c4b5fd; + border: 2px solid #7c3aed; } /* ============================================ @@ -562,75 +564,75 @@ ============================================ */ @media (max-width: 1024px) { - .history-table { - min-width: 800px; - } - - .stats-grid { - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - } + .history-table { + min-width: 800px; + } + + .stats-grid { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + } } @media (max-width: 768px) { - .stats-grid { - grid-template-columns: 1fr 1fr; - gap: 1rem; - } + .stats-grid { + grid-template-columns: 1fr 1fr; + gap: 1rem; + } - .stat-card2 { - padding: 1.25rem; - } + .stat-card2 { + padding: 1.25rem; + } - .history-table th, - .history-table td { - padding: 1rem 0.75rem; - font-size: 0.85rem; - } + .history-table th, + .history-table td { + padding: 1rem 0.75rem; + font-size: 0.85rem; + } - .order-id { - font-size: 0.9rem; - } + .order-id { + font-size: 0.9rem; + } - .order-total { - font-size: 1rem !important; - } + .order-total { + font-size: 1rem !important; + } - .address-text { - font-size: 0.85rem; - } + .address-text { + font-size: 0.85rem; + } } @media (max-width: 600px) { - .history-container { - padding: 1rem; - padding-top: calc(60px + 1rem); - } + .history-container { + padding: 1rem; + padding-top: calc(60px + 1rem); + } - .stats-grid { - grid-template-columns: 1fr; - } + .stats-grid { + grid-template-columns: 1fr; + } - .table-wrapper { - border-radius: 8px; - } + .table-wrapper { + border-radius: 8px; + } - .history-table { - min-width: 700px; - font-size: 0.8rem; - } + .history-table { + min-width: 700px; + font-size: 0.8rem; + } - .history-table th, - .history-table td { - padding: 0.875rem 0.625rem; - } + .history-table th, + .history-table td { + padding: 0.875rem 0.625rem; + } - .stat-value { - font-size: 1.5rem; - } + .stat-value { + font-size: 1.5rem; + } - .empty-history { - padding: 3rem 1.5rem; - } + .empty-history { + padding: 3rem 1.5rem; + } } /* ============================================ @@ -638,25 +640,25 @@ ============================================ */ @media (hover: hover) { - .clickable-row { - position: relative; - } + .clickable-row { + position: relative; + } - .clickable-row::before { - content: '→'; - position: absolute; - right: 1rem; - color: #a78bfa; - font-size: 1.5rem; - opacity: 0; - transform: translateX(-10px); - transition: all 0.2s ease; - } + .clickable-row::before { + content: "→"; + position: absolute; + right: 1rem; + color: #a78bfa; + font-size: 1.5rem; + opacity: 0; + transform: translateX(-10px); + transition: all 0.2s ease; + } - .clickable-row:hover::before { - opacity: 1; - transform: translateX(0); - } + .clickable-row:hover::before { + opacity: 1; + transform: translateX(0); + } } /* ============================================ @@ -664,24 +666,24 @@ ============================================ */ @media print { - .history-container { - background: white; - padding: 1rem; - } + .history-container { + background: white; + padding: 1rem; + } - .stats-grid, - .browse-button, - .status-badge { - display: none; - } + .stats-grid, + .browse-button, + .status-badge { + display: none; + } - .history-table { - border: 1px solid #000; - } + .history-table { + border: 1px solid #000; + } - .history-table th, - .history-table td { - color: #000; - border: 1px solid #ccc; - } -} \ No newline at end of file + .history-table th, + .history-table td { + color: #000; + border: 1px solid #ccc; + } +} diff --git a/frontend-prep/src/pages/User/ConsultationHistorique.tsx b/frontend-prep/src/pages/User/ConsultationHistorique.tsx index 041737bc..e9b80f8e 100644 --- a/frontend-prep/src/pages/User/ConsultationHistorique.tsx +++ b/frontend-prep/src/pages/User/ConsultationHistorique.tsx @@ -25,7 +25,10 @@ import { Package, MapPin, User, TrendingUp } from 'lucide-react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCannabis, - faWind, + faPills, + faFlask, + faMortarPestle, + faStar, faTrophy, faExclamationTriangle, faCheckCircle, @@ -38,7 +41,7 @@ function ConsultationHistorique() { const [orders, setOrders] = useState([]); const [clientStats, setClientStats] = useState(null); const [penalties, setPenalties] = useState(null); - const [appSettings, setAppSettings] = useState({ penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: true }); + const [appSettings, setAppSettings] = useState({ penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: true, pool_names: ['Pool 1', 'Pool 2'], crypto_payment_enabled: false, crypto_only: false, nowpayments_currencies: [] }); const [referralBalance, setReferralBalance] = useState(0); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(''); @@ -98,7 +101,6 @@ function ConsultationHistorique() { 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); @@ -190,36 +192,48 @@ function ConsultationHistorique() {
{/* Cartes points - affichées uniquement si le système de points est activé */} - {appSettings.points_enabled && ( - appSettings.points_separated ? ( + {appSettings.points_enabled && (() => { + const poolNames = clientStats.pool_names?.length ? clientStats.pool_names : appSettings.pool_names; + const poolPoints = clientStats.pool_points ?? [clientStats.points]; + const poolIcons = [faCannabis, faPills, faFlask, faMortarPestle, faStar]; + const poolClasses = poolNames.map((_, i) => `points-pool-${i}`); + const poolIconClasses = poolNames.map((_, i) => `icon-pool-${i}`); + + if (poolNames.length <= 1) { + return ( +
+
+ +
+
+

+ + Points {poolNames[0] ?? 'Points'} +

+

{poolPoints[0] || 0}

+
+
+ ); + } + + const total = poolPoints.reduce((s, v) => s + (v || 0), 0); + return ( <> -
-
- + {poolNames.map((name, i) => ( +
+
+ +
+
+

+ + Points {name} +

+

{poolPoints[i] || 0}

+
-
-

- - Points Weed/Hash -

-

{clientStats.points || 0}

-
-
- -
-
- -
-
-

- - Points Zipette -

-

{clientStats.points_zipette || 0}

-
-
- - {(clientStats.points || 0) > 0 && (clientStats.points_zipette || 0) > 0 && ( + ))} + {total > 0 && poolNames.length > 1 && (
@@ -229,28 +243,13 @@ function ConsultationHistorique() { Total Points

-

- {(clientStats.points || 0) + (clientStats.points_zipette || 0)} -

+

{total}

)} - ) : ( -
-
- -
-
-

- - Points -

-

{clientStats.points || 0}

-
-
- ) - )} + ); + })()} {/* Carte 5: Commandes Livrées */}
diff --git a/frontend-prep/src/pages/User/Parrainage.tsx b/frontend-prep/src/pages/User/Parrainage.tsx index 4d65bcf2..5e963f48 100644 --- a/frontend-prep/src/pages/User/Parrainage.tsx +++ b/frontend-prep/src/pages/User/Parrainage.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import Navbar from '../../components/Navbar'; -import { getReferralBalance, isUserAuthenticated } from '../../api/api'; +import { getReferralBalance, isUserAuthenticated, getPublicSettings } from '../../api/api'; import './Parrainage.css'; const TELEGRAM_URL = 'https://t.me/'; @@ -11,25 +11,25 @@ const steps = [ num: 1, title: 'Parrainez un ami', desc: 'Recommandez nos services à un proche. Il doit nous contacter directement sur Telegram pour s\'inscrire.', - icon: '👥', + icon: 'fa-users', }, { num: 2, title: 'Il passe sa 1ère commande', desc: 'Une fois votre ami inscrit et sa première commande validée, signalez-le nous sur Telegram.', - icon: '✅', + icon: 'fa-circle-check', }, { num: 3, title: 'Nous créditons votre compte', desc: 'L\'admin vérifie et crédite manuellement votre solde de parrainage. Vous êtes notifié dès que c\'est fait.', - icon: '💰', + icon: 'fa-coins', }, { num: 4, title: 'Utilisez votre solde', desc: 'Au moment du checkout, choisissez d\'utiliser votre solde ou de le cumuler pour une prochaine commande.', - icon: '🛒', + icon: 'fa-cart-shopping', }, ]; @@ -43,9 +43,15 @@ export default function Parrainage() { navigate('/login/client', { replace: true }); return; } - getReferralBalance().then((res) => { - if (res.success) setBalance(res.balance); - setLoading(false); + getPublicSettings().then((s) => { + if (!s.referral_enabled) { + navigate('/user/accueil', { replace: true }); + return; + } + getReferralBalance().then((res) => { + if (res.success) setBalance(res.balance); + setLoading(false); + }); }); }, [navigate]); @@ -55,7 +61,7 @@ export default function Parrainage() {
{/* En-tête */}
-
🎁
+

Programme de Parrainage

Parrainez vos amis et cumulez du crédit sur votre compte @@ -85,7 +91,7 @@ export default function Parrainage() {

{steps.map((step) => (
-
{step.icon}
+
Étape {step.num}

{step.title}

{step.desc}

@@ -96,7 +102,7 @@ export default function Parrainage() { {/* Règle zone minimum */}
-
⚠️
+

Règle du minimum de zone

@@ -127,7 +133,7 @@ export default function Parrainage() { rel="noopener noreferrer" className="telegram-btn" > - + Contacter sur Telegram

diff --git a/frontend-prep/src/pages/User/ProductDetail.css b/frontend-prep/src/pages/User/ProductDetail.css index 0ce93905..4adaa910 100644 --- a/frontend-prep/src/pages/User/ProductDetail.css +++ b/frontend-prep/src/pages/User/ProductDetail.css @@ -291,7 +291,7 @@ .grams-dropdown option:checked { background-color: var(--cat-color, #7c3aed); background: var(--cat-color, #7c3aed); - color: white; + color: var(--cat-text-color, white); } .grams-dropdown option:focus { @@ -344,7 +344,7 @@ .add-to-cart-button { width: 100%; background: var(--cat-color, #7c3aed); - color: white; + color: var(--cat-text-color, white); border: none; border-radius: 12px; padding: clamp(1.2rem, 3vw, 1.5rem); diff --git a/frontend-prep/src/pages/User/ProductDetail.tsx b/frontend-prep/src/pages/User/ProductDetail.tsx index 711f58d9..74577881 100644 --- a/frontend-prep/src/pages/User/ProductDetail.tsx +++ b/frontend-prep/src/pages/User/ProductDetail.tsx @@ -2,7 +2,7 @@ import { useParams, useNavigate } from "react-router-dom"; import { useState, useEffect } from "react"; import { getProductById, getCategories, isUserAuthenticated } from "../../api/api"; import type { Product } from "../../api/api"; -import { useCart } from "../../context/CartContext"; +import { useCart } from "../../context/useCart"; import Navbar from "../../components/Navbar"; import Toast from "../../components/Toast"; import "./ProductDetail.css"; @@ -208,6 +208,13 @@ function ProductDetail() { }; const catColorRgb = hexToRgb(catColor); + // Texte contrasté (noir sur fond clair, blanc sur fond foncé) + const h = catColor.replace("#", ""); + const r = parseInt(h.slice(0, 2), 16); + const g = parseInt(h.slice(2, 4), 16); + const b = parseInt(h.slice(4, 6), 16); + const catTextColor = (r * 299 + g * 587 + b * 114) / 1000 > 128 ? "#000000" : "#ffffff"; + return ( <> @@ -224,7 +231,7 @@ function ProductDetail() {
+
+ + {/* Section adresse par défaut */} +
+

+ + Adresse par défaut +

+

+ Sera pré-remplie dans le formulaire de commande. Vous pourrez la modifier si vous n'êtes pas à cette adresse. +

+
+ + setDefaultAddress(e.target.value)} + placeholder="Numéro, rue, ville, code postal" + /> +
+
+ + {/* Section contact commande */} +
+

+ + Contact livraison +

+

+ Numéro utilisé par le livreur lors de la livraison. Peut être différent du numéro de votre compte. +

+
+ + setDefaultPhone(e.target.value)} + placeholder="+33 6 12 34 56 78" + /> +
+
+ + setSignalPseudo(e.target.value)} + placeholder="@votre.pseudo.signal" + /> +
+ +
+
+ + ); +} diff --git a/frontend-prep/src/pages/User/SuiviLivraison.css b/frontend-prep/src/pages/User/SuiviLivraison.css index fc15b45c..2b1d2242 100644 --- a/frontend-prep/src/pages/User/SuiviLivraison.css +++ b/frontend-prep/src/pages/User/SuiviLivraison.css @@ -1,76 +1,60 @@ /* ============================================ - SuiviLivraison.css - COMPLET AVEC SCROLL MODAL + SuiviLivraison.css — Redesign ============================================ */ -/* Container principal */ .suivi-container { width: 100%; min-height: 100vh; - padding: clamp(1rem, 3vw, 2rem); - padding-top: calc(60px + clamp(1rem, 3vw, 2rem)); - max-width: 1200px; + padding: 0 clamp(1rem, 4vw, 2rem) clamp(3rem, 6vw, 4rem); + padding-top: calc(60px + clamp(1.5rem, 4vw, 2.5rem)); + max-width: 860px; margin: 0 auto; - background: linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 100%); + background: #0d0d0d; color: white; } -/* Header */ +/* ===== HEADER ===== */ .suivi-header { - text-align: left; - margin-top: 50px; - animation: slideDown 0.5s ease-out; -} - -@keyframes slideDown { - from { - opacity: 0; - transform: translateY(-20px); - } - to { - opacity: 1; - transform: translateY(0); - } + margin-top: -65px; + margin-bottom: clamp(1.5rem, 4vw, 2.5rem); } .suivi-header h1 { - font-size: clamp(1.8rem, 5vw, 2.5rem); - background: #ffffff; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - margin-bottom: 0.5rem; - font-weight: bold; + font-size: clamp(1.6rem, 5vw, 2.2rem); + color: #fff; + margin: 0 0 0.3rem 0; letter-spacing: -0.5px; } .suivi-header p { - font-size: clamp(1rem, 3vw, 1.1em); - color: rgba(255, 255, 255, 0.6); + font-size: clamp(0.85rem, 2.5vw, 0.95rem); + color: rgba(255, 255, 255, 0.4); margin: 0; } -/* Error Banner */ +/* ===== ERROR BANNER ===== */ .error-banner { - background: rgba(239, 68, 68, 0.1); - border-left: 4px solid #ef4444; - padding: clamp(1rem, 3vw, 1.5rem); - border-radius: 8px; - margin-bottom: clamp(1rem, 3vw, 1.5rem); + background: rgba(239, 68, 68, 0.08); + border: 1px solid rgba(239, 68, 68, 0.3); + padding: 0.9rem 1.2rem; + border-radius: 12px; + margin-bottom: 1.5rem; display: flex; justify-content: space-between; align-items: center; - color: #f472b6; - animation: slideDown 0.3s ease-out; + color: #f87171; + font-size: 0.95rem; + gap: 1rem; } .error-banner button { background: none; border: none; - color: #f472b6; - font-size: 1.2em; + color: #f87171; + font-size: 1rem; cursor: pointer; - padding: 0; opacity: 0.7; + flex-shrink: 0; transition: opacity 0.2s; } @@ -78,23 +62,23 @@ opacity: 1; } -/* Loading State */ +/* ===== LOADING ===== */ .loading-state { display: flex; flex-direction: column; align-items: center; justify-content: center; - min-height: 400px; + min-height: 50vh; gap: 1.5rem; } .spinner { - width: 50px; - height: 50px; - border: 4px solid rgba(16, 185, 129, 0.2); - border-top: 4px solid #10b981; + width: 44px; + height: 44px; + border: 3px solid rgba(124, 58, 237, 0.15); + border-top: 3px solid #7c3aed; border-radius: 50%; - animation: spin 1s linear infinite; + animation: spin 0.9s linear infinite; } @keyframes spin { @@ -104,82 +88,81 @@ } .loading-state p { - color: rgba(255, 255, 255, 0.6); - font-size: clamp(1rem, 3vw, 1.1em); + color: rgba(255, 255, 255, 0.45); + font-size: 0.95rem; } -/* Empty State */ +/* ===== EMPTY STATE ===== */ .empty-state { text-align: center; - padding: clamp(2rem, 6vw, 4rem); - background: linear-gradient( - 135deg, - rgba(255, 255, 255, 0.03) 0%, - rgba(255, 255, 255, 0.01) 100% - ); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 16px; - animation: slideDown 0.5s ease-out; + padding: clamp(3rem, 8vw, 5rem) 2rem; + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 20px; + background: rgba(255, 255, 255, 0.02); +} + +.empty-state-icon { + font-size: 3.5rem; + color: rgba(255, 255, 255, 0.15); + margin-bottom: 1.5rem; } .empty-state h2 { - font-size: clamp(1.4rem, 5vw, 1.8em); - color: rgba(255, 255, 255, 0.9); - margin-bottom: 0.5rem; + font-size: 1.3rem; + font-weight: 700; + color: rgba(255, 255, 255, 0.8); + margin: 0 0 0.5rem 0; } .empty-state p { - color: rgba(255, 255, 255, 0.6); - font-size: clamp(1rem, 3vw, 1.05em); - margin-bottom: clamp(1rem, 3vw, 1.5rem); + color: rgba(255, 255, 255, 0.4); + font-size: 0.95rem; + margin: 0 0 2rem 0; } .action-button { - background: linear-gradient(to right, #7c3aed, #6d28d9); + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: #7c3aed; color: white; border: none; - padding: clamp(0.75rem, 2vw, 1rem) clamp(1.5rem, 3vw, 2rem); + padding: 0.8rem 1.8rem; border-radius: 12px; - font-size: clamp(0.95rem, 3vw, 1.05em); + font-size: 0.95rem; font-weight: 700; cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - box-shadow: 0 0 30px rgba(124, 58, 237, 0.3); + transition: all 0.25s ease; } .action-button:hover { + background: #6d28d9; transform: translateY(-2px); - box-shadow: 0 12px 32px rgba(124, 58, 237, 0.3); } -/* Orders List */ +/* ===== ORDERS LIST ===== */ .orders-list { display: flex; flex-direction: column; - gap: clamp(1.5rem, 4vw, 2rem); - margin-bottom: clamp(1.5rem, 4vw, 2rem); + gap: 1rem; } -/* Order Card */ +/* ===== ORDER CARD ===== */ .order-card { - background: linear-gradient( - 135deg, - rgba(255, 255, 255, 0.03) 0%, - rgba(255, 255, 255, 0.01) 100% - ); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 16px; + background: #161616; + border: 1px solid rgba(255, 255, 255, 0.07); + border-radius: 18px; overflow: hidden; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - animation: slideUp 0.5s ease-out; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); - backdrop-filter: blur(8px); + transition: + border-color 0.25s ease, + box-shadow 0.25s ease; + animation: fadeUp 0.4s ease both; } -@keyframes slideUp { +@keyframes fadeUp { from { opacity: 0; - transform: translateY(20px); + transform: translateY(14px); } to { opacity: 1; @@ -188,235 +171,491 @@ } .order-card:hover { - border-color: rgba(16, 185, 129, 0.3); - box-shadow: 0 12px 32px rgba(16, 185, 129, 0.15); + border-color: rgba(124, 58, 237, 0.25); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); } -/* Order Header */ +/* ===== CARD TOP STRIP (status color) ===== */ +.order-status-strip { + height: 4px; + width: 100%; + border-radius: 18px 18px 0 0; + transition: background-image 0.3s ease; +} + +/* ===== ORDER HEADER (clickable) ===== */ .order-header { display: flex; justify-content: space-between; align-items: center; - padding: clamp(1rem, 3vw, 1.5rem); - background: linear-gradient( - 135deg, - rgba(255, 255, 255, 0.03) 0%, - rgba(255, 255, 255, 0.01) 100% - ); + padding: 1.2rem 1.4rem; cursor: pointer; user-select: none; - transition: all 0.2s ease; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); + gap: 1rem; + transition: background 0.2s ease; } .order-header:hover { - background: linear-gradient( - 135deg, - rgba(255, 255, 255, 0.05) 0%, - rgba(255, 255, 255, 0.02) 100% - ); + background: rgba(255, 255, 255, 0.02); } .order-header-left { display: flex; - align-items: center; - gap: clamp(1rem, 3vw, 1.5rem); + flex-direction: column; + gap: 0.5rem; flex: 1; + min-width: 0; +} + +.order-id-row { + display: flex; + align-items: center; + gap: 0.75rem; flex-wrap: wrap; } -.order-header-left h3 { - margin: 0; - font-size: clamp(1.1rem, 4vw, 1.3em); - color: white; - font-weight: 600; +.order-id { + font-size: 1rem; + font-weight: 700; + color: #fff; } -/* Status Badge */ .status-badge { - display: inline-block; - padding: 0.4rem 0.8rem; + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.28rem 0.7rem; border-radius: 20px; - color: white; - font-size: clamp(0.8rem, 2vw, 0.9em); - font-weight: 600; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + color: #fff; + font-size: 0.78rem; + font-weight: 700; white-space: nowrap; + letter-spacing: 0.2px; } .eta-badge { display: inline-flex; align-items: center; gap: 0.4rem; - padding: 0.35rem 0.75rem; + padding: 0.28rem 0.7rem; border-radius: 20px; - background: linear-gradient(135deg, #065f46 0%, #059669 100%); + background: linear-gradient(135deg, #065f46, #059669); color: #d1fae5; - font-size: clamp(0.75rem, 2vw, 0.85em); + font-size: 0.78rem; font-weight: 700; white-space: nowrap; - box-shadow: 0 2px 8px rgba(5, 150, 105, 0.4); animation: eta-pulse 2s ease-in-out infinite; } -@keyframes eta-pulse { - 0%, 100% { box-shadow: 0 2px 8px rgba(5, 150, 105, 0.4); } - 50% { box-shadow: 0 2px 16px rgba(5, 150, 105, 0.7); } -} - .eta-badge--preRoute { - background: linear-gradient(135deg, #78350f 0%, #d97706 100%); + background: linear-gradient(135deg, #78350f, #d97706); color: #fef3c7; - box-shadow: 0 2px 8px rgba(217, 119, 6, 0.4); animation: eta-pulse-amber 2s ease-in-out infinite; } +.eta-badge--enRoute { + background: linear-gradient(135deg, #1e3a5f, #2563eb); + color: #bfdbfe; + font-size: 0.82rem; + padding: 0.3rem 0.85rem; + animation: eta-pulse-blue 2s ease-in-out infinite; +} + +@keyframes eta-pulse-blue { + 0%, 100% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.45); } + 50% { box-shadow: 0 0 0 6px rgba(37, 99, 235, 0); } +} + +.eta-badge--arrived { + background: linear-gradient(135deg, #14532d, #16a34a); + color: #bbf7d0; + font-size: 0.82rem; + padding: 0.3rem 0.85rem; + animation: eta-pulse-green 2s ease-in-out infinite; +} + +@keyframes eta-pulse-green { + 0%, 100% { box-shadow: 0 0 0 0 rgba(22, 163, 74, 0.45); } + 50% { box-shadow: 0 0 0 6px rgba(22, 163, 74, 0); } +} + +@keyframes eta-pulse { + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(5, 150, 105, 0.4); + } + 50% { + box-shadow: 0 0 0 6px rgba(5, 150, 105, 0); + } +} @keyframes eta-pulse-amber { - 0%, 100% { box-shadow: 0 2px 8px rgba(217, 119, 6, 0.4); } - 50% { box-shadow: 0 2px 16px rgba(217, 119, 6, 0.7); } + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(217, 119, 6, 0.4); + } + 50% { + box-shadow: 0 0 0 6px rgba(217, 119, 6, 0); + } +} + +.order-date { + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.35); } .order-header-right { display: flex; align-items: center; - gap: clamp(1rem, 3vw, 1.5rem); + gap: 0.9rem; flex-shrink: 0; } .order-total2 { - font-size: clamp(1.1rem, 4vw, 1.3em); - font-weight: 700; - color: #6d28d9; + font-size: 1.1rem; + font-weight: 800; + color: #10b981; } -.expand-icon { - color: rgba(255, 255, 255, 0.6); - font-size: 1.2em; - transition: color 0.2s ease; -} - -.order-header:hover .expand-icon { - color: #6d28d9; -} - -/* Order Details */ -.order-details { - padding: clamp(1.5rem, 4vw, 2rem); - border-top: 1px solid rgba(255, 255, 255, 0.05); +.expand-btn { display: flex; - flex-direction: column; - gap: clamp(1rem, 3vw, 1.5rem); + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.4); + font-size: 0.75rem; + transition: all 0.2s ease; +} + +.order-header:hover .expand-btn { + background: rgba(124, 58, 237, 0.2); + color: #a78bfa; +} + +/* ===== EXPANDED DETAILS ===== */ +.order-details { + border-top: 1px solid rgba(255, 255, 255, 0.05); animation: expandDown 0.3s ease-out; + overflow: hidden; } @keyframes expandDown { from { opacity: 0; - max-height: 0; } to { opacity: 1; - max-height: 2000px; } } +/* ===== TIMELINE ===== */ +.status-timeline { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 1.4rem 1.4rem 1rem; + position: relative; + overflow-x: auto; + gap: 0; + scrollbar-width: none; +} + +.status-timeline::-webkit-scrollbar { + display: none; +} + +.timeline-step { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + min-width: 52px; + position: relative; + z-index: 1; +} + +.timeline-step::before { + content: ""; + position: absolute; + top: 14px; + left: calc(-50% + 14px); + right: calc(50% + 14px); + height: 2px; + background: rgba(255, 255, 255, 0.08); + z-index: 0; +} + +.timeline-step:first-child::before { + display: none; +} + +.timeline-step.active::before, +.timeline-step.done::before { + background: rgba(124, 58, 237, 0.5); +} + +.timeline-dot { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.7rem; + border: 2px solid rgba(255, 255, 255, 0.1); + background: #1e1e1e; + color: rgba(255, 255, 255, 0.3); + transition: all 0.3s ease; + z-index: 1; + position: relative; +} + +.timeline-step.done .timeline-dot { + background: rgba(124, 58, 237, 0.3); + border-color: #7c3aed; + color: #a78bfa; +} + +.timeline-step.active .timeline-dot { + background: #7c3aed; + border-color: #a78bfa; + color: #fff; + box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.2); +} + +.timeline-label { + font-size: 0.62rem; + color: rgba(255, 255, 255, 0.3); + margin-top: 0.4rem; + text-align: center; + line-height: 1.3; + font-weight: 500; +} + +.timeline-step.active .timeline-label { + color: #a78bfa; + font-weight: 700; +} + +.timeline-step.done .timeline-label { + color: rgba(255, 255, 255, 0.5); +} + +/* ===== DETAIL SECTIONS ===== */ +.details-grid { + display: flex; + flex-direction: column; + gap: 0; +} + .detail-section { - padding: clamp(1rem, 3vw, 1.5rem); - background: rgba(255, 255, 255, 0.03); - border-left: 3px solid #6d28d9; - border-radius: 8px; - width: 100%; + padding: 1rem 1.4rem; + border-top: 1px solid rgba(255, 255, 255, 0.04); +} + +.detail-section:first-child { + border-top: none; } .detail-section h4 { - margin: 0 0 0.75rem 0; - color: rgba(255, 255, 255, 0.9); - font-size: clamp(0.95rem, 2vw, 1.05em); + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0 0 0.6rem 0; + font-size: 0.72rem; text-transform: uppercase; - letter-spacing: 0.5px; - font-weight: 600; + letter-spacing: 0.8px; + color: rgba(255, 255, 255, 0.35); + font-weight: 700; } .detail-section p { - margin: 0.5rem 0; - color: rgba(255, 255, 255, 0.8); + margin: 0; + color: rgba(255, 255, 255, 0.85); + font-size: 0.95rem; line-height: 1.5; } .detail-section small { display: block; - color: rgba(255, 255, 255, 0.5); - margin-top: 0.5rem; - font-size: clamp(0.8rem, 2vw, 0.85em); + margin-top: 0.35rem; + color: rgba(255, 255, 255, 0.35); + font-size: 0.8rem; } .address { - font-size: clamp(1rem, 3vw, 1.05em); - font-weight: 500; - color: white; + font-weight: 600; + color: #fff; } - .contact { - color: rgba(255, 255, 255, 0.7); - font-size: clamp(0.9rem, 2vw, 0.95em); + color: rgba(255, 255, 255, 0.5); + font-size: 0.85rem; + margin-top: 0.25rem; } -/* Items List */ +/* ===== ETA CARD (en_route / assigned) ===== */ +.eta-card { + margin: 0 1.4rem 0.5rem; + padding: 1rem 1.2rem; + border-radius: 14px; + background: linear-gradient( + 135deg, + rgba(5, 150, 105, 0.12), + rgba(16, 185, 129, 0.06) + ); + border: 1px solid rgba(16, 185, 129, 0.2); + display: flex; + align-items: center; + gap: 1rem; +} + +.eta-card--preRoute { + background: linear-gradient( + 135deg, + rgba(217, 119, 6, 0.12), + rgba(251, 191, 36, 0.06) + ); + border-color: rgba(217, 119, 6, 0.25); +} + +.eta-card--enRoute { + background: linear-gradient( + 135deg, + rgba(37, 99, 235, 0.14), + rgba(96, 165, 250, 0.06) + ); + border-color: rgba(59, 130, 246, 0.3); +} + +.eta-card--enRoute .eta-card-icon { + color: #60a5fa; +} + +.eta-card--enRoute .eta-card-value { + color: #60a5fa; + font-size: 2rem; +} + +.eta-card--arrived { + background: linear-gradient( + 135deg, + rgba(22, 163, 74, 0.14), + rgba(74, 222, 128, 0.06) + ); + border-color: rgba(34, 197, 94, 0.3); +} + +.eta-card--arrived .eta-card-icon { + color: #4ade80; +} + +.eta-card--arrived .eta-card-value { + color: #4ade80; + font-size: 2rem; +} + +.eta-card-icon { + font-size: 1.6rem; + color: #10b981; + flex-shrink: 0; +} + +.eta-card--preRoute .eta-card-icon { + color: #f59e0b; +} + +.eta-card-body { + flex: 1; +} + +.eta-card-title { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.7px; + color: rgba(255, 255, 255, 0.4); + font-weight: 700; + margin: 0 0 0.2rem 0; +} + +.eta-card-value { + font-size: 1.5rem; + font-weight: 800; + color: #10b981; + line-height: 1; + margin: 0; +} + +.eta-card--preRoute .eta-card-value { + color: #f59e0b; +} + +.eta-card-sub { + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.4); + margin: 0.2rem 0 0 0; +} + +/* ===== ITEMS LIST ===== */ .items-list { display: flex; flex-direction: column; - gap: 0.5rem; + gap: 0.4rem; } .item { display: flex; justify-content: space-between; - padding: clamp(0.75rem, 2vw, 1rem); - background: rgba(255, 255, 255, 0.02); - border-radius: 8px; - color: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(255, 255, 255, 0.05); - transition: all 0.2s ease; -} - -.item:hover { - background: rgba(255, 255, 255, 0.04); + align-items: center; + padding: 0.6rem 0.9rem; + background: rgba(255, 255, 255, 0.025); + border-radius: 9px; + border: 1px solid rgba(255, 255, 255, 0.04); } .item-name { - flex: 1; + color: rgba(255, 255, 255, 0.8); + font-size: 0.9rem; } .item .price { color: #10b981; - font-weight: 600; - margin-left: 1rem; + font-weight: 700; + font-size: 0.9rem; + flex-shrink: 0; + margin-left: 0.75rem; } .total-amount { - font-size: 1.5rem; + font-size: 1.4rem; + font-weight: 800; color: #10b981; - font-weight: 700; margin: 0; } -/* Order Actions */ +/* ===== ACTIONS ===== */ .order-actions { - padding-top: 1rem; + padding: 1.2rem 1.4rem; border-top: 1px solid rgba(255, 255, 255, 0.05); + background: rgba(0, 0, 0, 0.15); } .action-buttons { display: flex; - gap: 1rem; + gap: 0.75rem; flex-wrap: wrap; } .confirmation-section { - background: #6d28d9; - padding: clamp(1rem, 3vw, 1.5rem); - border-radius: 12px; - border: 1px solid rgba(16, 185, 129, 0.2); + background: linear-gradient( + 135deg, + rgba(124, 58, 237, 0.15), + rgba(109, 40, 217, 0.08) + ); + border: 1px solid rgba(124, 58, 237, 0.25); + padding: 1.2rem; + border-radius: 14px; display: flex; flex-direction: column; gap: 1rem; @@ -427,57 +666,47 @@ } .notice-title { - font-size: clamp(1rem, 3vw, 1.1em); - font-weight: 600; - color: rgba(255, 255, 255, 0.9); - margin: 0 0 0.5rem 0; + font-size: 1rem; + font-weight: 700; + color: #fff; + margin: 0 0 0.3rem 0; + display: flex; + align-items: center; + justify-content: center; + gap: 0.4rem; } .notice-subtitle { - font-size: clamp(0.85rem, 2vw, 0.95em); - color: rgba(255, 255, 255, 0.7); + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.5); margin: 0; } -.btn-confirm-delivery { - padding: clamp(0.75rem, 2vw, 1rem); - border: none; - border-radius: 10px; - font-size: clamp(0.9rem, 2vw, 1em); - font-weight: 600; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - font-family: inherit; - text-transform: uppercase; - letter-spacing: 0.5px; - touch-action: manipulation; - -webkit-tap-highlight-color: transparent; - flex: 1; - min-width: 200px; -} - -/* Buttons */ +/* ===== BUTTONS ===== */ +.btn-confirm-delivery, .btn-primary, .btn-secondary, .btn-confirm, .btn-cancel, .btn-danger, .btn-cancel-order { - padding: clamp(0.75rem, 2vw, 1rem); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 1.4rem; border: none; - border-radius: 10px; - font-size: clamp(0.9rem, 2vw, 1em); - font-weight: 600; + border-radius: 11px; + font-size: 0.9rem; + font-weight: 700; cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + transition: all 0.25s ease; font-family: inherit; - text-transform: uppercase; - letter-spacing: 0.5px; + white-space: nowrap; + flex: 1; + min-width: 140px; touch-action: manipulation; -webkit-tap-highlight-color: transparent; - flex: 1; - margin-left: 45px; - min-width: 200px; } .btn-confirm-delivery, @@ -485,109 +714,105 @@ .btn-confirm { background: #7c3aed; color: white; - box-shadow: 0 0 30px rgba(124, 58, 237, 0.5); + box-shadow: 0 4px 20px rgba(124, 58, 237, 0.35); } -.btn-confirm-delivery:hover, +.btn-confirm-delivery:hover:not(:disabled), .btn-primary:hover, .btn-confirm:hover { - transform: translateY(-2px); - box-shadow: 0 12px 32px rgba(124, 58, 237, 0.4); -} - -.btn-confirm-delivery:disabled, -.btn-cancel-order:disabled, -.btn-danger:disabled { - background: rgba(255, 255, 255, 0.1); - cursor: not-allowed; - box-shadow: none; - transform: none; - opacity: 0.5; + background: #6d28d9; + transform: translateY(-1px); + box-shadow: 0 8px 24px rgba(124, 58, 237, 0.45); } .btn-secondary, .btn-cancel { - background: rgba(255, 255, 255, 0.05); - color: rgba(255, 255, 255, 0.8); + background: rgba(255, 255, 255, 0.06); + color: rgba(255, 255, 255, 0.7); border: 1px solid rgba(255, 255, 255, 0.1); } .btn-secondary:hover, .btn-cancel:hover { - background: rgba(255, 255, 255, 0.08); - border-color: rgba(255, 255, 255, 0.2); - transform: translateY(-2px); + background: rgba(255, 255, 255, 0.09); + color: #fff; } .btn-danger, .btn-cancel-order { - background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + background: linear-gradient(135deg, #ef4444, #dc2626); color: white; - box-shadow: 0 0 30px rgba(239, 68, 68, 0.3); + box-shadow: 0 4px 20px rgba(239, 68, 68, 0.25); } -.btn-danger:hover, -.btn-cancel-order:hover { - transform: translateY(-2px); - box-shadow: 0 12px 32px rgba(239, 68, 68, 0.4); +.btn-danger:hover:not(:disabled), +.btn-cancel-order:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 8px 24px rgba(239, 68, 68, 0.35); } -/* Status Messages */ +.btn-confirm-delivery:disabled, +.btn-cancel-order:disabled, +.btn-danger:disabled { + background: rgba(255, 255, 255, 0.07); + cursor: not-allowed; + box-shadow: none; + opacity: 0.5; + transform: none; +} + +/* ===== STATUS MESSAGES ===== */ .status-message { - padding: 1rem; - border-radius: 8px; + padding: 0.9rem 1.2rem; + border-radius: 11px; text-align: center; - font-weight: 500; + font-weight: 600; + font-size: 0.9rem; display: flex; align-items: center; justify-content: center; - gap: 0.5rem; + gap: 0.6rem; } .status-message.success { - background: rgba(16, 185, 129, 0.1); + background: rgba(16, 185, 129, 0.08); color: #10b981; - border: 1px solid rgba(16, 185, 129, 0.3); + border: 1px solid rgba(16, 185, 129, 0.2); } .status-message.cancelled { - background: rgba(239, 68, 68, 0.1); + background: rgba(239, 68, 68, 0.08); color: #f87171; - border: 1px solid rgba(239, 68, 68, 0.3); + border: 1px solid rgba(239, 68, 68, 0.2); } -/* Footer Info */ +/* ===== FOOTER INFO ===== */ .footer-info { + margin-top: 2rem; text-align: center; - padding: clamp(1rem, 3vw, 1.5rem); + padding: 1rem; background: rgba(255, 255, 255, 0.02); - border: 1px solid rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 12px; - color: rgba(255, 255, 255, 0.5); - font-size: clamp(0.9rem, 2vw, 1em); + color: rgba(255, 255, 255, 0.3); + font-size: 0.8rem; } .footer-info p { - margin: 0.5rem 0; + margin: 0.3rem 0; } -/* ============================================ - ✅ DIALOG DE CONFIRMATION - AVEC SCROLL - ============================================ */ - +/* ===== DIALOGS ===== */ .confirm-dialog-overlay { position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.8); - backdrop-filter: blur(8px); + inset: 0; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(10px); display: flex; align-items: center; justify-content: center; z-index: 9998; - animation: fadeIn 0.3s ease-out; + animation: fadeIn 0.25s ease-out; padding: 1rem; } @@ -601,128 +826,113 @@ } .confirm-dialog { - background: linear-gradient( - 135deg, - rgba(255, 255, 255, 0.1) 0%, - rgba(255, 255, 255, 0.05) 100% - ); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 16px; + background: #1a1a1a; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 20px; max-width: 480px; width: 90%; max-height: 85vh; display: flex; flex-direction: column; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); - animation: slideUp 0.3s ease-out; - backdrop-filter: blur(20px); + box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6); + animation: scaleIn 0.25s ease-out; overflow: hidden; } +@keyframes scaleIn { + from { + transform: scale(0.94); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + .cancel-dialog { - max-width: 600px; + max-width: 560px; } .confirm-dialog-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); + padding: 1.4rem 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.07); flex-shrink: 0; - background: linear-gradient( - 135deg, - rgba(255, 255, 255, 0.08) 0%, - rgba(255, 255, 255, 0.03) 100% - ); } .confirm-dialog-header h3 { margin: 0; - font-size: 1.3em; + font-size: 1.1rem; color: white; - font-weight: 600; + font-weight: 700; + display: flex; + align-items: center; + gap: 0.5rem; } .confirm-dialog-body { padding: 1.5rem; - color: rgba(255, 255, 255, 0.8); + color: rgba(255, 255, 255, 0.75); overflow-y: auto; flex: 1; - /* Smooth scrolling */ -webkit-overflow-scrolling: touch; -} - -/* Custom scrollbar pour webkit browsers */ -.confirm-dialog-body::-webkit-scrollbar { - width: 8px; -} - -.confirm-dialog-body::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); - border-radius: 4px; -} - -.confirm-dialog-body::-webkit-scrollbar-thumb { - background: rgba(124, 58, 237, 0.5); - border-radius: 4px; -} - -.confirm-dialog-body::-webkit-scrollbar-thumb:hover { - background: rgba(124, 58, 237, 0.7); -} - -/* Pour Firefox */ -.confirm-dialog-body { scrollbar-width: thin; - scrollbar-color: rgba(124, 58, 237, 0.5) rgba(255, 255, 255, 0.05); + scrollbar-color: rgba(124, 58, 237, 0.4) transparent; +} + +.confirm-dialog-body::-webkit-scrollbar { + width: 6px; +} +.confirm-dialog-body::-webkit-scrollbar-thumb { + background: rgba(124, 58, 237, 0.4); + border-radius: 3px; } .confirm-dialog-body p { margin: 0 0 1rem 0; - font-size: 1.05em; - line-height: 1.5; + font-size: 0.95rem; + line-height: 1.6; } .confirm-dialog-reward { - background: rgba(124, 58, 237, 0.3); - border: 1px solid rgba(124, 58, 237, 0.3); - border-radius: 8px; - padding: 1rem; + background: rgba(124, 58, 237, 0.12); + border: 1px solid rgba(124, 58, 237, 0.25); + border-radius: 12px; + padding: 1rem 1.2rem; text-align: center; - font-size: 1.1em; + font-size: 1rem; color: #a78bfa; + display: flex; + align-items: center; + justify-content: center; + gap: 0.6rem; } .confirm-dialog-reward strong { - color: white; + color: #fff; } -/* ✅ STYLE POUR LE MESSAGE DES 20 POINTS */ .points-info { - margin-top: 1rem; + margin-top: 0.75rem; } .points-info p { - font-size: 0.9em; - color: rgba(255, 255, 255, 0.7); + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.5); margin: 0; - padding: 0.75rem; - background: rgba(124, 58, 237, 0.1); - border-radius: 6px; - border: 1px solid rgba(124, 58, 237, 0.2); + padding: 0.65rem 0.9rem; + background: rgba(124, 58, 237, 0.06); + border-radius: 8px; text-align: center; line-height: 1.5; } .confirm-dialog-actions { - padding: 1rem 1.5rem 1.5rem; + padding: 1rem 1.5rem 1.4rem; display: flex; - gap: 1rem; + gap: 0.75rem; flex-shrink: 0; - border-top: 1px solid rgba(255, 255, 255, 0.05); - background: linear-gradient( - 135deg, - rgba(255, 255, 255, 0.03) 0%, - rgba(255, 255, 255, 0.01) 100% - ); + border-top: 1px solid rgba(255, 255, 255, 0.06); } .confirm-dialog-actions .btn-cancel, @@ -730,14 +940,10 @@ .confirm-dialog-actions .btn-danger, .confirm-dialog-actions .btn-secondary { flex: 1; - margin: 0; min-width: auto; } -/* ============================================ - ✅ ANNULATION - FORMULAIRE & PÉNALITÉ - ============================================ */ - +/* ===== CANCEL FORM ===== */ .cancel-form { display: flex; flex-direction: column; @@ -747,47 +953,51 @@ .cancel-form .form-group { display: flex; flex-direction: column; - gap: 0.5rem; + gap: 0.4rem; } .cancel-form label { font-weight: 600; - color: rgba(255, 255, 255, 0.9); - font-size: 0.95em; + color: rgba(255, 255, 255, 0.8); + font-size: 0.9rem; } .cancel-form textarea { - background: rgba(255, 255, 255, 0.05); + background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 8px; + border-radius: 10px; padding: 0.75rem; color: white; font-family: inherit; resize: vertical; - min-height: 100px; - font-size: 0.95em; + min-height: 90px; + font-size: 0.9rem; + transition: border-color 0.2s; } .cancel-form textarea:focus { outline: none; border-color: #7c3aed; - box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2); + box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.15); } .cancel-form small { - color: rgba(255, 255, 255, 0.5); - font-size: 0.85em; + color: rgba(255, 255, 255, 0.35); + font-size: 0.8rem; text-align: right; } .info-message { - background: rgba(124, 58, 237, 0.1); - border: 1px solid rgba(124, 58, 237, 0.3); - border-radius: 8px; - padding: 1rem; + background: rgba(124, 58, 237, 0.08); + border: 1px solid rgba(124, 58, 237, 0.2); + border-radius: 10px; + padding: 0.9rem 1rem; display: flex; - gap: 0.75rem; + gap: 0.6rem; align-items: flex-start; + font-size: 0.88rem; + line-height: 1.5; + color: rgba(255, 255, 255, 0.6); } .info-message svg { @@ -795,13 +1005,11 @@ flex-shrink: 0; margin-top: 2px; } - .info-message span { - line-height: 1.5; - font-size: 0.95em; + flex: 1; } -/* Penalty Warning */ +/* ===== PENALTY ===== */ .penalty-warning { display: flex; flex-direction: column; @@ -810,7 +1018,7 @@ .warning-icon { text-align: center; - font-size: 3em; + font-size: 2.5rem; color: #fbbf24; animation: pulse 2s infinite; } @@ -821,111 +1029,105 @@ opacity: 1; } 50% { - opacity: 0.6; + opacity: 0.55; } } .warning-title { - font-size: 1.1em; - font-weight: 600; + font-size: 1rem; + font-weight: 700; color: #fbbf24; text-align: center; } .warning-details { - background: rgba(255, 255, 255, 0.05); - padding: 1rem; - border-radius: 8px; - border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.04); + padding: 0.9rem; + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.08); } .warning-details p { - margin: 0.5rem 0; - color: rgba(255, 255, 255, 0.8); + margin: 0.4rem 0; + color: rgba(255, 255, 255, 0.7); + font-size: 0.9rem; } .penalty-info { - background: rgba(239, 68, 68, 0.1); - border: 1px solid rgba(239, 68, 68, 0.3); - border-radius: 8px; + background: rgba(239, 68, 68, 0.08); + border: 1px solid rgba(239, 68, 68, 0.2); + border-radius: 10px; padding: 1rem; } .penalty-amount { - font-size: 1.2em; + font-size: 1.1rem; color: #ef4444; text-align: center; margin-bottom: 0.5rem; } .penalty-message { - color: rgba(255, 255, 255, 0.8); + color: rgba(255, 255, 255, 0.65); text-align: center; margin-bottom: 1rem; - font-size: 0.95em; + font-size: 0.9rem; } .penalty-scale { background: rgba(255, 255, 255, 0.03); - padding: 1rem; - border-radius: 6px; + padding: 0.9rem; + border-radius: 8px; } .penalty-scale p { margin-bottom: 0.5rem; font-weight: 600; - font-size: 0.95em; + font-size: 0.9rem; } - .penalty-scale ul { list-style: none; padding: 0; margin: 0; } - .penalty-scale li { - padding: 0.5rem 0; + padding: 0.4rem 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05); - font-size: 0.9em; + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.65); } - .penalty-scale li:last-child { border-bottom: none; } .warning-question { - font-size: 1.05em; - font-weight: 600; - color: rgba(255, 255, 255, 0.9); + font-size: 0.95rem; + font-weight: 700; + color: rgba(255, 255, 255, 0.85); text-align: center; - margin-top: 1rem; + margin-top: 0.5rem; } -/* Responsive */ -@media (max-width: 768px) { - .suivi-container { - padding: clamp(1rem, 2vw, 1.5rem); - } - +/* ===== RESPONSIVE ===== */ +@media (max-width: 600px) { .order-header { - flex-direction: column; - align-items: flex-start; - gap: 1rem; + padding: 1rem; + } + .detail-section { + padding: 0.9rem 1rem; + } + .status-timeline { + padding: 1.2rem 1rem 0.8rem; + } + .order-actions { + padding: 1rem; + } + .eta-card { + margin: 0 1rem 0.5rem; } - .order-header-right { - width: 100%; - justify-content: space-between; - } - - .confirm-dialog, - .cancel-dialog { - width: 95%; - max-height: 90vh; - } - - .confirm-dialog-actions, - .action-buttons { + .action-buttons, + .confirm-dialog-actions { flex-direction: column; } @@ -936,70 +1138,25 @@ .btn-cancel, .btn-danger, .btn-cancel-order { - min-width: auto; width: 100%; - } -} - -@media (max-width: 480px) { - .suivi-container { - padding: 1rem; + min-width: auto; } - .order-header { - padding: 1rem; - } - - .order-details { - padding: 1rem; - } - - .penalty-scale { - font-size: 0.85em; - } - - .confirm-dialog { - max-height: 95vh; - } - - .confirm-dialog-overlay { - padding: 0.5rem; + .confirm-dialog, + .cancel-dialog { + width: 96%; + max-height: 92vh; } } @media (hover: none) { - .order-card:hover { - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); - } - - .btn-cancel:hover, - .btn-confirm:hover, + .btn-confirm-delivery:hover:not(:disabled), .btn-primary:hover, + .btn-confirm:hover, + .btn-danger:hover:not(:disabled), + .btn-cancel-order:hover:not(:disabled), .btn-secondary:hover, - .btn-danger:hover, - .btn-cancel-order:hover { - transform: none; - } - .action-button:hover { transform: none; } } - -/* ✅ Barre de progression du statut */ -.status-progress { - width: 100%; - height: 4px; - background: rgba(0, 0, 0, 0.1); - border-radius: 2px; - margin-bottom: 1.5rem; - overflow: hidden; -} - -.status-progress-bar { - height: 100%; - transition: - width 0.5s ease, - background-image 0.3s ease; - border-radius: 2px; -} diff --git a/frontend-prep/src/pages/User/SuiviLivraison.tsx b/frontend-prep/src/pages/User/SuiviLivraison.tsx index 922c5637..1b38e74e 100644 --- a/frontend-prep/src/pages/User/SuiviLivraison.tsx +++ b/frontend-prep/src/pages/User/SuiviLivraison.tsx @@ -6,57 +6,60 @@ // ✅ 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'; +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { + getMyOrders, + getOrderTracking, + getOrderETA, + confirmReception, + cancelCommand, + isUserAuthenticated, + getPublicSettings, +} 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, + faChevronUp, + faChevronDown, +} from "@fortawesome/free-solid-svg-icons"; interface OrderWithTracking extends OrderDetail { - tracking?: TrackingResponse; - eta?: ETAResponse; + tracking?: TrackingResponse; + eta?: ETAResponse; } interface ToastMessage { - id: string; - message: string; - type: 'success' | 'error' | 'warning' | 'info'; + id: string; + message: string; + type: "success" | "error" | "warning" | "info"; } // ============================================ @@ -68,200 +71,200 @@ interface ToastMessage { * 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; + // 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 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'; + 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 - }; + 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 '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%)'; - } + 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 "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 = { - 'pending': 'En attente d\'assignation', - 'assigned': 'Livreur assigné', - '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 statusMap: Record = { + pending: "En attente d'assignation", + assigned: "Livreur assigné", + 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 = { - 'pending': faHourglassHalf, - 'assigned': faBiking, - 'en_route': faTruck, - 'arrived': faMapMarkerAlt, - 'livre': faBox, - 'approved': faCheckCircle, - 'cancelled': faTimesCircle - }; - - return iconMap[status?.toLowerCase()] || faQuestionCircle; + const iconMap: Record = { + pending: faHourglassHalf, + assigned: faBiking, + en_route: faTruck, + arrived: faMapMarkerAlt, + livre: faBox, + approved: faCheckCircle, + cancelled: faTimesCircle, + }; + + return iconMap[status?.toLowerCase()] || faQuestionCircle; }; -const getStatusProgress = (status: string): number => { - const progressMap: Record = { - 'pending': 0, - 'assigned': 10, - '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; +const calculateOrderPoints = ( + order: OrderWithTracking, + poolNames: string[] = [], +): { + 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' + // Totaux indexés par pool (+ index spécial pour "gros&semi" exclu des points) + const poolTotals: number[] = poolNames.map(() => 0); + let excludedTotal = 0; + + if (order.items && order.items.length > 0) { + order.items.forEach((item: any) => { + const cat = (item.category || "").toLowerCase(); + const itemPrice = item.prix || item.price || 0; + + if (cat.includes("gros") || cat.includes("semi")) { + excludedTotal += itemPrice; + return; + } + + // Trouver le pool correspondant par nom (insensible à la casse) + const poolIdx = poolNames.findIndex((name) => + cat.includes(name.toLowerCase().replace(/[&\s]/g, "")), + ); + if (poolIdx >= 0) { + poolTotals[poolIdx] += itemPrice; + } else if (poolTotals.length > 0) { + // Fallback : pool 0 si aucun match + poolTotals[0] += itemPrice; + } + }); + } + + // Gros&Semi uniquement → 0 points + if (excludedTotal > 0 && poolTotals.every((t) => t === 0)) { + return { + points: 0, + category: "excluded", + categoryDisplay: "Gros&Semi", + categoryIcon: faBox, + categoryColor: "#9ca3af", + }; + } + + // Pool dominant = celui avec le plus grand total + const dominantIdx = poolTotals.reduce( + (best, val, i) => (val > poolTotals[best] ? i : best), + 0, + ); + const dominantTotal = poolTotals[dominantIdx]; + const categoryName = poolNames[dominantIdx] ?? ""; + const poolIcons = [faWind, faLeaf, faGift, faBox]; + const poolColors = ["#3b82f6", "#10b981", "#7c3aed", "#f59e0b"]; + + let points = 0; + if (dominantTotal >= 30 && dominantTotal <= 100) points = 1; + else if (dominantTotal >= 110 && dominantTotal <= 200) points = 2; + else if (dominantTotal >= 210 && dominantTotal <= 300) points = 3; + else if (dominantTotal >= 310 && dominantTotal <= 400) points = 5; + else if (dominantTotal > 400) points = 10; + + return { + points, + category: categoryName.toLowerCase().replace(/\s/g, "_"), + categoryDisplay: categoryName, + categoryIcon: poolIcons[dominantIdx] ?? faGift, + categoryColor: poolColors[dominantIdx] ?? "#7c3aed", }; - } - - // ✅ 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 }; +}; + +// ============================================ +// TIMELINE STEPS +// ============================================ + +const TIMELINE_STEPS = [ + { key: "pending", label: "En attente", icon: faHourglassHalf }, + { key: "assigned", label: "Assigné", icon: faBiking }, + { key: "en_route", label: "En route", icon: faTruck }, + { key: "arrived", label: "Arrivé", icon: faMapMarkerAlt }, + { key: "livre", label: "Livré", icon: faBox }, + { key: "approved", label: "Confirmé", icon: faCheckCircle }, +]; + +const STATUS_ORDER: Record = { + pending: 0, + assigned: 1, + en_route: 2, + arrived: 3, + livre: 4, + approved: 5, }; // ============================================ @@ -269,705 +272,1155 @@ const calculateOrderPoints = (order: OrderWithTracking): { // ============================================ function SuiviLivraison() { - const navigate = useNavigate(); - const [orders, setOrders] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - const [expandedOrder, setExpandedOrder] = useState(null); - const [confirming, setConfirming] = useState(null); - - const [toasts, setToasts] = useState([]); - - const [showConfirmDialog, setShowConfirmDialog] = useState(false); - const [orderToConfirm, setOrderToConfirm] = useState(null); - const [selectedOrderPoints, setSelectedOrderPoints] = useState(0); - const [selectedOrderCategoryDisplay, setSelectedOrderCategoryDisplay] = useState(''); + const navigate = useNavigate(); + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [expandedOrder, setExpandedOrder] = useState(null); + const [confirming, setConfirming] = useState(null); - const [cancellingOrder, setCancellingOrder] = useState(null); - const [showCancelDialog, setShowCancelDialog] = useState(false); - const [orderToCancel, setOrderToCancel] = useState(null); - const [cancelReason, setCancelReason] = useState(''); - const [showPenaltyWarning, setShowPenaltyWarning] = useState(false); - const [penaltyWarningData, setPenaltyWarningData] = useState(null); + const [toasts, setToasts] = useState([]); - // ✅ 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 }); - } + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const [orderToConfirm, setOrderToConfirm] = useState(null); + const [selectedOrderPoints, setSelectedOrderPoints] = useState(0); + const [selectedOrderCategoryDisplay, setSelectedOrderCategoryDisplay] = + useState(""); + + const [cancellingOrder, setCancellingOrder] = useState(null); + const [showCancelDialog, setShowCancelDialog] = useState(false); + const [orderToCancel, setOrderToCancel] = useState(null); + const [cancelReason, setCancelReason] = useState(""); + const [showPenaltyWarning, setShowPenaltyWarning] = useState(false); + const [penaltyWarningData, setPenaltyWarningData] = + useState(null); + const [poolNames, setPoolNames] = useState([]); + + useEffect(() => { + getPublicSettings().then((s) => setPoolNames(s.pool_names ?? [])); + }, []); + + // ✅ 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); + } }; - checkAuth(); - }, [navigate]); + const showToast = ( + message: string, + type: "success" | "error" | "warning" | "info", + ) => { + const id = Date.now().toString(); + setToasts((prev) => [...prev, { id, message, type }]); + }; - // ✅ 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); + const removeToast = (id: string) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }; - 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 apiCategory = response.category || (response as any).data?.category || ''; - - let displayCategory = selectedOrderCategoryDisplay; - if (apiCategory === 'total') { - displayCategory = '🏆 Total'; - } else if (apiCategory.toLowerCase().includes('zipette')) { - displayCategory = '💨 Zipette&Co'; - } else if (apiCategory.toLowerCase().includes('weed') || apiCategory.toLowerCase().includes('hash')) { - displayCategory = '🌿 Weed&Hash'; + 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; } - - 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; - } + const order = orders.find((o) => o.id === orderId); - 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'); + if (order) { + const { points, categoryDisplay } = calculateOrderPoints(order, poolNames); + setSelectedOrderPoints(points); + setSelectedOrderCategoryDisplay(categoryDisplay); } else { - showToast(message, 'success'); + setSelectedOrderPoints(0); + setSelectedOrderCategoryDisplay(""); } - 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); + 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 apiCategory = + response.category || (response as any).data?.category || ""; + + const displayCategory = + apiCategory && apiCategory !== "total" + ? apiCategory + : selectedOrderCategoryDisplay; + + 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 ( + <> + +
+
+
+

Chargement de vos commandes...

+
+
+ + ); } - }; - const confirmCancelWithPenalty = () => { - setShowPenaltyWarning(false); - handleCancelOrder(true); - }; - - if (loading && orders.length === 0) { return ( - <> - -
-
-
-

Chargement de vos commandes...

-
-
- - ); - } - - return ( - <> - -
-
-

Suivi de vos commandes

-
- - {error && ( -
- {error} - -
- )} - - {orders.length === 0 ? ( -
-

Aucune commande trouvée

-

Vous n'avez pas encore passé de commande.

- -
- ) : ( -
- {orders.map((order) => ( -
-
setExpandedOrder(expandedOrder === order.id ? null : order.id)} - role="button" - tabIndex={0} - > -
-

Commande #{order.id}

-
- {getStatusLabel(order.status)} -
- {order.eta?.eta_available && order.eta.eta_minutes > 0 && ( -
- - {order.status?.toLowerCase() === 'assigned' - ? ` Arrivée estimée ~${order.eta.eta_minutes} min` - : ` ~${order.eta.eta_minutes} min`} - {order.eta.estimated_arrival && ` (${order.eta.estimated_arrival})`} -
- )} -
- -
- - {getTotalAmount(order).toFixed(2)} € - - - {expandedOrder === order.id ? '▲' : '▼'} - -
+ <> + +
+
+

Suivi de vos commandes

- {expandedOrder === order.id && ( -
-
-
+ {error && ( +
+ {error} +
- -
-

Adresse de livraison

-

- {getDeliveryAddress(order)} -

- {(() => { - const clientInfo = getClientInfo(order); - if (clientInfo.firstName || clientInfo.lastName) { - return ( -

- {clientInfo.firstName} {clientInfo.lastName} - {clientInfo.phone && ` • ${clientInfo.phone}`} -

- ); - } - return null; - })()} -
- - {order.livreur_assign && ( -
-

Livreur assigné

-

{order.livreur_assign}

-
- )} - - {order.eta?.eta_available && order.eta.eta_minutes > 0 && ( -
-

- - {order.status?.toLowerCase() === 'assigned' - ? ' Temps d\'arrivée estimé' - : ' Heure estimée d\'arrivée'} -

-

~{order.eta.eta_minutes} min{order.eta.estimated_arrival ? ` — arrivée vers ${order.eta.estimated_arrival}` : ''}

- {order.status?.toLowerCase() === 'assigned' && ( - Le livreur n'a pas encore démarré — estimation basée sur sa position actuelle - )} - {order.eta.livreur_distance != null && ( - Distance : {typeof order.eta.livreur_distance === 'number' ? order.eta.livreur_distance.toFixed(1) : order.eta.livreur_distance} km - )} -
- )} - - {order.items && order.items.length > 0 && ( -
-

Produits

-
- {order.items.map((item: any, idx: number) => { - const formatted = formatOrderItem(item); - - return ( -
- - {formatted.name} ({formatted.quantity}g) - - - {formatted.price.toFixed(2)} € - -
- ); - })} -
-
- )} - -
-

Montant total

-

- {getTotalAmount(order).toFixed(2)} € -

-
- -
-

Date de commande

-

{new Date(order.created_at).toLocaleDateString('fr-FR', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - })}

-
- -
- {order.status?.toLowerCase() === 'livre' ? ( -
-
-

- Votre colis a été livré -

-

- Confirmez la réception pour valider la livraison et gagner des points -

-
- -
- ) : order.status?.toLowerCase() === 'approved' ? ( -
- Commande validée et confirmée -
- ) : order.status?.toLowerCase() === 'cancelled' ? ( -
- Cette commande a été annulée -
- ) : ( -
- - - -
- )} -
-
)} -
- ))} -
- )} -
-

Les informations se mettent à jour automatiquement chaque 10 secondes

-

Points de fidélité selon votre achat : 💨 Zipette&Co (1-3 pts) • 🌿 Weed&Hash (1-10 pts)

-
-
- - {/* Dialog de confirmation */} - {showConfirmDialog && ( -
-
e.stopPropagation()}> -
-

Confirmer la réception

-
-
-

Confirmez-vous avoir bien reçu votre commande ?

- -
- - - {selectedOrderPoints > 0 - ? `+${selectedOrderPoints} point${selectedOrderPoints > 1 ? 's' : ''} de fidélité` - : 'Commande éligible aux points de fidélité' - } - -
- - {selectedOrderPoints > 0 && ( -
-

Accumulez des points pour obtenir des récompenses !

-
- )} -
-
- - -
-
-
- )} - - {/* Dialog d'annulation */} - {showCancelDialog && ( -
-
e.stopPropagation()}> -
-

- - {showPenaltyWarning ? ' Confirmation requise' : ' Annuler la commande'} -

-
- -
- {showPenaltyWarning && penaltyWarningData ? ( -
-
- -
-

{penaltyWarningData.message}

- - {penaltyWarningData.details && ( -
-

Livreur assigné: {penaltyWarningData.details.livreur}

-

Statut: {getStatusLabel(penaltyWarningData.details.status || '')}

- {penaltyWarningData.details.position_in_queue && ( -

Position dans la queue: {penaltyWarningData.details.position_in_queue}

- )} -
- )} - - {penaltyWarningData.penalty_warning && ( -
- {penaltyWarningData.penalty_warning.will_apply ? ( - <> -

- ⚠️ Pénalité: {penaltyWarningData.penalty_warning.penalty_amount} points -

-

{penaltyWarningData.penalty_warning.message}

-
-

Barème des pénalités:

-
    -
  • 1ère annulation: {penaltyWarningData.penalty_warning.scale['1st_cancel']}
  • -
  • 2ème annulation: {penaltyWarningData.penalty_warning.scale['2nd_cancel']}
  • -
  • 3ème annulation: {penaltyWarningData.penalty_warning.scale['3rd_cancel']}
  • -
  • 4ème+ annulation: {penaltyWarningData.penalty_warning.scale['4th+_cancel']}
  • -
-
- - ) : ( -
- - {penaltyWarningData.penalty_warning.message} + {orders.length === 0 ? ( +
+
+
- )} +

Aucune commande trouvée

+

Vous n'avez pas encore passé de commande.

+
- )} - -

- {penaltyWarningData.penalty_warning?.will_apply - ? 'Voulez-vous vraiment annuler cette commande et accepter la pénalité ?' - : 'Voulez-vous vraiment annuler cette commande ?' - } -

-
- ) : ( -
-

Êtes-vous sûr de vouloir annuler cette commande ?

- -
- -