ADD new frontend

This commit is contained in:
Urtzi Alfaro
2025-08-28 10:41:04 +02:00
parent 9c247a5f99
commit 0fd273cfce
492 changed files with 114979 additions and 1632 deletions

View File

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

View File

@@ -1,296 +0,0 @@
// frontend/src/api/services/external.service.ts
/**
* External Data Service
* Handles weather and traffic data operations for the external microservice
*/
import { apiClient } from '../client';
import { RequestTimeouts } from '../client/config';
// Align with backend WeatherDataResponse schema
export interface WeatherData {
date: string;
temperature?: number;
precipitation?: number;
humidity?: number;
wind_speed?: number;
pressure?: number;
description?: string;
source: string;
}
// Align with backend TrafficDataResponse schema
export interface TrafficData {
date: string;
traffic_volume?: number;
pedestrian_count?: number;
congestion_level?: string;
average_speed?: number;
source: string;
}
export interface WeatherForecast {
date: string;
temperature_min: number;
temperature_max: number;
temperature_avg: number;
precipitation: number;
description: string;
humidity?: number;
wind_speed?: number;
}
export interface HourlyForecast {
forecast_datetime: string;
generated_at: string;
temperature: number;
precipitation: number;
humidity: number;
wind_speed: number;
description: string;
source: string;
hour: number;
}
export class ExternalService {
/**
* Get Current Weather Data
*/
async getCurrentWeather(
tenantId: string,
lat: number,
lon: number
): Promise<WeatherData> {
try {
// ✅ FIX 1: Correct endpoint path with tenant ID
const endpoint = `/tenants/${tenantId}/weather/current`;
// ✅ FIX 2: Correct parameter names (latitude/longitude, not lat/lon)
const response = await apiClient.get(endpoint, {
params: {
latitude: lat, // Backend expects 'latitude'
longitude: lon // Backend expects 'longitude'
}
});
console.log('Weather API response:', response);
// Return backend response directly (matches WeatherData interface)
return response;
} catch (error) {
console.error('Failed to fetch weather from AEMET API via backend:', error);
throw new Error(`Weather data unavailable: ${error instanceof Error ? error.message : 'AEMET API connection failed'}`);
}
}
/**
* Get Weather Forecast
*/
async getWeatherForecast(
tenantId: string,
lat: number,
lon: number,
days: number = 7
): Promise<WeatherForecast[]> {
try {
// Fix: Use POST with JSON body as expected by backend
const response = await apiClient.post(`/tenants/${tenantId}/weather/forecast`, {
latitude: lat,
longitude: lon,
days: days
});
// Handle response format
if (Array.isArray(response)) {
return response;
} else if (response && response.forecasts) {
return response.forecasts;
} else {
console.warn('Unexpected weather forecast response format:', response);
return [];
}
} catch (error) {
console.error('Failed to fetch weather forecast from AEMET API:', error);
throw new Error(`Weather forecast unavailable: ${error instanceof Error ? error.message : 'AEMET API connection failed'}`);
}
}
/**
* Get Hourly Weather Forecast (NEW)
*/
async getHourlyWeatherForecast(
tenantId: string,
lat: number,
lon: number,
hours: number = 48
): Promise<HourlyForecast[]> {
try {
console.log(`🕒 Fetching hourly weather forecast from AEMET API for tenant ${tenantId}`, {
latitude: lat,
longitude: lon,
hours: hours
});
const response = await apiClient.post(`/tenants/${tenantId}/weather/hourly-forecast`, {
latitude: lat,
longitude: lon,
hours: hours
});
// Handle response format
if (Array.isArray(response)) {
return response;
} else if (response && response.data) {
return response.data;
} else {
console.warn('Unexpected hourly forecast response format:', response);
return [];
}
} catch (error) {
console.error('Failed to fetch hourly forecast from AEMET API:', error);
throw new Error(`Hourly forecast unavailable: ${error instanceof Error ? error.message : 'AEMET API connection failed'}`);
}
}
/**
* Get Historical Weather Data
*/
async getHistoricalWeather(
tenantId: string,
lat: number,
lon: number,
startDate: string,
endDate: string
): Promise<WeatherData[]> {
try {
// Fix: Use POST with JSON body as expected by backend
const response = await apiClient.post(`/tenants/${tenantId}/weather/historical`, {
latitude: lat,
longitude: lon,
start_date: startDate,
end_date: endDate
});
// Return backend response directly (matches WeatherData interface)
return Array.isArray(response) ? response : response.data || [];
} catch (error) {
console.error('Failed to fetch historical weather from AEMET API:', error);
throw new Error(`Historical weather data unavailable: ${error instanceof Error ? error.message : 'AEMET API connection failed'}`);
}
}
/**
* Get Current Traffic Data
*/
async getCurrentTraffic(
tenantId: string,
lat: number,
lon: number
): Promise<TrafficData> {
try {
const response = await apiClient.get(`/tenants/${tenantId}/traffic/current`, {
params: {
latitude: lat,
longitude: lon
}
});
// Return backend response directly (matches TrafficData interface)
return response;
} catch (error) {
console.error('Failed to fetch traffic data from external API:', error);
throw new Error(`Traffic data unavailable: ${error instanceof Error ? error.message : 'External API connection failed'}`);
}
}
/**
* Get Traffic Forecast
*/
async getTrafficForecast(
tenantId: string,
lat: number,
lon: number,
hours: number = 24
): Promise<TrafficData[]> {
try {
// Fix: Use POST with JSON body as expected by backend
const response = await apiClient.post(`/tenants/${tenantId}/traffic/forecast`, {
latitude: lat,
longitude: lon,
hours: hours
});
// Return backend response directly (matches TrafficData interface)
return Array.isArray(response) ? response : response.data || [];
} catch (error) {
console.error('Failed to fetch traffic forecast from external API:', error);
throw new Error(`Traffic forecast unavailable: ${error instanceof Error ? error.message : 'External API connection failed'}`);
}
}
/**
* Get Historical Traffic Data
*/
async getHistoricalTraffic(
tenantId: string,
lat: number,
lon: number,
startDate: string,
endDate: string
): Promise<TrafficData[]> {
try {
// Fix: Use POST with JSON body as expected by backend
const response = await apiClient.post(`/tenants/${tenantId}/traffic/historical`, {
latitude: lat,
longitude: lon,
start_date: startDate,
end_date: endDate
});
// Return backend response directly (matches TrafficData interface)
return Array.isArray(response) ? response : response.data || [];
} catch (error) {
console.error('Failed to fetch historical traffic from external API:', error);
throw new Error(`Historical traffic data unavailable: ${error instanceof Error ? error.message : 'External API connection failed'}`);
}
}
/**
* Test External Service Connectivity
*/
async testConnectivity(tenantId: string): Promise<{
weather: boolean;
traffic: boolean;
overall: boolean;
}> {
const results = {
weather: false,
traffic: false,
overall: false
};
try {
// Test weather service (AEMET API)
await this.getCurrentWeather(tenantId, 40.4168, -3.7038); // Madrid coordinates
results.weather = true;
} catch (error) {
console.warn('AEMET weather service connectivity test failed:', error);
results.weather = false;
}
try {
// Test traffic service
await this.getCurrentTraffic(tenantId, 40.4168, -3.7038); // Madrid coordinates
results.traffic = true;
} catch (error) {
console.warn('Traffic service connectivity test failed:', error);
results.traffic = false;
}
results.overall = results.weather && results.traffic;
return results;
}
}
export const externalService = new ExternalService();

View File

@@ -1,301 +0,0 @@
// frontend/src/api/services/forecasting.service.ts
/**
* Forecasting Service
* Handles forecast operations and predictions
*/
import { apiClient } from '../client';
import { RequestTimeouts } from '../client/config';
import type {
SingleForecastRequest,
BatchForecastRequest,
ForecastResponse,
BatchForecastResponse,
ForecastAlert,
QuickForecast,
PaginatedResponse,
BaseQueryParams,
} from '../types';
export class ForecastingService {
/**
* Create Single Product Forecast
*/
async createSingleForecast(
tenantId: string,
request: SingleForecastRequest
): Promise<ForecastResponse[]> {
console.log('🔮 Creating single forecast:', { tenantId, request });
try {
// Backend returns single ForecastResponse object
const response = await apiClient.post(
`/tenants/${tenantId}/forecasts/single`,
request,
{
timeout: RequestTimeouts.MEDIUM,
}
);
console.log('🔮 Forecast API Response:', response);
console.log('- Type:', typeof response);
console.log('- Is Array:', Array.isArray(response));
// ✅ FIX: Convert single response to array
if (response && typeof response === 'object' && !Array.isArray(response)) {
// Single forecast response - wrap in array
const forecastArray = [response as ForecastResponse];
console.log('✅ Converted single forecast to array:', forecastArray);
return forecastArray;
} else if (Array.isArray(response)) {
// Already an array (unexpected but handle gracefully)
console.log('✅ Response is already an array:', response);
return response;
} else {
console.error('❌ Unexpected response format:', response);
throw new Error('Invalid forecast response format');
}
} catch (error) {
console.error('❌ Forecast API Error:', error);
throw error;
}
}
/**
* Create Batch Forecast
*/
async createBatchForecast(
tenantId: string,
request: BatchForecastRequest
): Promise<BatchForecastResponse> {
return apiClient.post(
`/tenants/${tenantId}/forecasts/batch`,
request,
{
timeout: RequestTimeouts.LONG,
}
);
}
/**
* Get Forecast by ID
*/
async getForecast(tenantId: string, forecastId: string): Promise<ForecastResponse> {
return apiClient.get(`/tenants/${tenantId}/forecasts/${forecastId}`);
}
/**
* Get Forecasts
*/
async getForecasts(
tenantId: string,
params?: BaseQueryParams & {
inventory_product_id?: string; // Primary way to filter by product
product_name?: string; // For backward compatibility - will need inventory service lookup
start_date?: string;
end_date?: string;
model_id?: string;
}
): Promise<PaginatedResponse<ForecastResponse>> {
return apiClient.get(`/tenants/${tenantId}/forecasts`, { params });
}
/**
* Get Batch Forecast Status
*/
async getBatchForecastStatus(
tenantId: string,
batchId: string
): Promise<BatchForecastResponse> {
return apiClient.get(`/tenants/${tenantId}/forecasts/batch/${batchId}/status`);
}
/**
* Get Batch Forecasts
*/
async getBatchForecasts(
tenantId: string,
params?: BaseQueryParams & {
status?: string;
start_date?: string;
end_date?: string;
}
): Promise<PaginatedResponse<BatchForecastResponse>> {
return apiClient.get(`/tenants/${tenantId}/forecasts/batch`, { params });
}
/**
* Cancel Batch Forecast
*/
async cancelBatchForecast(tenantId: string, batchId: string): Promise<{ message: string }> {
return apiClient.post(`/tenants/${tenantId}/forecasts/batch/${batchId}/cancel`);
}
/**
* Get Quick Forecasts for Dashboard
*/
async getQuickForecasts(tenantId: string, limit?: number): Promise<QuickForecast[]> {
try {
// TODO: Replace with actual /forecasts/quick endpoint when available
// For now, use regular forecasts endpoint and transform the data
const forecasts = await apiClient.get(`/tenants/${tenantId}/forecasts`, {
params: { limit: limit || 10 },
});
// Transform regular forecasts to QuickForecast format
// Handle response structure: { tenant_id, forecasts: [...], total_returned }
let forecastsArray: any[] = [];
if (Array.isArray(forecasts)) {
// Direct array response (unexpected)
forecastsArray = forecasts;
} else if (forecasts && typeof forecasts === 'object' && Array.isArray(forecasts.forecasts)) {
// Expected object response with forecasts array
forecastsArray = forecasts.forecasts;
} else {
console.warn('Unexpected forecasts response format:', forecasts);
return [];
}
return forecastsArray.map((forecast: any) => ({
inventory_product_id: forecast.inventory_product_id,
product_name: forecast.product_name, // Optional - for display
next_day_prediction: forecast.predicted_demand || 0,
next_week_avg: forecast.predicted_demand || 0,
trend_direction: 'stable' as const,
confidence_score: forecast.confidence_level || 0.8,
last_updated: forecast.created_at || new Date().toISOString()
}));
} catch (error) {
console.error('QuickForecasts API call failed, using fallback data:', error);
// Return mock data for common bakery products (using mock inventory_product_ids)
return [
{
inventory_product_id: 'mock-pan-de-molde-001',
product_name: 'Pan de Molde',
next_day_prediction: 25,
next_week_avg: 175,
trend_direction: 'stable',
confidence_score: 0.85,
last_updated: new Date().toISOString()
},
{
inventory_product_id: 'mock-baguettes-002',
product_name: 'Baguettes',
next_day_prediction: 20,
next_week_avg: 140,
trend_direction: 'up',
confidence_score: 0.92,
last_updated: new Date().toISOString()
},
{
inventory_product_id: 'mock-croissants-003',
product_name: 'Croissants',
next_day_prediction: 15,
next_week_avg: 105,
trend_direction: 'stable',
confidence_score: 0.78,
last_updated: new Date().toISOString()
},
{
inventory_product_id: 'mock-magdalenas-004',
product_name: 'Magdalenas',
next_day_prediction: 12,
next_week_avg: 84,
trend_direction: 'down',
confidence_score: 0.76,
last_updated: new Date().toISOString()
}
];
}
}
/**
* Get Forecast Alerts
*/
async getForecastAlerts(
tenantId: string,
params?: BaseQueryParams & {
is_active?: boolean;
severity?: string;
alert_type?: string;
}
): Promise<PaginatedResponse<ForecastAlert>> {
return apiClient.get(`/tenants/${tenantId}/forecasts/alerts`, { params });
}
/**
* Acknowledge Forecast Alert
*/
async acknowledgeForecastAlert(
tenantId: string,
alertId: string
): Promise<ForecastAlert> {
return apiClient.post(`/tenants/${tenantId}/forecasts/alerts/${alertId}/acknowledge`);
}
/**
* Delete Forecast
*/
async deleteForecast(tenantId: string, forecastId: string): Promise<{ message: string }> {
return apiClient.delete(`/tenants/${tenantId}/forecasts/${forecastId}`);
}
/**
* Export Forecasts
*/
async exportForecasts(
tenantId: string,
format: 'csv' | 'excel' | 'json',
params?: {
inventory_product_id?: string; // Primary way to filter by product
product_name?: string; // For backward compatibility
start_date?: string;
end_date?: string;
}
): Promise<Blob> {
const response = await apiClient.request(`/tenants/${tenantId}/forecasts/export`, {
method: 'GET',
params: { ...params, format },
headers: {
'Accept': format === 'csv' ? 'text/csv' :
format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
'application/json',
},
});
return new Blob([response], {
type: format === 'csv' ? 'text/csv' :
format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
'application/json',
});
}
/**
* Get Forecast Accuracy Metrics
*/
async getForecastAccuracy(
tenantId: string,
params?: {
inventory_product_id?: string; // Primary way to filter by product
product_name?: string; // For backward compatibility
model_id?: string;
start_date?: string;
end_date?: string;
}
): Promise<{
overall_accuracy: number;
product_accuracy: Array<{
inventory_product_id: string;
product_name?: string; // Optional - for display
accuracy: number;
sample_size: number;
}>;
}> {
return apiClient.get(`/tenants/${tenantId}/forecasts/accuracy`, { params });
}
}
export const forecastingService = new ForecastingService();

