Refactor UI imports and update component paths
Some checks failed
Build Frontend / build (push) Failing after 7s
Some checks failed
Build Frontend / build (push) Failing after 7s
Replaces imports from 'components/ui' with 'components/common' across the app and dashboard pages, and updates model and API imports to use new paths under 'lib'. Removes redundant authentication checks from several dashboard pages. Adds new dashboard components and utility files, and reorganizes hooks and services into the 'lib' directory for improved structure.
This commit is contained in:
515
lib/api/api-client.ts
Normal file
515
lib/api/api-client.ts
Normal file
@@ -0,0 +1,515 @@
|
||||
'use client';
|
||||
|
||||
// Import toast conditionally to prevent build errors
|
||||
let toast: any;
|
||||
try {
|
||||
// Try to import from the UI components
|
||||
toast = require("@/components/common/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;
|
||||
}
|
||||
|
||||
// Add cache invalidation and order refresh utilities
|
||||
interface CacheEntry {
|
||||
data: any;
|
||||
timestamp: number;
|
||||
ttl: number; // Time to live in milliseconds
|
||||
}
|
||||
|
||||
class ApiCache {
|
||||
private cache = new Map<string, CacheEntry>();
|
||||
private orderRefreshCallbacks = new Set<() => void>();
|
||||
|
||||
get(key: string): any | null {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) return null;
|
||||
|
||||
if (Date.now() - entry.timestamp > entry.ttl) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
set(key: string, data: any, ttl: number = 60000): void { // Default 1 minute TTL
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
ttl
|
||||
});
|
||||
}
|
||||
|
||||
invalidate(pattern?: string): void {
|
||||
if (pattern) {
|
||||
// Remove entries matching pattern
|
||||
for (const [key] of this.cache) {
|
||||
if (key.includes(pattern)) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Clear all cache
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Order-specific cache invalidation
|
||||
invalidateOrderData(orderId?: string): void {
|
||||
if (orderId) {
|
||||
this.invalidate(`orders/${orderId}`);
|
||||
this.invalidate(`orders?`); // Invalidate order lists
|
||||
} else {
|
||||
this.invalidate('orders');
|
||||
}
|
||||
|
||||
// Show a subtle notification when data is refreshed
|
||||
if (typeof window !== 'undefined') {
|
||||
console.log('🔄 Order data cache invalidated, refreshing components...');
|
||||
}
|
||||
|
||||
// Trigger order refresh callbacks
|
||||
this.orderRefreshCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
console.error('Error in order refresh callback:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Register callback for order data refresh
|
||||
onOrderRefresh(callback: () => void): () => void {
|
||||
this.orderRefreshCallbacks.add(callback);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
this.orderRefreshCallbacks.delete(callback);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Global cache instance
|
||||
const apiCache = new ApiCache();
|
||||
|
||||
// Export cache utilities
|
||||
export const cacheUtils = {
|
||||
invalidateOrderData: (orderId?: string) => apiCache.invalidateOrderData(orderId),
|
||||
onOrderRefresh: (callback: () => void) => apiCache.onOrderRefresh(callback),
|
||||
invalidateAll: () => apiCache.invalidate(),
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Note: HTTP-only cookies cannot be read by JavaScript, so we return null
|
||||
* and rely on the browser to automatically include them in requests
|
||||
*/
|
||||
export function getAuthToken(): string | null {
|
||||
if (typeof document === 'undefined') return null; // Guard for SSR
|
||||
|
||||
// Try localStorage first (for non-HTTP-only tokens)
|
||||
const localToken = localStorage.getItem('Authorization');
|
||||
if (localToken) {
|
||||
return localToken;
|
||||
}
|
||||
|
||||
// For HTTP-only cookies, we can't read them from JavaScript
|
||||
// The browser will automatically include them in requests
|
||||
// Check if the cookie exists (we can't read its value)
|
||||
const hasAuthCookie = document.cookie
|
||||
.split('; ')
|
||||
.some(row => row.startsWith('Authorization='));
|
||||
|
||||
if (hasAuthCookie) {
|
||||
// Return a special marker to indicate the cookie exists
|
||||
// The actual token will be sent automatically by the browser
|
||||
return 'HTTP_ONLY_COOKIE';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string, string> = {}): Headers {
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'accept': '*/*',
|
||||
...customHeaders
|
||||
});
|
||||
|
||||
const authToken = token || getAuthToken();
|
||||
if (authToken && authToken !== 'HTTP_ONLY_COOKIE') {
|
||||
// Only add Authorization header for non-HTTP-only tokens
|
||||
headers.set('authorization', `Bearer ${authToken}`);
|
||||
}
|
||||
// For HTTP_ONLY_COOKIE, the browser will automatically include the cookie
|
||||
|
||||
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<T = any>(url: string, options: RequestInit = {}): Promise<T> {
|
||||
try {
|
||||
// Create headers with authentication
|
||||
const headers = createApiHeaders(null, options.headers as Record<string, string>);
|
||||
|
||||
// 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',
|
||||
signal: AbortSignal.timeout(30000), // 30 second timeout
|
||||
});
|
||||
|
||||
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<T>(
|
||||
endpoint: string,
|
||||
options: FetchOptions = {}
|
||||
): Promise<T> {
|
||||
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}`;
|
||||
|
||||
// Handle API endpoint construction without exposing internal domains
|
||||
let url;
|
||||
// Check if this is a proxied API call (relative URLs starting with /api)
|
||||
if (apiUrl === '/api' || apiUrl.startsWith('/api')) {
|
||||
// For proxied requests, use relative URLs
|
||||
if (normalizedEndpoint.startsWith('/api/')) {
|
||||
url = normalizedEndpoint; // Use as-is for proxied requests
|
||||
} else {
|
||||
url = `/api${normalizedEndpoint}`;
|
||||
}
|
||||
} else {
|
||||
// For direct API calls, construct full URL
|
||||
// Ensure /api prefix is included if apiUrl doesn't already have it
|
||||
const baseUrl = apiUrl.endsWith('/api') ? apiUrl : `${apiUrl}/api`;
|
||||
url = `${baseUrl}${normalizedEndpoint}`;
|
||||
}
|
||||
|
||||
// Get auth token from cookies
|
||||
const authToken = getAuthToken();
|
||||
|
||||
// Prepare headers with authentication if token exists
|
||||
const requestHeaders: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(headers as Record<string, string>),
|
||||
};
|
||||
|
||||
if (authToken && authToken !== 'HTTP_ONLY_COOKIE') {
|
||||
// Backend expects "Bearer TOKEN" format
|
||||
requestHeaders['Authorization'] = `Bearer ${authToken}`;
|
||||
}
|
||||
// For HTTP_ONLY_COOKIE, the browser will automatically include the cookie
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method,
|
||||
credentials: 'include',
|
||||
headers: requestHeaders,
|
||||
signal: AbortSignal.timeout(30000), // 30 second timeout
|
||||
...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<CustomerResponse> => {
|
||||
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<CustomerStats> => {
|
||||
return clientFetch(`/customers/${userId}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Export orders by status to CSV
|
||||
* @param status Order status to filter by
|
||||
* @param storeId Optional store ID (uses authenticated user's store if not provided)
|
||||
* @returns Promise that resolves when download is triggered
|
||||
*/
|
||||
export const exportOrdersToCSV = async (status: string, storeId?: string): Promise<void> => {
|
||||
try {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||
const normalizedEndpoint = '/orders/export-csv';
|
||||
|
||||
// Handle API endpoint construction
|
||||
let url;
|
||||
if (apiUrl === '/api' || apiUrl.startsWith('/api')) {
|
||||
url = `/api${normalizedEndpoint}`;
|
||||
} else {
|
||||
url = `${apiUrl}${normalizedEndpoint}`;
|
||||
}
|
||||
|
||||
// Add query parameters
|
||||
const params = new URLSearchParams({ status });
|
||||
if (storeId) {
|
||||
params.append('storeId', storeId);
|
||||
}
|
||||
url = `${url}?${params.toString()}`;
|
||||
|
||||
// Get auth token
|
||||
const authToken = getAuthToken();
|
||||
|
||||
// Prepare headers
|
||||
const headers: Record<string, string> = {};
|
||||
if (authToken && authToken !== 'HTTP_ONLY_COOKIE') {
|
||||
headers['Authorization'] = `Bearer ${authToken}`;
|
||||
}
|
||||
|
||||
// Fetch the CSV file
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
const errorMessage = errorData.message || errorData.error || 'Failed to export orders';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// Get the CSV content
|
||||
const blob = await response.blob();
|
||||
|
||||
// Get filename from Content-Disposition header or generate one
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let filename = `orders_${status}_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/);
|
||||
if (filenameMatch) {
|
||||
filename = filenameMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Create download link and trigger download
|
||||
const downloadUrl = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = downloadUrl;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
} catch (error) {
|
||||
console.error('Error exporting orders to CSV:', error);
|
||||
const message = error instanceof Error ? error.message : 'Failed to export orders';
|
||||
|
||||
// Show error toast
|
||||
if (typeof window !== 'undefined') {
|
||||
if (toast?.title && toast?.description) {
|
||||
toast({
|
||||
title: 'Export Failed',
|
||||
description: message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} else if (typeof toast?.error === 'function') {
|
||||
toast.error(message);
|
||||
} else {
|
||||
console.error('CSV export error:', message);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Enhanced client-side fetch function with caching and automatic invalidation
|
||||
*/
|
||||
export async function clientFetchWithCache<T = any>(
|
||||
url: string,
|
||||
options: RequestInit = {},
|
||||
cacheKey?: string,
|
||||
ttl?: number
|
||||
): Promise<T> {
|
||||
// Check cache first for GET requests
|
||||
if (options.method === 'GET' || !options.method) {
|
||||
const cached = apiCache.get(cacheKey || url);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
// Make the request
|
||||
const result = await clientFetch<T>(url, options);
|
||||
|
||||
// Cache GET requests
|
||||
if ((options.method === 'GET' || !options.method) && cacheKey) {
|
||||
apiCache.set(cacheKey, result, ttl);
|
||||
}
|
||||
|
||||
// Invalidate cache for mutations that affect orders
|
||||
if (options.method && ['PUT', 'POST', 'DELETE', 'PATCH'].includes(options.method)) {
|
||||
if (url.includes('/orders/') && url.includes('/status')) {
|
||||
// Order status update - invalidate order cache
|
||||
const orderIdMatch = url.match(/\/orders\/([^\/]+)\/status/);
|
||||
if (orderIdMatch) {
|
||||
apiCache.invalidateOrderData(orderIdMatch[1]);
|
||||
}
|
||||
} else if (url.includes('/orders')) {
|
||||
// General order mutation
|
||||
apiCache.invalidateOrderData();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
154
lib/api/index.ts
Normal file
154
lib/api/index.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
// Re-export client API functions
|
||||
export {
|
||||
// Core client API functions
|
||||
clientFetch,
|
||||
fetchClient,
|
||||
getAuthToken,
|
||||
getCookie,
|
||||
|
||||
// Customer API
|
||||
getCustomers,
|
||||
getCustomerDetails,
|
||||
|
||||
// Orders API
|
||||
exportOrdersToCSV,
|
||||
|
||||
// Types
|
||||
type CustomerStats,
|
||||
type CustomerResponse,
|
||||
} from './api-client';
|
||||
|
||||
// Re-export product services
|
||||
export {
|
||||
getProducts,
|
||||
getProductDetails,
|
||||
createProduct,
|
||||
updateProduct,
|
||||
deleteProduct,
|
||||
uploadProductImage,
|
||||
getProductStock,
|
||||
updateProductStock,
|
||||
|
||||
// Types
|
||||
type Product,
|
||||
type ProductsResponse,
|
||||
type StockData,
|
||||
} from '../services/product-service';
|
||||
|
||||
// Re-export shipping services
|
||||
export {
|
||||
getShippingOptions,
|
||||
getShippingOption,
|
||||
createShippingOption,
|
||||
updateShippingOption,
|
||||
deleteShippingOption,
|
||||
|
||||
// Types
|
||||
type ShippingOption,
|
||||
type ShippingOptionsResponse,
|
||||
} from '../services/shipping-service';
|
||||
|
||||
// Re-export analytics services
|
||||
export {
|
||||
getAnalyticsOverview,
|
||||
getRevenueTrends,
|
||||
getProductPerformance,
|
||||
getCustomerInsights,
|
||||
getOrderAnalytics,
|
||||
getAnalyticsOverviewWithStore,
|
||||
getRevenueTrendsWithStore,
|
||||
getProductPerformanceWithStore,
|
||||
getCustomerInsightsWithStore,
|
||||
getOrderAnalyticsWithStore,
|
||||
getStoreIdForUser,
|
||||
// Types
|
||||
type AnalyticsOverview,
|
||||
type RevenueData,
|
||||
type ProductPerformance,
|
||||
type CustomerInsights,
|
||||
type OrderAnalytics,
|
||||
} from '../services/analytics-service';
|
||||
|
||||
export { formatGBP, formatNumber } from '../utils/format';
|
||||
|
||||
// Define the PlatformStats interface to match the expected format
|
||||
export interface PlatformStats {
|
||||
orders: {
|
||||
completed: number;
|
||||
};
|
||||
vendors: {
|
||||
total: number;
|
||||
};
|
||||
transactions: {
|
||||
volume: number;
|
||||
averageOrderValue?: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Re-export stats services
|
||||
export {
|
||||
getPlatformStats,
|
||||
getVendorStats,
|
||||
} from '../services/stats-service';
|
||||
|
||||
// Re-export server API functions
|
||||
export {
|
||||
fetchServer,
|
||||
getCustomersServer,
|
||||
getCustomerDetailsServer,
|
||||
getPlatformStatsServer,
|
||||
getAnalyticsOverviewServer,
|
||||
getRevenueTrendsServer,
|
||||
getProductPerformanceServer,
|
||||
getCustomerInsightsServer,
|
||||
getOrderAnalyticsServer,
|
||||
type AnalyticsOverview as ServerAnalyticsOverview,
|
||||
type RevenueData as ServerRevenueData,
|
||||
type ProductPerformance as ServerProductPerformance,
|
||||
type CustomerInsights as ServerCustomerInsights,
|
||||
type OrderAnalytics as ServerOrderAnalytics,
|
||||
} from './server-api';
|
||||
|
||||
// Get clientFetch first so we can use it in the compatibility functions
|
||||
import { clientFetch } from './api-client';
|
||||
import { getProductDetails, updateProduct } from '../services/product-service';
|
||||
import { getPlatformStats } from '../services/stats-service';
|
||||
|
||||
// Add missing functions for backward compatibility
|
||||
// These are functions from the old style that we need to maintain compatibility
|
||||
export const fetchData = async (endpoint: string, options: any = {}) => {
|
||||
console.warn('fetchData is deprecated, use clientFetch instead');
|
||||
return clientFetch(endpoint, options);
|
||||
};
|
||||
|
||||
export const apiRequest = async (endpoint: string, method = 'GET', data: any = null, token: string | null = null) => {
|
||||
console.warn('apiRequest is deprecated, use clientFetch instead');
|
||||
const options: RequestInit & { headers: Record<string, string> } = {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
};
|
||||
|
||||
if (token) {
|
||||
options.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return clientFetch(endpoint, options);
|
||||
};
|
||||
|
||||
// Product-specific compatibility functions
|
||||
export const fetchProductData = async (productId: string, token?: string) => {
|
||||
console.warn('fetchProductData is deprecated, use getProductDetails instead');
|
||||
return getProductDetails(productId);
|
||||
};
|
||||
|
||||
export const saveProductData = async (productData: any, productId: string, token?: string) => {
|
||||
console.warn('saveProductData is deprecated, use updateProduct instead');
|
||||
return updateProduct(productId, productData);
|
||||
};
|
||||
|
||||
// Stats compatibility function
|
||||
export const fetchPlatformStats = async () => {
|
||||
console.warn('fetchPlatformStats is deprecated, use getPlatformStats instead');
|
||||
return getPlatformStats();
|
||||
};
|
||||
320
lib/api/server-api.ts
Normal file
320
lib/api/server-api.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
// This module is only meant to be used in Server Components in the app/ directory
|
||||
// It cannot be imported in Client Components or pages/ directory
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
import { CustomerResponse, CustomerStats } from './api-client';
|
||||
|
||||
// Dynamically import cookies to prevent build errors
|
||||
let cookiesModule: any;
|
||||
try {
|
||||
// This will only work in server components
|
||||
cookiesModule = require('next/headers');
|
||||
} catch (error) {
|
||||
console.warn('Warning: next/headers only works in Server Components in the app/ directory');
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a server-side API URL for backend requests
|
||||
* Used in Server Components and API routes to directly access the backend API
|
||||
*
|
||||
* @param endpoint The API endpoint path
|
||||
* @returns A complete URL to the backend API
|
||||
*/
|
||||
function getServerApiUrl(endpoint: string): string {
|
||||
const apiUrl = process.env.SERVER_API_URL || 'http://localhost:3001/api';
|
||||
|
||||
// Validate API URL to prevent 500 errors
|
||||
if (!apiUrl || apiUrl === 'undefined' || apiUrl === 'null') {
|
||||
console.warn('SERVER_API_URL not properly set, using localhost fallback');
|
||||
const fallbackUrl = 'http://localhost:3001/api';
|
||||
const cleanEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
|
||||
return fallbackUrl.endsWith('/')
|
||||
? `${fallbackUrl}${cleanEndpoint}`
|
||||
: `${fallbackUrl}/${cleanEndpoint}`;
|
||||
}
|
||||
|
||||
const cleanEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
|
||||
|
||||
return apiUrl.endsWith('/')
|
||||
? `${apiUrl}${cleanEndpoint}`
|
||||
: `${apiUrl}/${cleanEndpoint}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side fetch wrapper with authentication.
|
||||
* Used in Server Components to make authenticated API requests to the backend.
|
||||
* This uses the SERVER_API_URL environment variable and is different from client-side fetching.
|
||||
*
|
||||
* @throws Error if used outside of a Server Component in the app/ directory
|
||||
*/
|
||||
export async function fetchServer<T = unknown>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
// Check if we're in a server component context
|
||||
if (!cookiesModule?.cookies) {
|
||||
throw new Error(
|
||||
"fetchServer can only be used in Server Components in the app/ directory. " +
|
||||
"For client components, use clientFetch or fetchClient instead."
|
||||
);
|
||||
}
|
||||
|
||||
// Get auth token from cookies
|
||||
const cookieStore = await cookiesModule.cookies();
|
||||
const authToken = cookieStore.get('Authorization')?.value;
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
if (!authToken) redirect('/auth/login');
|
||||
|
||||
try {
|
||||
// Get the complete backend URL using the utility function
|
||||
const url = getServerApiUrl(endpoint);
|
||||
|
||||
// Make the request with proper auth headers
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
...options.headers,
|
||||
},
|
||||
cache: 'no-store', // Always fetch fresh data
|
||||
});
|
||||
|
||||
// Handle auth failures
|
||||
if (res.status === 401) redirect('/auth/login');
|
||||
|
||||
// Handle other errors
|
||||
if (!res.ok) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await res.json();
|
||||
} catch {
|
||||
errorData = {};
|
||||
}
|
||||
|
||||
// Handle new error format: { success: false, error: { message: "...", code: "..." } }
|
||||
// or old format: { error: "...", message: "..." }
|
||||
let errorMessage: string;
|
||||
if (errorData.error?.message) {
|
||||
errorMessage = errorData.error.message;
|
||||
} else if (typeof errorData.error === 'string') {
|
||||
errorMessage = errorData.error;
|
||||
} else if (errorData.message) {
|
||||
errorMessage = errorData.message;
|
||||
} else {
|
||||
errorMessage = `Request failed: ${res.status} ${res.statusText}`;
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// Handle 204 No Content responses
|
||||
if (res.status === 204) {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await res.json();
|
||||
return data;
|
||||
} catch (parseError) {
|
||||
// If JSON parsing fails, throw a proper error
|
||||
throw new Error(`Failed to parse response from ${endpoint}: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Server request to ${endpoint} failed:`, error);
|
||||
// Ensure we always throw an Error instance, not an object
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
} else if (typeof error === 'string') {
|
||||
throw new Error(error);
|
||||
} else {
|
||||
const errorStr = error && typeof error === 'object' ? JSON.stringify(error) : String(error);
|
||||
throw new Error(`Request failed: ${errorStr}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a paginated list of customers (server-side)
|
||||
* @param page Page number (starting from 1)
|
||||
* @param limit Number of items per page
|
||||
* @returns Promise with customers data and total count
|
||||
*/
|
||||
export const getCustomersServer = async (page: number = 1, limit: number = 25): Promise<CustomerResponse> => {
|
||||
return fetchServer(`/customers?page=${page}&limit=${limit}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get detailed stats for a specific customer (server-side)
|
||||
* @param userId The customer's user ID
|
||||
* @returns Promise with detailed customer stats
|
||||
*/
|
||||
export const getCustomerDetailsServer = async (userId: string): Promise<CustomerStats> => {
|
||||
return fetchServer(`/customers/${userId}`);
|
||||
};
|
||||
|
||||
// Server-side platform stats function
|
||||
export async function getPlatformStatsServer() {
|
||||
try {
|
||||
const url = getServerApiUrl('/stats/platform');
|
||||
|
||||
// Make direct request without auth
|
||||
const res = await fetch(url, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// If we have real data, use it
|
||||
if (data && Object.keys(data).length > 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
console.info('Using sample stats data for demo');
|
||||
return {
|
||||
orders: {
|
||||
completed: 1289
|
||||
},
|
||||
vendors: {
|
||||
total: 15
|
||||
},
|
||||
transactions: {
|
||||
volume: 38450,
|
||||
averageOrderValue: 29.83
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching platform stats (server):', error);
|
||||
// Return default stats to prevent UI breakage
|
||||
return {
|
||||
orders: {
|
||||
completed: 0
|
||||
},
|
||||
vendors: {
|
||||
total: 0
|
||||
},
|
||||
transactions: {
|
||||
volume: 0,
|
||||
averageOrderValue: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Analytics Types for server-side
|
||||
export interface AnalyticsOverview {
|
||||
orders: {
|
||||
total: number;
|
||||
completed: number;
|
||||
pending: number;
|
||||
completionRate: string;
|
||||
};
|
||||
revenue: {
|
||||
total: number;
|
||||
monthly: number;
|
||||
weekly: number;
|
||||
averageOrderValue: number;
|
||||
};
|
||||
products: {
|
||||
total: number;
|
||||
};
|
||||
customers: {
|
||||
unique: number;
|
||||
};
|
||||
userType?: 'vendor' | 'staff';
|
||||
}
|
||||
|
||||
export interface RevenueData {
|
||||
_id: {
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
};
|
||||
revenue: number;
|
||||
orders: number;
|
||||
}
|
||||
|
||||
export interface ProductPerformance {
|
||||
productId: string;
|
||||
name: string;
|
||||
image: string;
|
||||
unitType: string;
|
||||
currentStock: number;
|
||||
stockStatus: string;
|
||||
totalSold: number;
|
||||
totalRevenue: number;
|
||||
orderCount: number;
|
||||
averagePrice: number;
|
||||
}
|
||||
|
||||
export interface CustomerInsights {
|
||||
totalCustomers: number;
|
||||
segments: {
|
||||
new: number;
|
||||
returning: number;
|
||||
loyal: number;
|
||||
vip: number;
|
||||
};
|
||||
topCustomers: Array<{
|
||||
_id: string;
|
||||
orderCount: number;
|
||||
totalSpent: number;
|
||||
averageOrderValue: number;
|
||||
firstOrder: string;
|
||||
lastOrder: string;
|
||||
}>;
|
||||
averageOrdersPerCustomer: string;
|
||||
}
|
||||
|
||||
export interface OrderAnalytics {
|
||||
statusDistribution: Array<{
|
||||
_id: string;
|
||||
count: number;
|
||||
}>;
|
||||
dailyOrders: Array<{
|
||||
_id: {
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
};
|
||||
orders: number;
|
||||
revenue: number;
|
||||
}>;
|
||||
averageProcessingDays: number;
|
||||
}
|
||||
|
||||
// Server-side analytics functions
|
||||
export const getAnalyticsOverviewServer = async (storeId?: string): Promise<AnalyticsOverview> => {
|
||||
const url = storeId ? `/analytics/overview?storeId=${storeId}` : '/analytics/overview';
|
||||
return fetchServer<AnalyticsOverview>(url);
|
||||
};
|
||||
|
||||
export const getRevenueTrendsServer = async (period: string = '30', storeId?: string): Promise<RevenueData[]> => {
|
||||
const params = new URLSearchParams({ period });
|
||||
if (storeId) params.append('storeId', storeId);
|
||||
|
||||
const url = `/analytics/revenue-trends?${params.toString()}`;
|
||||
return fetchServer<RevenueData[]>(url);
|
||||
};
|
||||
|
||||
export const getProductPerformanceServer = async (storeId?: string): Promise<ProductPerformance[]> => {
|
||||
const url = storeId ? `/analytics/product-performance?storeId=${storeId}` : '/analytics/product-performance';
|
||||
return fetchServer<ProductPerformance[]>(url);
|
||||
};
|
||||
|
||||
export const getCustomerInsightsServer = async (storeId?: string): Promise<CustomerInsights> => {
|
||||
const url = storeId ? `/analytics/customer-insights?storeId=${storeId}` : '/analytics/customer-insights';
|
||||
return fetchServer<CustomerInsights>(url);
|
||||
};
|
||||
|
||||
export const getOrderAnalyticsServer = async (period: string = '30', storeId?: string): Promise<OrderAnalytics> => {
|
||||
const params = new URLSearchParams({ period });
|
||||
if (storeId) params.append('storeId', storeId);
|
||||
|
||||
const url = `/analytics/order-analytics?${params.toString()}`;
|
||||
return fetchServer<OrderAnalytics>(url);
|
||||
};
|
||||
Reference in New Issue
Block a user