Add new Frontend API folder
This commit is contained in:
107
frontend/src/api/services/auth.service.ts
Normal file
107
frontend/src/api/services/auth.service.ts
Normal 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();
|
||||
@@ -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();
|
||||
182
frontend/src/api/services/data.service.ts
Normal file
182
frontend/src/api/services/data.service.ts
Normal 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();
|
||||
@@ -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();
|
||||
198
frontend/src/api/services/forecasting.service.ts
Normal file
198
frontend/src/api/services/forecasting.service.ts
Normal 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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
185
frontend/src/api/services/notification.service.ts
Normal file
185
frontend/src/api/services/notification.service.ts
Normal 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();
|
||||
@@ -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();
|
||||
101
frontend/src/api/services/tenant.service.ts
Normal file
101
frontend/src/api/services/tenant.service.ts
Normal 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();
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
}
|
||||
160
frontend/src/api/services/training.service.ts
Normal file
160
frontend/src/api/services/training.service.ts
Normal 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();
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user