View File

@@ -1,145 +0,0 @@
// frontend/src/api/services/index.ts
/**
* Main Services Export
* Central export point for all API services
*/
// Import and export individual services
import { AuthService } from './auth.service';
import { TenantService } from './tenant.service';
import { SalesService } from './sales.service';
import { ExternalService } from './external.service';
import { TrainingService } from './training.service';
import { ForecastingService } from './forecasting.service';
import { NotificationService } from './notification.service';
import { OnboardingService } from './onboarding.service';
import { InventoryService } from './inventory.service';
import { RecipesService } from './recipes.service';
import { ProductionService } from './production.service';
import { OrdersService } from './orders.service';
import { SuppliersService } from './suppliers.service';
import { ProcurementService } from './procurement.service';
// Create service instances
export const authService = new AuthService();
export const tenantService = new TenantService();
export const salesService = new SalesService();
export const externalService = new ExternalService();
export const trainingService = new TrainingService();
export const forecastingService = new ForecastingService();
export const notificationService = new NotificationService();
export const onboardingService = new OnboardingService();
export const inventoryService = new InventoryService();
export const recipesService = new RecipesService();
export const productionService = new ProductionService();
export const ordersService = new OrdersService();
export const suppliersService = new SuppliersService();
export const procurementService = new ProcurementService();
// Export the classes as well
export {
AuthService,
TenantService,
SalesService,
ExternalService,
TrainingService,
ForecastingService,
NotificationService,
OnboardingService,
InventoryService,
RecipesService,
ProductionService,
OrdersService,
SuppliersService,
ProcurementService
};
// Import base client
import { apiClient } from '../client';
export { apiClient };
// Re-export all types
export * from '../types';
// Create unified API object
export const api = {
auth: authService,
tenant: tenantService,
sales: salesService,
external: externalService,
training: trainingService,
forecasting: forecastingService,
notification: notificationService,
onboarding: onboardingService,
inventory: inventoryService,
recipes: recipesService,
production: productionService,
orders: ordersService,
suppliers: suppliersService,
procurement: procurementService,
} as const;
// Service status checking
export interface ServiceHealth {
service: string;
status: 'healthy' | 'degraded' | 'down';
lastChecked: Date;
responseTime?: number;
error?: string;
}
export class HealthService {
async checkServiceHealth(): Promise<ServiceHealth[]> {
const services = [
{ name: 'Auth', endpoint: '/auth/health' },
{ name: 'Tenant', endpoint: '/tenants/health' },
{ name: 'Sales', endpoint: '/sales/health' },
{ name: 'External', endpoint: '/external/health' },
{ name: 'Training', endpoint: '/training/health' },
{ name: 'Inventory', endpoint: '/inventory/health' },
{ name: 'Production', endpoint: '/production/health' },
{ name: 'Orders', endpoint: '/orders/health' },
{ name: 'Suppliers', endpoint: '/suppliers/health' },
{ name: 'Forecasting', endpoint: '/forecasting/health' },
{ name: 'Notification', endpoint: '/notifications/health' },
{ name: 'Procurement', endpoint: '/procurement-plans/health' },
];
const healthChecks = await Promise.allSettled(
services.map(async (service) => {
const startTime = Date.now();
try {
await apiClient.get(service.endpoint, { timeout: 5000 });
const responseTime = Date.now() - startTime;
return {
service: service.name,
status: 'healthy' as const,
lastChecked: new Date(),
responseTime,
};
} catch (error) {
return {
service: service.name,
status: 'down' as const,
lastChecked: new Date(),
error: error instanceof Error ? error.message : 'Unknown error',
};
}
})
);
return healthChecks.map((result, index) =>
result.status === 'fulfilled'
? result.value
: {
service: services[index].name,
status: 'down' as const,
lastChecked: new Date(),
error: 'Health check failed',
}
);
}
}
export const healthService = new HealthService();

View File

