first commit

This commit is contained in:
Urtzi Alfaro
2025-07-17 13:54:51 +02:00
parent 347ff51bd7
commit 5bb3e93da4
41 changed files with 10084 additions and 94 deletions

View File

@@ -0,0 +1,212 @@
// frontend/dashboard/src/api/base/apiClient.ts
/**
* Base API client with authentication and error handling
*/
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { ApiError, TokenResponse } from '../../types/api';
export interface ApiClientConfig {
baseURL?: string;
timeout?: number;
enableAuth?: boolean;
enableRetry?: boolean;
}
export class ApiClient {
private client: AxiosInstance;
private enableAuth: boolean;
private refreshPromise: Promise<string> | null = null;
constructor(config: ApiClientConfig = {}) {
const {
baseURL = process.env.REACT_APP_API_URL || 'http://localhost:8000',
timeout = 10000,
enableAuth = true,
enableRetry = true,
} = config;
this.enableAuth = enableAuth;
this.client = axios.create({
baseURL: `${baseURL}/api/v1`,
timeout,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors(enableRetry);
}
private setupInterceptors(enableRetry: boolean) {
// Request interceptor - add auth token
this.client.interceptors.request.use(
(config) => {
if (this.enableAuth) {
const token = this.getStoredToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor - handle auth errors and retries
this.client.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Handle 401 errors with token refresh
if (
error.response?.status === 401 &&
this.enableAuth &&
!originalRequest._retry
) {
originalRequest._retry = true;
try {
const newToken = await this.refreshToken();
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return this.client(originalRequest);
} catch (refreshError) {
this.handleAuthFailure();
return Promise.reject(refreshError);
}
}
// Handle other errors
return Promise.reject(this.formatError(error));
}
);
}
private async refreshToken(): Promise<string> {
// Prevent multiple simultaneous refresh requests
if (this.refreshPromise) {
return this.refreshPromise;
}
this.refreshPromise = this.performTokenRefresh();
try {
const token = await this.refreshPromise;
this.refreshPromise = null;
return token;
} catch (error) {
this.refreshPromise = null;
throw error;
}
}
private async performTokenRefresh(): Promise<string> {
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) {
throw new Error('No refresh token available');
}
try {
const response = await axios.post<TokenResponse>(
`${this.client.defaults.baseURL}/auth/refresh`,
{ refresh_token: refreshToken }
);
const { access_token, refresh_token: newRefreshToken } = response.data;
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', newRefreshToken);
return access_token;
} catch (error) {
throw new Error('Token refresh failed');
}
}
private getStoredToken(): string | null {
return localStorage.getItem('access_token');
}
private handleAuthFailure(): void {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user_profile');
// Redirect to login
window.location.href = '/login';
}
private formatError(error: any): ApiError {
if (error.response?.data) {
return {
detail: error.response.data.detail || 'An error occurred',
service: error.response.data.service,
error_code: error.response.data.error_code,
timestamp: new Date().toISOString(),
};
}
return {
detail: error.message || 'Network error occurred',
timestamp: new Date().toISOString(),
};
}
// HTTP methods
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.get<T>(url, config);
return response.data;
}
async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.post<T>(url, data, config);
return response.data;
}
async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.put<T>(url, data, config);
return response.data;
}
async patch<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.patch<T>(url, data, config);
return response.data;
}
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.delete<T>(url, config);
return response.data;
}
// File upload
async uploadFile<T>(url: string, file: File, onProgress?: (progress: number) => void): Promise<T> {
const formData = new FormData();
formData.append('file', file);
const response = await this.client.post<T>(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
if (onProgress && progressEvent.total) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
onProgress(progress);
}
},
});
return response.data;
}
// WebSocket connection helper
createWebSocket(path: string): WebSocket {
const wsUrl = this.client.defaults.baseURL?.replace('http', 'ws') + path;
return new WebSocket(wsUrl);
}
}
// Default client instance
export const apiClient = new ApiClient();

76
frontend/src/api/cache.ts Normal file
View File

