Start integrating the onboarding flow with backend 6
This commit is contained in:
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -1 +0,0 @@
|
||||
/Users/urtzialfaro/Documents/bakery-ia/frontend/src/services/api/orders.service.ts
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user