Start integrating the onboarding flow with backend 6

This commit is contained in:
Urtzi Alfaro
2025-09-05 17:49:48 +02:00
parent 236c3a32ae
commit 069954981a
131 changed files with 5217 additions and 22838 deletions

View File

@@ -1,272 +0,0 @@
import { apiClient, ApiResponse } from './client';
import {
UserRegistration,
UserLogin,
UserData,
TokenResponse,
RefreshTokenRequest,
PasswordChange,
PasswordReset,
PasswordResetConfirm,
TokenVerification,
UserResponse,
UserUpdate,
OnboardingStatus,
OnboardingProgressRequest
} from '../../types/auth.types';
class AuthService {
private readonly baseUrl = '/auth';
// Authentication endpoints
async register(userData: UserRegistration): Promise<ApiResponse<TokenResponse>> {
const response = await apiClient.post<TokenResponse>(`${this.baseUrl}/register`, userData);
if (response.success && response.data) {
this.handleSuccessfulAuth(response.data);
}
return response;
}
async login(credentials: UserLogin): Promise<ApiResponse<TokenResponse>> {
const response = await apiClient.post<TokenResponse>(`${this.baseUrl}/login`, credentials);
if (response.success && response.data) {
this.handleSuccessfulAuth(response.data);
}
return response;
}
async logout(): Promise<ApiResponse<{ message: string; success: boolean }>> {
try {
const response = await apiClient.post(`${this.baseUrl}/logout`);
this.clearAuthData();
return response;
} catch (error) {
// Even if logout fails on server, clear local data
this.clearAuthData();
throw error;
}
}
async refreshToken(refreshToken?: string): Promise<ApiResponse<TokenResponse>> {
const token = refreshToken || localStorage.getItem('refresh_token');
if (!token) {
throw new Error('No refresh token available');
}
const response = await apiClient.post<TokenResponse>(`${this.baseUrl}/refresh`, {
refresh_token: token
});
if (response.success && response.data) {
this.handleSuccessfulAuth(response.data);
}
return response;
}
async verifyToken(token?: string): Promise<ApiResponse<TokenVerification>> {
const authToken = token || localStorage.getItem('access_token');
return apiClient.post(`${this.baseUrl}/verify`, { token: authToken });
}
// Password management
async changePassword(passwordData: PasswordChange): Promise<ApiResponse<{ message: string }>> {
return apiClient.post(`${this.baseUrl}/change-password`, passwordData);
}
async resetPassword(email: string): Promise<ApiResponse<{ message: string; reset_token?: string }>> {
return apiClient.post(`${this.baseUrl}/reset-password`, { email });
}
async confirmPasswordReset(data: PasswordResetConfirm): Promise<ApiResponse<{ message: string }>> {
return apiClient.post(`${this.baseUrl}/reset-password/confirm`, data);
}
// User management
async getCurrentUser(): Promise<ApiResponse<UserResponse>> {
return apiClient.get(`${this.baseUrl}/me`);
}
async updateProfile(userData: UserUpdate): Promise<ApiResponse<UserResponse>> {
return apiClient.patch(`${this.baseUrl}/me`, userData);
}
// Email verification
async sendEmailVerification(): Promise<ApiResponse<{ message: string }>> {
return apiClient.post(`${this.baseUrl}/verify-email`);
}
async confirmEmailVerification(token: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.post(`${this.baseUrl}/verify-email/confirm`, { token });
}
// Local auth state management - Now handled by Zustand store
private handleSuccessfulAuth(tokenData: TokenResponse) {
// Set auth token for API client
apiClient.setAuthToken(tokenData.access_token);
// Set tenant ID for API client if available
if (tokenData.user?.tenant_id) {
apiClient.setTenantId(tokenData.user.tenant_id);
}
}
private clearAuthData() {
// Clear API client tokens
apiClient.removeAuthToken();
}
// Utility methods - Now get data from Zustand store
isAuthenticated(): boolean {
const authStorage = localStorage.getItem('auth-storage');
if (!authStorage) return false;
try {
const { state } = JSON.parse(authStorage);
return state?.isAuthenticated || false;
} catch {
return false;
}
}
getCurrentUserData(): UserData | null {
const authStorage = localStorage.getItem('auth-storage');
if (!authStorage) return null;
try {
const { state } = JSON.parse(authStorage);
return state?.user || null;
} catch {
return null;
}
}
getAccessToken(): string | null {
const authStorage = localStorage.getItem('auth-storage');
if (!authStorage) return null;
try {
const { state } = JSON.parse(authStorage);
return state?.token || null;
} catch {
return null;
}
}
getRefreshToken(): string | null {
const authStorage = localStorage.getItem('auth-storage');
if (!authStorage) return null;
try {
const { state } = JSON.parse(authStorage);
return state?.refreshToken || null;
} catch {
return null;
}
}
getTenantId(): string | null {
const userData = this.getCurrentUserData();
return userData?.tenant_id || null;
}
// Check if token is expired (basic check)
isTokenExpired(): boolean {
const token = this.getAccessToken();
if (!token) return true;
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const currentTime = Date.now() / 1000;
return payload.exp < currentTime;
} catch {
return true;
}
}
// Auto-refresh token if needed
async ensureValidToken(): Promise<boolean> {
if (!this.isAuthenticated()) {
return false;
}
if (this.isTokenExpired()) {
try {
await this.refreshToken();
return true;
} catch {
this.clearAuthData();
return false;
}
}
return true;
}
// Onboarding progress tracking (moved from onboarding)
async checkOnboardingStatus(): Promise<ApiResponse<OnboardingStatus>> {
try {
// Use the /me endpoint which gets proxied to auth service
const response = await apiClient.get<any>('/me');
if (response.success && response.data) {
// Extract onboarding status from user profile
const onboardingStatus = {
completed: response.data.onboarding_completed || false,
steps_completed: response.data.completed_steps || []
};
return {
success: true,
data: onboardingStatus,
message: 'Onboarding status retrieved successfully'
};
}
return {
success: false,
data: { completed: false, steps_completed: [] },
message: 'Could not retrieve onboarding status',
error: 'Invalid response data'
};
} catch (error) {
console.warn('Could not check onboarding status:', error);
return {
success: false,
data: { completed: false, steps_completed: [] },
message: 'Could not retrieve onboarding status',
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
async completeOnboarding(metadata?: any): Promise<ApiResponse<{ message: string }>> {
try {
// Update user profile to mark onboarding as complete
const response = await apiClient.patch<any>('/me', {
onboarding_completed: true,
completed_steps: ['setup', 'data-processing', 'review', 'inventory', 'suppliers', 'ml-training', 'completion'],
onboarding_metadata: metadata
});
if (response.success) {
return {
success: true,
data: { message: 'Onboarding completed successfully' },
message: 'Onboarding marked as complete'
};
}
return response;
} catch (error) {
console.warn('Could not mark onboarding as complete:', error);
return {
success: false,
data: { message: 'Failed to complete onboarding' },
message: 'Could not complete onboarding',
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
}
export const authService = new AuthService();

View File

@@ -1,277 +0,0 @@
import axios, { AxiosInstance, AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios';
import { ApiResponse, ApiError } from '../../types/api.types';
// Utility functions to access auth and tenant store data from localStorage
const getAuthData = () => {
const authStorage = localStorage.getItem('auth-storage');
if (!authStorage) return null;
try {
const { state } = JSON.parse(authStorage);
return state;
} catch {
return null;
}
};
const getTenantData = () => {
const tenantStorage = localStorage.getItem('tenant-storage');
if (!tenantStorage) return null;
try {
const { state } = JSON.parse(tenantStorage);
return state;
} catch {
return null;
}
};
const clearAuthData = () => {
localStorage.removeItem('auth-storage');
};
// Client-specific error interface
interface ClientError {
success: boolean;
error: {
message: string;
code?: string;
};
timestamp: string;
}
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api/v1';
class ApiClient {
private axiosInstance: AxiosInstance;
private refreshTokenPromise: Promise<string> | null = null;
constructor() {
this.axiosInstance = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
// Helper method to build tenant-scoped URLs
private buildTenantUrl(path: string): string {
// If path already starts with /tenants, return as-is
if (path.startsWith('/tenants/')) {
return path;
}
// If it's an auth endpoint, return as-is
if (path.startsWith('/auth')) {
return path;
}
// Get tenant ID from stores
const tenantData = getTenantData();
const authData = getAuthData();
const tenantId = tenantData?.currentTenant?.id || authData?.user?.tenant_id;
if (!tenantId) {
throw new Error('Tenant ID not available for API call');
}
// Build tenant-scoped URL: /tenants/{tenant-id}{original-path}
return `/tenants/${tenantId}${path}`;
}
private setupInterceptors(): void {
// Request interceptor - add auth token and tenant ID
this.axiosInstance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const authData = getAuthData();
if (authData?.token) {
config.headers.Authorization = `Bearer ${authData.token}`;
}
// Get tenant ID from tenant store (priority) or fallback to user's tenant_id
const tenantData = getTenantData();
const tenantId = tenantData?.currentTenant?.id || authData?.user?.tenant_id;
if (tenantId) {
config.headers['X-Tenant-ID'] = tenantId;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor - handle common responses and errors
this.axiosInstance.interceptors.response.use(
(response: AxiosResponse) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
// Handle 401 - Token expired
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const authData = getAuthData();
if (authData?.refreshToken) {
const newToken = await this.refreshToken();
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return this.axiosInstance(originalRequest);
}
} catch (refreshError) {
this.handleAuthFailure();
return Promise.reject(refreshError);
}
}
// Handle 403 - Forbidden
if (error.response?.status === 403) {
console.warn('Access denied - insufficient permissions');
}
// Handle network errors
if (!error.response) {
const networkError: ClientError = {
success: false,
error: {
message: 'Network error - please check your connection',
code: 'NETWORK_ERROR'
},
timestamp: new Date().toISOString()
};
return Promise.reject(networkError);
}
return Promise.reject(error);
}
);
}
private async refreshToken(): Promise<string> {
if (!this.refreshTokenPromise) {
this.refreshTokenPromise = this.performTokenRefresh();
}
const token = await this.refreshTokenPromise;
this.refreshTokenPromise = null;
return token;
}
private async performTokenRefresh(): Promise<string> {
const authData = getAuthData();
if (!authData?.refreshToken) {
throw new Error('No refresh token available');
}
const response = await axios.post(`${API_BASE_URL}/auth/refresh`, {
refresh_token: authData.refreshToken,
});
const { access_token, refresh_token } = response.data;
// Update the Zustand store by modifying the auth-storage directly
const newAuthData = {
...authData,
token: access_token,
refreshToken: refresh_token || authData.refreshToken
};
localStorage.setItem('auth-storage', JSON.stringify({
state: newAuthData,
version: 0
}));
return access_token;
}
private handleAuthFailure() {
clearAuthData();
// Redirect to login
window.location.href = '/login';
}
// HTTP Methods with consistent response format and automatic tenant scoping
async get<T = any>(url: string, config = {}): Promise<ApiResponse<T>> {
const tenantScopedUrl = this.buildTenantUrl(url);
const response = await this.axiosInstance.get(tenantScopedUrl, config);
return this.transformResponse(response);
}
async post<T = any>(url: string, data = {}, config = {}): Promise<ApiResponse<T>> {
const tenantScopedUrl = this.buildTenantUrl(url);
const response = await this.axiosInstance.post(tenantScopedUrl, data, config);
return this.transformResponse(response);
}
async put<T = any>(url: string, data = {}, config = {}): Promise<ApiResponse<T>> {
const tenantScopedUrl = this.buildTenantUrl(url);
const response = await this.axiosInstance.put(tenantScopedUrl, data, config);
return this.transformResponse(response);
}
async patch<T = any>(url: string, data = {}, config = {}): Promise<ApiResponse<T>> {
const tenantScopedUrl = this.buildTenantUrl(url);
const response = await this.axiosInstance.patch(tenantScopedUrl, data, config);
return this.transformResponse(response);
}
async delete<T = any>(url: string, config = {}): Promise<ApiResponse<T>> {
const tenantScopedUrl = this.buildTenantUrl(url);
const response = await this.axiosInstance.delete(tenantScopedUrl, config);
return this.transformResponse(response);
}
// File upload helper with automatic tenant scoping
async uploadFile<T = any>(url: string, file: File, progressCallback?: (progress: number) => void): Promise<ApiResponse<T>> {
const formData = new FormData();
formData.append('file', file);
const config = {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent: any) => {
if (progressCallback && progressEvent.total) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
progressCallback(progress);
}
},
};
const tenantScopedUrl = this.buildTenantUrl(url);
const response = await this.axiosInstance.post(tenantScopedUrl, formData, config);
return this.transformResponse(response);
}
// Transform response to consistent format
private transformResponse<T>(response: AxiosResponse): ApiResponse<T> {
return {
data: response.data,
success: response.status >= 200 && response.status < 300,
message: response.statusText,
};
}
// Utility methods - Now work with Zustand store
setAuthToken(token: string) {
this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${token}`;
}
removeAuthToken() {
delete this.axiosInstance.defaults.headers.common['Authorization'];
}
setTenantId(tenantId: string) {
this.axiosInstance.defaults.headers.common['X-Tenant-ID'] = tenantId;
}
getBaseURL(): string {
return API_BASE_URL;
}
}
export { ApiClient };
export const apiClient = new ApiClient();

View File

@@ -1,246 +0,0 @@
import { apiClient, ApiResponse } from './client';
import {
WeatherData,
WeatherDataParams,
TrafficData,
TrafficDataParams,
TrafficPatternsParams,
TrafficPattern,
EventData,
EventsParams,
CustomEventCreate,
LocationConfig,
LocationCreate,
ExternalFactorsImpact,
ExternalFactorsParams,
DataQualityReport,
DataSettings,
DataSettingsUpdate,
RefreshDataResponse,
DeleteResponse,
WeatherCondition,
EventType,
RefreshInterval
} from '../../types/data.types';
class DataService {
private readonly baseUrl = '/data';
// Location management
async getLocations(): Promise<ApiResponse<LocationConfig[]>> {
return apiClient.get(`${this.baseUrl}/locations`);
}
async getLocation(locationId: string): Promise<ApiResponse<LocationConfig>> {
return apiClient.get(`${this.baseUrl}/locations/${locationId}`);
}
async createLocation(locationData: LocationCreate): Promise<ApiResponse<LocationConfig>> {
return apiClient.post(`${this.baseUrl}/locations`, locationData);
}
async updateLocation(locationId: string, locationData: Partial<LocationConfig>): Promise<ApiResponse<LocationConfig>> {
return apiClient.put(`${this.baseUrl}/locations/${locationId}`, locationData);
}
async deleteLocation(locationId: string): Promise<ApiResponse<DeleteResponse>> {
return apiClient.delete(`${this.baseUrl}/locations/${locationId}`);
}
// Weather data
async getWeatherData(params?: WeatherDataParams): Promise<ApiResponse<{ items: WeatherData[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/weather?${queryParams.toString()}`
: `${this.baseUrl}/weather`;
return apiClient.get(url);
}
async getCurrentWeather(locationId: string): Promise<ApiResponse<WeatherData>> {
return apiClient.get(`${this.baseUrl}/weather/current/${locationId}`);
}
async getWeatherForecast(locationId: string, days: number = 7): Promise<ApiResponse<WeatherData[]>> {
return apiClient.get(`${this.baseUrl}/weather/forecast/${locationId}?days=${days}`);
}
async refreshWeatherData(locationId?: string): Promise<ApiResponse<RefreshDataResponse>> {
const url = locationId
? `${this.baseUrl}/weather/refresh/${locationId}`
: `${this.baseUrl}/weather/refresh`;
return apiClient.post(url);
}
// Traffic data
async getTrafficData(params?: TrafficDataParams): Promise<ApiResponse<{ items: TrafficData[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/traffic?${queryParams.toString()}`
: `${this.baseUrl}/traffic`;
return apiClient.get(url);
}
async getCurrentTraffic(locationId: string): Promise<ApiResponse<TrafficData>> {
return apiClient.get(`${this.baseUrl}/traffic/current/${locationId}`);
}
async getTrafficPatterns(locationId: string, params?: TrafficPatternsParams): Promise<ApiResponse<TrafficPattern[]>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/traffic/patterns/${locationId}?${queryParams.toString()}`
: `${this.baseUrl}/traffic/patterns/${locationId}`;
return apiClient.get(url);
}
async refreshTrafficData(locationId?: string): Promise<ApiResponse<RefreshDataResponse>> {
const url = locationId
? `${this.baseUrl}/traffic/refresh/${locationId}`
: `${this.baseUrl}/traffic/refresh`;
return apiClient.post(url);
}
// Events data
async getEvents(params?: EventsParams): Promise<ApiResponse<{ items: EventData[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/events?${queryParams.toString()}`
: `${this.baseUrl}/events`;
return apiClient.get(url);
}
async getUpcomingEvents(locationId: string, days: number = 30): Promise<ApiResponse<EventData[]>> {
return apiClient.get(`${this.baseUrl}/events/upcoming/${locationId}?days=${days}`);
}
async createCustomEvent(eventData: CustomEventCreate): Promise<ApiResponse<EventData>> {
return apiClient.post(`${this.baseUrl}/events`, eventData);
}
async updateEvent(eventId: string, eventData: Partial<EventData>): Promise<ApiResponse<EventData>> {
return apiClient.put(`${this.baseUrl}/events/${eventId}`, eventData);
}
async deleteEvent(eventId: string): Promise<ApiResponse<DeleteResponse>> {
return apiClient.delete(`${this.baseUrl}/events/${eventId}`);
}
async refreshEventsData(locationId?: string): Promise<ApiResponse<RefreshDataResponse>> {
const url = locationId
? `${this.baseUrl}/events/refresh/${locationId}`
: `${this.baseUrl}/events/refresh`;
return apiClient.post(url);
}
// Combined analytics
async getExternalFactorsImpact(params?: ExternalFactorsParams): Promise<ApiResponse<ExternalFactorsImpact>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/impact-analysis?${queryParams.toString()}`
: `${this.baseUrl}/impact-analysis`;
return apiClient.get(url);
}
async getDataQualityReport(): Promise<ApiResponse<DataQualityReport>> {
return apiClient.get(`${this.baseUrl}/quality-report`);
}
// Data configuration
async getDataSettings(): Promise<ApiResponse<DataSettings>> {
return apiClient.get(`${this.baseUrl}/settings`);
}
async updateDataSettings(settings: DataSettingsUpdate): Promise<ApiResponse<DataSettings>> {
return apiClient.put(`${this.baseUrl}/settings`, settings);
}
// Utility methods
getWeatherConditions(): WeatherCondition[] {
return [
{ value: 'sunny', label: 'Sunny', impact: 'positive' },
{ value: 'cloudy', label: 'Cloudy', impact: 'neutral' },
{ value: 'rainy', label: 'Rainy', impact: 'negative' },
{ value: 'stormy', label: 'Stormy', impact: 'negative' },
{ value: 'snowy', label: 'Snowy', impact: 'negative' },
{ value: 'foggy', label: 'Foggy', impact: 'negative' },
];
}
getEventTypes(): EventType[] {
return [
{ value: 'festival', label: 'Festival', typical_impact: 'positive' },
{ value: 'concert', label: 'Concert', typical_impact: 'positive' },
{ value: 'sports_event', label: 'Sports Event', typical_impact: 'positive' },
{ value: 'conference', label: 'Conference', typical_impact: 'positive' },
{ value: 'construction', label: 'Construction', typical_impact: 'negative' },
{ value: 'roadwork', label: 'Road Work', typical_impact: 'negative' },
{ value: 'protest', label: 'Protest', typical_impact: 'negative' },
{ value: 'holiday', label: 'Holiday', typical_impact: 'neutral' },
];
}
getRefreshIntervals(): RefreshInterval[] {
return [
{ value: 5, label: '5 minutes', suitable_for: ['traffic'] },
{ value: 15, label: '15 minutes', suitable_for: ['traffic'] },
{ value: 30, label: '30 minutes', suitable_for: ['weather', 'traffic'] },
{ value: 60, label: '1 hour', suitable_for: ['weather'] },
{ value: 240, label: '4 hours', suitable_for: ['weather'] },
{ value: 1440, label: '24 hours', suitable_for: ['events'] },
];
}
}
export const dataService = new DataService();

View File

@@ -1,268 +0,0 @@
import { apiClient, ApiResponse } from './client';
import {
ForecastRequest,
ForecastResponse,
PredictionBatch,
ModelPerformance
} from '../../types/forecasting.types';
class ForecastingService {
private readonly baseUrl = '/forecasting';
// Forecast generation
async createForecast(forecastData: ForecastRequest): Promise<ApiResponse<ForecastResponse[]>> {
return apiClient.post(`${this.baseUrl}/forecasts`, forecastData);
}
async getForecasts(params?: {
page?: number;
size?: number;
product_name?: string;
start_date?: string;
end_date?: string;
}): Promise<ApiResponse<{ items: ForecastResponse[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/forecasts?${queryParams.toString()}`
: `${this.baseUrl}/forecasts`;
return apiClient.get(url);
}
async getForecast(forecastId: string): Promise<ApiResponse<ForecastResponse>> {
return apiClient.get(`${this.baseUrl}/forecasts/${forecastId}`);
}
async updateForecastActual(forecastId: string, actualDemand: number): Promise<ApiResponse<ForecastResponse>> {
return apiClient.patch(`${this.baseUrl}/forecasts/${forecastId}`, {
actual_demand: actualDemand
});
}
// Batch predictions
async createPredictionBatch(batchData: {
name: string;
description?: string;
products: string[];
days_ahead: number;
parameters?: Record<string, any>;
}): Promise<ApiResponse<PredictionBatch>> {
return apiClient.post(`${this.baseUrl}/batches`, batchData);
}
async getPredictionBatches(): Promise<ApiResponse<PredictionBatch[]>> {
return apiClient.get(`${this.baseUrl}/batches`);
}
async getPredictionBatch(batchId: string): Promise<ApiResponse<PredictionBatch>> {
return apiClient.get(`${this.baseUrl}/batches/${batchId}`);
}
async getPredictionBatchResults(batchId: string): Promise<ApiResponse<ForecastResponse[]>> {
return apiClient.get(`${this.baseUrl}/batches/${batchId}/results`);
}
// Model performance and metrics
async getModelPerformance(): Promise<ApiResponse<ModelPerformance[]>> {
return apiClient.get(`${this.baseUrl}/models/performance`);
}
async getAccuracyReport(params?: {
start_date?: string;
end_date?: string;
product_name?: string;
}): Promise<ApiResponse<{
overall_accuracy: number;
product_accuracy: Array<{
product_name: string;
accuracy: number;
total_predictions: number;
recent_trend: 'improving' | 'stable' | 'declining';
}>;
accuracy_trends: Array<{
date: string;
accuracy: number;
}>;
}>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/accuracy?${queryParams.toString()}`
: `${this.baseUrl}/accuracy`;
return apiClient.get(url);
}
// Demand analytics
async getDemandTrends(params?: {
product_name?: string;
start_date?: string;
end_date?: string;
granularity?: 'daily' | 'weekly' | 'monthly';
}): Promise<ApiResponse<Array<{
date: string;
actual_demand: number;
predicted_demand: number;
confidence_lower: number;
confidence_upper: number;
accuracy: number;
}>>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/trends?${queryParams.toString()}`
: `${this.baseUrl}/trends`;
return apiClient.get(url);
}
async getSeasonalPatterns(productName?: string): Promise<ApiResponse<{
product_name: string;
seasonal_components: Array<{
period: 'monthly' | 'weekly' | 'daily';
strength: number;
pattern: number[];
}>;
holiday_effects: Array<{
holiday: string;
impact_factor: number;
confidence: number;
}>;
}>> {
const url = productName
? `${this.baseUrl}/patterns?product_name=${encodeURIComponent(productName)}`
: `${this.baseUrl}/patterns`;
return apiClient.get(url);
}
// External factors impact
async getWeatherImpact(params?: {
product_name?: string;
weather_conditions?: string[];
}): Promise<ApiResponse<Array<{
weather_condition: string;
impact_factor: number;
confidence: number;
affected_products: string[];
}>>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
if (Array.isArray(value)) {
value.forEach(v => queryParams.append(key, v));
} else {
queryParams.append(key, value.toString());
}
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/weather-impact?${queryParams.toString()}`
: `${this.baseUrl}/weather-impact`;
return apiClient.get(url);
}
// Model management
async retrainModel(params?: {
model_type?: string;
data_range?: { start_date: string; end_date: string };
hyperparameters?: Record<string, any>;
}): Promise<ApiResponse<{ task_id: string; message: string }>> {
return apiClient.post(`${this.baseUrl}/models/retrain`, params);
}
async getTrainingStatus(taskId: string): Promise<ApiResponse<{
status: 'pending' | 'training' | 'completed' | 'failed';
progress: number;
message: string;
results?: {
accuracy_improvement: number;
training_time: number;
model_size: number;
};
}>> {
return apiClient.get(`${this.baseUrl}/models/training-status/${taskId}`);
}
// Configuration
async getForecastingSettings(): Promise<ApiResponse<{
default_forecast_horizon: number;
confidence_level: number;
retraining_frequency: number;
external_data_sources: string[];
notification_preferences: Record<string, boolean>;
}>> {
return apiClient.get(`${this.baseUrl}/settings`);
}
async updateForecastingSettings(settings: {
default_forecast_horizon?: number;
confidence_level?: number;
retraining_frequency?: number;
external_data_sources?: string[];
notification_preferences?: Record<string, boolean>;
}): Promise<ApiResponse<any>> {
return apiClient.put(`${this.baseUrl}/settings`, settings);
}
// Utility methods
getConfidenceLevels(): { value: number; label: string }[] {
return [
{ value: 0.80, label: '80%' },
{ value: 0.90, label: '90%' },
{ value: 0.95, label: '95%' },
{ value: 0.99, label: '99%' },
];
}
getForecastHorizons(): { value: number; label: string }[] {
return [
{ value: 1, label: '1 day' },
{ value: 7, label: '1 week' },
{ value: 14, label: '2 weeks' },
{ value: 30, label: '1 month' },
{ value: 90, label: '3 months' },
];
}
getGranularityOptions(): { value: string; label: string }[] {
return [
{ value: 'daily', label: 'Daily' },
{ value: 'weekly', label: 'Weekly' },
{ value: 'monthly', label: 'Monthly' },
];
}
}
export { ForecastingService };
export const forecastingService = new ForecastingService();

View File

@@ -1,35 +0,0 @@
// Export API client and types
export * from './client';
export { ApiClient } from './client';
// Export all services
export * from './auth.service';
export * from './tenant.service';
export * from './inventory.service';
export * from './production.service';
export * from './sales.service';
export * from './forecasting.service';
export * from './training.service';
export * from './orders.service';
export * from './procurement.service';
export * from './pos.service';
export * from './data.service';
export * from './notification.service';
export * from './subscription.service';
// Service instances for easy importing
export { authService } from './auth.service';
export { tenantService } from './tenant.service';
export { inventoryService } from './inventory.service';
export { productionService } from './production.service';
export { salesService } from './sales.service';
export { forecastingService } from './forecasting.service';
export { ordersService } from './orders.service';
export { procurementService } from './procurement.service';
export { posService } from './pos.service';
export { dataService } from './data.service';
export { notificationService } from './notification.service';
export { subscriptionService } from './subscription.service';
// API client instance
export { apiClient } from './client';

View File

@@ -1,485 +0,0 @@
import { apiClient } from './client';
import { ApiResponse } from '../../types/api.types';
import {
UnitOfMeasure,
ProductType,
StockMovementType,
Ingredient,
Stock,
StockMovement,
StockAlert,
InventorySummary,
StockLevelSummary,
ProductSuggestion,
ProductSuggestionsResponse,
InventoryCreationResponse,
BatchClassificationRequest
} from '../../types/inventory.types';
import { PaginatedResponse } from '../../types/api.types';
// Service-specific types for Create/Update operations
interface IngredientCreate {
name: string;
product_type?: ProductType;
sku?: string;
barcode?: string;
category?: string;
subcategory?: string;
description?: string;
brand?: string;
unit_of_measure: UnitOfMeasure;
package_size?: number;
average_cost?: number;
standard_cost?: number;
low_stock_threshold?: number;
reorder_point?: number;
reorder_quantity?: number;
max_stock_level?: number;
requires_refrigeration?: boolean;
requires_freezing?: boolean;
storage_temperature_min?: number;
storage_temperature_max?: number;
storage_humidity_max?: number;
shelf_life_days?: number;
storage_instructions?: string;
is_perishable?: boolean;
allergen_info?: Record<string, any>;
}
interface IngredientUpdate extends Partial<IngredientCreate> {
is_active?: boolean;
}
interface StockCreate {
ingredient_id: string;
batch_number?: string;
lot_number?: string;
supplier_batch_ref?: string;
current_quantity: number;
received_date?: string;
expiration_date?: string;
best_before_date?: string;
unit_cost?: number;
storage_location?: string;
warehouse_zone?: string;
shelf_position?: string;
quality_status?: string;
}
interface StockUpdate extends Partial<StockCreate> {
reserved_quantity?: number;
is_available?: boolean;
}
interface StockMovementCreate {
ingredient_id: string;
stock_id?: string;
movement_type: StockMovementType;
quantity: number;
unit_cost?: number;
reference_number?: string;
supplier_id?: string;
notes?: string;
reason_code?: string;
movement_date?: string;
}
// Type aliases for response consistency
type IngredientResponse = Ingredient;
type StockResponse = Stock;
type StockMovementResponse = StockMovement;
type StockAlertResponse = StockAlert;
class InventoryService {
private readonly baseUrl = '/inventory';
// Ingredient management
async getIngredients(params?: {
page?: number;
size?: number;
category?: string;
is_active?: boolean;
is_low_stock?: boolean;
needs_reorder?: boolean;
search?: string;
}): Promise<ApiResponse<PaginatedResponse<IngredientResponse>>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/ingredients?${queryParams.toString()}`
: `${this.baseUrl}/ingredients`;
return apiClient.get(url);
}
async getIngredient(ingredientId: string): Promise<ApiResponse<IngredientResponse>> {
return apiClient.get(`${this.baseUrl}/ingredients/${ingredientId}`);
}
async createIngredient(ingredientData: IngredientCreate): Promise<ApiResponse<IngredientResponse>> {
return apiClient.post(`${this.baseUrl}/ingredients`, ingredientData);
}
async updateIngredient(ingredientId: string, ingredientData: IngredientUpdate): Promise<ApiResponse<IngredientResponse>> {
return apiClient.put(`${this.baseUrl}/ingredients/${ingredientId}`, ingredientData);
}
async deleteIngredient(ingredientId: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.delete(`${this.baseUrl}/ingredients/${ingredientId}`);
}
// Stock management
async getStock(params?: {
page?: number;
size?: number;
ingredient_id?: string;
is_available?: boolean;
is_expired?: boolean;
expiring_within_days?: number;
storage_location?: string;
quality_status?: string;
}): Promise<ApiResponse<PaginatedResponse<StockResponse>>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/stock?${queryParams.toString()}`
: `${this.baseUrl}/stock`;
return apiClient.get(url);
}
async getStockById(stockId: string): Promise<ApiResponse<StockResponse>> {
return apiClient.get(`${this.baseUrl}/stock/${stockId}`);
}
async getIngredientStock(ingredientId: string): Promise<ApiResponse<StockResponse[]>> {
return apiClient.get(`${this.baseUrl}/ingredients/${ingredientId}/stock`);
}
async createStock(stockData: StockCreate): Promise<ApiResponse<StockResponse>> {
return apiClient.post(`${this.baseUrl}/stock`, stockData);
}
async updateStock(stockId: string, stockData: StockUpdate): Promise<ApiResponse<StockResponse>> {
return apiClient.put(`${this.baseUrl}/stock/${stockId}`, stockData);
}
async deleteStock(stockId: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.delete(`${this.baseUrl}/stock/${stockId}`);
}
// Stock movements
async getStockMovements(params?: {
page?: number;
size?: number;
ingredient_id?: string;
movement_type?: StockMovementType;
start_date?: string;
end_date?: string;
}): Promise<ApiResponse<PaginatedResponse<StockMovementResponse>>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/movements?${queryParams.toString()}`
: `${this.baseUrl}/movements`;
return apiClient.get(url);
}
async createStockMovement(movementData: StockMovementCreate): Promise<ApiResponse<StockMovementResponse>> {
return apiClient.post(`${this.baseUrl}/movements`, movementData);
}
async getIngredientMovements(ingredientId: string, params?: {
page?: number;
size?: number;
movement_type?: StockMovementType;
start_date?: string;
end_date?: string;
}): Promise<ApiResponse<PaginatedResponse<StockMovementResponse>>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/ingredients/${ingredientId}/movements?${queryParams.toString()}`
: `${this.baseUrl}/ingredients/${ingredientId}/movements`;
return apiClient.get(url);
}
// Alerts and notifications
async getStockAlerts(params?: {
page?: number;
size?: number;
alert_type?: string;
severity?: string;
is_active?: boolean;
is_acknowledged?: boolean;
is_resolved?: boolean;
}): Promise<ApiResponse<PaginatedResponse<StockAlertResponse>>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/alerts?${queryParams.toString()}`
: `${this.baseUrl}/alerts`;
return apiClient.get(url);
}
async acknowledgeAlert(alertId: string): Promise<ApiResponse<StockAlertResponse>> {
return apiClient.post(`${this.baseUrl}/alerts/${alertId}/acknowledge`);
}
async resolveAlert(alertId: string, resolutionNotes?: string): Promise<ApiResponse<StockAlertResponse>> {
return apiClient.post(`${this.baseUrl}/alerts/${alertId}/resolve`, {
resolution_notes: resolutionNotes
});
}
// Dashboard and summaries
async getInventorySummary(): Promise<ApiResponse<InventorySummary>> {
return apiClient.get(`${this.baseUrl}/summary`);
}
async getStockLevels(): Promise<ApiResponse<StockLevelSummary[]>> {
return apiClient.get(`${this.baseUrl}/stock-levels`);
}
async getLowStockItems(): Promise<ApiResponse<IngredientResponse[]>> {
return apiClient.get(`${this.baseUrl}/low-stock`);
}
async getExpiringItems(days: number = 7): Promise<ApiResponse<StockResponse[]>> {
return apiClient.get(`${this.baseUrl}/expiring?days=${days}`);
}
async getExpiredItems(): Promise<ApiResponse<StockResponse[]>> {
return apiClient.get(`${this.baseUrl}/expired`);
}
// Classification and categorization
async classifyProduct(name: string, description?: string): Promise<ApiResponse<{
category: string;
subcategory: string;
confidence: number;
suggested_unit: UnitOfMeasure;
is_perishable: boolean;
storage_requirements: {
requires_refrigeration: boolean;
requires_freezing: boolean;
estimated_shelf_life_days: number;
};
}>> {
return apiClient.post(`${this.baseUrl}/classify`, {
name,
description
});
}
// Food safety and compliance
async getFoodSafetyAlerts(): Promise<ApiResponse<any[]>> {
return apiClient.get(`${this.baseUrl}/food-safety/alerts`);
}
async getTemperatureLog(locationId: string, startDate: string, endDate: string): Promise<ApiResponse<any[]>> {
return apiClient.get(`${this.baseUrl}/food-safety/temperature-log`, {
location_id: locationId,
start_date: startDate,
end_date: endDate
});
}
// Batch operations
async bulkUpdateStock(updates: Array<{ stock_id: string; quantity: number; notes?: string }>): Promise<ApiResponse<{ updated: number; errors: any[] }>> {
return apiClient.post(`${this.baseUrl}/stock/bulk-update`, { updates });
}
async bulkCreateIngredients(ingredients: IngredientCreate[]): Promise<ApiResponse<{ created: number; errors: any[] }>> {
return apiClient.post(`${this.baseUrl}/ingredients/bulk-create`, { ingredients });
}
// Import/Export
async importInventory(file: File, progressCallback?: (progress: number) => void): Promise<ApiResponse<{
imported: number;
errors: any[];
warnings: any[];
}>> {
return apiClient.uploadFile(`${this.baseUrl}/import`, file, progressCallback);
}
async exportInventory(format: 'csv' | 'xlsx' = 'csv'): Promise<ApiResponse<{ download_url: string }>> {
return apiClient.get(`${this.baseUrl}/export?format=${format}`);
}
// Utility methods
getUnitOfMeasureOptions(): { value: UnitOfMeasure; label: string }[] {
return [
{ value: UnitOfMeasure.KILOGRAM, label: 'Kilogram (kg)' },
{ value: UnitOfMeasure.GRAM, label: 'Gram (g)' },
{ value: UnitOfMeasure.LITER, label: 'Liter (l)' },
{ value: UnitOfMeasure.MILLILITER, label: 'Milliliter (ml)' },
{ value: UnitOfMeasure.PIECE, label: 'Piece' },
{ value: UnitOfMeasure.PACKAGE, label: 'Package' },
{ value: UnitOfMeasure.BAG, label: 'Bag' },
{ value: UnitOfMeasure.BOX, label: 'Box' },
{ value: UnitOfMeasure.DOZEN, label: 'Dozen' },
];
}
getProductTypeOptions(): { value: ProductType; label: string }[] {
return [
{ value: ProductType.INGREDIENT, label: 'Ingredient' },
{ value: ProductType.FINISHED_PRODUCT, label: 'Finished Product' },
];
}
getMovementTypeOptions(): { value: StockMovementType; label: string }[] {
return [
{ value: StockMovementType.PURCHASE, label: 'Purchase' },
{ value: StockMovementType.SALE, label: 'Sale' },
{ value: StockMovementType.USAGE, label: 'Usage' },
{ value: StockMovementType.WASTE, label: 'Waste' },
{ value: StockMovementType.ADJUSTMENT, label: 'Adjustment' },
{ value: StockMovementType.TRANSFER, label: 'Transfer' },
{ value: StockMovementType.RETURN, label: 'Return' },
];
}
getQualityStatusOptions(): { value: string; label: string; color: string }[] {
return [
{ value: 'good', label: 'Good', color: 'green' },
{ value: 'fair', label: 'Fair', color: 'yellow' },
{ value: 'poor', label: 'Poor', color: 'orange' },
{ value: 'damaged', label: 'Damaged', color: 'red' },
{ value: 'expired', label: 'Expired', color: 'red' },
{ value: 'quarantine', label: 'Quarantine', color: 'purple' },
];
}
// AI-powered inventory classification and suggestions (moved from onboarding)
async generateInventorySuggestions(
productList: string[]
): Promise<ApiResponse<ProductSuggestionsResponse>> {
try {
if (!productList || !Array.isArray(productList) || productList.length === 0) {
throw new Error('Product list is empty or invalid');
}
// Transform product list into the expected format for BatchClassificationRequest
const products = productList.map(productName => ({
product_name: productName,
sales_data: {} // Additional context can be added later
}));
const requestData: BatchClassificationRequest = {
products: products
};
const response = await apiClient.post<ProductSuggestionsResponse>(
`${this.baseUrl}/classify-products-batch`,
requestData
);
return response;
} catch (error) {
console.error('Suggestion generation failed:', error);
throw error;
}
}
async createInventoryFromSuggestions(
approvedSuggestions: ProductSuggestion[]
): Promise<ApiResponse<InventoryCreationResponse>> {
try {
const createdItems: any[] = [];
const failedItems: any[] = [];
const inventoryMapping: { [productName: string]: string } = {};
// Create inventory items one by one using inventory service
for (const suggestion of approvedSuggestions) {
try {
const ingredientData = {
name: suggestion.suggested_name,
category: suggestion.category,
unit_of_measure: suggestion.unit_of_measure,
shelf_life_days: suggestion.estimated_shelf_life_days,
requires_refrigeration: suggestion.requires_refrigeration,
requires_freezing: suggestion.requires_freezing,
is_seasonal: suggestion.is_seasonal,
product_type: suggestion.product_type
};
const response = await apiClient.post<any>(
'/ingredients',
ingredientData
);
if (response.success) {
createdItems.push(response.data);
inventoryMapping[suggestion.original_name] = response.data.id;
} else {
failedItems.push({ suggestion, error: response.error });
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
failedItems.push({ suggestion, error: errorMessage });
}
}
const result = {
created_items: createdItems,
failed_items: failedItems,
total_approved: approvedSuggestions.length,
success_rate: createdItems.length / approvedSuggestions.length,
inventory_mapping: inventoryMapping
};
return { success: true, data: result };
} catch (error) {
console.error('Inventory creation failed:', error);
throw error;
}
}
}
export { InventoryService };
export const inventoryService = new InventoryService();

View File

@@ -1,406 +0,0 @@
import { apiClient, ApiResponse } from './client';
// Notification types
export interface Notification {
id: string;
tenant_id: string;
type: 'info' | 'warning' | 'error' | 'success';
category: 'system' | 'inventory' | 'production' | 'sales' | 'forecasting' | 'orders' | 'pos';
title: string;
message: string;
data?: Record<string, any>;
priority: 'low' | 'normal' | 'high' | 'urgent';
is_read: boolean;
is_dismissed: boolean;
read_at?: string;
dismissed_at?: string;
expires_at?: string;
created_at: string;
user_id?: string;
}
export interface NotificationTemplate {
id: string;
tenant_id: string;
name: string;
description?: string;
category: string;
type: string;
subject_template: string;
message_template: string;
channels: ('email' | 'sms' | 'push' | 'in_app' | 'whatsapp')[];
variables: Array<{
name: string;
type: 'string' | 'number' | 'date' | 'boolean';
required: boolean;
description?: string;
}>;
conditions?: Array<{
field: string;
operator: 'eq' | 'ne' | 'gt' | 'lt' | 'gte' | 'lte' | 'contains';
value: any;
}>;
is_active: boolean;
created_at: string;
updated_at: string;
}
export interface NotificationPreferences {
id: string;
user_id: string;
tenant_id: string;
email_notifications: boolean;
sms_notifications: boolean;
push_notifications: boolean;
whatsapp_notifications: boolean;
quiet_hours: {
enabled: boolean;
start_time: string;
end_time: string;
timezone: string;
};
categories: Record<string, {
enabled: boolean;
channels: string[];
min_priority: 'low' | 'normal' | 'high' | 'urgent';
}>;
updated_at: string;
}
export interface AlertRule {
id: string;
tenant_id: string;
name: string;
description?: string;
category: string;
rule_type: 'threshold' | 'trend' | 'anomaly' | 'schedule';
conditions: Array<{
metric: string;
operator: string;
value: any;
time_window?: string;
}>;
actions: Array<{
type: 'notification' | 'webhook' | 'email';
config: Record<string, any>;
}>;
is_active: boolean;
last_triggered?: string;
trigger_count: number;
created_at: string;
updated_at: string;
}
class NotificationService {
private readonly baseUrl = '/notifications';
// Notification management
async getNotifications(params?: {
page?: number;
size?: number;
category?: string;
type?: string;
priority?: string;
is_read?: boolean;
is_dismissed?: boolean;
}): Promise<ApiResponse<{ items: Notification[]; total: number; page: number; size: number; pages: number; unread_count: number }>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}?${queryParams.toString()}`
: this.baseUrl;
return apiClient.get(url);
}
async getNotification(notificationId: string): Promise<ApiResponse<Notification>> {
return apiClient.get(`${this.baseUrl}/${notificationId}`);
}
async markAsRead(notificationId: string): Promise<ApiResponse<Notification>> {
return apiClient.post(`${this.baseUrl}/${notificationId}/read`);
}
async markAsUnread(notificationId: string): Promise<ApiResponse<Notification>> {
return apiClient.post(`${this.baseUrl}/${notificationId}/unread`);
}
async dismiss(notificationId: string): Promise<ApiResponse<Notification>> {
return apiClient.post(`${this.baseUrl}/${notificationId}/dismiss`);
}
async markAllAsRead(category?: string): Promise<ApiResponse<{ updated_count: number }>> {
const url = category
? `${this.baseUrl}/mark-all-read?category=${encodeURIComponent(category)}`
: `${this.baseUrl}/mark-all-read`;
return apiClient.post(url);
}
async dismissAll(category?: string): Promise<ApiResponse<{ updated_count: number }>> {
const url = category
? `${this.baseUrl}/dismiss-all?category=${encodeURIComponent(category)}`
: `${this.baseUrl}/dismiss-all`;
return apiClient.post(url);
}
async deleteNotification(notificationId: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.delete(`${this.baseUrl}/${notificationId}`);
}
// Notification creation and sending
async createNotification(notificationData: {
type: Notification['type'];
category: string;
title: string;
message: string;
priority?: Notification['priority'];
data?: Record<string, any>;
user_id?: string;
expires_at?: string;
channels?: string[];
}): Promise<ApiResponse<Notification>> {
return apiClient.post(this.baseUrl, notificationData);
}
async sendBulkNotification(notificationData: {
type: Notification['type'];
category: string;
title: string;
message: string;
priority?: Notification['priority'];
data?: Record<string, any>;
user_ids?: string[];
user_roles?: string[];
channels?: string[];
}): Promise<ApiResponse<{ sent_count: number; failed_count: number }>> {
return apiClient.post(`${this.baseUrl}/bulk-send`, notificationData);
}
// Template management
async getTemplates(category?: string): Promise<ApiResponse<NotificationTemplate[]>> {
const url = category
? `${this.baseUrl}/templates?category=${encodeURIComponent(category)}`
: `${this.baseUrl}/templates`;
return apiClient.get(url);
}
async getTemplate(templateId: string): Promise<ApiResponse<NotificationTemplate>> {
return apiClient.get(`${this.baseUrl}/templates/${templateId}`);
}
async createTemplate(templateData: {
name: string;
description?: string;
category: string;
type: string;
subject_template: string;
message_template: string;
channels: string[];
variables: NotificationTemplate['variables'];
conditions?: NotificationTemplate['conditions'];
}): Promise<ApiResponse<NotificationTemplate>> {
return apiClient.post(`${this.baseUrl}/templates`, templateData);
}
async updateTemplate(templateId: string, templateData: Partial<NotificationTemplate>): Promise<ApiResponse<NotificationTemplate>> {
return apiClient.put(`${this.baseUrl}/templates/${templateId}`, templateData);
}
async deleteTemplate(templateId: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.delete(`${this.baseUrl}/templates/${templateId}`);
}
async previewTemplate(templateId: string, variables: Record<string, any>): Promise<ApiResponse<{
subject: string;
message: string;
rendered_html?: string;
}>> {
return apiClient.post(`${this.baseUrl}/templates/${templateId}/preview`, { variables });
}
// User preferences
async getPreferences(userId?: string): Promise<ApiResponse<NotificationPreferences>> {
const url = userId
? `${this.baseUrl}/preferences/${userId}`
: `${this.baseUrl}/preferences`;
return apiClient.get(url);
}
async updatePreferences(preferencesData: Partial<NotificationPreferences>, userId?: string): Promise<ApiResponse<NotificationPreferences>> {
const url = userId
? `${this.baseUrl}/preferences/${userId}`
: `${this.baseUrl}/preferences`;
return apiClient.put(url, preferencesData);
}
// Alert rules
async getAlertRules(): Promise<ApiResponse<AlertRule[]>> {
return apiClient.get(`${this.baseUrl}/alert-rules`);
}
async getAlertRule(ruleId: string): Promise<ApiResponse<AlertRule>> {
return apiClient.get(`${this.baseUrl}/alert-rules/${ruleId}`);
}
async createAlertRule(ruleData: {
name: string;
description?: string;
category: string;
rule_type: AlertRule['rule_type'];
conditions: AlertRule['conditions'];
actions: AlertRule['actions'];
}): Promise<ApiResponse<AlertRule>> {
return apiClient.post(`${this.baseUrl}/alert-rules`, ruleData);
}
async updateAlertRule(ruleId: string, ruleData: Partial<AlertRule>): Promise<ApiResponse<AlertRule>> {
return apiClient.put(`${this.baseUrl}/alert-rules/${ruleId}`, ruleData);
}
async deleteAlertRule(ruleId: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.delete(`${this.baseUrl}/alert-rules/${ruleId}`);
}
async testAlertRule(ruleId: string): Promise<ApiResponse<{
would_trigger: boolean;
current_values: Record<string, any>;
evaluation_result: string;
}>> {
return apiClient.post(`${this.baseUrl}/alert-rules/${ruleId}/test`);
}
// Real-time notifications (SSE)
async getSSEEndpoint(): Promise<ApiResponse<{ endpoint_url: string; auth_token: string }>> {
return apiClient.get(`${this.baseUrl}/sse/endpoint`);
}
connectSSE(onNotification: (notification: Notification) => void, onError?: (error: Error) => void): EventSource {
const token = localStorage.getItem('access_token');
const tenantId = localStorage.getItem('tenant_id');
const url = new URL(`${apiClient.getBaseURL()}/notifications/sse/stream`);
if (token) url.searchParams.append('token', token);
if (tenantId) url.searchParams.append('tenant_id', tenantId);
const eventSource = new EventSource(url.toString());
eventSource.onmessage = (event) => {
try {
const notification = JSON.parse(event.data);
onNotification(notification);
} catch (error) {
console.error('Failed to parse SSE notification:', error);
onError?.(error as Error);
}
};
eventSource.onerror = (error) => {
console.error('SSE connection error:', error);
onError?.(new Error('SSE connection failed'));
};
return eventSource;
}
// Analytics and reporting
async getNotificationStats(params?: {
start_date?: string;
end_date?: string;
category?: string;
}): Promise<ApiResponse<{
total_sent: number;
total_read: number;
total_dismissed: number;
read_rate: number;
dismiss_rate: number;
by_category: Array<{
category: string;
sent: number;
read: number;
dismissed: number;
}>;
by_channel: Array<{
channel: string;
sent: number;
delivered: number;
failed: number;
}>;
trends: Array<{
date: string;
sent: number;
read: number;
}>;
}>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/stats?${queryParams.toString()}`
: `${this.baseUrl}/stats`;
return apiClient.get(url);
}
// Utility methods
getNotificationTypes(): { value: string; label: string; icon: string; color: string }[] {
return [
{ value: 'info', label: 'Information', icon: 'info', color: 'blue' },
{ value: 'success', label: 'Success', icon: 'check', color: 'green' },
{ value: 'warning', label: 'Warning', icon: 'warning', color: 'yellow' },
{ value: 'error', label: 'Error', icon: 'error', color: 'red' },
];
}
getNotificationCategories(): { value: string; label: string; description: string }[] {
return [
{ value: 'system', label: 'System', description: 'System-wide notifications and updates' },
{ value: 'inventory', label: 'Inventory', description: 'Stock levels, expiration alerts, reorder notifications' },
{ value: 'production', label: 'Production', description: 'Production schedules, quality checks, batch completion' },
{ value: 'sales', label: 'Sales', description: 'Sales targets, performance alerts, revenue notifications' },
{ value: 'forecasting', label: 'Forecasting', description: 'Demand predictions, model updates, accuracy alerts' },
{ value: 'orders', label: 'Orders', description: 'New orders, order updates, delivery notifications' },
{ value: 'pos', label: 'POS', description: 'Point of sale integration, sync status, transaction alerts' },
];
}
getPriorityLevels(): { value: string; label: string; color: string; urgency: number }[] {
return [
{ value: 'low', label: 'Low', color: 'gray', urgency: 1 },
{ value: 'normal', label: 'Normal', color: 'blue', urgency: 2 },
{ value: 'high', label: 'High', color: 'orange', urgency: 3 },
{ value: 'urgent', label: 'Urgent', color: 'red', urgency: 4 },
];
}
getNotificationChannels(): { value: string; label: string; description: string; requires_setup: boolean }[] {
return [
{ value: 'in_app', label: 'In-App', description: 'Notifications within the application', requires_setup: false },
{ value: 'email', label: 'Email', description: 'Email notifications', requires_setup: true },
{ value: 'sms', label: 'SMS', description: 'Text message notifications', requires_setup: true },
{ value: 'push', label: 'Push', description: 'Browser/mobile push notifications', requires_setup: true },
{ value: 'whatsapp', label: 'WhatsApp', description: 'WhatsApp notifications', requires_setup: true },
];
}
}
export const notificationService = new NotificationService();

View File

@@ -1 +0,0 @@
/Users/urtzialfaro/Documents/bakery-ia/frontend/src/services/api/orders.service.ts

View File

@@ -1,192 +0,0 @@
import { apiClient } from './client';
import { ApiResponse } from '../../types/api.types';
import {
OrderStatus,
OrderType,
OrderItem,
OrderCreate,
OrderUpdate,
OrderResponse,
Customer,
OrderAnalytics,
OrderFilters,
CustomerFilters,
OrderTrendsParams,
OrderTrendData
} from '../../types/orders.types';
class OrdersService {
private readonly baseUrl = '/orders';
// Order management
async getOrders(params?: OrderFilters): Promise<ApiResponse<{ items: OrderResponse[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}?${queryParams.toString()}`
: this.baseUrl;
return apiClient.get(url);
}
async getOrder(orderId: string): Promise<ApiResponse<OrderResponse>> {
return apiClient.get(`${this.baseUrl}/${orderId}`);
}
async createOrder(orderData: OrderCreate): Promise<ApiResponse<OrderResponse>> {
return apiClient.post(this.baseUrl, orderData);
}
async updateOrder(orderId: string, orderData: OrderUpdate): Promise<ApiResponse<OrderResponse>> {
return apiClient.put(`${this.baseUrl}/${orderId}`, orderData);
}
async cancelOrder(orderId: string, reason?: string): Promise<ApiResponse<OrderResponse>> {
return apiClient.post(`${this.baseUrl}/${orderId}/cancel`, { reason });
}
async confirmOrder(orderId: string): Promise<ApiResponse<OrderResponse>> {
return apiClient.post(`${this.baseUrl}/${orderId}/confirm`);
}
async startPreparation(orderId: string): Promise<ApiResponse<OrderResponse>> {
return apiClient.post(`${this.baseUrl}/${orderId}/start-preparation`);
}
async markReady(orderId: string): Promise<ApiResponse<OrderResponse>> {
return apiClient.post(`${this.baseUrl}/${orderId}/mark-ready`);
}
async markDelivered(orderId: string): Promise<ApiResponse<OrderResponse>> {
return apiClient.post(`${this.baseUrl}/${orderId}/mark-delivered`);
}
// Customer management
async getCustomers(params?: CustomerFilters): Promise<ApiResponse<{ items: Customer[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/customers?${queryParams.toString()}`
: `${this.baseUrl}/customers`;
return apiClient.get(url);
}
async getCustomer(customerId: string): Promise<ApiResponse<Customer>> {
return apiClient.get(`${this.baseUrl}/customers/${customerId}`);
}
async getCustomerOrders(customerId: string): Promise<ApiResponse<OrderResponse[]>> {
return apiClient.get(`${this.baseUrl}/customers/${customerId}/orders`);
}
// Analytics and reporting
async getOrderAnalytics(params?: {
start_date?: string;
end_date?: string;
order_type?: OrderType;
}): Promise<ApiResponse<OrderAnalytics>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/analytics?${queryParams.toString()}`
: `${this.baseUrl}/analytics`;
return apiClient.get(url);
}
async getOrderTrends(params?: OrderTrendsParams): Promise<ApiResponse<OrderTrendData[]>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/trends?${queryParams.toString()}`
: `${this.baseUrl}/trends`;
return apiClient.get(url);
}
// Queue management
async getOrderQueue(filterBy?: OrderStatus[]): Promise<ApiResponse<OrderResponse[]>> {
const queryParams = new URLSearchParams();
if (filterBy) {
filterBy.forEach(status => queryParams.append('status', status));
}
const url = queryParams.toString()
? `${this.baseUrl}/queue?${queryParams.toString()}`
: `${this.baseUrl}/queue`;
return apiClient.get(url);
}
async updateQueuePosition(orderId: string, newPosition: number): Promise<ApiResponse<OrderResponse>> {
return apiClient.post(`${this.baseUrl}/${orderId}/queue-position`, { position: newPosition });
}
// Utility methods
getOrderStatusOptions(): { value: OrderStatus; label: string; color: string }[] {
return [
{ value: OrderStatus.PENDING, label: 'Pending', color: 'gray' },
{ value: OrderStatus.CONFIRMED, label: 'Confirmed', color: 'blue' },
{ value: OrderStatus.IN_PREPARATION, label: 'In Preparation', color: 'yellow' },
{ value: OrderStatus.READY, label: 'Ready', color: 'green' },
{ value: OrderStatus.DELIVERED, label: 'Delivered', color: 'green' },
{ value: OrderStatus.CANCELLED, label: 'Cancelled', color: 'red' },
];
}
getOrderTypeOptions(): { value: OrderType; label: string }[] {
return [
{ value: OrderType.DINE_IN, label: 'Dine In' },
{ value: OrderType.TAKEAWAY, label: 'Takeaway' },
{ value: OrderType.DELIVERY, label: 'Delivery' },
{ value: OrderType.CATERING, label: 'Catering' },
];
}
getPaymentMethods(): { value: string; label: string }[] {
return [
{ value: 'cash', label: 'Cash' },
{ value: 'card', label: 'Card' },
{ value: 'digital_wallet', label: 'Digital Wallet' },
{ value: 'bank_transfer', label: 'Bank Transfer' },
];
}
}
export { OrdersService };
export { OrdersService as OrderService }; // Alias for compatibility
export const ordersService = new OrdersService();

View File

@@ -1,317 +0,0 @@
import { apiClient, ApiResponse } from './client';
// Request/Response Types
export interface POSConfiguration {
id: string;
tenant_id: string;
provider: 'square' | 'stripe' | 'toast' | 'clover' | 'custom';
config_name: string;
is_active: boolean;
credentials: {
api_key?: string;
secret_key?: string;
application_id?: string;
location_id?: string;
webhook_signature?: string;
};
sync_settings: {
auto_sync_enabled: boolean;
sync_interval_minutes: number;
sync_sales: boolean;
sync_inventory: boolean;
sync_customers: boolean;
};
created_at: string;
updated_at: string;
last_sync_at?: string;
}
export interface POSTransaction {
id: string;
tenant_id: string;
pos_transaction_id: string;
provider: string;
location_id: string;
transaction_type: 'sale' | 'refund' | 'void';
amount: number;
tax_amount: number;
tip_amount?: number;
discount_amount?: number;
payment_method: string;
customer_id?: string;
items: Array<{
product_id: string;
product_name: string;
quantity: number;
unit_price: number;
total_price: number;
}>;
transaction_date: string;
created_at: string;
updated_at: string;
}
export interface SyncStatus {
id: string;
tenant_id: string;
sync_type: 'manual' | 'automatic';
status: 'pending' | 'in_progress' | 'completed' | 'failed';
progress: number;
total_records: number;
processed_records: number;
failed_records: number;
error_message?: string;
started_at: string;
completed_at?: string;
}
class POSService {
private readonly baseUrl = '/pos';
// Configuration management
async getPOSConfigs(): Promise<ApiResponse<POSConfiguration[]>> {
return apiClient.get(`${this.baseUrl}/config`);
}
async getPOSConfig(configId: string): Promise<ApiResponse<POSConfiguration>> {
return apiClient.get(`${this.baseUrl}/config/${configId}`);
}
async createPOSConfig(configData: {
provider: string;
config_name: string;
credentials: Record<string, string>;
sync_settings?: Partial<POSConfiguration['sync_settings']>;
}): Promise<ApiResponse<POSConfiguration>> {
return apiClient.post(`${this.baseUrl}/config`, configData);
}
async updatePOSConfig(configId: string, configData: Partial<POSConfiguration>): Promise<ApiResponse<POSConfiguration>> {
return apiClient.put(`${this.baseUrl}/config/${configId}`, configData);
}
async deletePOSConfig(configId: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.delete(`${this.baseUrl}/config/${configId}`);
}
async testPOSConnection(configId: string): Promise<ApiResponse<{
success: boolean;
message: string;
connection_details?: Record<string, any>;
}>> {
return apiClient.post(`${this.baseUrl}/config/${configId}/test`);
}
// Synchronization
async startManualSync(configId: string, syncOptions?: {
sync_sales?: boolean;
sync_inventory?: boolean;
sync_customers?: boolean;
date_from?: string;
date_to?: string;
}): Promise<ApiResponse<{ sync_id: string; message: string }>> {
return apiClient.post(`${this.baseUrl}/sync/${configId}/start`, syncOptions);
}
async getSyncStatus(syncId: string): Promise<ApiResponse<SyncStatus>> {
return apiClient.get(`${this.baseUrl}/sync/status/${syncId}`);
}
async getSyncHistory(params?: {
page?: number;
size?: number;
config_id?: string;
status?: string;
}): Promise<ApiResponse<{ items: SyncStatus[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/sync/history?${queryParams.toString()}`
: `${this.baseUrl}/sync/history`;
return apiClient.get(url);
}
async cancelSync(syncId: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.post(`${this.baseUrl}/sync/${syncId}/cancel`);
}
// Transaction management
async getPOSTransactions(params?: {
page?: number;
size?: number;
config_id?: string;
start_date?: string;
end_date?: string;
transaction_type?: string;
}): Promise<ApiResponse<{ items: POSTransaction[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/transactions?${queryParams.toString()}`
: `${this.baseUrl}/transactions`;
return apiClient.get(url);
}
async getPOSTransaction(transactionId: string): Promise<ApiResponse<POSTransaction>> {
return apiClient.get(`${this.baseUrl}/transactions/${transactionId}`);
}
// Webhook management
async getWebhooks(configId: string): Promise<ApiResponse<Array<{
id: string;
event_type: string;
endpoint_url: string;
is_active: boolean;
created_at: string;
}>>> {
return apiClient.get(`${this.baseUrl}/webhooks/${configId}`);
}
async createWebhook(configId: string, webhookData: {
event_types: string[];
endpoint_url?: string;
}): Promise<ApiResponse<{ webhook_id: string; endpoint_url: string }>> {
return apiClient.post(`${this.baseUrl}/webhooks/${configId}`, webhookData);
}
async deleteWebhook(configId: string, webhookId: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.delete(`${this.baseUrl}/webhooks/${configId}/${webhookId}`);
}
// Analytics and reporting
async getPOSAnalytics(params?: {
config_id?: string;
start_date?: string;
end_date?: string;
}): Promise<ApiResponse<{
total_transactions: number;
total_revenue: number;
average_transaction_value: number;
refund_rate: number;
top_payment_methods: Array<{
method: string;
count: number;
total_amount: number;
}>;
hourly_sales: Array<{
hour: number;
transaction_count: number;
total_amount: number;
}>;
}>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/analytics?${queryParams.toString()}`
: `${this.baseUrl}/analytics`;
return apiClient.get(url);
}
// Product mapping
async getProductMappings(configId: string): Promise<ApiResponse<Array<{
pos_product_id: string;
pos_product_name: string;
internal_product_id?: string;
internal_product_name?: string;
is_mapped: boolean;
last_sync_at?: string;
}>>> {
return apiClient.get(`${this.baseUrl}/products/${configId}/mappings`);
}
async updateProductMapping(configId: string, mappingData: {
pos_product_id: string;
internal_product_id: string;
}): Promise<ApiResponse<{ message: string }>> {
return apiClient.put(`${this.baseUrl}/products/${configId}/mappings`, mappingData);
}
async bulkUpdateProductMappings(configId: string, mappings: Array<{
pos_product_id: string;
internal_product_id: string;
}>): Promise<ApiResponse<{ updated: number; errors: any[] }>> {
return apiClient.post(`${this.baseUrl}/products/${configId}/mappings/bulk`, { mappings });
}
// Utility methods
getSupportedProviders(): { value: string; label: string; features: string[] }[] {
return [
{
value: 'square',
label: 'Square',
features: ['sales_sync', 'inventory_sync', 'customer_sync', 'webhooks']
},
{
value: 'stripe',
label: 'Stripe',
features: ['sales_sync', 'customer_sync', 'webhooks']
},
{
value: 'toast',
label: 'Toast',
features: ['sales_sync', 'inventory_sync', 'webhooks']
},
{
value: 'clover',
label: 'Clover',
features: ['sales_sync', 'inventory_sync', 'customer_sync']
},
{
value: 'custom',
label: 'Custom Integration',
features: ['api_integration']
}
];
}
getSyncIntervalOptions(): { value: number; label: string }[] {
return [
{ value: 5, label: 'Every 5 minutes' },
{ value: 15, label: 'Every 15 minutes' },
{ value: 30, label: 'Every 30 minutes' },
{ value: 60, label: 'Every hour' },
{ value: 240, label: 'Every 4 hours' },
{ value: 1440, label: 'Daily' },
];
}
getWebhookEventTypes(): { value: string; label: string; description: string }[] {
return [
{ value: 'payment.created', label: 'Payment Created', description: 'Triggered when a new payment is processed' },
{ value: 'payment.updated', label: 'Payment Updated', description: 'Triggered when a payment is updated' },
{ value: 'order.created', label: 'Order Created', description: 'Triggered when a new order is created' },
{ value: 'order.updated', label: 'Order Updated', description: 'Triggered when an order is updated' },
{ value: 'inventory.updated', label: 'Inventory Updated', description: 'Triggered when inventory levels change' },
{ value: 'customer.created', label: 'Customer Created', description: 'Triggered when a new customer is created' },
];
}
}
export const posService = new POSService();

View File

@@ -1,219 +0,0 @@
import { apiClient } from './client';
import { ApiResponse } from '../../types/api.types';
import {
SupplierCreate,
SupplierUpdate,
SupplierResponse,
SupplierSummary,
SupplierSearchParams,
SupplierApproval,
SupplierStatistics,
PurchaseOrderCreate,
PurchaseOrderUpdate,
PurchaseOrderResponse,
PurchaseOrderStatus,
DeliveryCreate,
DeliveryResponse,
DeliveryStatus,
DeliveryReceiptConfirmation,
Supplier
} from '../../types/suppliers.types';
class ProcurementService {
private getTenantId(): string {
const tenantStorage = localStorage.getItem('tenant-storage');
if (tenantStorage) {
try {
const { state } = JSON.parse(tenantStorage);
return state?.currentTenant?.id;
} catch {
return '';
}
}
return '';
}
private getBaseUrl(): string {
return '';
}
// Purchase Order management
async getPurchaseOrders(params?: {
page?: number;
size?: number;
status?: PurchaseOrderStatus;
supplier_id?: string;
start_date?: string;
end_date?: string;
}): Promise<ApiResponse<{ items: PurchaseOrderResponse[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.getBaseUrl()}/purchase-orders?${queryParams.toString()}`
: `${this.getBaseUrl()}/purchase-orders`;
return apiClient.get(url);
}
async getPurchaseOrder(orderId: string): Promise<ApiResponse<PurchaseOrderResponse>> {
return apiClient.get(`${this.getBaseUrl()}/purchase-orders/${orderId}`);
}
async createPurchaseOrder(orderData: PurchaseOrderCreate): Promise<ApiResponse<PurchaseOrderResponse>> {
return apiClient.post(`${this.getBaseUrl()}/purchase-orders`, orderData);
}
async updatePurchaseOrder(orderId: string, orderData: PurchaseOrderUpdate): Promise<ApiResponse<PurchaseOrderResponse>> {
return apiClient.put(`${this.getBaseUrl()}/purchase-orders/${orderId}`, orderData);
}
async approvePurchaseOrder(orderId: string): Promise<ApiResponse<PurchaseOrderResponse>> {
return apiClient.post(`${this.getBaseUrl()}/purchase-orders/${orderId}/approve`);
}
async sendPurchaseOrder(orderId: string, sendEmail: boolean = true): Promise<ApiResponse<{ message: string; sent_at: string }>> {
return apiClient.post(`${this.getBaseUrl()}/purchase-orders/${orderId}/send`, { send_email: sendEmail });
}
async cancelPurchaseOrder(orderId: string, reason?: string): Promise<ApiResponse<PurchaseOrderResponse>> {
return apiClient.post(`${this.getBaseUrl()}/purchase-orders/${orderId}/cancel`, { reason });
}
// Supplier management
async getSuppliers(params?: SupplierSearchParams): Promise<ApiResponse<SupplierSummary[]>> {
const queryParams = new URLSearchParams();
if (params) {
if (params.search_term) queryParams.append('search_term', params.search_term);
if (params.supplier_type) queryParams.append('supplier_type', params.supplier_type);
if (params.status) queryParams.append('status', params.status);
if (params.limit) queryParams.append('limit', params.limit.toString());
if (params.offset) queryParams.append('offset', params.offset.toString());
}
const url = queryParams.toString()
? `${this.getBaseUrl()}/suppliers?${queryParams.toString()}`
: `${this.getBaseUrl()}/suppliers`;
return apiClient.get(url);
}
async getSupplier(supplierId: string): Promise<ApiResponse<SupplierResponse>> {
return apiClient.get(`${this.getBaseUrl()}/suppliers/${supplierId}`);
}
async createSupplier(supplierData: SupplierCreate): Promise<ApiResponse<SupplierResponse>> {
return apiClient.post(`${this.getBaseUrl()}/suppliers`, supplierData);
}
async updateSupplier(supplierId: string, supplierData: SupplierUpdate): Promise<ApiResponse<SupplierResponse>> {
return apiClient.put(`${this.getBaseUrl()}/suppliers/${supplierId}`, supplierData);
}
async deleteSupplier(supplierId: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.delete(`${this.getBaseUrl()}/suppliers/${supplierId}`);
}
async approveSupplier(supplierId: string, approval: SupplierApproval): Promise<ApiResponse<SupplierResponse>> {
return apiClient.post(`${this.getBaseUrl()}/suppliers/${supplierId}/approve`, approval);
}
async getSupplierStatistics(): Promise<ApiResponse<SupplierStatistics>> {
return apiClient.get(`${this.getBaseUrl()}/suppliers/statistics`);
}
async getActiveSuppliers(): Promise<ApiResponse<SupplierSummary[]>> {
return apiClient.get(`${this.getBaseUrl()}/suppliers/active`);
}
async getTopSuppliers(limit: number = 10): Promise<ApiResponse<SupplierSummary[]>> {
return apiClient.get(`${this.getBaseUrl()}/suppliers/top?limit=${limit}`);
}
// Delivery management
async getDeliveries(params?: {
page?: number;
size?: number;
status?: DeliveryStatus;
supplier_id?: string;
purchase_order_id?: string;
start_date?: string;
end_date?: string;
}): Promise<ApiResponse<{ items: DeliveryResponse[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.getBaseUrl()}/deliveries?${queryParams.toString()}`
: `${this.getBaseUrl()}/deliveries`;
return apiClient.get(url);
}
async getDelivery(deliveryId: string): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.get(`${this.getBaseUrl()}/deliveries/${deliveryId}`);
}
async createDelivery(deliveryData: DeliveryCreate): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.post(`${this.getBaseUrl()}/deliveries`, deliveryData);
}
async updateDelivery(deliveryId: string, deliveryData: Partial<DeliveryCreate>): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.put(`${this.getBaseUrl()}/deliveries/${deliveryId}`, deliveryData);
}
async updateDeliveryStatus(deliveryId: string, status: DeliveryStatus, notes?: string): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.put(`${this.getBaseUrl()}/deliveries/${deliveryId}/status`, { status, notes });
}
async confirmDeliveryReceipt(deliveryId: string, confirmation: DeliveryReceiptConfirmation): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.post(`${this.getBaseUrl()}/deliveries/${deliveryId}/confirm-receipt`, confirmation);
}
// Utility methods
getPurchaseOrderStatusOptions(): { value: PurchaseOrderStatus; label: string; color: string }[] {
return [
{ value: PurchaseOrderStatus.DRAFT, label: 'Draft', color: 'gray' },
{ value: PurchaseOrderStatus.PENDING, label: 'Pending', color: 'yellow' },
{ value: PurchaseOrderStatus.APPROVED, label: 'Approved', color: 'blue' },
{ value: PurchaseOrderStatus.SENT, label: 'Sent', color: 'purple' },
{ value: PurchaseOrderStatus.PARTIALLY_RECEIVED, label: 'Partially Received', color: 'orange' },
{ value: PurchaseOrderStatus.RECEIVED, label: 'Received', color: 'green' },
{ value: PurchaseOrderStatus.CANCELLED, label: 'Cancelled', color: 'red' },
];
}
getDeliveryStatusOptions(): { value: DeliveryStatus; label: string; color: string }[] {
return [
{ value: DeliveryStatus.SCHEDULED, label: 'Scheduled', color: 'blue' },
{ value: DeliveryStatus.IN_TRANSIT, label: 'In Transit', color: 'yellow' },
{ value: DeliveryStatus.DELIVERED, label: 'Delivered', color: 'green' },
{ value: DeliveryStatus.FAILED, label: 'Failed', color: 'red' },
{ value: DeliveryStatus.RETURNED, label: 'Returned', color: 'orange' },
];
}
}
export { ProcurementService };
export const procurementService = new ProcurementService();