@@ -0,0 +1,76 @@
// frontend/dashboard/src/api/cache.ts
/**
* Simple in-memory cache for API responses
*/
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
class ApiCache {
private cache = new Map<string, CacheEntry<any>>();
set<T>(key: string, data: T, ttl: number = 300000): void { // 5 minutes default
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl,
});
}
get<T>(key: string): T | 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;
}
delete(key: string): void {
this.cache.delete(key);
}
clear(): void {
this.cache.clear();
}
has(key: string): boolean {
const entry = this.cache.get(key);
if (!entry) return false;
if (Date.now() - entry.timestamp > entry.ttl) {
this.cache.delete(key);
return false;
}
return true;
}
}
export const apiCache = new ApiCache();
// Cache helper for API client
export function withCache<T>(
key: string,
fetcher: () => Promise<T>,
ttl?: number
): Promise<T> {
const cached = apiCache.get<T>(key);
if (cached) {
return Promise.resolve(cached);
}
return fetcher().then(data => {
apiCache.set(key, data, ttl);
return data;
});
}

View File

@@ -0,0 +1,70 @@
// frontend/dashboard/src/api/config.ts
/**
* API configuration and constants
*/
export const API_CONFIG = {
BASE_URL: process.env.REACT_APP_API_URL || 'http://localhost:8000',
TIMEOUT: 10000,
RETRY_ATTEMPTS: 3,
RETRY_DELAY: 1000,
} as const;
export const ENDPOINTS = {
AUTH: {
LOGIN: '/auth/login',
REGISTER: '/auth/register',
LOGOUT: '/auth/logout',
REFRESH: '/auth/refresh',
PROFILE: '/auth/me',
CHANGE_PASSWORD: '/auth/change-password',
PASSWORD_RESET: '/auth/password-reset',
},
TRAINING: {
TRAIN: '/training/train',
STATUS: '/training/status',
JOBS: '/training/jobs',
MODELS: '/training/models',
PROGRESS_WS: '/training/progress',
},
FORECASTING: {
FORECASTS: '/forecasting/forecasts',
GENERATE: '/forecasting/generate',
PERFORMANCE: '/forecasting/performance',
},
DATA: {
SALES: '/data/sales',
SALES_UPLOAD: '/data/sales/upload',
SALES_ANALYTICS: '/data/sales/analytics',
WEATHER: '/data/weather',
TRAFFIC: '/data/traffic',
SYNC: '/data/sync',
QUALITY: '/data/quality',
},
TENANTS: {
CURRENT: '/tenants/current',
NOTIFICATIONS: '/tenants/notifications',
STATS: '/tenants/stats',
},
} as const;
export const HTTP_STATUS = {
OK: 200,
CREATED: 201,
NO_CONTENT: 204,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
CONFLICT: 409,
INTERNAL_SERVER_ERROR: 500,
SERVICE_UNAVAILABLE: 503,
} as const;
export const STORAGE_KEYS = {
ACCESS_TOKEN: 'access_token',
REFRESH_TOKEN: 'refresh_token',
USER_PROFILE: 'user_profile',
THEME: 'theme',
LANGUAGE: 'language',
} as const;

View File

@@ -0,0 +1,92 @@
// frontend/dashboard/src/api/hooks/useApi.ts
/**
* React hooks for API state management
*/
import { useState, useEffect, useCallback } from 'react';
import { ApiError } from '../../types/api';
export interface ApiState<T> {
data: T | null;
loading: boolean;
error: ApiError | null;
}
export function useApi<T>(
apiCall: () => Promise<T>,
dependencies: any[] = []
): ApiState<T> & {
refetch: () => Promise<void>;
reset: () => void;
} {
const [state, setState] = useState<ApiState<T>>({
data: null,
loading: false,
error: null,
});
const execute = useCallback(async () => {
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const data = await apiCall();
setState({ data, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error as ApiError,
});
}
}, dependencies);
const reset = useCallback(() => {
setState({ data: null, loading: false, error: null });
}, []);
useEffect(() => {
execute();
}, [execute]);
return {
...state,
refetch: execute,
reset,
};
}
export function useAsyncAction<T, P extends any[] = []>(
action: (...params: P) => Promise<T>
): {
execute: (...params: P) => Promise<T>;
loading: boolean;
error: ApiError | null;
reset: () => void;
} {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ApiError | null>(null);
const execute = useCallback(async (...params: P) => {
setLoading(true);
setError(null);
try {
const result = await action(...params);
setLoading(false);
return result;
} catch (err) {
const apiError = err as ApiError;
setError(apiError);
setLoading(false);
throw apiError;
}
}, [action]);
const reset = useCallback(() => {
setLoading(false);
setError(null);
}, []);
return { execute, loading, error, reset };
}

View File

View File