@@ -1,749 +0,0 @@
// frontend/src/api/services/inventory.service.ts
/**
* Inventory Service
* Handles inventory management, stock tracking, and product operations
*/
import { apiClient } from '../client';
import type { ProductInfo } from '../types';
// ========== TYPES AND INTERFACES ==========
export type ProductType = 'ingredient' | 'finished_product';
export type UnitOfMeasure =
| 'kilograms' | 'grams' | 'liters' | 'milliliters'
| 'units' | 'pieces' | 'dozens' | 'boxes';
export type IngredientCategory =
| 'flour' | 'yeast' | 'dairy' | 'eggs' | 'sugar'
| 'fats' | 'salt' | 'spices' | 'additives' | 'packaging';
export type ProductCategory =
| 'bread' | 'croissants' | 'pastries' | 'cakes'
| 'cookies' | 'muffins' | 'sandwiches' | 'beverages' | 'other_products';
export type StockMovementType =
| 'purchase' | 'consumption' | 'adjustment'
| 'waste' | 'transfer' | 'return';
export interface InventoryItem {
id: string;
tenant_id: string;
name: string;
product_type: ProductType;
category: IngredientCategory | ProductCategory;
unit_of_measure: UnitOfMeasure;
estimated_shelf_life_days?: number;
requires_refrigeration: boolean;
requires_freezing: boolean;
is_seasonal: boolean;
minimum_stock_level?: number;
maximum_stock_level?: number;
reorder_point?: number;
supplier?: string;
notes?: string;
barcode?: string;
sku?: string;
cost_per_unit?: number;
is_active: boolean;
created_at: string;
updated_at: string;
// Computed fields
current_stock?: StockLevel;
low_stock_alert?: boolean;
expiring_soon_alert?: boolean;
recent_movements?: StockMovement[];
}
export interface StockLevel {
item_id: string;
current_quantity: number;
available_quantity: number;
reserved_quantity: number;
unit_of_measure: UnitOfMeasure;
value_estimate?: number;
last_updated: string;
// Batch information
batches?: StockBatch[];
oldest_batch_date?: string;
newest_batch_date?: string;
}
export interface StockBatch {
id: string;
item_id: string;
batch_number?: string;
quantity: number;
unit_cost?: number;
purchase_date?: string;
expiration_date?: string;
supplier?: string;
notes?: string;
is_expired: boolean;
days_until_expiration?: number;
}
export interface StockMovement {
id: string;
item_id: string;
movement_type: StockMovementType;
quantity: number;
unit_cost?: number;
total_cost?: number;
batch_id?: string;
reference_id?: string;
notes?: string;
movement_date: string;
created_by: string;
created_at: string;
// Related data
item_name?: string;
batch_info?: StockBatch;
}
// ========== REQUEST/RESPONSE TYPES ==========
export interface CreateInventoryItemRequest {
name: string;
product_type: ProductType;
category: IngredientCategory | ProductCategory;
unit_of_measure: UnitOfMeasure;
estimated_shelf_life_days?: number;
requires_refrigeration?: boolean;
requires_freezing?: boolean;
is_seasonal?: boolean;
minimum_stock_level?: number;
maximum_stock_level?: number;
reorder_point?: number;
supplier?: string;
notes?: string;
barcode?: string;
cost_per_unit?: number;
}
export interface UpdateInventoryItemRequest extends Partial<CreateInventoryItemRequest> {
is_active?: boolean;
}
export interface StockAdjustmentRequest {
movement_type: StockMovementType;
quantity: number;
unit_cost?: number;
batch_number?: string;
expiration_date?: string;
supplier?: string;
notes?: string;
}
export interface InventorySearchParams {
search?: string;
product_type?: ProductType;
category?: string;
is_active?: boolean;
low_stock_only?: boolean;
expiring_soon_only?: boolean;
page?: number;
limit?: number;
sort_by?: 'name' | 'category' | 'stock_level' | 'last_movement' | 'created_at';
sort_order?: 'asc' | 'desc';
}
export interface StockMovementSearchParams {
item_id?: string;
movement_type?: StockMovementType;
date_from?: string;
date_to?: string;
page?: number;
limit?: number;
}
export interface InventoryDashboardData {
total_items: number;
total_value: number;
low_stock_count: number;
expiring_soon_count: number;
recent_movements: StockMovement[];
top_items_by_value: InventoryItem[];
category_breakdown: {
category: string;
count: number;
value: number;
}[];
movement_trends: {
date: string;
purchases: number;
consumption: number;
waste: number;
}[];
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
limit: number;
total_pages: number;
}
// ========== INVENTORY SERVICE CLASS ==========
export class InventoryService {
private baseEndpoint = '';
// ========== INVENTORY ITEMS ==========
/**
* Get inventory items with filtering and pagination
*/
async getInventoryItems(
tenantId: string,
params?: InventorySearchParams
): Promise<PaginatedResponse<InventoryItem>> {
const searchParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
searchParams.append(key, value.toString());
}
});
}
const query = searchParams.toString();
const url = `/tenants/${tenantId}/ingredients${query ? `?${query}` : ''}`;
console.log('🔍 InventoryService: Fetching inventory items from:', url);
try {
console.log('🔑 InventoryService: Making request with auth token:', localStorage.getItem('auth_token') ? 'Present' : 'Missing');
const response = await apiClient.get(url);
console.log('📋 InventoryService: Raw response:', response);
console.log('📋 InventoryService: Response type:', typeof response);
console.log('📋 InventoryService: Response keys:', response ? Object.keys(response) : 'null');
// Handle different response formats
if (Array.isArray(response)) {
// Direct array response
console.log('✅ InventoryService: Array response with', response.length, 'items');
return {
items: response,
total: response.length,
page: 1,
limit: response.length,
total_pages: 1
};
} else if (response && typeof response === 'object') {
// Check if it's already paginated
if ('items' in response && Array.isArray(response.items)) {
console.log('✅ InventoryService: Paginated response with', response.items.length, 'items');
return response;
}
// Handle object with numeric keys (convert to array)
const keys = Object.keys(response);
if (keys.length > 0 && keys.every(key => !isNaN(Number(key)))) {
const items = Object.values(response);
console.log('✅ InventoryService: Numeric keys response with', items.length, 'items');
return {
items,
total: items.length,
page: 1,
limit: items.length,
total_pages: 1
};
}
// Handle empty object - this seems to be what we're getting
if (keys.length === 0) {
console.log('📭 InventoryService: Empty object response - backend has no inventory items for this tenant');
throw new Error('NO_INVENTORY_ITEMS'); // This will trigger fallback in useInventory
}
}
// Fallback: unexpected response format
console.warn('⚠️ InventoryService: Unexpected response format, keys:', Object.keys(response || {}));
throw new Error('UNEXPECTED_RESPONSE_FORMAT');
} catch (error) {
console.error('❌ InventoryService: Failed to fetch inventory items:', error);
throw error;
}
}
/**
* Get single inventory item by ID
*/
async getInventoryItem(tenantId: string, itemId: string): Promise<InventoryItem> {
return apiClient.get(`/tenants/${tenantId}/ingredients/${itemId}`);
}
/**
* Create new inventory item
*/
async createInventoryItem(
tenantId: string,
data: CreateInventoryItemRequest
): Promise<InventoryItem> {
return apiClient.post(`/tenants/${tenantId}/ingredients`, data);
}
/**
* Update existing inventory item
*/
async updateInventoryItem(
tenantId: string,
itemId: string,
data: UpdateInventoryItemRequest
): Promise<InventoryItem> {
return apiClient.put(`/tenants/${tenantId}/ingredients/${itemId}`, data);
}
/**
* Delete inventory item (soft delete)
*/
async deleteInventoryItem(tenantId: string, itemId: string): Promise<void> {
return apiClient.delete(`/tenants/${tenantId}/ingredients/${itemId}`);
}
/**
* Bulk update inventory items
*/
async bulkUpdateInventoryItems(
tenantId: string,
updates: { id: string; data: UpdateInventoryItemRequest }[]
): Promise<{ success: number; failed: number; errors: string[] }> {
return apiClient.post(`/tenants/${tenantId}/ingredients/bulk-update`, {
updates
});
}
// ========== STOCK MANAGEMENT ==========
/**
* Get current stock level for an item
*/
async getStockLevel(tenantId: string, itemId: string): Promise<StockLevel> {
return apiClient.get(`/tenants/${tenantId}/ingredients/${itemId}/stock`);
}
/**
* Get stock levels for all items
*/
async getAllStockLevels(tenantId: string): Promise<StockLevel[]> {
// TODO: Map to correct endpoint when available
return [];
// return apiClient.get(`/stock/summary`);
}
/**
* Adjust stock level (purchase, consumption, waste, etc.)
*/
async adjustStock(
tenantId: string,
itemId: string,
adjustment: StockAdjustmentRequest
): Promise<StockMovement> {
return apiClient.post(
`/stock/consume`,
adjustment
);
}
/**
* Bulk stock adjustments
*/
async bulkAdjustStock(
tenantId: string,
adjustments: { item_id: string; adjustment: StockAdjustmentRequest }[]
): Promise<{ success: number; failed: number; movements: StockMovement[]; errors: string[] }> {
return apiClient.post(`${this.baseEndpoint}/tenants/${tenantId}/inventory/stock/bulk-adjust`, {
adjustments
});
}
/**
* Get stock movements with filtering
*/
async getStockMovements(
tenantId: string,
params?: StockMovementSearchParams
): Promise<PaginatedResponse<StockMovement>> {
const searchParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
searchParams.append(key, value.toString());
}
});
}
const query = searchParams.toString();
const url = `${this.baseEndpoint}/tenants/${tenantId}/inventory/movements${query ? `?${query}` : ''}`;
return apiClient.get(url);
}
// ========== DASHBOARD & ANALYTICS ==========
/**
* Get inventory value report
*/
async getInventoryValue(tenantId: string): Promise<{
total_value: number;
by_category: { category: string; value: number; percentage: number }[];
by_product_type: { type: ProductType; value: number; percentage: number }[];
}> {
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/value`);
}
/**
* Get low stock report
*/
async getLowStockReport(tenantId: string): Promise<{
items: InventoryItem[];
total_affected: number;
estimated_loss: number;
}> {
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/reports/low-stock`);
}
/**
* Get expiring items report
*/
async getExpiringItemsReport(tenantId: string, days?: number): Promise<{
items: (InventoryItem & { batches: StockBatch[] })[];
total_affected: number;
estimated_loss: number;
}> {
const params = days ? `?days=${days}` : '';
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/reports/expiring${params}`);
}
// ========== IMPORT/EXPORT ==========
/**
* Export inventory data to CSV
*/
async exportInventory(tenantId: string, format: 'csv' | 'excel' = 'csv'): Promise<Blob> {
const response = await apiClient.getRaw(
`${this.baseEndpoint}/tenants/${tenantId}/inventory/export?format=${format}`
);
return response.blob();
}
/**
* Import inventory from file
*/
async importInventory(tenantId: string, file: File): Promise<{
success: number;
failed: number;
errors: string[];
created_items: InventoryItem[];
}> {
const formData = new FormData();
formData.append('file', file);
return apiClient.post(`${this.baseEndpoint}/tenants/${tenantId}/inventory/import`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
// ========== SEARCH & SUGGESTIONS ==========
/**
* Search inventory items with autocomplete
*/
async searchItems(tenantId: string, query: string, limit = 10): Promise<InventoryItem[]> {
return apiClient.get(
`${this.baseEndpoint}/tenants/${tenantId}/inventory/search?q=${encodeURIComponent(query)}&limit=${limit}`
);
}
/**
* Get category suggestions based on product type
*/
async getCategorySuggestions(productType: ProductType): Promise<string[]> {
return apiClient.get(`${this.baseEndpoint}/inventory/categories?type=${productType}`);
}
/**
* Get supplier suggestions
*/
async getSupplierSuggestions(tenantId: string): Promise<string[]> {
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/suppliers`);
}
// ========== PRODUCTS FOR FORECASTING ==========
/**
* Get Products List with IDs for Forecasting
*/
async getProductsList(tenantId: string): Promise<ProductInfo[]> {
try {
console.log('🔍 Fetching products for forecasting...', { tenantId });
// First try to get finished products (preferred for forecasting)
const response = await apiClient.get(`/tenants/${tenantId}/ingredients`, {
params: {
limit: 100,
product_type: 'finished_product'
},
});
console.log('🔍 Inventory Products API Response:', response);
console.log('🔍 Raw response data:', response.data);
console.log('🔍 Response status:', response.status);
console.log('🔍 Response headers:', response.headers);
console.log('🔍 Full response object keys:', Object.keys(response || {}));
console.log('🔍 Response data type:', typeof response);
console.log('🔍 Response data constructor:', response?.constructor?.name);
// Check if response.data exists and what type it is
if (response && 'data' in response) {
console.log('🔍 Response.data exists:', typeof response.data);
console.log('🔍 Response.data keys:', Object.keys(response.data || {}));
console.log('🔍 Response.data constructor:', response.data?.constructor?.name);
}
let productsArray: any[] = [];
// Check response.data first (typical API client behavior)
const dataToProcess = response?.data || response;
if (Array.isArray(dataToProcess)) {
productsArray = dataToProcess;
console.log('✅ Found array data with', productsArray.length, 'items');
} else if (dataToProcess && typeof dataToProcess === 'object') {
// Handle different response formats
const keys = Object.keys(dataToProcess);
if (keys.length > 0 && keys.every(key => !isNaN(Number(key)))) {
productsArray = Object.values(dataToProcess);
console.log('✅ Found object with numeric keys, converted to array with', productsArray.length, 'items');
} else {
console.warn('⚠️ Response is object but not with numeric keys:', dataToProcess);
console.warn('⚠️ Object keys:', keys);
return [];
}
} else {
console.warn('⚠️ Response data is not array or object:', dataToProcess);
return [];
}
// Convert to ProductInfo objects
const products: ProductInfo[] = productsArray
.map((product: any) => ({
inventory_product_id: product.id || product.inventory_product_id,
name: product.name || product.product_name || `Product ${product.id || ''}`,
category: product.category,
// Add additional fields if available from inventory
current_stock: product.current_stock,
unit: product.unit,
cost_per_unit: product.cost_per_unit
}))
.filter(product => product.inventory_product_id && product.name);
console.log('📋 Processed finished products:', products);
// If no finished products found, try to get all products as fallback
if (products.length === 0) {
console.log('⚠️ No finished products found, trying to get all products as fallback...');
const fallbackResponse = await apiClient.get(`/tenants/${tenantId}/ingredients`, {
params: {
limit: 100,
// No product_type filter to get all products
},
});
console.log('🔍 Fallback API Response:', fallbackResponse);
const fallbackDataToProcess = fallbackResponse?.data || fallbackResponse;
let fallbackProductsArray: any[] = [];
if (Array.isArray(fallbackDataToProcess)) {
fallbackProductsArray = fallbackDataToProcess;
} else if (fallbackDataToProcess && typeof fallbackDataToProcess === 'object') {
const keys = Object.keys(fallbackDataToProcess);
if (keys.length > 0 && keys.every(key => !isNaN(Number(key)))) {
fallbackProductsArray = Object.values(fallbackDataToProcess);
}
}
const fallbackProducts: ProductInfo[] = fallbackProductsArray
.map((product: any) => ({
inventory_product_id: product.id || product.inventory_product_id,
name: product.name || product.product_name || `Product ${product.id || ''}`,
category: product.category,
current_stock: product.current_stock,
unit: product.unit,
cost_per_unit: product.cost_per_unit
}))
.filter(product => product.inventory_product_id && product.name);
console.log('📋 Processed fallback products (all inventory items):', fallbackProducts);
return fallbackProducts;
}
return products;
} catch (error) {
console.error('❌ Failed to fetch inventory products:', error);
console.error('❌ Error details:', {
message: error instanceof Error ? error.message : 'Unknown error',
response: (error as any)?.response,
status: (error as any)?.response?.status,
data: (error as any)?.response?.data
});
// If it's an authentication error, throw it to trigger auth flow
if ((error as any)?.response?.status === 401) {
throw error;
}
// Return empty array on other errors - let dashboard handle fallback
return [];
}
}
/**
* Get Product by ID
*/
async getProductById(tenantId: string, productId: string): Promise<ProductInfo | null> {
try {
const response = await apiClient.get(`/tenants/${tenantId}/ingredients/${productId}`);
if (response) {
return {
inventory_product_id: response.id || response.inventory_product_id,
name: response.name || response.product_name,
category: response.category,
current_stock: response.current_stock,
unit: response.unit,
cost_per_unit: response.cost_per_unit
};
}
return null;
} catch (error) {
console.error('❌ Failed to fetch product by ID:', error);
return null;
}
}
// ========== ENHANCED DASHBOARD FEATURES ==========
/**
* Get inventory dashboard data with analytics
*/
async getDashboardData(tenantId: string, params?: {
date_from?: string;
date_to?: string;
location?: string;
}): Promise<{
summary: {
total_items: number;
low_stock_count: number;
out_of_stock_items: number;
expiring_soon: number;
total_value: number;
};
recent_movements: any[];
active_alerts: any[];
stock_trends: {
dates: string[];
stock_levels: number[];
movements_in: number[];
movements_out: number[];
};
}> {
try {
return await apiClient.get(`/tenants/${tenantId}/inventory/dashboard`, { params });
} catch (error) {
console.error('❌ Error fetching inventory dashboard:', error);
throw error;
}
}
/**
* Get food safety compliance data
*/
async getFoodSafetyCompliance(tenantId: string): Promise<{
compliant_items: number;
non_compliant_items: number;
expiring_items: any[];
temperature_violations: any[];
compliance_score: number;
}> {
try {
return await apiClient.get(`/tenants/${tenantId}/inventory/food-safety/compliance`);
} catch (error) {
console.error('❌ Error fetching food safety compliance:', error);
throw error;
}
}
/**
* Get temperature monitoring data
*/
async getTemperatureMonitoring(tenantId: string, params?: {
item_id?: string;
location?: string;
date_from?: string;
date_to?: string;
}): Promise<{
readings: any[];
violations: any[];
}> {
try {
return await apiClient.get(`/tenants/${tenantId}/inventory/food-safety/temperature-monitoring`, { params });
} catch (error) {
console.error('❌ Error fetching temperature monitoring:', error);
throw error;
}
}
/**
* Record temperature reading
*/
async recordTemperatureReading(tenantId: string, params: {
item_id: string;
temperature: number;
humidity?: number;
location: string;
notes?: string;
}): Promise<void> {
try {
return await apiClient.post(`/tenants/${tenantId}/inventory/food-safety/temperature-reading`, params);
} catch (error) {
console.error('❌ Error recording temperature reading:', error);
throw error;
}
}
/**
* Get restock recommendations
*/
async getRestockRecommendations(tenantId: string): Promise<{
urgent_restocks: any[];
optimal_orders: any[];
}> {
try {
return await apiClient.get(`/tenants/${tenantId}/inventory/forecasting/restock-recommendations`);
} catch (error) {
console.error('❌ Error fetching restock recommendations:', error);
throw error;
}
}
}
export const inventoryService = new InventoryService();

View File

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

View File

