ADD new frontend
This commit is contained in:
251
frontend/src/services/api/auth.service.ts
Normal file
251
frontend/src/services/api/auth.service.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { apiClient, ApiResponse } from './client';
|
||||
|
||||
// Request/Response Types based on backend schemas
|
||||
export interface UserRegistration {
|
||||
email: string;
|
||||
password: string;
|
||||
full_name: string;
|
||||
tenant_name?: string;
|
||||
role?: 'user' | 'admin' | 'manager';
|
||||
}
|
||||
|
||||
export interface UserLogin {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface UserData {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name: string;
|
||||
is_active: boolean;
|
||||
is_verified: boolean;
|
||||
created_at: string;
|
||||
tenant_id?: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
export interface TokenResponse {
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
user?: UserData;
|
||||
}
|
||||
|
||||
export interface RefreshTokenRequest {
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export interface PasswordChange {
|
||||
current_password: string;
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
export interface PasswordReset {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface PasswordResetConfirm {
|
||||
token: string;
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
export interface TokenVerification {
|
||||
valid: boolean;
|
||||
user_id?: string;
|
||||
email?: string;
|
||||
exp?: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface UserResponse {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name: string;
|
||||
is_active: boolean;
|
||||
is_verified: boolean;
|
||||
created_at: string;
|
||||
last_login?: string;
|
||||
phone?: string;
|
||||
language?: string;
|
||||
timezone?: string;
|
||||
tenant_id?: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
export interface UserUpdate {
|
||||
full_name?: string;
|
||||
phone?: string;
|
||||
language?: string;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
class AuthService {
|
||||
private readonly baseUrl = '/auth';
|
||||
|
||||
// Authentication endpoints
|
||||
async register(userData: UserRegistration): Promise<ApiResponse<TokenResponse>> {
|
||||
return apiClient.post(`${this.baseUrl}/register`, userData);
|
||||
}
|
||||
|
||||
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
|
||||
private handleSuccessfulAuth(tokenData: TokenResponse) {
|
||||
localStorage.setItem('access_token', tokenData.access_token);
|
||||
|
||||
if (tokenData.refresh_token) {
|
||||
localStorage.setItem('refresh_token', tokenData.refresh_token);
|
||||
}
|
||||
|
||||
if (tokenData.user) {
|
||||
localStorage.setItem('user_data', JSON.stringify(tokenData.user));
|
||||
|
||||
if (tokenData.user.tenant_id) {
|
||||
localStorage.setItem('tenant_id', tokenData.user.tenant_id);
|
||||
apiClient.setTenantId(tokenData.user.tenant_id);
|
||||
}
|
||||
}
|
||||
|
||||
apiClient.setAuthToken(tokenData.access_token);
|
||||
}
|
||||
|
||||
private clearAuthData() {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
localStorage.removeItem('user_data');
|
||||
localStorage.removeItem('tenant_id');
|
||||
apiClient.removeAuthToken();
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
isAuthenticated(): boolean {
|
||||
return !!localStorage.getItem('access_token');
|
||||
}
|
||||
|
||||
getCurrentUserData(): UserData | null {
|
||||
const userData = localStorage.getItem('user_data');
|
||||
return userData ? JSON.parse(userData) : null;
|
||||
}
|
||||
|
||||
getAccessToken(): string | null {
|
||||
return localStorage.getItem('access_token');
|
||||
}
|
||||
|
||||
getRefreshToken(): string | null {
|
||||
return localStorage.getItem('refresh_token');
|
||||
}
|
||||
|
||||
getTenantId(): string | null {
|
||||
return localStorage.getItem('tenant_id');
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
export const authService = new AuthService();
|
||||
221
frontend/src/services/api/client.ts
Normal file
221
frontend/src/services/api/client.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import axios, { AxiosInstance, AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
data: T;
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ErrorDetail {
|
||||
message: string;
|
||||
code?: string;
|
||||
field?: string;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
success: boolean;
|
||||
error: ErrorDetail;
|
||||
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();
|
||||
}
|
||||
|
||||
private setupInterceptors(): void {
|
||||
// Request interceptor - add auth token and tenant ID
|
||||
this.axiosInstance.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const tenantId = localStorage.getItem('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 refreshToken = localStorage.getItem('refresh_token');
|
||||
if (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: ApiError = {
|
||||
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 refreshToken = localStorage.getItem('refresh_token');
|
||||
if (!refreshToken) {
|
||||
throw new Error('No refresh token available');
|
||||
}
|
||||
|
||||
const response = await axios.post(`${API_BASE_URL}/auth/refresh`, {
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
|
||||
const { access_token, refresh_token } = response.data;
|
||||
localStorage.setItem('access_token', access_token);
|
||||
if (refresh_token) {
|
||||
localStorage.setItem('refresh_token', refresh_token);
|
||||
}
|
||||
|
||||
return access_token;
|
||||
}
|
||||
|
||||
private handleAuthFailure() {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
localStorage.removeItem('user_data');
|
||||
localStorage.removeItem('tenant_id');
|
||||
|
||||
// Redirect to login
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
// HTTP Methods with consistent response format
|
||||
async get<T = any>(url: string, config = {}): Promise<ApiResponse<T>> {
|
||||
const response = await this.axiosInstance.get(url, config);
|
||||
return this.transformResponse(response);
|
||||
}
|
||||
|
||||
async post<T = any>(url: string, data = {}, config = {}): Promise<ApiResponse<T>> {
|
||||
const response = await this.axiosInstance.post(url, data, config);
|
||||
return this.transformResponse(response);
|
||||
}
|
||||
|
||||
async put<T = any>(url: string, data = {}, config = {}): Promise<ApiResponse<T>> {
|
||||
const response = await this.axiosInstance.put(url, data, config);
|
||||
return this.transformResponse(response);
|
||||
}
|
||||
|
||||
async patch<T = any>(url: string, data = {}, config = {}): Promise<ApiResponse<T>> {
|
||||
const response = await this.axiosInstance.patch(url, data, config);
|
||||
return this.transformResponse(response);
|
||||
}
|
||||
|
||||
async delete<T = any>(url: string, config = {}): Promise<ApiResponse<T>> {
|
||||
const response = await this.axiosInstance.delete(url, config);
|
||||
return this.transformResponse(response);
|
||||
}
|
||||
|
||||
// File upload helper
|
||||
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 response = await this.axiosInstance.post(url, 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
|
||||
setAuthToken(token: string) {
|
||||
localStorage.setItem('access_token', token);
|
||||
this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
removeAuthToken() {
|
||||
localStorage.removeItem('access_token');
|
||||
delete this.axiosInstance.defaults.headers.common['Authorization'];
|
||||
}
|
||||
|
||||
setTenantId(tenantId: string) {
|
||||
localStorage.setItem('tenant_id', tenantId);
|
||||
this.axiosInstance.defaults.headers.common['X-Tenant-ID'] = tenantId;
|
||||
}
|
||||
|
||||
getBaseURL(): string {
|
||||
return API_BASE_URL;
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient();
|
||||
395
frontend/src/services/api/data.service.ts
Normal file
395
frontend/src/services/api/data.service.ts
Normal file
@@ -0,0 +1,395 @@
|
||||
import { apiClient, ApiResponse } from './client';
|
||||
|
||||
// External data types
|
||||
export interface WeatherData {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
location_id: string;
|
||||
date: string;
|
||||
temperature_avg: number;
|
||||
temperature_min: number;
|
||||
temperature_max: number;
|
||||
humidity: number;
|
||||
precipitation: number;
|
||||
wind_speed: number;
|
||||
condition: string;
|
||||
description: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface TrafficData {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
location_id: string;
|
||||
date: string;
|
||||
hour: number;
|
||||
traffic_level: number;
|
||||
congestion_index: number;
|
||||
average_speed: number;
|
||||
incident_count: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface EventData {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
location_id: string;
|
||||
event_name: string;
|
||||
event_type: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
expected_attendance?: number;
|
||||
impact_radius_km?: number;
|
||||
impact_score: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface LocationConfig {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
name: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
address: string;
|
||||
city: string;
|
||||
country: string;
|
||||
is_primary: boolean;
|
||||
data_sources: {
|
||||
weather_enabled: boolean;
|
||||
traffic_enabled: boolean;
|
||||
events_enabled: boolean;
|
||||
};
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
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: {
|
||||
name: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
address: string;
|
||||
city: string;
|
||||
country?: string;
|
||||
is_primary?: boolean;
|
||||
data_sources?: LocationConfig['data_sources'];
|
||||
}): 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<{ message: string }>> {
|
||||
return apiClient.delete(`${this.baseUrl}/locations/${locationId}`);
|
||||
}
|
||||
|
||||
// Weather data
|
||||
async getWeatherData(params?: {
|
||||
location_id?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}): 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<{ message: string; updated_records: number }>> {
|
||||
const url = locationId
|
||||
? `${this.baseUrl}/weather/refresh/${locationId}`
|
||||
: `${this.baseUrl}/weather/refresh`;
|
||||
|
||||
return apiClient.post(url);
|
||||
}
|
||||
|
||||
// Traffic data
|
||||
async getTrafficData(params?: {
|
||||
location_id?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
hour?: number;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}): 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?: {
|
||||
days_back?: number;
|
||||
granularity?: 'hourly' | 'daily';
|
||||
}): Promise<ApiResponse<Array<{
|
||||
period: string;
|
||||
average_traffic_level: number;
|
||||
peak_hours: number[];
|
||||
congestion_patterns: Record<string, 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/patterns/${locationId}?${queryParams.toString()}`
|
||||
: `${this.baseUrl}/traffic/patterns/${locationId}`;
|
||||
|
||||
return apiClient.get(url);
|
||||
}
|
||||
|
||||
async refreshTrafficData(locationId?: string): Promise<ApiResponse<{ message: string; updated_records: number }>> {
|
||||
const url = locationId
|
||||
? `${this.baseUrl}/traffic/refresh/${locationId}`
|
||||
: `${this.baseUrl}/traffic/refresh`;
|
||||
|
||||
return apiClient.post(url);
|
||||
}
|
||||
|
||||
// Events data
|
||||
async getEvents(params?: {
|
||||
location_id?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
event_type?: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}): 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: {
|
||||
location_id: string;
|
||||
event_name: string;
|
||||
event_type: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
expected_attendance?: number;
|
||||
impact_radius_km?: number;
|
||||
impact_score?: number;
|
||||
}): 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<{ message: string }>> {
|
||||
return apiClient.delete(`${this.baseUrl}/events/${eventId}`);
|
||||
}
|
||||
|
||||
async refreshEventsData(locationId?: string): Promise<ApiResponse<{ message: string; updated_records: number }>> {
|
||||
const url = locationId
|
||||
? `${this.baseUrl}/events/refresh/${locationId}`
|
||||
: `${this.baseUrl}/events/refresh`;
|
||||
|
||||
return apiClient.post(url);
|
||||
}
|
||||
|
||||
// Combined analytics
|
||||
async getExternalFactorsImpact(params?: {
|
||||
location_id?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}): Promise<ApiResponse<{
|
||||
weather_impact: {
|
||||
temperature_correlation: number;
|
||||
precipitation_impact: number;
|
||||
most_favorable_conditions: string;
|
||||
};
|
||||
traffic_impact: {
|
||||
congestion_correlation: number;
|
||||
peak_traffic_effect: number;
|
||||
optimal_traffic_levels: number[];
|
||||
};
|
||||
events_impact: {
|
||||
positive_events: EventData[];
|
||||
negative_events: EventData[];
|
||||
average_event_boost: 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}/impact-analysis?${queryParams.toString()}`
|
||||
: `${this.baseUrl}/impact-analysis`;
|
||||
|
||||
return apiClient.get(url);
|
||||
}
|
||||
|
||||
async getDataQualityReport(): Promise<ApiResponse<{
|
||||
overall_score: number;
|
||||
data_sources: Array<{
|
||||
source: 'weather' | 'traffic' | 'events';
|
||||
completeness: number;
|
||||
freshness_hours: number;
|
||||
reliability_score: number;
|
||||
last_update: string;
|
||||
}>;
|
||||
recommendations: Array<{
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
message: string;
|
||||
action: string;
|
||||
}>;
|
||||
}>> {
|
||||
return apiClient.get(`${this.baseUrl}/quality-report`);
|
||||
}
|
||||
|
||||
// Data configuration
|
||||
async getDataSettings(): Promise<ApiResponse<{
|
||||
auto_refresh_enabled: boolean;
|
||||
refresh_intervals: {
|
||||
weather_minutes: number;
|
||||
traffic_minutes: number;
|
||||
events_hours: number;
|
||||
};
|
||||
data_retention_days: {
|
||||
weather: number;
|
||||
traffic: number;
|
||||
events: number;
|
||||
};
|
||||
external_apis: {
|
||||
weather_provider: string;
|
||||
traffic_provider: string;
|
||||
events_provider: string;
|
||||
};
|
||||
}>> {
|
||||
return apiClient.get(`${this.baseUrl}/settings`);
|
||||
}
|
||||
|
||||
async updateDataSettings(settings: {
|
||||
auto_refresh_enabled?: boolean;
|
||||
refresh_intervals?: {
|
||||
weather_minutes?: number;
|
||||
traffic_minutes?: number;
|
||||
events_hours?: number;
|
||||
};
|
||||
data_retention_days?: {
|
||||
weather?: number;
|
||||
traffic?: number;
|
||||
events?: number;
|
||||
};
|
||||
}): Promise<ApiResponse<any>> {
|
||||
return apiClient.put(`${this.baseUrl}/settings`, settings);
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
getWeatherConditions(): { value: string; label: string; impact: 'positive' | 'negative' | 'neutral' }[] {
|
||||
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(): { value: string; label: string; typical_impact: 'positive' | 'negative' | 'neutral' }[] {
|
||||
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(): { value: number; label: string; suitable_for: string[] }[] {
|
||||
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();
|
||||
325
frontend/src/services/api/forecasting.service.ts
Normal file
325
frontend/src/services/api/forecasting.service.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
import { apiClient, ApiResponse } from './client';
|
||||
|
||||
// Request/Response Types
|
||||
export interface ForecastRequest {
|
||||
product_name: string;
|
||||
days_ahead: number;
|
||||
start_date?: string;
|
||||
include_confidence_intervals?: boolean;
|
||||
external_factors?: {
|
||||
weather?: string[];
|
||||
events?: string[];
|
||||
holidays?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ForecastResponse {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
product_name: string;
|
||||
forecast_date: string;
|
||||
predicted_demand: number;
|
||||
confidence_lower: number;
|
||||
confidence_upper: number;
|
||||
confidence_level: number;
|
||||
external_factors: Record<string, any>;
|
||||
model_version: string;
|
||||
created_at: string;
|
||||
actual_demand?: number;
|
||||
accuracy_score?: number;
|
||||
}
|
||||
|
||||
export interface PredictionBatch {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
parameters: Record<string, any>;
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||
progress: number;
|
||||
total_predictions: number;
|
||||
completed_predictions: number;
|
||||
failed_predictions: number;
|
||||
created_at: string;
|
||||
completed_at?: string;
|
||||
error_message?: string;
|
||||
}
|
||||
|
||||
export interface ModelPerformance {
|
||||
model_id: string;
|
||||
model_name: string;
|
||||
version: string;
|
||||
accuracy_metrics: {
|
||||
mape: number; // Mean Absolute Percentage Error
|
||||
rmse: number; // Root Mean Square Error
|
||||
mae: number; // Mean Absolute Error
|
||||
r2_score: number;
|
||||
};
|
||||
training_data_period: {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
total_records: number;
|
||||
};
|
||||
last_training_date: string;
|
||||
performance_trend: 'improving' | 'stable' | 'declining';
|
||||
}
|
||||
|
||||
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 const forecastingService = new ForecastingService();
|
||||
33
frontend/src/services/api/index.ts
Normal file
33
frontend/src/services/api/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// Export API client and types
|
||||
export * 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 './orders.service';
|
||||
export * from './procurement.service';
|
||||
export * from './pos.service';
|
||||
export * from './data.service';
|
||||
export * from './training.service';
|
||||
export * from './notification.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 { trainingService } from './training.service';
|
||||
export { notificationService } from './notification.service';
|
||||
|
||||
// API client instance
|
||||
export { apiClient } from './client';
|
||||
607
frontend/src/services/api/inventory.service.ts
Normal file
607
frontend/src/services/api/inventory.service.ts
Normal file
@@ -0,0 +1,607 @@
|
||||
import { apiClient, ApiResponse } from './client';
|
||||
|
||||
// Enums
|
||||
export enum UnitOfMeasure {
|
||||
KILOGRAM = 'kg',
|
||||
GRAM = 'g',
|
||||
LITER = 'l',
|
||||
MILLILITER = 'ml',
|
||||
PIECE = 'piece',
|
||||
PACKAGE = 'package',
|
||||
BAG = 'bag',
|
||||
BOX = 'box',
|
||||
DOZEN = 'dozen',
|
||||
}
|
||||
|
||||
export enum ProductType {
|
||||
INGREDIENT = 'ingredient',
|
||||
FINISHED_PRODUCT = 'finished_product',
|
||||
}
|
||||
|
||||
export enum StockMovementType {
|
||||
PURCHASE = 'purchase',
|
||||
SALE = 'sale',
|
||||
USAGE = 'usage',
|
||||
WASTE = 'waste',
|
||||
ADJUSTMENT = 'adjustment',
|
||||
TRANSFER = 'transfer',
|
||||
RETURN = 'return',
|
||||
}
|
||||
|
||||
// Request/Response Types
|
||||
export 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>;
|
||||
}
|
||||
|
||||
export interface IngredientUpdate {
|
||||
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_active?: boolean;
|
||||
is_perishable?: boolean;
|
||||
allergen_info?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface IngredientResponse {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
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;
|
||||
last_purchase_price?: 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_active: boolean;
|
||||
is_perishable: boolean;
|
||||
allergen_info?: Record<string, any>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by?: string;
|
||||
current_stock?: number;
|
||||
is_low_stock?: boolean;
|
||||
needs_reorder?: boolean;
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export interface StockUpdate {
|
||||
batch_number?: string;
|
||||
lot_number?: string;
|
||||
supplier_batch_ref?: string;
|
||||
current_quantity?: number;
|
||||
reserved_quantity?: number;
|
||||
received_date?: string;
|
||||
expiration_date?: string;
|
||||
best_before_date?: string;
|
||||
unit_cost?: number;
|
||||
storage_location?: string;
|
||||
warehouse_zone?: string;
|
||||
shelf_position?: string;
|
||||
is_available?: boolean;
|
||||
quality_status?: string;
|
||||
}
|
||||
|
||||
export interface StockResponse {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
ingredient_id: string;
|
||||
batch_number?: string;
|
||||
lot_number?: string;
|
||||
supplier_batch_ref?: string;
|
||||
current_quantity: number;
|
||||
reserved_quantity: number;
|
||||
available_quantity: number;
|
||||
received_date?: string;
|
||||
expiration_date?: string;
|
||||
best_before_date?: string;
|
||||
unit_cost?: number;
|
||||
total_cost?: number;
|
||||
storage_location?: string;
|
||||
warehouse_zone?: string;
|
||||
shelf_position?: string;
|
||||
is_available: boolean;
|
||||
is_expired: boolean;
|
||||
quality_status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
ingredient?: IngredientResponse;
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export interface StockMovementResponse {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
ingredient_id: string;
|
||||
stock_id?: string;
|
||||
movement_type: StockMovementType;
|
||||
quantity: number;
|
||||
unit_cost?: number;
|
||||
total_cost?: number;
|
||||
quantity_before?: number;
|
||||
quantity_after?: number;
|
||||
reference_number?: string;
|
||||
supplier_id?: string;
|
||||
notes?: string;
|
||||
reason_code?: string;
|
||||
movement_date: string;
|
||||
created_at: string;
|
||||
created_by?: string;
|
||||
ingredient?: IngredientResponse;
|
||||
}
|
||||
|
||||
export interface StockAlertResponse {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
ingredient_id: string;
|
||||
stock_id?: string;
|
||||
alert_type: string;
|
||||
severity: string;
|
||||
title: string;
|
||||
message: string;
|
||||
current_quantity?: number;
|
||||
threshold_value?: number;
|
||||
expiration_date?: string;
|
||||
is_active: boolean;
|
||||
is_acknowledged: boolean;
|
||||
acknowledged_by?: string;
|
||||
acknowledged_at?: string;
|
||||
is_resolved: boolean;
|
||||
resolved_by?: string;
|
||||
resolved_at?: string;
|
||||
resolution_notes?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
ingredient?: IngredientResponse;
|
||||
}
|
||||
|
||||
export interface InventorySummary {
|
||||
total_ingredients: number;
|
||||
total_stock_value: number;
|
||||
low_stock_alerts: number;
|
||||
expiring_soon_items: number;
|
||||
expired_items: number;
|
||||
out_of_stock_items: number;
|
||||
stock_by_category: Record<string, Record<string, any>>;
|
||||
recent_movements: number;
|
||||
recent_purchases: number;
|
||||
recent_waste: number;
|
||||
}
|
||||
|
||||
export interface StockLevelSummary {
|
||||
ingredient_id: string;
|
||||
ingredient_name: string;
|
||||
unit_of_measure: string;
|
||||
total_quantity: number;
|
||||
available_quantity: number;
|
||||
reserved_quantity: number;
|
||||
is_low_stock: boolean;
|
||||
needs_reorder: boolean;
|
||||
has_expired_stock: boolean;
|
||||
total_batches: number;
|
||||
oldest_batch_date?: string;
|
||||
newest_batch_date?: string;
|
||||
next_expiration_date?: string;
|
||||
average_unit_cost?: number;
|
||||
total_stock_value?: number;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export interface InventoryFilter {
|
||||
category?: string;
|
||||
is_active?: boolean;
|
||||
is_low_stock?: boolean;
|
||||
needs_reorder?: boolean;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface StockFilter {
|
||||
ingredient_id?: string;
|
||||
is_available?: boolean;
|
||||
is_expired?: boolean;
|
||||
expiring_within_days?: number;
|
||||
storage_location?: string;
|
||||
quality_status?: string;
|
||||
}
|
||||
|
||||
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' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export const inventoryService = new InventoryService();
|
||||
406
frontend/src/services/api/notification.service.ts
Normal file
406
frontend/src/services/api/notification.service.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
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();
|
||||
312
frontend/src/services/api/orders.service.ts
Normal file
312
frontend/src/services/api/orders.service.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
import { apiClient, ApiResponse } from './client';
|
||||
|
||||
// Enums
|
||||
export enum OrderStatus {
|
||||
PENDING = 'pending',
|
||||
CONFIRMED = 'confirmed',
|
||||
IN_PREPARATION = 'in_preparation',
|
||||
READY = 'ready',
|
||||
DELIVERED = 'delivered',
|
||||
CANCELLED = 'cancelled',
|
||||
}
|
||||
|
||||
export enum OrderType {
|
||||
DINE_IN = 'dine_in',
|
||||
TAKEAWAY = 'takeaway',
|
||||
DELIVERY = 'delivery',
|
||||
CATERING = 'catering',
|
||||
}
|
||||
|
||||
// Request/Response Types
|
||||
export interface OrderItem {
|
||||
product_id?: string;
|
||||
product_name: string;
|
||||
quantity: number;
|
||||
unit_price: number;
|
||||
total_price: number;
|
||||
notes?: string;
|
||||
customizations?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface OrderCreate {
|
||||
customer_id?: string;
|
||||
customer_name: string;
|
||||
customer_email?: string;
|
||||
customer_phone?: string;
|
||||
order_type: OrderType;
|
||||
items: OrderItem[];
|
||||
special_instructions?: string;
|
||||
delivery_address?: string;
|
||||
delivery_date?: string;
|
||||
delivery_time?: string;
|
||||
payment_method?: string;
|
||||
}
|
||||
|
||||
export interface OrderUpdate {
|
||||
status?: OrderStatus;
|
||||
customer_name?: string;
|
||||
customer_email?: string;
|
||||
customer_phone?: string;
|
||||
special_instructions?: string;
|
||||
delivery_address?: string;
|
||||
delivery_date?: string;
|
||||
delivery_time?: string;
|
||||
estimated_completion_time?: string;
|
||||
actual_completion_time?: string;
|
||||
}
|
||||
|
||||
export interface OrderResponse {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
order_number: string;
|
||||
customer_id?: string;
|
||||
customer_name: string;
|
||||
customer_email?: string;
|
||||
customer_phone?: string;
|
||||
order_type: OrderType;
|
||||
status: OrderStatus;
|
||||
items: OrderItem[];
|
||||
subtotal: number;
|
||||
tax_amount: number;
|
||||
discount_amount: number;
|
||||
total_amount: number;
|
||||
special_instructions?: string;
|
||||
delivery_address?: string;
|
||||
delivery_date?: string;
|
||||
delivery_time?: string;
|
||||
estimated_completion_time?: string;
|
||||
actual_completion_time?: string;
|
||||
payment_method?: string;
|
||||
payment_status?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by?: string;
|
||||
}
|
||||
|
||||
export interface Customer {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
name: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
address?: string;
|
||||
preferences?: Record<string, any>;
|
||||
total_orders: number;
|
||||
total_spent: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface OrderAnalytics {
|
||||
total_orders: number;
|
||||
total_revenue: number;
|
||||
average_order_value: number;
|
||||
order_completion_rate: number;
|
||||
delivery_success_rate: number;
|
||||
customer_satisfaction_score?: number;
|
||||
popular_products: Array<{
|
||||
product_name: string;
|
||||
quantity_sold: number;
|
||||
revenue: number;
|
||||
}>;
|
||||
order_trends: Array<{
|
||||
date: string;
|
||||
orders: number;
|
||||
revenue: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
class OrdersService {
|
||||
private readonly baseUrl = '/orders';
|
||||
|
||||
// Order management
|
||||
async getOrders(params?: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
status?: OrderStatus;
|
||||
order_type?: OrderType;
|
||||
customer_id?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}): 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?: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
search?: string;
|
||||
}): 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?: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
granularity?: 'hourly' | 'daily' | 'weekly' | 'monthly';
|
||||
}): Promise<ApiResponse<Array<{
|
||||
period: string;
|
||||
orders: number;
|
||||
revenue: number;
|
||||
avg_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?${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 const ordersService = new OrdersService();
|
||||
317
frontend/src/services/api/pos.service.ts
Normal file
317
frontend/src/services/api/pos.service.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
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();
|
||||
408
frontend/src/services/api/procurement.service.ts
Normal file
408
frontend/src/services/api/procurement.service.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
import { apiClient, ApiResponse } from './client';
|
||||
|
||||
// Enums
|
||||
export enum PurchaseOrderStatus {
|
||||
DRAFT = 'draft',
|
||||
PENDING = 'pending',
|
||||
APPROVED = 'approved',
|
||||
SENT = 'sent',
|
||||
PARTIALLY_RECEIVED = 'partially_received',
|
||||
RECEIVED = 'received',
|
||||
CANCELLED = 'cancelled',
|
||||
}
|
||||
|
||||
export enum DeliveryStatus {
|
||||
SCHEDULED = 'scheduled',
|
||||
IN_TRANSIT = 'in_transit',
|
||||
DELIVERED = 'delivered',
|
||||
FAILED = 'failed',
|
||||
RETURNED = 'returned',
|
||||
}
|
||||
|
||||
// Request/Response Types
|
||||
export interface PurchaseOrderItem {
|
||||
ingredient_id: string;
|
||||
ingredient_name: string;
|
||||
quantity: number;
|
||||
unit_price: number;
|
||||
total_price: number;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface PurchaseOrderCreate {
|
||||
supplier_id: string;
|
||||
items: PurchaseOrderItem[];
|
||||
delivery_date?: string;
|
||||
notes?: string;
|
||||
priority?: 'low' | 'normal' | 'high' | 'urgent';
|
||||
}
|
||||
|
||||
export interface PurchaseOrderUpdate {
|
||||
supplier_id?: string;
|
||||
delivery_date?: string;
|
||||
notes?: string;
|
||||
priority?: 'low' | 'normal' | 'high' | 'urgent';
|
||||
status?: PurchaseOrderStatus;
|
||||
}
|
||||
|
||||
export interface PurchaseOrderResponse {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
order_number: string;
|
||||
supplier_id: string;
|
||||
supplier_name: string;
|
||||
status: PurchaseOrderStatus;
|
||||
items: PurchaseOrderItem[];
|
||||
subtotal: number;
|
||||
tax_amount: number;
|
||||
total_amount: number;
|
||||
delivery_date?: string;
|
||||
expected_delivery_date?: string;
|
||||
actual_delivery_date?: string;
|
||||
notes?: string;
|
||||
priority: 'low' | 'normal' | 'high' | 'urgent';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: string;
|
||||
approved_by?: string;
|
||||
approved_at?: string;
|
||||
}
|
||||
|
||||
export interface Supplier {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
name: string;
|
||||
contact_name?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
address: string;
|
||||
tax_id?: string;
|
||||
payment_terms?: string;
|
||||
delivery_terms?: string;
|
||||
rating?: number;
|
||||
is_active: boolean;
|
||||
performance_metrics: {
|
||||
on_time_delivery_rate: number;
|
||||
quality_score: number;
|
||||
total_orders: number;
|
||||
average_delivery_time: number;
|
||||
};
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface DeliveryResponse {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
purchase_order_id: string;
|
||||
delivery_number: string;
|
||||
supplier_id: string;
|
||||
status: DeliveryStatus;
|
||||
scheduled_date: string;
|
||||
actual_delivery_date?: string;
|
||||
delivery_items: Array<{
|
||||
ingredient_id: string;
|
||||
ingredient_name: string;
|
||||
ordered_quantity: number;
|
||||
delivered_quantity: number;
|
||||
unit_price: number;
|
||||
batch_number?: string;
|
||||
expiration_date?: string;
|
||||
quality_notes?: string;
|
||||
}>;
|
||||
total_items: number;
|
||||
delivery_notes?: string;
|
||||
quality_check_notes?: string;
|
||||
received_by?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
class ProcurementService {
|
||||
private readonly baseUrl = '/procurement';
|
||||
|
||||
// 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.baseUrl}/purchase-orders?${queryParams.toString()}`
|
||||
: `${this.baseUrl}/purchase-orders`;
|
||||
|
||||
return apiClient.get(url);
|
||||
}
|
||||
|
||||
async getPurchaseOrder(orderId: string): Promise<ApiResponse<PurchaseOrderResponse>> {
|
||||
return apiClient.get(`${this.baseUrl}/purchase-orders/${orderId}`);
|
||||
}
|
||||
|
||||
async createPurchaseOrder(orderData: PurchaseOrderCreate): Promise<ApiResponse<PurchaseOrderResponse>> {
|
||||
return apiClient.post(`${this.baseUrl}/purchase-orders`, orderData);
|
||||
}
|
||||
|
||||
async updatePurchaseOrder(orderId: string, orderData: PurchaseOrderUpdate): Promise<ApiResponse<PurchaseOrderResponse>> {
|
||||
return apiClient.put(`${this.baseUrl}/purchase-orders/${orderId}`, orderData);
|
||||
}
|
||||
|
||||
async approvePurchaseOrder(orderId: string): Promise<ApiResponse<PurchaseOrderResponse>> {
|
||||
return apiClient.post(`${this.baseUrl}/purchase-orders/${orderId}/approve`);
|
||||
}
|
||||
|
||||
async sendPurchaseOrder(orderId: string, sendEmail: boolean = true): Promise<ApiResponse<{ message: string; sent_at: string }>> {
|
||||
return apiClient.post(`${this.baseUrl}/purchase-orders/${orderId}/send`, { send_email: sendEmail });
|
||||
}
|
||||
|
||||
async cancelPurchaseOrder(orderId: string, reason?: string): Promise<ApiResponse<PurchaseOrderResponse>> {
|
||||
return apiClient.post(`${this.baseUrl}/purchase-orders/${orderId}/cancel`, { reason });
|
||||
}
|
||||
|
||||
// Supplier management
|
||||
async getSuppliers(params?: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
is_active?: boolean;
|
||||
search?: string;
|
||||
}): Promise<ApiResponse<{ items: Supplier[]; 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}/suppliers?${queryParams.toString()}`
|
||||
: `${this.baseUrl}/suppliers`;
|
||||
|
||||
return apiClient.get(url);
|
||||
}
|
||||
|
||||
async getSupplier(supplierId: string): Promise<ApiResponse<Supplier>> {
|
||||
return apiClient.get(`${this.baseUrl}/suppliers/${supplierId}`);
|
||||
}
|
||||
|
||||
async createSupplier(supplierData: Omit<Supplier, 'id' | 'tenant_id' | 'performance_metrics' | 'created_at' | 'updated_at'>): Promise<ApiResponse<Supplier>> {
|
||||
return apiClient.post(`${this.baseUrl}/suppliers`, supplierData);
|
||||
}
|
||||
|
||||
async updateSupplier(supplierId: string, supplierData: Partial<Supplier>): Promise<ApiResponse<Supplier>> {
|
||||
return apiClient.put(`${this.baseUrl}/suppliers/${supplierId}`, supplierData);
|
||||
}
|
||||
|
||||
async deleteSupplier(supplierId: string): Promise<ApiResponse<{ message: string }>> {
|
||||
return apiClient.delete(`${this.baseUrl}/suppliers/${supplierId}`);
|
||||
}
|
||||
|
||||
async getSupplierPerformance(supplierId: string): Promise<ApiResponse<{
|
||||
supplier: Supplier;
|
||||
performance_history: Array<{
|
||||
month: string;
|
||||
on_time_delivery_rate: number;
|
||||
quality_score: number;
|
||||
order_count: number;
|
||||
total_value: number;
|
||||
}>;
|
||||
recent_deliveries: DeliveryResponse[];
|
||||
}>> {
|
||||
return apiClient.get(`${this.baseUrl}/suppliers/${supplierId}/performance`);
|
||||
}
|
||||
|
||||
// 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.baseUrl}/deliveries?${queryParams.toString()}`
|
||||
: `${this.baseUrl}/deliveries`;
|
||||
|
||||
return apiClient.get(url);
|
||||
}
|
||||
|
||||
async getDelivery(deliveryId: string): Promise<ApiResponse<DeliveryResponse>> {
|
||||
return apiClient.get(`${this.baseUrl}/deliveries/${deliveryId}`);
|
||||
}
|
||||
|
||||
async receiveDelivery(deliveryId: string, deliveryData: {
|
||||
delivered_items: Array<{
|
||||
ingredient_id: string;
|
||||
delivered_quantity: number;
|
||||
batch_number?: string;
|
||||
expiration_date?: string;
|
||||
quality_notes?: string;
|
||||
}>;
|
||||
delivery_notes?: string;
|
||||
quality_check_notes?: string;
|
||||
}): Promise<ApiResponse<DeliveryResponse>> {
|
||||
return apiClient.post(`${this.baseUrl}/deliveries/${deliveryId}/receive`, deliveryData);
|
||||
}
|
||||
|
||||
async reportDeliveryIssue(deliveryId: string, issue: {
|
||||
issue_type: 'late_delivery' | 'quality_issue' | 'quantity_mismatch' | 'damaged_goods' | 'other';
|
||||
description: string;
|
||||
affected_items?: string[];
|
||||
severity: 'low' | 'medium' | 'high';
|
||||
}): Promise<ApiResponse<{ message: string; issue_id: string }>> {
|
||||
return apiClient.post(`${this.baseUrl}/deliveries/${deliveryId}/report-issue`, issue);
|
||||
}
|
||||
|
||||
// Analytics and reporting
|
||||
async getProcurementAnalytics(params?: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
supplier_id?: string;
|
||||
}): Promise<ApiResponse<{
|
||||
total_purchase_value: number;
|
||||
total_orders: number;
|
||||
average_order_value: number;
|
||||
on_time_delivery_rate: number;
|
||||
quality_score: number;
|
||||
cost_savings: number;
|
||||
top_suppliers: Array<{
|
||||
supplier_name: string;
|
||||
total_value: number;
|
||||
order_count: number;
|
||||
performance_score: number;
|
||||
}>;
|
||||
spending_trends: Array<{
|
||||
month: string;
|
||||
total_spending: number;
|
||||
order_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}/analytics?${queryParams.toString()}`
|
||||
: `${this.baseUrl}/analytics`;
|
||||
|
||||
return apiClient.get(url);
|
||||
}
|
||||
|
||||
async getSpendingByCategory(params?: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}): Promise<ApiResponse<Array<{
|
||||
category: string;
|
||||
total_spending: number;
|
||||
percentage: number;
|
||||
trend: 'up' | 'down' | 'stable';
|
||||
}>>> {
|
||||
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}/spending/by-category?${queryParams.toString()}`
|
||||
: `${this.baseUrl}/spending/by-category`;
|
||||
|
||||
return apiClient.get(url);
|
||||
}
|
||||
|
||||
// Automated procurement
|
||||
async getReorderSuggestions(): Promise<ApiResponse<Array<{
|
||||
ingredient_id: string;
|
||||
ingredient_name: string;
|
||||
current_stock: number;
|
||||
reorder_point: number;
|
||||
suggested_quantity: number;
|
||||
preferred_supplier: {
|
||||
id: string;
|
||||
name: string;
|
||||
last_price: number;
|
||||
lead_time_days: number;
|
||||
};
|
||||
urgency: 'low' | 'medium' | 'high' | 'critical';
|
||||
}>>> {
|
||||
return apiClient.get(`${this.baseUrl}/reorder-suggestions`);
|
||||
}
|
||||
|
||||
async createReorderFromSuggestions(suggestions: Array<{
|
||||
ingredient_id: string;
|
||||
supplier_id: string;
|
||||
quantity: number;
|
||||
}>): Promise<ApiResponse<PurchaseOrderResponse[]>> {
|
||||
return apiClient.post(`${this.baseUrl}/auto-reorder`, { suggestions });
|
||||
}
|
||||
|
||||
// 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' },
|
||||
];
|
||||
}
|
||||
|
||||
getPriorityOptions(): { value: string; label: string; color: string }[] {
|
||||
return [
|
||||
{ value: 'low', label: 'Low', color: 'gray' },
|
||||
{ value: 'normal', label: 'Normal', color: 'blue' },
|
||||
{ value: 'high', label: 'High', color: 'orange' },
|
||||
{ value: 'urgent', label: 'Urgent', color: 'red' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export const procurementService = new ProcurementService();
|
||||
473
frontend/src/services/api/production.service.ts
Normal file
473
frontend/src/services/api/production.service.ts
Normal file
@@ -0,0 +1,473 @@
|
||||
import { apiClient, ApiResponse } from './client';
|
||||
|
||||
// Enums
|
||||
export enum ProductionBatchStatus {
|
||||
PLANNED = 'planned',
|
||||
IN_PROGRESS = 'in_progress',
|
||||
COMPLETED = 'completed',
|
||||
CANCELLED = 'cancelled',
|
||||
ON_HOLD = 'on_hold',
|
||||
}
|
||||
|
||||
export enum QualityCheckStatus {
|
||||
PASSED = 'passed',
|
||||
FAILED = 'failed',
|
||||
PENDING = 'pending',
|
||||
REQUIRES_REVIEW = 'requires_review',
|
||||
}
|
||||
|
||||
export enum ProductionPriority {
|
||||
LOW = 'low',
|
||||
NORMAL = 'normal',
|
||||
HIGH = 'high',
|
||||
URGENT = 'urgent',
|
||||
}
|
||||
|
||||
// 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 const productionService = new ProductionService();
|
||||
443
frontend/src/services/api/sales.service.ts
Normal file
443
frontend/src/services/api/sales.service.ts
Normal file
@@ -0,0 +1,443 @@
|
||||
import { apiClient, ApiResponse } from './client';
|
||||
|
||||
// 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<ApiResponse<SalesImportResult>> {
|
||||
return apiClient.uploadFile(`${this.baseUrl}/import`, file, progressCallback);
|
||||
}
|
||||
|
||||
async validateSalesData(file: File): Promise<ApiResponse<{
|
||||
valid_records: number;
|
||||
invalid_records: number;
|
||||
errors: Array<{
|
||||
row: number;
|
||||
field: string;
|
||||
message: string;
|
||||
}>;
|
||||
preview: SalesData[];
|
||||
}>> {
|
||||
return apiClient.uploadFile(`${this.baseUrl}/validate`, file);
|
||||
}
|
||||
|
||||
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)' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export const salesService = new SalesService();
|
||||
297
frontend/src/services/api/tenant.service.ts
Normal file
297
frontend/src/services/api/tenant.service.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import { apiClient, ApiResponse } from './client';
|
||||
|
||||
// Request/Response Types based on backend schemas
|
||||
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}`, 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 }>> {
|
||||
const response = await apiClient.post(`${this.baseUrl}/${tenantId}/switch`);
|
||||
|
||||
if (response.success && response.data?.tenant) {
|
||||
// Update local tenant context
|
||||
localStorage.setItem('tenant_id', tenantId);
|
||||
apiClient.setTenantId(tenantId);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// 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(): Promise<ApiResponse<TenantResponse[]>> {
|
||||
return apiClient.get(`${this.baseUrl}/my-tenants`);
|
||||
}
|
||||
|
||||
async validateTenantSlug(slug: string): Promise<ApiResponse<{ available: boolean; suggestions?: string[] }>> {
|
||||
return apiClient.get(`${this.baseUrl}/validate-slug/${slug}`);
|
||||
}
|
||||
|
||||
// Local state management helpers
|
||||
getCurrentTenantId(): string | null {
|
||||
return localStorage.getItem('tenant_id');
|
||||
}
|
||||
|
||||
getCurrentTenantData(): TenantResponse | null {
|
||||
const tenantData = localStorage.getItem('tenant_data');
|
||||
return tenantData ? JSON.parse(tenantData) : null;
|
||||
}
|
||||
|
||||
setCurrentTenant(tenant: TenantResponse) {
|
||||
localStorage.setItem('tenant_id', tenant.id);
|
||||
localStorage.setItem('tenant_data', JSON.stringify(tenant));
|
||||
apiClient.setTenantId(tenant.id);
|
||||
}
|
||||
|
||||
clearCurrentTenant() {
|
||||
localStorage.removeItem('tenant_id');
|
||||
localStorage.removeItem('tenant_data');
|
||||
}
|
||||
|
||||
// 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();
|
||||
447
frontend/src/services/api/training.service.ts
Normal file
447
frontend/src/services/api/training.service.ts
Normal file
@@ -0,0 +1,447 @@
|
||||
import { apiClient, ApiResponse } from './client';
|
||||
|
||||
// Model and training types
|
||||
export interface TrainingJob {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
name: string;
|
||||
model_type: 'demand_forecasting' | 'sales_prediction' | 'inventory_optimization' | 'production_planning';
|
||||
status: 'pending' | 'initializing' | 'training' | 'validating' | 'completed' | 'failed' | 'cancelled';
|
||||
progress: number;
|
||||
parameters: {
|
||||
data_start_date: string;
|
||||
data_end_date: string;
|
||||
validation_split: number;
|
||||
hyperparameters: Record<string, any>;
|
||||
};
|
||||
metrics?: {
|
||||
accuracy: number;
|
||||
mse: number;
|
||||
mae: number;
|
||||
r2_score: number;
|
||||
validation_accuracy: number;
|
||||
};
|
||||
training_duration_seconds?: number;
|
||||
model_size_mb?: number;
|
||||
created_at: string;
|
||||
started_at?: string;
|
||||
completed_at?: string;
|
||||
error_message?: string;
|
||||
created_by: string;
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
name: string;
|
||||
model_type: string;
|
||||
version: string;
|
||||
status: 'training' | 'active' | 'deprecated' | 'archived';
|
||||
is_production: boolean;
|
||||
performance_metrics: {
|
||||
accuracy: number;
|
||||
precision: number;
|
||||
recall: number;
|
||||
f1_score: number;
|
||||
last_evaluated: string;
|
||||
};
|
||||
training_data_info: {
|
||||
record_count: number;
|
||||
date_range: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
features_used: string[];
|
||||
};
|
||||
deployment_info?: {
|
||||
deployed_at: string;
|
||||
prediction_count: number;
|
||||
avg_response_time_ms: number;
|
||||
};
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface TrainingConfiguration {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
model_type: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
is_default: boolean;
|
||||
parameters: {
|
||||
algorithm: string;
|
||||
hyperparameters: Record<string, any>;
|
||||
feature_selection: string[];
|
||||
validation_method: string;
|
||||
cross_validation_folds?: number;
|
||||
};
|
||||
data_requirements: {
|
||||
minimum_records: number;
|
||||
required_columns: string[];
|
||||
date_range_days: number;
|
||||
};
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
class TrainingService {
|
||||
private readonly baseUrl = '/training';
|
||||
|
||||
// Training job management
|
||||
async getTrainingJobs(params?: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
status?: string;
|
||||
model_type?: string;
|
||||
}): Promise<ApiResponse<{ items: TrainingJob[]; 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}/jobs?${queryParams.toString()}`
|
||||
: `${this.baseUrl}/jobs`;
|
||||
|
||||
return apiClient.get(url);
|
||||
}
|
||||
|
||||
async getTrainingJob(jobId: string): Promise<ApiResponse<TrainingJob>> {
|
||||
return apiClient.get(`${this.baseUrl}/jobs/${jobId}`);
|
||||
}
|
||||
|
||||
async createTrainingJob(jobData: {
|
||||
name: string;
|
||||
model_type: string;
|
||||
config_id?: string;
|
||||
parameters: {
|
||||
data_start_date: string;
|
||||
data_end_date: string;
|
||||
validation_split?: number;
|
||||
hyperparameters?: Record<string, any>;
|
||||
};
|
||||
}): Promise<ApiResponse<TrainingJob>> {
|
||||
return apiClient.post(`${this.baseUrl}/jobs`, jobData);
|
||||
}
|
||||
|
||||
async cancelTrainingJob(jobId: string): Promise<ApiResponse<{ message: string }>> {
|
||||
return apiClient.post(`${this.baseUrl}/jobs/${jobId}/cancel`);
|
||||
}
|
||||
|
||||
async retryTrainingJob(jobId: string): Promise<ApiResponse<TrainingJob>> {
|
||||
return apiClient.post(`${this.baseUrl}/jobs/${jobId}/retry`);
|
||||
}
|
||||
|
||||
async getTrainingLogs(jobId: string, params?: {
|
||||
level?: 'debug' | 'info' | 'warning' | 'error';
|
||||
limit?: number;
|
||||
}): Promise<ApiResponse<Array<{
|
||||
timestamp: string;
|
||||
level: string;
|
||||
message: string;
|
||||
details?: Record<string, any>;
|
||||
}>>> {
|
||||
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}/jobs/${jobId}/logs?${queryParams.toString()}`
|
||||
: `${this.baseUrl}/jobs/${jobId}/logs`;
|
||||
|
||||
return apiClient.get(url);
|
||||
}
|
||||
|
||||
// Model management
|
||||
async getModels(params?: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
model_type?: string;
|
||||
status?: string;
|
||||
is_production?: boolean;
|
||||
}): Promise<ApiResponse<{ items: ModelInfo[]; 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}/models?${queryParams.toString()}`
|
||||
: `${this.baseUrl}/models`;
|
||||
|
||||
return apiClient.get(url);
|
||||
}
|
||||
|
||||
async getModel(modelId: string): Promise<ApiResponse<ModelInfo>> {
|
||||
return apiClient.get(`${this.baseUrl}/models/${modelId}`);
|
||||
}
|
||||
|
||||
async deployModel(modelId: string): Promise<ApiResponse<{ message: string; deployment_id: string }>> {
|
||||
return apiClient.post(`${this.baseUrl}/models/${modelId}/deploy`);
|
||||
}
|
||||
|
||||
async undeployModel(modelId: string): Promise<ApiResponse<{ message: string }>> {
|
||||
return apiClient.post(`${this.baseUrl}/models/${modelId}/undeploy`);
|
||||
}
|
||||
|
||||
async deleteModel(modelId: string): Promise<ApiResponse<{ message: string }>> {
|
||||
return apiClient.delete(`${this.baseUrl}/models/${modelId}`);
|
||||
}
|
||||
|
||||
async compareModels(modelIds: string[]): Promise<ApiResponse<{
|
||||
models: ModelInfo[];
|
||||
comparison: {
|
||||
accuracy_comparison: Array<{ model_id: string; accuracy: number; rank: number }>;
|
||||
performance_metrics: Record<string, Array<{ model_id: string; value: number }>>;
|
||||
recommendation: {
|
||||
best_model_id: string;
|
||||
reason: string;
|
||||
confidence: number;
|
||||
};
|
||||
};
|
||||
}>> {
|
||||
return apiClient.post(`${this.baseUrl}/models/compare`, { model_ids: modelIds });
|
||||
}
|
||||
|
||||
// Model evaluation
|
||||
async evaluateModel(modelId: string, evaluationData?: {
|
||||
test_data_start?: string;
|
||||
test_data_end?: string;
|
||||
metrics?: string[];
|
||||
}): Promise<ApiResponse<{
|
||||
evaluation_id: string;
|
||||
status: 'pending' | 'running' | 'completed' | 'failed';
|
||||
}>> {
|
||||
return apiClient.post(`${this.baseUrl}/models/${modelId}/evaluate`, evaluationData);
|
||||
}
|
||||
|
||||
async getEvaluationResults(evaluationId: string): Promise<ApiResponse<{
|
||||
model_id: string;
|
||||
status: string;
|
||||
metrics: Record<string, number>;
|
||||
predictions_sample: Array<{
|
||||
actual: number;
|
||||
predicted: number;
|
||||
date: string;
|
||||
error: number;
|
||||
}>;
|
||||
feature_importance?: Array<{
|
||||
feature: string;
|
||||
importance: number;
|
||||
}>;
|
||||
completed_at: string;
|
||||
}>> {
|
||||
return apiClient.get(`${this.baseUrl}/evaluations/${evaluationId}`);
|
||||
}
|
||||
|
||||
// Training configuration
|
||||
async getTrainingConfigs(modelType?: string): Promise<ApiResponse<TrainingConfiguration[]>> {
|
||||
const url = modelType
|
||||
? `${this.baseUrl}/configs?model_type=${encodeURIComponent(modelType)}`
|
||||
: `${this.baseUrl}/configs`;
|
||||
|
||||
return apiClient.get(url);
|
||||
}
|
||||
|
||||
async getTrainingConfig(configId: string): Promise<ApiResponse<TrainingConfiguration>> {
|
||||
return apiClient.get(`${this.baseUrl}/configs/${configId}`);
|
||||
}
|
||||
|
||||
async createTrainingConfig(configData: {
|
||||
model_type: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
parameters: TrainingConfiguration['parameters'];
|
||||
data_requirements?: Partial<TrainingConfiguration['data_requirements']>;
|
||||
}): Promise<ApiResponse<TrainingConfiguration>> {
|
||||
return apiClient.post(`${this.baseUrl}/configs`, configData);
|
||||
}
|
||||
|
||||
async updateTrainingConfig(configId: string, configData: Partial<TrainingConfiguration>): Promise<ApiResponse<TrainingConfiguration>> {
|
||||
return apiClient.put(`${this.baseUrl}/configs/${configId}`, configData);
|
||||
}
|
||||
|
||||
async deleteTrainingConfig(configId: string): Promise<ApiResponse<{ message: string }>> {
|
||||
return apiClient.delete(`${this.baseUrl}/configs/${configId}`);
|
||||
}
|
||||
|
||||
// Data analysis and preparation
|
||||
async analyzeTrainingData(params: {
|
||||
model_type: string;
|
||||
data_start_date: string;
|
||||
data_end_date: string;
|
||||
}): Promise<ApiResponse<{
|
||||
data_quality: {
|
||||
total_records: number;
|
||||
complete_records: number;
|
||||
missing_data_percentage: number;
|
||||
duplicate_records: number;
|
||||
};
|
||||
feature_analysis: Array<{
|
||||
feature: string;
|
||||
data_type: string;
|
||||
completeness: number;
|
||||
unique_values: number;
|
||||
correlation_with_target?: number;
|
||||
}>;
|
||||
recommendations: Array<{
|
||||
type: 'data_cleaning' | 'feature_engineering' | 'model_selection';
|
||||
message: string;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
}>;
|
||||
training_feasibility: {
|
||||
can_train: boolean;
|
||||
minimum_requirements_met: boolean;
|
||||
estimated_training_time_minutes: number;
|
||||
};
|
||||
}>> {
|
||||
return apiClient.post(`${this.baseUrl}/analyze-data`, params);
|
||||
}
|
||||
|
||||
async getDataAlignmentStatus(): Promise<ApiResponse<{
|
||||
status: 'aligned' | 'misaligned' | 'processing';
|
||||
last_alignment: string;
|
||||
data_sources: Array<{
|
||||
source: string;
|
||||
status: 'synced' | 'out_of_sync' | 'error';
|
||||
last_sync: string;
|
||||
record_count: number;
|
||||
}>;
|
||||
issues?: Array<{
|
||||
source: string;
|
||||
issue_type: string;
|
||||
description: string;
|
||||
severity: 'low' | 'medium' | 'high';
|
||||
}>;
|
||||
}>> {
|
||||
return apiClient.get(`${this.baseUrl}/data-alignment/status`);
|
||||
}
|
||||
|
||||
async triggerDataAlignment(): Promise<ApiResponse<{ message: string; task_id: string }>> {
|
||||
return apiClient.post(`${this.baseUrl}/data-alignment/trigger`);
|
||||
}
|
||||
|
||||
// Training insights and recommendations
|
||||
async getTrainingInsights(): Promise<ApiResponse<{
|
||||
model_performance_trends: Array<{
|
||||
model_type: string;
|
||||
accuracy_trend: 'improving' | 'stable' | 'declining';
|
||||
last_training_date: string;
|
||||
recommendation: string;
|
||||
}>;
|
||||
training_frequency_suggestions: Array<{
|
||||
model_type: string;
|
||||
current_frequency: string;
|
||||
suggested_frequency: string;
|
||||
reason: string;
|
||||
}>;
|
||||
data_quality_alerts: Array<{
|
||||
alert_type: string;
|
||||
severity: 'info' | 'warning' | 'critical';
|
||||
message: string;
|
||||
affected_models: string[];
|
||||
}>;
|
||||
}>> {
|
||||
return apiClient.get(`${this.baseUrl}/insights`);
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
getModelTypes(): { value: string; label: string; description: string }[] {
|
||||
return [
|
||||
{
|
||||
value: 'demand_forecasting',
|
||||
label: 'Demand Forecasting',
|
||||
description: 'Predict future demand for products based on historical sales and external factors'
|
||||
},
|
||||
{
|
||||
value: 'sales_prediction',
|
||||
label: 'Sales Prediction',
|
||||
description: 'Forecast sales revenue and patterns'
|
||||
},
|
||||
{
|
||||
value: 'inventory_optimization',
|
||||
label: 'Inventory Optimization',
|
||||
description: 'Optimize inventory levels and reorder points'
|
||||
},
|
||||
{
|
||||
value: 'production_planning',
|
||||
label: 'Production Planning',
|
||||
description: 'Optimize production schedules and capacity planning'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getAlgorithmOptions(): { value: string; label: string; suitable_for: string[] }[] {
|
||||
return [
|
||||
{
|
||||
value: 'prophet',
|
||||
label: 'Prophet',
|
||||
suitable_for: ['demand_forecasting', 'sales_prediction']
|
||||
},
|
||||
{
|
||||
value: 'arima',
|
||||
label: 'ARIMA',
|
||||
suitable_for: ['demand_forecasting', 'sales_prediction']
|
||||
},
|
||||
{
|
||||
value: 'random_forest',
|
||||
label: 'Random Forest',
|
||||
suitable_for: ['inventory_optimization', 'production_planning']
|
||||
},
|
||||
{
|
||||
value: 'xgboost',
|
||||
label: 'XGBoost',
|
||||
suitable_for: ['demand_forecasting', 'inventory_optimization']
|
||||
},
|
||||
{
|
||||
value: 'lstm',
|
||||
label: 'LSTM Neural Network',
|
||||
suitable_for: ['demand_forecasting', 'sales_prediction']
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getValidationMethods(): { value: string; label: string; description: string }[] {
|
||||
return [
|
||||
{
|
||||
value: 'time_series_split',
|
||||
label: 'Time Series Split',
|
||||
description: 'Split data chronologically for time series validation'
|
||||
},
|
||||
{
|
||||
value: 'k_fold',
|
||||
label: 'K-Fold Cross Validation',
|
||||
description: 'Standard k-fold cross validation'
|
||||
},
|
||||
{
|
||||
value: 'stratified_k_fold',
|
||||
label: 'Stratified K-Fold',
|
||||
description: 'Stratified sampling for cross validation'
|
||||
},
|
||||
{
|
||||
value: 'holdout',
|
||||
label: 'Holdout Validation',
|
||||
description: 'Simple train/validation split'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export const trainingService = new TrainingService();
|
||||
Reference in New Issue
Block a user