@@ -0,0 +1,67 @@
// frontend/dashboard/src/api/hooks/useTraining.ts
/**
* Training-specific hooks
*/
import { useState, useEffect, useRef } from 'react';
import { TrainingJobStatus, TrainingRequest } from '../../types/api';
import { trainingApi } from '../index';
import { useApi, useAsyncAction } from './useApi';
export function useTraining() {
const { data: jobs, loading, error, refetch } = useApi(() => trainingApi.getTrainingJobs());
const { data: models, refetch: refetchModels } = useApi(() => trainingApi.getTrainedModels());
const { execute: startTraining, loading: startingTraining } = useAsyncAction(
trainingApi.startTraining.bind(trainingApi)
);
return {
jobs: jobs || [],
models: models || [],
loading,
error,
startingTraining,
startTraining: async (request: TrainingRequest) => {
const job = await startTraining(request);
await refetch();
return job;
},
refresh: refetch,
refreshModels: refetchModels,
};
}
export function useTrainingProgress(jobId: string | null) {
const [progress, setProgress] = useState<TrainingJobStatus | null>(null);
const [error, setError] = useState<string | null>(null);
const wsRef = useRef<WebSocket | null>(null);
useEffect(() => {
if (!jobId) return;
// Initial status fetch
trainingApi.getTrainingStatus(jobId).then(setProgress).catch(setError);
// Set up WebSocket for real-time updates
wsRef.current = trainingApi.subscribeToTrainingProgress(
jobId,
(updatedProgress) => {
setProgress(updatedProgress);
setError(null);
},
(wsError) => {
setError(wsError.message);
}
);
return () => {
if (wsRef.current) {
wsRef.current.close();
}
};
}, [jobId]);
return { progress, error };
}

View File

@@ -0,0 +1,109 @@
// frontend/dashboard/src/api/hooks/useWebSocket.ts
/**
* Generic WebSocket hook for real-time updates
*/
import { useState, useEffect, useRef, useCallback } from 'react';
export interface WebSocketOptions {
reconnectAttempts?: number;
reconnectInterval?: number;
onOpen?: () => void;
onClose?: () => void;
onError?: (error: Event) => void;
}
export function useWebSocket<T>(
url: string | null,
options: WebSocketOptions = {}
): {
data: T | null;
connectionState: 'connecting' | 'open' | 'closed' | 'error';
send: (data: any) => void;
close: () => void;
} {
const [data, setData] = useState<T | null>(null);
const [connectionState, setConnectionState] = useState<'connecting' | 'open' | 'closed' | 'error'>('closed');
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const reconnectAttemptsRef = useRef(0);
const {
reconnectAttempts = 3,
reconnectInterval = 3000,
onOpen,
onClose,
onError,
} = options;
const connect = useCallback(() => {
if (!url || wsRef.current?.readyState === WebSocket.OPEN) return;
try {
setConnectionState('connecting');
wsRef.current = new WebSocket(url);
wsRef.current.onopen = () => {
setConnectionState('open');
reconnectAttemptsRef.current = 0;
onOpen?.();
};
wsRef.current.onmessage = (event) => {
try {
const parsedData = JSON.parse(event.data);
setData(parsedData);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
wsRef.current.onclose = () => {
setConnectionState('closed');
onClose?.();
// Attempt to reconnect
if (reconnectAttemptsRef.current < reconnectAttempts) {
reconnectAttemptsRef.current++;
reconnectTimeoutRef.current = setTimeout(connect, reconnectInterval);
}
};
wsRef.current.onerror = (error) => {
setConnectionState('error');
onError?.(error);
};
} catch (error) {
setConnectionState('error');
console.error('WebSocket connection failed:', error);
}
}, [url, reconnectAttempts, reconnectInterval, onOpen, onClose, onError]);
const send = useCallback((data: any) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify(data));
}
}, []);
const close = useCallback(() => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
if (wsRef.current) {
wsRef.current.close();
}
}, []);
useEffect(() => {
if (url) {
connect();
}
return () => {
close();
};
}, [url, connect, close]);
return { data, connectionState, send, close };
}

35
frontend/src/api/index.ts Normal file
View File

