Add new frontend - fix 9
This commit is contained in:
@@ -27,6 +27,9 @@ volumes:
|
|||||||
grafana_data:
|
grafana_data:
|
||||||
model_storage:
|
model_storage:
|
||||||
log_storage:
|
log_storage:
|
||||||
|
nominatim_db_data:
|
||||||
|
nominatim_data:
|
||||||
|
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# SERVICES - USING ONLY .env FILE
|
# SERVICES - USING ONLY .env FILE
|
||||||
@@ -215,6 +218,72 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# LOCATION SERVICES (NEW SECTION)
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
nominatim-db:
|
||||||
|
image: postgis/postgis:15-3.3 # Use PostGIS enabled PostgreSQL image
|
||||||
|
container_name: bakery-nominatim-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=${NOMINATIM_DB_NAME}
|
||||||
|
- POSTGRES_USER=${NOMINATIM_DB_USER}
|
||||||
|
- POSTGRES_PASSWORD=${NOMINATIM_DB_PASSWORD}
|
||||||
|
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||||
|
- POSTGRES_INITDB_ARGS="--auth-host=scram-sha-256" # Recommended for PostGIS
|
||||||
|
volumes:
|
||||||
|
- nominatim_db_data:/var/lib/postgresql/data
|
||||||
|
profiles:
|
||||||
|
- development
|
||||||
|
networks:
|
||||||
|
bakery-network:
|
||||||
|
ipv4_address: 172.20.0.30 # Assign a static IP for Nominatim to find it
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${NOMINATIM_DB_USER} -d ${NOMINATIM_DB_NAME}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
nominatim:
|
||||||
|
image: mediagis/nominatim:4.2 # A pre-built Nominatim image
|
||||||
|
container_name: bakery-nominatim
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: .env # Load environment variables from .env file
|
||||||
|
environment:
|
||||||
|
# Database connection details for Nominatim
|
||||||
|
- POSTGRES_HOST=nominatim-db
|
||||||
|
- POSTGRES_PORT=5432
|
||||||
|
- POSTGRES_USER=${NOMINATIM_DB_USER}
|
||||||
|
- POSTGRES_PASSWORD=${NOMINATIM_DB_PASSWORD}
|
||||||
|
- POSTGRES_DB=${NOMINATIM_DB_NAME}
|
||||||
|
- PBF_URL=${NOMINATIM_PBF_URL} # URL to your OpenStreetMap PBF data (e.g., Spain)
|
||||||
|
ports:
|
||||||
|
- "${NOMINATIM_PORT}:8080" # Expose Nominatim web interface
|
||||||
|
volumes:
|
||||||
|
- nominatim_data:/var/lib/nominatim # Persistent storage for Nominatim data and configuration
|
||||||
|
networks:
|
||||||
|
bakery-network:
|
||||||
|
ipv4_address: 172.20.0.120 # Assign a static IP for Nominatim service
|
||||||
|
depends_on:
|
||||||
|
nominatim-db:
|
||||||
|
condition: service_healthy # Ensure database is ready before Nominatim starts
|
||||||
|
# By default, mediagis/nominatim image will try to import data on first run
|
||||||
|
# if PBF_URL is set and the database is empty.
|
||||||
|
profiles:
|
||||||
|
- development
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080/nominatim/status.php"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: ${NOMINATIM_MEMORY_LIMIT:-8G} # Nominatim is memory-intensive for import
|
||||||
|
cpus: '${NOMINATIM_CPU_LIMIT:-4}' # Adjust based on your system and data
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# MICROSERVICES - CLEAN APPROACH
|
# MICROSERVICES - CLEAN APPROACH
|
||||||
# ================================================================
|
# ================================================================
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
// src/api/services/api.ts
|
|
||||||
import { apiClient } from '../base/apiClient';
|
|
||||||
import {
|
|
||||||
ApiResponse,
|
|
||||||
LoginRequest,
|
|
||||||
RegisterRequest,
|
|
||||||
TokenResponse,
|
|
||||||
UserProfile,
|
|
||||||
TenantInfo,
|
|
||||||
SalesRecord,
|
|
||||||
TrainingRequest,
|
|
||||||
TrainedModel,
|
|
||||||
ForecastRecord,
|
|
||||||
ForecastRequest,
|
|
||||||
WeatherData,
|
|
||||||
TrafficData,
|
|
||||||
NotificationSettings,
|
|
||||||
// ... other types from your api.ts file
|
|
||||||
} from '../types/api'; // This should point to your main types file (api.ts)
|
|
||||||
|
|
||||||
// Assuming your api.ts defines these interfaces:
|
|
||||||
// interface DashboardStats { ... }
|
|
||||||
// interface ApiResponse<T> { ... }
|
|
||||||
|
|
||||||
|
|
||||||
// Define DashboardStats interface here or ensure it's imported from your main types file
|
|
||||||
export interface DashboardStats {
|
|
||||||
totalSales: number;
|
|
||||||
totalRevenue: number;
|
|
||||||
lastTrainingDate: string | null;
|
|
||||||
forecastAccuracy: number; // e.g., MAPE or RMSE
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const dataApi = {
|
|
||||||
uploadSalesHistory: (file: File, additionalData?: Record<string, any>) =>
|
|
||||||
apiClient.upload<ApiResponse<any>>('/data/upload-sales', file, additionalData),
|
|
||||||
getDashboardStats: () =>
|
|
||||||
apiClient.get<ApiResponse<DashboardStats>>('/dashboard/stats'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const forecastingApi = {
|
|
||||||
getForecast: (params: ForecastRequest) =>
|
|
||||||
apiClient.get<ApiResponse<ForecastRecord[]>>('/forecast', { params }),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Re-export all types from the original api.ts file
|
|
||||||
export * from '../types/api'
|
|
||||||
@@ -1,116 +1,126 @@
|
|||||||
// frontend/src/api/services/authService.ts - UPDATED TO HANDLE TOKENS FROM REGISTRATION
|
// src/api/services/AuthService.ts
|
||||||
import { tokenManager } from '../auth/tokenManager';
|
|
||||||
import { apiClient } from '../base/apiClient';
|
import { apiClient } from '../base/apiClient';
|
||||||
|
import {
|
||||||
|
ApiResponse,
|
||||||
|
LoginRequest,
|
||||||
|
RegisterRequest,
|
||||||
|
TokenResponse,
|
||||||
|
UserProfile,
|
||||||
|
} from '../types/api';
|
||||||
|
|
||||||
export interface LoginCredentials {
|
export class AuthService {
|
||||||
email: string;
|
/**
|
||||||
password: string;
|
* User login
|
||||||
|
*/
|
||||||
|
async login(credentials: LoginRequest): Promise<TokenResponse> {
|
||||||
|
const response = await apiClient.post<ApiResponse<TokenResponse>>(
|
||||||
|
'/auth/login',
|
||||||
|
credentials
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterData {
|
/**
|
||||||
email: string;
|
* User registration
|
||||||
password: string;
|
*/
|
||||||
full_name: string;
|
async register(userData: RegisterRequest): Promise<UserProfile> {
|
||||||
tenant_name?: string;
|
const response = await apiClient.post<ApiResponse<UserProfile>>(
|
||||||
|
'/auth/register',
|
||||||
|
userData
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserProfile {
|
/**
|
||||||
id: string;
|
* Refresh access token
|
||||||
email: string;
|
*/
|
||||||
full_name: string;
|
async refreshToken(refreshToken: string): Promise<TokenResponse> {
|
||||||
tenant_id?: string;
|
const response = await apiClient.post<ApiResponse<TokenResponse>>(
|
||||||
role?: string;
|
'/auth/refresh',
|
||||||
is_active: boolean;
|
{ refresh_token: refreshToken }
|
||||||
is_verified?: boolean;
|
);
|
||||||
created_at: string;
|
return response.data!;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TokenResponse {
|
/**
|
||||||
access_token: string;
|
* Get current user profile
|
||||||
refresh_token?: string;
|
*/
|
||||||
token_type: string;
|
async getProfile(): Promise<UserProfile> {
|
||||||
expires_in?: number;
|
const response = await apiClient.get<ApiResponse<UserProfile>>('/users/me');
|
||||||
user?: UserProfile;
|
return response.data!;
|
||||||
}
|
|
||||||
|
|
||||||
class AuthService {
|
|
||||||
async register(data: RegisterData): Promise<UserProfile> {
|
|
||||||
// NEW: Registration now returns tokens directly - no auto-login needed!
|
|
||||||
const response: TokenResponse = await apiClient.post('/api/v1/auth/register', data);
|
|
||||||
|
|
||||||
// Store tokens immediately from registration response
|
|
||||||
await tokenManager.storeTokens(response);
|
|
||||||
|
|
||||||
// Return user profile from registration response
|
|
||||||
if (response.user) {
|
|
||||||
return response.user;
|
|
||||||
} else {
|
|
||||||
// Fallback: get user profile if not included in response
|
|
||||||
return this.getCurrentUser();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async login(credentials: LoginCredentials): Promise<UserProfile> {
|
|
||||||
// UPDATED: Use correct endpoint and unified response handling
|
|
||||||
const response: TokenResponse = await apiClient.post('/api/v1/auth/login', credentials);
|
|
||||||
|
|
||||||
// Store tokens from login response
|
|
||||||
await tokenManager.storeTokens(response);
|
|
||||||
|
|
||||||
// Return user profile from login response
|
|
||||||
if (response.user) {
|
|
||||||
return response.user;
|
|
||||||
} else {
|
|
||||||
// Fallback: get user profile if not included in response
|
|
||||||
return this.getCurrentUser();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async logout(): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Get refresh token for logout request
|
|
||||||
const refreshToken = tokenManager.getRefreshToken();
|
|
||||||
if (refreshToken) {
|
|
||||||
await apiClient.post('/api/v1/auth/logout', {
|
|
||||||
refresh_token: refreshToken
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout API call failed:', error);
|
|
||||||
// Continue with local cleanup even if API fails
|
|
||||||
} finally {
|
|
||||||
tokenManager.clearTokens();
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCurrentUser(): Promise<UserProfile> {
|
|
||||||
return apiClient.get('/api/v1/auth/me');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user profile
|
||||||
|
*/
|
||||||
async updateProfile(updates: Partial<UserProfile>): Promise<UserProfile> {
|
async updateProfile(updates: Partial<UserProfile>): Promise<UserProfile> {
|
||||||
return apiClient.patch('/api/v1/auth/profile', updates);
|
const response = await apiClient.put<ApiResponse<UserProfile>>(
|
||||||
|
'/users/me',
|
||||||
|
updates
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async changePassword(currentPassword: string, newPassword: string): Promise<void> {
|
/**
|
||||||
await apiClient.post('/api/v1/auth/change-password', {
|
* Change password
|
||||||
|
*/
|
||||||
|
async changePassword(
|
||||||
|
currentPassword: string,
|
||||||
|
newPassword: string
|
||||||
|
): Promise<void> {
|
||||||
|
await apiClient.post('/auth/change-password', {
|
||||||
current_password: currentPassword,
|
current_password: currentPassword,
|
||||||
new_password: newPassword
|
new_password: newPassword,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshToken(): Promise<void> {
|
/**
|
||||||
await tokenManager.refreshAccessToken();
|
* Request password reset
|
||||||
|
*/
|
||||||
|
async requestPasswordReset(email: string): Promise<void> {
|
||||||
|
await apiClient.post('/auth/reset-password', { email });
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuthenticated(): boolean {
|
/**
|
||||||
return tokenManager.isAuthenticated();
|
* Confirm password reset
|
||||||
|
*/
|
||||||
|
async confirmPasswordReset(
|
||||||
|
token: string,
|
||||||
|
newPassword: string
|
||||||
|
): Promise<void> {
|
||||||
|
await apiClient.post('/auth/confirm-reset', {
|
||||||
|
token,
|
||||||
|
new_password: newPassword,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser(): UserProfile | null {
|
/**
|
||||||
// This method would need to be implemented to return cached user data
|
* Verify email
|
||||||
// For now, it returns null and components should use getCurrentUser()
|
*/
|
||||||
return null;
|
async verifyEmail(token: string): Promise<void> {
|
||||||
|
await apiClient.post('/auth/verify-email', { token });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resend verification email
|
||||||
|
*/
|
||||||
|
async resendVerification(): Promise<void> {
|
||||||
|
await apiClient.post('/auth/resend-verification');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout (invalidate tokens)
|
||||||
|
*/
|
||||||
|
async logout(): Promise<void> {
|
||||||
|
await apiClient.post('/auth/logout');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user permissions
|
||||||
|
*/
|
||||||
|
async getPermissions(): Promise<string[]> {
|
||||||
|
const response = await apiClient.get<ApiResponse<string[]>>('/auth/permissions');
|
||||||
|
return response.data!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
208
frontend/src/api/services/dataService.ts
Normal file
208
frontend/src/api/services/dataService.ts
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
// src/api/services/DataService.ts
|
||||||
|
import { apiClient } from '../base/apiClient';
|
||||||
|
import {
|
||||||
|
ApiResponse,
|
||||||
|
SalesRecord,
|
||||||
|
CreateSalesRequest,
|
||||||
|
WeatherData,
|
||||||
|
TrafficData,
|
||||||
|
} 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 {
|
||||||
|
valid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
warnings: string[];
|
||||||
|
recordCount: number;
|
||||||
|
duplicates: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataService {
|
||||||
|
/**
|
||||||
|
* Upload sales history file
|
||||||
|
*/
|
||||||
|
async uploadSalesHistory(
|
||||||
|
file: File,
|
||||||
|
additionalData?: Record<string, any>
|
||||||
|
): Promise<UploadResponse> {
|
||||||
|
const response = await apiClient.upload<ApiResponse<UploadResponse>>(
|
||||||
|
'/data/upload-sales',
|
||||||
|
file,
|
||||||
|
additionalData
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate sales data before upload
|
||||||
|
*/
|
||||||
|
async validateSalesData(file: File): Promise<DataValidation> {
|
||||||
|
const response = await apiClient.upload<ApiResponse<DataValidation>>(
|
||||||
|
'/data/validate-sales',
|
||||||
|
file
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dashboard statistics
|
||||||
|
*/
|
||||||
|
async getDashboardStats(): Promise<DashboardStats> {
|
||||||
|
const response = await apiClient.get<ApiResponse<DashboardStats>>(
|
||||||
|
'/data/dashboard/stats'
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sales records
|
||||||
|
*/
|
||||||
|
async getSalesRecords(params?: {
|
||||||
|
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;
|
||||||
|
}>>('/data/sales', { params });
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create single sales record
|
||||||
|
*/
|
||||||
|
async createSalesRecord(record: CreateSalesRequest): Promise<SalesRecord> {
|
||||||
|
const response = await apiClient.post<ApiResponse<SalesRecord>>(
|
||||||
|
'/data/sales',
|
||||||
|
record
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update sales record
|
||||||
|
*/
|
||||||
|
async updateSalesRecord(
|
||||||
|
id: string,
|
||||||
|
updates: Partial<CreateSalesRequest>
|
||||||
|
): Promise<SalesRecord> {
|
||||||
|
const response = await apiClient.put<ApiResponse<SalesRecord>>(
|
||||||
|
`/data/sales/${id}`,
|
||||||
|
updates
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete sales record
|
||||||
|
*/
|
||||||
|
async deleteSalesRecord(id: string): Promise<void> {
|
||||||
|
await apiClient.delete(`/data/sales/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get weather data
|
||||||
|
*/
|
||||||
|
async getWeatherData(params?: {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
location?: string;
|
||||||
|
}): Promise<WeatherData[]> {
|
||||||
|
const response = await apiClient.get<ApiResponse<WeatherData[]>>(
|
||||||
|
'/data/weather',
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get traffic data
|
||||||
|
*/
|
||||||
|
async getTrafficData(params?: {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
location?: string;
|
||||||
|
}): Promise<TrafficData[]> {
|
||||||
|
const response = await apiClient.get<ApiResponse<TrafficData[]>>(
|
||||||
|
'/data/traffic',
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data quality report
|
||||||
|
*/
|
||||||
|
async getDataQuality(): Promise<{
|
||||||
|
salesData: { completeness: number; quality: number; lastUpdate: string };
|
||||||
|
weatherData: { completeness: number; quality: number; lastUpdate: string };
|
||||||
|
trafficData: { completeness: number; quality: number; lastUpdate: string };
|
||||||
|
}> {
|
||||||
|
const response = await apiClient.get<ApiResponse<any>>('/data/quality');
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export sales data
|
||||||
|
*/
|
||||||
|
async exportSalesData(params?: {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
format?: 'csv' | 'excel';
|
||||||
|
}): Promise<Blob> {
|
||||||
|
const response = await apiClient.get('/data/sales/export', {
|
||||||
|
params,
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
return response as unknown as Blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get product list
|
||||||
|
*/
|
||||||
|
async getProducts(): Promise<string[]> {
|
||||||
|
const response = await apiClient.get<ApiResponse<string[]>>('/data/products');
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data sync status
|
||||||
|
*/
|
||||||
|
async getSyncStatus(): Promise<{
|
||||||
|
weather: { lastSync: string; status: 'ok' | 'error'; nextSync: string };
|
||||||
|
traffic: { lastSync: string; status: 'ok' | 'error'; nextSync: string };
|
||||||
|
}> {
|
||||||
|
const response = await apiClient.get<ApiResponse<any>>('/data/sync/status');
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger manual data sync
|
||||||
|
*/
|
||||||
|
async triggerSync(dataType: 'weather' | 'traffic' | 'all'): Promise<void> {
|
||||||
|
await apiClient.post('/data/sync/trigger', { data_type: dataType });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dataService = new DataService();
|
||||||
296
frontend/src/api/services/forecastingService.ts
Normal file
296
frontend/src/api/services/forecastingService.ts
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
// src/api/services/ForecastingService.ts
|
||||||
|
import { apiClient } from '../base/apiClient';
|
||||||
|
import {
|
||||||
|
ApiResponse,
|
||||||
|
ForecastRecord,
|
||||||
|
ForecastRequest,
|
||||||
|
} from '../types/api';
|
||||||
|
|
||||||
|
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>>(
|
||||||
|
`/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>>(
|
||||||
|
`/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>>(
|
||||||
|
`/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>>(
|
||||||
|
`/forecasting/batch/${batchId}/status`
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel batch forecast
|
||||||
|
*/
|
||||||
|
async cancelBatchForecast(batchId: string): Promise<void> {
|
||||||
|
await apiClient.post(`/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>>('/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>>('/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('/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>>('/forecasting/insights', { params });
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const forecastingService = new ForecastingService();
|
||||||
275
frontend/src/api/services/index.ts
Normal file
275
frontend/src/api/services/index.ts
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
// src/api/services/index.ts
|
||||||
|
/**
|
||||||
|
* Main API Services Index
|
||||||
|
* Central import point for all service modules
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Import all service classes
|
||||||
|
export { AuthService, authService } from './AuthService';
|
||||||
|
export { DataService, dataService } from './DataService';
|
||||||
|
export { TrainingService, trainingService } from './TrainingService';
|
||||||
|
export { ForecastingService, forecastingService } from './ForecastingService';
|
||||||
|
export { NotificationService, notificationService } from './NotificationService';
|
||||||
|
export { TenantService, tenantService } from './TenantService';
|
||||||
|
|
||||||
|
// Import base API client for custom implementations
|
||||||
|
export { apiClient } from '../base/apiClient';
|
||||||
|
|
||||||
|
// Re-export all types from the main types file
|
||||||
|
export * from '../types/api';
|
||||||
|
|
||||||
|
// Export additional service-specific types
|
||||||
|
export type {
|
||||||
|
DashboardStats,
|
||||||
|
UploadResponse,
|
||||||
|
DataValidation,
|
||||||
|
} from './DataService';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
TrainingJobProgress,
|
||||||
|
ModelMetrics,
|
||||||
|
TrainingConfiguration,
|
||||||
|
} 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,
|
||||||
|
} from './TenantService';
|
||||||
|
|
||||||
|
// Create a unified API object for convenience
|
||||||
|
export const api = {
|
||||||
|
auth: authService,
|
||||||
|
data: dataService,
|
||||||
|
training: trainingService,
|
||||||
|
forecasting: forecastingService,
|
||||||
|
notifications: notificationService,
|
||||||
|
tenant: tenantService,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Type for the unified API object
|
||||||
|
export type ApiServices = typeof api;
|
||||||
|
|
||||||
|
// Service status type for monitoring
|
||||||
|
export interface ServiceStatus {
|
||||||
|
service: string;
|
||||||
|
status: 'healthy' | 'degraded' | 'down';
|
||||||
|
lastChecked: Date;
|
||||||
|
responseTime?: number;
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
351
frontend/src/api/services/notificationServices.ts
Normal file
351
frontend/src/api/services/notificationServices.ts
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
// src/api/services/NotificationService.ts
|
||||||
|
import { apiClient } from '../base/apiClient';
|
||||||
|
import {
|
||||||
|
ApiResponse,
|
||||||
|
NotificationSettings,
|
||||||
|
} from '../types/api';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationService {
|
||||||
|
/**
|
||||||
|
* Send single notification
|
||||||
|
*/
|
||||||
|
async sendNotification(notification: NotificationCreate): Promise<NotificationResponse> {
|
||||||
|
const response = await apiClient.post<ApiResponse<NotificationResponse>>(
|
||||||
|
'/notifications/send',
|
||||||
|
notification
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send bulk notifications
|
||||||
|
*/
|
||||||
|
async sendBulkNotifications(request: BulkNotificationRequest): Promise<BulkNotificationStatus> {
|
||||||
|
const response = await apiClient.post<ApiResponse<BulkNotificationStatus>>(
|
||||||
|
'/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>>('/notifications/history', { params });
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get notification by ID
|
||||||
|
*/
|
||||||
|
async getNotification(notificationId: string): Promise<NotificationResponse> {
|
||||||
|
const response = await apiClient.get<ApiResponse<NotificationResponse>>(
|
||||||
|
`/notifications/${notificationId}`
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry failed notification
|
||||||
|
*/
|
||||||
|
async retryNotification(notificationId: string): Promise<NotificationResponse> {
|
||||||
|
const response = await apiClient.post<ApiResponse<NotificationResponse>>(
|
||||||
|
`/notifications/${notificationId}/retry`
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel scheduled notification
|
||||||
|
*/
|
||||||
|
async cancelNotification(notificationId: string): Promise<void> {
|
||||||
|
await apiClient.post(`/notifications/${notificationId}/cancel`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get notification statistics
|
||||||
|
*/
|
||||||
|
async getNotificationStats(params?: {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
type?: string;
|
||||||
|
}): Promise<NotificationStats> {
|
||||||
|
const response = await apiClient.get<ApiResponse<NotificationStats>>(
|
||||||
|
'/notifications/stats',
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bulk notification status
|
||||||
|
*/
|
||||||
|
async getBulkStatus(batchId: string): Promise<BulkNotificationStatus> {
|
||||||
|
const response = await apiClient.get<ApiResponse<BulkNotificationStatus>>(
|
||||||
|
`/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>>('/notifications/templates', { params });
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get template by ID
|
||||||
|
*/
|
||||||
|
async getTemplate(templateId: string): Promise<NotificationTemplate> {
|
||||||
|
const response = await apiClient.get<ApiResponse<NotificationTemplate>>(
|
||||||
|
`/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>>(
|
||||||
|
'/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>>(
|
||||||
|
`/notifications/templates/${templateId}`,
|
||||||
|
updates
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete notification template
|
||||||
|
*/
|
||||||
|
async deleteTemplate(templateId: string): Promise<void> {
|
||||||
|
await apiClient.delete(`/notifications/templates/${templateId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user notification preferences
|
||||||
|
*/
|
||||||
|
async getPreferences(): Promise<NotificationSettings> {
|
||||||
|
const response = await apiClient.get<ApiResponse<NotificationSettings>>(
|
||||||
|
'/notifications/preferences'
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user notification preferences
|
||||||
|
*/
|
||||||
|
async updatePreferences(preferences: Partial<NotificationSettings>): Promise<NotificationSettings> {
|
||||||
|
const response = await apiClient.put<ApiResponse<NotificationSettings>>(
|
||||||
|
'/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>>(
|
||||||
|
'/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>>('/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>>('/notifications/subscribe', {
|
||||||
|
events,
|
||||||
|
webhook_url: webhookUrl,
|
||||||
|
});
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from notification events
|
||||||
|
*/
|
||||||
|
async unsubscribeFromEvents(subscriptionId: string): Promise<void> {
|
||||||
|
await apiClient.delete(`/notifications/subscribe/${subscriptionId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const notificationService = new NotificationService();
|
||||||
369
frontend/src/api/services/tenantServices.ts
Normal file
369
frontend/src/api/services/tenantServices.ts
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
// src/api/services/TenantService.ts
|
||||||
|
import { apiClient } from '../base/apiClient';
|
||||||
|
import {
|
||||||
|
ApiResponse,
|
||||||
|
TenantInfo,
|
||||||
|
} from '../types/api';
|
||||||
|
|
||||||
|
export interface TenantCreate {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
address: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
business_type: 'individual_bakery' | 'central_workshop';
|
||||||
|
subscription_plan?: string;
|
||||||
|
settings?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 TenantUser {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
full_name: string;
|
||||||
|
role: string;
|
||||||
|
is_active: boolean;
|
||||||
|
last_login: string | null;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InviteUser {
|
||||||
|
email: string;
|
||||||
|
role: 'admin' | 'manager' | 'user';
|
||||||
|
full_name?: string;
|
||||||
|
send_invitation_email?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TenantService {
|
||||||
|
/**
|
||||||
|
* Get current tenant info
|
||||||
|
*/
|
||||||
|
async getCurrentTenant(): Promise<TenantInfo> {
|
||||||
|
const response = await apiClient.get<ApiResponse<TenantInfo>>('/tenants/current');
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update current tenant
|
||||||
|
*/
|
||||||
|
async updateCurrentTenant(updates: TenantUpdate): Promise<TenantInfo> {
|
||||||
|
const response = await apiClient.put<ApiResponse<TenantInfo>>('/tenants/current', updates);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tenant settings
|
||||||
|
*/
|
||||||
|
async getTenantSettings(): Promise<TenantSettings> {
|
||||||
|
const response = await apiClient.get<ApiResponse<TenantSettings>>('/tenants/settings');
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update tenant settings
|
||||||
|
*/
|
||||||
|
async updateTenantSettings(settings: Partial<TenantSettings>): Promise<TenantSettings> {
|
||||||
|
const response = await apiClient.put<ApiResponse<TenantSettings>>(
|
||||||
|
'/tenants/settings',
|
||||||
|
settings
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tenant statistics
|
||||||
|
*/
|
||||||
|
async getTenantStats(): Promise<TenantStats> {
|
||||||
|
const response = await apiClient.get<ApiResponse<TenantStats>>('/tenants/stats');
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tenant users
|
||||||
|
*/
|
||||||
|
async getTenantUsers(params?: {
|
||||||
|
role?: string;
|
||||||
|
active?: boolean;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}): Promise<{
|
||||||
|
users: TenantUser[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pages: number;
|
||||||
|
}> {
|
||||||
|
const response = await apiClient.get<ApiResponse<any>>('/tenants/users', { params });
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invite user to tenant
|
||||||
|
*/
|
||||||
|
async inviteUser(invitation: InviteUser): Promise<{
|
||||||
|
invitation_id: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
expires_at: string;
|
||||||
|
invitation_token: string;
|
||||||
|
}> {
|
||||||
|
const response = await apiClient.post<ApiResponse<any>>('/tenants/users/invite', invitation);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user role
|
||||||
|
*/
|
||||||
|
async updateUserRole(userId: string, role: string): Promise<TenantUser> {
|
||||||
|
const response = await apiClient.patch<ApiResponse<TenantUser>>(
|
||||||
|
`/tenants/users/${userId}`,
|
||||||
|
{ role }
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivate user
|
||||||
|
*/
|
||||||
|
async deactivateUser(userId: string): Promise<TenantUser> {
|
||||||
|
const response = await apiClient.patch<ApiResponse<TenantUser>>(
|
||||||
|
`/tenants/users/${userId}`,
|
||||||
|
{ is_active: false }
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reactivate user
|
||||||
|
*/
|
||||||
|
async reactivateUser(userId: string): Promise<TenantUser> {
|
||||||
|
const response = await apiClient.patch<ApiResponse<TenantUser>>(
|
||||||
|
`/tenants/users/${userId}`,
|
||||||
|
{ is_active: true }
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove user from tenant
|
||||||
|
*/
|
||||||
|
async removeUser(userId: string): Promise<void> {
|
||||||
|
await apiClient.delete(`/tenants/users/${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pending invitations
|
||||||
|
*/
|
||||||
|
async getPendingInvitations(): Promise<{
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
invited_at: string;
|
||||||
|
expires_at: string;
|
||||||
|
invited_by: string;
|
||||||
|
}[]> {
|
||||||
|
const response = await apiClient.get<ApiResponse<any>>('/tenants/invitations');
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel invitation
|
||||||
|
*/
|
||||||
|
async cancelInvitation(invitationId: string): Promise<void> {
|
||||||
|
await apiClient.delete(`/tenants/invitations/${invitationId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resend invitation
|
||||||
|
*/
|
||||||
|
async resendInvitation(invitationId: string): Promise<void> {
|
||||||
|
await apiClient.post(`/tenants/invitations/${invitationId}/resend`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tenant activity log
|
||||||
|
*/
|
||||||
|
async getActivityLog(params?: {
|
||||||
|
userId?: string;
|
||||||
|
action?: string;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}): Promise<{
|
||||||
|
activities: {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
user_name: string;
|
||||||
|
action: string;
|
||||||
|
resource: string;
|
||||||
|
resource_id: string;
|
||||||
|
details?: Record<string, any>;
|
||||||
|
ip_address?: string;
|
||||||
|
user_agent?: string;
|
||||||
|
created_at: string;
|
||||||
|
}[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pages: number;
|
||||||
|
}> {
|
||||||
|
const response = await apiClient.get<ApiResponse<any>>('/tenants/activity', { params });
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tenant billing info
|
||||||
|
*/
|
||||||
|
async getBillingInfo(): Promise<{
|
||||||
|
subscription_plan: string;
|
||||||
|
billing_cycle: 'monthly' | 'yearly';
|
||||||
|
next_billing_date: string;
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
payment_method: {
|
||||||
|
type: string;
|
||||||
|
last_four: string;
|
||||||
|
expires: string;
|
||||||
|
};
|
||||||
|
usage: {
|
||||||
|
api_calls: number;
|
||||||
|
storage_mb: number;
|
||||||
|
users: number;
|
||||||
|
limits: {
|
||||||
|
api_calls_per_month: number;
|
||||||
|
storage_mb: number;
|
||||||
|
max_users: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}> {
|
||||||
|
const response = await apiClient.get<ApiResponse<any>>('/tenants/billing');
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update billing info
|
||||||
|
*/
|
||||||
|
async updateBillingInfo(billingData: {
|
||||||
|
payment_method_token?: string;
|
||||||
|
billing_address?: {
|
||||||
|
street: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
zip: string;
|
||||||
|
country: string;
|
||||||
|
};
|
||||||
|
}): Promise<void> {
|
||||||
|
await apiClient.put('/tenants/billing', billingData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change subscription plan
|
||||||
|
*/
|
||||||
|
async changeSubscriptionPlan(
|
||||||
|
planId: string,
|
||||||
|
billingCycle: 'monthly' | 'yearly'
|
||||||
|
): Promise<{
|
||||||
|
subscription_id: string;
|
||||||
|
plan: string;
|
||||||
|
billing_cycle: string;
|
||||||
|
next_billing_date: string;
|
||||||
|
proration_amount?: number;
|
||||||
|
}> {
|
||||||
|
const response = await apiClient.post<ApiResponse<any>>('/tenants/subscription/change', {
|
||||||
|
plan_id: planId,
|
||||||
|
billing_cycle: billingCycle,
|
||||||
|
});
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel subscription
|
||||||
|
*/
|
||||||
|
async cancelSubscription(cancelAt: 'immediately' | 'end_of_period'): Promise<{
|
||||||
|
cancelled_at: string;
|
||||||
|
will_cancel_at: string;
|
||||||
|
refund_amount?: number;
|
||||||
|
}> {
|
||||||
|
const response = await apiClient.post<ApiResponse<any>>('/tenants/subscription/cancel', {
|
||||||
|
cancel_at: cancelAt,
|
||||||
|
});
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export tenant data
|
||||||
|
*/
|
||||||
|
async exportTenantData(dataTypes: string[], format: 'json' | 'csv'): Promise<Blob> {
|
||||||
|
const response = await apiClient.post('/tenants/export', {
|
||||||
|
data_types: dataTypes,
|
||||||
|
format,
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
return response as unknown as Blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete tenant (GDPR compliance)
|
||||||
|
*/
|
||||||
|
async deleteTenant(confirmationToken: string): Promise<{
|
||||||
|
deletion_scheduled_at: string;
|
||||||
|
data_retention_until: string;
|
||||||
|
recovery_period_days: number;
|
||||||
|
}> {
|
||||||
|
const response = await apiClient.delete<ApiResponse<any>>('/tenants/current', {
|
||||||
|
data: { confirmation_token: confirmationToken },
|
||||||
|
});
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tenantService = new TenantService();
|
||||||
221
frontend/src/api/services/trainingService.ts
Normal file
221
frontend/src/api/services/trainingService.ts
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
// src/api/services/TrainingService.ts
|
||||||
|
import { apiClient } from '../base/apiClient';
|
||||||
|
import {
|
||||||
|
ApiResponse,
|
||||||
|
TrainingRequest,
|
||||||
|
TrainingJobStatus,
|
||||||
|
TrainedModel,
|
||||||
|
} from '../types/api';
|
||||||
|
|
||||||
|
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(config: TrainingConfiguration): Promise<TrainingJobStatus> {
|
||||||
|
const response = await apiClient.post<ApiResponse<TrainingJobStatus>>(
|
||||||
|
'/training/jobs',
|
||||||
|
config
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get training job status
|
||||||
|
*/
|
||||||
|
async getTrainingStatus(jobId: string): Promise<TrainingJobProgress> {
|
||||||
|
const response = await apiClient.get<ApiResponse<TrainingJobProgress>>(
|
||||||
|
`/training/jobs/${jobId}`
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all training jobs
|
||||||
|
*/
|
||||||
|
async getTrainingHistory(params?: {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
status?: string;
|
||||||
|
}): Promise<{
|
||||||
|
jobs: TrainingJobStatus[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pages: number;
|
||||||
|
}> {
|
||||||
|
const response = await apiClient.get<ApiResponse<any>>('/training/jobs', { params });
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel training job
|
||||||
|
*/
|
||||||
|
async cancelTraining(jobId: string): Promise<void> {
|
||||||
|
await apiClient.post(`/training/jobs/${jobId}/cancel`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get trained models
|
||||||
|
*/
|
||||||
|
async getModels(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>>('/training/models', { params });
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get specific model details
|
||||||
|
*/
|
||||||
|
async getModel(modelId: string): Promise<TrainedModel> {
|
||||||
|
const response = await apiClient.get<ApiResponse<TrainedModel>>(
|
||||||
|
`/training/models/${modelId}`
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get model metrics
|
||||||
|
*/
|
||||||
|
async getModelMetrics(modelId: string): Promise<ModelMetrics> {
|
||||||
|
const response = await apiClient.get<ApiResponse<ModelMetrics>>(
|
||||||
|
`/training/models/${modelId}/metrics`
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate/deactivate model
|
||||||
|
*/
|
||||||
|
async toggleModelStatus(modelId: string, active: boolean): Promise<TrainedModel> {
|
||||||
|
const response = await apiClient.patch<ApiResponse<TrainedModel>>(
|
||||||
|
`/training/models/${modelId}`,
|
||||||
|
{ is_active: active }
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete model
|
||||||
|
*/
|
||||||
|
async deleteModel(modelId: string): Promise<void> {
|
||||||
|
await apiClient.delete(`/training/models/${modelId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Train specific product
|
||||||
|
*/
|
||||||
|
async trainProduct(productName: string, config?: Partial<TrainingConfiguration>): Promise<TrainingJobStatus> {
|
||||||
|
const response = await apiClient.post<ApiResponse<TrainingJobStatus>>(
|
||||||
|
'/training/products/train',
|
||||||
|
{
|
||||||
|
product_name: productName,
|
||||||
|
...config,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get training statistics
|
||||||
|
*/
|
||||||
|
async getTrainingStats(): 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>>('/training/stats');
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate training data
|
||||||
|
*/
|
||||||
|
async validateTrainingData(products?: string[]): Promise<{
|
||||||
|
valid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
warnings: string[];
|
||||||
|
product_data_points: Record<string, number>;
|
||||||
|
recommendation: string;
|
||||||
|
}> {
|
||||||
|
const response = await apiClient.post<ApiResponse<any>>('/training/validate', {
|
||||||
|
products,
|
||||||
|
});
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get training recommendations
|
||||||
|
*/
|
||||||
|
async getTrainingRecommendations(): Promise<{
|
||||||
|
should_retrain: boolean;
|
||||||
|
reasons: string[];
|
||||||
|
recommended_products: string[];
|
||||||
|
optimal_config: TrainingConfiguration;
|
||||||
|
}> {
|
||||||
|
const response = await apiClient.get<ApiResponse<any>>('/training/recommendations');
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get training logs
|
||||||
|
*/
|
||||||
|
async getTrainingLogs(jobId: string): Promise<string[]> {
|
||||||
|
const response = await apiClient.get<ApiResponse<string[]>>(`/training/jobs/${jobId}/logs`);
|
||||||
|
return response.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export model
|
||||||
|
*/
|
||||||
|
async exportModel(modelId: string, format: 'pickle' | 'onnx' = 'pickle'): Promise<Blob> {
|
||||||
|
const response = await apiClient.get(`/training/models/${modelId}/export`, {
|
||||||
|
params: { format },
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
return response as unknown as Blob;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const trainingService = new TrainingService();
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
// frontend/src/contexts/AuthContext.tsx - UPDATED TO USE NEW REGISTRATION FLOW
|
// frontend/src/contexts/AuthContext.tsx - UPDATED TO USE NEW REGISTRATION FLOW
|
||||||
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
||||||
import { authService, UserProfile, RegisterData } from '../api/services/authService';
|
import { authService } from '../api/services/authService';
|
||||||
import { tokenManager } from '../api/auth/tokenManager';
|
import { tokenManager } from '../api/auth/tokenManager';
|
||||||
|
|
||||||
|
import {
|
||||||
|
UserProfile
|
||||||
|
} from '@/api/services';
|
||||||
|
|
||||||
|
import api from '@/api/services';
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: UserProfile | null;
|
user: UserProfile | null;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
@@ -34,7 +40,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
try {
|
try {
|
||||||
await tokenManager.initialize();
|
await tokenManager.initialize();
|
||||||
|
|
||||||
if (authService.isAuthenticated()) {
|
if (api.auth.isAuthenticated()) {
|
||||||
// Get user from token first (faster), then validate with API
|
// Get user from token first (faster), then validate with API
|
||||||
const tokenUser = tokenManager.getUserFromToken();
|
const tokenUser = tokenManager.getUserFromToken();
|
||||||
if (tokenUser) {
|
if (tokenUser) {
|
||||||
@@ -50,7 +56,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
|
|
||||||
// Validate with API and get complete profile
|
// Validate with API and get complete profile
|
||||||
try {
|
try {
|
||||||
const profile = await authService.getCurrentUser();
|
const profile = await api.auth.getCurrentUser();
|
||||||
setUser(profile);
|
setUser(profile);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch user profile:', error);
|
console.error('Failed to fetch user profile:', error);
|
||||||
@@ -124,10 +130,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const refreshUser = useCallback(async () => {
|
const refreshUser = useCallback(async () => {
|
||||||
if (!authService.isAuthenticated()) return;
|
if (!api.auth.isAuthenticated()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const profile = await authService.getCurrentUser();
|
const profile = await api.auth.getCurrentUser();
|
||||||
setUser(profile);
|
setUser(profile);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('User refresh error:', error);
|
console.error('User refresh error:', error);
|
||||||
@@ -160,7 +166,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
const checkTokenValidity = () => {
|
const checkTokenValidity = () => {
|
||||||
if (!authService.isAuthenticated()) {
|
if (!api.auth.isAuthenticated()) {
|
||||||
console.warn('Token became invalid, logging out user');
|
console.warn('Token became invalid, logging out user');
|
||||||
logout();
|
logout();
|
||||||
}
|
}
|
||||||
@@ -173,7 +179,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
|
|
||||||
const contextValue = {
|
const contextValue = {
|
||||||
user,
|
user,
|
||||||
isAuthenticated: !!user && authService.isAuthenticated(),
|
isAuthenticated: !!user && api.auth.isAuthenticated(),
|
||||||
isLoading,
|
isLoading,
|
||||||
login,
|
login,
|
||||||
register,
|
register,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ArrowPathIcon,
|
ArrowPathIcon,
|
||||||
ScaleIcon, // For accuracy
|
ScaleIcon, // For accuracy
|
||||||
CalendarDaysIcon, // For last training date
|
CalendarDaysIcon, // For last training date
|
||||||
|
CurrencyEuroIcon
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { useTrainingProgress } from '../../api/hooks/useTrainingProgress'; // Path corrected
|
import { useTrainingProgress } from '../../api/hooks/useTrainingProgress'; // Path corrected
|
||||||
@@ -17,14 +18,17 @@ import { ForecastChart } from '../../components/charts/ForecastChart';
|
|||||||
import { SalesUploader } from '../../components/data/SalesUploader';
|
import { SalesUploader } from '../../components/data/SalesUploader';
|
||||||
import { NotificationToast } from '../../components/common/NotificationToast';
|
import { NotificationToast } from '../../components/common/NotificationToast';
|
||||||
import { ErrorBoundary } from '../../components/common/ErrorBoundary';
|
import { ErrorBoundary } from '../../components/common/ErrorBoundary';
|
||||||
|
import { defaultProducts } from '../../components/common/ProductSelector';
|
||||||
import {
|
import {
|
||||||
dataApi,
|
|
||||||
forecastingApi,
|
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
ForecastRecord,
|
ForecastRecord,
|
||||||
SalesRecord,
|
|
||||||
TrainingRequest,
|
TrainingRequest,
|
||||||
} from '../../api/services/api'; // Consolidated API services and types
|
TrainingJobProgress
|
||||||
|
} from '@/api/services';
|
||||||
|
|
||||||
|
|
||||||
|
import api from '@/api/services';
|
||||||
|
|
||||||
|
|
||||||
// Dashboard specific types
|
// Dashboard specific types
|
||||||
interface DashboardStats {
|
interface DashboardStats {
|
||||||
@@ -141,7 +145,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
setLoadingData(true);
|
setLoadingData(true);
|
||||||
try {
|
try {
|
||||||
// Fetch Dashboard Stats
|
// Fetch Dashboard Stats
|
||||||
const statsResponse: ApiResponse<DashboardStats> = await dataApi.getDashboardStats();
|
const statsResponse: ApiResponse<DashboardStats> = await api.data.dataApi.getDashboardStats();
|
||||||
if (statsResponse.data) {
|
if (statsResponse.data) {
|
||||||
setStats(statsResponse.data);
|
setStats(statsResponse.data);
|
||||||
} else if (statsResponse.message) {
|
} else if (statsResponse.message) {
|
||||||
@@ -149,7 +153,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch initial forecasts (e.g., for a default product or the first available product)
|
// Fetch initial forecasts (e.g., for a default product or the first available product)
|
||||||
const forecastResponse: ApiResponse<ForecastRecord[]> = await forecastingApi.getForecast({
|
const forecastResponse: ApiResponse<ForecastRecord[]> = await api.forecasting.getForecast({
|
||||||
forecast_days: 7, // Example: 7 days forecast
|
forecast_days: 7, // Example: 7 days forecast
|
||||||
product_name: user?.tenant_id ? 'pan' : undefined, // Default to 'pan' or first product
|
product_name: user?.tenant_id ? 'pan' : undefined, // Default to 'pan' or first product
|
||||||
});
|
});
|
||||||
@@ -177,7 +181,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
const handleSalesUpload = async (file: File) => {
|
const handleSalesUpload = async (file: File) => {
|
||||||
try {
|
try {
|
||||||
addNotification('info', 'Subiendo archivo', 'Cargando historial de ventas...');
|
addNotification('info', 'Subiendo archivo', 'Cargando historial de ventas...');
|
||||||
const response = await dataApi.uploadSalesHistory(file);
|
const response = await api.data.dataApi.uploadSalesHistory(file);
|
||||||
addNotification('success', 'Subida Completa', 'Historial de ventas cargado exitosamente.');
|
addNotification('success', 'Subida Completa', 'Historial de ventas cargado exitosamente.');
|
||||||
|
|
||||||
// After upload, trigger a new training (assuming this is the flow)
|
// After upload, trigger a new training (assuming this is the flow)
|
||||||
@@ -186,9 +190,9 @@ const DashboardPage: React.FC = () => {
|
|||||||
// You might want to specify products if the uploader supports it,
|
// You might want to specify products if the uploader supports it,
|
||||||
// or let the backend determine based on the uploaded data.
|
// or let the backend determine based on the uploaded data.
|
||||||
};
|
};
|
||||||
const trainingTask: TrainingTask = await trainingApi.startTraining(trainingRequest);
|
const trainingTask: TrainingJobProgress = await api.training.trainingApi.startTraining(trainingRequest);
|
||||||
setActiveJobId(trainingTask.job_id);
|
setActiveJobId(trainingTask.id);
|
||||||
addNotification('info', 'Entrenamiento iniciado', `Un nuevo entrenamiento ha comenzado (ID: ${trainingTask.job_id}).`);
|
addNotification('info', 'Entrenamiento iniciado', `Un nuevo entrenamiento ha comenzado (ID: ${trainingTask.id}).`);
|
||||||
// No need to fetch dashboard data here, as useEffect for isTrainingComplete will handle it
|
// No need to fetch dashboard data here, as useEffect for isTrainingComplete will handle it
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error uploading sales or starting training:', error);
|
console.error('Error uploading sales or starting training:', error);
|
||||||
@@ -199,7 +203,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
const handleForecastProductChange = async (productName: string) => {
|
const handleForecastProductChange = async (productName: string) => {
|
||||||
setLoadingData(true);
|
setLoadingData(true);
|
||||||
try {
|
try {
|
||||||
const forecastResponse: ApiResponse<ForecastRecord[]> = await forecastingApi.getForecast({
|
const forecastResponse: ApiResponse<ForecastRecord[]> = await api.forecasting.forecastingApi.getForecast({
|
||||||
forecast_days: 7,
|
forecast_days: 7,
|
||||||
product_name: productName,
|
product_name: productName,
|
||||||
});
|
});
|
||||||
@@ -280,7 +284,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
<StatsCard
|
<StatsCard
|
||||||
title="Ingresos Totales"
|
title="Ingresos Totales"
|
||||||
value={stats?.totalRevenue}
|
value={stats?.totalRevenue}
|
||||||
icon={CurrencyEuroIcon} {/* Assuming CurrencyEuroIcon from heroicons */}
|
icon={CurrencyEuroIcon}
|
||||||
format="currency"
|
format="currency"
|
||||||
loading={loadingData}
|
loading={loadingData}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// frontend/src/pages/onboarding.tsx - ORIGINAL DESIGN WITH AUTH FIXES ONLY
|
// frontend/src/pages/onboarding.tsx - ORIGINAL DESIGN WITH AUTH FIXES ONLY
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import {
|
import {
|
||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
import { SalesUploader } from '../components/data/SalesUploader';
|
import { SalesUploader } from '../components/data/SalesUploader';
|
||||||
import { TrainingProgressCard } from '../components/training/TrainingProgressCard';
|
import { TrainingProgressCard } from '../components/training/TrainingProgressCard';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { RegisterData } from '../api/services/authService';
|
|
||||||
import { dataApi, TrainingRequest, TrainingTask } from '../api/services/api';
|
import { dataApi, TrainingRequest, TrainingTask } from '../api/services/api';
|
||||||
import { NotificationToast } from '../components/common/NotificationToast';
|
import { NotificationToast } from '../components/common/NotificationToast';
|
||||||
import { Product, defaultProducts } from '../components/common/ProductSelector';
|
import { Product, defaultProducts } from '../components/common/ProductSelector';
|
||||||
@@ -76,6 +75,76 @@ const OnboardingPage: React.FC = () => {
|
|||||||
|
|
||||||
const [errors, setErrors] = useState<Partial<OnboardingFormData>>({});
|
const [errors, setErrors] = useState<Partial<OnboardingFormData>>({});
|
||||||
|
|
||||||
|
const addressInputRef = useRef<HTMLInputElement>(null); // Ref for the address input
|
||||||
|
let autocompleteTimeout: NodeJS.Timeout | null = null; // For debouncing API calls
|
||||||
|
|
||||||
|
const handleAddressInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const query = e.target.value;
|
||||||
|
setFormData(prevData => ({ ...prevData, address: query })); // Update address immediately
|
||||||
|
|
||||||
|
if (autocompleteTimeout) {
|
||||||
|
clearTimeout(autocompleteTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.length < 3) { // Only search if at least 3 characters are typed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
autocompleteTimeout = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
// Construct the Nominatim API URL
|
||||||
|
// Make sure NOMINATIM_PORT matches your .env file, default is 8080
|
||||||
|
const gatewayNominatimApiUrl = `/api/v1/nominatim/search`; // Relative path if frontend serves from gateway's domain/port
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
q: query,
|
||||||
|
format: 'json',
|
||||||
|
addressdetails: '1', // Request detailed address components
|
||||||
|
limit: '5', // Number of results to return
|
||||||
|
'accept-language': 'es', // Request results in Spanish
|
||||||
|
countrycodes: 'es' // Restrict search to Spain
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`${gatewayNominatimApiUrl}?${params.toString()}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Process Nominatim results and update form data
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
// Take the first result or let the user choose from suggestions if you implement a dropdown
|
||||||
|
const place = data[0]; // For simplicity, take the first result
|
||||||
|
|
||||||
|
let address = '';
|
||||||
|
let city = '';
|
||||||
|
let postal_code = '';
|
||||||
|
|
||||||
|
// Nominatim's 'address' object contains components
|
||||||
|
if (place.address) {
|
||||||
|
const addr = place.address;
|
||||||
|
|
||||||
|
// Reconstruct the address in a common format
|
||||||
|
const street = addr.road || '';
|
||||||
|
const houseNumber = addr.house_number || '';
|
||||||
|
address = `${street} ${houseNumber}`.trim();
|
||||||
|
|
||||||
|
city = addr.city || addr.town || addr.village || '';
|
||||||
|
postal_code = addr.postcode || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormData(prevData => ({
|
||||||
|
...prevData,
|
||||||
|
address: address || query, // Use parsed address or fall back to user input
|
||||||
|
city: city || prevData.city,
|
||||||
|
postal_code: postal_code || prevData.postal_code,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching Nominatim suggestions:', error);
|
||||||
|
// Optionally show an error notification
|
||||||
|
// showNotification('error', 'Error de Autocompletado', 'No se pudieron cargar las sugerencias de dirección.');
|
||||||
|
}
|
||||||
|
}, 500); // Debounce time: 500ms
|
||||||
|
}, []); // Re-create if dependencies change, none for now
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If user is already authenticated and on onboarding, redirect to dashboard
|
// If user is already authenticated and on onboarding, redirect to dashboard
|
||||||
if (user && currentStep === 1) {
|
if (user && currentStep === 1) {
|
||||||
@@ -106,7 +175,7 @@ const OnboardingPage: React.FC = () => {
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const registerData: RegisterData = {
|
const registerData: dataApi.auth.RegisterData = {
|
||||||
full_name: formData.full_name,
|
full_name: formData.full_name,
|
||||||
email: formData.email,
|
email: formData.email,
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
@@ -287,9 +356,11 @@ const OnboardingPage: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="address"
|
id="address"
|
||||||
|
ref={addressInputRef}
|
||||||
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:ring-pania-blue focus:border-pania-blue sm:text-sm"
|
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:ring-pania-blue focus:border-pania-blue sm:text-sm"
|
||||||
value={formData.address}
|
value={formData.address}
|
||||||
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
|
// Use the new handler for changes to trigger autocomplete
|
||||||
|
onChange={handleAddressInputChange}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
{errors.address && <p className="mt-1 text-sm text-red-600">{errors.address}</p>}
|
{errors.address && <p className="mt-1 text-sm text-red-600">{errors.address}</p>}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from app.core.service_discovery import ServiceDiscovery
|
|||||||
from app.middleware.auth import AuthMiddleware
|
from app.middleware.auth import AuthMiddleware
|
||||||
from app.middleware.logging import LoggingMiddleware
|
from app.middleware.logging import LoggingMiddleware
|
||||||
from app.middleware.rate_limit import RateLimitMiddleware
|
from app.middleware.rate_limit import RateLimitMiddleware
|
||||||
from app.routes import auth, training, forecasting, data, tenant, notification
|
from app.routes import auth, training, forecasting, data, tenant, notification, nominatim
|
||||||
from shared.monitoring.logging import setup_logging
|
from shared.monitoring.logging import setup_logging
|
||||||
from shared.monitoring.metrics import MetricsCollector
|
from shared.monitoring.metrics import MetricsCollector
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ app.include_router(forecasting.router, prefix="/api/v1/forecasting", tags=["fore
|
|||||||
app.include_router(data.router, prefix="/api/v1/data", tags=["data"])
|
app.include_router(data.router, prefix="/api/v1/data", tags=["data"])
|
||||||
app.include_router(tenant.router, prefix="/api/v1/tenants", tags=["tenants"])
|
app.include_router(tenant.router, prefix="/api/v1/tenants", tags=["tenants"])
|
||||||
app.include_router(notification.router, prefix="/api/v1/notifications", tags=["notifications"])
|
app.include_router(notification.router, prefix="/api/v1/notifications", tags=["notifications"])
|
||||||
|
app.include_router(nominatim.router, prefix="/api/v1/nominatim", tags=["location"])
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def startup_event():
|
async def startup_event():
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ PUBLIC_ROUTES = [
|
|||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
"/api/v1/auth/register",
|
"/api/v1/auth/register",
|
||||||
"/api/v1/auth/refresh",
|
"/api/v1/auth/refresh",
|
||||||
"/api/v1/auth/verify"
|
"/api/v1/auth/verify",
|
||||||
|
"/api/v1/nominatim/search"
|
||||||
]
|
]
|
||||||
|
|
||||||
class AuthMiddleware(BaseHTTPMiddleware):
|
class AuthMiddleware(BaseHTTPMiddleware):
|
||||||
|
|||||||
48
gateway/app/routes/nominatim.py
Normal file
48
gateway/app/routes/nominatim.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# gateway/app/routes/nominatim.py
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Request, HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
import httpx
|
||||||
|
import structlog
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
logger = structlog.get_logger()
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/search")
|
||||||
|
async def proxy_nominatim_search(request: Request):
|
||||||
|
"""
|
||||||
|
Proxies requests to the Nominatim geocoding search API.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Construct the internal Nominatim URL
|
||||||
|
# All query parameters from the client request are forwarded
|
||||||
|
nominatim_url = f"{settings.NOMINATIM_SERVICE_URL}/nominatim/search"
|
||||||
|
|
||||||
|
# httpx client for making async HTTP requests
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.get(
|
||||||
|
nominatim_url,
|
||||||
|
params=request.query_params # Forward all query parameters from frontend
|
||||||
|
)
|
||||||
|
response.raise_for_status() # Raise an exception for HTTP errors (4xx or 5xx)
|
||||||
|
|
||||||
|
# Return the JSON response from Nominatim directly
|
||||||
|
return JSONResponse(content=response.json())
|
||||||
|
|
||||||
|
except httpx.RequestError as exc:
|
||||||
|
logger.error("Nominatim service request failed", error=str(exc))
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail=f"Nominatim service unavailable: {exc}"
|
||||||
|
)
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
logger.error(f"Nominatim service responded with error {exc.response.status_code}",
|
||||||
|
detail=exc.response.text)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=exc.response.status_code,
|
||||||
|
detail=f"Nominatim service error: {exc.response.text}"
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Unexpected error in Nominatim proxy", error=str(exc))
|
||||||
|
raise HTTPException(status_code=500, detail="Internal server error in Nominatim proxy")
|
||||||
@@ -120,6 +120,7 @@ class BaseServiceSettings(BaseSettings):
|
|||||||
DATA_SERVICE_URL: str = os.getenv("DATA_SERVICE_URL", "http://data-service:8000")
|
DATA_SERVICE_URL: str = os.getenv("DATA_SERVICE_URL", "http://data-service:8000")
|
||||||
TENANT_SERVICE_URL: str = os.getenv("TENANT_SERVICE_URL", "http://tenant-service:8000")
|
TENANT_SERVICE_URL: str = os.getenv("TENANT_SERVICE_URL", "http://tenant-service:8000")
|
||||||
NOTIFICATION_SERVICE_URL: str = os.getenv("NOTIFICATION_SERVICE_URL", "http://notification-service:8000")
|
NOTIFICATION_SERVICE_URL: str = os.getenv("NOTIFICATION_SERVICE_URL", "http://notification-service:8000")
|
||||||
|
NOMINATIM_SERVICE_URL: str = os.getenv("NOMINATIM_SERVICE_URL", "http://nominatim:8080")
|
||||||
|
|
||||||
# HTTP Client Settings
|
# HTTP Client Settings
|
||||||
HTTP_TIMEOUT: int = int(os.getenv("HTTP_TIMEOUT", "30"))
|
HTTP_TIMEOUT: int = int(os.getenv("HTTP_TIMEOUT", "30"))
|
||||||
|
|||||||
Reference in New Issue
Block a user