View File

@@ -1,468 +0,0 @@
import { apiClient, ApiResponse } from './client';
import {
ProductionBatchStatus,
QualityCheckStatus,
ProductionPriority,
ProductionBatch,
ProductionSchedule,
QualityCheck,
Recipe
} from '../../types/production.types';
// Type aliases for service compatibility
type ProductionBatchCreate = Omit<ProductionBatch, 'id' | 'tenant_id' | 'created_at' | 'updated_at'>;
type ProductionBatchUpdate = Partial<ProductionBatchCreate>;
type ProductionBatchResponse = ProductionBatch;
type ProductionScheduleEntry = ProductionSchedule;
type QualityCheckCreate = Omit<QualityCheck, 'id' | 'tenant_id' | 'created_at' | 'updated_at'>;
type QualityCheckResponse = QualityCheck;
// Request/Response Types
export interface ProductionBatchCreate {
recipe_id: string;
planned_quantity: number;
planned_start_date: string;
planned_end_date?: string;
priority?: ProductionPriority;
notes?: string;
assigned_staff?: string[];
equipment_required?: string[];
}
export interface ProductionBatchUpdate {
planned_quantity?: number;
actual_quantity?: number;
planned_start_date?: string;
planned_end_date?: string;
actual_start_date?: string;
actual_end_date?: string;
status?: ProductionBatchStatus;
priority?: ProductionPriority;
notes?: string;
assigned_staff?: string[];
equipment_required?: string[];
}
export interface ProductionBatchResponse {
id: string;
tenant_id: string;
recipe_id: string;
batch_number: string;
planned_quantity: number;
actual_quantity?: number;
planned_start_date: string;
planned_end_date?: string;
actual_start_date?: string;
actual_end_date?: string;
status: ProductionBatchStatus;
priority: ProductionPriority;
notes?: string;
assigned_staff: string[];
equipment_required: string[];
cost_per_unit?: number;
total_cost?: number;
yield_percentage?: number;
created_at: string;
updated_at: string;
created_by: string;
recipe?: any; // Recipe details
}
export interface ProductionScheduleEntry {
id: string;
tenant_id: string;
batch_id: string;
scheduled_date: string;
scheduled_start_time: string;
scheduled_end_time: string;
estimated_duration_minutes: number;
equipment_reservations: string[];
staff_assignments: string[];
dependencies: string[];
is_locked: boolean;
created_at: string;
updated_at: string;
batch?: ProductionBatchResponse;
}
export interface QualityCheckCreate {
batch_id: string;
check_type: string;
criteria: Record<string, any>;
inspector?: string;
scheduled_date?: string;
}
export interface QualityCheckResponse {
id: string;
tenant_id: string;
batch_id: string;
check_type: string;
status: QualityCheckStatus;
criteria: Record<string, any>;
results: Record<string, any>;
inspector?: string;
scheduled_date?: string;
completed_date?: string;
notes?: string;
corrective_actions?: string[];
created_at: string;
updated_at: string;
batch?: ProductionBatchResponse;
}
export interface ProductionCapacity {
id: string;
tenant_id: string;
resource_type: 'equipment' | 'staff' | 'facility';
resource_id: string;
resource_name: string;
daily_capacity: number;
hourly_capacity?: number;
utilization_rate: number;
maintenance_schedule?: Array<{
start_date: string;
end_date: string;
type: string;
}>;
availability_windows: Array<{
day_of_week: string;
start_time: string;
end_time: string;
}>;
}
export interface ProductionMetrics {
total_batches: number;
completed_batches: number;
in_progress_batches: number;
average_yield: number;
on_time_delivery_rate: number;
quality_pass_rate: number;
equipment_utilization: number;
production_efficiency: number;
waste_percentage: number;
cost_per_unit_average: number;
}
export interface ProductionAlert {
id: string;
tenant_id: string;
alert_type: string;
severity: 'low' | 'medium' | 'high' | 'critical';
title: string;
message: string;
batch_id?: string;
equipment_id?: string;
is_active: boolean;
acknowledged_at?: string;
resolved_at?: string;
created_at: string;
}
class ProductionService {
private readonly baseUrl = '/production';
// Production batch management
async getProductionBatches(params?: {
page?: number;
size?: number;
status?: ProductionBatchStatus;
priority?: ProductionPriority;
start_date?: string;
end_date?: string;
recipe_id?: string;
}): Promise<ApiResponse<{ items: ProductionBatchResponse[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/batches?${queryParams.toString()}`
: `${this.baseUrl}/batches`;
return apiClient.get(url);
}
async getProductionBatch(batchId: string): Promise<ApiResponse<ProductionBatchResponse>> {
return apiClient.get(`${this.baseUrl}/batches/${batchId}`);
}
async createProductionBatch(batchData: ProductionBatchCreate): Promise<ApiResponse<ProductionBatchResponse>> {
return apiClient.post(`${this.baseUrl}/batches`, batchData);
}
async updateProductionBatch(batchId: string, batchData: ProductionBatchUpdate): Promise<ApiResponse<ProductionBatchResponse>> {
return apiClient.put(`${this.baseUrl}/batches/${batchId}`, batchData);
}
async deleteProductionBatch(batchId: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.delete(`${this.baseUrl}/batches/${batchId}`);
}
async startProductionBatch(batchId: string): Promise<ApiResponse<ProductionBatchResponse>> {
return apiClient.post(`${this.baseUrl}/batches/${batchId}/start`);
}
async completeProductionBatch(batchId: string, completionData: {
actual_quantity: number;
yield_notes?: string;
quality_notes?: string;
}): Promise<ApiResponse<ProductionBatchResponse>> {
return apiClient.post(`${this.baseUrl}/batches/${batchId}/complete`, completionData);
}
// Production scheduling
async getProductionSchedule(params?: {
start_date?: string;
end_date?: string;
equipment_id?: string;
staff_id?: string;
}): Promise<ApiResponse<ProductionScheduleEntry[]>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/schedule?${queryParams.toString()}`
: `${this.baseUrl}/schedule`;
return apiClient.get(url);
}
async scheduleProductionBatch(scheduleData: {
batch_id: string;
scheduled_date: string;
scheduled_start_time: string;
scheduled_end_time: string;
equipment_reservations?: string[];
staff_assignments?: string[];
}): Promise<ApiResponse<ProductionScheduleEntry>> {
return apiClient.post(`${this.baseUrl}/schedule`, scheduleData);
}
async updateScheduleEntry(entryId: string, updateData: {
scheduled_date?: string;
scheduled_start_time?: string;
scheduled_end_time?: string;
equipment_reservations?: string[];
staff_assignments?: string[];
}): Promise<ApiResponse<ProductionScheduleEntry>> {
return apiClient.put(`${this.baseUrl}/schedule/${entryId}`, updateData);
}
// Quality control
async getQualityChecks(params?: {
page?: number;
size?: number;
batch_id?: string;
status?: QualityCheckStatus;
check_type?: string;
}): Promise<ApiResponse<{ items: QualityCheckResponse[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/quality-checks?${queryParams.toString()}`
: `${this.baseUrl}/quality-checks`;
return apiClient.get(url);
}
async createQualityCheck(checkData: QualityCheckCreate): Promise<ApiResponse<QualityCheckResponse>> {
return apiClient.post(`${this.baseUrl}/quality-checks`, checkData);
}
async completeQualityCheck(checkId: string, results: {
status: QualityCheckStatus;
results: Record<string, any>;
notes?: string;
corrective_actions?: string[];
}): Promise<ApiResponse<QualityCheckResponse>> {
return apiClient.put(`${this.baseUrl}/quality-checks/${checkId}/complete`, results);
}
// Capacity management
async getProductionCapacity(): Promise<ApiResponse<ProductionCapacity[]>> {
return apiClient.get(`${this.baseUrl}/capacity`);
}
async updateCapacity(capacityId: string, capacityData: Partial<ProductionCapacity>): Promise<ApiResponse<ProductionCapacity>> {
return apiClient.put(`${this.baseUrl}/capacity/${capacityId}`, capacityData);
}
async getCapacityUtilization(params?: {
start_date?: string;
end_date?: string;
resource_type?: string;
}): Promise<ApiResponse<Array<{
resource_id: string;
resource_name: string;
utilization_rate: number;
available_capacity: number;
used_capacity: number;
bottleneck_risk: 'low' | 'medium' | 'high';
}>>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/capacity/utilization?${queryParams.toString()}`
: `${this.baseUrl}/capacity/utilization`;
return apiClient.get(url);
}
// Production metrics and analytics
async getProductionMetrics(params?: {
start_date?: string;
end_date?: string;
recipe_id?: string;
}): Promise<ApiResponse<ProductionMetrics>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/metrics?${queryParams.toString()}`
: `${this.baseUrl}/metrics`;
return apiClient.get(url);
}
async getProductionTrends(params?: {
start_date?: string;
end_date?: string;
granularity?: 'daily' | 'weekly' | 'monthly';
}): Promise<ApiResponse<Array<{
date: string;
total_production: number;
efficiency_rate: number;
quality_rate: number;
on_time_rate: number;
}>>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/trends?${queryParams.toString()}`
: `${this.baseUrl}/trends`;
return apiClient.get(url);
}
// Alerts and notifications
async getProductionAlerts(params?: {
is_active?: boolean;
severity?: string;
}): Promise<ApiResponse<ProductionAlert[]>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/alerts?${queryParams.toString()}`
: `${this.baseUrl}/alerts`;
return apiClient.get(url);
}
async acknowledgeAlert(alertId: string): Promise<ApiResponse<ProductionAlert>> {
return apiClient.post(`${this.baseUrl}/alerts/${alertId}/acknowledge`);
}
async resolveAlert(alertId: string, resolutionNotes?: string): Promise<ApiResponse<ProductionAlert>> {
return apiClient.post(`${this.baseUrl}/alerts/${alertId}/resolve`, {
resolution_notes: resolutionNotes
});
}
// Utility methods
getBatchStatusOptions(): { value: ProductionBatchStatus; label: string; color: string }[] {
return [
{ value: ProductionBatchStatus.PLANNED, label: 'Planned', color: 'blue' },
{ value: ProductionBatchStatus.IN_PROGRESS, label: 'In Progress', color: 'yellow' },
{ value: ProductionBatchStatus.COMPLETED, label: 'Completed', color: 'green' },
{ value: ProductionBatchStatus.CANCELLED, label: 'Cancelled', color: 'red' },
{ value: ProductionBatchStatus.ON_HOLD, label: 'On Hold', color: 'orange' },
];
}
getPriorityOptions(): { value: ProductionPriority; label: string; color: string }[] {
return [
{ value: ProductionPriority.LOW, label: 'Low', color: 'gray' },
{ value: ProductionPriority.NORMAL, label: 'Normal', color: 'blue' },
{ value: ProductionPriority.HIGH, label: 'High', color: 'orange' },
{ value: ProductionPriority.URGENT, label: 'Urgent', color: 'red' },
];
}
getQualityCheckStatusOptions(): { value: QualityCheckStatus; label: string; color: string }[] {
return [
{ value: QualityCheckStatus.PENDING, label: 'Pending', color: 'gray' },
{ value: QualityCheckStatus.PASSED, label: 'Passed', color: 'green' },
{ value: QualityCheckStatus.FAILED, label: 'Failed', color: 'red' },
{ value: QualityCheckStatus.REQUIRES_REVIEW, label: 'Requires Review', color: 'orange' },
];
}
getQualityCheckTypes(): { value: string; label: string }[] {
return [
{ value: 'visual_inspection', label: 'Visual Inspection' },
{ value: 'weight_check', label: 'Weight Check' },
{ value: 'temperature_check', label: 'Temperature Check' },
{ value: 'texture_assessment', label: 'Texture Assessment' },
{ value: 'taste_test', label: 'Taste Test' },
{ value: 'packaging_quality', label: 'Packaging Quality' },
{ value: 'food_safety', label: 'Food Safety' },
];
}
}
export { ProductionService };
export const productionService = new ProductionService();

View File

@@ -1,570 +0,0 @@
import { apiClient } from './client';
import { ApiResponse } from '../../types/api.types';
import { BusinessModelGuide, BusinessModelType, TemplateData } from '../../types/sales.types';
// Request/Response Types
export interface SalesData {
id: string;
tenant_id: string;
date: string;
product_id?: string;
product_name: string;
category?: string;
quantity_sold: number;
unit_price: number;
total_revenue: number;
cost_of_goods: number;
gross_profit: number;
discount_applied: number;
tax_amount: number;
weather_condition?: string;
temperature?: number;
day_of_week: string;
is_holiday: boolean;
special_event?: string;
created_at: string;
updated_at: string;
}
export interface SalesCreate {
date: string;
product_id?: string;
product_name: string;
category?: string;
quantity_sold: number;
unit_price: number;
discount_applied?: number;
tax_amount?: number;
weather_condition?: string;
temperature?: number;
special_event?: string;
}
export interface SalesUpdate {
product_name?: string;
category?: string;
quantity_sold?: number;
unit_price?: number;
discount_applied?: number;
tax_amount?: number;
weather_condition?: string;
temperature?: number;
special_event?: string;
}
export interface SalesSummary {
total_revenue: number;
total_quantity: number;
average_order_value: number;
total_orders: number;
gross_profit: number;
profit_margin: number;
growth_rate?: number;
period_comparison?: {
revenue_change: number;
quantity_change: number;
order_change: number;
};
}
export interface SalesAnalytics {
daily_sales: Array<{
date: string;
revenue: number;
quantity: number;
orders: number;
}>;
product_performance: Array<{
product_name: string;
total_revenue: number;
total_quantity: number;
growth_rate: number;
}>;
hourly_patterns: Array<{
hour: number;
average_sales: number;
peak_day: string;
}>;
weather_impact: Array<{
condition: string;
average_revenue: number;
impact_factor: number;
}>;
}
export interface SalesImportResult {
imported_records: number;
failed_records: number;
errors: Array<{
row: number;
field: string;
message: string;
}>;
summary: SalesSummary;
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
size: number;
pages: number;
}
class SalesService {
private readonly baseUrl = '/sales';
// Sales data CRUD operations
async getSales(params?: {
page?: number;
size?: number;
start_date?: string;
end_date?: string;
product_name?: string;
category?: string;
min_revenue?: number;
max_revenue?: number;
}): Promise<ApiResponse<PaginatedResponse<SalesData>>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}?${queryParams.toString()}`
: this.baseUrl;
return apiClient.get(url);
}
async getSalesRecord(salesId: string): Promise<ApiResponse<SalesData>> {
return apiClient.get(`${this.baseUrl}/${salesId}`);
}
async createSalesRecord(salesData: SalesCreate): Promise<ApiResponse<SalesData>> {
return apiClient.post(this.baseUrl, salesData);
}
async updateSalesRecord(salesId: string, salesData: SalesUpdate): Promise<ApiResponse<SalesData>> {
return apiClient.put(`${this.baseUrl}/${salesId}`, salesData);
}
async deleteSalesRecord(salesId: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.delete(`${this.baseUrl}/${salesId}`);
}
// Analytics and reporting
async getSalesSummary(params?: {
start_date?: string;
end_date?: string;
groupBy?: 'day' | 'week' | 'month' | 'year';
}): Promise<ApiResponse<SalesSummary>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/summary?${queryParams.toString()}`
: `${this.baseUrl}/summary`;
return apiClient.get(url);
}
async getSalesAnalytics(params?: {
start_date?: string;
end_date?: string;
granularity?: 'daily' | 'weekly' | 'monthly';
}): Promise<ApiResponse<SalesAnalytics>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/analytics?${queryParams.toString()}`
: `${this.baseUrl}/analytics`;
return apiClient.get(url);
}
async getProductPerformance(params?: {
start_date?: string;
end_date?: string;
limit?: number;
sort_by?: 'revenue' | 'quantity' | 'growth';
}): Promise<ApiResponse<Array<{
product_name: string;
category?: string;
total_revenue: number;
total_quantity: number;
average_price: number;
growth_rate: number;
rank: number;
}>>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/products/performance?${queryParams.toString()}`
: `${this.baseUrl}/products/performance`;
return apiClient.get(url);
}
async getDailySalesTrends(params?: {
start_date?: string;
end_date?: string;
product_name?: string;
category?: string;
}): Promise<ApiResponse<Array<{
date: string;
revenue: number;
quantity: number;
orders: number;
average_order_value: number;
}>>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/trends/daily?${queryParams.toString()}`
: `${this.baseUrl}/trends/daily`;
return apiClient.get(url);
}
// Data import and export
async importSalesData(file: File, progressCallback?: (progress: number) => void): Promise<{
status: 'completed' | 'failed' | 'partial';
records_processed: number;
records_created: number;
records_failed: number;
errors: string[];
warnings: string[];
processing_time?: number;
}> {
const response = await apiClient.uploadFile(`${this.baseUrl}/import`, file, progressCallback);
if (!response.success) {
throw new Error(`Sales import failed: ${response.error || response.detail || 'Unknown error'}`);
}
return response.data;
}
async validateSalesData(file: File): Promise<{
is_valid: boolean;
total_records: number;
unique_products: number;
product_list: string[];
errors: string[];
warnings: string[];
summary: {
date_range: string;
total_sales: number;
average_daily_sales: number;
};
}> {
const response = await apiClient.uploadFile(`${this.baseUrl}/import/validate`, file);
if (!response.success) {
throw new Error(`Validation failed: ${response.error || response.detail || 'Unknown error'}`);
}
return response.data;
}
async exportSalesData(params?: {
format?: 'csv' | 'xlsx';
start_date?: string;
end_date?: string;
include_analytics?: boolean;
}): Promise<ApiResponse<{ download_url: string }>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/export?${queryParams.toString()}`
: `${this.baseUrl}/export`;
return apiClient.get(url);
}
// AI and onboarding
async startOnboardingAnalysis(): Promise<ApiResponse<{ task_id: string }>> {
return apiClient.post(`${this.baseUrl}/onboarding/analyze`);
}
async getOnboardingStatus(taskId: string): Promise<ApiResponse<{
status: 'pending' | 'processing' | 'completed' | 'failed';
progress: number;
message: string;
results?: {
data_quality_score: number;
recommendations: string[];
detected_patterns: string[];
suggested_categories: string[];
};
}>> {
return apiClient.get(`${this.baseUrl}/onboarding/status/${taskId}`);
}
async getDataQualityReport(): Promise<ApiResponse<{
overall_score: number;
issues: Array<{
type: string;
severity: 'low' | 'medium' | 'high';
description: string;
affected_records: number;
suggestions: string[];
}>;
completeness: {
required_fields: number;
optional_fields: number;
};
}>> {
return apiClient.get(`${this.baseUrl}/data-quality`);
}
// Comparative analysis
async comparePeriods(params: {
current_start: string;
current_end: string;
comparison_start: string;
comparison_end: string;
metrics?: string[];
}): Promise<ApiResponse<{
current_period: SalesSummary;
comparison_period: SalesSummary;
changes: {
revenue_change: number;
quantity_change: number;
orders_change: number;
profit_change: number;
};
significant_changes: Array<{
metric: string;
change: number;
significance: 'positive' | 'negative' | 'neutral';
}>;
}>> {
return apiClient.post(`${this.baseUrl}/compare-periods`, params);
}
// Weather and external factor analysis
async getWeatherImpact(params?: {
start_date?: string;
end_date?: string;
weather_conditions?: string[];
}): Promise<ApiResponse<Array<{
condition: string;
average_revenue: number;
average_quantity: number;
impact_factor: number;
confidence: number;
}>>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
if (Array.isArray(value)) {
value.forEach(v => queryParams.append(key, v));
} else {
queryParams.append(key, value.toString());
}
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/weather-impact?${queryParams.toString()}`
: `${this.baseUrl}/weather-impact`;
return apiClient.get(url);
}
// Utility methods
getWeatherConditions(): { value: string; label: string }[] {
return [
{ value: 'sunny', label: 'Sunny' },
{ value: 'cloudy', label: 'Cloudy' },
{ value: 'rainy', label: 'Rainy' },
{ value: 'stormy', label: 'Stormy' },
{ value: 'snowy', label: 'Snowy' },
{ value: 'foggy', label: 'Foggy' },
{ value: 'windy', label: 'Windy' },
];
}
getDaysOfWeek(): { value: string; label: string }[] {
return [
{ value: 'monday', label: 'Monday' },
{ value: 'tuesday', label: 'Tuesday' },
{ value: 'wednesday', label: 'Wednesday' },
{ value: 'thursday', label: 'Thursday' },
{ value: 'friday', label: 'Friday' },
{ value: 'saturday', label: 'Saturday' },
{ value: 'sunday', label: 'Sunday' },
];
}
getAnalyticsMetrics(): { value: string; label: string }[] {
return [
{ value: 'revenue', label: 'Revenue' },
{ value: 'quantity', label: 'Quantity Sold' },
{ value: 'orders', label: 'Number of Orders' },
{ value: 'average_order_value', label: 'Average Order Value' },
{ value: 'profit_margin', label: 'Profit Margin' },
{ value: 'customer_count', label: 'Customer Count' },
];
}
getExportFormats(): { value: string; label: string }[] {
return [
{ value: 'csv', label: 'CSV' },
{ value: 'xlsx', label: 'Excel (XLSX)' },
];
}
// Business model guidance (moved from onboarding)
async getBusinessModelGuide(
model: BusinessModelType
): Promise<ApiResponse<BusinessModelGuide>> {
// Return static business model guides since we removed orchestration
const guides = {
[BusinessModelType.PRODUCTION]: {
title: 'Production Bakery Setup',
description: 'Your bakery focuses on creating products from raw ingredients.',
next_steps: [
'Set up ingredient inventory management',
'Configure recipe management',
'Set up production planning',
'Implement quality control processes'
],
recommended_features: [
'Inventory tracking for raw ingredients',
'Recipe costing and management',
'Production scheduling',
'Supplier management'
],
sample_workflows: [
'Daily production planning based on demand forecasts',
'Inventory reordering based on production schedules',
'Quality control checkpoints during production'
]
},
[BusinessModelType.RETAIL]: {
title: 'Retail Bakery Setup',
description: 'Your bakery focuses on selling finished products to customers.',
next_steps: [
'Set up finished product inventory',
'Configure point-of-sale integration',
'Set up customer management',
'Implement sales analytics'
],
recommended_features: [
'Finished product inventory tracking',
'Sales analytics and reporting',
'Customer loyalty programs',
'Promotional campaign management'
],
sample_workflows: [
'Daily sales reporting and analysis',
'Inventory reordering based on sales velocity',
'Customer engagement and retention campaigns'
]
},
[BusinessModelType.HYBRID]: {
title: 'Hybrid Bakery Setup',
description: 'Your bakery combines production and retail operations.',
next_steps: [
'Set up both ingredient and finished product inventory',
'Configure production-to-retail workflows',
'Set up integrated analytics',
'Implement comprehensive supplier management'
],
recommended_features: [
'Dual inventory management system',
'Production-to-sales analytics',
'Integrated supplier and customer management',
'Cross-channel reporting'
],
sample_workflows: [
'Production planning based on both wholesale and retail demand',
'Integrated inventory management across production and retail',
'Comprehensive business intelligence and reporting'
]
}
};
const guide = guides[model] || guides[BusinessModelType.HYBRID];
return { success: true, data: guide, message: 'Business model guide retrieved successfully' };
}
// Template download utility (moved from onboarding)
downloadTemplate(templateData: TemplateData, filename: string, format: 'csv' | 'json' = 'csv'): void {
let content: string;
let mimeType: string;
if (format === 'csv') {
content = typeof templateData.template === 'string' ? templateData.template : JSON.stringify(templateData.template);
mimeType = 'text/csv';
} else {
content = JSON.stringify(templateData.template, null, 2);
mimeType = 'application/json';
}
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
}
export { SalesService };
export const salesService = new SalesService();

