2312 lines
66 KiB
TypeScript
2312 lines
66 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;
|
|
}
|
|
};
|
|
|
|
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,
|
|
invalid_address: !!data.corrected_address,
|
|
suggested_address: data.corrected_address as string | undefined,
|
|
};
|
|
}
|
|
|
|
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;
|
|
client_color_primary: string;
|
|
client_color_secondary: string;
|
|
client_color_success: string;
|
|
client_color_danger: string;
|
|
client_color_warning: string;
|
|
client_title_gradient_from: string;
|
|
client_title_gradient_to: 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: "",
|
|
client_color_primary: "#8b5cf6",
|
|
client_color_secondary: "#22d3ee",
|
|
client_color_success: "#22c55e",
|
|
client_color_danger: "#ef4444",
|
|
client_color_warning: "#f59e0b",
|
|
client_title_gradient_from: "#a78bfa",
|
|
client_title_gradient_to: "#22d3ee",
|
|
};
|
|
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 || "",
|
|
client_color_primary: data.client_color_primary || "#8b5cf6",
|
|
client_color_secondary: data.client_color_secondary || "#22d3ee",
|
|
client_color_success: data.client_color_success || "#22c55e",
|
|
client_color_danger: data.client_color_danger || "#ef4444",
|
|
client_color_warning: data.client_color_warning || "#f59e0b",
|
|
client_title_gradient_from:
|
|
data.client_title_gradient_from || "#a78bfa",
|
|
client_title_gradient_to:
|
|
data.client_title_gradient_to || "#22d3ee",
|
|
};
|
|
} 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;
|
|
type: "free_product" | "half_price_product";
|
|
all_products: boolean;
|
|
product_ids: number[];
|
|
product_names: string[];
|
|
quantity: number;
|
|
};
|
|
|
|
export type RewardItemConfig = {
|
|
product_id: number;
|
|
product_name: string;
|
|
quantity: number;
|
|
price: number;
|
|
type: "free_product" | "half_price_product";
|
|
};
|
|
|
|
export type PointsPoolInfo = {
|
|
key: string;
|
|
name: string;
|
|
points: number;
|
|
rewards_earned: number;
|
|
rewards_claimed: number;
|
|
rewards_available: number;
|
|
eligible_configs: RewardCategoryConfig[];
|
|
eligible_reward_items: RewardItemConfig[];
|
|
};
|
|
|
|
export type PointsRewardConfig = {
|
|
threshold: number;
|
|
description: string;
|
|
reward_items: RewardItemConfig[];
|
|
};
|
|
|
|
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 submitLivreurRating = async (
|
|
orderId: number,
|
|
rating: number,
|
|
comment: string,
|
|
): Promise<{ success: boolean; error?: string }> => {
|
|
const token = getAuthToken();
|
|
if (!token) return { success: false, error: "Non authentifié" };
|
|
try {
|
|
const response = await fetch(`${API_URL}/orders/${orderId}/rate`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({ rating, comment }),
|
|
});
|
|
if (!response.ok) {
|
|
const data = await safeJson(response);
|
|
return { success: false, error: data.error || "Erreur" };
|
|
}
|
|
return { success: true };
|
|
} catch {
|
|
return { success: false, error: "Erreur de connexion" };
|
|
}
|
|
};
|
|
|
|
export const getOrderRatingStatus = async (
|
|
orderId: number,
|
|
): Promise<{ rated: boolean; rating?: number; comment?: string }> => {
|
|
const token = getAuthToken();
|
|
if (!token) return { rated: false };
|
|
try {
|
|
const response = await fetch(`${API_URL}/orders/${orderId}/rating`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
if (!response.ok) return { rated: false };
|
|
const data = await safeJson(response);
|
|
return data;
|
|
} catch {
|
|
return { rated: false };
|
|
}
|
|
};
|
|
|
|
export const claimMyReward = async (
|
|
poolKey: string,
|
|
productId?: number,
|
|
): Promise<{
|
|
success: boolean;
|
|
description?: string;
|
|
remaining_rewards?: number;
|
|
product_added?: boolean;
|
|
product_names?: 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,
|
|
product_id: productId ?? 0,
|
|
}),
|
|
});
|
|
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_names: data.product_names,
|
|
};
|
|
} catch {
|
|
return { success: false, error: "Erreur de connexion" };
|
|
}
|
|
};
|