@@ -0,0 +1,35 @@
// frontend/dashboard/src/api/index.ts
/**
* Main API exports - centralized access to all services
*/
import { ApiClient, apiClient } from './base/apiClient';
import { AuthApi } from './services/authApi';
import { TrainingApi } from './services/trainingApi';
import { ForecastingApi } from './services/forecastingApi';
import { SalesApi } from './services/salesApi';
import { DataApi } from './services/dataApi';
import { TenantApi } from './services/tenantApi';
// Service instances using the default client
export const authApi = new AuthApi(apiClient);
export const trainingApi = new TrainingApi(apiClient);
export const forecastingApi = new ForecastingApi(apiClient);
export const salesApi = new SalesApi(apiClient);
export const dataApi = new DataApi(apiClient);
export const tenantApi = new TenantApi(apiClient);
// Export everything for flexibility
export * from './base/apiClient';
export * from './services/authApi';
export * from './services/trainingApi';
export * from './services/forecastingApi';
export * from './services/salesApi';
export * from './services/dataApi';
export * from './services/tenantApi';
export * from '../types/api';
// Convenience hooks for React
export { useApi } from './hooks/useApi';
export { useAuth } from './hooks/useAuth';
export { useTraining } from './hooks/useTraining';

View File

@@ -0,0 +1,96 @@
// frontend/dashboard/src/api/interceptors.ts
/**
* Request/Response interceptors for additional functionality
*/
import { AxiosRequestConfig, AxiosResponse } from 'axios';
import { apiCache } from './cache';
export function addLoggingInterceptor(client: any) {
// Request logging
client.interceptors.request.use(
(config: AxiosRequestConfig) => {
if (process.env.NODE_ENV === 'development') {
console.log(`🚀 API Request: ${config.method?.toUpperCase()} ${config.url}`, {
params: config.params,
data: config.data,
});
}
return config;
},
(error: any) => {
if (process.env.NODE_ENV === 'development') {
console.error('❌ API Request Error:', error);
}
return Promise.reject(error);
}
);
// Response logging
client.interceptors.response.use(
(response: AxiosResponse) => {
if (process.env.NODE_ENV === 'development') {
console.log(`✅ API Response: ${response.config.method?.toUpperCase()} ${response.config.url}`, {
status: response.status,
data: response.data,
});
}
return response;
},
(error: any) => {
if (process.env.NODE_ENV === 'development') {
console.error('❌ API Response Error:', {
url: error.config?.url,
status: error.response?.status,
data: error.response?.data,
});
}
return Promise.reject(error);
}
);
}
export function addCacheInterceptor(client: any) {
// Response caching for GET requests
client.interceptors.response.use(
(response: AxiosResponse) => {
const { method, url } = response.config;
if (method === 'get' && url) {
const cacheKey = `${method}:${url}`;
apiCache.set(cacheKey, response.data, 300000); // 5 minutes
}
return response;
},
(error: any) => Promise.reject(error)
);
}
export function addRetryInterceptor(client: any, maxRetries: number = 3) {
client.interceptors.response.use(
(response: AxiosResponse) => response,
async (error: any) => {
const { config } = error;
if (!config || config.__retryCount >= maxRetries) {
return Promise.reject(error);
}
config.__retryCount = config.__retryCount || 0;
config.__retryCount += 1;
// Only retry on network errors or 5xx errors
if (
!error.response ||
(error.response.status >= 500 && error.response.status < 600)
) {
const delay = Math.pow(2, config.__retryCount) * 1000; // Exponential backoff
await new Promise(resolve => setTimeout(resolve, delay));
return client(config);
}
return Promise.reject(error);
}
);
}

View File

@@ -0,0 +1,98 @@
// frontend/dashboard/src/api/services/authApi.ts
/**
* Authentication API service
*/
import { ApiClient } from '../base/apiClient';
import {
LoginRequest,
RegisterRequest,
TokenResponse,
UserProfile,
ApiResponse,
} from '../../types/api';
export class AuthApi {
constructor(private client: ApiClient) {}
async login(credentials: LoginRequest): Promise<TokenResponse> {
const response = await this.client.post<TokenResponse>('/auth/login', credentials);
// Store tokens
localStorage.setItem('access_token', response.access_token);
localStorage.setItem('refresh_token', response.refresh_token);
return response;
}
async register(userData: RegisterRequest): Promise<UserProfile> {
return this.client.post<UserProfile>('/auth/register', userData);
}
async logout(): Promise<void> {
try {
await this.client.post('/auth/logout');
} finally {
// Always clear local storage
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user_profile');
}
}
async refreshToken(): Promise<TokenResponse> {
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await this.client.post<TokenResponse>('/auth/refresh', {
refresh_token: refreshToken,
});
// Update stored tokens
localStorage.setItem('access_token', response.access_token);
localStorage.setItem('refresh_token', response.refresh_token);
return response;
}
async getCurrentUser(): Promise<UserProfile> {
const profile = await this.client.get<UserProfile>('/auth/me');
localStorage.setItem('user_profile', JSON.stringify(profile));
return profile;
}
async updateProfile(updates: Partial<UserProfile>): Promise<UserProfile> {
return this.client.patch<UserProfile>('/auth/profile', updates);
}
async changePassword(currentPassword: string, newPassword: string): Promise<void> {
return this.client.post('/auth/change-password', {
current_password: currentPassword,
new_password: newPassword,
});
}
async requestPasswordReset(email: string): Promise<void> {
return this.client.post('/auth/password-reset', { email });
}
async confirmPasswordReset(token: string, newPassword: string): Promise<void> {
return this.client.post('/auth/password-reset/confirm', {
token,
new_password: newPassword,
});
}
// Helper methods
isAuthenticated(): boolean {
return !!localStorage.getItem('access_token');
}
getStoredUser(): UserProfile | null {
const stored = localStorage.getItem('user_profile');
return stored ? JSON.parse(stored) : null;
}
}

