Refactor UI imports and update component paths
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:
g
2026-01-13 05:02:13 +00:00
parent a6e6cd0757
commit fe01f31538
173 changed files with 1512 additions and 867 deletions

515
lib/api/api-client.ts Normal file
View 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
View 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
View 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);
};