'use client'; // Import toast conditionally to prevent build errors let toast: any; try { // Try to import from the UI components toast = require("@/components/ui/use-toast").toast; } catch (error) { // Fallback toast function if not available toast = { error: (message: string) => console.error(message) }; } // Types type FetchMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; interface FetchOptions { method?: FetchMethod; body?: any; cache?: RequestCache; headers?: HeadersInit; } // Customer types export interface CustomerStats { userId: string; telegramUserId: number; telegramUsername: string; totalOrders: number; totalSpent: number; ordersByStatus: { paid: number; completed: number; acknowledged: number; shipped: number; }; lastOrderDate: string | null; firstOrderDate: string; chatId: number; hasOrders?: boolean; } export interface CustomerResponse { customers: CustomerStats[]; total: number; success?: boolean; } /** * Normalizes a URL to ensure it has the correct /api prefix * This prevents double prefixing which causes API errors */ function normalizeApiUrl(url: string): string { // Remove any existing /api or api prefix const cleanPath = url.replace(/^\/?(api\/)+/, ''); // Add a single /api prefix return `/api/${cleanPath.replace(/^\//, '')}`; } /** * Get the authentication token from cookies or localStorage */ export function getAuthToken(): string | null { if (typeof document === 'undefined') return null; // Guard for SSR return document.cookie .split('; ') .find(row => row.startsWith('Authorization=')) ?.split('=')[1] || localStorage.getItem('Authorization'); } /** * Get a cookie value by name */ export function getCookie(name: string): string | undefined { if (typeof document === 'undefined') return undefined; // Guard for SSR return document.cookie .split('; ') .find(row => row.startsWith(`${name}=`)) ?.split('=')[1]; } /** * Creates standard API request headers with authentication * * @param token Optional auth token (fetched automatically if not provided) * @param customHeaders Additional headers to include * @returns Headers object ready for fetch requests */ function createApiHeaders(token?: string | null, customHeaders: Record = {}): Headers { const headers = new Headers({ 'Content-Type': 'application/json', 'accept': '*/*', ...customHeaders }); const authToken = token || getAuthToken(); if (authToken) { headers.set('authorization', `Bearer ${authToken}`); } return headers; } /** * Simple client-side fetch function for making API calls with Authorization header. * Uses the Next.js API proxy to make requests through the same domain. */ export async function clientFetch(url: string, options: RequestInit = {}): Promise { try { // Create headers with authentication const headers = createApiHeaders(null, options.headers as Record); // Normalize URL to ensure it uses the Next.js API proxy const fullUrl = normalizeApiUrl(url); const res = await fetch(fullUrl, { ...options, headers, credentials: 'include', mode: 'cors', referrerPolicy: 'strict-origin-when-cross-origin' }); if (!res.ok) { const errorData = await res.json().catch(() => ({})); const errorMessage = errorData.message || errorData.error || `Request failed: ${res.status} ${res.statusText}`; throw new Error(errorMessage); } // Handle 204 No Content responses if (res.status === 204) { return {} as T; } return await res.json(); } catch (error) { console.error(`Client fetch error at ${url}:`, error); throw error; } } /** * Enhanced client-side fetch function with error toast notifications. * Use this when you want automatic error handling with toast notifications. */ export async function fetchClient( endpoint: string, options: FetchOptions = {} ): Promise { const { method = 'GET', body, headers = {}, ...rest } = options; // Get the base API URL from environment or fallback const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; // Ensure the endpoint starts with a slash const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; // For the specific case of internal-api.inboxi.ng - remove duplicate /api let url; if (apiUrl.includes('internal-api.inboxi.ng')) { // Special case for internal-api.inboxi.ng if (normalizedEndpoint.startsWith('/api/')) { url = `${apiUrl}${normalizedEndpoint.substring(4)}`; // Remove the /api part } else { url = `${apiUrl}${normalizedEndpoint}`; } } else { // Normal case for other environments url = `${apiUrl}${normalizedEndpoint}`; } // Get auth token from cookies const authToken = getAuthToken(); // Prepare headers with authentication if token exists const requestHeaders: Record = { 'Content-Type': 'application/json', ...(headers as Record), }; if (authToken) { // Backend expects "Bearer TOKEN" format requestHeaders['Authorization'] = `Bearer ${authToken}`; } const fetchOptions: RequestInit = { method, credentials: 'include', headers: requestHeaders, ...rest, }; if (body && method !== 'GET') { fetchOptions.body = JSON.stringify(body); } try { const response = await fetch(url, fetchOptions); if (!response.ok) { const errorData = await response.json().catch(() => ({})); const errorMessage = errorData.message || errorData.error || 'An error occurred'; throw new Error(errorMessage); } if (response.status === 204) { return {} as T; } const data = await response.json(); return data; } catch (error) { console.error('API request failed:', error); // Only show toast if this is a client-side error (not during SSR) if (typeof window !== 'undefined') { const message = error instanceof Error ? error.message : 'Failed to connect to server'; // Handle different toast implementations if (toast?.title && toast?.description) { // shadcn/ui toast format toast({ title: 'Error', description: message, variant: 'destructive', }); } else if (typeof toast?.error === 'function') { // sonner or other simple toast toast.error(message); } else { // Fallback to console console.error('API error:', message); } } throw error; } } // =========== API SERVICES =========== /** * Get a paginated list of customers * @param page Page number (starting from 1) * @param limit Number of items per page * @returns Promise with customers data and total count */ export const getCustomers = async (page: number = 1, limit: number = 25): Promise => { return clientFetch(`/customers?page=${page}&limit=${limit}`); }; /** * Get detailed stats for a specific customer * @param userId The customer's user ID * @returns Promise with detailed customer stats */ export const getCustomerDetails = async (userId: string): Promise => { return clientFetch(`/customers/${userId}`); };