View File

@@ -1,481 +0,0 @@
/**
* Subscription Service
* Handles API calls for subscription management, billing, and plan limits
*/
import { ApiClient } from './client';
import { isMockMode, getMockSubscription } from '../../config/mock.config';
export interface SubscriptionLimits {
plan: string;
max_users: number;
max_locations: number;
max_products: number;
features: Record<string, any>;
}
export interface UsageSummary {
plan: string;
monthly_price: number;
status: string;
usage: {
users: {
current: number;
limit: number;
unlimited: boolean;
usage_percentage: number;
};
locations: {
current: number;
limit: number;
unlimited: boolean;
usage_percentage: number;
};
products: {
current: number;
limit: number;
unlimited: boolean;
usage_percentage: number;
};
};
features: Record<string, any>;
next_billing_date?: string;
trial_ends_at?: string;
}
export interface LimitCheckResult {
can_add: boolean;
current_count?: number;
max_allowed?: number;
reason: string;
}
export interface FeatureCheckResult {
has_feature: boolean;
feature_value?: any;
plan: string;
reason: string;
}
export interface PlanUpgradeValidation {
can_upgrade: boolean;
current_plan?: string;
new_plan?: string;
price_change?: number;
new_features?: Record<string, any>;
reason: string;
}
export interface AvailablePlan {
name: string;
description: string;
monthly_price: number;
max_users: number;
max_locations: number;
max_products: number;
features: Record<string, any>;
trial_available: boolean;
popular?: boolean;
contact_sales?: boolean;
}
export interface AvailablePlans {
plans: Record<string, AvailablePlan>;
}
export interface BillingHistoryItem {
id: string;
date: string;
amount: number;
status: 'paid' | 'pending' | 'failed';
description: string;
}
export interface SubscriptionData {
id: string;
tenant_id: string;
plan: string;
status: string;
monthly_price: number;
currency: string;
billing_cycle: string;
current_period_start: string;
current_period_end: string;
next_billing_date: string;
trial_ends_at?: string | null;
canceled_at?: string | null;
created_at: string;
updated_at: string;
max_users: number;
max_locations: number;
max_products: number;
features: Record<string, any>;
usage: {
users: number;
locations: number;
products: number;
storage_gb: number;
api_calls_month: number;
reports_generated: number;
};
billing_history: BillingHistoryItem[];
}
class SubscriptionService {
private apiClient: ApiClient;
constructor() {
this.apiClient = new ApiClient();
}
/**
* Get current subscription limits for a tenant
*/
async getSubscriptionLimits(tenantId: string): Promise<SubscriptionLimits> {
if (isMockMode()) {
const mockSub = getMockSubscription();
return {
plan: mockSub.plan,
max_users: mockSub.max_users,
max_locations: mockSub.max_locations,
max_products: mockSub.max_products,
features: mockSub.features
};
}
const response = await this.apiClient.get(`/subscriptions/${tenantId}/limits`);
return response.data;
}
/**
* Get usage summary vs limits for a tenant
*/
async getUsageSummary(tenantId: string): Promise<UsageSummary> {
if (isMockMode()) {
console.log('🧪 Mock mode: Returning usage summary for tenant:', tenantId);
const mockSub = getMockSubscription();
return {
plan: mockSub.plan,
monthly_price: mockSub.monthly_price,
status: mockSub.status,
usage: {
users: {
current: mockSub.usage.users,
limit: mockSub.max_users,
unlimited: mockSub.max_users === -1,
usage_percentage: mockSub.max_users === -1 ? 0 : Math.round((mockSub.usage.users / mockSub.max_users) * 100)
},
locations: {
current: mockSub.usage.locations,
limit: mockSub.max_locations,
unlimited: mockSub.max_locations === -1,
usage_percentage: mockSub.max_locations === -1 ? 0 : Math.round((mockSub.usage.locations / mockSub.max_locations) * 100)
},
products: {
current: mockSub.usage.products,
limit: mockSub.max_products,
unlimited: mockSub.max_products === -1,
usage_percentage: mockSub.max_products === -1 ? 0 : Math.round((mockSub.usage.products / mockSub.max_products) * 100)
}
},
features: mockSub.features,
next_billing_date: mockSub.next_billing_date,
trial_ends_at: mockSub.trial_ends_at
};
}
const response = await this.apiClient.get(`/subscriptions/${tenantId}/usage`);
return response.data;
}
/**
* Check if tenant can add another location
*/
async canAddLocation(tenantId: string): Promise<LimitCheckResult> {
if (isMockMode()) {
const mockSub = getMockSubscription();
const canAdd = mockSub.max_locations === -1 || mockSub.usage.locations < mockSub.max_locations;
return {
can_add: canAdd,
current_count: mockSub.usage.locations,
max_allowed: mockSub.max_locations,
reason: canAdd ? 'Can add more locations' : 'Location limit reached. Upgrade plan to add more locations.'
};
}
const response = await this.apiClient.get(`/subscriptions/${tenantId}/can-add-location`);
return response.data;
}
/**
* Check if tenant can add another product
*/
async canAddProduct(tenantId: string): Promise<LimitCheckResult> {
if (isMockMode()) {
const mockSub = getMockSubscription();
const canAdd = mockSub.max_products === -1 || mockSub.usage.products < mockSub.max_products;
return {
can_add: canAdd,
current_count: mockSub.usage.products,
max_allowed: mockSub.max_products,
reason: canAdd ? 'Can add more products' : 'Product limit reached. Upgrade plan to add more products.'
};
}
const response = await this.apiClient.get(`/subscriptions/${tenantId}/can-add-product`);
return response.data;
}
/**
* Check if tenant can add another user/member
*/
async canAddUser(tenantId: string): Promise<LimitCheckResult> {
if (isMockMode()) {
const mockSub = getMockSubscription();
const canAdd = mockSub.max_users === -1 || mockSub.usage.users < mockSub.max_users;
return {
can_add: canAdd,
current_count: mockSub.usage.users,
max_allowed: mockSub.max_users,
reason: canAdd ? 'Can add more users' : 'User limit reached. Upgrade plan to add more users.'
};
}
const response = await this.apiClient.get(`/subscriptions/${tenantId}/can-add-user`);
return response.data;
}
/**
* Check if tenant has access to a specific feature
*/
async hasFeature(tenantId: string, feature: string): Promise<FeatureCheckResult> {
if (isMockMode()) {
const mockSub = getMockSubscription();
const hasFeature = feature in mockSub.features;
return {
has_feature: hasFeature,
feature_value: hasFeature ? mockSub.features[feature] : null,
plan: mockSub.plan,
reason: hasFeature ? `Feature ${feature} is available in ${mockSub.plan} plan` : `Feature ${feature} is not available in ${mockSub.plan} plan`
};
}
const response = await this.apiClient.get(`/subscriptions/${tenantId}/features/${feature}`);
return response.data;
}
/**
* Validate if tenant can upgrade to a new plan
*/
async validatePlanUpgrade(tenantId: string, newPlan: string): Promise<PlanUpgradeValidation> {
const response = await this.apiClient.get(`/subscriptions/${tenantId}/validate-upgrade/${newPlan}`);
return response.data;
}
/**
* Upgrade subscription plan for a tenant
*/
async upgradePlan(tenantId: string, newPlan: string): Promise<{
success: boolean;
message: string;
validation: PlanUpgradeValidation;
}> {
const response = await this.apiClient.post(`/subscriptions/${tenantId}/upgrade`, null, {
params: { new_plan: newPlan }
});
return response.data;
}
/**
* Get full subscription data including billing history for admin@bakery.com
*/
async getSubscriptionData(tenantId: string): Promise<SubscriptionData> {
if (isMockMode()) {
return getMockSubscription();
}
const response = await this.apiClient.get(`/subscriptions/${tenantId}/details`);
return response.data;
}
/**
* Get billing history for a subscription
*/
async getBillingHistory(tenantId: string): Promise<BillingHistoryItem[]> {
if (isMockMode()) {
return getMockSubscription().billing_history;
}
const response = await this.apiClient.get(`/subscriptions/${tenantId}/billing-history`);
return response.data;
}
/**
* Get all available subscription plans with features and pricing
*/
async getAvailablePlans(): Promise<AvailablePlans> {
if (isMockMode()) {
return {
plans: {
starter: {
name: 'Starter',
description: 'Perfecto para panaderías pequeñas que están comenzando',
monthly_price: 49.0,
max_users: 5,
max_locations: 1,
max_products: 50,
features: {
inventory_management: 'basic',
demand_prediction: 'basic',
production_reports: 'basic',
analytics: 'basic',
support: 'email',
trial_days: 14,
locations: '1_location'
},
trial_available: true
},
professional: {
name: 'Professional',
description: 'Para panaderías en crecimiento que necesitan más control',
monthly_price: 129.0,
max_users: 15,
max_locations: 2,
max_products: -1,
features: {
inventory_management: 'advanced',
demand_prediction: 'ai_92_percent',
production_management: 'complete',
pos_integrated: true,
logistics: 'basic',
analytics: 'advanced',
support: 'priority_24_7',
trial_days: 14,
locations: '1_2_locations'
},
trial_available: true,
popular: true
},
enterprise: {
name: 'Enterprise',
description: 'Para cadenas de panaderías con necesidades avanzadas',
monthly_price: 399.0,
max_users: -1,
max_locations: -1,
max_products: -1,
features: {
inventory_management: 'multi_location',
demand_prediction: 'ai_personalized',
production_optimization: 'capacity',
erp_integration: true,
logistics: 'advanced',
analytics: 'predictive',
api_access: 'personalized',
account_manager: true,
demo: 'personalized',
locations: 'unlimited_obradores'
},
trial_available: false,
contact_sales: true
}
}
};
}
const response = await this.apiClient.get('/plans/available');
return response.data;
}
/**
* Helper method to check if a feature is enabled for current tenant
*/
async isFeatureEnabled(tenantId: string, feature: string): Promise<boolean> {
try {
const result = await this.hasFeature(tenantId, feature);
return result.has_feature;
} catch (error) {
console.error(`Error checking feature ${feature}:`, error);
return false;
}
}
/**
* Helper method to get feature level (basic, advanced, etc.)
*/
async getFeatureLevel(tenantId: string, feature: string): Promise<string | null> {
try {
const result = await this.hasFeature(tenantId, feature);
return result.feature_value || null;
} catch (error) {
console.error(`Error getting feature level for ${feature}:`, error);
return null;
}
}
/**
* Helper method to check if usage is approaching limits
*/
async isUsageNearLimit(tenantId: string, threshold: number = 80): Promise<{
users: boolean;
locations: boolean;
products: boolean;
}> {
try {
const usage = await this.getUsageSummary(tenantId);
return {
users: !usage.usage.users.unlimited && usage.usage.users.usage_percentage >= threshold,
locations: !usage.usage.locations.unlimited && usage.usage.locations.usage_percentage >= threshold,
products: !usage.usage.products.unlimited && usage.usage.products.usage_percentage >= threshold,
};
} catch (error) {
console.error('Error checking usage limits:', error);
return { users: false, locations: false, products: false };
}
}
/**
* Helper method to format pricing for display
*/
formatPrice(price: number, currency: string = 'EUR'): string {
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: currency,
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}).format(price);
}
/**
* Helper method to get plan display information
*/
getPlanDisplayInfo(plan: string): {
name: string;
color: string;
badge?: string;
} {
const planInfo = {
starter: {
name: 'Starter',
color: 'blue',
},
professional: {
name: 'Professional',
color: 'purple',
badge: 'Más Popular'
},
enterprise: {
name: 'Enterprise',
color: 'gold',
}
};
return planInfo[plan as keyof typeof planInfo] || {
name: plan,
color: 'gray'
};
}
}
export const subscriptionService = new SubscriptionService();
export default subscriptionService;

