Files
projet_gestion_commande/frontend-prep/src/api/api.ts
T
2026-03-05 19:09:16 +01:00

1808 lines
50 KiB
TypeScript

// ============================================
// 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<AuthResponse> => {
// ✅ 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<void> => {
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<BasketResponse> => {
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<TrackingResponse> => {
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<ETAResponse> => {
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<ConfirmReceptionResponse> => {
// ✅ 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<number> => {
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<HistoryResponse> => {
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<CancelCommandResponse> => {
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<CancellationHistoryResponse> => {
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<PenaltiesResponse> => {
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<CheckoutCartResponse> => {
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<NotificationsResponse> => {
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 };
}
};