Add new Frontend API folder

This commit is contained in:
Urtzi Alfaro
2025-08-03 17:48:34 +02:00
parent 935f45a283
commit 03e9dc6469
50 changed files with 4737 additions and 3321 deletions

View File

@@ -0,0 +1,107 @@
// frontend/src/api/services/auth.service.ts
/**
* Authentication Service
* Handles all authentication-related API calls
*/
import { apiClient } from '../client';
import { serviceEndpoints } from '../client/config';
import type {
LoginRequest,
LoginResponse,
RegisterRequest,
UserResponse,
PasswordResetRequest,
PasswordResetResponse,
PasswordResetConfirmRequest,
TokenVerification,
LogoutResponse,
} from '../types';
export class AuthService {
private baseEndpoint = serviceEndpoints.auth;
/**
* User Registration
*/
async register(data: RegisterRequest): Promise<{ user: UserResponse }> {
return apiClient.post(`${this.baseEndpoint}/register`, data);
}
/**
* User Login
*/
async login(credentials: LoginRequest): Promise<LoginResponse> {
return apiClient.post(`${this.baseEndpoint}/login`, credentials);
}
/**
* User Logout
*/
async logout(): Promise<LogoutResponse> {
return apiClient.post(`${this.baseEndpoint}/logout`);
}
/**
* Get Current User Profile
*/
async getCurrentUser(): Promise<UserResponse> {
return apiClient.get(`/users/me`);
}
/**
* Update User Profile
*/
async updateProfile(data: Partial<UserResponse>): Promise<UserResponse> {
return apiClient.put(`/users/me`, data);
}
/**
* Verify Token
*/
async verifyToken(token: string): Promise<TokenVerification> {
return apiClient.post(`${this.baseEndpoint}/verify-token`, { token });
}
/**
* Refresh Access Token
*/
async refreshToken(refreshToken: string): Promise<LoginResponse> {
return apiClient.post(`${this.baseEndpoint}/refresh`, {
refresh_token: refreshToken,
});
}
/**
* Request Password Reset
*/
async requestPasswordReset(data: PasswordResetRequest): Promise<PasswordResetResponse> {
return apiClient.post(`${this.baseEndpoint}/password-reset`, data);
}
/**
* Confirm Password Reset
*/
async confirmPasswordReset(data: PasswordResetConfirmRequest): Promise<{ message: string }> {
return apiClient.post(`${this.baseEndpoint}/password-reset/confirm`, data);
}
/**
* Change Password (for authenticated users)
*/
async changePassword(currentPassword: string, newPassword: string): Promise<{ message: string }> {
return apiClient.post(`/users/me/change-password`, {
current_password: currentPassword,
new_password: newPassword,
});
}
/**
* Delete User Account
*/
async deleteAccount(): Promise<{ message: string }> {
return apiClient.delete(`/users/me`);
}
}
export const authService = new AuthService();

View File

@@ -1,254 +0,0 @@
// src/api/services/AuthService.ts - UPDATED with missing methods
import { apiClient } from '../base/apiClient';
import { tokenManager } from '../auth/tokenManager';
import {
ApiResponse
} from '../types/api';
// Auth types
export interface LoginRequest {
email: string;
password: string;
}
export interface RegisterRequest {
email: string;
password: string;
full_name: string;
phone?: string;
language?: string;
}
export interface TokenResponse {
access_token: string;
refresh_token: string;
token_type: string;
expires_in: number;
}
export interface UserProfile {
id: string;
email: string;
full_name: string;
is_active: boolean;
is_verified: boolean;
tenant_id?: string;
role: string;
phone?: string;
language: string;
timezone: string;
created_at?: string;
last_login?: string;
}
export class AuthService {
/**
* Check if user is authenticated (has valid token)
* Note: This is a synchronous check using the tokenManager's isAuthenticated method
*/
isAuthenticated(): boolean {
try {
return tokenManager.isAuthenticated();
} catch (error) {
console.error('Error checking authentication status:', error);
return false;
}
}
/**
* Get current user profile
*/
async getCurrentUser(): Promise<UserProfile> {
const response = await apiClient.get<UserProfile>('/users/me');
return response;
}
async register(userData: RegisterRequest): Promise<TokenResponse> {
try {
// ✅ FIX: apiClient returns direct response, not wrapped in ApiResponse
const response = await apiClient.post<TokenResponse>(
'/api/v1/auth/register',
userData
);
// ✅ FIX: Check if response contains token data (direct response)
if (!response || !response.access_token) {
throw new Error('Registration successful but no tokens received');
}
// Store tokens after successful registration
await tokenManager.storeTokens(response);
return response;
} catch (error: any) {
// ✅ FIX: Better error handling for different scenarios
if (error.response) {
// Server responded with an error status
const status = error.response.status;
const data = error.response.data;
if (status === 409) {
throw new Error('User with this email already exists');
} else if (status === 400) {
const detail = data?.detail || 'Invalid registration data';
throw new Error(detail);
} else if (status >= 500) {
throw new Error('Server error during registration. Please try again.');
} else {
throw new Error(data?.detail || `Registration failed with status ${status}`);
}
} else if (error.request) {
// Request was made but no response received
throw new Error('Network error. Please check your connection.');
} else {
// Something else happened
throw new Error(error.message || 'Registration failed');
}
}
}
/**
* User login - Also improved error handling
*/
async login(credentials: LoginRequest): Promise<TokenResponse> {
try {
// ✅ FIX: apiClient returns direct response, not wrapped in ApiResponse
const response = await apiClient.post<TokenResponse>(
'/api/v1/auth/login',
credentials
);
// ✅ FIX: Check if response contains token data (direct response)
if (!response || !response.access_token) {
throw new Error('Login successful but no tokens received');
}
// Store tokens after successful login
await tokenManager.storeTokens(response);
return response;
} catch (error: any) {
// ✅ FIX: Better error handling
if (error.response) {
const status = error.response.status;
const data = error.response.data;
if (status === 401) {
throw new Error('Invalid email or password');
} else if (status === 429) {
throw new Error('Too many login attempts. Please try again later.');
} else if (status >= 500) {
throw new Error('Server error during login. Please try again.');
} else {
throw new Error(data?.detail || `Login failed with status ${status}`);
}
} else if (error.request) {
throw new Error('Network error. Please check your connection.');
} else {
throw new Error(error.message || 'Login failed');
}
}
}
/**
* Refresh access token
*/
async refreshToken(refreshToken: string): Promise<TokenResponse> {
const response = await apiClient.post<TokenResponse>(
'/api/v1/auth/refresh',
{ refresh_token: refreshToken }
);
return response;
}
/**
* Get current user profile (alias for getCurrentUser)
*/
async getProfile(): Promise<UserProfile> {
return this.getCurrentUser();
}
/**
* Update user profile
*/
async updateProfile(updates: Partial<UserProfile>): Promise<UserProfile> {
const response = await apiClient.put<UserProfile>(
'/api/v1/users/me',
updates
);
return response;
}
/**
* Change password
*/
async changePassword(
currentPassword: string,
newPassword: string
): Promise<void> {
await apiClient.post('/auth/change-password', {
current_password: currentPassword,
new_password: newPassword,
});
}
/**
* Request password reset
*/
async requestPasswordReset(email: string): Promise<void> {
await apiClient.post('/api/v1/auth/reset-password', { email });
}
/**
* Confirm password reset
*/
async confirmPasswordReset(
token: string,
newPassword: string
): Promise<void> {
await apiClient.post('/api/v1/auth/confirm-reset', {
token,
new_password: newPassword,
});
}
/**
* Verify email
*/
async verifyEmail(token: string): Promise<void> {
await apiClient.post('/api/v1/auth/verify-email', { token });
}
/**
* Resend verification email
*/
async resendVerification(): Promise<void> {
await apiClient.post('/api/v1/auth/resend-verification');
}
/**
* Logout (invalidate tokens)
*/
async logout(): Promise<void> {
try {
await apiClient.post('/api/v1/auth/logout');
} catch (error) {
console.error('Logout API call failed:', error);
} finally {
// Always clear tokens regardless of API call success
tokenManager.clearTokens();
}
}
/**
* Get user permissions
*/
async getPermissions(): Promise<string[]> {
const response = await apiClient.get<ApiResponse<string[]>>('/auth/permissions');
return response.data!;
}
}
export const authService = new AuthService();

View File

