Files
projet_gestion_commande/frontend-prep/src/api/api.ts
T
2026-06-13 14:23:23 +02:00

2228 lines
63 KiB
TypeScript

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,
HistoryResponse,
ETAResponse,
TrackingResponse,
CancelCommandResponse,
PenaltiesResponse,
} from "./api_types";
export interface AuthResponse {
success: boolean;
message?: string;
access_token?: string;
token_type?: string;
expires_in?: number;
requires_2fa?: boolean;
session_token?: string;
user?: {
id: number;
username: string;
nom?: string;
prenom?: string;
telephone?: string;
role?: string;
session_id?: string;
must_change_password?: boolean;
};
}
export const extractUsernameFromToken = (): string | null => {
try {
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 safeJson(response);
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 safeJson(response);
console.log("📋 [LOGIN] Réponse:", data);
// 2FA requis — retourner sans token
if (data.requires_2fa) {
return {
success: true,
requires_2fa: true,
session_token: data.session_token,
};
}
// ✅ 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 safeJson(response);
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?: Record<string, unknown>[];
data?: { panier?: Record<string, unknown>[] };
}
/**
* ✅ 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}`,
},
});
if (!response.ok) {
return {
success: false,
message: "Erreur récupération",
panier: [],
};
}
const responseData = await safeJson(response);
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 safeJson(response);
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 safeJson(response);
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 safeJson(response);
console.error("❌ [CLEAR] Erreur API:", errorData);
return {
success: false,
message: errorData.error || "Erreur vidage",
};
} catch {
// 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 safeJson(response);
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 safeJson(response);
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;
pay_currency?: string;
use_referral_balance?: boolean;
}
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,
use_referral_balance:
checkoutData.use_referral_balance ?? false,
}),
});
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,
};
}
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,
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,
referral_used: data.referral_used as number | undefined,
referral_balance: data.referral_balance as number | undefined,
client_order_number: data.client_order_number as number | undefined,
};
} 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 safeJson(response);
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; active_price?: boolean }>;
media?: MediaItem[]; // ✅ CHANGÉ: string[] → MediaItem[]
coming_soon?: boolean;
}
export interface Category {
id: number;
name: string;
color: string;
is_coming_soon: boolean;
created_at: string;
}
export const getCategories = async (): Promise<Category[]> => {
try {
const response = await fetch(`${API_URL}/categories`);
const data = await safeJson(response);
return data.categories || [];
} catch {
return [];
}
};
export const getAllProducts = async () => {
try {
const response = await fetch(`${API_URL}/products`);
return await safeJson(response);
} 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 {
const response = await fetch(
`${API_URL}/products/category/${category}`,
);
return await safeJson(response);
} 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 safeJson(response);
} 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}/tracking`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
},
);
const data = await safeJson(response);
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 safeJson(response);
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 || "",
status: data.status || "pending",
eta_available: data.eta_available === true,
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 safeJson(response);
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 safeJson(response);
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 safeJson(response);
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: Record<string, unknown>): 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 as Record<string, unknown>[]).reduce(
(sum: number, item) => {
const price = Number(item.prix || item.price || 0);
const quantity = Number(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 safeJson(response);
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 safeJson(response);
// ⚠️ 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 safeJson(response);
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 safeJson(response);
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 safeJson(response);
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 safeJson(response);
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 safeJson(response);
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 safeJson(response);
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 };
}
};
export interface PublicSettings {
penalties_enabled: boolean;
show_amende_score: boolean;
points_enabled: boolean;
points_separated: boolean;
referral_enabled: boolean;
referral_amount: number;
pool_names: string[];
crypto_payment_enabled: boolean;
crypto_only: boolean;
nowpayments_currencies: string[];
shop_name: string;
two_fa_enabled: boolean;
contact_telegram: string;
}
export const getPublicSettings = async (): Promise<PublicSettings> => {
const defaults: PublicSettings = {
penalties_enabled: true,
show_amende_score: true,
points_enabled: true,
points_separated: true,
referral_enabled: true,
referral_amount: 0,
pool_names: ["Pool 1", "Pool 2"],
crypto_payment_enabled: false,
crypto_only: false,
nowpayments_currencies: [],
shop_name: "Milieu-Nantais",
two_fa_enabled: false,
contact_telegram: "",
};
try {
const response = await fetch(`${API_URL}/app-settings`);
if (!response.ok) return defaults;
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,
referral_amount: data.referral_amount ?? 0,
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
: [],
shop_name: data.shop_name || "Milieu-Nantais",
two_fa_enabled: data.two_fa_enabled ?? false,
contact_telegram: data.contact_telegram || "",
};
} catch {
return defaults;
}
};
export const verify2FA = async (
sessionToken: string,
code: string,
): Promise<AuthResponse> => {
try {
const response = await fetch(`${API_URL}/auth/2fa/verify`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session_token: sessionToken, code }),
});
const data = await safeJson(response);
if (!response.ok) {
return { success: false, message: data.error || "Code invalide" };
}
sessionStorage.setItem("token", data.access_token);
syncUsernameFromJWT();
return {
success: true,
access_token: data.access_token,
token_type: data.token_type,
expires_in: data.expires_in,
user: data.user,
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : "Erreur",
};
}
};
export const get2FAStatus = async (): Promise<{
two_fa_enabled: boolean;
telegram_linked: boolean;
admin_2fa_enabled: boolean;
}> => {
const token = sessionStorage.getItem("token");
try {
const response = await fetch(`${API_URL}/two-fa/status`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok)
return {
two_fa_enabled: false,
telegram_linked: false,
admin_2fa_enabled: false,
};
return await safeJson(response);
} catch {
return {
two_fa_enabled: false,
telegram_linked: false,
admin_2fa_enabled: false,
};
}
};
export const toggle2FA = async (
enabled: boolean,
): Promise<{ success: boolean; error?: string }> => {
const token = sessionStorage.getItem("token");
try {
const response = await fetch(`${API_URL}/two-fa/toggle`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ enabled }),
});
const data = await safeJson(response);
if (!response.ok) return { success: false, error: data.error };
return { success: true };
} catch {
return { success: false, error: "Erreur réseau" };
}
};
export interface CryptoPaymentStatus {
command_id: number;
client_order_number?: 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<CryptoPaymentStatus | null> => {
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<ReferralBalanceResponse> => {
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, message: "Non authentifié" };
try {
const response = await fetch(`${API_URL}/profile`, {
headers: { Authorization: `Bearer ${token}` },
});
const data = await safeJson(response);
return data;
} catch {
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" };
}
};
// ============================================
// 🤖 TELEGRAM
// ============================================
export const getTelegramStatus = async (): Promise<{
linked: boolean;
enabled: boolean;
}> => {
const token = getAuthToken();
if (!token) return { linked: false, enabled: false };
try {
const response = await fetch(`${API_URL}/telegram/status`, {
headers: { Authorization: `Bearer ${token}` },
});
return await safeJson(response);
} catch {
return { linked: false, enabled: false };
}
};
export const generateTelegramLinkToken = async (): Promise<{
link_url?: string;
error?: string;
}> => {
const token = getAuthToken();
if (!token) return { error: "Non authentifié" };
try {
const response = await fetch(`${API_URL}/telegram/link-token`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
return await safeJson(response);
} catch {
return { error: "Erreur de connexion" };
}
};
export const unlinkTelegram = async (): Promise<void> => {
const token = getAuthToken();
if (!token) return;
try {
await fetch(`${API_URL}/telegram/unlink`, {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});
} catch {
/* silencieux */
}
};
// ============================================
// 🏆 POINTS — RÉCOMPENSES
// ============================================
export type RewardCategoryConfig = {
category: string;
all_products: boolean;
product_ids: number[];
product_names: string[];
amount: number;
};
export type PointsPoolInfo = {
key: string;
name: string;
points: number;
rewards_earned: number;
rewards_claimed: number;
rewards_available: number;
eligible_configs: RewardCategoryConfig[];
};
export type PointsRewardConfig = {
threshold: number;
type: string;
description: string;
};
export const getMyPointsRewards = async (): Promise<{
success: boolean;
enabled: boolean;
pools: PointsPoolInfo[];
reward: PointsRewardConfig | null;
}> => {
const token = getAuthToken();
if (!token) return { success: false, enabled: false, pools: [], reward: null };
try {
const response = await fetch(`${API_URL}/points/rewards`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) return { success: false, enabled: false, pools: [], reward: null };
const data = await safeJson(response);
return {
success: true,
enabled: data.enabled ?? false,
pools: data.pools ?? [],
reward: data.reward ?? null,
};
} catch {
return { success: false, enabled: false, pools: [], reward: null };
}
};
export const claimMyReward = async (poolKey: string): Promise<{
success: boolean;
description?: string;
remaining_rewards?: number;
product_added?: boolean;
product_name?: string;
error?: string;
}> => {
const token = getAuthToken();
if (!token) return { success: false, error: "Non authentifié" };
try {
const response = await fetch(`${API_URL}/points/claim`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ pool_key: poolKey }),
});
const data = await safeJson(response);
if (!response.ok) return { success: false, error: data.error || "Erreur" };
return {
success: true,
description: data.description,
remaining_rewards: data.remaining_rewards,
product_added: data.product_added,
product_name: data.product_name,
};
} catch {
return { success: false, error: "Erreur de connexion" };
}
};