View File

@@ -1,313 +0,0 @@
import { apiClient, ApiResponse } from './client';
// Request/Response Types based on backend schemas - UPDATED TO MATCH BACKEND
export interface BakeryRegistration {
name: string;
address: string;
city?: string;
postal_code: string;
phone: string;
business_type?: string;
business_model?: string;
}
export interface TenantResponse {
id: string;
name: string;
subdomain?: string;
business_type: string;
business_model?: string;
address: string;
city: string;
postal_code: string;
phone?: string;
is_active: boolean;
subscription_tier: string;
model_trained: boolean;
last_training_date?: string;
owner_id: string;
created_at: string;
}
export interface TenantUpdate {
name?: string;
address?: string;
phone?: string;
business_type?: string;
business_model?: string;
}
export interface TenantAccessResponse {
has_access: boolean;
role: string;
permissions: string[];
}
export interface TenantMemberResponse {
id: string;
user_id: string;
role: string;
is_active: boolean;
joined_at?: string;
}
export interface TenantMemberInvitation {
email: string;
role: 'admin' | 'member' | 'viewer';
message?: string;
}
export interface TenantMemberUpdate {
role?: 'owner' | 'admin' | 'member' | 'viewer';
is_active?: boolean;
}
export interface TenantSubscriptionUpdate {
plan: 'basic' | 'professional' | 'enterprise';
billing_cycle?: 'monthly' | 'yearly';
}
export interface TenantStatsResponse {
tenant_id: string;
total_members: number;
active_members: number;
total_predictions: number;
models_trained: number;
last_training_date?: string;
subscription_plan: string;
subscription_status: string;
}
export interface TenantListResponse {
tenants: TenantResponse[];
total: number;
page: number;
per_page: number;
has_next: boolean;
has_prev: boolean;
}
export interface TenantSearchRequest {
query?: string;
business_type?: string;
city?: string;
status?: string;
limit?: number;
offset?: number;
}
class TenantService {
private readonly baseUrl = '/tenants';
// Tenant CRUD operations
async createTenant(tenantData: BakeryRegistration): Promise<ApiResponse<TenantResponse>> {
return apiClient.post(`${this.baseUrl}/register`, tenantData);
}
async getTenant(tenantId: string): Promise<ApiResponse<TenantResponse>> {
return apiClient.get(`${this.baseUrl}/${tenantId}`);
}
async getCurrentTenant(): Promise<ApiResponse<TenantResponse>> {
return apiClient.get(`${this.baseUrl}/current`);
}
async updateTenant(tenantId: string, tenantData: TenantUpdate): Promise<ApiResponse<TenantResponse>> {
return apiClient.put(`${this.baseUrl}/${tenantId}`, tenantData);
}
async deleteTenant(tenantId: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.delete(`${this.baseUrl}/${tenantId}`);
}
async listTenants(params?: {
page?: number;
per_page?: number;
search?: string;
business_type?: string;
city?: string;
status?: string;
}): Promise<ApiResponse<TenantListResponse>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}?${queryParams.toString()}`
: this.baseUrl;
return apiClient.get(url);
}
// Tenant access and verification
async checkTenantAccess(tenantId: string): Promise<ApiResponse<TenantAccessResponse>> {
return apiClient.get(`${this.baseUrl}/${tenantId}/access`);
}
async switchTenant(tenantId: string): Promise<ApiResponse<{ message: string; tenant: TenantResponse }>> {
// Frontend-only tenant switching since backend doesn't have this endpoint
// We'll simulate the response and update the tenant store directly
try {
const tenant = await this.getTenant(tenantId);
if (tenant.success) {
// Update API client tenant context
apiClient.setTenantId(tenantId);
return {
success: true,
data: {
message: 'Tenant switched successfully',
tenant: tenant.data
}
};
} else {
throw new Error('Tenant not found');
}
} catch (error) {
return {
success: false,
data: null,
error: error instanceof Error ? error.message : 'Failed to switch tenant'
};
}
}
// Member management
async getTenantMembers(tenantId: string): Promise<ApiResponse<TenantMemberResponse[]>> {
return apiClient.get(`${this.baseUrl}/${tenantId}/members`);
}
async inviteMember(tenantId: string, invitation: TenantMemberInvitation): Promise<ApiResponse<{ message: string; invitation_id: string }>> {
return apiClient.post(`${this.baseUrl}/${tenantId}/members/invite`, invitation);
}
async updateMember(tenantId: string, memberId: string, memberData: TenantMemberUpdate): Promise<ApiResponse<TenantMemberResponse>> {
return apiClient.put(`${this.baseUrl}/${tenantId}/members/${memberId}`, memberData);
}
async removeMember(tenantId: string, memberId: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.delete(`${this.baseUrl}/${tenantId}/members/${memberId}`);
}
async acceptInvitation(invitationToken: string): Promise<ApiResponse<{ message: string; tenant: TenantResponse }>> {
return apiClient.post(`${this.baseUrl}/invitations/${invitationToken}/accept`);
}
async rejectInvitation(invitationToken: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.post(`${this.baseUrl}/invitations/${invitationToken}/reject`);
}
// Subscription management
async updateSubscription(tenantId: string, subscriptionData: TenantSubscriptionUpdate): Promise<ApiResponse<{ message: string; subscription: any }>> {
return apiClient.put(`${this.baseUrl}/${tenantId}/subscription`, subscriptionData);
}
async getTenantStats(tenantId: string): Promise<ApiResponse<TenantStatsResponse>> {
return apiClient.get(`${this.baseUrl}/${tenantId}/stats`);
}
// Settings and configuration
async getTenantSettings(tenantId: string): Promise<ApiResponse<any>> {
return apiClient.get(`${this.baseUrl}/${tenantId}/settings`);
}
async updateTenantSettings(tenantId: string, settings: any): Promise<ApiResponse<any>> {
return apiClient.put(`${this.baseUrl}/${tenantId}/settings`, settings);
}
// Search and filtering
async searchTenants(searchParams: TenantSearchRequest): Promise<ApiResponse<TenantListResponse>> {
const queryParams = new URLSearchParams();
Object.entries(searchParams).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
queryParams.append(key, value.toString());
}
});
return apiClient.get(`${this.baseUrl}/search?${queryParams.toString()}`);
}
// Health and status checks
async getTenantHealth(tenantId: string): Promise<ApiResponse<{
status: string;
last_activity: string;
services_status: Record<string, string>;
}>> {
return apiClient.get(`${this.baseUrl}/${tenantId}/health`);
}
// Utility methods
async getUserTenants(userId?: string): Promise<ApiResponse<TenantResponse[]>> {
// If no userId provided, we'll get it from the auth store/token
return apiClient.get(`${this.baseUrl}/users/${userId || 'current'}`);
}
async validateTenantSlug(slug: string): Promise<ApiResponse<{ available: boolean; suggestions?: string[] }>> {
return apiClient.get(`${this.baseUrl}/validate-slug/${slug}`);
}
// Local state management helpers - Now uses tenant store
getCurrentTenantId(): string | null {
// This will be handled by the tenant store
return null;
}
getCurrentTenantData(): TenantResponse | null {
// This will be handled by the tenant store
return null;
}
setCurrentTenant(tenant: TenantResponse) {
// This will be handled by the tenant store
apiClient.setTenantId(tenant.id);
}
clearCurrentTenant() {
// This will be handled by the tenant store
}
// Business type helpers
getBusinessTypes(): { value: string; label: string }[] {
return [
{ value: 'bakery', label: 'Bakery' },
{ value: 'coffee_shop', label: 'Coffee Shop' },
{ value: 'pastry_shop', label: 'Pastry Shop' },
{ value: 'restaurant', label: 'Restaurant' }
];
}
getBusinessModels(): { value: string; label: string }[] {
return [
{ value: 'individual_bakery', label: 'Individual Bakery' },
{ value: 'central_baker_satellite', label: 'Central Baker with Satellites' },
{ value: 'retail_bakery', label: 'Retail Bakery' },
{ value: 'hybrid_bakery', label: 'Hybrid Bakery' }
];
}
getSubscriptionTiers(): { value: string; label: string; description: string }[] {
return [
{ value: 'basic', label: 'Basic', description: 'Essential features for small bakeries' },
{ value: 'professional', label: 'Professional', description: 'Advanced features for growing businesses' },
{ value: 'enterprise', label: 'Enterprise', description: 'Full suite for large operations' }
];
}
getMemberRoles(): { value: string; label: string; description: string }[] {
return [
{ value: 'owner', label: 'Owner', description: 'Full access to all features and settings' },
{ value: 'admin', label: 'Admin', description: 'Manage users and most settings' },
{ value: 'member', label: 'Member', description: 'Access to operational features' },
{ value: 'viewer', label: 'Viewer', description: 'Read-only access to reports and data' }
];
}
}
export const tenantService = new TenantService();

View File

@@ -1,76 +0,0 @@
/**
* Training service for ML model training operations
*/
import { apiClient } from './client';
import { ApiResponse } from '../../types/api.types';
import {
TrainingJob,
TrainingJobCreate,
TrainingJobUpdate
} from '../../types/training.types';
export class TrainingService {
private getTenantId(): string {
const tenantStorage = localStorage.getItem('tenant-storage');
if (tenantStorage) {
try {
const { state } = JSON.parse(tenantStorage);
return state?.currentTenant?.id;
} catch {
return '';
}
}
return '';
}
private getBaseUrl(): string {
return '/training';
}
async getTrainingJobs(modelId?: string): Promise<ApiResponse<TrainingJob[]>> {
const params = modelId ? { model_id: modelId } : {};
const queryParams = new URLSearchParams();
if (params.model_id) {
queryParams.append('model_id', params.model_id);
}
const url = queryParams.toString()
? `${this.getBaseUrl()}/jobs?${queryParams.toString()}`
: `${this.getBaseUrl()}/jobs`;
return apiClient.get(url);
}
async getTrainingJob(id: string): Promise<ApiResponse<TrainingJob>> {
return apiClient.get(`${this.getBaseUrl()}/jobs/${id}`);
}
async createTrainingJob(data: TrainingJobCreate): Promise<ApiResponse<TrainingJob>> {
return apiClient.post(`${this.getBaseUrl()}/jobs`, data);
}
async updateTrainingJob(id: string, data: TrainingJobUpdate): Promise<ApiResponse<TrainingJob>> {
return apiClient.put(`${this.getBaseUrl()}/jobs/${id}`, data);
}
async deleteTrainingJob(id: string): Promise<ApiResponse<void>> {
return apiClient.delete(`${this.getBaseUrl()}/jobs/${id}`);
}
async startTraining(id: string): Promise<ApiResponse<TrainingJob>> {
return apiClient.post(`${this.getBaseUrl()}/jobs/${id}/start`);
}
async stopTraining(id: string): Promise<ApiResponse<TrainingJob>> {
return apiClient.post(`${this.getBaseUrl()}/jobs/${id}/stop`);
}
async getTrainingLogs(id: string): Promise<ApiResponse<string[]>> {
return apiClient.get(`${this.getBaseUrl()}/jobs/${id}/logs`);
}
async getTrainingMetrics(id: string): Promise<ApiResponse<Record<string, number>>> {
return apiClient.get(`${this.getBaseUrl()}/jobs/${id}/metrics`);
}
}
export const trainingService = new TrainingService();