import apiClient from "./client"; import { API_BASE_URL } from "./client"; import type { ConfirmReceptionResponse, CheckoutCartResponse, HistoryResponse, ETAResponse, TrackingResponse, CancelCommandResponse, PenaltiesResponse, ReferralBalanceResponse, } from "./api_types"; import { getToken } from "../auth/tokenStorage"; import { extractUsernameFromToken } from "../auth/jwtUtils"; const V1 = `${API_BASE_URL}/api/v1`; export const getJwtUsername = async (): Promise => { const token = await getToken(); if (!token) return null; return extractUsernameFromToken(token); }; 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 loginUser = async ( username: string, password: string, ): Promise => { try { const { data } = await apiClient.post(`${V1}/auth/login`, { username, password, }); if (data.requires_2fa) { return { success: false, requires_2fa: true, session_token: data.session_token, }; } if (!data.access_token) { return { success: false, message: "Token non reçu du serveur" }; } return { success: true, message: "Connexion réussie", access_token: data.access_token, token_type: data.token_type, expires_in: data.expires_in, user: data.user, }; } catch (error: any) { const msg = error.response?.data?.error || error.message || "Erreur de connexion"; return { success: false, message: msg }; } }; export const verifyClient2FA = async ( sessionToken: string, code: string, ): Promise => { try { const { data } = await apiClient.post(`${V1}/auth/2fa/verify`, { session_token: sessionToken, code, }); if (!data.access_token) { return { success: false, message: "Token non reçu du serveur" }; } return { success: true, message: "Connexion réussie", access_token: data.access_token, token_type: data.token_type, expires_in: data.expires_in, user: data.user, }; } catch (error: any) { const msg = error.response?.data?.error || error.message || "Code invalide"; return { success: false, message: msg }; } }; export const registerUser = async ( username: string, password: string, nom: string, prenom: string, telephone: string, ): Promise => { try { const { data } = await apiClient.post(`${V1}/auth/register`, { username, password, nom, prenom, telephone, }); if (!data.access_token) { return { success: false, message: "Token non reçu du serveur" }; } return { success: true, message: "Inscription réussie", access_token: data.access_token, token_type: data.token_type, expires_in: data.expires_in, user: data.user, }; } catch (error: any) { const msg = error.response?.data?.error || error.message || "Erreur d'inscription"; return { success: false, message: msg }; } }; export const logoutUser = async (): Promise => { try { await apiClient.post(`${V1}/auth/logout`); } catch { /* ignore */ } }; export interface Category { id: number; name: string; color: string; is_coming_soon: boolean; created_at: string; } export const getCategories = async (): Promise => { try { const { data } = await apiClient.get(`${V1}/categories`); return data.categories || []; } catch { return []; } }; export const getAllProducts = async () => { try { const { data } = await apiClient.get(`${V1}/products`); return data; } catch { return { success: false, data: [] }; } }; export const getProductsByCategory = async (category: string) => { try { const { data } = await apiClient.get( `${V1}/products/category/${category}`, ); return data; } catch { return { success: false, data: [] }; } }; export const getProductById = async (id: number) => { try { const { data } = await apiClient.get(`${V1}/products/${id}`); return data; } catch { return { success: false, data: null }; } }; export const getCart = async (username: string) => { const jwtUsername = await getJwtUsername(); if (!jwtUsername || jwtUsername !== username) { return { success: false, message: "Accès refusé", panier: [] }; } try { const { data } = await apiClient.get(`${V1}/panier/${jwtUsername}`); return { success: true, panier: data.panier || [], message: data.message, }; } catch (error: any) { return { success: false, message: error.response?.data?.error || "Erreur récupération", panier: [], }; } }; export const addToCart = async (cartItem: { username?: string; product_id?: number; name_product: string; category: string; quantity: number; price: number; }) => { const jwtUsername = await getJwtUsername(); if (!jwtUsername) return { success: false, message: "Session invalide" }; cartItem.username = jwtUsername; try { const { data } = await apiClient.post(`${V1}/panier/add`, cartItem); return { success: true, message: "Produit ajouté", panier: data.panier, }; } catch (error: any) { return { success: false, message: error.response?.data?.error || "Erreur ajout", }; } }; export const removeFromCart = async (id: number, username: string) => { const jwtUsername = await getJwtUsername(); if (!jwtUsername || jwtUsername !== username) return { success: false, message: "Accès refusé" }; try { const { data } = await apiClient.delete(`${V1}/panier/remove`, { data: { id, username: jwtUsername }, }); return { success: true, message: "Produit supprimé" }; } catch (error: any) { return { success: false, message: error.response?.data?.error || "Erreur suppression", }; } }; export const clearCart = async (username: string) => { const jwtUsername = await getJwtUsername(); if (!jwtUsername || jwtUsername !== username) return { success: false, message: "Accès refusé" }; try { const { data } = await apiClient.delete(`${V1}/panier/clear`); return { success: true, message: data.message || "Panier vidé", stock_released: data.stock_released, }; } catch (error: any) { return { success: false, message: error.response?.data?.error || "Erreur vidage", }; } }; export const getMyOrders = async () => { try { const { data } = await apiClient.get(`${V1}/my-commands`); return { success: true, commands: data.commands || [] }; } catch { return { success: false, message: "Erreur récupération", commands: [] }; } }; export const createCheckout = async (checkoutData: { username?: string; delivery_address: string; }) => { const jwtUsername = await getJwtUsername(); if (!jwtUsername) return { success: false, message: "Session invalide" }; checkoutData.username = jwtUsername; try { const { data } = await apiClient.post(`${V1}/checkout`, checkoutData); return { success: true, message: "Commande créée", command_id: data.command_id, command: data.command, delivery_address: data.delivery_address, assigned_to: data.assigned_to, queue_info: data.queue_info, }; } catch (error: any) { return { success: false, message: error.response?.data?.error || "Erreur création", }; } }; export const checkoutCart = async ( delivery_address: string, nom?: string, prenom?: string, telephone?: string, use_referral_balance?: boolean, payment_method?: string, pay_currency?: string, ): Promise => { const jwtUsername = await getJwtUsername(); if (!jwtUsername) return { success: false, message: "Session invalide" }; if (!delivery_address.trim()) return { success: false, message: "Veuillez saisir une adresse de livraison", }; try { const payload: Record = { username: jwtUsername, delivery_address, use_referral_balance: use_referral_balance ?? false, }; if (nom) payload.nom = nom; if (prenom) payload.prenom = prenom; if (telephone) payload.telephone = telephone; if (payment_method) payload.payment_method = payment_method; if (pay_currency) payload.pay_currency = pay_currency; const { data } = await apiClient.post(`${V1}/checkout`, payload); 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, referral_used: data.referral_used, referral_balance: data.referral_balance, payment_method: data.payment_method, payment_status: data.payment_status, pay_address: data.pay_address, pay_amount: data.pay_amount, pay_currency: data.pay_currency, price_amount: data.price_amount, price_currency: data.price_currency, }; } catch (error: any) { const data = error.response?.data; if (data?.corrected_address) { return { success: false, invalid_address: true, suggested_address: data.corrected_address, message: data.error || "Adresse non reconnue", }; } return { success: false, message: data?.error || "Erreur serveur", }; } }; export const getReferralBalance = async (): Promise => { try { const { data } = await apiClient.get(`${V1}/referral/balance`); return { success: true, balance: data.balance ?? 0 }; } catch { return { success: false, balance: 0, message: "Erreur récupération solde", }; } }; export const approveDelivery = async ( commandId: number, reqData?: { rating?: number; comment?: string }, ) => { try { const { data } = await apiClient.post( `${V1}/commands/${commandId}/approve`, reqData || {}, ); return { success: true, message: "Commande approuvée", points_earned: data.points_earned, }; } catch (error: any) { return { success: false, message: error.response?.data?.error || "Erreur approbation", }; } }; export const confirmReception = async ( commandId: number, ): Promise => { try { const { data } = await apiClient.post( `${V1}/commands/${commandId}/approve`, {}, ); return { success: true, message: "Commande confirmée avec succès", points_earned: data.points_earned || 0, category: data.category, data, }; } catch (error: any) { return { success: false, message: error.response?.data?.error || "Erreur lors de la confirmation", }; } }; export const respondToAddressProposal = async ( commandId: number, accepted: boolean, ): Promise<{ success: boolean; message: string }> => { try { const { data } = await apiClient.post( `${V1}/commands/${commandId}/address/respond`, { accepted }, ); return { success: true, message: data.message || "Réponse enregistrée", }; } catch (error: any) { return { success: false, message: error.response?.data?.error || "Erreur lors de la réponse", }; } }; export const getOrderTracking = async ( commandId: number, ): Promise => { try { const { data } = await apiClient.get( `${V1}/commands/${commandId}/track`, ); 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: any) { return { success: false, command_id: commandId, status: "error", current_step: "Erreur", message: error.response?.data?.error || "Erreur récupération suivi", }; } }; export const getOrderETA = async (commandId: number): Promise => { try { const { data } = await apiClient.get(`${V1}/commands/${commandId}/eta`); return { success: true, command_id: data.id || commandId, eta_minutes: data.eta_minutes || 0, estimated_arrival: data.estimated_arrival || "N/A", status: data.status || "pending", livreur_distance: data.livreur_distance, message: "ETA récupéré", }; } catch { return { success: false, command_id: commandId, eta_minutes: 0, estimated_arrival: "N/A", status: "error", }; } }; export const getOrdersWithTracking = async () => { try { const { data } = await apiClient.get(`${V1}/my-commands`); return { success: true, commands: data.commands || [], count: data.count || 0, }; } catch { return { success: false, commands: [], message: "Erreur récupération" }; } }; // ============================================ // HISTORY // ============================================ export const getMyCompletedOrders = async (): Promise => { try { const { data } = await apiClient.get(`${V1}/my-commands/history`); return { success: true, commands: data.commands || [], count: data.count || 0, client_stats: data.client_stats, }; } catch { return { success: false, commands: [], count: 0, message: "Erreur récupération", }; } }; export const cancelCommand = async ( commandId: number, reason?: string, force = false, ): Promise => { try { const { data } = await apiClient.post( `${V1}/commands/${commandId}/cancel`, { reason: reason || "Annulation par le client", force, }, ); 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: any) { const resp = error.response; if (resp?.status === 409 && resp?.data?.warning) { return { success: false, warning: true, message: resp.data.message, details: resp.data.details, penalty_warning: resp.data.penalty_warning, action_required: resp.data.action_required, }; } return { success: false, message: resp?.data?.error || "Erreur lors de l'annulation", }; } }; // ============================================ // PENALTIES // ============================================ export const changePassword = async ( currentPassword: string, newPassword: string, ): Promise<{ success: boolean; message?: string }> => { try { await apiClient.put(`${V1}/auth/change-password`, { current_password: currentPassword, new_password: newPassword, }); return { success: true }; } catch (error: any) { return { success: false, message: error.response?.data?.error || "Erreur lors du changement de mot de passe", }; } }; export const getMyPenalties = async (): Promise => { try { const { data } = await apiClient.get(`${V1}/penalties`); return { success: true, data: data.data }; } catch { return { success: false, message: "Erreur récupération" }; } }; // ============================================ // 👤 PROFIL CLIENT // ============================================ export const getMyProfile = async (): Promise<{ success: boolean; client?: any; message?: string; }> => { try { const { data } = await apiClient.get(`${V1}/profile`); return { success: true, client: data.client }; } catch { return { success: false, message: "Erreur récupération profil" }; } }; export const updateMyProfile = async (fields: { nom?: string; prenom?: string; telephone?: string; }): Promise<{ success: boolean; message?: string; client?: any }> => { try { const { data } = await apiClient.put(`${V1}/profile/update`, fields); return { success: true, client: data.client, message: data.message }; } catch (error: any) { return { success: false, message: error.response?.data?.error || error.response?.data?.message || "Erreur mise à jour profil", }; } }; // ============================================ // ORDER DETAILS // ============================================ export const getOrderDetails = async (commandId: number) => { try { const { data } = await apiClient.get(`${V1}/commands/${commandId}`); return { success: true, order: data.command || data, message: "Commande récupérée", }; } catch (error: any) { return { success: false, message: error.response?.data?.error || "Erreur de connexion", order: null, }; } }; export const getCommandItemsWithDetails = async (commandId: number) => { try { const { data } = await apiClient.get( `${V1}/commands/${commandId}/items`, ); return { success: true, data, message: "Items récupérés" }; } catch (error: any) { return { success: false, message: error.response?.data?.error || "Erreur de connexion", data: null, }; } }; // ============================================ // UTILS // ============================================ export const formatOrderDate = (dateString: string): string => { try { const date = new Date(dateString); if (isNaN(date.getTime())) return ""; return date.toLocaleDateString("fr-FR", { day: "2-digit", month: "long", year: "numeric", hour: "2-digit", minute: "2-digit", }); } catch { return ""; } }; export const getOrderAge = (dateString: string): string => { try { const diffMs = Date.now() - new Date(dateString).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 w = Math.floor(diffDays / 7); return `Il y a ${w} semaine${w > 1 ? "s" : ""}`; } const m = Math.floor(diffDays / 30); return `Il y a ${m} mois`; } catch { return "Date inconnue"; } }; export const formatPrice = (price: number): string => `${price.toFixed(2)} €`; // ============================================ // NOTIFICATIONS // ============================================ 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; message?: string; } export const getClientNotifications = async (): Promise => { try { const { data } = await apiClient.get(`${V1}/notifications`); 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, message: "Erreur récupération notifications", }; } }; export const markNotificationsRead = async (): Promise<{ success: boolean; marked_count?: number; }> => { try { const { data } = await apiClient.post(`${V1}/notifications/read`); return { success: true, marked_count: data.marked_count }; } catch { return { success: false }; } }; // ============================================ // PUBLIC SETTINGS // ============================================ export interface PublicSettings { penalties_enabled: boolean; show_amende_score: boolean; points_enabled: boolean; points_separated: boolean; referral_enabled: boolean; pool_names: string[]; crypto_payment_enabled: boolean; crypto_only: boolean; nowpayments_currencies: string[]; telegram_notifications_enabled: boolean; 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; } export const getPublicSettings = async (): Promise => { const defaults: PublicSettings = { penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: true, pool_names: ["Pool 1", "Pool 2"], crypto_payment_enabled: false, crypto_only: false, nowpayments_currencies: [], telegram_notifications_enabled: false, two_fa_enabled: false, contact_telegram: "", client_color_primary: "#7c3aed", client_color_secondary: "#22d3ee", client_color_success: "#4ade80", client_color_danger: "#ef4444", client_color_warning: "#f59e0b", }; try { const { data } = await apiClient.get(`${V1}/app-settings`); return { penalties_enabled: data.penalties_enabled ?? true, show_amende_score: data.show_amende_score ?? true, points_enabled: data.points_enabled ?? true, points_separated: data.points_separated ?? true, referral_enabled: data.referral_enabled ?? true, pool_names: Array.isArray(data.pool_names) && data.pool_names.length > 0 ? data.pool_names : defaults.pool_names, crypto_payment_enabled: data.crypto_payment_enabled ?? false, crypto_only: data.crypto_only ?? false, nowpayments_currencies: Array.isArray(data.nowpayments_currencies) ? data.nowpayments_currencies : [], telegram_notifications_enabled: data.telegram_notifications_enabled ?? false, two_fa_enabled: data.two_fa_enabled ?? false, contact_telegram: data.contact_telegram || "", client_color_primary: data.client_color_primary || "#7c3aed", client_color_secondary: data.client_color_secondary || "#22d3ee", client_color_success: data.client_color_success || "#4ade80", client_color_danger: data.client_color_danger || "#ef4444", client_color_warning: data.client_color_warning || "#f59e0b", }; } catch { return defaults; } }; 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 => { try { const { data } = await apiClient.get( `${V1}/commands/${commandId}/payment-status`, ); return data; } catch { return null; } }; // ============================================ // TELEGRAM // ============================================ export const getTelegramStatus = async (): Promise<{ linked: boolean; enabled: boolean; }> => { try { const { data } = await apiClient.get(`${V1}/telegram/status`); return data; } catch { return { linked: false, enabled: false }; } }; export const generateTelegramLinkToken = async (): Promise<{ token?: string; link_url?: string; expires_in?: number; error?: string; }> => { try { const { data } = await apiClient.post(`${V1}/telegram/link-token`); return data; } catch (e: any) { return { error: e?.response?.data?.error || "Erreur" }; } }; export const unlinkTelegram = async (): Promise => { try { await apiClient.delete(`${V1}/telegram/unlink`); } catch { // silencieux } }; export const get2FAStatus = async (): Promise<{ two_fa_enabled: boolean; telegram_linked: boolean; admin_2fa_enabled: boolean; }> => { try { const { data } = await apiClient.get(`${V1}/two-fa/status`); return data; } catch { return { two_fa_enabled: false, telegram_linked: false, admin_2fa_enabled: false, }; } }; export const toggle2FA = async ( enabled: boolean, ): Promise<{ success: boolean; error?: string }> => { try { const { data } = await apiClient.post(`${V1}/two-fa/toggle`, { enabled, }); return { success: data.success ?? true }; } catch (e: any) { return { success: false, error: e?.response?.data?.error || "Erreur" }; } }; // ============================================ // 🏆 POINTS — RÉCOMPENSES // ============================================ export type RewardCategoryConfig = { category: string; all_products: boolean; product_ids: number[]; product_names: string[]; amount: number; }; export type RewardItemConfig = { product_id: number; product_name: string; quantity: number; price: number; }; 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; type: string; description: string; reward_items: RewardItemConfig[]; }; export const getMyPointsRewards = async (): Promise<{ success: boolean; enabled: boolean; pools: PointsPoolInfo[]; reward: PointsRewardConfig | null; }> => { try { const { data } = await apiClient.get(`${V1}/points/rewards`); 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, productId?: number, ): Promise<{ success: boolean; description?: string; remaining_rewards?: number; product_added?: boolean; product_name?: string; error?: string; }> => { try { const body: { pool_key: string; product_id?: number } = { pool_key: poolKey, }; if (productId !== undefined) body.product_id = productId; const { data } = await apiClient.post(`${V1}/points/claim`, body); return { success: true, description: data.description, remaining_rewards: data.remaining_rewards, product_added: data.product_added, product_name: data.product_name, }; } catch (e: any) { return { success: false, error: e?.response?.data?.error || "Erreur" }; } }; export const submitLivreurRating = async ( orderId: number, rating: number, comment: string, ): Promise<{ success: boolean; error?: string }> => { try { await apiClient.post(`${V1}/orders/${orderId}/rate`, { rating, comment, }); return { success: true }; } catch (e: any) { return { success: false, error: e?.response?.data?.error || "Erreur" }; } }; export const getOrderRatingStatus = async ( orderId: number, ): Promise<{ rated: boolean; rating?: number; comment?: string }> => { try { const { data } = await apiClient.get(`${V1}/orders/${orderId}/rating`); return data; } catch { return { rated: false }; } }; export const calculateOrderTotal = (order: any): number => { if (typeof order.total_prix === "number" && order.total_prix > 0) return order.total_prix; if (typeof order.total === "number" && order.total > 0) return order.total; if (Array.isArray(order.items) && order.items.length > 0) { return order.items.reduce((sum: number, item: any) => { return ( sum + (item.prix || item.price || 0) * (item.quantite || item.quantity || 1) ); }, 0); } return 0; };