@@ -0,0 +1,182 @@
// frontend/src/api/services/data.service.ts
/**
* Data Management Service
* Handles sales data operations
*/
import { apiClient } from '../client';
import { RequestTimeouts } from '../client/config';
import type {
SalesData,
SalesDataQuery,
SalesDataImport,
SalesImportResult,
DashboardStats,
PaginatedResponse,
ActivityItem,
} from '../types';
export class DataService {
/**
* Upload Sales History File
*/
async uploadSalesHistory(
tenantId: string,
file: File,
additionalData?: Record<string, any>
): Promise<SalesImportResult> {
// Determine file format
const fileName = file.name.toLowerCase();
let fileFormat: string;
if (fileName.endsWith('.csv')) {
fileFormat = 'csv';
} else if (fileName.endsWith('.json')) {
fileFormat = 'json';
} else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
fileFormat = 'excel';
} else {
fileFormat = 'csv'; // Default fallback
}
const uploadData = {
file_format: fileFormat,
...additionalData,
};
return apiClient.upload(
`/tenants/${tenantId}/sales/import`,
file,
uploadData,
{
timeout: RequestTimeouts.LONG,
}
);
}
/**
* Validate Sales Data
*/
async validateSalesData(
tenantId: string,
file: File
): Promise<SalesImportResult> {
const fileName = file.name.toLowerCase();
let fileFormat: string;
if (fileName.endsWith('.csv')) {
fileFormat = 'csv';
} else if (fileName.endsWith('.json')) {
fileFormat = 'json';
} else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
fileFormat = 'excel';
} else {
fileFormat = 'csv';
}
return apiClient.upload(
`/tenants/${tenantId}/sales/validate`,
file,
{
file_format: fileFormat,
validate_only: true,
},
{
timeout: RequestTimeouts.MEDIUM,
}
);
}
/**
* Get Sales Data
*/
async getSalesData(
tenantId: string,
query?: SalesDataQuery
): Promise<PaginatedResponse<SalesData>> {
return apiClient.get(`/tenants/${tenantId}/sales`, { params: query });
}
/**
* Get Single Sales Record
*/
async getSalesRecord(tenantId: string, recordId: string): Promise<SalesData> {
return apiClient.get(`/tenants/${tenantId}/sales/${recordId}`);
}
/**
* Update Sales Record
*/
async updateSalesRecord(
tenantId: string,
recordId: string,
data: Partial<SalesData>
): Promise<SalesData> {
return apiClient.put(`/tenants/${tenantId}/sales/${recordId}`, data);
}
/**
* Delete Sales Record
*/
async deleteSalesRecord(tenantId: string, recordId: string): Promise<{ message: string }> {
return apiClient.delete(`/tenants/${tenantId}/sales/${recordId}`);
}
/**
* Get Dashboard Statistics
*/
async getDashboardStats(tenantId: string): Promise<DashboardStats> {
return apiClient.get(`/tenants/${tenantId}/sales/stats`);
}
/**
* Get Analytics Data
*/
async getAnalytics(
tenantId: string,
params?: {
start_date?: string;
end_date?: string;
product_names?: string[];
metrics?: string[];
}
): Promise<any> {
return apiClient.get(`/tenants/${tenantId}/analytics`, { params });
}
/**
* Export Sales Data
*/
async exportSalesData(
tenantId: string,
format: 'csv' | 'excel' | 'json',
query?: SalesDataQuery
): Promise<Blob> {
const response = await apiClient.request(`/tenants/${tenantId}/sales/export`, {
method: 'GET',
params: { ...query, format },
headers: {
'Accept': format === 'csv' ? 'text/csv' :
format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
'application/json',
},
});
return new Blob([response], {
type: format === 'csv' ? 'text/csv' :
format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
'application/json',
});
}
/**
* Get Recent Activity
*/
async getRecentActivity(tenantId: string, limit?: number): Promise<ActivityItem[]> {
return apiClient.get(`/tenants/${tenantId}/activity`, {
params: { limit },
});
}
}
export const dataService = new DataService();

View File

