// ============================================ // api/api.ts - VERSION CORRIGÉE // ============================================ // ✅ AuthResponse inclut access_token // ✅ loginUser et registerUser retournent AuthResponse // ✅ sessionStorage (pas localStorage) const API_URL = "https://uber-stup.club/api/v1"; const BACKEND_URL = "https://uber-stup.club"; export function getMediaUrl(url: string): string { if (!url) return ""; if (url.startsWith("http")) return url; return `${BACKEND_URL}${url}`; } import type { ConfirmReceptionResponse, CheckoutCartResponse, HistoryResponse, ETAResponse, TrackingResponse, CancelCommandResponse, PenaltiesResponse, } from "./api_types"; // ============================================ // 🔐 TYPES - AUTHRESPONSE COMPLETE // ============================================ /** * ✅ TYPE CORRECT - Inclut access_token! */ export interface AuthResponse { success: boolean; message?: string; access_token?: string; // ✅ CRITICAL! token_type?: string; expires_in?: number; user?: { id: number; username: string; nom?: string; prenom?: string; telephone?: string; role?: string; session_id?: string; must_change_password?: boolean; }; } // ============================================ // 🔐 GESTION CENTRALISÉE DU JWT // ============================================ /** * ✅ Extraire username du JWT * Source de vérité UNIQUE pour le username */ export const extractUsernameFromToken = (): string | null => { try { // ✅ CRITICAL: sessionStorage (pas localStorage!) const token = sessionStorage.getItem("token"); if (!token) { console.log("❌ [JWT] Aucun token dans sessionStorage"); return null; } console.log("🔐 [JWT] Token trouvé, décodage..."); // JWT format: header.payload.signature const parts = token.split("."); if (parts.length !== 3) { console.error("❌ [JWT] Format invalide"); return null; } // Décoder le payload (base64 -> JSON) const base64Url = parts[1]; const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); const jsonPayload = decodeURIComponent( atob(base64) .split("") .map( (c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2), ) .join(""), ); const payload = JSON.parse(jsonPayload); console.log("📋 [JWT] Payload:", payload); // Extraire le username const username = payload.username; if (!username) { console.error("❌ [JWT] Username non trouvé dans payload"); return null; } console.log("✅ [JWT] Username extrait:", username); return username; } catch (error) { console.error("❌ [JWT] Erreur décodage:", error); return null; } }; /** * ✅ Synchroniser sessionStorage avec JWT */ export const syncUsernameFromJWT = (): string | null => { const jwtUsername = extractUsernameFromToken(); if (!jwtUsername) { console.log("❌ [SYNC] Impossible d'extraire username du JWT"); return null; } const storedUsername = sessionStorage.getItem("username"); // Mismatch détecté if (storedUsername && storedUsername !== jwtUsername) { console.warn(`⚠️ [SYNC] MISMATCH!`); console.warn(` Ancien: ${storedUsername}`); console.warn(` JWT: ${jwtUsername}`); sessionStorage.removeItem("username"); } // Toujours stocker le JWT username (source de vérité) sessionStorage.setItem("username", jwtUsername); console.log(`✅ [SYNC] Username synchronisé: ${jwtUsername}`); return jwtUsername; }; /** * ✅ Récupérer le username authentifié */ export const getAuthenticatedUsername = (): string | null => { return extractUsernameFromToken(); }; /** * ✅ Vérifier si utilisateur est authentifié */ export const isUserAuthenticated = (): boolean => { const token = sessionStorage.getItem("token"); const username = extractUsernameFromToken(); return !!(token && username); }; /** * ✅ Récupérer le token */ export const getAuthToken = (): string | null => { return sessionStorage.getItem("token"); }; // ============================================ // 🔐 AUTHENTIFICATION // ============================================ /** * ✅ LOGIN - Retourne AuthResponse avec access_token * POST /api/v1/auth/login */ export const loginUser = async ( username: string, password: string, ): Promise => { // ✅ Type de retour CORRECT try { console.log("🔐 [LOGIN] Appel API..."); const response = await fetch(`${API_URL}/auth/login`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username, password }), }); if (!response.ok) { let errorMessage = "Erreur de connexion"; try { const errorData = await response.json(); errorMessage = errorData.error || errorData.message || errorMessage; } catch { // body vide ou non-JSON } console.error( "❌ [LOGIN] Erreur API:", response.status, errorMessage, ); return { success: false, message: errorMessage, }; } const data = await response.json(); console.log("📋 [LOGIN] Réponse:", data); // ✅ Vérifier access_token if (!data.access_token) { console.error("❌ [LOGIN] Pas de access_token"); return { success: false, message: "Token non reçu du serveur", }; } // ✅ Stocker en sessionStorage sessionStorage.setItem("token", data.access_token); console.log("✅ [LOGIN] Token stocké"); // ✅ Synchroniser username const jwtUsername = syncUsernameFromJWT(); if (!jwtUsername) { console.warn("⚠️ [LOGIN] Impossible de synchroniser username"); return { success: false, message: "Erreur synchronisation JWT", }; } console.log(`✅ [LOGIN] Connecté: ${jwtUsername}`); // ✅ Retourner avec access_token return { success: true, message: "Connexion réussie", access_token: data.access_token, // ✅ IMPORTANT! token_type: data.token_type, expires_in: data.expires_in, user: data.user, }; } catch (error) { console.error("❌ [LOGIN] Erreur:", error); return { success: false, message: error instanceof Error ? error.message : "Erreur de connexion", }; } }; /** * ✅ LOGOUT */ export const logoutUser = async (): Promise => { const token = sessionStorage.getItem("token"); if (token) { try { await fetch(`${API_URL}/auth/logout`, { method: "POST", headers: { Authorization: `Bearer ${token}`, }, }); } catch (error) { console.warn("⚠️ [LOGOUT] Erreur backend:", error); } } // Nettoyer sessionStorage sessionStorage.removeItem("token"); sessionStorage.removeItem("username"); console.log("✅ [LOGOUT] sessionStorage nettoyé"); }; /** * ✅ CHANGE PASSWORD * PUT /api/v1/auth/change-password */ export const changePassword = async ( currentPassword: string, newPassword: string, ): Promise<{ success: boolean; message: string }> => { const token = sessionStorage.getItem("token"); try { const response = await fetch(`${API_URL}/auth/change-password`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ current_password: currentPassword, new_password: newPassword, }), }); const data = await response.json(); if (!response.ok) { return { success: false, message: data.error || data.message || "Erreur lors du changement de mot de passe", }; } return { success: true, message: data.message || "Mot de passe mis à jour", }; } catch (error) { return { success: false, message: error instanceof Error ? error.message : "Erreur de connexion", }; } }; // ============================================ // 🛒 PANIER // ============================================ export interface BasketResponse { success: boolean; message?: string; panier?: any[]; data?: { panier?: any[] }; } /** * ✅ GET CART * GET /api/v1/panier/:username */ export const getCart = async (username: string): Promise => { const token = sessionStorage.getItem("token"); if (!token) { console.log("❌ [CART] Aucun token"); return { success: false, message: "Session invalide", panier: [], }; } // ✅ Extraire depuis JWT (source de vérité) const jwtUsername = extractUsernameFromToken(); if (!jwtUsername) { console.log("❌ [CART] JWT invalide"); return { success: false, message: "Session invalide", panier: [], }; } // ✅ Vérifier accès if (jwtUsername !== username) { console.log(`❌ [CART] ACCÈS REFUSÉ!`); return { success: false, message: "Accès refusé", panier: [], }; } try { const response = await fetch(`${API_URL}/panier/${jwtUsername}`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); const responseData = await response.json(); if (!response.ok) { return { success: false, message: responseData.error || "Erreur récupération", panier: [], }; } return { success: true, panier: responseData.panier || [], message: responseData.message || "Panier récupéré", }; } catch (error) { console.error("❌ [CART] Erreur:", error); return { success: false, message: "Erreur récupération", panier: [], }; } }; /** * ✅ ADD TO CART * POST /api/v1/panier/add */ export const addToCart = async (cartItem: { username?: string; name_product: string; category: string; quantity: number; price: number; }) => { const token = sessionStorage.getItem("token"); if (!token) { return { success: false, message: "Session invalide" }; } // ✅ Extraire depuis JWT const jwtUsername = extractUsernameFromToken(); if (!jwtUsername) { return { success: false, message: "Session invalide" }; } // ✅ Vérifier accès if (cartItem.username && cartItem.username !== jwtUsername) { console.log(`❌ [ADD] ACCÈS REFUSÉ!`); return { success: false, message: "Accès refusé" }; } // ✅ FORCER le JWT username cartItem.username = jwtUsername; console.log("💾 [ADD] Ajout au panier:", cartItem); try { const response = await fetch(`${API_URL}/panier/add`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(cartItem), }); const data = await response.json(); if (!response.ok) { return { success: false, message: data.error || "Erreur ajout", }; } return { success: true, message: "Produit ajouté", panier: data.panier, }; } catch (error) { console.error("❌ [ADD] Erreur:", error); return { success: false, message: "Erreur ajout" }; } }; /** * ✅ REMOVE FROM CART * DELETE /api/v1/panier/remove */ export const removeFromCart = async (id: number, username: string) => { const token = sessionStorage.getItem("token"); if (!token) { return { success: false, message: "Session invalide" }; } // ✅ Extraire depuis JWT const jwtUsername = extractUsernameFromToken(); if (!jwtUsername) { return { success: false, message: "Session invalide" }; } // ✅ Vérifier accès if (username && jwtUsername !== username) { console.log(`❌ [REMOVE] ACCÈS REFUSÉ!`); return { success: false, message: "Accès refusé" }; } try { const response = await fetch(`${API_URL}/panier/remove`, { method: "DELETE", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ id, username: jwtUsername, }), }); const data = await response.json(); if (!response.ok) { return { success: false, message: data.error || "Erreur suppression", }; } return { success: true, message: "Produit supprimé", }; } catch (error) { console.error("❌ [REMOVE] Erreur:", error); return { success: false, message: "Erreur suppression" }; } }; /** * ✅ CLEAR CART * DELETE /api/v1/panier/clear/:username */ export const clearCart = async (username: string) => { const token = sessionStorage.getItem("token"); if (!token) { return { success: false, message: "Session invalide" }; } // ✅ Extraire depuis JWT const jwtUsername = extractUsernameFromToken(); if (!jwtUsername) { return { success: false, message: "Session invalide" }; } // ✅ Vérifier accès if (jwtUsername !== username) { return { success: false, message: "Accès refusé" }; } try { // ✅ CHANGÉ: De /panier/clear/:username à /panier/clear (sans paramètre) const response = await fetch(`${API_URL}/panier/clear`, { method: "DELETE", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, // ✅ Pas de body nécessaire, le backend utilise le JWT }); // ✅ Gérer les erreurs HTTP avant de parser JSON if (!response.ok) { try { const errorData = await response.json(); console.error("❌ [CLEAR] Erreur API:", errorData); return { success: false, message: errorData.error || "Erreur vidage", }; } catch (parseError) { // Si le parsing JSON échoue (HTML retourné) console.error("❌ [CLEAR] Réponse non-JSON du serveur"); return { success: false, message: "Erreur serveur (non-JSON)", }; } } // ✅ Parser JSON seulement si response.ok const data = await response.json(); console.log("✅ [CLEAR] Panier vidé:", { stock_released: data.stock_released, message: data.message, }); return { success: true, message: data.message || "Panier vidé", stock_released: data.stock_released, }; } catch (error) { console.error("❌ [CLEAR] Erreur:", error); return { success: false, message: error instanceof Error ? error.message : "Erreur vidage", }; } }; // ============================================ // 📦 COMMANDES // ============================================ /** * ✅ GET MY ORDERS * GET /api/v1/my-commands */ export const getMyOrders = async () => { const token = sessionStorage.getItem("token"); if (!token) { return { success: false, message: "Session invalide", commands: [] }; } try { const response = await fetch(`${API_URL}/my-commands`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); const data = await response.json(); if (!response.ok) { return { success: false, message: data.error || "Erreur récupération", commands: [], }; } return { success: true, commands: data.commands || [], }; } catch (error) { console.error("❌ [ORDERS] Erreur:", error); return { success: false, message: "Erreur récupération", commands: [] }; } }; /** * ✅ CREATE CHECKOUT * POST /api/v1/checkout */ export interface CheckoutData { username?: string; delivery_address: string; first_name?: string; last_name?: string; phone?: string; payment_method?: string; } export const createCheckout = async (checkoutData: CheckoutData) => { const token = sessionStorage.getItem("token"); if (!token) { return { success: false, message: "Session invalide" }; } // ✅ Extraire depuis JWT const jwtUsername = extractUsernameFromToken(); if (!jwtUsername) { return { success: false, message: "Session invalide" }; } // ✅ Vérifier accès if (checkoutData.username && checkoutData.username !== jwtUsername) { console.log(`❌ [CHECKOUT] ACCÈS REFUSÉ!`); return { success: false, message: "Accès refusé" }; } // ✅ FORCER le JWT username checkoutData.username = jwtUsername; try { const response = await fetch(`${API_URL}/checkout`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(checkoutData), }); const data = await response.json(); if (!response.ok) { return { success: false, message: data.error || "Erreur création", }; } return { success: true, message: "Commande créée", command_id: data.command_id, command: data.command, delivery_address: data.delivery_address, assigned_to: data.assigned_to, queue_info: data.queue_info, }; } catch (error) { console.error("❌ [CHECKOUT] Erreur:", error); return { success: false, message: "Erreur création" }; } }; /** * ✅ APPROVE DELIVERY * POST /api/v1/commands/:id/approve */ export const approveDelivery = async ( commandId: number, data?: { rating?: number; comment?: string; }, ) => { const token = sessionStorage.getItem("token"); if (!token) { return { success: false, message: "Session invalide" }; } try { const response = await fetch( `${API_URL}/commands/${commandId}/approve`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(data || {}), }, ); const responseData = await response.json(); if (!response.ok) { return { success: false, message: responseData.error || "Erreur approbation", }; } return { success: true, message: "Commande approuvée", points_earned: responseData.points_earned, }; } catch (error) { console.error("❌ [APPROVE] Erreur:", error); return { success: false, message: "Erreur approbation" }; } }; // ============================================ // 📊 PRODUITS // ============================================ export interface MediaItem { id?: number; product_id?: number; url: string; type: string; created_at?: string; } export interface Product { id: number; name: string; description?: string; category: string; unit?: string; stock: number; prices?: Array<{ quantity: number; price: number }>; media?: MediaItem[]; // ✅ CHANGÉ: string[] → MediaItem[] } export const getAllProducts = async () => { try { const response = await fetch(`${API_URL}/products`); return await response.json(); } catch (error) { console.error("❌ [PRODUCTS] Erreur:", error); return { success: false, data: [] }; } }; /** * ✅ GET PRODUCTS BY CATEGORY - Public * ⚠️ IMPORTANT: Utilise PATH param (:category), pas query param */ 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(); } catch (error) { console.error("❌ [PRODUCTS] Erreur:", error); return { success: false, data: [] }; } }; /** * ✅ GET PRODUCT BY ID - Public */ export const getProductById = async (id: number) => { try { const response = await fetch(`${API_URL}/products/${id}`); return await response.json(); } catch (error) { console.error("❌ [PRODUCT] Erreur:", error); return { success: false, data: null }; } }; // ============================================ // 📍 SUIVI COMMANDE - ORDER TRACKING // ============================================ /** * ✅ GET ORDER TRACKING - Suivi d'une commande * GET /api/v1/commands/:id/track */ export const getOrderTracking = async ( commandId: number, ): Promise => { const token = sessionStorage.getItem("token"); if (!token) { console.log("❌ [TRACKING] Aucun token"); return { success: false, command_id: commandId, status: "error", current_step: "Authentification requise", message: "Session invalide - veuillez vous reconnecter", }; } try { console.log( "📍 [TRACKING] Récupération suivi pour commande:", commandId, ); const response = await fetch(`${API_URL}/commands/${commandId}/track`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); const data = await response.json(); if (!response.ok) { console.error("❌ [TRACKING] Erreur API:", data); return { success: false, command_id: commandId, status: "error", current_step: "Erreur", message: data.error || "Erreur récupération suivi", }; } console.log("✅ [TRACKING] Suivi récupéré:", { command_id: data.id || commandId, status: data.status, current_step: data.current_step, livreur: data.livreur_username || data.livreur, distance: data.livreur_distance, eta: data.eta_minutes, }); return { success: true, command_id: data.id || commandId, status: data.status || "pending", current_step: data.current_step || "En attente", livreur_username: data.livreur_username || data.livreur, livreur_distance: data.livreur_distance, eta_minutes: data.eta_minutes, estimated_arrival: data.estimated_arrival, delivery_address: data.delivery_address || data.adresse, location: data.location, incidents_detected: data.incidents_detected || 0, updated_at: data.updated_at, created_at: data.created_at, message: "Suivi récupéré", }; } catch (error) { console.error("❌ [TRACKING] Erreur fetch:", error); return { success: false, command_id: commandId, status: "error", current_step: "Erreur de connexion", message: error instanceof Error ? error.message : "Erreur récupération suivi", }; } }; /** * ✅ GET ORDER ETA - Récupérer l'ETA d'une commande * GET /api/v1/commands/:id/eta */ export const getOrderETA = async (commandId: number): Promise => { const token = sessionStorage.getItem("token"); if (!token) { console.log("❌ [ETA] Aucun token"); return { success: false, command_id: commandId, eta_minutes: 0, estimated_arrival: "N/A", status: "error", message: "Session invalide", }; } try { console.log("⏱️ [ETA] Récupération ETA pour commande:", commandId); const response = await fetch(`${API_URL}/commands/${commandId}/eta`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); const data = await response.json(); if (!response.ok) { console.error("❌ [ETA] Erreur API:", data); return { success: false, command_id: commandId, eta_minutes: 0, estimated_arrival: "N/A", status: "error", message: data.error || "Erreur récupération ETA", }; } console.log("✅ [ETA] ETA récupéré:", { command_id: commandId, eta_minutes: data.eta_minutes, estimated_arrival: data.estimated_arrival, }); return { success: true, command_id: data.id || commandId, eta_minutes: data.eta_minutes || 0, estimated_arrival: data.estimated_arrival || "N/A", status: data.status || "pending", livreur_distance: data.livreur_distance, message: "ETA récupéré", }; } catch (error) { console.error("❌ [ETA] Erreur fetch:", error); return { success: false, command_id: commandId, eta_minutes: 0, estimated_arrival: "N/A", status: "error", message: error instanceof Error ? error.message : "Erreur récupération ETA", }; } }; /** * ✅ GET ALL ORDERS WITH TRACKING - Récupérer les commandes avec suivi * GET /api/v1/my-commands */ export const getOrdersWithTracking = async () => { const token = sessionStorage.getItem("token"); if (!token) { console.log("❌ [ORDERS_TRACKING] Aucun token"); return { success: false, commands: [], message: "Session invalide", }; } try { console.log( "📦 [ORDERS_TRACKING] Récupération commandes avec suivi...", ); const response = await fetch(`${API_URL}/my-commands`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); const data = await response.json(); if (!response.ok) { console.error("❌ [ORDERS_TRACKING] Erreur API:", data); return { success: false, commands: [], message: data.error || "Erreur récupération", }; } console.log( "✅ [ORDERS_TRACKING] Commandes récupérées:", data.commands?.length || 0, ); return { success: true, commands: data.commands || [], count: data.count || data.commands?.length || 0, message: "Commandes récupérées", }; } catch (error) { console.error("❌ [ORDERS_TRACKING] Erreur fetch:", error); return { success: false, commands: [], message: error instanceof Error ? error.message : "Erreur récupération", }; } }; /** * ✅ CONFIRM RECEPTION - Confirmer la réception d'une commande * POST /api/v1/commands/:id/approve * * Utilisée dans SuiviLivraison.tsx * Permet au client de confirmer qu'il a reçu sa commande */ export const confirmReception = async ( commandId: number, ): Promise => { // ✅ Type de retour explicite const token = sessionStorage.getItem("token"); if (!token) { console.log("❌ [CONFIRM] Aucun token"); return { success: false, message: "Session invalide - veuillez vous reconnecter", }; } try { console.log( "✅ [CONFIRM] Confirmation de réception pour commande:", commandId, ); const response = await fetch( `${API_URL}/commands/${commandId}/approve`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({}), }, ); const responseData = await response.json(); if (!response.ok) { console.error("❌ [CONFIRM] Erreur API:", responseData); return { success: false, message: responseData.error || "Erreur lors de la confirmation", }; } console.log("✅ [CONFIRM] Confirmation réussie:", { command_id: commandId, points_earned: responseData.points_earned, category: responseData.category, // ✅ Log de la catégorie }); return { success: true, message: "Commande confirmée avec succès", points_earned: responseData.points_earned || 10, data: responseData, // ✅ Inclut category dans data }; } catch (error) { console.error("❌ [CONFIRM] Erreur fetch:", error); return { success: false, message: error instanceof Error ? error.message : "Erreur lors de la confirmation", }; } }; export const getOrderTotal = async (commandId: number): Promise => { const token = sessionStorage.getItem("token"); if (!token) { console.log("❌ [TOTAL] Aucun token"); return 0; } try { console.log("💰 [TOTAL] Récupération total pour commande:", commandId); const response = await fetch(`${API_URL}/commands/${commandId}/total`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); const data = await response.json(); if (!response.ok) { console.error("❌ [TOTAL] Erreur API:", data); return 0; } const total = data.total || data.total_prix || 0; console.log("✅ [TOTAL] Total récupéré:", total); return total; } catch (error) { console.error("❌ [TOTAL] Erreur fetch:", error); return 0; } }; export const calculateOrderTotal = (order: any): number => { // Préférer total (colonne calculée par le backend) if (typeof order.total === "number" && order.total > 0) { return order.total; } // Sinon utiliser total_prix (alias ancien) if (typeof order.total_prix === "number" && order.total_prix > 0) { return order.total_prix; } // Fallback: calculer depuis les items si présents if (Array.isArray(order.items) && order.items.length > 0) { return order.items.reduce((sum: number, item: any) => { const price = item.prix || item.price || 0; const quantity = item.quantite || item.quantity || 1; return sum + price * quantity; }, 0); } return 0; }; // ============================================ // 📚 HISTORIQUE DES COMMANDES // ============================================ /** * ✅ GET MY COMPLETED ORDERS - Historique des commandes terminées * GET /api/v1/my-commands/history */ export const getMyCompletedOrders = async (): Promise => { const token = sessionStorage.getItem("token"); if (!token) { console.log("❌ [HISTORY] Aucun token"); return { success: false, commands: [], count: 0, message: "Session invalide", }; } try { console.log("📚 [HISTORY] Récupération historique..."); const response = await fetch(`${API_URL}/my-commands/history`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); const data = await response.json(); if (!response.ok) { console.error("❌ [HISTORY] Erreur API:", data); return { success: false, commands: [], count: 0, message: data.error || "Erreur récupération historique", }; } console.log( "✅ [HISTORY] Historique récupéré:", data.count, "commandes", ); return { success: true, commands: data.commands || [], count: data.count || 0, client_stats: data.client_stats, }; } catch (error) { console.error("❌ [HISTORY] Erreur fetch:", error); return { success: false, commands: [], count: 0, message: error instanceof Error ? error.message : "Erreur récupération", }; } }; /** * ✅ Formater la date de la commande */ export const formatOrderDate = (dateString: string): string => { try { const date = new Date(dateString); return date.toLocaleDateString("fr-FR", { day: "2-digit", month: "long", year: "numeric", hour: "2-digit", minute: "2-digit", }); } catch { return dateString; } }; /** * ✅ Calculer le temps écoulé depuis la commande */ export const getOrderAge = (dateString: string): string => { try { const orderDate = new Date(dateString); const now = new Date(); const diffMs = now.getTime() - orderDate.getTime(); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays === 0) return "Aujourd'hui"; if (diffDays === 1) return "Hier"; if (diffDays < 7) return `Il y a ${diffDays} jours`; if (diffDays < 30) { const weeks = Math.floor(diffDays / 7); return `Il y a ${weeks} semaine${weeks > 1 ? "s" : ""}`; } const months = Math.floor(diffDays / 30); return `Il y a ${months} mois`; } catch { return "Date inconnue"; } }; /** * ✅ Formater le prix */ export const formatPrice = (price: number): string => { return `${price.toFixed(2)} €`; }; // ============================================ // 🚫 ANNULATION DE COMMANDES (SUITE) // ============================================ /** * ✅ CANCEL COMMAND - Annuler une commande * POST /api/v1/commands/:id/cancel */ export const cancelCommand = async ( commandId: number, reason?: string, force: boolean = false, ): Promise => { const token = sessionStorage.getItem("token"); if (!token) { console.log("❌ [CANCEL] Aucun token"); return { success: false, message: "Session invalide", }; } try { console.log("🚫 [CANCEL] Annulation commande:", commandId, { reason, force, }); const response = await fetch( `${API_URL}/commands/${commandId}/cancel`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ reason: reason || "Annulation par le client", force: force, }), }, ); const data = await response.json(); // ⚠️ AVERTISSEMENT (409 Conflict) - Livreur assigné if (response.status === 409 && data.warning) { console.warn( "⚠️ [CANCEL] Avertissement pénalité:", data.penalty_warning, ); return { success: false, warning: true, message: data.message, details: data.details, penalty_warning: data.penalty_warning, action_required: data.action_required, example: data.example, }; } if (!response.ok) { console.error("❌ [CANCEL] Erreur API:", data); return { success: false, message: data.error || "Erreur lors de l'annulation", }; } console.log("✅ [CANCEL] Commande annulée:", { command_id: commandId, penalty: data.penalty, }); return { success: true, message: data.message || "Commande annulée avec succès", command_id: data.command_id, new_status: data.new_status, cancelled_by: data.cancelled_by, reason: data.reason, penalty: data.penalty, info: data.info, }; } catch (error) { console.error("❌ [CANCEL] Erreur fetch:", error); return { success: false, message: error instanceof Error ? error.message : "Erreur lors de l'annulation", }; } }; /** * ✅ GET MY CANCELLATION HISTORY - Historique des annulations * GET /api/v1/my-cancellation-history */ export interface CancellationHistoryItem { command_id: number; cancelled_at: string; reason: string; penalty_applied: number; had_livreur: boolean; livreur_username?: string; } export interface CancellationHistoryResponse { success: boolean; data?: { username: string; history: CancellationHistoryItem[]; total_penalties: number; warning?: string; info?: string; }; message?: string; } export const getMyCancellationHistory = async (): Promise => { const token = sessionStorage.getItem("token"); if (!token) { console.log("❌ [CANCEL_HISTORY] Aucun token"); return { success: false, message: "Session invalide", }; } try { console.log("📊 [CANCEL_HISTORY] Récupération historique..."); const response = await fetch(`${API_URL}/my-cancellation-history`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); const data = await response.json(); if (!response.ok) { console.error("❌ [CANCEL_HISTORY] Erreur API:", data); return { success: false, message: data.error || "Erreur récupération historique", }; } console.log( "✅ [CANCEL_HISTORY] Historique récupéré:", data.data?.history?.length || 0, ); return { success: true, data: data.data, message: "Historique récupéré", }; } catch (error) { console.error("❌ [CANCEL_HISTORY] Erreur fetch:", error); return { success: false, message: error instanceof Error ? error.message : "Erreur récupération", }; } }; export const getOrderDetails = async (commandId: number) => { const token = sessionStorage.getItem("token"); if (!token) { console.log("❌ [ORDER DETAILS] Aucun token"); return { success: false, message: "Session invalide", }; } try { console.log("📦 [ORDER DETAILS] Récupération commande:", commandId); const response = await fetch(`${API_URL}/commands/${commandId}`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); const data = await response.json(); if (!response.ok) { console.error("❌ [ORDER DETAILS] Erreur API:", data); return { success: false, message: data.error || "Erreur lors de la récupération", order: null, }; } console.log("✅ [ORDER DETAILS] Commande récupérée:", data); return { success: true, order: data.command || data, message: "Commande récupérée", }; } catch (error) { console.error("❌ [ORDER DETAILS] Erreur fetch:", error); return { success: false, message: error instanceof Error ? error.message : "Erreur de connexion", order: null, }; } }; // ============================================ // À REMPLACER dans api/api.ts // ============================================ /** * ✅ GET COMMAND ITEMS WITH DETAILS * GET /api/v1/commands/:id/items * Récupère les items d'une commande depuis command_items */ export const getCommandItemsWithDetails = async (commandId: number) => { const token = sessionStorage.getItem("token"); if (!token) { console.log("❌ [COMMAND ITEMS] Aucun token"); return { success: false, message: "Session invalide", data: null, }; } try { console.log( "📦 [COMMAND ITEMS] Récupération items pour commande:", commandId, ); // ✅ CORRECTION: Utiliser fetch() correctement (pas fetch`...`) const response = await fetch(`${API_URL}/commands/${commandId}/items`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); const data = await response.json(); if (!response.ok) { console.error("❌ [COMMAND ITEMS] Erreur API:", data); return { success: false, message: data.error || "Erreur lors de la récupération", data: null, }; } console.log("✅ [COMMAND ITEMS] Items récupérés:", data); return { success: true, data: data, message: "Items récupérés", }; } catch (error) { console.error("❌ [COMMAND ITEMS] Erreur fetch:", error); return { success: false, message: error instanceof Error ? error.message : "Erreur de connexion", data: null, }; } }; export const getMyPenalties = async (): Promise => { const token = sessionStorage.getItem("token"); if (!token) { console.log("❌ [PENALTIES] Aucun token"); return { success: false, message: "Session invalide", }; } try { console.log("🚨 [PENALTIES] Récupération pénalités..."); const response = await fetch(`${API_URL}/penalties`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); const data = await response.json(); if (!response.ok) { console.error("❌ [PENALTIES] Erreur API:", data); return { success: false, message: data.error || "Erreur récupération pénalités", }; } console.log("✅ [PENALTIES] Pénalités récupérées:", { penalty_points: data.data?.penalty_points || 0, cancellations: data.data?.cancellations_count || 0, can_order: data.data?.can_order, }); return { success: true, data: data.data, message: "Pénalités récupérées", }; } catch (error) { console.error("❌ [PENALTIES] Erreur fetch:", error); return { success: false, message: error instanceof Error ? error.message : "Erreur récupération", }; } }; export const checkoutCart = async ( delivery_address: string, ): Promise => { const token = sessionStorage.getItem("token"); if (!token) { console.log("❌ [CHECKOUT] Aucun token"); return { success: false, message: "Session invalide - veuillez vous reconnecter", }; } // ✅ Extraire username depuis JWT const jwtUsername = extractUsernameFromToken(); if (!jwtUsername) { console.log("❌ [CHECKOUT] JWT invalide"); return { success: false, message: "Session invalide", }; } if (!delivery_address.trim()) { return { success: false, message: "Veuillez saisir une adresse de livraison", }; } try { console.log("🛒 [CHECKOUT] Validation commande...", { username: jwtUsername, delivery_address, }); const response = await fetch(`${API_URL}/cart/checkout`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ username: jwtUsername, delivery_address, }), }); const data = await response.json(); if (!response.ok) { console.error("❌ [CHECKOUT] Erreur API:", data); return { success: false, message: data.error || data.message || "Erreur lors de la validation", }; } console.log("✅ [CHECKOUT] Commande validée:", { command_id: data.command_id, assigned_to: data.assigned_to?.username, queue_position: data.queue_info?.position, }); return { success: true, message: data.message || "Commande validée avec succès", command_id: data.command_id, delivery_address: data.delivery_address, command: data.command, assigned_to: data.assigned_to, queue_info: data.queue_info, }; } catch (error) { console.error("❌ [CHECKOUT] Erreur fetch:", error); return { success: false, message: error instanceof Error ? error.message : "Erreur serveur", }; } }; // ============================================ // 🔔 NOTIFICATIONS CLIENT // ============================================ export interface ClientNotification { command_id: number; type: string; message: string; created_at: string; read: boolean; } export interface NotificationsResponse { success: boolean; notifications: ClientNotification[]; unread_count: number; total: number; } export const getClientNotifications = async (): Promise => { const token = getAuthToken(); if (!token) return { success: false, notifications: [], unread_count: 0, total: 0, }; try { const response = await fetch(`${API_URL}/notifications`, { headers: { Authorization: `Bearer ${token}` }, }); if (!response.ok) return { success: false, notifications: [], unread_count: 0, total: 0, }; const data = await response.json(); return { success: true, notifications: data.notifications || [], unread_count: data.unread_count || 0, total: data.total || 0, }; } catch { return { success: false, notifications: [], unread_count: 0, total: 0, }; } }; export const markNotificationsRead = async (): Promise<{ success: boolean; }> => { const token = getAuthToken(); if (!token) return { success: false }; try { const response = await fetch(`${API_URL}/notifications/read`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, }); return { success: response.ok }; } catch { return { success: false }; } };