Add new frontend - fix 9
This commit is contained in:
@@ -27,6 +27,9 @@ volumes:
|
||||
grafana_data:
|
||||
model_storage:
|
||||
log_storage:
|
||||
nominatim_db_data:
|
||||
nominatim_data:
|
||||
|
||||
|
||||
# ================================================================
|
||||
# SERVICES - USING ONLY .env FILE
|
||||
@@ -215,6 +218,72 @@ services:
|
||||
timeout: 5s
|
||||
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
|
||||
# ================================================================
|
||||
|
||||
@@ -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
|
||||
import { tokenManager } from '../auth/tokenManager';
|
||||
// src/api/services/AuthService.ts
|
||||
import { apiClient } from '../base/apiClient';
|
||||
import {
|
||||
ApiResponse,
|
||||
LoginRequest,
|
||||
RegisterRequest,
|
||||
TokenResponse,
|
||||
UserProfile,
|
||||
} from '../types/api';
|
||||
|
||||
export interface LoginCredentials {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterData {
|
||||
email: string;
|
||||
password: string;
|
||||
full_name: string;
|
||||
tenant_name?: string;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name: string;
|
||||
tenant_id?: string;
|
||||
role?: string;
|
||||
is_active: boolean;
|
||||
is_verified?: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface TokenResponse {
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
token_type: string;
|
||||
expires_in?: number;
|
||||
user?: UserProfile;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
export class AuthService {
|
||||
/**
|
||||
* User login
|
||||
*/
|
||||
async login(credentials: LoginRequest): Promise<TokenResponse> {
|
||||
const response = await apiClient.post<ApiResponse<TokenResponse>>(
|
||||
'/auth/login',
|
||||
credentials
|
||||
);
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
/**
|
||||
* User registration
|
||||
*/
|
||||
async register(userData: RegisterRequest): Promise<UserProfile> {
|
||||
const response = await apiClient.post<ApiResponse<UserProfile>>(
|
||||
'/auth/register',
|
||||
userData
|
||||
);
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
/**
|
||||
* Refresh access token
|
||||
*/
|
||||
async refreshToken(refreshToken: string): Promise<TokenResponse> {
|
||||
const response = await apiClient.post<ApiResponse<TokenResponse>>(
|
||||
'/auth/refresh',
|
||||
{ refresh_token: refreshToken }
|
||||
);
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
async getCurrentUser(): Promise<UserProfile> {
|
||||
return apiClient.get('/api/v1/auth/me');
|
||||
/**
|
||||
* Get current user profile
|
||||
*/
|
||||
async getProfile(): Promise<UserProfile> {
|
||||
const response = await apiClient.get<ApiResponse<UserProfile>>('/users/me');
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user profile
|
||||
*/
|
||||
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,
|
||||
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
|
||||
// For now, it returns null and components should use getCurrentUser()
|
||||
return null;
|
||||
/**
|
||||
* Verify email
|
||||
*/
|
||||
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
|
||||
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 {
|
||||
UserProfile
|
||||
} from '@/api/services';
|
||||
|
||||
import api from '@/api/services';
|
||||
|
||||
interface AuthContextType {
|
||||
user: UserProfile | null;
|
||||
isAuthenticated: boolean;
|
||||
@@ -34,7 +40,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
try {
|
||||
await tokenManager.initialize();
|
||||
|
||||
if (authService.isAuthenticated()) {
|
||||
if (api.auth.isAuthenticated()) {
|
||||
// Get user from token first (faster), then validate with API
|
||||
const tokenUser = tokenManager.getUserFromToken();
|
||||
if (tokenUser) {
|
||||
@@ -50,7 +56,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
// Validate with API and get complete profile
|
||||
try {
|
||||
const profile = await authService.getCurrentUser();
|
||||
const profile = await api.auth.getCurrentUser();
|
||||
setUser(profile);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user profile:', error);
|
||||
@@ -124,10 +130,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
}, [user]);
|
||||
|
||||
const refreshUser = useCallback(async () => {
|
||||
if (!authService.isAuthenticated()) return;
|
||||
if (!api.auth.isAuthenticated()) return;
|
||||
|
||||
try {
|
||||
const profile = await authService.getCurrentUser();
|
||||
const profile = await api.auth.getCurrentUser();
|
||||
setUser(profile);
|
||||
} catch (error) {
|
||||
console.error('User refresh error:', error);
|
||||
@@ -160,7 +166,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
if (!user) return;
|
||||
|
||||
const checkTokenValidity = () => {
|
||||
if (!authService.isAuthenticated()) {
|
||||
if (!api.auth.isAuthenticated()) {
|
||||
console.warn('Token became invalid, logging out user');
|
||||
logout();
|
||||
}
|
||||
@@ -173,7 +179,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
const contextValue = {
|
||||
user,
|
||||
isAuthenticated: !!user && authService.isAuthenticated(),
|
||||
isAuthenticated: !!user && api.auth.isAuthenticated(),
|
||||
isLoading,
|
||||
login,
|
||||
register,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ArrowPathIcon,
|
||||
ScaleIcon, // For accuracy
|
||||
CalendarDaysIcon, // For last training date
|
||||
CurrencyEuroIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
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 { NotificationToast } from '../../components/common/NotificationToast';
|
||||
import { ErrorBoundary } from '../../components/common/ErrorBoundary';
|
||||
import { defaultProducts } from '../../components/common/ProductSelector';
|
||||
import {
|
||||
dataApi,
|
||||
forecastingApi,
|
||||
ApiResponse,
|
||||
ForecastRecord,
|
||||
SalesRecord,
|
||||
TrainingRequest,
|
||||
} from '../../api/services/api'; // Consolidated API services and types
|
||||
TrainingJobProgress
|
||||
} from '@/api/services';
|
||||
|
||||
|
||||
import api from '@/api/services';
|
||||
|
||||
|
||||
// Dashboard specific types
|
||||
interface DashboardStats {
|
||||
@@ -141,7 +145,7 @@ const DashboardPage: React.FC = () => {
|
||||
setLoadingData(true);
|
||||
try {
|
||||
// Fetch Dashboard Stats
|
||||
const statsResponse: ApiResponse<DashboardStats> = await dataApi.getDashboardStats();
|
||||
const statsResponse: ApiResponse<DashboardStats> = await api.data.dataApi.getDashboardStats();
|
||||
if (statsResponse.data) {
|
||||
setStats(statsResponse.data);
|
||||
} 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)
|
||||
const forecastResponse: ApiResponse<ForecastRecord[]> = await forecastingApi.getForecast({
|
||||
const forecastResponse: ApiResponse<ForecastRecord[]> = await api.forecasting.getForecast({
|
||||
forecast_days: 7, // Example: 7 days forecast
|
||||
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) => {
|
||||
try {
|
||||
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.');
|
||||
|
||||
// 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,
|
||||
// or let the backend determine based on the uploaded data.
|
||||
};
|
||||
const trainingTask: TrainingTask = await trainingApi.startTraining(trainingRequest);
|
||||
setActiveJobId(trainingTask.job_id);
|
||||
addNotification('info', 'Entrenamiento iniciado', `Un nuevo entrenamiento ha comenzado (ID: ${trainingTask.job_id}).`);
|
||||
const trainingTask: TrainingJobProgress = await api.training.trainingApi.startTraining(trainingRequest);
|
||||
setActiveJobId(trainingTask.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
|
||||
} catch (error: any) {
|
||||
console.error('Error uploading sales or starting training:', error);
|
||||
@@ -199,7 +203,7 @@ const DashboardPage: React.FC = () => {
|
||||
const handleForecastProductChange = async (productName: string) => {
|
||||
setLoadingData(true);
|
||||
try {
|
||||
const forecastResponse: ApiResponse<ForecastRecord[]> = await forecastingApi.getForecast({
|
||||
const forecastResponse: ApiResponse<ForecastRecord[]> = await api.forecasting.forecastingApi.getForecast({
|
||||
forecast_days: 7,
|
||||
product_name: productName,
|
||||
});
|
||||
@@ -280,7 +284,7 @@ const DashboardPage: React.FC = () => {
|
||||
<StatsCard
|
||||
title="Ingresos Totales"
|
||||
value={stats?.totalRevenue}
|
||||
icon={CurrencyEuroIcon} {/* Assuming CurrencyEuroIcon from heroicons */}
|
||||
icon={CurrencyEuroIcon}
|
||||
format="currency"
|
||||
loading={loadingData}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// 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 Head from 'next/head';
|
||||
import {
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
import { SalesUploader } from '../components/data/SalesUploader';
|
||||
import { TrainingProgressCard } from '../components/training/TrainingProgressCard';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { RegisterData } from '../api/services/authService';
|
||||
import { dataApi, TrainingRequest, TrainingTask } from '../api/services/api';
|
||||
import { NotificationToast } from '../components/common/NotificationToast';
|
||||
import { Product, defaultProducts } from '../components/common/ProductSelector';
|
||||
@@ -76,6 +75,76 @@ const OnboardingPage: React.FC = () => {
|
||||
|
||||
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(() => {
|
||||
// If user is already authenticated and on onboarding, redirect to dashboard
|
||||
if (user && currentStep === 1) {
|
||||
@@ -106,7 +175,7 @@ const OnboardingPage: React.FC = () => {
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const registerData: RegisterData = {
|
||||
const registerData: dataApi.auth.RegisterData = {
|
||||
full_name: formData.full_name,
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
@@ -287,9 +356,11 @@ const OnboardingPage: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
value={formData.address}
|
||||
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
|
||||
// Use the new handler for changes to trigger autocomplete
|
||||
onChange={handleAddressInputChange}
|
||||
required
|
||||
/>
|
||||
{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.logging import LoggingMiddleware
|
||||
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.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(tenant.router, prefix="/api/v1/tenants", tags=["tenants"])
|
||||
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")
|
||||
async def startup_event():
|
||||
|
||||
@@ -31,7 +31,8 @@ PUBLIC_ROUTES = [
|
||||
"/api/v1/auth/login",
|
||||
"/api/v1/auth/register",
|
||||
"/api/v1/auth/refresh",
|
||||
"/api/v1/auth/verify"
|
||||
"/api/v1/auth/verify",
|
||||
"/api/v1/nominatim/search"
|
||||
]
|
||||
|
||||
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")
|
||||
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")
|
||||
NOMINATIM_SERVICE_URL: str = os.getenv("NOMINATIM_SERVICE_URL", "http://nominatim:8080")
|
||||
|
||||
# HTTP Client Settings
|
||||
HTTP_TIMEOUT: int = int(os.getenv("HTTP_TIMEOUT", "30"))
|
||||
|
||||
Reference in New Issue
Block a user