View File

@@ -0,0 +1,53 @@
// frontend/dashboard/src/api/services/dataApi.ts
/**
* External data API service (weather, traffic, etc.)
*/
import { ApiClient } from '../base/apiClient';
import { WeatherData, TrafficData } from '../../types/api';
export class DataApi {
constructor(private client: ApiClient) {}
async getWeatherData(
startDate: string,
endDate: string
): Promise<WeatherData[]> {
return this.client.get<WeatherData[]>('/data/weather', {
params: { start_date: startDate, end_date: endDate },
});
}
async getTrafficData(
startDate: string,
endDate: string
): Promise<TrafficData[]> {
return this.client.get<TrafficData[]>('/data/traffic', {
params: { start_date: startDate, end_date: endDate },
});
}
async getCurrentWeather(): Promise<WeatherData> {
return this.client.get<WeatherData>('/data/weather/current');
}
async getWeatherForecast(days: number = 7): Promise<WeatherData[]> {
return this.client.get<WeatherData[]>('/data/weather/forecast', {
params: { days },
});
}
async syncExternalData(): Promise<{ message: string; synced_records: number }> {
return this.client.post('/data/sync');
}
async getDataQuality(): Promise<{
weather_coverage: number;
traffic_coverage: number;
last_sync: string;
issues: string[];
}> {
return this.client.get('/data/quality');
}
}

View File

@@ -0,0 +1,48 @@
// frontend/dashboard/src/api/services/forecastingApi.ts
/**
* Forecasting API service
*/
import { ApiClient } from '../base/apiClient';
import {
ForecastRecord,
ForecastRequest,
ApiResponse,
} from '../../types/api';
export class ForecastingApi {
constructor(private client: ApiClient) {}
async getForecasts(request: ForecastRequest = {}): Promise<ForecastRecord[]> {
return this.client.get<ForecastRecord[]>('/forecasting/forecasts', {
params: request,
});
}
async getForecastByProduct(
productName: string,
daysAhead: number = 7
): Promise<ForecastRecord[]> {
return this.client.get<ForecastRecord[]>(`/forecasting/forecasts/${productName}`, {
params: { days_ahead: daysAhead },
});
}
async generateForecast(request: ForecastRequest): Promise<ForecastRecord[]> {
return this.client.post<ForecastRecord[]>('/forecasting/generate', request);
}
async updateForecast(forecastId: string, adjustments: Partial<ForecastRecord>): Promise<ForecastRecord> {
return this.client.patch<ForecastRecord>(`/forecasting/forecasts/${forecastId}`, adjustments);
}
async deleteForecast(forecastId: string): Promise<void> {
return this.client.delete(`/forecasting/forecasts/${forecastId}`);
}
async getProductPerformance(productName: string, days: number = 30): Promise<any> {
return this.client.get(`/forecasting/performance/${productName}`, {
params: { days },
});
}
}

View File

