Replaces hardcoded production API URLs with localhost defaults for local development in both server and client code. Updates Dockerfile to require API URLs via deployment environment variables. Improves ChatTable to use a batch endpoint for chats and unread counts, with backward compatibility. Adds an env.example file to document required environment variables. Updates next.config.mjs to use environment variables for backend API rewrites and image domains.
278 lines
7.6 KiB
TypeScript
278 lines
7.6 KiB
TypeScript
// 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';
|
|
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('/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('/login');
|
|
|
|
// Handle other errors
|
|
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(`Server request to ${endpoint} failed:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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', // Always fetch fresh data
|
|
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;
|
|
}
|
|
|
|
// If API returned empty data, use sample 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);
|
|
};
|