@@ -1,619 +0,0 @@
// frontend/src/api/services/dataService.ts - COMPLETE WORKING FIX
import { apiClient } from '../base/apiClient';
import { ApiResponse } from '../types/api';
export interface DashboardStats {
totalSales: number;
totalRevenue: number;
lastTrainingDate: string | null;
forecastAccuracy: number;
totalProducts: number;
activeTenants: number;
lastDataUpdate: string;
}
export interface UploadResponse {
message: string;
records_processed: number;
errors?: string[];
upload_id?: string;
}
export interface DataValidation {
// ✅ NEW: Backend SalesValidationResult schema fields
is_valid: boolean;
total_records: number;
valid_records: number;
invalid_records: number;
errors: Array<{
type: string;
message: string;
field?: string;
row?: number;
code?: string;
}>;
warnings: Array<{
type: string;
message: string;
field?: string;
row?: number;
code?: string;
}>;
summary: {
status: string;
file_format?: string;
file_size_bytes?: number;
file_size_mb?: number;
estimated_processing_time_seconds?: number;
validation_timestamp?: string;
suggestions: string[];
[key: string]: any;
};
}
// Data types
export interface WeatherData {
date: string;
temperature: number;
humidity: number;
precipitation: number;
wind_speed: number;
}
export interface TrafficData {
date: string;
traffic_volume: number;
pedestrian_count: number;
}
export interface SalesRecord {
id: string;
tenant_id: string;
product_name: string;
quantity_sold: number;
revenue: number;
date: string;
created_at: string;
}
export interface CreateSalesRequest {
product_name: string;
quantity_sold: number;
revenue: number;
date: string;
}
// ✅ FIXED: Interface for import data that matches backend SalesDataImport schema
export interface SalesDataImportRequest {
tenant_id: string;
data: string; // File content as string
data_format: 'csv' | 'json' | 'excel';
source?: string;
validate_only?: boolean;
}
export class DataService {
/**
* ✅ FIXED: Upload sales history file to the correct backend endpoint
* Backend expects: UploadFile + Form data at /api/v1/data/sales/import
*/
async uploadSalesHistory(
file: File,
tenantId: string, // Tenant ID is now a required path parameter
additionalData?: Record<string, any>
): Promise<UploadResponse> {
try {
console.log('Uploading sales file:', file.name);
// ✅ CRITICAL FIX: Use the correct endpoint that exists in backend
// Backend endpoint: @router.post("/import", response_model=SalesImportResult)
// Full path: /api/v1/tenants/{tenant_id}/sales/import
// Determine file format
const fileName = file.name.toLowerCase();
let fileFormat: string;
if (fileName.endsWith('.csv')) {
fileFormat = 'csv';
} else if (fileName.endsWith('.json')) {
fileFormat = 'json';
} else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
fileFormat = 'excel';
} else {
fileFormat = 'csv'; // Default fallback
}
// ✅ FIXED: Create FormData manually to match backend expectations
const formData = new FormData();
formData.append('file', file);
formData.append('file_format', fileFormat);
// tenantId is no longer appended to FormData as it's a path parameter
// if (tenantId) {
// formData.append('tenant_id', tenantId);
// }
// Add additional data if provided
if (additionalData) {
Object.entries(additionalData).forEach(([key, value]) => {
formData.append(key, String(value));
});
}
console.log('Uploading with file_format:', fileFormat);
// ✅ FIXED: Use the correct endpoint that exists in the backend
const response = await apiClient.request<ApiResponse<any>>(
`/api/v1/tenants/${tenantId}/sales/import`, // Correct endpoint path with tenant_id
{
method: 'POST',
body: formData,
// Don't set Content-Type header - let browser set it with boundary
headers: {} // Empty headers to avoid setting Content-Type manually
}
);
console.log('Upload response:', response);
// ✅ Handle the SalesImportResult response structure
if (response && typeof response === 'object') {
// Handle API errors
if ('detail' in response) {
throw new Error(typeof response.detail === 'string' ? response.detail : 'Upload failed');
}
// Extract data from response
let uploadResult: any;
if ('data' in response && response.data) {
uploadResult = response.data;
} else {
uploadResult = response;
}
// ✅ FIXED: Map backend SalesImportResult to frontend UploadResponse
return {
message: uploadResult.success
? `Successfully processed ${uploadResult.records_created || uploadResult.records_processed || 0} records`
: 'Upload completed with issues',
records_processed: uploadResult.records_created || uploadResult.records_processed || 0,
errors: uploadResult.errors ?
(Array.isArray(uploadResult.errors) ?
uploadResult.errors.map((err: any) =>
typeof err === 'string' ? err : (err.message || String(err))
) : [String(uploadResult.errors)]
) : [],
upload_id: uploadResult.id || undefined
};
}
throw new Error('Invalid response format from upload service');
} catch (error: any) {
console.error('Error uploading file:', error);
let errorMessage = 'Error al subir el archivo';
if (error.response?.status === 422) {
errorMessage = 'Formato de archivo inválido';
} else if (error.response?.status === 400) {
errorMessage = 'El archivo no se puede procesar';
} else if (error.response?.status === 500) {
errorMessage = 'Error del servidor. Inténtalo más tarde.';
} else if (error.message) {
errorMessage = error.message;
}
// Throw structured error that can be caught by the frontend
throw {
message: errorMessage,
status: error.response?.status || 0,
code: error.code,
details: error.response?.data || {}
};
}
}
// ✅ Alternative method: Upload using the import JSON endpoint instead of file upload
/**
* ✅ ALTERNATIVE: Upload sales data using the JSON import endpoint
* This uses the same endpoint as validation but with validate_only: false
*/
async uploadSalesDataAsJson(file: File, tenantId: string): Promise<UploadResponse> { // tenantId made required
try {
console.log('Uploading sales data as JSON:', file.name);
const fileContent = await this.readFileAsText(file);
if (!fileContent) {
throw new Error('Failed to read file content');
}
// Determine file format
const fileName = file.name.toLowerCase();
let dataFormat: 'csv' | 'json' | 'excel';
if (fileName.endsWith('.csv')) {
dataFormat = 'csv';
} else if (fileName.endsWith('.json')) {
dataFormat = 'json';
} else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
dataFormat = 'excel';
} else {
dataFormat = 'csv';
}
// ✅ Use the same structure as validation but with validate_only: false
const importData: SalesDataImportRequest = {
tenant_id: tenantId, // Use the provided tenantId
data: fileContent,
data_format: dataFormat,
validate_only: false, // This makes it actually import the data
source: 'onboarding_upload'
};
console.log('Uploading data with validate_only: false');
// ✅ OPTION: Add a new JSON import endpoint to the backend
// Current backend sales.py does not have a /import/json endpoint,
// it only has a file upload endpoint.
// If a JSON import endpoint is desired, it needs to be added to sales.py
// For now, this method will target the existing /import endpoint with a JSON body
// This will require the backend to support JSON body for /import, which it currently
// does not for the direct file upload endpoint.
// THIS ALTERNATIVE METHOD IS LEFT AS-IS, ASSUMING A FUTURE BACKEND ENDPOINT
// OR A MODIFICATION TO THE EXISTING /import ENDPOINT TO ACCEPT JSON BODY.
const response = await apiClient.post<ApiResponse<any>>(
`/api/v1/tenants/${tenantId}/sales/import/json`, // This endpoint does not exist in sales.py
importData
);
console.log('JSON upload response:', response);
// Handle response similar to file upload
if (response && typeof response === 'object') {
if ('detail' in response) {
throw new Error(typeof response.detail === 'string' ? response.detail : 'Upload failed');
}
let uploadResult: any;
if ('data' in response && response.data) {
uploadResult = response.data;
} else {
uploadResult = response;
}
return {
message: uploadResult.success
? `Successfully processed ${uploadResult.records_created || uploadResult.records_processed || 0} records`
: 'Upload completed with issues',
records_processed: uploadResult.records_created || uploadResult.records_processed || 0,
errors: uploadResult.errors ?
(Array.isArray(uploadResult.errors) ?
uploadResult.errors.map((err: any) =>
typeof err === 'string' ? err : (err.message || String(err))
) : [String(uploadResult.errors)]
) : [],
upload_id: uploadResult.id || undefined
};
}
throw new Error('Invalid response format from upload service');
} catch (error: any) {
console.error('Error uploading JSON data:', error);
let errorMessage = 'Error al subir los datos';
if (error.response?.status === 422) {
errorMessage = 'Formato de datos inválido';
} else if (error.response?.status === 400) {
errorMessage = 'Los datos no se pueden procesar';
} else if (error.response?.status === 500) {
errorMessage = 'Error del servidor. Inténtalo más tarde.';
} else if (error.message) {
errorMessage = error.message;
}
throw {
message: errorMessage,
status: error.response?.status || 0,
code: error.code,
details: error.response?.data || {}
};
}
}
async validateSalesData(file: File, tenantId: string): Promise<DataValidation> { // tenantId made required
try {
console.log('Reading file content...', file.name);
const fileContent = await this.readFileAsText(file);
if (!fileContent) {
throw new Error('Failed to read file content');
}
console.log('File content read successfully, length:', fileContent.length);
// Determine file format from extension
const fileName = file.name.toLowerCase();
let dataFormat: 'csv' | 'json' | 'excel';
if (fileName.endsWith('.csv')) {
dataFormat = 'csv';
} else if (fileName.endsWith('.json')) {
dataFormat = 'json';
} else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
dataFormat = 'excel';
} else {
dataFormat = 'csv'; // Default fallback
}
console.log('Detected file format:', dataFormat);
// ✅ FIXED: Use proper tenant ID when available
const importData: SalesDataImportRequest = {
tenant_id: tenantId, // Use the provided tenantId
data: fileContent,
data_format: dataFormat,
validate_only: true
};
console.log('Sending validation request with tenant_id:', importData.tenant_id);
const response = await apiClient.post<ApiResponse<DataValidation>>(
`/api/v1/tenants/${tenantId}/sales/import/validate`, // Correct endpoint with tenant_id
importData
);
console.log('Raw response from API:', response);
// ✅ ENHANCED: Handle the new backend response structure
if (response && typeof response === 'object') {
// Handle API errors
if ('detail' in response) {
console.error('API returned error:', response.detail);
if (Array.isArray(response.detail)) {
// Handle Pydantic validation errors
const errorMessages = response.detail.map(err => ({
type: 'pydantic_error',
message: `${err.loc ? err.loc.join('.') + ': ' : ''}${err.msg}`,
field: err.loc ? err.loc[err.loc.length - 1] : null,
code: err.type
}));
return {
is_valid: false,
total_records: 0,
valid_records: 0,
invalid_records: 0,
errors: errorMessages,
warnings: [],
summary: {
status: 'error',
suggestions: ['Revisa el formato de los datos enviados']
}
};
}
// Handle simple error messages
return {
is_valid: false,
total_records: 0,
valid_records: 0,
invalid_records: 0,
errors: [{
type: 'api_error',
message: typeof response.detail === 'string' ? response.detail : 'Error de validación',
code: 'API_ERROR'
}],
warnings: [],
summary: {
status: 'error',
suggestions: ['Verifica el archivo y vuelve a intentar']
}
};
}
// ✅ SUCCESS: Handle successful validation response
let validationResult: DataValidation;
// Check if response has nested data
if ('data' in response && response.data) {
validationResult = response.data;
} else if ('is_valid' in response) {
// Direct response
validationResult = response as DataValidation;
} else {
throw new Error('Invalid response format from validation service');
}
// ✅ ENHANCED: Normalize the response to ensure all required fields exist
return {
is_valid: validationResult.is_valid,
total_records: validationResult.total_records || 0,
valid_records: validationResult.valid_records || 0,
invalid_records: validationResult.invalid_records || 0,
errors: validationResult.errors || [],
warnings: validationResult.warnings || [],
summary: validationResult.summary || { status: 'unknown', suggestions: [] },
// Backward compatibility fields
valid: validationResult.is_valid, // Map for legacy code
recordCount: validationResult.total_records,
suggestions: validationResult.summary?.suggestions || []
};
}
throw new Error('Invalid response format from validation service');
} catch (error: any) {
console.error('Error validating file:', error);
let errorMessage = 'Error al validar el archivo';
let errorCode = 'UNKNOWN_ERROR';
if (error.response?.status === 422) {
errorMessage = 'Formato de archivo inválido';
errorCode = 'INVALID_FORMAT';
} else if (error.response?.status === 400) {
errorMessage = 'El archivo no se puede procesar';
errorCode = 'PROCESSING_ERROR';
} else if (error.response?.status === 500) {
errorMessage = 'Error del servidor. Inténtalo más tarde.';
errorCode = 'SERVER_ERROR';
} else if (error.message) {
errorMessage = error.message;
errorCode = 'CLIENT_ERROR';
}
// Return properly structured error response matching new schema
return {
is_valid: false,
total_records: 0,
valid_records: 0,
invalid_records: 0,
errors: [{
type: 'client_error',
message: errorMessage,
code: errorCode
}],
warnings: [],
summary: {
status: 'error',
suggestions: ['Intenta con un archivo diferente o contacta soporte']
},
// Backward compatibility
valid: false,
recordCount: 0,
suggestions: ['Intenta con un archivo diferente o contacta soporte']
};
}
}
/**
* ✅ FIXED: Proper helper method to read file as text with error handling
*/
private readFileAsText(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target?.result;
if (typeof result === 'string') {
resolve(result);
} else {
reject(new Error('Failed to read file as text'));
}
};
reader.onerror = () => {
reject(new Error('Failed to read file'));
};
reader.onabort = () => {
reject(new Error('File reading was aborted'));
};
// Read the file as text
reader.readAsText(file);
});
}
/**
* Get dashboard statistics
*/
async getDashboardStats(): Promise<DashboardStats> {
const response = await apiClient.get<ApiResponse<DashboardStats>>(
'/api/v1/data/dashboard/stats'
);
return response.data!;
}
/**
* Get sales records
*/
async getSalesRecords(tenantId: string, params?: { // Add tenantId
startDate?: string;
endDate?: string;
productName?: string;
page?: number;
limit?: number;
}): Promise<{ records: SalesRecord[]; total: number; page: number; pages: number }> {
const response = await apiClient.get<ApiResponse<{
records: SalesRecord[];
total: number;
page: number;
pages: number;
}>>(`/api/v1/tenants/${tenantId}/sales`, { params }); // Use tenantId in path
return response.data!;
}
/**
* Create single sales record
*/
async createSalesRecord(tenantId: string, record: CreateSalesRequest): Promise<SalesRecord> { // Add tenantId
const response = await apiClient.post<ApiResponse<SalesRecord>>(
`/api/v1/tenants/${tenantId}/sales`, // Use tenantId in path
record
);
return response.data!;
}
/**
* Update sales record
*/
async updateSalesRecord(tenantId: string, id: string, record: Partial<CreateSalesRequest>): Promise<SalesRecord> { // Add tenantId
const response = await apiClient.put<ApiResponse<SalesRecord>>(
`/api/v1/tenants/${tenantId}/sales/${id}`, // Use tenantId in path
record
);
return response.data!;
}
/**
* Delete sales record
*/
async deleteSalesRecord(tenantId: string, id: string): Promise<void> { // Add tenantId
await apiClient.delete(`/api/v1/tenants/${tenantId}/sales/${id}`); // Use tenantId in path
}
/**
* Get weather data
*/
async getWeatherData(params?: {
startDate?: string;
endDate?: string;
page?: number;
limit?: number;
}): Promise<{ data: WeatherData[]; total: number; page: number; pages: number }> {
const response = await apiClient.get<ApiResponse<{
data: WeatherData[];
total: number;
page: number;
pages: number;
}>>('/api/v1/data/weather', { params });
return response.data!;
}
/**
* Get traffic data
*/
async getTrafficData(params?: {
startDate?: string;
endDate?: string;
page?: number;
limit?: number;
}): Promise<{ data: TrafficData[]; total: number; page: number; pages: number }> {
const response = await apiClient.get<ApiResponse<{
data: TrafficData[];
total: number;
page: number;
pages: number;
}>>('/api/v1/data/traffic', { params });
return response.data!;
}
}
// ✅ CRITICAL FIX: Export the instance that index.ts expects
export const dataService = new DataService();

View File