@@ -0,0 +1,70 @@
// frontend/dashboard/src/api/services/salesApi.ts
/**
* Sales data API service
*/
import { ApiClient } from '../base/apiClient';
import {
SalesRecord,
CreateSalesRequest,
ApiResponse,
} from '../../types/api';
export interface SalesQuery {
start_date?: string;
end_date?: string;
product_name?: string;
limit?: number;
offset?: number;
}
export class SalesApi {
constructor(private client: ApiClient) {}
async getSales(query: SalesQuery = {}): Promise<SalesRecord[]> {
return this.client.get<SalesRecord[]>('/data/sales', {
params: query,
});
}
async createSalesRecord(salesData: CreateSalesRequest): Promise<SalesRecord> {
return this.client.post<SalesRecord>('/data/sales', salesData);
}
async updateSalesRecord(id: string, updates: Partial<CreateSalesRequest>): Promise<SalesRecord> {
return this.client.patch<SalesRecord>(`/data/sales/${id}`, updates);
}
async deleteSalesRecord(id: string): Promise<void> {
return this.client.delete(`/data/sales/${id}`);
}
async bulkCreateSales(salesData: CreateSalesRequest[]): Promise<SalesRecord[]> {
return this.client.post<SalesRecord[]>('/data/sales/bulk', salesData);
}
async uploadSalesFile(
file: File,
onProgress?: (progress: number) => void
): Promise<{ imported: number; errors: any[] }> {
return this.client.uploadFile('/data/sales/upload', file, onProgress);
}
async getSalesAnalytics(
startDate: string,
endDate: string
): Promise<{
totalRevenue: number;
totalQuantity: number;
topProducts: Array<{ product_name: string; quantity: number; revenue: number }>;
dailyTrends: Array<{ date: string; quantity: number; revenue: number }>;
}> {
return this.client.get('/data/sales/analytics', {
params: { start_date: startDate, end_date: endDate },
});
}
async getProductList(): Promise<string[]> {
return this.client.get<string[]>('/data/sales/products');
}
}

View File

@@ -0,0 +1,41 @@
// frontend/dashboard/src/api/services/tenantApi.ts
/**
* Tenant management API service
*/
import { ApiClient } from '../base/apiClient';
import { TenantInfo, NotificationSettings } from '../../types/api';
export class TenantApi {
constructor(private client: ApiClient) {}
async getCurrentTenant(): Promise<TenantInfo> {
return this.client.get<TenantInfo>('/tenants/current');
}
async updateTenant(updates: Partial<TenantInfo>): Promise<TenantInfo> {
return this.client.patch<TenantInfo>('/tenants/current', updates);
}
async getNotificationSettings(): Promise<NotificationSettings> {
return this.client.get<NotificationSettings>('/tenants/notifications');
}
async updateNotificationSettings(settings: Partial<NotificationSettings>): Promise<NotificationSettings> {
return this.client.patch<NotificationSettings>('/tenants/notifications', settings);
}
async testNotification(type: 'email' | 'whatsapp'): Promise<{ sent: boolean; message: string }> {
return this.client.post(`/tenants/notifications/test/${type}`);
}
async getTenantStats(): Promise<{
total_sales_records: number;
total_forecasts: number;
active_models: number;
last_training: string;
data_quality_score: number;
}> {
return this.client.get('/tenants/stats');
}
}

View File

@@ -0,0 +1,62 @@
// frontend/dashboard/src/api/services/trainingApi.ts
/**
* Training API service
*/
import { ApiClient } from '../base/apiClient';
import {
TrainingJobStatus,
TrainingRequest,
TrainedModel,
ApiResponse,
} from '../../types/api';
export class TrainingApi {
constructor(private client: ApiClient) {}
async startTraining(request: TrainingRequest = {}): Promise<TrainingJobStatus> {
return this.client.post<TrainingJobStatus>('/training/train', request);
}
async getTrainingStatus(jobId: string): Promise<TrainingJobStatus> {
return this.client.get<TrainingJobStatus>(`/training/status/${jobId}`);
}
async getTrainingJobs(limit: number = 10, offset: number = 0): Promise<TrainingJobStatus[]> {
return this.client.get<TrainingJobStatus[]>('/training/jobs', {
params: { limit, offset },
});
}
async getTrainedModels(): Promise<TrainedModel[]> {
return this.client.get<TrainedModel[]>('/training/models');
}
async cancelTraining(jobId: string): Promise<void> {
return this.client.delete(`/training/jobs/${jobId}`);
}
// WebSocket for real-time training progress
subscribeToTrainingProgress(
jobId: string,
onProgress: (progress: TrainingJobStatus) => void,
onError?: (error: Error) => void
): WebSocket {
const ws = this.client.createWebSocket(`/training/progress/${jobId}`);
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
onProgress(data);
} catch (error) {
onError?.(new Error('Failed to parse progress data'));
}
};
ws.onerror = (event) => {
onError?.(new Error('WebSocket connection error'));
};
return ws;
}
}