const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001/api"; // ─── Auth token storage ───────────────────────────────────────────────────── export const getToken = (): string | null => { if (typeof window === "undefined") return null; return localStorage.getItem("mw_access"); }; export const setToken = (t: string) => localStorage.setItem("mw_access", t); export const getRefresh = (): string | null => { if (typeof window === "undefined") return null; return localStorage.getItem("mw_refresh"); }; export const setRefresh = (t: string) => localStorage.setItem("mw_refresh", t); export const clearAuth = () => { localStorage.removeItem("mw_access"); localStorage.removeItem("mw_refresh"); localStorage.removeItem("mw_user"); }; export const getUser = (): any | null => { if (typeof window === "undefined") return null; try { return JSON.parse(localStorage.getItem("mw_user") ?? "null"); } catch { return null; } }; export const setUser = (u: any) => localStorage.setItem("mw_user", JSON.stringify(u)); // ─── Fetch wrapper ────────────────────────────────────────────────────────── async function request(path: string, opts: RequestInit = {}): Promise { const token = getToken(); const headers: Record = { "Content-Type": "application/json", ...(opts.headers as Record), }; if (token) headers["Authorization"] = `Bearer ${token}`; const res = await fetch(`${API_BASE}${path}`, { ...opts, headers }); // Token expirado — intentar refresh if (res.status === 401 && getRefresh()) { const refreshed = await tryRefresh(); if (refreshed) { headers["Authorization"] = `Bearer ${getToken()}`; const retry = await fetch(`${API_BASE}${path}`, { ...opts, headers }); if (!retry.ok) throw await extractError(retry); return retry.json(); } else { clearAuth(); window.location.href = "/login"; throw new Error("Session expired"); } } if (!res.ok) throw await extractError(res); if (res.status === 204) return undefined as T; return res.json(); } async function extractError(res: Response): Promise { try { const body = await res.json(); return new Error(body.message ?? "Error del servidor"); } catch { return new Error(`HTTP ${res.status}`); } } async function tryRefresh(): Promise { const token = getRefresh(); if (!token) return false; try { const res = await fetch(`${API_BASE}/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken: token }), }); if (!res.ok) return false; const data = await res.json(); setToken(data.accessToken); setRefresh(data.refreshToken); return true; } catch { return false; } } // ─── Auth ─────────────────────────────────────────────────────────────────── export const api = { auth: { register: (body: any) => request("/auth/register", { method: "POST", body: JSON.stringify(body) }), login: (body: any) => request("/auth/login", { method: "POST", body: JSON.stringify(body) }), me: () => request("/auth/me"), updateProfile: (body: { firstName?: string; lastName?: string; phone?: string }) => request("/auth/me", { method: "PATCH", body: JSON.stringify(body) }), changePassword: (body: { oldPassword: string; newPassword: string }) => request("/auth/password", { method: "PATCH", body: JSON.stringify(body) }), logout: (refreshToken: string) => request("/auth/logout", { method: "POST", body: JSON.stringify({ refreshToken }) }), setupMfa: () => request("/auth/mfa/setup", { method: "POST" }), verifyMfa: (totpCode: string) => request("/auth/mfa/verify", { method: "POST", body: JSON.stringify({ totpCode }) }), disableMfa: (totpCode: string) => request("/auth/mfa/disable", { method: "POST", body: JSON.stringify({ totpCode }) }), }, packages: { list: (params?: Record) => request("/packages" + (params ? "?" + new URLSearchParams(params) : "")), get: (id: string) => request(`/packages/${id}`), create: (body: any) => request("/packages", { method: "POST", body: JSON.stringify(body) }), updateStatus: (id: string, body: any) => request(`/packages/${id}/status`, { method: "PATCH", body: JSON.stringify(body) }), verify: (id: string, body: any) => request(`/packages/${id}/verify`, { method: "PATCH", body: JSON.stringify(body) }), pendingDeclaration: () => request("/packages/pending-declaration"), uploadPhotos: (id: string, formData: FormData) => { const token = getToken(); return fetch(`${API_BASE}/packages/${id}/photos`, { method: "POST", headers: token ? { Authorization: `Bearer ${token}` } : {}, body: formData, }).then(r => { if (!r.ok) throw new Error("Upload failed"); return r.json(); }); }, senaeDeclare: (id: string, body: any) => request(`/packages/${id}/senae/declare`, { method: "POST", body: JSON.stringify(body) }), register: (body: any) => request("/packages/register", { method: "POST", body: JSON.stringify(body) }), }, preAlerts: { list: () => request("/pre-alerts"), create: (body: any) => request("/pre-alerts", { method: "POST", body: JSON.stringify(body) }), delete: (id: string) => request(`/pre-alerts/${id}`, { method: "DELETE" }), updateStatus: (id: string, body: any) => request(`/pre-alerts/${id}/status`, { method: "PATCH", body: JSON.stringify(body) }), uploadInvoice: (id: string, file: File) => { const token = getToken(); const fd = new FormData(); fd.append("invoice", file); return fetch(`${API_BASE}/pre-alerts/${id}/invoice`, { method: "POST", headers: token ? { Authorization: `Bearer ${token}` } : {}, body: fd, }).then(r => { if (!r.ok) throw new Error("Upload failed"); return r.json(); }); }, }, users: { list: (search?: string) => request("/users" + (search ? `?search=${search}` : "")), get: (id: string) => request(`/users/${id}`), create: (body: { email: string; password: string; firstName: string; lastName: string; phone?: string; role: string }) => request("/users", { method: "POST", body: JSON.stringify(body) }), updateRole: (id: string, role: string) => request(`/users/${id}/role`, { method: "PATCH", body: JSON.stringify({ role }) }), setActive: (id: string, isActive: boolean) => request(`/users/${id}/active`, { method: "PATCH", body: JSON.stringify({ isActive }) }), }, tracking: { search: (id: string) => request(`/tracking/${id}`), }, calculator: { calculate: (params: Record) => request("/calculator?" + new URLSearchParams(params)), }, b2b: { create: (body: any) => request("/b2b", { method: "POST", body: JSON.stringify(body) }), list: () => request("/b2b"), updateStatus: (id: string, body: any) => request(`/b2b/${id}/status`, { method: "PATCH", body: JSON.stringify(body) }), }, tariffs: { get: () => request("/tariffs"), update: (body: any) => request("/tariffs", { method: "PUT", body: JSON.stringify(body) }), }, auditLogs: { list: (params?: Record) => request("/audit-logs" + (params ? "?" + new URLSearchParams(params) : "")), }, products: { scan: (url: string) => request("/products/scan", { method: "POST", body: JSON.stringify({ url }) }), }, warehouses: { list: () => request("/warehouses"), getDefault: () => request("/warehouses/default"), get: (id: string) => request(`/warehouses/${id}`), create: (body: any) => request("/warehouses", { method: "POST", body: JSON.stringify(body) }), update: (id: string, body: any) => request(`/warehouses/${id}`, { method: "PATCH", body: JSON.stringify(body) }), setDefault: (id: string) => request(`/warehouses/${id}/set-default`, { method: "PATCH" }), remove: (id: string) => request(`/warehouses/${id}`, { method: "DELETE" }), }, integrations: { list: () => request("/integrations"), status: () => request("/integrations/status"), batchUpsert: (items: Array<{ key: string; value: string | null; isActive: boolean }>) => request("/integrations/batch", { method: "PUT", body: JSON.stringify({ items }) }), }, notificationTemplates: { list: () => request("/notification-templates"), seed: () => request("/notification-templates/seed", { method: "POST" }), update: (id: string, body: { body: string; subject?: string; isActive?: boolean }) => request(`/notification-templates/${id}`, { method: "PUT", body: JSON.stringify(body) }), }, notifications: { list: (limit = 20) => request(`/notifications?limit=${limit}`), }, payments: { list: (status?: string) => request(`/payments${status ? `?status=${status}` : ""}`), packageDetail: (packageId: string) => request(`/payments/package/${packageId}`), byTracking: (trackingId: string) => request(`/payments/track/${trackingId}`), createIntent: (packageId: string, provider = "stripe") => request("/payments/intent", { method: "POST", body: JSON.stringify({ packageId, provider }) }), confirm: (paymentId: string) => request(`/payments/${paymentId}/confirm`, { method: "POST" }), confirmSession: (sessionId: string) => request("/payments/confirm-session", { method: "POST", body: JSON.stringify({ sessionId }) }), }, consolidations: { list: () => request("/consolidations"), get: (id: string) => request(`/consolidations/${id}`), create: (notes?: string) => request("/consolidations", { method: "POST", body: JSON.stringify({ notes }) }), addPackage: (id: string, packageId: string) => request(`/consolidations/${id}/packages`, { method: "POST", body: JSON.stringify({ packageId }) }), removePackage: (id: string, packageId: string) => request(`/consolidations/${id}/packages/${packageId}`, { method: "DELETE" }), close: (id: string, courierTracking?: string) => request(`/consolidations/${id}/close`, { method: "POST", body: JSON.stringify({ courierTracking }) }), dispatch: (id: string, courierTracking: string) => request(`/consolidations/${id}/dispatch`, { method: "POST", body: JSON.stringify({ courierTracking }) }), }, };