@@ -0,0 +1,198 @@
// frontend/src/api/services/forecasting.service.ts
/**
* Forecasting Service
* Handles forecast operations and predictions
*/
import { apiClient } from '../client';
import { RequestTimeouts } from '../client/config';
import type {
SingleForecastRequest,
BatchForecastRequest,
ForecastResponse,
BatchForecastResponse,
ForecastAlert,
QuickForecast,
PaginatedResponse,
BaseQueryParams,
} from '../types';
export class ForecastingService {
/**
* Create Single Product Forecast
*/
async createSingleForecast(
tenantId: string,
request: SingleForecastRequest
): Promise<ForecastResponse[]> {
return apiClient.post(
`/tenants/${tenantId}/forecasts/single`,
request,
{
timeout: RequestTimeouts.MEDIUM,
}
);
}
/**
* Create Batch Forecast
*/
async createBatchForecast(
tenantId: string,
request: BatchForecastRequest
): Promise<BatchForecastResponse> {
return apiClient.post(
`/tenants/${tenantId}/forecasts/batch`,
request,
{
timeout: RequestTimeouts.LONG,
}
);
}
/**
* Get Forecast by ID
*/
async getForecast(tenantId: string, forecastId: string): Promise<ForecastResponse> {
return apiClient.get(`/tenants/${tenantId}/forecasts/${forecastId}`);
}
/**
* Get Forecasts
*/
async getForecasts(
tenantId: string,
params?: BaseQueryParams & {
product_name?: string;
start_date?: string;
end_date?: string;
model_id?: string;
}
): Promise<PaginatedResponse<ForecastResponse>> {
return apiClient.get(`/tenants/${tenantId}/forecasts`, { params });
}
/**
* Get Batch Forecast Status
*/
async getBatchForecastStatus(
tenantId: string,
batchId: string
): Promise<BatchForecastResponse> {
return apiClient.get(`/tenants/${tenantId}/forecasts/batch/${batchId}/status`);
}
/**
* Get Batch Forecasts
*/
async getBatchForecasts(
tenantId: string,
params?: BaseQueryParams & {
status?: string;
start_date?: string;
end_date?: string;
}
): Promise<PaginatedResponse<BatchForecastResponse>> {
return apiClient.get(`/tenants/${tenantId}/forecasts/batch`, { params });
}
/**
* Cancel Batch Forecast
*/
async cancelBatchForecast(tenantId: string, batchId: string): Promise<{ message: string }> {
return apiClient.post(`/tenants/${tenantId}/forecasts/batch/${batchId}/cancel`);
}
/**
* Get Quick Forecasts for Dashboard
*/
async getQuickForecasts(tenantId: string, limit?: number): Promise<QuickForecast[]> {
return apiClient.get(`/tenants/${tenantId}/forecasts/quick`, {
params: { limit },
});
}
/**
* Get Forecast Alerts
*/
async getForecastAlerts(
tenantId: string,
params?: BaseQueryParams & {
is_active?: boolean;
severity?: string;
alert_type?: string;
}
): Promise<PaginatedResponse<ForecastAlert>> {
return apiClient.get(`/tenants/${tenantId}/forecasts/alerts`, { params });
}
/**
* Acknowledge Forecast Alert
*/
async acknowledgeForecastAlert(
tenantId: string,
alertId: string
): Promise<ForecastAlert> {
return apiClient.post(`/tenants/${tenantId}/forecasts/alerts/${alertId}/acknowledge`);
}
/**
* Delete Forecast
*/
async deleteForecast(tenantId: string, forecastId: string): Promise<{ message: string }> {
return apiClient.delete(`/tenants/${tenantId}/forecasts/${forecastId}`);
}
/**
* Export Forecasts
*/
async exportForecasts(
tenantId: string,
format: 'csv' | 'excel' | 'json',
params?: {
product_name?: string;
start_date?: string;
end_date?: string;
}
): Promise<Blob> {
const response = await apiClient.request(`/tenants/${tenantId}/forecasts/export`, {
method: 'GET',
params: { ...params, format },
headers: {
'Accept': format === 'csv' ? 'text/csv' :
format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
'application/json',
},
});
return new Blob([response], {
type: format === 'csv' ? 'text/csv' :
format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
'application/json',
});
}
/**
* Get Forecast Accuracy Metrics
*/
async getForecastAccuracy(
tenantId: string,
params?: {
product_name?: string;
model_id?: string;
start_date?: string;
end_date?: string;
}
): Promise<{
overall_accuracy: number;
product_accuracy: Array<{
product_name: string;
accuracy: number;
sample_size: number;
}>;
}> {
return apiClient.get(`/tenants/${tenantId}/forecasts/accuracy`, { params });
}
}
export const forecastingService = new ForecastingService();

View File

@@ -1,315 +0,0 @@
// src/api/services/ForecastingService.ts
import { apiClient } from '../base/apiClient';
import {
ApiResponse
} from '../types/api';
// Forecast types
export interface ForecastRecord {
id: string;
tenant_id: string;
product_name: string;
forecast_date: string;
predicted_quantity: number;
confidence_lower: number;
confidence_upper: number;
model_version: string;
created_at: string;
}
export interface ForecastRequest {
product_name?: string;
forecast_days?: number;
include_confidence?: boolean;
}
export interface SingleForecastRequest {
product_name: string;
forecast_date: string;
include_weather?: boolean;
include_traffic?: boolean;
confidence_level?: number;
}
export interface BatchForecastRequest {
products: string[];
start_date: string;
end_date: string;
include_weather?: boolean;
include_traffic?: boolean;
confidence_level?: number;
batch_name?: string;
}
export interface ForecastAlert {
id: string;
forecast_id: string;
alert_type: 'high_demand' | 'low_demand' | 'anomaly' | 'model_drift';
severity: 'low' | 'medium' | 'high';
message: string;
threshold_value?: number;
actual_value?: number;
is_active: boolean;
created_at: string;
acknowledged_at?: string;
notification_sent: boolean;
}
export interface QuickForecast {
product_name: string;
forecasts: {
date: string;
predicted_quantity: number;
confidence_lower: number;
confidence_upper: number;
}[];
model_info: {
model_id: string;
algorithm: string;
accuracy: number;
};
}
export interface BatchForecastStatus {
id: string;
batch_name: string;
status: 'queued' | 'running' | 'completed' | 'failed';
total_products: number;
completed_products: number;
failed_products: number;
progress: number;
created_at: string;
completed_at?: string;
error_message?: string;
}
export class ForecastingService {
/**
* Generate single forecast
*/
async createSingleForecast(request: SingleForecastRequest): Promise<ForecastRecord> {
const response = await apiClient.post<ApiResponse<ForecastRecord>>(
'/forecasting/single',
request
);
return response.data!;
}
/**
* Generate batch forecasts
*/
async createBatchForecast(request: BatchForecastRequest): Promise<BatchForecastStatus> {
const response = await apiClient.post<ApiResponse<BatchForecastStatus>>(
'/forecasting/batch',
request
);
return response.data!;
}
/**
* Get forecast records
*/
async getForecasts(params?: {
productName?: string;
startDate?: string;
endDate?: string;
page?: number;
limit?: number;
}): Promise<{
forecasts: ForecastRecord[];
total: number;
page: number;
pages: number;
}> {
const response = await apiClient.get<ApiResponse<any>>('/forecasting/list', { params });
return response.data!;
}
/**
* Get specific forecast
*/
async getForecast(forecastId: string): Promise<ForecastRecord> {
const response = await apiClient.get<ApiResponse<ForecastRecord>>(
`/api/v1/forecasting/forecasts/${forecastId}`
);
return response.data!;
}
/**
* Get forecast alerts
*/
async getForecastAlerts(params?: {
active?: boolean;
severity?: string;
alertType?: string;
page?: number;
limit?: number;
}): Promise<{
alerts: ForecastAlert[];
total: number;
page: number;
pages: number;
}> {
const response = await apiClient.get<ApiResponse<any>>('/forecasting/alerts', { params });
return response.data!;
}
/**
* Acknowledge alert
*/
async acknowledgeAlert(alertId: string): Promise<ForecastAlert> {
const response = await apiClient.put<ApiResponse<ForecastAlert>>(
`/api/v1/forecasting/alerts/${alertId}/acknowledge`
);
return response.data!;
}
/**
* Get quick forecast for product (next 7 days)
*/
async getQuickForecast(productName: string, days: number = 7): Promise<QuickForecast> {
const response = await apiClient.get<ApiResponse<QuickForecast>>(
`/api/v1/forecasting/quick/${productName}`,
{ params: { days } }
);
return response.data!;
}
/**
* Get real-time prediction
*/
async getRealtimePrediction(
productName: string,
date: string,
includeWeather: boolean = true,
includeTraffic: boolean = true
): Promise<{
product_name: string;
forecast_date: string;
predicted_quantity: number;
confidence_lower: number;
confidence_upper: number;
external_factors: {
weather?: any;
traffic?: any;
holidays?: any;
};
processing_time_ms: number;
}> {
const response = await apiClient.post<ApiResponse<any>>(
'/forecasting/realtime',
{
product_name: productName,
forecast_date: date,
include_weather: includeWeather,
include_traffic: includeTraffic,
}
);
return response.data!;
}
/**
* Get batch forecast status
*/
async getBatchStatus(batchId: string): Promise<BatchForecastStatus> {
const response = await apiClient.get<ApiResponse<BatchForecastStatus>>(
`/api/v1/forecasting/batch/${batchId}/status`
);
return response.data!;
}
/**
* Cancel batch forecast
*/
async cancelBatchForecast(batchId: string): Promise<void> {
await apiClient.post(`/api/v1/forecasting/batch/${batchId}/cancel`);
}
/**
* Get forecasting statistics
*/
async getForecastingStats(): Promise<{
total_forecasts: number;
accuracy_avg: number;
active_alerts: number;
forecasts_today: number;
products_forecasted: number;
last_forecast_date: string | null;
}> {
const response = await apiClient.get<ApiResponse<any>>('/api/v1/forecasting/stats');
return response.data!;
}
/**
* Compare forecast vs actual
*/
async compareForecastActual(params?: {
productName?: string;
startDate?: string;
endDate?: string;
}): Promise<{
comparisons: {
date: string;
product_name: string;
predicted: number;
actual: number;
error: number;
percentage_error: number;
}[];
summary: {
mape: number;
rmse: number;
mae: number;
accuracy: number;
};
}> {
const response = await apiClient.get<ApiResponse<any>>('/api/v1/forecasting/compare', { params });
return response.data!;
}
/**
* Export forecasts
*/
async exportForecasts(params?: {
productName?: string;
startDate?: string;
endDate?: string;
format?: 'csv' | 'excel';
}): Promise<Blob> {
const response = await apiClient.get('/api/v1/forecasting/export', {
params,
responseType: 'blob',
});
return response as unknown as Blob;
}
/**
* Get business insights
*/
async getBusinessInsights(params?: {
period?: 'week' | 'month' | 'quarter';
products?: string[];
}): Promise<{
insights: {
type: 'trend' | 'seasonality' | 'anomaly' | 'opportunity';
title: string;
description: string;
confidence: number;
impact: 'low' | 'medium' | 'high';
products_affected: string[];
}[];
recommendations: {
title: string;
description: string;
priority: number;
estimated_impact: string;
}[];
}> {
const response = await apiClient.get<ApiResponse<any>>('/api/v1/forecasting/insights', { params });
return response.data!;
}
}
export const forecastingService = new ForecastingService();