@@ -1,288 +0,0 @@
// frontend/src/api/services/onboarding.service.ts
/**
* Onboarding Service
* Handles user progress tracking and onboarding flow management
*/
import { apiClient } from '../client';
export interface OnboardingStepStatus {
step_name: string;
completed: boolean;
completed_at?: string;
data?: Record<string, any>;
}
export interface UserProgress {
user_id: string;
steps: OnboardingStepStatus[];
current_step: string;
next_step?: string;
completion_percentage: number;
fully_completed: boolean;
last_updated: string;
}
export interface UpdateStepRequest {
step_name: string;
completed: boolean;
data?: Record<string, any>;
}
export interface InventorySuggestion {
suggestion_id: string;
original_name: string;
suggested_name: string;
product_type: 'ingredient' | 'finished_product';
category: string;
unit_of_measure: string;
confidence_score: number;
estimated_shelf_life_days?: number;
requires_refrigeration: boolean;
requires_freezing: boolean;
is_seasonal: boolean;
suggested_supplier?: string;
notes?: string;
user_approved?: boolean;
user_modifications?: Record<string, any>;
}
export interface BusinessModelAnalysis {
model: 'production' | 'retail' | 'hybrid';
confidence: number;
ingredient_count: number;
finished_product_count: number;
ingredient_ratio: number;
recommendations: string[];
}
// Step 1: File validation result
export interface FileValidationResult {
is_valid: boolean;
total_records: number;
unique_products: number;
product_list: string[];
validation_errors: any[];
validation_warnings: any[];
summary: Record<string, any>;
}
// Step 2: AI suggestions result
export interface ProductSuggestionsResult {
suggestions: InventorySuggestion[];
business_model_analysis: BusinessModelAnalysis;
total_products: number;
high_confidence_count: number;
low_confidence_count: number;
processing_time_seconds: number;
}
// Legacy support - will be deprecated
export interface OnboardingAnalysisResult {
total_products_found: number;
inventory_suggestions: InventorySuggestion[];
business_model_analysis: BusinessModelAnalysis;
import_job_id: string;
status: string;
processed_rows: number;
errors: string[];
warnings: string[];
}
export interface InventoryCreationResult {
created_items: any[];
failed_items: any[];
total_approved: number;
success_rate: number;
}
export interface SalesImportResult {
import_job_id: string;
status: string;
processed_rows: number;
successful_imports: number;
failed_imports: number;
errors: string[];
warnings: string[];
}
export class OnboardingService {
private baseEndpoint = '/users/me/onboarding';
/**
* Get user's current onboarding progress
*/
async getUserProgress(): Promise<UserProgress> {
return apiClient.get(`${this.baseEndpoint}/progress`);
}
/**
* Update a specific onboarding step
*/
async updateStep(data: UpdateStepRequest): Promise<UserProgress> {
return apiClient.put(`${this.baseEndpoint}/step`, data);
}
/**
* Mark step as completed with optional data
*/
async completeStep(stepName: string, data?: Record<string, any>): Promise<UserProgress> {
return this.updateStep({
step_name: stepName,
completed: true,
data
});
}
/**
* Reset a step (mark as incomplete)
*/
async resetStep(stepName: string): Promise<UserProgress> {
return this.updateStep({
step_name: stepName,
completed: false
});
}
/**
* Get next required step for user
*/
async getNextStep(): Promise<{ step: string; data?: Record<string, any> }> {
return apiClient.get(`${this.baseEndpoint}/next-step`);
}
/**
* Complete entire onboarding process
*/
async completeOnboarding(): Promise<{ success: boolean; message: string }> {
return apiClient.post(`${this.baseEndpoint}/complete`);
}
/**
* Check if user can access a specific step
*/
async canAccessStep(stepName: string): Promise<{ can_access: boolean; reason?: string }> {
return apiClient.get(`${this.baseEndpoint}/can-access/${stepName}`);
}
// ========== NEW 4-STEP AUTOMATED INVENTORY CREATION METHODS ==========
/**
* Step 1: Validate file and extract unique products
*/
async validateFileAndExtractProducts(tenantId: string, file: File): Promise<FileValidationResult> {
const formData = new FormData();
formData.append('file', file);
return apiClient.post(`/tenants/${tenantId}/onboarding/validate-file`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
/**
* Step 2: Generate AI-powered inventory suggestions
*/
async generateInventorySuggestions(
tenantId: string,
file: File,
productList: string[]
): Promise<ProductSuggestionsResult> {
const formData = new FormData();
formData.append('file', file);
formData.append('product_list', JSON.stringify(productList));
return apiClient.post(`/tenants/${tenantId}/onboarding/generate-suggestions`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
/**
* Step 3: Create inventory from approved suggestions
*/
async createInventoryFromSuggestions(
tenantId: string,
suggestions: InventorySuggestion[]
): Promise<InventoryCreationResult> {
return apiClient.post(`/tenants/${tenantId}/onboarding/create-inventory`, {
suggestions: suggestions.map(s => ({
suggestion_id: s.suggestion_id,
approved: s.user_approved ?? true,
modifications: s.user_modifications || {},
// Include full suggestion data for backend processing
original_name: s.original_name,
suggested_name: s.suggested_name,
product_type: s.product_type,
category: s.category,
unit_of_measure: s.unit_of_measure,
confidence_score: s.confidence_score,
estimated_shelf_life_days: s.estimated_shelf_life_days,
requires_refrigeration: s.requires_refrigeration,
requires_freezing: s.requires_freezing,
is_seasonal: s.is_seasonal,
suggested_supplier: s.suggested_supplier,
notes: s.notes
}))
});
}
/**
* Step 4: Final sales data import with inventory mapping
*/
async importSalesWithInventory(
tenantId: string,
file: File,
inventoryMapping: Record<string, string>
): Promise<SalesImportResult> {
const formData = new FormData();
formData.append('file', file);
formData.append('inventory_mapping', JSON.stringify(inventoryMapping));
return apiClient.post(`/tenants/${tenantId}/onboarding/import-sales`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
// ========== LEGACY METHODS (for backward compatibility) ==========
/**
* @deprecated Use the new 4-step flow instead
* Phase 1: Analyze sales data and get AI suggestions (OLD METHOD)
*/
async analyzeSalesDataForOnboarding(tenantId: string, file: File): Promise<OnboardingAnalysisResult> {
// This method will use the new flow under the hood for backward compatibility
const validationResult = await this.validateFileAndExtractProducts(tenantId, file);
if (!validationResult.is_valid) {
throw new Error(`File validation failed: ${validationResult.validation_errors.map(e => e.message || e).join(', ')}`);
}
const suggestionsResult = await this.generateInventorySuggestions(tenantId, file, validationResult.product_list);
// Convert to legacy format
return {
total_products_found: suggestionsResult.total_products,
inventory_suggestions: suggestionsResult.suggestions,
business_model_analysis: suggestionsResult.business_model_analysis,
import_job_id: `legacy-${Date.now()}`,
status: 'completed',
processed_rows: validationResult.total_records,
errors: validationResult.validation_errors.map(e => e.message || String(e)),
warnings: validationResult.validation_warnings.map(w => w.message || String(w))
};
}
/**
* Get business model guidance based on analysis
*/
async getBusinessModelGuide(tenantId: string, model: string): Promise<any> {
return apiClient.get(`/tenants/${tenantId}/onboarding/business-model-guide?model=${model}`);
}
}
export const onboardingService = new OnboardingService();

View File

@@ -1,349 +0,0 @@
// ================================================================
// frontend/src/api/services/orders.service.ts
// ================================================================
/**
* Orders Service - API client for Orders Service endpoints
*/
import { apiClient } from '../client';
// Order Types
export interface Order {
id: string;
tenant_id: string;
customer_id?: string;
customer_name?: string;
customer_email?: string;
customer_phone?: string;
order_number: string;
status: 'pending' | 'confirmed' | 'in_production' | 'ready' | 'delivered' | 'cancelled';
order_type: 'walk_in' | 'online' | 'phone' | 'catering';
business_model: 'individual_bakery' | 'central_bakery';
items: OrderItem[];
subtotal: number;
tax_amount: number;
discount_amount: number;
total_amount: number;
delivery_date?: string;
delivery_address?: string;
notes?: string;
created_at: string;
updated_at: string;
}
export interface OrderItem {
id: string;
recipe_id: string;
recipe_name: string;
quantity: number;
unit_price: number;
total_price: number;
customizations?: Record<string, any>;
production_notes?: string;
}
export interface Customer {
id: string;
name: string;
email?: string;
phone?: string;
address?: string;
customer_type: 'individual' | 'business' | 'catering';
preferences?: string[];
loyalty_points?: number;
total_orders: number;
total_spent: number;
created_at: string;
updated_at: string;
}
export interface OrderDashboardData {
summary: {
total_orders_today: number;
pending_orders: number;
orders_in_production: number;
completed_orders: number;
revenue_today: number;
average_order_value: number;
};
recent_orders: Order[];
peak_hours: { hour: number; orders: number }[];
popular_items: { recipe_name: string; quantity: number }[];
business_model_distribution: { model: string; count: number; revenue: number }[];
}
export interface ProcurementPlan {
id: string;
date: string;
status: 'draft' | 'approved' | 'ordered' | 'completed';
total_cost: number;
items: ProcurementItem[];
supplier_orders: SupplierOrder[];
created_at: string;
updated_at: string;
}
export interface ProcurementItem {
ingredient_id: string;
ingredient_name: string;
required_quantity: number;
current_stock: number;
quantity_to_order: number;
unit: string;
estimated_cost: number;
priority: 'low' | 'medium' | 'high' | 'critical';
supplier_id?: string;
supplier_name?: string;
}
export interface SupplierOrder {
supplier_id: string;
supplier_name: string;
items: ProcurementItem[];
total_cost: number;
delivery_date?: string;
notes?: string;
}
export interface OrderCreateRequest {
customer_id?: string;
customer_name?: string;
customer_email?: string;
customer_phone?: string;
order_type: 'walk_in' | 'online' | 'phone' | 'catering';
business_model: 'individual_bakery' | 'central_bakery';
items: {
recipe_id: string;
quantity: number;
customizations?: Record<string, any>;
}[];
delivery_date?: string;
delivery_address?: string;
notes?: string;
}
export interface OrderUpdateRequest {
status?: 'pending' | 'confirmed' | 'in_production' | 'ready' | 'delivered' | 'cancelled';
items?: {
recipe_id: string;
quantity: number;
customizations?: Record<string, any>;
}[];
delivery_date?: string;
delivery_address?: string;
notes?: string;
}
export class OrdersService {
private readonly basePath = '/orders';
// Dashboard
async getDashboardData(params?: {
date_from?: string;
date_to?: string;
}): Promise<OrderDashboardData> {
return apiClient.get(`${this.basePath}/dashboard`, { params });
}
async getDashboardMetrics(params?: {
date_from?: string;
date_to?: string;
granularity?: 'hour' | 'day' | 'week' | 'month';
}): Promise<{
dates: string[];
order_counts: number[];
revenue: number[];
average_order_values: number[];
business_model_breakdown: { model: string; orders: number[]; revenue: number[] }[];
}> {
return apiClient.get(`${this.basePath}/dashboard/metrics`, { params });
}
// Orders
async getOrders(params?: {
status?: string;
order_type?: string;
business_model?: string;
customer_id?: string;
date_from?: string;
date_to?: string;
limit?: number;
offset?: number;
}): Promise<Order[]> {
return apiClient.get(`${this.basePath}`, { params });
}
async getOrder(orderId: string): Promise<Order> {
return apiClient.get(`${this.basePath}/${orderId}`);
}
async createOrder(order: OrderCreateRequest): Promise<Order> {
return apiClient.post(`${this.basePath}`, order);
}
async updateOrder(orderId: string, updates: OrderUpdateRequest): Promise<Order> {
return apiClient.put(`${this.basePath}/${orderId}`, updates);
}
async deleteOrder(orderId: string): Promise<void> {
return apiClient.delete(`${this.basePath}/${orderId}`);
}
async updateOrderStatus(orderId: string, status: Order['status']): Promise<Order> {
return apiClient.patch(`${this.basePath}/${orderId}/status`, { status });
}
async getOrderHistory(orderId: string): Promise<{
order: Order;
status_changes: {
status: string;
timestamp: string;
user: string;
notes?: string
}[];
}> {
return apiClient.get(`${this.basePath}/${orderId}/history`);
}
// Customers
async getCustomers(params?: {
search?: string;
customer_type?: string;
limit?: number;
offset?: number;
}): Promise<Customer[]> {
return apiClient.get(`${this.basePath}/customers`, { params });
}
async getCustomer(customerId: string): Promise<Customer> {
return apiClient.get(`${this.basePath}/customers/${customerId}`);
}
async createCustomer(customer: {
name: string;
email?: string;
phone?: string;
address?: string;
customer_type: 'individual' | 'business' | 'catering';
preferences?: string[];
}): Promise<Customer> {
return apiClient.post(`${this.basePath}/customers`, customer);
}
async updateCustomer(customerId: string, updates: {
name?: string;
email?: string;
phone?: string;
address?: string;
customer_type?: 'individual' | 'business' | 'catering';
preferences?: string[];
}): Promise<Customer> {
return apiClient.put(`${this.basePath}/customers/${customerId}`, updates);
}
async getCustomerOrders(customerId: string, params?: {
limit?: number;
offset?: number;
}): Promise<Order[]> {
return apiClient.get(`${this.basePath}/customers/${customerId}/orders`, { params });
}
// Procurement Planning
async getProcurementPlans(params?: {
status?: string;
date_from?: string;
date_to?: string;
limit?: number;
offset?: number;
}): Promise<ProcurementPlan[]> {
return apiClient.get(`${this.basePath}/procurement/plans`, { params });
}
async getProcurementPlan(planId: string): Promise<ProcurementPlan> {
return apiClient.get(`${this.basePath}/procurement/plans/${planId}`);
}
async createProcurementPlan(params: {
date: string;
orders?: string[];
forecast_days?: number;
}): Promise<ProcurementPlan> {
return apiClient.post(`${this.basePath}/procurement/plans`, params);
}
async updateProcurementPlan(planId: string, updates: {
items?: ProcurementItem[];
notes?: string;
}): Promise<ProcurementPlan> {
return apiClient.put(`${this.basePath}/procurement/plans/${planId}`, updates);
}
async approveProcurementPlan(planId: string): Promise<ProcurementPlan> {
return apiClient.post(`${this.basePath}/procurement/plans/${planId}/approve`);
}
async generateSupplierOrders(planId: string): Promise<SupplierOrder[]> {
return apiClient.post(`${this.basePath}/procurement/plans/${planId}/generate-orders`);
}
// Business Model Detection
async detectBusinessModel(): Promise<{
detected_model: 'individual_bakery' | 'central_bakery';
confidence: number;
factors: {
daily_order_volume: number;
delivery_ratio: number;
catering_ratio: number;
average_order_size: number;
};
recommendations: string[];
}> {
return apiClient.post(`${this.basePath}/business-model/detect`);
}
async updateBusinessModel(model: 'individual_bakery' | 'central_bakery'): Promise<void> {
return apiClient.put(`${this.basePath}/business-model`, { business_model: model });
}
// Analytics
async getOrderTrends(params?: {
date_from?: string;
date_to?: string;
granularity?: 'hour' | 'day' | 'week' | 'month';
}): Promise<{
dates: string[];
order_counts: number[];
revenue: number[];
popular_items: { recipe_name: string; count: number }[];
}> {
return apiClient.get(`${this.basePath}/analytics/trends`, { params });
}
async getCustomerAnalytics(params?: {
date_from?: string;
date_to?: string;
}): Promise<{
new_customers: number;
returning_customers: number;
customer_retention_rate: number;
average_lifetime_value: number;
top_customers: Customer[];
}> {
return apiClient.get(`${this.basePath}/analytics/customers`, { params });
}
async getSeasonalAnalysis(params?: {
date_from?: string;
date_to?: string;
}): Promise<{
seasonal_patterns: { month: string; order_count: number; revenue: number }[];
weekly_patterns: { day: string; order_count: number }[];
hourly_patterns: { hour: number; order_count: number }[];
trending_products: { recipe_name: string; growth_rate: number }[];
}> {
return apiClient.get(`${this.basePath}/analytics/seasonal`, { params });
}
}

View File

@@ -1,392 +0,0 @@
// frontend/src/api/services/pos.service.ts
/**
* POS Integration API Service
* Handles all communication with the POS service backend
*/
import { apiClient } from '../client';
// ============================================================================
// TYPES & INTERFACES
// ============================================================================
export interface POSConfiguration {
id: string;
tenant_id: string;
pos_system: 'square' | 'toast' | 'lightspeed';
provider_name: string;
is_active: boolean;
is_connected: boolean;
environment: 'sandbox' | 'production';
location_id?: string;
merchant_id?: string;
sync_enabled: boolean;
sync_interval_minutes: string;
auto_sync_products: boolean;
auto_sync_transactions: boolean;
webhook_url?: string;
last_sync_at?: string;
last_successful_sync_at?: string;
last_sync_status?: 'success' | 'failed' | 'partial';
last_sync_message?: string;
provider_settings?: Record<string, any>;
last_health_check_at?: string;
health_status: 'healthy' | 'unhealthy' | 'warning' | 'unknown';
health_message?: string;
created_at: string;
updated_at: string;
created_by?: string;
notes?: string;
}
export interface CreatePOSConfigurationRequest {
pos_system: 'square' | 'toast' | 'lightspeed';
provider_name: string;
environment: 'sandbox' | 'production';
location_id?: string;
merchant_id?: string;
sync_enabled?: boolean;
sync_interval_minutes?: string;
auto_sync_products?: boolean;
auto_sync_transactions?: boolean;
notes?: string;
// Credentials
api_key?: string;
api_secret?: string;
access_token?: string;
application_id?: string;
webhook_secret?: string;
}
export interface UpdatePOSConfigurationRequest {
provider_name?: string;
is_active?: boolean;
environment?: 'sandbox' | 'production';
location_id?: string;
merchant_id?: string;
sync_enabled?: boolean;
sync_interval_minutes?: string;
auto_sync_products?: boolean;
auto_sync_transactions?: boolean;
notes?: string;
// Credentials (only if updating)
api_key?: string;
api_secret?: string;
access_token?: string;
application_id?: string;
webhook_secret?: string;
}
export interface POSTransaction {
id: string;
tenant_id: string;
pos_config_id: string;
pos_system: string;
external_transaction_id: string;
external_order_id?: string;
transaction_type: 'sale' | 'refund' | 'void' | 'exchange';
status: 'completed' | 'pending' | 'failed' | 'refunded' | 'voided';
subtotal: number;
tax_amount: number;
tip_amount: number;
discount_amount: number;
total_amount: number;
currency: string;
payment_method?: string;
payment_status?: string;
transaction_date: string;
pos_created_at: string;
pos_updated_at?: string;
location_id?: string;
location_name?: string;
staff_id?: string;
staff_name?: string;
customer_id?: string;
customer_email?: string;
customer_phone?: string;
order_type?: string;
table_number?: string;
receipt_number?: string;
is_synced_to_sales: boolean;
sales_record_id?: string;
sync_attempted_at?: string;
sync_completed_at?: string;
sync_error?: string;
sync_retry_count: number;
is_processed: boolean;
processing_error?: string;
is_duplicate: boolean;
duplicate_of?: string;
created_at: string;
updated_at: string;
items: POSTransactionItem[];
}
export interface POSTransactionItem {
id: string;
transaction_id: string;
external_item_id?: string;
sku?: string;
product_name: string;
product_category?: string;
product_subcategory?: string;
quantity: number;
unit_price: number;
total_price: number;
discount_amount: number;
tax_amount: number;
modifiers?: Record<string, any>;
inventory_product_id?: string;
is_mapped_to_inventory: boolean;
is_synced_to_sales: boolean;
sync_error?: string;
}
export interface POSSyncLog {
id: string;
tenant_id: string;
pos_config_id: string;
sync_type: 'full' | 'incremental' | 'manual' | 'webhook_triggered';
sync_direction: 'inbound' | 'outbound' | 'bidirectional';
data_type: 'transactions' | 'products' | 'customers' | 'orders';
pos_system: string;
status: 'started' | 'in_progress' | 'completed' | 'failed' | 'cancelled';
started_at: string;
completed_at?: string;
duration_seconds?: number;
sync_from_date?: string;
sync_to_date?: string;
records_requested: number;
records_processed: number;
records_created: number;
records_updated: number;
records_skipped: number;
records_failed: number;
api_calls_made: number;
error_message?: string;
error_code?: string;
retry_attempt: number;
max_retries: number;
progress_percentage?: number;
revenue_synced?: number;
transactions_synced: number;
triggered_by?: string;
triggered_by_user_id?: string;
created_at: string;
updated_at: string;
}
export interface SyncRequest {
sync_type?: 'full' | 'incremental';
data_types?: ('transactions' | 'products' | 'customers')[];
from_date?: string;
to_date?: string;
}
export interface SyncStatus {
current_sync?: POSSyncLog;
last_successful_sync?: POSSyncLog;
recent_syncs: POSSyncLog[];
sync_health: {
status: 'healthy' | 'unhealthy' | 'warning';
success_rate: number;
average_duration_minutes: number;
last_error?: string;
};
}
export interface SupportedPOSSystem {
id: string;
name: string;
description: string;
features: string[];
supported_regions: string[];
}
export interface POSAnalytics {
period_days: number;
total_syncs: number;
successful_syncs: number;
failed_syncs: number;
success_rate: number;
average_duration_minutes: number;
total_transactions_synced: number;
total_revenue_synced: number;
sync_frequency: {
daily_average: number;
peak_day?: string;
peak_count: number;
};
error_analysis: {
common_errors: Array<{ error: string; count: number }>;
error_trends: Array<{ date: string; count: number }>;
};
}
export interface ConnectionTestResult {
status: 'success' | 'error';
message: string;
tested_at: string;
}
// ============================================================================
// API FUNCTIONS
// ============================================================================
export const posService = {
// Configuration Management
async getConfigurations(tenantId: string, params?: {
pos_system?: string;
is_active?: boolean;
limit?: number;
offset?: number;
}): Promise<{ configurations: POSConfiguration[]; total: number }> {
const response = await apiClient.get(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations`, {
params
});
return response.data;
},
async createConfiguration(tenantId: string, data: CreatePOSConfigurationRequest): Promise<POSConfiguration> {
const response = await apiClient.post(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations`, data);
return response.data;
},
async getConfiguration(tenantId: string, configId: string): Promise<POSConfiguration> {
const response = await apiClient.get(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations/${configId}`);
return response.data;
},
async updateConfiguration(tenantId: string, configId: string, data: UpdatePOSConfigurationRequest): Promise<POSConfiguration> {
const response = await apiClient.put(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations/${configId}`, data);
return response.data;
},
async deleteConfiguration(tenantId: string, configId: string): Promise<void> {
await apiClient.delete(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations/${configId}`);
},
async testConnection(tenantId: string, configId: string): Promise<ConnectionTestResult> {
const response = await apiClient.post(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations/${configId}/test-connection`);
return response.data;
},
// Synchronization
async triggerSync(tenantId: string, configId: string, syncRequest: SyncRequest): Promise<{
message: string;
sync_id: string;
status: string;
sync_type: string;
data_types: string[];
estimated_duration: string;
}> {
const response = await apiClient.post(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations/${configId}/sync`, syncRequest);
return response.data;
},
async getSyncStatus(tenantId: string, configId: string, limit?: number): Promise<SyncStatus> {
const response = await apiClient.get(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations/${configId}/sync/status`, {
params: { limit }
});
return response.data;
},
async getSyncLogs(tenantId: string, configId: string, params?: {
limit?: number;
offset?: number;
status?: string;
sync_type?: string;
data_type?: string;
}): Promise<{ logs: POSSyncLog[]; total: number; has_more: boolean }> {
const response = await apiClient.get(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations/${configId}/sync/logs`, {
params
});
return response.data;
},
// Transaction Management
async getTransactions(tenantId: string, params?: {
pos_system?: string;
start_date?: string;
end_date?: string;
status?: string;
is_synced?: boolean;
limit?: number;
offset?: number;
}): Promise<{
transactions: POSTransaction[];
total: number;
has_more: boolean;
summary: {
total_amount: number;
transaction_count: number;
sync_status: {
synced: number;
pending: number;
failed: number;
};
};
}> {
const response = await apiClient.get(`/pos-service/api/v1/tenants/${tenantId}/pos/transactions`, {
params
});
return response.data;
},
async syncSingleTransaction(tenantId: string, transactionId: string, force?: boolean): Promise<{
message: string;
transaction_id: string;
sync_status: string;
sales_record_id?: string;
}> {
const response = await apiClient.post(`/pos-service/api/v1/tenants/${tenantId}/pos/transactions/${transactionId}/sync`,
{}, { params: { force } }
);
return response.data;
},
async resyncFailedTransactions(tenantId: string, daysBack: number): Promise<{
message: string;
job_id: string;
scope: string;
estimated_transactions: number;
}> {
const response = await apiClient.post(`/pos-service/api/v1/tenants/${tenantId}/pos/data/resync`,
{}, { params: { days_back: daysBack } }
);
return response.data;
},
// Analytics
async getSyncAnalytics(tenantId: string, days: number = 30): Promise<POSAnalytics> {
const response = await apiClient.get(`/pos-service/api/v1/tenants/${tenantId}/pos/analytics/sync-performance`, {
params: { days }
});
return response.data;
},
// System Information
async getSupportedSystems(): Promise<{ systems: SupportedPOSSystem[] }> {
const response = await apiClient.get('/pos-service/api/v1/pos/supported-systems');
return response.data;
},
// Webhook Status
async getWebhookStatus(posSystem: string): Promise<{
pos_system: string;
status: string;
endpoint: string;
supported_events: {
events: string[];
format: string;
authentication: string;
};
last_received?: string;
total_received: number;
}> {
const response = await apiClient.get(`/pos-service/api/v1/webhooks/${posSystem}/status`);
return response.data;
}
};
export default posService;

View File

@@ -1,135 +0,0 @@
// ================================================================
// frontend/src/api/services/procurement.service.ts
// ================================================================
/**
* Procurement Service - API client for procurement planning endpoints
*/
import { ApiClient } from '../client';
import type {
ProcurementPlan,
GeneratePlanRequest,
GeneratePlanResponse,
DashboardData,
ProcurementRequirement,
PaginatedProcurementPlans
} from '../types/procurement';
export class ProcurementService {
constructor(private client: ApiClient) {}
// ================================================================
// PROCUREMENT PLAN OPERATIONS
// ================================================================
/**
* Get the procurement plan for the current day
*/
async getCurrentPlan(): Promise<ProcurementPlan | null> {
return this.client.get('/procurement-plans/current');
}
/**
* Get procurement plan for a specific date
*/
async getPlanByDate(date: string): Promise<ProcurementPlan | null> {
return this.client.get(`/procurement-plans/${date}`);
}
/**
* Get procurement plan by ID
*/
async getPlanById(planId: string): Promise<ProcurementPlan | null> {
return this.client.get(`/procurement-plans/id/${planId}`);
}
/**
* List procurement plans with optional filters
*/
async listPlans(params?: {
status?: string;
startDate?: string;
endDate?: string;
limit?: number;
offset?: number;
}): Promise<PaginatedProcurementPlans> {
return this.client.get('/procurement-plans/', { params });
}
/**
* Generate a new procurement plan
*/
async generatePlan(request: GeneratePlanRequest): Promise<GeneratePlanResponse> {
return this.client.post('/procurement-plans/generate', request);
}
/**
* Update procurement plan status
*/
async updatePlanStatus(planId: string, status: string): Promise<ProcurementPlan> {
return this.client.put(`/procurement-plans/${planId}/status`, null, {
params: { status }
});
}
// ================================================================
// REQUIREMENTS OPERATIONS
// ================================================================
/**
* Get all requirements for a specific procurement plan
*/
async getPlanRequirements(
planId: string,
params?: {
status?: string;
priority?: string;
}
): Promise<ProcurementRequirement[]> {
return this.client.get(`/procurement-plans/${planId}/requirements`, { params });
}
/**
* Get all critical priority requirements
*/
async getCriticalRequirements(): Promise<ProcurementRequirement[]> {
return this.client.get('/procurement-plans/requirements/critical');
}
// ================================================================
// DASHBOARD OPERATIONS
// ================================================================
/**
* Get procurement dashboard data
*/
async getDashboardData(): Promise<DashboardData | null> {
return this.client.get('/procurement-plans/dashboard/data');
}
// ================================================================
// UTILITY OPERATIONS
// ================================================================
/**
* Manually trigger the daily scheduler
*/
async triggerDailyScheduler(): Promise<{ success: boolean; message: string; tenant_id: string }> {
return this.client.post('/procurement-plans/scheduler/trigger');
}
/**
* Health check for procurement service
*/
async healthCheck(): Promise<{
status: string;
service: string;
procurement_enabled: boolean;
timestamp: string;
}> {
return this.client.get('/procurement-plans/health');
}
}
// Export singleton instance
export const procurementService = new ProcurementService(new ApiClient());

View File

@@ -1,296 +0,0 @@
// ================================================================
// frontend/src/api/services/production.service.ts
// ================================================================
/**
* Production Service - API client for Production Service endpoints
*/
import { apiClient } from '../client';
// Production Types
export interface ProductionBatch {
id: string;
recipe_id: string;
recipe_name: string;
quantity: number;
unit: string;
status: 'scheduled' | 'in_progress' | 'completed' | 'delayed' | 'failed';
scheduled_start: string;
actual_start?: string;
expected_end: string;
actual_end?: string;
equipment_id: string;
equipment_name: string;
operator_id: string;
operator_name: string;
temperature?: number;
humidity?: number;
quality_score?: number;
notes?: string;
created_at: string;
updated_at: string;
}
export interface ProductionPlan {
id: string;
date: string;
total_capacity: number;
allocated_capacity: number;
efficiency_target: number;
quality_target: number;
batches: ProductionBatch[];
status: 'draft' | 'approved' | 'in_progress' | 'completed';
created_at: string;
updated_at: string;
}
export interface Equipment {
id: string;
name: string;
type: string;
status: 'active' | 'idle' | 'maintenance' | 'error';
location: string;
capacity: number;
current_batch_id?: string;
temperature?: number;
utilization: number;
last_maintenance: string;
next_maintenance: string;
created_at: string;
updated_at: string;
}
export interface ProductionDashboardData {
summary: {
active_batches: number;
equipment_in_use: number;
current_efficiency: number;
todays_production: number;
};
efficiency_trend: { date: string; efficiency: number }[];
quality_trend: { date: string; quality: number }[];
equipment_status: Equipment[];
active_batches: ProductionBatch[];
}
export interface BatchCreateRequest {
recipe_id: string;
quantity: number;
scheduled_start: string;
expected_end: string;
equipment_id: string;
operator_id: string;
notes?: string;
priority?: number;
}
export interface BatchUpdateRequest {
status?: 'scheduled' | 'in_progress' | 'completed' | 'delayed' | 'failed';
actual_start?: string;
actual_end?: string;
temperature?: number;
humidity?: number;
quality_score?: number;
notes?: string;
}
export interface PlanCreateRequest {
date: string;
batches: BatchCreateRequest[];
efficiency_target?: number;
quality_target?: number;
}
export class ProductionService {
private readonly basePath = '/production';
// Dashboard
async getDashboardData(params?: {
date_from?: string;
date_to?: string;
}): Promise<ProductionDashboardData> {
return apiClient.get(`${this.basePath}/dashboard`, { params });
}
async getDashboardMetrics(params?: {
date_from?: string;
date_to?: string;
granularity?: 'hour' | 'day' | 'week' | 'month';
}): Promise<{
dates: string[];
efficiency: number[];
quality: number[];
production_volume: number[];
equipment_utilization: number[];
}> {
return apiClient.get(`${this.basePath}/dashboard/metrics`, { params });
}
// Batches
async getBatches(params?: {
status?: string;
equipment_id?: string;
date_from?: string;
date_to?: string;
limit?: number;
offset?: number;
}): Promise<ProductionBatch[]> {
return apiClient.get(`${this.basePath}/batches`, { params });
}
async getBatch(batchId: string): Promise<ProductionBatch> {
return apiClient.get(`${this.basePath}/batches/${batchId}`);
}
async createBatch(batch: BatchCreateRequest): Promise<ProductionBatch> {
return apiClient.post(`${this.basePath}/batches`, batch);
}
async updateBatch(batchId: string, updates: BatchUpdateRequest): Promise<ProductionBatch> {
return apiClient.put(`${this.basePath}/batches/${batchId}`, updates);
}
async deleteBatch(batchId: string): Promise<void> {
return apiClient.delete(`${this.basePath}/batches/${batchId}`);
}
async startBatch(batchId: string): Promise<ProductionBatch> {
return apiClient.post(`${this.basePath}/batches/${batchId}/start`);
}
async completeBatch(batchId: string, qualityScore?: number, notes?: string): Promise<ProductionBatch> {
return apiClient.post(`${this.basePath}/batches/${batchId}/complete`, {
quality_score: qualityScore,
notes
});
}
async getBatchStatus(batchId: string): Promise<{
status: string;
progress: number;
current_phase: string;
temperature: number;
humidity: number;
estimated_completion: string;
}> {
return apiClient.get(`${this.basePath}/batches/${batchId}/status`);
}
// Production Plans
async getPlans(params?: {
date_from?: string;
date_to?: string;
status?: string;
limit?: number;
offset?: number;
}): Promise<ProductionPlan[]> {
return apiClient.get(`${this.basePath}/plans`, { params });
}
async getPlan(planId: string): Promise<ProductionPlan> {
return apiClient.get(`${this.basePath}/plans/${planId}`);
}
async createPlan(plan: PlanCreateRequest): Promise<ProductionPlan> {
return apiClient.post(`${this.basePath}/plans`, plan);
}
async updatePlan(planId: string, updates: Partial<PlanCreateRequest>): Promise<ProductionPlan> {
return apiClient.put(`${this.basePath}/plans/${planId}`, updates);
}
async deletePlan(planId: string): Promise<void> {
return apiClient.delete(`${this.basePath}/plans/${planId}`);
}
async approvePlan(planId: string): Promise<ProductionPlan> {
return apiClient.post(`${this.basePath}/plans/${planId}/approve`);
}
async optimizePlan(planId: string): Promise<ProductionPlan> {
return apiClient.post(`${this.basePath}/plans/${planId}/optimize`);
}
// Equipment
async getEquipment(params?: {
status?: string;
type?: string;
location?: string;
limit?: number;
offset?: number;
}): Promise<Equipment[]> {
return apiClient.get(`${this.basePath}/equipment`, { params });
}
async getEquipmentById(equipmentId: string): Promise<Equipment> {
return apiClient.get(`${this.basePath}/equipment/${equipmentId}`);
}
async updateEquipment(equipmentId: string, updates: {
status?: 'active' | 'idle' | 'maintenance' | 'error';
temperature?: number;
notes?: string;
}): Promise<Equipment> {
return apiClient.put(`${this.basePath}/equipment/${equipmentId}`, updates);
}
async getEquipmentMetrics(equipmentId: string, params?: {
date_from?: string;
date_to?: string;
}): Promise<{
utilization: number[];
temperature: number[];
maintenance_events: any[];
performance_score: number;
}> {
return apiClient.get(`${this.basePath}/equipment/${equipmentId}/metrics`, { params });
}
async scheduleMaintenanceForEquipment(equipmentId: string, scheduledDate: string, notes?: string): Promise<void> {
return apiClient.post(`${this.basePath}/equipment/${equipmentId}/maintenance`, {
scheduled_date: scheduledDate,
notes
});
}
// Analytics
async getEfficiencyTrends(params?: {
date_from?: string;
date_to?: string;
equipment_id?: string;
}): Promise<{
dates: string[];
efficiency: number[];
quality: number[];
volume: number[];
}> {
return apiClient.get(`${this.basePath}/analytics/efficiency`, { params });
}
async getProductionForecast(params?: {
days?: number;
include_weather?: boolean;
}): Promise<{
dates: string[];
predicted_volume: number[];
confidence_intervals: number[][];
factors: string[];
}> {
return apiClient.get(`${this.basePath}/analytics/forecast`, { params });
}
async getQualityAnalysis(params?: {
date_from?: string;
date_to?: string;
recipe_id?: string;
}): Promise<{
average_quality: number;
quality_trend: number[];
quality_factors: { factor: string; impact: number }[];
recommendations: string[];
}> {
return apiClient.get(`${this.basePath}/analytics/quality`, { params });
}
}

View File

@@ -1,551 +0,0 @@
// frontend/src/api/services/recipes.service.ts
/**
* Recipe Service API Client
* Handles all recipe and production management API calls
*/
import { apiClient } from '../client';
import type {
PaginatedResponse,
ApiResponse,
CreateResponse,
UpdateResponse
} from '../types';
// Recipe Types
export interface Recipe {
id: string;
tenant_id: string;
name: string;
recipe_code?: string;
version: string;
finished_product_id: string;
description?: string;
category?: string;
cuisine_type?: string;
difficulty_level: number;
yield_quantity: number;
yield_unit: string;
prep_time_minutes?: number;
cook_time_minutes?: number;
total_time_minutes?: number;
rest_time_minutes?: number;
estimated_cost_per_unit?: number;
last_calculated_cost?: number;
cost_calculation_date?: string;
target_margin_percentage?: number;
suggested_selling_price?: number;
instructions?: Record<string, any>;
preparation_notes?: string;
storage_instructions?: string;
quality_standards?: string;
serves_count?: number;
nutritional_info?: Record<string, any>;
allergen_info?: Record<string, any>;
dietary_tags?: Record<string, any>;
batch_size_multiplier: number;
minimum_batch_size?: number;
maximum_batch_size?: number;
optimal_production_temperature?: number;
optimal_humidity?: number;
quality_check_points?: Record<string, any>;
common_issues?: Record<string, any>;
status: 'draft' | 'active' | 'testing' | 'archived' | 'discontinued';
is_seasonal: boolean;
season_start_month?: number;
season_end_month?: number;
is_signature_item: boolean;
created_at: string;
updated_at: string;
created_by?: string;
updated_by?: string;
ingredients?: RecipeIngredient[];
}
export interface RecipeIngredient {
id: string;
tenant_id: string;
recipe_id: string;
ingredient_id: string;
quantity: number;
unit: string;
quantity_in_base_unit?: number;
alternative_quantity?: number;
alternative_unit?: string;
preparation_method?: string;
ingredient_notes?: string;
is_optional: boolean;
ingredient_order: number;
ingredient_group?: string;
substitution_options?: Record<string, any>;
substitution_ratio?: number;
unit_cost?: number;
total_cost?: number;
cost_updated_at?: string;
}
export interface CreateRecipeRequest {
name: string;
recipe_code?: string;
version?: string;
finished_product_id: string;
description?: string;
category?: string;
cuisine_type?: string;
difficulty_level?: number;
yield_quantity: number;
yield_unit: string;
prep_time_minutes?: number;
cook_time_minutes?: number;
total_time_minutes?: number;
rest_time_minutes?: number;
instructions?: Record<string, any>;
preparation_notes?: string;
storage_instructions?: string;
quality_standards?: string;
serves_count?: number;
nutritional_info?: Record<string, any>;
allergen_info?: Record<string, any>;
dietary_tags?: Record<string, any>;
batch_size_multiplier?: number;
minimum_batch_size?: number;
maximum_batch_size?: number;
optimal_production_temperature?: number;
optimal_humidity?: number;
quality_check_points?: Record<string, any>;
common_issues?: Record<string, any>;
is_seasonal?: boolean;
season_start_month?: number;
season_end_month?: number;
is_signature_item?: boolean;
target_margin_percentage?: number;
ingredients: CreateRecipeIngredientRequest[];
}
export interface CreateRecipeIngredientRequest {
ingredient_id: string;
quantity: number;
unit: string;
alternative_quantity?: number;
alternative_unit?: string;
preparation_method?: string;
ingredient_notes?: string;
is_optional?: boolean;
ingredient_order: number;
ingredient_group?: string;
substitution_options?: Record<string, any>;
substitution_ratio?: number;
}
export interface UpdateRecipeRequest {
name?: string;
recipe_code?: string;
version?: string;
description?: string;
category?: string;
cuisine_type?: string;
difficulty_level?: number;
yield_quantity?: number;
yield_unit?: string;
prep_time_minutes?: number;
cook_time_minutes?: number;
total_time_minutes?: number;
rest_time_minutes?: number;
instructions?: Record<string, any>;
preparation_notes?: string;
storage_instructions?: string;
quality_standards?: string;
serves_count?: number;
nutritional_info?: Record<string, any>;
allergen_info?: Record<string, any>;
dietary_tags?: Record<string, any>;
batch_size_multiplier?: number;
minimum_batch_size?: number;
maximum_batch_size?: number;
optimal_production_temperature?: number;
optimal_humidity?: number;
quality_check_points?: Record<string, any>;
common_issues?: Record<string, any>;
status?: 'draft' | 'active' | 'testing' | 'archived' | 'discontinued';
is_seasonal?: boolean;
season_start_month?: number;
season_end_month?: number;
is_signature_item?: boolean;
target_margin_percentage?: number;
ingredients?: CreateRecipeIngredientRequest[];
}
export interface RecipeSearchParams {
search_term?: string;
status?: string;
category?: string;
is_seasonal?: boolean;
is_signature?: boolean;
difficulty_level?: number;
limit?: number;
offset?: number;
}
export interface RecipeFeasibility {
recipe_id: string;
recipe_name: string;
batch_multiplier: number;
feasible: boolean;
missing_ingredients: Array<{
ingredient_id: string;
ingredient_name: string;
required_quantity: number;
unit: string;
}>;
insufficient_ingredients: Array<{
ingredient_id: string;
ingredient_name: string;
required_quantity: number;
available_quantity: number;
unit: string;
}>;
}
export interface RecipeStatistics {
total_recipes: number;
active_recipes: number;
signature_recipes: number;
seasonal_recipes: number;
category_breakdown: Array<{
category: string;
count: number;
}>;
}
// Production Types
export interface ProductionBatch {
id: string;
tenant_id: string;
recipe_id: string;
batch_number: string;
production_date: string;
planned_start_time?: string;
actual_start_time?: string;
planned_end_time?: string;
actual_end_time?: string;
planned_quantity: number;
actual_quantity?: number;
yield_percentage?: number;
batch_size_multiplier: number;
status: 'planned' | 'in_progress' | 'completed' | 'failed' | 'cancelled';
priority: 'low' | 'normal' | 'high' | 'urgent';
assigned_staff?: string[];
production_notes?: string;
quality_score?: number;
quality_notes?: string;
defect_rate?: number;
rework_required: boolean;
planned_material_cost?: number;
actual_material_cost?: number;
labor_cost?: number;
overhead_cost?: number;
total_production_cost?: number;
cost_per_unit?: number;
production_temperature?: number;
production_humidity?: number;
oven_temperature?: number;
baking_time_minutes?: number;
waste_quantity: number;
waste_reason?: string;
efficiency_percentage?: number;
customer_order_reference?: string;
pre_order_quantity?: number;
shelf_quantity?: number;
created_at: string;
updated_at: string;
created_by?: string;
completed_by?: string;
ingredient_consumptions?: ProductionIngredientConsumption[];
}
export interface ProductionIngredientConsumption {
id: string;
tenant_id: string;
production_batch_id: string;
recipe_ingredient_id: string;
ingredient_id: string;
stock_id?: string;
planned_quantity: number;
actual_quantity: number;
unit: string;
variance_quantity?: number;
variance_percentage?: number;
unit_cost?: number;
total_cost?: number;
consumption_time: string;
consumption_notes?: string;
staff_member?: string;
ingredient_condition?: string;
quality_impact?: string;
substitution_used: boolean;
substitution_details?: string;
}
export interface CreateProductionBatchRequest {
recipe_id: string;
batch_number?: string;
production_date: string;
planned_start_time?: string;
planned_end_time?: string;
planned_quantity: number;
batch_size_multiplier?: number;
priority?: 'low' | 'normal' | 'high' | 'urgent';
assigned_staff?: string[];
production_notes?: string;
customer_order_reference?: string;
pre_order_quantity?: number;
shelf_quantity?: number;
}
export interface UpdateProductionBatchRequest {
batch_number?: string;
production_date?: string;
planned_start_time?: string;
actual_start_time?: string;
planned_end_time?: string;
actual_end_time?: string;
planned_quantity?: number;
actual_quantity?: number;
batch_size_multiplier?: number;
status?: 'planned' | 'in_progress' | 'completed' | 'failed' | 'cancelled';
priority?: 'low' | 'normal' | 'high' | 'urgent';
assigned_staff?: string[];
production_notes?: string;
quality_score?: number;
quality_notes?: string;
defect_rate?: number;
rework_required?: boolean;
labor_cost?: number;
overhead_cost?: number;
production_temperature?: number;
production_humidity?: number;
oven_temperature?: number;
baking_time_minutes?: number;
waste_quantity?: number;
waste_reason?: string;
customer_order_reference?: string;
pre_order_quantity?: number;
shelf_quantity?: number;
}
export interface ProductionBatchSearchParams {
search_term?: string;
status?: string;
priority?: string;
start_date?: string;
end_date?: string;
recipe_id?: string;
limit?: number;
offset?: number;
}
export interface ProductionStatistics {
total_batches: number;
completed_batches: number;
failed_batches: number;
success_rate: number;
average_yield_percentage: number;
average_quality_score: number;
total_production_cost: number;
status_breakdown: Array<{
status: string;
count: number;
}>;
}
export class RecipesService {
private baseUrl = '/api/recipes/v1';
// Recipe Management
async getRecipes(tenantId: string, params?: RecipeSearchParams): Promise<Recipe[]> {
const response = await apiClient.get<Recipe[]>(`${this.baseUrl}/recipes`, {
headers: { 'X-Tenant-ID': tenantId },
params
});
return response;
}
async getRecipe(tenantId: string, recipeId: string): Promise<Recipe> {
const response = await apiClient.get<Recipe>(`${this.baseUrl}/recipes/${recipeId}`, {
headers: { 'X-Tenant-ID': tenantId }
});
return response;
}
async createRecipe(tenantId: string, userId: string, data: CreateRecipeRequest): Promise<Recipe> {
const response = await apiClient.post<Recipe>(`${this.baseUrl}/recipes`, data, {
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
});
return response;
}
async updateRecipe(tenantId: string, userId: string, recipeId: string, data: UpdateRecipeRequest): Promise<Recipe> {
const response = await apiClient.put<Recipe>(`${this.baseUrl}/recipes/${recipeId}`, data, {
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
});
return response;
}
async deleteRecipe(tenantId: string, recipeId: string): Promise<void> {
await apiClient.delete(`${this.baseUrl}/recipes/${recipeId}`, {
headers: { 'X-Tenant-ID': tenantId }
});
}
async duplicateRecipe(tenantId: string, userId: string, recipeId: string, newName: string): Promise<Recipe> {
const response = await apiClient.post<Recipe>(`${this.baseUrl}/recipes/${recipeId}/duplicate`,
{ new_name: newName },
{
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
}
);
return response;
}
async activateRecipe(tenantId: string, userId: string, recipeId: string): Promise<Recipe> {
const response = await apiClient.post<Recipe>(`${this.baseUrl}/recipes/${recipeId}/activate`, {}, {
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
});
return response;
}
async checkRecipeFeasibility(tenantId: string, recipeId: string, batchMultiplier: number = 1.0): Promise<RecipeFeasibility> {
const response = await apiClient.get<RecipeFeasibility>(`${this.baseUrl}/recipes/${recipeId}/feasibility`, {
headers: { 'X-Tenant-ID': tenantId },
params: { batch_multiplier: batchMultiplier }
});
return response;
}
async getRecipeStatistics(tenantId: string): Promise<RecipeStatistics> {
const response = await apiClient.get<RecipeStatistics>(`${this.baseUrl}/recipes/statistics/dashboard`, {
headers: { 'X-Tenant-ID': tenantId }
});
return response;
}
async getRecipeCategories(tenantId: string): Promise<string[]> {
const response = await apiClient.get<{ categories: string[] }>(`${this.baseUrl}/recipes/categories/list`, {
headers: { 'X-Tenant-ID': tenantId }
});
return response.categories;
}
// Production Management
async getProductionBatches(tenantId: string, params?: ProductionBatchSearchParams): Promise<ProductionBatch[]> {
const response = await apiClient.get<ProductionBatch[]>(`${this.baseUrl}/production/batches`, {
headers: { 'X-Tenant-ID': tenantId },
params
});
return response;
}
async getProductionBatch(tenantId: string, batchId: string): Promise<ProductionBatch> {
const response = await apiClient.get<ProductionBatch>(`${this.baseUrl}/production/batches/${batchId}`, {
headers: { 'X-Tenant-ID': tenantId }
});
return response;
}
async createProductionBatch(tenantId: string, userId: string, data: CreateProductionBatchRequest): Promise<ProductionBatch> {
const response = await apiClient.post<ProductionBatch>(`${this.baseUrl}/production/batches`, data, {
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
});
return response;
}
async updateProductionBatch(tenantId: string, userId: string, batchId: string, data: UpdateProductionBatchRequest): Promise<ProductionBatch> {
const response = await apiClient.put<ProductionBatch>(`${this.baseUrl}/production/batches/${batchId}`, data, {
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
});
return response;
}
async deleteProductionBatch(tenantId: string, batchId: string): Promise<void> {
await apiClient.delete(`${this.baseUrl}/production/batches/${batchId}`, {
headers: { 'X-Tenant-ID': tenantId }
});
}
async getActiveProductionBatches(tenantId: string): Promise<ProductionBatch[]> {
const response = await apiClient.get<ProductionBatch[]>(`${this.baseUrl}/production/batches/active/list`, {
headers: { 'X-Tenant-ID': tenantId }
});
return response;
}
async startProductionBatch(tenantId: string, userId: string, batchId: string, data: {
staff_member?: string;
production_notes?: string;
ingredient_consumptions: Array<{
recipe_ingredient_id: string;
ingredient_id: string;
stock_id?: string;
planned_quantity: number;
actual_quantity: number;
unit: string;
consumption_notes?: string;
ingredient_condition?: string;
substitution_used?: boolean;
substitution_details?: string;
}>;
}): Promise<ProductionBatch> {
const response = await apiClient.post<ProductionBatch>(`${this.baseUrl}/production/batches/${batchId}/start`, data, {
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
});
return response;
}
async completeProductionBatch(tenantId: string, userId: string, batchId: string, data: {
actual_quantity: number;
quality_score?: number;
quality_notes?: string;
defect_rate?: number;
waste_quantity?: number;
waste_reason?: string;
production_notes?: string;
staff_member?: string;
}): Promise<ProductionBatch> {
const response = await apiClient.post<ProductionBatch>(`${this.baseUrl}/production/batches/${batchId}/complete`, data, {
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
});
return response;
}
async getProductionStatistics(tenantId: string, startDate?: string, endDate?: string): Promise<ProductionStatistics> {
const response = await apiClient.get<ProductionStatistics>(`${this.baseUrl}/production/statistics/dashboard`, {
headers: { 'X-Tenant-ID': tenantId },
params: { start_date: startDate, end_date: endDate }
});
return response;
}
}

View File

@@ -1,219 +0,0 @@
// frontend/src/api/services/sales.service.ts
/**
* Sales Data Service
* Handles sales data operations for the sales microservice
*/
import { apiClient } from '../client';
import { RequestTimeouts } from '../client/config';
import type {
SalesData,
SalesValidationResult,
SalesDataQuery,
SalesDataImport,
SalesImportResult,
DashboardStats,
PaginatedResponse,
ActivityItem,
} from '../types';
export class SalesService {
/**
* Upload Sales History File
*/
async uploadSalesHistory(
tenantId: string,
file: File,
additionalData?: Record<string, any>
): Promise<SalesImportResult> {
// Determine file format
const fileName = file.name.toLowerCase();
let fileFormat: string;
if (fileName.endsWith('.csv')) {
fileFormat = 'csv';
} else if (fileName.endsWith('.json')) {
fileFormat = 'json';
} else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
fileFormat = 'excel';
} else {
fileFormat = 'csv'; // Default fallback
}
const uploadData = {
file_format: fileFormat,
...additionalData,
};
return apiClient.upload(
`/tenants/${tenantId}/sales/import`,
file,
uploadData,
{
timeout: RequestTimeouts.LONG,
}
);
}
/**
* Validate Sales Data
*/
async validateSalesData(
tenantId: string,
file: File
): Promise<SalesValidationResult> {
const fileName = file.name.toLowerCase();
let fileFormat: string;
if (fileName.endsWith('.csv')) {
fileFormat = 'csv';
} else if (fileName.endsWith('.json')) {
fileFormat = 'json';
} else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
fileFormat = 'excel';
} else {
fileFormat = 'csv';
}
return apiClient.upload(
`/tenants/${tenantId}/sales/import/validate`,
file,
{
file_format: fileFormat,
validate_only: true,
source: 'onboarding_upload',
},
{
timeout: RequestTimeouts.MEDIUM,
}
);
}
/**
* Get Sales Data
*/
async getSalesData(
tenantId: string,
query?: SalesDataQuery
): Promise<PaginatedResponse<SalesData>> {
return apiClient.get(`/tenants/${tenantId}/sales`, { params: query });
}
/**
* Get Single Sales Record
*/
async getSalesRecord(tenantId: string, recordId: string): Promise<SalesData> {
return apiClient.get(`/tenants/${tenantId}/sales/${recordId}`);
}
/**
* Update Sales Record
*/
async updateSalesRecord(
tenantId: string,
recordId: string,
data: Partial<SalesData>
): Promise<SalesData> {
return apiClient.put(`/tenants/${tenantId}/sales/${recordId}`, data);
}
/**
* Delete Sales Record
*/
async deleteSalesRecord(tenantId: string, recordId: string): Promise<{ message: string }> {
return apiClient.delete(`/tenants/${tenantId}/sales/${recordId}`);
}
/**
* Get Dashboard Statistics
*/
async getDashboardStats(tenantId: string): Promise<DashboardStats> {
return apiClient.get(`/tenants/${tenantId}/sales/stats`);
}
/**
* Get Analytics Data
*/
async getAnalytics(
tenantId: string,
params?: {
start_date?: string;
end_date?: string;
inventory_product_ids?: string[]; // Primary way to filter by products
product_names?: string[]; // For backward compatibility - will need inventory service lookup
metrics?: string[];
}
): Promise<any> {
return apiClient.get(`/tenants/${tenantId}/sales/analytics`, { params });
}
/**
* Export Sales Data
*/
async exportSalesData(
tenantId: string,
format: 'csv' | 'excel' | 'json',
query?: SalesDataQuery
): Promise<Blob> {
const response = await apiClient.request(`/tenants/${tenantId}/sales/export`, {
method: 'GET',
params: { ...query, format },
headers: {
'Accept': format === 'csv' ? 'text/csv' :
format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
'application/json',
},
});
return new Blob([response], {
type: format === 'csv' ? 'text/csv' :
format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
'application/json',
});
}
/**
* Get Recent Activity
*/
async getRecentActivity(tenantId: string, limit?: number): Promise<ActivityItem[]> {
return apiClient.get(`/tenants/${tenantId}/sales/activity`, {
params: { limit },
});
}
/**
* Get Sales Summary by Period
*/
async getSalesSummary(
tenantId: string,
period: 'daily' | 'weekly' | 'monthly' = 'daily'
): Promise<any> {
return apiClient.get(`/tenants/${tenantId}/sales/summary`, {
params: { period }
});
}
/**
* Get Sales Analytics
*/
async getSalesAnalytics(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<{
total_revenue: number;
waste_reduction_percentage?: number;
forecast_accuracy?: number;
stockout_events?: number;
}> {
return apiClient.get(`/tenants/${tenantId}/sales/analytics/summary`, {
params: {
start_date: startDate,
end_date: endDate
}
});
}
}
export const salesService = new SalesService();

View File

@@ -1,475 +0,0 @@
// ================================================================
// frontend/src/api/services/suppliers.service.ts
// ================================================================
/**
* Suppliers Service - API client for Suppliers Service endpoints
*/
import { apiClient } from '../client';
// Supplier Types
export interface Supplier {
id: string;
tenant_id: string;
name: string;
contact_person?: string;
email?: string;
phone?: string;
address?: string;
supplier_type: 'ingredients' | 'packaging' | 'equipment' | 'services';
status: 'active' | 'inactive' | 'pending_approval';
payment_terms?: string;
lead_time_days: number;
minimum_order_value?: number;
delivery_areas: string[];
certifications: string[];
rating?: number;
notes?: string;
created_at: string;
updated_at: string;
}
export interface SupplierPerformance {
supplier_id: string;
supplier_name: string;
period_start: string;
period_end: string;
metrics: {
delivery_performance: {
on_time_delivery_rate: number;
average_delay_days: number;
total_deliveries: number;
};
quality_performance: {
quality_score: number;
defect_rate: number;
complaints_count: number;
returns_count: number;
};
cost_performance: {
price_competitiveness: number;
cost_savings: number;
invoice_accuracy: number;
};
service_performance: {
responsiveness_score: number;
communication_score: number;
flexibility_score: number;
};
};
overall_score: number;
performance_trend: 'improving' | 'stable' | 'declining';
risk_level: 'low' | 'medium' | 'high' | 'critical';
recommendations: string[];
}
// Additional types for hooks
export interface SupplierSummary {
id: string;
name: string;
supplier_type: string;
status: string;
rating?: number;
total_orders: number;
total_spent: number;
last_delivery_date?: string;
}
export interface CreateSupplierRequest {
name: string;
contact_person?: string;
email?: string;
phone?: string;
address?: string;
supplier_type: 'ingredients' | 'packaging' | 'equipment' | 'services';
payment_terms?: string;
lead_time_days: number;
minimum_order_value?: number;
delivery_areas: string[];
certifications?: string[];
notes?: string;
}
export interface UpdateSupplierRequest extends Partial<CreateSupplierRequest> {}
export interface SupplierSearchParams {
supplier_type?: string;
status?: string;
search?: string;
delivery_area?: string;
limit?: number;
offset?: number;
}
export interface SupplierStatistics {
total_suppliers: number;
active_suppliers: number;
average_rating: number;
top_performing_suppliers: SupplierSummary[];
}
export interface PurchaseOrder {
id: string;
supplier_id: string;
supplier_name: string;
order_number: string;
status: 'pending' | 'confirmed' | 'shipped' | 'delivered' | 'cancelled';
total_amount: number;
created_at: string;
expected_delivery: string;
}
export interface CreatePurchaseOrderRequest {
supplier_id: string;
items: Array<{
product_id: string;
quantity: number;
unit_price: number;
}>;
delivery_date: string;
notes?: string;
}
export interface PurchaseOrderSearchParams {
supplier_id?: string;
status?: string;
date_from?: string;
date_to?: string;
limit?: number;
offset?: number;
}
export interface PurchaseOrderStatistics {
total_orders: number;
total_value: number;
pending_orders: number;
overdue_orders: number;
}
export interface Delivery {
id: string;
purchase_order_id: string;
supplier_name: string;
delivered_at: string;
status: 'on_time' | 'late' | 'early';
quality_rating?: number;
}
export interface DeliverySearchParams {
supplier_id?: string;
status?: string;
date_from?: string;
date_to?: string;
limit?: number;
offset?: number;
}
export interface DeliveryPerformanceStats {
on_time_delivery_rate: number;
average_delay_days: number;
quality_average: number;
total_deliveries: number;
}
export interface SupplierDashboardData {
summary: {
total_suppliers: number;
active_suppliers: number;
pending_orders: number;
overdue_deliveries: number;
average_performance_score: number;
total_monthly_spend: number;
};
top_performers: SupplierPerformance[];
recent_orders: any[];
performance_trends: {
dates: string[];
delivery_rates: number[];
quality_scores: number[];
cost_savings: number[];
};
contract_expirations: { supplier_name: string; days_until_expiry: number }[];
}
export class SuppliersService {
private readonly basePath = '/suppliers';
// Dashboard
async getDashboardData(params?: {
date_from?: string;
date_to?: string;
}): Promise<SupplierDashboardData> {
return apiClient.get(`${this.basePath}/dashboard`, { params });
}
async getDashboardMetrics(params?: {
date_from?: string;
date_to?: string;
supplier_id?: string;
}): Promise<{
dates: string[];
delivery_performance: number[];
quality_scores: number[];
cost_savings: number[];
order_volumes: number[];
}> {
return apiClient.get(`${this.basePath}/dashboard/metrics`, { params });
}
// Suppliers
async getSuppliers(params?: {
supplier_type?: string;
status?: string;
search?: string;
delivery_area?: string;
limit?: number;
offset?: number;
}): Promise<Supplier[]> {
return apiClient.get(`${this.basePath}`, { params });
}
async getSupplier(supplierId: string): Promise<Supplier> {
return apiClient.get(`${this.basePath}/${supplierId}`);
}
async createSupplier(supplier: {
name: string;
contact_person?: string;
email?: string;
phone?: string;
address?: string;
supplier_type: 'ingredients' | 'packaging' | 'equipment' | 'services';
payment_terms?: string;
lead_time_days: number;
minimum_order_value?: number;
delivery_areas: string[];
certifications?: string[];
notes?: string;
}): Promise<Supplier> {
return apiClient.post(`${this.basePath}`, supplier);
}
async updateSupplier(supplierId: string, updates: Partial<Supplier>): Promise<Supplier> {
return apiClient.put(`${this.basePath}/${supplierId}`, updates);
}
async deleteSupplier(supplierId: string): Promise<void> {
return apiClient.delete(`${this.basePath}/${supplierId}`);
}
// Performance Management
async getSupplierPerformance(supplierId: string, params?: {
period_start?: string;
period_end?: string;
}): Promise<SupplierPerformance> {
return apiClient.get(`${this.basePath}/${supplierId}/performance`, { params });
}
async getAllSupplierPerformance(params?: {
period_start?: string;
period_end?: string;
min_score?: number;
risk_level?: string;
limit?: number;
offset?: number;
}): Promise<SupplierPerformance[]> {
return apiClient.get(`${this.basePath}/performance`, { params });
}
async updateSupplierRating(supplierId: string, rating: {
overall_rating: number;
delivery_rating: number;
quality_rating: number;
service_rating: number;
comments?: string;
}): Promise<void> {
return apiClient.post(`${this.basePath}/${supplierId}/rating`, rating);
}
// Analytics
async getCostAnalysis(params?: {
date_from?: string;
date_to?: string;
supplier_id?: string;
category?: string;
}): Promise<{
total_spend: number;
cost_by_supplier: { supplier_name: string; amount: number }[];
cost_by_category: { category: string; amount: number }[];
cost_trends: { date: string; amount: number }[];
cost_savings_opportunities: string[];
}> {
return apiClient.get(`${this.basePath}/analytics/costs`, { params });
}
async getSupplyChainRiskAnalysis(): Promise<{
high_risk_suppliers: {
supplier_id: string;
supplier_name: string;
risk_factors: string[];
risk_score: number;
}[];
diversification_analysis: {
category: string;
supplier_count: number;
concentration_risk: number;
}[];
recommendations: string[];
}> {
return apiClient.get(`${this.basePath}/analytics/risk-analysis`);
}
// Additional methods for hooks compatibility
async getSupplierStatistics(): Promise<SupplierStatistics> {
const suppliers = await this.getSuppliers();
const activeSuppliers = suppliers.filter(s => s.status === 'active');
const averageRating = suppliers.reduce((sum, s) => sum + (s.rating || 0), 0) / suppliers.length;
return {
total_suppliers: suppliers.length,
active_suppliers: activeSuppliers.length,
average_rating: averageRating,
top_performing_suppliers: suppliers.slice(0, 5).map(s => ({
id: s.id,
name: s.name,
supplier_type: s.supplier_type,
status: s.status,
rating: s.rating,
total_orders: 0, // Would come from backend
total_spent: 0, // Would come from backend
last_delivery_date: undefined
}))
};
}
async getActiveSuppliers(): Promise<SupplierSummary[]> {
const suppliers = await this.getSuppliers({ status: 'active' });
return suppliers.map(s => ({
id: s.id,
name: s.name,
supplier_type: s.supplier_type,
status: s.status,
rating: s.rating,
total_orders: 0, // Would come from backend
total_spent: 0, // Would come from backend
last_delivery_date: undefined
}));
}
async getTopSuppliers(): Promise<SupplierSummary[]> {
const suppliers = await this.getSuppliers();
return suppliers
.sort((a, b) => (b.rating || 0) - (a.rating || 0))
.slice(0, 10)
.map(s => ({
id: s.id,
name: s.name,
supplier_type: s.supplier_type,
status: s.status,
rating: s.rating,
total_orders: 0, // Would come from backend
total_spent: 0, // Would come from backend
last_delivery_date: undefined
}));
}
async getSuppliersNeedingReview(): Promise<SupplierSummary[]> {
const suppliers = await this.getSuppliers();
return suppliers
.filter(s => !s.rating || s.rating < 3)
.map(s => ({
id: s.id,
name: s.name,
supplier_type: s.supplier_type,
status: s.status,
rating: s.rating,
total_orders: 0, // Would come from backend
total_spent: 0, // Would come from backend
last_delivery_date: undefined
}));
}
// Purchase Order Management Methods
async getPurchaseOrders(params?: PurchaseOrderSearchParams): Promise<PurchaseOrder[]> {
return apiClient.get(`${this.basePath}/purchase-orders`, { params });
}
async getPurchaseOrder(orderId: string): Promise<PurchaseOrder> {
return apiClient.get(`${this.basePath}/purchase-orders/${orderId}`);
}
async createPurchaseOrder(orderData: CreatePurchaseOrderRequest): Promise<PurchaseOrder> {
return apiClient.post(`${this.basePath}/purchase-orders`, orderData);
}
async updatePurchaseOrderStatus(orderId: string, status: string): Promise<PurchaseOrder> {
return apiClient.put(`${this.basePath}/purchase-orders/${orderId}/status`, { status });
}
async approvePurchaseOrder(orderId: string, approval: any): Promise<void> {
return apiClient.post(`${this.basePath}/purchase-orders/${orderId}/approve`, approval);
}
async sendToSupplier(orderId: string): Promise<void> {
return apiClient.post(`${this.basePath}/purchase-orders/${orderId}/send`);
}
async cancelPurchaseOrder(orderId: string, reason?: string): Promise<void> {
return apiClient.post(`${this.basePath}/purchase-orders/${orderId}/cancel`, { reason });
}
async getPurchaseOrderStatistics(): Promise<PurchaseOrderStatistics> {
const orders = await this.getPurchaseOrders();
return {
total_orders: orders.length,
total_value: orders.reduce((sum, o) => sum + o.total_amount, 0),
pending_orders: orders.filter(o => o.status === 'pending').length,
overdue_orders: 0, // Would calculate based on expected delivery dates
};
}
async getOrdersRequiringApproval(): Promise<PurchaseOrder[]> {
return this.getPurchaseOrders({ status: 'pending' });
}
async getOverdueOrders(): Promise<PurchaseOrder[]> {
const today = new Date();
const orders = await this.getPurchaseOrders();
return orders.filter(o => new Date(o.expected_delivery) < today && o.status !== 'delivered');
}
// Delivery Management Methods
async getDeliveries(params?: DeliverySearchParams): Promise<Delivery[]> {
return apiClient.get(`${this.basePath}/deliveries`, { params });
}
async getDelivery(deliveryId: string): Promise<Delivery> {
return apiClient.get(`${this.basePath}/deliveries/${deliveryId}`);
}
async getTodaysDeliveries(): Promise<Delivery[]> {
const today = new Date().toISOString().split('T')[0];
return this.getDeliveries({ date_from: today, date_to: today });
}
async getDeliveryPerformanceStats(): Promise<DeliveryPerformanceStats> {
const deliveries = await this.getDeliveries();
const onTimeCount = deliveries.filter(d => d.status === 'on_time').length;
const totalCount = deliveries.length;
const qualitySum = deliveries.reduce((sum, d) => sum + (d.quality_rating || 0), 0);
return {
on_time_delivery_rate: totalCount > 0 ? (onTimeCount / totalCount) * 100 : 0,
average_delay_days: 0, // Would calculate based on actual vs expected delivery
quality_average: totalCount > 0 ? qualitySum / totalCount : 0,
total_deliveries: totalCount,
};
}
// Additional utility methods
async approveSupplier(supplierId: string): Promise<void> {
await this.updateSupplier(supplierId, { status: 'active' });
}
}

View File

@@ -1,150 +0,0 @@
// frontend/src/api/services/tenant.service.ts
/**
* Tenant Management Service
* Handles all tenant-related operations
*/
import { apiClient } from '../client';
import { serviceEndpoints } from '../client/config';
import type {
TenantInfo,
TenantCreate,
TenantUpdate,
TenantMember,
InviteUser,
TenantStats,
PaginatedResponse,
BaseQueryParams,
} from '../types';
export class TenantService {
private baseEndpoint = serviceEndpoints.tenant;
/**
* Create New Tenant
*/
async createTenant(data: TenantCreate): Promise<TenantInfo> {
return apiClient.post(`${this.baseEndpoint}/register`, data);
}
/**
* Get Tenant Details
*/
async getTenant(tenantId: string): Promise<TenantInfo> {
return apiClient.get(`${this.baseEndpoint}/${tenantId}`);
}
/**
* Update Tenant
*/
async updateTenant(tenantId: string, data: TenantUpdate): Promise<TenantInfo> {
return apiClient.put(`${this.baseEndpoint}/${tenantId}`, data);
}
/**
* Delete Tenant
*/
async deleteTenant(tenantId: string): Promise<{ message: string }> {
return apiClient.delete(`${this.baseEndpoint}/${tenantId}`);
}
/**
* Get Tenant Members
*/
async getTenantMembers(
tenantId: string,
params?: BaseQueryParams
): Promise<PaginatedResponse<TenantMember>> {
return apiClient.get(`${this.baseEndpoint}/${tenantId}/members`, { params });
}
/**
* Invite User to Tenant
*/
async inviteUser(tenantId: string, invitation: InviteUser): Promise<{ message: string }> {
return apiClient.post(`${this.baseEndpoint}/${tenantId}/invite`, invitation);
}
/**
* Remove Member from Tenant
*/
async removeMember(tenantId: string, userId: string): Promise<{ message: string }> {
return apiClient.delete(`${this.baseEndpoint}/${tenantId}/members/${userId}`);
}
/**
* Update Member Role
*/
async updateMemberRole(
tenantId: string,
userId: string,
role: string
): Promise<TenantMember> {
return apiClient.patch(`${this.baseEndpoint}/${tenantId}/members/${userId}`, { role });
}
/**
* Get Tenant Statistics
*/
async getTenantStats(tenantId: string): Promise<TenantStats> {
return apiClient.get(`${this.baseEndpoint}/${tenantId}/stats`);
}
/**
* Get User's Tenants - Get tenants where user is owner
*/
async getUserTenants(): Promise<TenantInfo[]> {
try {
// Extract user ID from the JWT token in localStorage
const token = localStorage.getItem('auth_token');
console.log('🔑 TenantService: Auth token present:', !!token);
if (!token) {
throw new Error('No auth token found');
}
// Decode JWT to get user ID (simple base64 decode)
const payload = JSON.parse(atob(token.split('.')[1]));
const userId = payload.user_id || payload.sub;
console.log('👤 TenantService: Extracted user ID:', userId);
if (!userId) {
throw new Error('No user ID found in token');
}
// Get tenants owned by this user
const url = `${this.baseEndpoint}/user/${userId}/owned`;
console.log('🌐 TenantService: Making request to:', url);
const result = await apiClient.get(url);
console.log('📦 TenantService: API response:', result);
console.log('📏 TenantService: Response length:', Array.isArray(result) ? result.length : 'Not an array');
// Ensure we always return an array
if (!Array.isArray(result)) {
console.warn('⚠️ TenantService: Response is not an array, converting...');
// If it's an object with numeric keys, convert to array
if (result && typeof result === 'object') {
const converted = Object.values(result);
console.log('🔄 TenantService: Converted to array:', converted);
return converted as TenantInfo[];
}
console.log('🔄 TenantService: Returning empty array');
return [];
}
if (result.length > 0) {
console.log('✅ TenantService: First tenant:', result[0]);
console.log('🆔 TenantService: First tenant ID:', result[0]?.id);
}
return result;
} catch (error) {
console.error('❌ TenantService: Failed to get user tenants:', error);
// Return empty array if API call fails
return [];
}
}
}
export const tenantService = new TenantService();

View File

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