View File

@@ -1,80 +1,36 @@
// src/api/services/index.ts
// frontend/src/api/services/index.ts
/**
* Main API Services Index
* Central import point for all service modules
* Main Services Export
* Central export point for all API services
*/
// Import all service classes
import { AuthService, authService } from './authService';
import { DataService, dataService } from './dataService';
import { TrainingService, trainingService } from './trainingService';
import { ForecastingService, forecastingService } from './forecastingService';
import { NotificationService, notificationService } from './notificationService';
import { TenantService, tenantService } from './tenantService';
// Import all services
export { AuthService, authService } from './auth.service';
export { TenantService, tenantService } from './tenant.service';
export { DataService, dataService } from './data.service';
export { TrainingService, trainingService } from './training.service';
export { ForecastingService, forecastingService } from './forecasting.service';
export { NotificationService, notificationService } from './notification.service';
// Import base API client for custom implementations
export { apiClient } from '../base/apiClient';
// Import base client
export { apiClient } from '../client';
// Re-export all types from the main types file
export * from '../types/api';
// Re-export all types
export * from '../types';
// Export additional service-specific types
export type {
DashboardStats,
UploadResponse,
DataValidation,
} from './dataService';
export type {
TrainingJobProgress,
ModelMetrics,
TrainingConfiguration,
TrainingJobStatus
} from './trainingService';
export type {
SingleForecastRequest,
BatchForecastRequest,
ForecastAlert,
QuickForecast,
BatchForecastStatus,
} from './forecastingService';
export type {
NotificationCreate,
NotificationResponse,
NotificationHistory,
NotificationTemplate,
NotificationStats,
BulkNotificationRequest,
BulkNotificationStatus,
} from './notificationService';
export type {
TenantCreate,
TenantUpdate,
TenantSettings,
TenantStats,
TenantUser,
InviteUser,
TenantInfo
} from './tenantService';
// Create a unified API object for convenience
// Create unified API object
export const api = {
auth: authService,
tenant: tenantService,
data: dataService,
training: trainingService,
forecasting: forecastingService,
notifications: notificationService,
tenant: tenantService,
notification: notificationService,
client: apiClient,
} as const;
// Type for the unified API object
export type ApiServices = typeof api;
// Service status type for monitoring
export interface ServiceStatus {
// Service status checking
export interface ServiceHealth {
service: string;
status: 'healthy' | 'degraded' | 'down';
lastChecked: Date;
@@ -82,196 +38,52 @@ export interface ServiceStatus {
error?: string;
}
// Health check utilities
export class ApiHealthChecker {
private static healthCheckEndpoints = {
auth: '/auth/health',
data: '/data/health',
training: '/training/health',
forecasting: '/forecasting/health',
notifications: '/notifications/health',
tenant: '/tenants/health',
};
export class HealthService {
async checkServiceHealth(): Promise<ServiceHealth[]> {
const services = [
{ name: 'Auth', endpoint: '/auth/health' },
{ name: 'Tenant', endpoint: '/tenants/health' },
{ name: 'Data', endpoint: '/data/health' },
{ name: 'Training', endpoint: '/training/health' },
{ name: 'Forecasting', endpoint: '/forecasting/health' },
{ name: 'Notification', endpoint: '/notifications/health' },
];
/**
* Check health of all services
*/
static async checkAllServices(): Promise<Record<string, ServiceStatus>> {
const results: Record<string, ServiceStatus> = {};
for (const [serviceName, endpoint] of Object.entries(this.healthCheckEndpoints)) {
results[serviceName] = await this.checkService(serviceName, endpoint);
}
return results;
}
const healthChecks = await Promise.allSettled(
services.map(async (service) => {
const startTime = Date.now();
try {
await apiClient.get(service.endpoint, { timeout: 5000 });
const responseTime = Date.now() - startTime;
return {
service: service.name,
status: 'healthy' as const,
lastChecked: new Date(),
responseTime,
};
} catch (error) {
return {
service: service.name,
status: 'down' as const,
lastChecked: new Date(),
error: error instanceof Error ? error.message : 'Unknown error',
};
}
})
);
/**
* Check health of a specific service
*/
static async checkService(serviceName: string, endpoint: string): Promise<ServiceStatus> {
const startTime = Date.now();
try {
const response = await apiClient.get(endpoint, { timeout: 5000 });
const responseTime = Date.now() - startTime;
return {
service: serviceName,
status: response.status === 200 ? 'healthy' : 'degraded',
lastChecked: new Date(),
responseTime,
};
} catch (error: any) {
return {
service: serviceName,
status: 'down',
lastChecked: new Date(),
responseTime: Date.now() - startTime,
error: error.message || 'Unknown error',
};
}
}
/**
* Check if core services are available
*/
static async checkCoreServices(): Promise<boolean> {
const coreServices = ['auth', 'data', 'forecasting'];
const results = await this.checkAllServices();
return coreServices.every(
service => results[service]?.status === 'healthy'
return healthChecks.map((result, index) =>
result.status === 'fulfilled'
? result.value
: {
service: services[index].name,
status: 'down' as const,
lastChecked: new Date(),
error: 'Health check failed',
}
);
}
}
// Error handling utilities
export class ApiErrorHandler {
/**
* Handle common API errors
*/
static handleError(error: any): never {
if (error.response) {
// Server responded with error status
const { status, data } = error.response;
switch (status) {
case 401:
throw new Error('Authentication required. Please log in again.');
case 403:
throw new Error('You do not have permission to perform this action.');
case 404:
throw new Error('The requested resource was not found.');
case 429:
throw new Error('Too many requests. Please try again later.');
case 500:
throw new Error('Server error. Please try again later.');
default:
throw new Error(data?.message || `Request failed with status ${status}`);
}
} else if (error.request) {
// Network error
throw new Error('Network error. Please check your connection.');
} else {
// Other error
throw new Error(error.message || 'An unexpected error occurred.');
}
}
/**
* Retry failed requests with exponential backoff
*/
static async retryRequest<T>(
requestFn: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<T> {
let lastError: any;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await requestFn();
} catch (error: any) {
lastError = error;
// Don't retry on certain errors
if (error.response?.status === 401 || error.response?.status === 403) {
throw error;
}
// Don't retry on last attempt
if (attempt === maxRetries) {
break;
}
// Wait before retrying with exponential backoff
const delay = baseDelay * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
}
// Request cache utilities for performance optimization
export class ApiCache {
private static cache = new Map<string, { data: any; expires: number }>();
/**
* Get cached response
*/
static get<T>(key: string): T | null {
const cached = this.cache.get(key);
if (cached && cached.expires > Date.now()) {
return cached.data;
}
// Remove expired cache entry
if (cached) {
this.cache.delete(key);
}
return null;
}
/**
* Set cached response
*/
static set(key: string, data: any, ttlMs: number = 300000): void { // 5 minutes default
const expires = Date.now() + ttlMs;
this.cache.set(key, { data, expires });
}
/**
* Clear cache
*/
static clear(): void {
this.cache.clear();
}
/**
* Clear expired entries
*/
static cleanup(): void {
const now = Date.now();
for (const [key, value] of this.cache.entries()) {
if (value.expires <= now) {
this.cache.delete(key);
}
}
}
/**
* Generate cache key
*/
static generateKey(method: string, url: string, params?: any): string {
const paramStr = params ? JSON.stringify(params) : '';
return `${method}:${url}:${paramStr}`;
}
}
// Export default as the unified API object
export default api;
export const healthService = new HealthService();

View File

@@ -0,0 +1,185 @@
// frontend/src/api/services/notification.service.ts
/**
* Notification Service
* Handles notification operations
*/
import { apiClient } from '../client';
import type {
NotificationCreate,
NotificationResponse,
NotificationTemplate,
NotificationHistory,
NotificationStats,
BulkNotificationRequest,
BulkNotificationStatus,
PaginatedResponse,
BaseQueryParams,
} from '../types';
export class NotificationService {
/**
* Send Notification
*/
async sendNotification(
tenantId: string,
notification: NotificationCreate
): Promise<NotificationResponse> {
return apiClient.post(`/tenants/${tenantId}/notifications`, notification);
}
/**
* Send Bulk Notifications
*/
async sendBulkNotifications(
tenantId: string,
request: BulkNotificationRequest
): Promise<BulkNotificationStatus> {
return apiClient.post(`/tenants/${tenantId}/notifications/bulk`, request);
}
/**
* Get Notifications
*/
async getNotifications(
tenantId: string,
params?: BaseQueryParams & {
channel?: string;
status?: string;
recipient_email?: string;
start_date?: string;
end_date?: string;
}
): Promise<PaginatedResponse<NotificationResponse>> {
return apiClient.get(`/tenants/${tenantId}/notifications`, { params });
}
/**
* Get Notification by ID
*/
async getNotification(tenantId: string, notificationId: string): Promise<NotificationResponse> {
return apiClient.get(`/tenants/${tenantId}/notifications/${notificationId}`);
}
/**
* Get Notification History
*/
async getNotificationHistory(
tenantId: string,
notificationId: string
): Promise<NotificationHistory[]> {
return apiClient.get(`/tenants/${tenantId}/notifications/${notificationId}/history`);
}
/**
* Cancel Scheduled Notification
*/
async cancelNotification(
tenantId: string,
notificationId: string
): Promise<{ message: string }> {
return apiClient.post(`/tenants/${tenantId}/notifications/${notificationId}/cancel`);
}
/**
* Get Bulk Notification Status
*/
async getBulkNotificationStatus(
tenantId: string,
batchId: string
): Promise<BulkNotificationStatus> {
return apiClient.get(`/tenants/${tenantId}/notifications/bulk/${batchId}/status`);
}
/**
* Get Notification Templates
*/
async getTemplates(
tenantId: string,
params?: BaseQueryParams & {
channel?: string;
is_active?: boolean;
}
): Promise<PaginatedResponse<NotificationTemplate>> {
return apiClient.get(`/tenants/${tenantId}/notifications/templates`, { params });
}
/**
* Create Notification Template
*/
async createTemplate(
tenantId: string,
template: Omit<NotificationTemplate, 'id' | 'tenant_id' | 'created_at' | 'updated_at'>
): Promise<NotificationTemplate> {
return apiClient.post(`/tenants/${tenantId}/notifications/templates`, template);
}
/**
* Update Notification Template
*/
async updateTemplate(
tenantId: string,
templateId: string,
template: Partial<NotificationTemplate>
): Promise<NotificationTemplate> {
return apiClient.put(`/tenants/${tenantId}/notifications/templates/${templateId}`, template);
}
/**
* Delete Notification Template
*/
async deleteTemplate(tenantId: string, templateId: string): Promise<{ message: string }> {
return apiClient.delete(`/tenants/${tenantId}/notifications/templates/${templateId}`);
}
/**
* Get Notification Statistics
*/
async getNotificationStats(
tenantId: string,
params?: {
start_date?: string;
end_date?: string;
channel?: string;
}
): Promise<NotificationStats> {
return apiClient.get(`/tenants/${tenantId}/notifications/stats`, { params });
}
/**
* Test Notification Configuration
*/
async testNotificationConfig(
tenantId: string,
config: {
channel: string;
recipient: string;
test_message: string;
}
): Promise<{ success: boolean; message: string }> {
return apiClient.post(`/tenants/${tenantId}/notifications/test`, config);
}
/**
* Get User Notification Preferences
*/
async getUserPreferences(tenantId: string, userId: string): Promise<Record<string, boolean>> {
return apiClient.get(`/tenants/${tenantId}/notifications/preferences/${userId}`);
}
/**
* Update User Notification Preferences
*/
async updateUserPreferences(
tenantId: string,
userId: string,
preferences: Record<string, boolean>
): Promise<{ message: string }> {
return apiClient.put(
`/tenants/${tenantId}/notifications/preferences/${userId}`,
preferences
);
}
}
export const notificationService = new NotificationService();

View File

@@ -1,363 +0,0 @@
// src/api/services/NotificationService.ts
import { apiClient } from '../base/apiClient';
import {
ApiResponse
} from '@/api/services';
export interface NotificationCreate {
type: 'email' | 'whatsapp' | 'push';
recipient_email?: string;
recipient_phone?: string;
recipient_push_token?: string;
subject?: string;
message: string;
template_id?: string;
template_data?: Record<string, any>;
scheduled_for?: string;
broadcast?: boolean;
priority?: 'low' | 'normal' | 'high';
}
export interface NotificationResponse {
id: string;
type: string;
recipient_email?: string;
recipient_phone?: string;
subject?: string;
message: string;
status: 'pending' | 'sent' | 'delivered' | 'failed';
created_at: string;
sent_at?: string;
delivered_at?: string;
error_message?: string;
}
export interface NotificationHistory {
id: string;
type: string;
recipient: string;
subject?: string;
status: string;
created_at: string;
sent_at?: string;
delivered_at?: string;
opened_at?: string;
clicked_at?: string;
error_message?: string;
}
export interface NotificationTemplate {
id: string;
name: string;
description: string;
type: 'email' | 'whatsapp' | 'push';
subject?: string;
content: string;
variables: string[];
is_system: boolean;
is_active: boolean;
created_at: string;
}
export interface NotificationStats {
total_sent: number;
total_delivered: number;
total_failed: number;
delivery_rate: number;
open_rate: number;
click_rate: number;
by_type: {
email: { sent: number; delivered: number; opened: number; clicked: number };
whatsapp: { sent: number; delivered: number; read: number };
push: { sent: number; delivered: number; opened: number };
};
}
export interface BulkNotificationRequest {
type: 'email' | 'whatsapp' | 'push';
recipients: {
email?: string;
phone?: string;
push_token?: string;
template_data?: Record<string, any>;
}[];
template_id?: string;
subject?: string;
message?: string;
scheduled_for?: string;
batch_name?: string;
}
export interface BulkNotificationStatus {
id: string;
batch_name?: string;
total_recipients: number;
sent: number;
delivered: number;
failed: number;
status: 'queued' | 'processing' | 'completed' | 'failed';
created_at: string;
completed_at?: string;
}
// Notification types
export interface NotificationSettings {
email_enabled: boolean;
whatsapp_enabled: boolean;
training_notifications: boolean;
forecast_notifications: boolean;
alert_thresholds: {
low_stock_percentage: number;
high_demand_increase: number;
};
}
export class NotificationService {
/**
* Send single notification
*/
async sendNotification(notification: NotificationCreate): Promise<NotificationResponse> {
const response = await apiClient.post<ApiResponse<NotificationResponse>>(
'/api/v1/notifications/send',
notification
);
return response.data!;
}
/**
* Send bulk notifications
*/
async sendBulkNotifications(request: BulkNotificationRequest): Promise<BulkNotificationStatus> {
const response = await apiClient.post<ApiResponse<BulkNotificationStatus>>(
'/api/v1/notifications/bulk',
request
);
return response.data!;
}
/**
* Get notification history
*/
async getNotificationHistory(params?: {
type?: string;
status?: string;
startDate?: string;
endDate?: string;
page?: number;
limit?: number;
}): Promise<{
notifications: NotificationHistory[];
total: number;
page: number;
pages: number;
}> {
const response = await apiClient.get<ApiResponse<any>>('/api/v1/notifications/history', { params });
return response.data!;
}
/**
* Get notification by ID
*/
async getNotification(notificationId: string): Promise<NotificationResponse> {
const response = await apiClient.get<ApiResponse<NotificationResponse>>(
`/api/v1/notifications/${notificationId}`
);
return response.data!;
}
/**
* Retry failed notification
*/
async retryNotification(notificationId: string): Promise<NotificationResponse> {
const response = await apiClient.post<ApiResponse<NotificationResponse>>(
`/api/v1/notifications/${notificationId}/retry`
);
return response.data!;
}
/**
* Cancel scheduled notification
*/
async cancelNotification(notificationId: string): Promise<void> {
await apiClient.post(`/api/v1/notifications/${notificationId}/cancel`);
}
/**
* Get notification statistics
*/
async getNotificationStats(params?: {
startDate?: string;
endDate?: string;
type?: string;
}): Promise<NotificationStats> {
const response = await apiClient.get<ApiResponse<NotificationStats>>(
'/api/v1/notifications/stats',
{ params }
);
return response.data!;
}
/**
* Get bulk notification status
*/
async getBulkStatus(batchId: string): Promise<BulkNotificationStatus> {
const response = await apiClient.get<ApiResponse<BulkNotificationStatus>>(
`/api/v1/notifications/bulk/${batchId}/status`
);
return response.data!;
}
/**
* Get notification templates
*/
async getTemplates(params?: {
type?: string;
active?: boolean;
page?: number;
limit?: number;
}): Promise<{
templates: NotificationTemplate[];
total: number;
page: number;
pages: number;
}> {
const response = await apiClient.get<ApiResponse<any>>('/api/v1/notifications/templates', { params });
return response.data!;
}
/**
* Get template by ID
*/
async getTemplate(templateId: string): Promise<NotificationTemplate> {
const response = await apiClient.get<ApiResponse<NotificationTemplate>>(
`/api/v1/notifications/templates/${templateId}`
);
return response.data!;
}
/**
* Create notification template
*/
async createTemplate(template: {
name: string;
description: string;
type: 'email' | 'whatsapp' | 'push';
subject?: string;
content: string;
variables?: string[];
}): Promise<NotificationTemplate> {
const response = await apiClient.post<ApiResponse<NotificationTemplate>>(
'/api/v1/notifications/templates',
template
);
return response.data!;
}
/**
* Update notification template
*/
async updateTemplate(
templateId: string,
updates: Partial<NotificationTemplate>
): Promise<NotificationTemplate> {
const response = await apiClient.put<ApiResponse<NotificationTemplate>>(
`/api/v1/notifications/templates/${templateId}`,
updates
);
return response.data!;
}
/**
* Delete notification template
*/
async deleteTemplate(templateId: string): Promise<void> {
await apiClient.delete(`/api/v1/notifications/templates/${templateId}`);
}
/**
* Get user notification preferences
*/
async getPreferences(): Promise<NotificationSettings> {
const response = await apiClient.get<ApiResponse<NotificationSettings>>(
'/api/v1/notifications/preferences'
);
return response.data!;
}
/**
* Update user notification preferences
*/
async updatePreferences(preferences: Partial<NotificationSettings>): Promise<NotificationSettings> {
const response = await apiClient.put<ApiResponse<NotificationSettings>>(
'/api/v1/notifications/preferences',
preferences
);
return response.data!;
}
/**
* Test notification delivery
*/
async testNotification(type: 'email' | 'whatsapp' | 'push', recipient: string): Promise<{
success: boolean;
message: string;
delivery_time_ms?: number;
}> {
const response = await apiClient.post<ApiResponse<any>>(
'/api/v1/notifications/test',
{ type, recipient }
);
return response.data!;
}
/**
* Get delivery webhooks
*/
async getWebhooks(params?: {
type?: string;
status?: string;
page?: number;
limit?: number;
}): Promise<{
webhooks: {
id: string;
notification_id: string;
event_type: string;
status: string;
payload: any;
received_at: string;
}[];
total: number;
page: number;
pages: number;
}> {
const response = await apiClient.get<ApiResponse<any>>('/api/v1/notifications/webhooks', { params });
return response.data!;
}
/**
* Subscribe to notification events
*/
async subscribeToEvents(events: string[], webhookUrl: string): Promise<{
subscription_id: string;
events: string[];
webhook_url: string;
created_at: string;
}> {
const response = await apiClient.post<ApiResponse<any>>('/api/v1/notifications/subscribe', {
events,
webhook_url: webhookUrl,
});
return response.data!;
}
/**
* Unsubscribe from notification events
*/
async unsubscribeFromEvents(subscriptionId: string): Promise<void> {
await apiClient.delete(`/api/v1/notifications/subscribe/${subscriptionId}`);
}
}
export const notificationService = new NotificationService();

View File

@@ -0,0 +1,101 @@
// frontend/src/api/services/tenant.service.ts
/**
* Tenant Management Service
* Handles all tenant-related operations
*/
import { apiClient } from '../client';
import { serviceEndpoints } from '../client/config';
import type {
TenantInfo,
TenantCreate,
TenantUpdate,
TenantMember,
InviteUser,
TenantStats,
PaginatedResponse,
BaseQueryParams,
} from '../types';
export class TenantService {
private baseEndpoint = serviceEndpoints.tenant;
/**
* Create New Tenant
*/
async createTenant(data: TenantCreate): Promise<TenantInfo> {
return apiClient.post(`${this.baseEndpoint}/register`, data);
}
/**
* Get Tenant Details
*/
async getTenant(tenantId: string): Promise<TenantInfo> {
return apiClient.get(`${this.baseEndpoint}/${tenantId}`);
}
/**
* Update Tenant
*/
async updateTenant(tenantId: string, data: TenantUpdate): Promise<TenantInfo> {
return apiClient.put(`${this.baseEndpoint}/${tenantId}`, data);
}
/**
* Delete Tenant
*/
async deleteTenant(tenantId: string): Promise<{ message: string }> {
return apiClient.delete(`${this.baseEndpoint}/${tenantId}`);
}
/**
* Get Tenant Members
*/
async getTenantMembers(
tenantId: string,
params?: BaseQueryParams
): Promise<PaginatedResponse<TenantMember>> {
return apiClient.get(`${this.baseEndpoint}/${tenantId}/members`, { params });
}
/**
* Invite User to Tenant
*/
async inviteUser(tenantId: string, invitation: InviteUser): Promise<{ message: string }> {
return apiClient.post(`${this.baseEndpoint}/${tenantId}/invite`, invitation);
}
/**
* Remove Member from Tenant
*/
async removeMember(tenantId: string, userId: string): Promise<{ message: string }> {
return apiClient.delete(`${this.baseEndpoint}/${tenantId}/members/${userId}`);
}
/**
* Update Member Role
*/
async updateMemberRole(
tenantId: string,
userId: string,
role: string
): Promise<TenantMember> {
return apiClient.patch(`${this.baseEndpoint}/${tenantId}/members/${userId}`, { role });
}
/**
* Get Tenant Statistics
*/
async getTenantStats(tenantId: string): Promise<TenantStats> {
return apiClient.get(`${this.baseEndpoint}/${tenantId}/stats`);
}
/**
* Get User's Tenants
*/
async getUserTenants(): Promise<TenantInfo[]> {
return apiClient.get(`/users/me/tenants`);
}
}
export const tenantService = new TenantService();

View File

@@ -1,149 +0,0 @@
// src/api/services/TenantService.ts
import { apiClient } from '../base/apiClient';
import {
ApiResponse
} from '@/api/services';
export interface TenantCreate {
name: string;
address: string;
city?: string; // Optional with default "Madrid"
postal_code: string; // Required, must match pattern ^\d{5}$
phone: string; // Required, validated for Spanish format
business_type?: string; // Optional with default "bakery", must be one of: ['bakery', 'coffee_shop', 'pastry_shop', 'restaurant']
}
export interface TenantUpdate extends Partial<TenantCreate> {
is_active?: boolean;
}
export interface TenantSettings {
business_hours: {
monday: { open: string; close: string; closed: boolean };
tuesday: { open: string; close: string; closed: boolean };
wednesday: { open: string; close: string; closed: boolean };
thursday: { open: string; close: string; closed: boolean };
friday: { open: string; close: string; closed: boolean };
saturday: { open: string; close: string; closed: boolean };
sunday: { open: string; close: string; closed: boolean };
};
timezone: string;
currency: string;
language: string;
notification_preferences: {
email_enabled: boolean;
whatsapp_enabled: boolean;
forecast_alerts: boolean;
training_notifications: boolean;
weekly_reports: boolean;
};
forecast_preferences: {
default_forecast_days: number;
confidence_level: number;
include_weather: boolean;
include_traffic: boolean;
alert_thresholds: {
high_demand_increase: number;
low_demand_decrease: number;
};
};
data_retention_days: number;
}
export interface TenantStats {
total_users: number;
active_users: number;
total_sales_records: number;
total_forecasts: number;
total_notifications_sent: number;
storage_used_mb: number;
api_calls_this_month: number;
last_activity: string;
subscription_status: 'active' | 'inactive' | 'suspended';
subscription_expires: string;
}
export interface TenantInfo {
id: string;
name: string;
subdomain?: string;
business_type: string;
address: string;
city: string;
postal_code: string;
phone?: string;
is_active: boolean;
subscription_tier: string;
model_trained: boolean;
last_training_date?: string;
owner_id: string;
created_at: string;
}
export interface InviteUser {
email: string;
role: 'admin' | 'manager' | 'user';
full_name?: string;
send_invitation_email?: boolean;
}
// New interface for tenant member response based on backend
export interface TenantMemberResponse {
user_id: string;
tenant_id: string;
role: string;
// Add any other fields expected from the backend's TenantMemberResponse
}
// Tenant types
export interface TenantInfo {
id: string;
name: string;
email: string;
phone: string;
address: string;
latitude: number;
longitude: number;
business_type: string;
is_active: boolean;
created_at: string;
}
export class TenantService {
/**
* Register a new bakery (tenant)
* Corresponds to POST /tenants/register
*/
async registerBakery(bakeryData: TenantCreate): Promise<TenantInfo> {
const response = await apiClient.post<TenantInfo>('/api/v1/tenants/register', bakeryData);
return response;
}
/**
* Get a specific tenant by ID
* Corresponds to GET /tenants/{tenant_id}
*/
async getTenantById(tenantId: string): Promise<TenantInfo> {
const response = await apiClient.get<ApiResponse<TenantInfo>>(`/api/v1/tenants/${tenantId}`);
return response.data!;
}
/**
* Update a specific tenant by ID
* Corresponds to PUT /tenants/{tenant_id}
*/
async updateTenant(tenantId: string, updates: TenantUpdate): Promise<TenantInfo> {
const response = await apiClient.put<ApiResponse<TenantInfo>>(`/api/v1/tenants/${tenantId}`, updates);
return response.data!;
}
/**
* Get all tenants associated with a user
* Corresponds to GET /users/{user_id}/tenants
*/
async getUserTenants(userId: string): Promise<TenantInfo[]> {
const response = await apiClient.get<ApiResponse<TenantInfo[]>>(`/api/v1/tenants/user/${userId}`);
return response.data!;
}
}

View File

@@ -0,0 +1,160 @@
// frontend/src/api/services/training.service.ts
/**
* Training Service
* Handles ML model training operations
*/
import { apiClient } from '../client';
import { RequestTimeouts } from '../client/config';
import type {
TrainingJobRequest,
TrainingJobResponse,
SingleProductTrainingRequest,
ModelInfo,
ModelTrainingStats,
PaginatedResponse,
BaseQueryParams,
} from '../types';
export class TrainingService {
/**
* Start Training Job for All Products
*/
async startTrainingJob(
tenantId: string,
request: TrainingJobRequest
): Promise<TrainingJobResponse> {
return apiClient.post(
`/tenants/${tenantId}/training/jobs`,
request,
{
timeout: RequestTimeouts.EXTENDED,
}
);
}
/**
* Start Training for Single Product
*/
async startSingleProductTraining(
tenantId: string,
request: SingleProductTrainingRequest
): Promise<TrainingJobResponse> {
return apiClient.post(
`/tenants/${tenantId}/training/single`,
request,
{
timeout: RequestTimeouts.EXTENDED,
}
);
}
/**
* Get Training Job Status
*/
async getTrainingJobStatus(tenantId: string, jobId: string): Promise<TrainingJobResponse> {
return apiClient.get(`/tenants/${tenantId}/training/jobs/${jobId}/status`);
}
/**
* Get Training Job Logs
*/
async getTrainingJobLogs(tenantId: string, jobId: string): Promise<{ logs: string[] }> {
return apiClient.get(`/tenants/${tenantId}/training/jobs/${jobId}/logs`);
}
/**
* Cancel Training Job
*/
async cancelTrainingJob(tenantId: string, jobId: string): Promise<{ message: string }> {
return apiClient.post(`/tenants/${tenantId}/training/jobs/${jobId}/cancel`);
}
/**
* Get Training Jobs
*/
async getTrainingJobs(
tenantId: string,
params?: BaseQueryParams & {
status?: string;
start_date?: string;
end_date?: string;
}
): Promise<PaginatedResponse<TrainingJobResponse>> {
return apiClient.get(`/tenants/${tenantId}/training/jobs`, { params });
}
/**
* Validate Data for Training
*/
async validateTrainingData(tenantId: string): Promise<{
is_valid: boolean;
message: string;
details?: any;
}> {
return apiClient.post(`/tenants/${tenantId}/training/validate`);
}
/**
* Get Trained Models
*/
async getModels(
tenantId: string,
params?: BaseQueryParams & {
product_name?: string;
is_active?: boolean;
}
): Promise<PaginatedResponse<ModelInfo>> {
return apiClient.get(`/tenants/${tenantId}/models`, { params });
}
/**
* Get Model Details
*/
async getModel(tenantId: string, modelId: string): Promise<ModelInfo> {
return apiClient.get(`/tenants/${tenantId}/models/${modelId}`);
}
/**
* Update Model Status
*/
async updateModelStatus(
tenantId: string,
modelId: string,
isActive: boolean
): Promise<ModelInfo> {
return apiClient.patch(`/tenants/${tenantId}/models/${modelId}`, {
is_active: isActive,
});
}
/**
* Delete Model
*/
async deleteModel(tenantId: string, modelId: string): Promise<{ message: string }> {
return apiClient.delete(`/tenants/${tenantId}/models/${modelId}`);
}
/**
* Get Training Statistics
*/
async getTrainingStats(tenantId: string): Promise<ModelTrainingStats> {
return apiClient.get(`/tenants/${tenantId}/training/stats`);
}
/**
* Download Model File
*/
async downloadModel(tenantId: string, modelId: string): Promise<Blob> {
const response = await apiClient.request(`/tenants/${tenantId}/models/${modelId}/download`, {
method: 'GET',
headers: {
'Accept': 'application/octet-stream',
},
});
return new Blob([response]);
}
}
export const trainingService = new TrainingService();

View File

@@ -1,254 +0,0 @@
// src/api/services/TrainingService.ts
import { apiClient } from '../base/apiClient';
import {
ApiResponse
} from '../types/api';
export interface TrainingJobStatus {
job_id: string;
tenant_id: string;
status: 'queued' | 'running' | 'completed' | 'failed' | 'cancelled';
progress: number;
current_step?: string;
started_at: string;
completed_at?: string;
duration_seconds?: number;
models_trained?: Record<string, any>;
metrics?: Record<string, any>;
error_message?: string;
}
export interface TrainingRequest {
force_retrain?: boolean;
products?: string[];
training_days?: number;
}
export interface TrainedModel {
id: string;
product_name: string;
model_type: string;
model_version: string;
mape?: number;
rmse?: number;
mae?: number;
r2_score?: number;
training_samples?: number;
features_used?: string[];
is_active: boolean;
created_at: string;
last_used_at?: string;
}
export interface TrainingJobProgress {
id: string;
status: 'queued' | 'running' | 'completed' | 'failed' | 'cancelled';
progress: number;
current_step?: string;
total_steps?: number;
step_details?: string;
estimated_completion?: string;
logs?: string[];
}
export interface ModelMetrics {
mape: number;
rmse: number;
mae: number;
r2_score: number;
training_samples: number;
validation_samples: number;
features_used: string[];
}
export interface TrainingConfiguration {
include_weather: boolean;
include_traffic: boolean;
min_data_points: number;
forecast_horizon_days: number;
cross_validation_folds: number;
hyperparameter_tuning: boolean;
products?: string[];
}
export class TrainingService {
/**
* Start new training job
*/
async startTraining(tenantId: string, config: TrainingConfiguration): Promise<TrainingJobStatus> {
const response = await apiClient.post<TrainingJobStatus>(
`/api/v1/tenants/${tenantId}/training/jobs`,
config
);
return response.data!;
}
/**
* Get training job status
*/
async getTrainingStatus(tenantId: string, jobId: string): Promise<TrainingJobProgress> {
const response = await apiClient.get<ApiResponse<TrainingJobProgress>>(
`/api/v1/tenants/${tenantId}/training/jobs/${jobId}`
);
return response.data!;
}
/**
* Get all training jobs
*/
async getTrainingHistory(tenantId: string, params?: {
page?: number;
limit?: number;
status?: string;
}): Promise<{
jobs: TrainingJobStatus[];
total: number;
page: number;
pages: number;
}> {
const response = await apiClient.get<ApiResponse<any>>('/api/v1/training/jobs', { params });
return response.data!;
}
/**
* Cancel training job
*/
async cancelTraining(tenantId: string, jobId: string): Promise<void> {
await apiClient.post(`/api/v1/tenants/${tenantId}/training/jobs/${jobId}/cancel`);
}
/**
* Get trained models
*/
async getModels(tenantId: string, params?: {
productName?: string;
active?: boolean;
page?: number;
limit?: number;
}): Promise<{
models: TrainedModel[];
total: number;
page: number;
pages: number;
}> {
const response = await apiClient.get<ApiResponse<any>>(`/api/v1/tenants/${tenantId}/training/models`, { params });
return response.data!;
}
/**
* Get specific model details
*/
async getModel(tenantId: string, modelId: string): Promise<TrainedModel> {
const response = await apiClient.get<ApiResponse<TrainedModel>>(
`/api/v1/training/models/${modelId}`
);
return response.data!;
}
/**
* Get model metrics
*/
async getModelMetrics(tenantId: string, modelId: string): Promise<ModelMetrics> {
const response = await apiClient.get<ApiResponse<ModelMetrics>>(
`/api/v1/tenants/${tenantId}/training/models/${modelId}/metrics`
);
return response.data!;
}
/**
* Activate/deactivate model
*/
async toggleModelStatus(tenantId: string, modelId: string, active: boolean): Promise<TrainedModel> {
const response = await apiClient.patch<ApiResponse<TrainedModel>>(
`/api/v1/tenants/${tenantId}/training/models/${modelId}`,
{ is_active: active }
);
return response.data!;
}
/**
* Delete model
*/
async deleteModel(tenantId: string, modelId: string): Promise<void> {
await apiClient.delete(`/api/v1/training/models/${modelId}`);
}
/**
* Train specific product
*/
async trainProduct(tenantId: string, productName: string, config?: Partial<TrainingConfiguration>): Promise<TrainingJobStatus> {
const response = await apiClient.post<ApiResponse<TrainingJobStatus>>(
`/api/v1/tenants/${tenantId}/training/products/train`,
{
product_name: productName,
...config,
}
);
return response.data!;
}
/**
* Get training statistics
*/
async getTrainingStats(tenantId: string): Promise<{
total_models: number;
active_models: number;
avg_accuracy: number;
last_training_date: string | null;
products_trained: number;
training_time_avg_minutes: number;
}> {
const response = await apiClient.get<ApiResponse<any>>(`/api/v1/tenants/${tenantId}/training/stats`);
return response.data!;
}
/**
* Validate training data
*/
async validateTrainingData(tenantId: string, products?: string[]): Promise<{
valid: boolean;
errors: string[];
warnings: string[];
product_data_points: Record<string, number>;
recommendation: string;
}> {
const response = await apiClient.post<ApiResponse<any>>(`/api/v1/tenants/${tenantId}/training/validate`, {
products,
});
return response.data!;
}
/**
* Get training recommendations
*/
async getTrainingRecommendations(tenantId: string): Promise<{
should_retrain: boolean;
reasons: string[];
recommended_products: string[];
optimal_config: TrainingConfiguration;
}> {
const response = await apiClient.get<ApiResponse<any>>(`/api/v1/tenants/${tenantId}/training/recommendations`);
return response.data!;
}
/**
* Get training logs
*/
async getTrainingLogs(tenantId: string, jobId: string): Promise<string[]> {
const response = await apiClient.get<ApiResponse<string[]>>(`/api/v1/tenants/${tenantId}/training/jobs/${jobId}/logs`);
return response.data!;
}
/**
* Export model
*/
async exportModel(tenantId: string, modelId: string, format: 'pickle' | 'onnx' = 'pickle'): Promise<Blob> {
const response = await apiClient.get(`/api/v1/tenants/${tenantId}/training/models/${modelId}/export`, {
params: { format },
responseType: 'blob',
});
return response as unknown as Blob;
}
}
export const trainingService = new TrainingService();