Start integrating the onboarding flow with backend 6

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

View File

@@ -0,0 +1,176 @@
/**
* Core HTTP client for React Query integration
*
* Architecture:
* - Axios: HTTP client for making requests
* - This Client: Handles auth tokens, tenant context, and error formatting
* - Services: Business logic that uses this client
* - React Query Hooks: Data fetching layer that uses services
*
* React Query doesn't replace HTTP clients - it manages data fetching/caching/sync
*/
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
export interface ApiError {
message: string;
status?: number;
code?: string;
details?: any;
}
class ApiClient {
private client: AxiosInstance;
private baseURL: string;
private authToken: string | null = null;
private tenantId: string | null = null;
constructor(baseURL: string = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api/v1') {
this.baseURL = baseURL;
this.client = axios.create({
baseURL: this.baseURL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
private setupInterceptors() {
// Request interceptor to add auth headers
this.client.interceptors.request.use(
(config) => {
if (this.authToken) {
config.headers.Authorization = `Bearer ${this.authToken}`;
}
if (this.tenantId) {
config.headers['X-Tenant-ID'] = this.tenantId;
}
return config;
},
(error) => {
return Promise.reject(this.handleError(error));
}
);
// Response interceptor for error handling
this.client.interceptors.response.use(
(response) => response,
(error) => {
return Promise.reject(this.handleError(error));
}
);
}
private handleError(error: AxiosError): ApiError {
if (error.response) {
// Server responded with error status
const { status, data } = error.response;
return {
message: (data as any)?.detail || (data as any)?.message || `Request failed with status ${status}`,
status,
code: (data as any)?.code,
details: data,
};
} else if (error.request) {
// Network error
return {
message: 'Network error - please check your connection',
status: 0,
};
} else {
// Other error
return {
message: error.message || 'Unknown error occurred',
};
}
}
// Configuration methods
setAuthToken(token: string | null) {
this.authToken = token;
}
setTenantId(tenantId: string | null) {
this.tenantId = tenantId;
}
getAuthToken(): string | null {
return this.authToken;
}
getTenantId(): string | null {
return this.tenantId;
}
// HTTP Methods - Return direct data for React Query
async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.client.get(url, config);
return response.data;
}
async post<T = any, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig
): Promise<T> {
const response: AxiosResponse<T> = await this.client.post(url, data, config);
return response.data;
}
async put<T = any, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig
): Promise<T> {
const response: AxiosResponse<T> = await this.client.put(url, data, config);
return response.data;
}
async patch<T = any, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig
): Promise<T> {
const response: AxiosResponse<T> = await this.client.patch(url, data, config);
return response.data;
}
async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.client.delete(url, config);
return response.data;
}
// File upload helper
async uploadFile<T = any>(
url: string,
file: File | FormData,
config?: AxiosRequestConfig
): Promise<T> {
const formData = file instanceof FormData ? file : new FormData();
if (file instanceof File) {
formData.append('file', file);
}
return this.post<T>(url, formData, {
...config,
headers: {
...config?.headers,
'Content-Type': 'multipart/form-data',
},
});
}
// Raw axios instance for advanced usage
getAxiosInstance(): AxiosInstance {
return this.client;
}
}
// Create and export singleton instance
export const apiClient = new ApiClient();
export default apiClient;

View File

@@ -0,0 +1,2 @@
export { apiClient, default } from './apiClient';
export type { ApiError } from './apiClient';

View File

@@ -0,0 +1,171 @@
/**
* Auth React Query hooks
*/
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
import { authService } from '../services/auth';
import {
UserRegistration,
UserLogin,
TokenResponse,
PasswordChange,
PasswordReset,
UserResponse,
UserUpdate,
TokenVerificationResponse,
AuthHealthResponse
} from '../types/auth';
import { ApiError } from '../client';
// Query Keys
export const authKeys = {
all: ['auth'] as const,
profile: () => [...authKeys.all, 'profile'] as const,
health: () => [...authKeys.all, 'health'] as const,
verify: (token?: string) => [...authKeys.all, 'verify', token] as const,
} as const;
// Queries
export const useAuthProfile = (
options?: Omit<UseQueryOptions<UserResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<UserResponse, ApiError>({
queryKey: authKeys.profile(),
queryFn: () => authService.getProfile(),
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
export const useAuthHealth = (
options?: Omit<UseQueryOptions<AuthHealthResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<AuthHealthResponse, ApiError>({
queryKey: authKeys.health(),
queryFn: () => authService.healthCheck(),
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
export const useVerifyToken = (
token?: string,
options?: Omit<UseQueryOptions<TokenVerificationResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<TokenVerificationResponse, ApiError>({
queryKey: authKeys.verify(token),
queryFn: () => authService.verifyToken(token),
enabled: !!token,
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
// Mutations
export const useRegister = (
options?: UseMutationOptions<TokenResponse, ApiError, UserRegistration>
) => {
const queryClient = useQueryClient();
return useMutation<TokenResponse, ApiError, UserRegistration>({
mutationFn: (userData: UserRegistration) => authService.register(userData),
onSuccess: (data) => {
// Update profile query with new user data
if (data.user) {
queryClient.setQueryData(authKeys.profile(), data.user);
}
},
...options,
});
};
export const useLogin = (
options?: UseMutationOptions<TokenResponse, ApiError, UserLogin>
) => {
const queryClient = useQueryClient();
return useMutation<TokenResponse, ApiError, UserLogin>({
mutationFn: (loginData: UserLogin) => authService.login(loginData),
onSuccess: (data) => {
// Update profile query with new user data
if (data.user) {
queryClient.setQueryData(authKeys.profile(), data.user);
}
// Invalidate all queries to refresh data
queryClient.invalidateQueries({ queryKey: ['auth'] });
},
...options,
});
};
export const useRefreshToken = (
options?: UseMutationOptions<TokenResponse, ApiError, string>
) => {
return useMutation<TokenResponse, ApiError, string>({
mutationFn: (refreshToken: string) => authService.refreshToken(refreshToken),
...options,
});
};
export const useLogout = (
options?: UseMutationOptions<{ message: string }, ApiError, string>
) => {
const queryClient = useQueryClient();
return useMutation<{ message: string }, ApiError, string>({
mutationFn: (refreshToken: string) => authService.logout(refreshToken),
onSuccess: () => {
// Clear all queries on logout
queryClient.clear();
},
...options,
});
};
export const useChangePassword = (
options?: UseMutationOptions<{ message: string }, ApiError, PasswordChange>
) => {
return useMutation<{ message: string }, ApiError, PasswordChange>({
mutationFn: (passwordData: PasswordChange) => authService.changePassword(passwordData),
...options,
});
};
export const useResetPassword = (
options?: UseMutationOptions<{ message: string }, ApiError, PasswordReset>
) => {
return useMutation<{ message: string }, ApiError, PasswordReset>({
mutationFn: (resetData: PasswordReset) => authService.resetPassword(resetData),
...options,
});
};
export const useUpdateProfile = (
options?: UseMutationOptions<UserResponse, ApiError, UserUpdate>
) => {
const queryClient = useQueryClient();
return useMutation<UserResponse, ApiError, UserUpdate>({
mutationFn: (updateData: UserUpdate) => authService.updateProfile(updateData),
onSuccess: (data) => {
// Update the profile cache
queryClient.setQueryData(authKeys.profile(), data);
},
...options,
});
};
export const useVerifyEmail = (
options?: UseMutationOptions<{ message: string }, ApiError, { userId: string; verificationToken: string }>
) => {
const queryClient = useQueryClient();
return useMutation<{ message: string }, ApiError, { userId: string; verificationToken: string }>({
mutationFn: ({ userId, verificationToken }) =>
authService.verifyEmail(userId, verificationToken),
onSuccess: () => {
// Invalidate profile to get updated verification status
queryClient.invalidateQueries({ queryKey: authKeys.profile() });
},
...options,
});
};

View File

@@ -0,0 +1,200 @@
/**
* Classification React Query hooks
*/
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
import { classificationService } from '../services/classification';
import {
ProductClassificationRequest,
BatchClassificationRequest,
ProductSuggestionResponse,
BusinessModelAnalysisResponse,
ClassificationApprovalRequest,
ClassificationApprovalResponse,
} from '../types/classification';
import { ApiError } from '../client';
// Query Keys
export const classificationKeys = {
all: ['classification'] as const,
suggestions: {
all: () => [...classificationKeys.all, 'suggestions'] as const,
pending: (tenantId: string) => [...classificationKeys.suggestions.all(), 'pending', tenantId] as const,
history: (tenantId: string, limit?: number, offset?: number) =>
[...classificationKeys.suggestions.all(), 'history', tenantId, { limit, offset }] as const,
},
analysis: {
all: () => [...classificationKeys.all, 'analysis'] as const,
businessModel: (tenantId: string) => [...classificationKeys.analysis.all(), 'business-model', tenantId] as const,
},
} as const;
// Queries
export const usePendingSuggestions = (
tenantId: string,
options?: Omit<UseQueryOptions<ProductSuggestionResponse[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<ProductSuggestionResponse[], ApiError>({
queryKey: classificationKeys.suggestions.pending(tenantId),
queryFn: () => classificationService.getPendingSuggestions(tenantId),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
export const useSuggestionHistory = (
tenantId: string,
limit: number = 50,
offset: number = 0,
options?: Omit<UseQueryOptions<{ items: ProductSuggestionResponse[]; total: number }, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<{ items: ProductSuggestionResponse[]; total: number }, ApiError>({
queryKey: classificationKeys.suggestions.history(tenantId, limit, offset),
queryFn: () => classificationService.getSuggestionHistory(tenantId, limit, offset),
enabled: !!tenantId,
staleTime: 2 * 60 * 1000, // 2 minutes
...options,
});
};
export const useBusinessModelAnalysis = (
tenantId: string,
options?: Omit<UseQueryOptions<BusinessModelAnalysisResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<BusinessModelAnalysisResponse, ApiError>({
queryKey: classificationKeys.analysis.businessModel(tenantId),
queryFn: () => classificationService.getBusinessModelAnalysis(tenantId),
enabled: !!tenantId,
staleTime: 10 * 60 * 1000, // 10 minutes
...options,
});
};
// Mutations
export const useClassifyProduct = (
options?: UseMutationOptions<
ProductSuggestionResponse,
ApiError,
{ tenantId: string; classificationData: ProductClassificationRequest }
>
) => {
const queryClient = useQueryClient();
return useMutation<
ProductSuggestionResponse,
ApiError,
{ tenantId: string; classificationData: ProductClassificationRequest }
>({
mutationFn: ({ tenantId, classificationData }) =>
classificationService.classifyProduct(tenantId, classificationData),
onSuccess: (data, { tenantId }) => {
// Invalidate pending suggestions to include the new one
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.pending(tenantId) });
},
...options,
});
};
export const useClassifyProductsBatch = (
options?: UseMutationOptions<
ProductSuggestionResponse[],
ApiError,
{ tenantId: string; batchData: BatchClassificationRequest }
>
) => {
const queryClient = useQueryClient();
return useMutation<
ProductSuggestionResponse[],
ApiError,
{ tenantId: string; batchData: BatchClassificationRequest }
>({
mutationFn: ({ tenantId, batchData }) =>
classificationService.classifyProductsBatch(tenantId, batchData),
onSuccess: (data, { tenantId }) => {
// Invalidate pending suggestions to include the new ones
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.pending(tenantId) });
},
...options,
});
};
export const useApproveClassification = (
options?: UseMutationOptions<
ClassificationApprovalResponse,
ApiError,
{ tenantId: string; approvalData: ClassificationApprovalRequest }
>
) => {
const queryClient = useQueryClient();
return useMutation<
ClassificationApprovalResponse,
ApiError,
{ tenantId: string; approvalData: ClassificationApprovalRequest }
>({
mutationFn: ({ tenantId, approvalData }) =>
classificationService.approveClassification(tenantId, approvalData),
onSuccess: (data, { tenantId }) => {
// Invalidate suggestions lists
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.pending(tenantId) });
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.history(tenantId) });
// If approved and ingredient was created, invalidate inventory queries
if (data.approved && data.created_ingredient) {
queryClient.invalidateQueries({ queryKey: ['inventory', 'ingredients'] });
}
},
...options,
});
};
export const useUpdateSuggestion = (
options?: UseMutationOptions<
ProductSuggestionResponse,
ApiError,
{ tenantId: string; suggestionId: string; updateData: Partial<ProductSuggestionResponse> }
>
) => {
const queryClient = useQueryClient();
return useMutation<
ProductSuggestionResponse,
ApiError,
{ tenantId: string; suggestionId: string; updateData: Partial<ProductSuggestionResponse> }
>({
mutationFn: ({ tenantId, suggestionId, updateData }) =>
classificationService.updateSuggestion(tenantId, suggestionId, updateData),
onSuccess: (data, { tenantId }) => {
// Invalidate suggestions lists
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.pending(tenantId) });
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.history(tenantId) });
},
...options,
});
};
export const useDeleteSuggestion = (
options?: UseMutationOptions<
{ message: string },
ApiError,
{ tenantId: string; suggestionId: string }
>
) => {
const queryClient = useQueryClient();
return useMutation<
{ message: string },
ApiError,
{ tenantId: string; suggestionId: string }
>({
mutationFn: ({ tenantId, suggestionId }) =>
classificationService.deleteSuggestion(tenantId, suggestionId),
onSuccess: (data, { tenantId }) => {
// Invalidate suggestions lists
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.pending(tenantId) });
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.history(tenantId) });
},
...options,
});
};

View File

@@ -0,0 +1,384 @@
/**
* Food Safety React Query hooks
*/
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
import { foodSafetyService } from '../services/foodSafety';
import {
FoodSafetyComplianceCreate,
FoodSafetyComplianceUpdate,
FoodSafetyComplianceResponse,
TemperatureLogCreate,
BulkTemperatureLogCreate,
TemperatureLogResponse,
FoodSafetyAlertCreate,
FoodSafetyAlertUpdate,
FoodSafetyAlertResponse,
FoodSafetyFilter,
TemperatureMonitoringFilter,
FoodSafetyMetrics,
TemperatureAnalytics,
FoodSafetyDashboard,
} from '../types/foodSafety';
import { PaginatedResponse } from '../types/inventory';
import { ApiError } from '../client';
// Query Keys
export const foodSafetyKeys = {
all: ['food-safety'] as const,
compliance: {
all: () => [...foodSafetyKeys.all, 'compliance'] as const,
lists: () => [...foodSafetyKeys.compliance.all(), 'list'] as const,
list: (tenantId: string, filter?: FoodSafetyFilter) =>
[...foodSafetyKeys.compliance.lists(), tenantId, filter] as const,
details: () => [...foodSafetyKeys.compliance.all(), 'detail'] as const,
detail: (tenantId: string, recordId: string) =>
[...foodSafetyKeys.compliance.details(), tenantId, recordId] as const,
},
temperature: {
all: () => [...foodSafetyKeys.all, 'temperature'] as const,
lists: () => [...foodSafetyKeys.temperature.all(), 'list'] as const,
list: (tenantId: string, filter?: TemperatureMonitoringFilter) =>
[...foodSafetyKeys.temperature.lists(), tenantId, filter] as const,
analytics: (tenantId: string, location: string, startDate?: string, endDate?: string) =>
[...foodSafetyKeys.temperature.all(), 'analytics', tenantId, location, { startDate, endDate }] as const,
violations: (tenantId: string, limit?: number) =>
[...foodSafetyKeys.temperature.all(), 'violations', tenantId, limit] as const,
},
alerts: {
all: () => [...foodSafetyKeys.all, 'alerts'] as const,
lists: () => [...foodSafetyKeys.alerts.all(), 'list'] as const,
list: (tenantId: string, status?: string, severity?: string, limit?: number, offset?: number) =>
[...foodSafetyKeys.alerts.lists(), tenantId, { status, severity, limit, offset }] as const,
details: () => [...foodSafetyKeys.alerts.all(), 'detail'] as const,
detail: (tenantId: string, alertId: string) =>
[...foodSafetyKeys.alerts.details(), tenantId, alertId] as const,
},
dashboard: (tenantId: string) =>
[...foodSafetyKeys.all, 'dashboard', tenantId] as const,
metrics: (tenantId: string, startDate?: string, endDate?: string) =>
[...foodSafetyKeys.all, 'metrics', tenantId, { startDate, endDate }] as const,
complianceRate: (tenantId: string, startDate?: string, endDate?: string) =>
[...foodSafetyKeys.all, 'compliance-rate', tenantId, { startDate, endDate }] as const,
} as const;
// Compliance Queries
export const useComplianceRecords = (
tenantId: string,
filter?: FoodSafetyFilter,
options?: Omit<UseQueryOptions<PaginatedResponse<FoodSafetyComplianceResponse>, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<PaginatedResponse<FoodSafetyComplianceResponse>, ApiError>({
queryKey: foodSafetyKeys.compliance.list(tenantId, filter),
queryFn: () => foodSafetyService.getComplianceRecords(tenantId, filter),
enabled: !!tenantId,
staleTime: 1 * 60 * 1000, // 1 minute
...options,
});
};
export const useComplianceRecord = (
tenantId: string,
recordId: string,
options?: Omit<UseQueryOptions<FoodSafetyComplianceResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<FoodSafetyComplianceResponse, ApiError>({
queryKey: foodSafetyKeys.compliance.detail(tenantId, recordId),
queryFn: () => foodSafetyService.getComplianceRecord(tenantId, recordId),
enabled: !!tenantId && !!recordId,
staleTime: 2 * 60 * 1000, // 2 minutes
...options,
});
};
// Temperature Monitoring Queries
export const useTemperatureLogs = (
tenantId: string,
filter?: TemperatureMonitoringFilter,
options?: Omit<UseQueryOptions<PaginatedResponse<TemperatureLogResponse>, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<PaginatedResponse<TemperatureLogResponse>, ApiError>({
queryKey: foodSafetyKeys.temperature.list(tenantId, filter),
queryFn: () => foodSafetyService.getTemperatureLogs(tenantId, filter),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
export const useTemperatureAnalytics = (
tenantId: string,
location: string,
startDate?: string,
endDate?: string,
options?: Omit<UseQueryOptions<TemperatureAnalytics, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<TemperatureAnalytics, ApiError>({
queryKey: foodSafetyKeys.temperature.analytics(tenantId, location, startDate, endDate),
queryFn: () => foodSafetyService.getTemperatureAnalytics(tenantId, location, startDate, endDate),
enabled: !!tenantId && !!location,
staleTime: 2 * 60 * 1000, // 2 minutes
...options,
});
};
export const useTemperatureViolations = (
tenantId: string,
limit: number = 20,
options?: Omit<UseQueryOptions<TemperatureLogResponse[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<TemperatureLogResponse[], ApiError>({
queryKey: foodSafetyKeys.temperature.violations(tenantId, limit),
queryFn: () => foodSafetyService.getTemperatureViolations(tenantId, limit),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
// Alert Queries
export const useFoodSafetyAlerts = (
tenantId: string,
status?: 'open' | 'in_progress' | 'resolved' | 'dismissed',
severity?: 'critical' | 'warning' | 'info',
limit: number = 50,
offset: number = 0,
options?: Omit<UseQueryOptions<PaginatedResponse<FoodSafetyAlertResponse>, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<PaginatedResponse<FoodSafetyAlertResponse>, ApiError>({
queryKey: foodSafetyKeys.alerts.list(tenantId, status, severity, limit, offset),
queryFn: () => foodSafetyService.getFoodSafetyAlerts(tenantId, status, severity, limit, offset),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
export const useFoodSafetyAlert = (
tenantId: string,
alertId: string,
options?: Omit<UseQueryOptions<FoodSafetyAlertResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<FoodSafetyAlertResponse, ApiError>({
queryKey: foodSafetyKeys.alerts.detail(tenantId, alertId),
queryFn: () => foodSafetyService.getFoodSafetyAlert(tenantId, alertId),
enabled: !!tenantId && !!alertId,
staleTime: 1 * 60 * 1000, // 1 minute
...options,
});
};
// Dashboard and Metrics Queries
export const useFoodSafetyDashboard = (
tenantId: string,
options?: Omit<UseQueryOptions<FoodSafetyDashboard, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<FoodSafetyDashboard, ApiError>({
queryKey: foodSafetyKeys.dashboard(tenantId),
queryFn: () => foodSafetyService.getFoodSafetyDashboard(tenantId),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
export const useFoodSafetyMetrics = (
tenantId: string,
startDate?: string,
endDate?: string,
options?: Omit<UseQueryOptions<FoodSafetyMetrics, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<FoodSafetyMetrics, ApiError>({
queryKey: foodSafetyKeys.metrics(tenantId, startDate, endDate),
queryFn: () => foodSafetyService.getFoodSafetyMetrics(tenantId, startDate, endDate),
enabled: !!tenantId,
staleTime: 2 * 60 * 1000, // 2 minutes
...options,
});
};
export const useComplianceRate = (
tenantId: string,
startDate?: string,
endDate?: string,
options?: Omit<UseQueryOptions<{
overall_rate: number;
by_type: Record<string, number>;
trend: Array<{ date: string; rate: number }>;
}, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<{
overall_rate: number;
by_type: Record<string, number>;
trend: Array<{ date: string; rate: number }>;
}, ApiError>({
queryKey: foodSafetyKeys.complianceRate(tenantId, startDate, endDate),
queryFn: () => foodSafetyService.getComplianceRate(tenantId, startDate, endDate),
enabled: !!tenantId,
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
// Compliance Mutations
export const useCreateComplianceRecord = (
options?: UseMutationOptions<
FoodSafetyComplianceResponse,
ApiError,
{ tenantId: string; complianceData: FoodSafetyComplianceCreate }
>
) => {
const queryClient = useQueryClient();
return useMutation<
FoodSafetyComplianceResponse,
ApiError,
{ tenantId: string; complianceData: FoodSafetyComplianceCreate }
>({
mutationFn: ({ tenantId, complianceData }) =>
foodSafetyService.createComplianceRecord(tenantId, complianceData),
onSuccess: (data, { tenantId }) => {
// Add to cache
queryClient.setQueryData(foodSafetyKeys.compliance.detail(tenantId, data.id), data);
// Invalidate lists
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.compliance.lists() });
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.dashboard(tenantId) });
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.metrics(tenantId) });
},
...options,
});
};
export const useUpdateComplianceRecord = (
options?: UseMutationOptions<
FoodSafetyComplianceResponse,
ApiError,
{ tenantId: string; recordId: string; updateData: FoodSafetyComplianceUpdate }
>
) => {
const queryClient = useQueryClient();
return useMutation<
FoodSafetyComplianceResponse,
ApiError,
{ tenantId: string; recordId: string; updateData: FoodSafetyComplianceUpdate }
>({
mutationFn: ({ tenantId, recordId, updateData }) =>
foodSafetyService.updateComplianceRecord(tenantId, recordId, updateData),
onSuccess: (data, { tenantId, recordId }) => {
// Update cache
queryClient.setQueryData(foodSafetyKeys.compliance.detail(tenantId, recordId), data);
// Invalidate lists
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.compliance.lists() });
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.dashboard(tenantId) });
},
...options,
});
};
// Temperature Mutations
export const useCreateTemperatureLog = (
options?: UseMutationOptions<
TemperatureLogResponse,
ApiError,
{ tenantId: string; logData: TemperatureLogCreate }
>
) => {
const queryClient = useQueryClient();
return useMutation<
TemperatureLogResponse,
ApiError,
{ tenantId: string; logData: TemperatureLogCreate }
>({
mutationFn: ({ tenantId, logData }) => foodSafetyService.createTemperatureLog(tenantId, logData),
onSuccess: (data, { tenantId }) => {
// Invalidate temperature queries
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.temperature.lists() });
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.dashboard(tenantId) });
// If alert was triggered, invalidate alerts
if (data.alert_triggered) {
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.alerts.lists() });
}
},
...options,
});
};
export const useCreateBulkTemperatureLogs = (
options?: UseMutationOptions<
{ created_count: number; failed_count: number; errors?: string[] },
ApiError,
{ tenantId: string; bulkData: BulkTemperatureLogCreate }
>
) => {
const queryClient = useQueryClient();
return useMutation<
{ created_count: number; failed_count: number; errors?: string[] },
ApiError,
{ tenantId: string; bulkData: BulkTemperatureLogCreate }
>({
mutationFn: ({ tenantId, bulkData }) => foodSafetyService.createBulkTemperatureLogs(tenantId, bulkData),
onSuccess: (data, { tenantId }) => {
// Invalidate temperature queries
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.temperature.lists() });
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.dashboard(tenantId) });
},
...options,
});
};
// Alert Mutations
export const useCreateFoodSafetyAlert = (
options?: UseMutationOptions<
FoodSafetyAlertResponse,
ApiError,
{ tenantId: string; alertData: FoodSafetyAlertCreate }
>
) => {
const queryClient = useQueryClient();
return useMutation<
FoodSafetyAlertResponse,
ApiError,
{ tenantId: string; alertData: FoodSafetyAlertCreate }
>({
mutationFn: ({ tenantId, alertData }) => foodSafetyService.createFoodSafetyAlert(tenantId, alertData),
onSuccess: (data, { tenantId }) => {
// Add to cache
queryClient.setQueryData(foodSafetyKeys.alerts.detail(tenantId, data.id), data);
// Invalidate lists
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.alerts.lists() });
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.dashboard(tenantId) });
},
...options,
});
};
export const useUpdateFoodSafetyAlert = (
options?: UseMutationOptions<
FoodSafetyAlertResponse,
ApiError,
{ tenantId: string; alertId: string; updateData: FoodSafetyAlertUpdate }
>
) => {
const queryClient = useQueryClient();
return useMutation<
FoodSafetyAlertResponse,
ApiError,
{ tenantId: string; alertId: string; updateData: FoodSafetyAlertUpdate }
>({
mutationFn: ({ tenantId, alertId, updateData }) =>
foodSafetyService.updateFoodSafetyAlert(tenantId, alertId, updateData),
onSuccess: (data, { tenantId, alertId }) => {
// Update cache
queryClient.setQueryData(foodSafetyKeys.alerts.detail(tenantId, alertId), data);
// Invalidate lists
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.alerts.lists() });
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.dashboard(tenantId) });
},
...options,
});
};

View File

@@ -0,0 +1,372 @@
/**
* Inventory React Query hooks
*/
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
import { inventoryService } from '../services/inventory';
import {
IngredientCreate,
IngredientUpdate,
IngredientResponse,
StockCreate,
StockUpdate,
StockResponse,
StockMovementCreate,
StockMovementResponse,
InventoryFilter,
StockFilter,
StockConsumptionRequest,
StockConsumptionResponse,
PaginatedResponse,
} from '../types/inventory';
import { ApiError } from '../client';
// Query Keys
export const inventoryKeys = {
all: ['inventory'] as const,
ingredients: {
all: () => [...inventoryKeys.all, 'ingredients'] as const,
lists: () => [...inventoryKeys.ingredients.all(), 'list'] as const,
list: (tenantId: string, filters?: InventoryFilter) =>
[...inventoryKeys.ingredients.lists(), tenantId, filters] as const,
details: () => [...inventoryKeys.ingredients.all(), 'detail'] as const,
detail: (tenantId: string, ingredientId: string) =>
[...inventoryKeys.ingredients.details(), tenantId, ingredientId] as const,
byCategory: (tenantId: string) =>
[...inventoryKeys.ingredients.all(), 'by-category', tenantId] as const,
lowStock: (tenantId: string) =>
[...inventoryKeys.ingredients.all(), 'low-stock', tenantId] as const,
},
stock: {
all: () => [...inventoryKeys.all, 'stock'] as const,
lists: () => [...inventoryKeys.stock.all(), 'list'] as const,
list: (tenantId: string, filters?: StockFilter) =>
[...inventoryKeys.stock.lists(), tenantId, filters] as const,
details: () => [...inventoryKeys.stock.all(), 'detail'] as const,
detail: (tenantId: string, stockId: string) =>
[...inventoryKeys.stock.details(), tenantId, stockId] as const,
byIngredient: (tenantId: string, ingredientId: string, includeUnavailable?: boolean) =>
[...inventoryKeys.stock.all(), 'by-ingredient', tenantId, ingredientId, includeUnavailable] as const,
expiring: (tenantId: string, withinDays?: number) =>
[...inventoryKeys.stock.all(), 'expiring', tenantId, withinDays] as const,
expired: (tenantId: string) =>
[...inventoryKeys.stock.all(), 'expired', tenantId] as const,
movements: (tenantId: string, ingredientId?: string) =>
[...inventoryKeys.stock.all(), 'movements', tenantId, ingredientId] as const,
},
analytics: (tenantId: string, startDate?: string, endDate?: string) =>
[...inventoryKeys.all, 'analytics', tenantId, { startDate, endDate }] as const,
} as const;
// Ingredient Queries
export const useIngredients = (
tenantId: string,
filter?: InventoryFilter,
options?: Omit<UseQueryOptions<PaginatedResponse<IngredientResponse>, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<PaginatedResponse<IngredientResponse>, ApiError>({
queryKey: inventoryKeys.ingredients.list(tenantId, filter),
queryFn: () => inventoryService.getIngredients(tenantId, filter),
enabled: !!tenantId,
staleTime: 2 * 60 * 1000, // 2 minutes
...options,
});
};
export const useIngredient = (
tenantId: string,
ingredientId: string,
options?: Omit<UseQueryOptions<IngredientResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<IngredientResponse, ApiError>({
queryKey: inventoryKeys.ingredients.detail(tenantId, ingredientId),
queryFn: () => inventoryService.getIngredient(tenantId, ingredientId),
enabled: !!tenantId && !!ingredientId,
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
export const useIngredientsByCategory = (
tenantId: string,
options?: Omit<UseQueryOptions<Record<string, IngredientResponse[]>, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<Record<string, IngredientResponse[]>, ApiError>({
queryKey: inventoryKeys.ingredients.byCategory(tenantId),
queryFn: () => inventoryService.getIngredientsByCategory(tenantId),
enabled: !!tenantId,
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
export const useLowStockIngredients = (
tenantId: string,
options?: Omit<UseQueryOptions<IngredientResponse[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<IngredientResponse[], ApiError>({
queryKey: inventoryKeys.ingredients.lowStock(tenantId),
queryFn: () => inventoryService.getLowStockIngredients(tenantId),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
// Stock Queries
export const useStock = (
tenantId: string,
filter?: StockFilter,
options?: Omit<UseQueryOptions<PaginatedResponse<StockResponse>, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<PaginatedResponse<StockResponse>, ApiError>({
queryKey: inventoryKeys.stock.list(tenantId, filter),
queryFn: () => inventoryService.getAllStock(tenantId, filter),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
export const useStockByIngredient = (
tenantId: string,
ingredientId: string,
includeUnavailable: boolean = false,
options?: Omit<UseQueryOptions<StockResponse[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<StockResponse[], ApiError>({
queryKey: inventoryKeys.stock.byIngredient(tenantId, ingredientId, includeUnavailable),
queryFn: () => inventoryService.getStockByIngredient(tenantId, ingredientId, includeUnavailable),
enabled: !!tenantId && !!ingredientId,
staleTime: 1 * 60 * 1000, // 1 minute
...options,
});
};
export const useExpiringStock = (
tenantId: string,
withinDays: number = 7,
options?: Omit<UseQueryOptions<StockResponse[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<StockResponse[], ApiError>({
queryKey: inventoryKeys.stock.expiring(tenantId, withinDays),
queryFn: () => inventoryService.getExpiringStock(tenantId, withinDays),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
export const useExpiredStock = (
tenantId: string,
options?: Omit<UseQueryOptions<StockResponse[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<StockResponse[], ApiError>({
queryKey: inventoryKeys.stock.expired(tenantId),
queryFn: () => inventoryService.getExpiredStock(tenantId),
enabled: !!tenantId,
staleTime: 1 * 60 * 1000, // 1 minute
...options,
});
};
export const useStockMovements = (
tenantId: string,
ingredientId?: string,
limit: number = 50,
offset: number = 0,
options?: Omit<UseQueryOptions<PaginatedResponse<StockMovementResponse>, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<PaginatedResponse<StockMovementResponse>, ApiError>({
queryKey: inventoryKeys.stock.movements(tenantId, ingredientId),
queryFn: () => inventoryService.getStockMovements(tenantId, ingredientId, limit, offset),
enabled: !!tenantId,
staleTime: 1 * 60 * 1000, // 1 minute
...options,
});
};
export const useStockAnalytics = (
tenantId: string,
startDate?: string,
endDate?: string,
options?: Omit<UseQueryOptions<any, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<any, ApiError>({
queryKey: inventoryKeys.analytics(tenantId, startDate, endDate),
queryFn: () => inventoryService.getStockAnalytics(tenantId, startDate, endDate),
enabled: !!tenantId,
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
// Ingredient Mutations
export const useCreateIngredient = (
options?: UseMutationOptions<IngredientResponse, ApiError, { tenantId: string; ingredientData: IngredientCreate }>
) => {
const queryClient = useQueryClient();
return useMutation<IngredientResponse, ApiError, { tenantId: string; ingredientData: IngredientCreate }>({
mutationFn: ({ tenantId, ingredientData }) => inventoryService.createIngredient(tenantId, ingredientData),
onSuccess: (data, { tenantId }) => {
// Add to cache
queryClient.setQueryData(inventoryKeys.ingredients.detail(tenantId, data.id), data);
// Invalidate lists
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() });
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.byCategory(tenantId) });
},
...options,
});
};
export const useUpdateIngredient = (
options?: UseMutationOptions<
IngredientResponse,
ApiError,
{ tenantId: string; ingredientId: string; updateData: IngredientUpdate }
>
) => {
const queryClient = useQueryClient();
return useMutation<
IngredientResponse,
ApiError,
{ tenantId: string; ingredientId: string; updateData: IngredientUpdate }
>({
mutationFn: ({ tenantId, ingredientId, updateData }) =>
inventoryService.updateIngredient(tenantId, ingredientId, updateData),
onSuccess: (data, { tenantId, ingredientId }) => {
// Update cache
queryClient.setQueryData(inventoryKeys.ingredients.detail(tenantId, ingredientId), data);
// Invalidate lists
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() });
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.byCategory(tenantId) });
},
...options,
});
};
export const useDeleteIngredient = (
options?: UseMutationOptions<{ message: string }, ApiError, { tenantId: string; ingredientId: string }>
) => {
const queryClient = useQueryClient();
return useMutation<{ message: string }, ApiError, { tenantId: string; ingredientId: string }>({
mutationFn: ({ tenantId, ingredientId }) => inventoryService.deleteIngredient(tenantId, ingredientId),
onSuccess: (data, { tenantId, ingredientId }) => {
// Remove from cache
queryClient.removeQueries({ queryKey: inventoryKeys.ingredients.detail(tenantId, ingredientId) });
// Invalidate lists
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() });
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.byCategory(tenantId) });
},
...options,
});
};
// Stock Mutations
export const useAddStock = (
options?: UseMutationOptions<StockResponse, ApiError, { tenantId: string; stockData: StockCreate }>
) => {
const queryClient = useQueryClient();
return useMutation<StockResponse, ApiError, { tenantId: string; stockData: StockCreate }>({
mutationFn: ({ tenantId, stockData }) => inventoryService.addStock(tenantId, stockData),
onSuccess: (data, { tenantId }) => {
// Invalidate stock queries
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() });
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.byIngredient(tenantId, data.ingredient_id) });
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() });
},
...options,
});
};
export const useUpdateStock = (
options?: UseMutationOptions<
StockResponse,
ApiError,
{ tenantId: string; stockId: string; updateData: StockUpdate }
>
) => {
const queryClient = useQueryClient();
return useMutation<
StockResponse,
ApiError,
{ tenantId: string; stockId: string; updateData: StockUpdate }
>({
mutationFn: ({ tenantId, stockId, updateData }) =>
inventoryService.updateStock(tenantId, stockId, updateData),
onSuccess: (data, { tenantId, stockId }) => {
// Update cache
queryClient.setQueryData(inventoryKeys.stock.detail(tenantId, stockId), data);
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() });
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.byIngredient(tenantId, data.ingredient_id) });
},
...options,
});
};
export const useConsumeStock = (
options?: UseMutationOptions<
StockConsumptionResponse,
ApiError,
{ tenantId: string; consumptionData: StockConsumptionRequest }
>
) => {
const queryClient = useQueryClient();
return useMutation<
StockConsumptionResponse,
ApiError,
{ tenantId: string; consumptionData: StockConsumptionRequest }
>({
mutationFn: ({ tenantId, consumptionData }) => inventoryService.consumeStock(tenantId, consumptionData),
onSuccess: (data, { tenantId, consumptionData }) => {
// Invalidate stock queries for the affected ingredient
queryClient.invalidateQueries({
queryKey: inventoryKeys.stock.byIngredient(tenantId, consumptionData.ingredient_id)
});
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() });
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId) });
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() });
},
...options,
});
};
export const useCreateStockMovement = (
options?: UseMutationOptions<
StockMovementResponse,
ApiError,
{ tenantId: string; movementData: StockMovementCreate }
>
) => {
const queryClient = useQueryClient();
return useMutation<
StockMovementResponse,
ApiError,
{ tenantId: string; movementData: StockMovementCreate }
>({
mutationFn: ({ tenantId, movementData }) => inventoryService.createStockMovement(tenantId, movementData),
onSuccess: (data, { tenantId, movementData }) => {
// Invalidate movement queries
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId) });
queryClient.invalidateQueries({
queryKey: inventoryKeys.stock.movements(tenantId, movementData.ingredient_id)
});
// Invalidate stock queries if this affects stock levels
if (['in', 'out', 'adjustment'].includes(movementData.movement_type)) {
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() });
queryClient.invalidateQueries({
queryKey: inventoryKeys.stock.byIngredient(tenantId, movementData.ingredient_id)
});
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() });
}
},
...options,
});
};

View File

@@ -0,0 +1,183 @@
/**
* Inventory Dashboard React Query hooks
*/
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
import { inventoryDashboardService } from '../services/inventoryDashboard';
import {
InventoryDashboardSummary,
InventoryAnalytics,
BusinessModelInsights,
DashboardFilter,
AlertsFilter,
RecentActivity,
} from '../types/dashboard';
import { ApiError } from '../client';
// Query Keys
export const inventoryDashboardKeys = {
all: ['inventory-dashboard'] as const,
summary: (tenantId: string, filter?: DashboardFilter) =>
[...inventoryDashboardKeys.all, 'summary', tenantId, filter] as const,
analytics: (tenantId: string, startDate?: string, endDate?: string) =>
[...inventoryDashboardKeys.all, 'analytics', tenantId, { startDate, endDate }] as const,
insights: (tenantId: string) =>
[...inventoryDashboardKeys.all, 'business-insights', tenantId] as const,
activity: (tenantId: string, limit?: number) =>
[...inventoryDashboardKeys.all, 'recent-activity', tenantId, limit] as const,
alerts: (tenantId: string, filter?: AlertsFilter) =>
[...inventoryDashboardKeys.all, 'alerts', tenantId, filter] as const,
stockSummary: (tenantId: string) =>
[...inventoryDashboardKeys.all, 'stock-summary', tenantId] as const,
topCategories: (tenantId: string, limit?: number) =>
[...inventoryDashboardKeys.all, 'top-categories', tenantId, limit] as const,
expiryCalendar: (tenantId: string, daysAhead?: number) =>
[...inventoryDashboardKeys.all, 'expiry-calendar', tenantId, daysAhead] as const,
} as const;
// Queries
export const useInventoryDashboardSummary = (
tenantId: string,
filter?: DashboardFilter,
options?: Omit<UseQueryOptions<InventoryDashboardSummary, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<InventoryDashboardSummary, ApiError>({
queryKey: inventoryDashboardKeys.summary(tenantId, filter),
queryFn: () => inventoryDashboardService.getDashboardSummary(tenantId, filter),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
export const useInventoryAnalytics = (
tenantId: string,
startDate?: string,
endDate?: string,
options?: Omit<UseQueryOptions<InventoryAnalytics, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<InventoryAnalytics, ApiError>({
queryKey: inventoryDashboardKeys.analytics(tenantId, startDate, endDate),
queryFn: () => inventoryDashboardService.getInventoryAnalytics(tenantId, startDate, endDate),
enabled: !!tenantId,
staleTime: 2 * 60 * 1000, // 2 minutes
...options,
});
};
export const useBusinessModelInsights = (
tenantId: string,
options?: Omit<UseQueryOptions<BusinessModelInsights, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<BusinessModelInsights, ApiError>({
queryKey: inventoryDashboardKeys.insights(tenantId),
queryFn: () => inventoryDashboardService.getBusinessModelInsights(tenantId),
enabled: !!tenantId,
staleTime: 10 * 60 * 1000, // 10 minutes
...options,
});
};
export const useRecentActivity = (
tenantId: string,
limit: number = 20,
options?: Omit<UseQueryOptions<RecentActivity[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<RecentActivity[], ApiError>({
queryKey: inventoryDashboardKeys.activity(tenantId, limit),
queryFn: () => inventoryDashboardService.getRecentActivity(tenantId, limit),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
export const useInventoryAlerts = (
tenantId: string,
filter?: AlertsFilter,
options?: Omit<UseQueryOptions<{ items: any[]; total: number }, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<{ items: any[]; total: number }, ApiError>({
queryKey: inventoryDashboardKeys.alerts(tenantId, filter),
queryFn: () => inventoryDashboardService.getAlerts(tenantId, filter),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
export const useStockSummary = (
tenantId: string,
options?: Omit<UseQueryOptions<{
in_stock: number;
low_stock: number;
out_of_stock: number;
overstock: number;
total_value: number;
}, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<{
in_stock: number;
low_stock: number;
out_of_stock: number;
overstock: number;
total_value: number;
}, ApiError>({
queryKey: inventoryDashboardKeys.stockSummary(tenantId),
queryFn: () => inventoryDashboardService.getStockSummary(tenantId),
enabled: !!tenantId,
staleTime: 1 * 60 * 1000, // 1 minute
...options,
});
};
export const useTopCategories = (
tenantId: string,
limit: number = 10,
options?: Omit<UseQueryOptions<Array<{
category: string;
ingredient_count: number;
total_value: number;
low_stock_count: number;
}>, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<Array<{
category: string;
ingredient_count: number;
total_value: number;
low_stock_count: number;
}>, ApiError>({
queryKey: inventoryDashboardKeys.topCategories(tenantId, limit),
queryFn: () => inventoryDashboardService.getTopCategories(tenantId, limit),
enabled: !!tenantId,
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
export const useExpiryCalendar = (
tenantId: string,
daysAhead: number = 30,
options?: Omit<UseQueryOptions<Array<{
date: string;
items: Array<{
ingredient_name: string;
quantity: number;
batch_number?: string;
}>;
}>, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<Array<{
date: string;
items: Array<{
ingredient_name: string;
quantity: number;
batch_number?: string;
}>;
}>, ApiError>({
queryKey: inventoryDashboardKeys.expiryCalendar(tenantId, daysAhead),
queryFn: () => inventoryDashboardService.getExpiryCalendar(tenantId, daysAhead),
enabled: !!tenantId,
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};

View File

@@ -0,0 +1,128 @@
/**
* Onboarding React Query hooks
*/
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
import { onboardingService } from '../services/onboarding';
import { UserProgress, UpdateStepRequest } from '../types/onboarding';
import { ApiError } from '../client';
// Query Keys
export const onboardingKeys = {
all: ['onboarding'] as const,
progress: (userId: string) => [...onboardingKeys.all, 'progress', userId] as const,
steps: () => [...onboardingKeys.all, 'steps'] as const,
stepDetail: (stepName: string) => [...onboardingKeys.steps(), stepName] as const,
} as const;
// Queries
export const useUserProgress = (
userId: string,
options?: Omit<UseQueryOptions<UserProgress, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<UserProgress, ApiError>({
queryKey: onboardingKeys.progress(userId),
queryFn: () => onboardingService.getUserProgress(userId),
enabled: !!userId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
export const useAllSteps = (
options?: Omit<UseQueryOptions<Array<{
name: string;
description: string;
dependencies: string[];
estimated_time_minutes: number;
}>, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<Array<{
name: string;
description: string;
dependencies: string[];
estimated_time_minutes: number;
}>, ApiError>({
queryKey: onboardingKeys.steps(),
queryFn: () => onboardingService.getAllSteps(),
staleTime: 10 * 60 * 1000, // 10 minutes
...options,
});
};
export const useStepDetails = (
stepName: string,
options?: Omit<UseQueryOptions<{
name: string;
description: string;
dependencies: string[];
estimated_time_minutes: number;
}, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<{
name: string;
description: string;
dependencies: string[];
estimated_time_minutes: number;
}, ApiError>({
queryKey: onboardingKeys.stepDetail(stepName),
queryFn: () => onboardingService.getStepDetails(stepName),
enabled: !!stepName,
staleTime: 10 * 60 * 1000, // 10 minutes
...options,
});
};
// Mutations
export const useUpdateStep = (
options?: UseMutationOptions<UserProgress, ApiError, { userId: string; stepData: UpdateStepRequest }>
) => {
const queryClient = useQueryClient();
return useMutation<UserProgress, ApiError, { userId: string; stepData: UpdateStepRequest }>({
mutationFn: ({ userId, stepData }) => onboardingService.updateStep(userId, stepData),
onSuccess: (data, { userId }) => {
// Update progress cache
queryClient.setQueryData(onboardingKeys.progress(userId), data);
},
...options,
});
};
export const useMarkStepCompleted = (
options?: UseMutationOptions<
UserProgress,
ApiError,
{ userId: string; stepName: string; data?: Record<string, any> }
>
) => {
const queryClient = useQueryClient();
return useMutation<
UserProgress,
ApiError,
{ userId: string; stepName: string; data?: Record<string, any> }
>({
mutationFn: ({ userId, stepName, data }) =>
onboardingService.markStepCompleted(userId, stepName, data),
onSuccess: (data, { userId }) => {
// Update progress cache
queryClient.setQueryData(onboardingKeys.progress(userId), data);
},
...options,
});
};
export const useResetProgress = (
options?: UseMutationOptions<UserProgress, ApiError, string>
) => {
const queryClient = useQueryClient();
return useMutation<UserProgress, ApiError, string>({
mutationFn: (userId: string) => onboardingService.resetProgress(userId),
onSuccess: (data, userId) => {
// Update progress cache
queryClient.setQueryData(onboardingKeys.progress(userId), data);
},
...options,
});
};

View File

@@ -0,0 +1,190 @@
/**
* Sales React Query hooks
*/
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
import { salesService } from '../services/sales';
import {
SalesDataCreate,
SalesDataUpdate,
SalesDataResponse,
SalesDataQuery,
SalesAnalytics,
} from '../types/sales';
import { ApiError } from '../client';
// Query Keys
export const salesKeys = {
all: ['sales'] as const,
lists: () => [...salesKeys.all, 'list'] as const,
list: (tenantId: string, filters?: SalesDataQuery) => [...salesKeys.lists(), tenantId, filters] as const,
details: () => [...salesKeys.all, 'detail'] as const,
detail: (tenantId: string, recordId: string) => [...salesKeys.details(), tenantId, recordId] as const,
analytics: (tenantId: string, startDate?: string, endDate?: string) =>
[...salesKeys.all, 'analytics', tenantId, { startDate, endDate }] as const,
productSales: (tenantId: string, productId: string, startDate?: string, endDate?: string) =>
[...salesKeys.all, 'product-sales', tenantId, productId, { startDate, endDate }] as const,
categories: (tenantId: string) => [...salesKeys.all, 'categories', tenantId] as const,
} as const;
// Queries
export const useSalesRecords = (
tenantId: string,
query?: SalesDataQuery,
options?: Omit<UseQueryOptions<SalesDataResponse[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<SalesDataResponse[], ApiError>({
queryKey: salesKeys.list(tenantId, query),
queryFn: () => salesService.getSalesRecords(tenantId, query),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
export const useSalesRecord = (
tenantId: string,
recordId: string,
options?: Omit<UseQueryOptions<SalesDataResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<SalesDataResponse, ApiError>({
queryKey: salesKeys.detail(tenantId, recordId),
queryFn: () => salesService.getSalesRecord(tenantId, recordId),
enabled: !!tenantId && !!recordId,
staleTime: 2 * 60 * 1000, // 2 minutes
...options,
});
};
export const useSalesAnalytics = (
tenantId: string,
startDate?: string,
endDate?: string,
options?: Omit<UseQueryOptions<SalesAnalytics, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<SalesAnalytics, ApiError>({
queryKey: salesKeys.analytics(tenantId, startDate, endDate),
queryFn: () => salesService.getSalesAnalytics(tenantId, startDate, endDate),
enabled: !!tenantId,
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
export const useProductSales = (
tenantId: string,
inventoryProductId: string,
startDate?: string,
endDate?: string,
options?: Omit<UseQueryOptions<SalesDataResponse[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<SalesDataResponse[], ApiError>({
queryKey: salesKeys.productSales(tenantId, inventoryProductId, startDate, endDate),
queryFn: () => salesService.getProductSales(tenantId, inventoryProductId, startDate, endDate),
enabled: !!tenantId && !!inventoryProductId,
staleTime: 2 * 60 * 1000, // 2 minutes
...options,
});
};
export const useProductCategories = (
tenantId: string,
options?: Omit<UseQueryOptions<string[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<string[], ApiError>({
queryKey: salesKeys.categories(tenantId),
queryFn: () => salesService.getProductCategories(tenantId),
enabled: !!tenantId,
staleTime: 10 * 60 * 1000, // 10 minutes
...options,
});
};
// Mutations
export const useCreateSalesRecord = (
options?: UseMutationOptions<SalesDataResponse, ApiError, { tenantId: string; salesData: SalesDataCreate }>
) => {
const queryClient = useQueryClient();
return useMutation<SalesDataResponse, ApiError, { tenantId: string; salesData: SalesDataCreate }>({
mutationFn: ({ tenantId, salesData }) => salesService.createSalesRecord(tenantId, salesData),
onSuccess: (data, { tenantId }) => {
// Invalidate sales lists to refresh data
queryClient.invalidateQueries({ queryKey: salesKeys.lists() });
queryClient.invalidateQueries({ queryKey: salesKeys.analytics(tenantId) });
// Set the new record in cache
queryClient.setQueryData(salesKeys.detail(tenantId, data.id), data);
},
...options,
});
};
export const useUpdateSalesRecord = (
options?: UseMutationOptions<
SalesDataResponse,
ApiError,
{ tenantId: string; recordId: string; updateData: SalesDataUpdate }
>
) => {
const queryClient = useQueryClient();
return useMutation<
SalesDataResponse,
ApiError,
{ tenantId: string; recordId: string; updateData: SalesDataUpdate }
>({
mutationFn: ({ tenantId, recordId, updateData }) =>
salesService.updateSalesRecord(tenantId, recordId, updateData),
onSuccess: (data, { tenantId, recordId }) => {
// Update the record cache
queryClient.setQueryData(salesKeys.detail(tenantId, recordId), data);
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: salesKeys.lists() });
queryClient.invalidateQueries({ queryKey: salesKeys.analytics(tenantId) });
},
...options,
});
};
export const useDeleteSalesRecord = (
options?: UseMutationOptions<{ message: string }, ApiError, { tenantId: string; recordId: string }>
) => {
const queryClient = useQueryClient();
return useMutation<{ message: string }, ApiError, { tenantId: string; recordId: string }>({
mutationFn: ({ tenantId, recordId }) => salesService.deleteSalesRecord(tenantId, recordId),
onSuccess: (data, { tenantId, recordId }) => {
// Remove from cache
queryClient.removeQueries({ queryKey: salesKeys.detail(tenantId, recordId) });
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: salesKeys.lists() });
queryClient.invalidateQueries({ queryKey: salesKeys.analytics(tenantId) });
},
...options,
});
};
export const useValidateSalesRecord = (
options?: UseMutationOptions<
SalesDataResponse,
ApiError,
{ tenantId: string; recordId: string; validationNotes?: string }
>
) => {
const queryClient = useQueryClient();
return useMutation<
SalesDataResponse,
ApiError,
{ tenantId: string; recordId: string; validationNotes?: string }
>({
mutationFn: ({ tenantId, recordId, validationNotes }) =>
salesService.validateSalesRecord(tenantId, recordId, validationNotes),
onSuccess: (data, { tenantId, recordId }) => {
// Update the record cache
queryClient.setQueryData(salesKeys.detail(tenantId, recordId), data);
// Invalidate sales lists to reflect validation status
queryClient.invalidateQueries({ queryKey: salesKeys.lists() });
},
...options,
});
};

View File

@@ -0,0 +1,316 @@
/**
* Tenant React Query hooks
*/
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
import { tenantService } from '../services/tenant';
import {
BakeryRegistration,
TenantResponse,
TenantAccessResponse,
TenantUpdate,
TenantMemberResponse,
TenantStatistics,
TenantSearchParams,
TenantNearbyParams,
} from '../types/tenant';
import { ApiError } from '../client';
// Query Keys
export const tenantKeys = {
all: ['tenant'] as const,
lists: () => [...tenantKeys.all, 'list'] as const,
list: (filters: string) => [...tenantKeys.lists(), { filters }] as const,
details: () => [...tenantKeys.all, 'detail'] as const,
detail: (id: string) => [...tenantKeys.details(), id] as const,
subdomain: (subdomain: string) => [...tenantKeys.all, 'subdomain', subdomain] as const,
userTenants: (userId: string) => [...tenantKeys.all, 'user', userId] as const,
userOwnedTenants: (userId: string) => [...tenantKeys.all, 'user-owned', userId] as const,
access: (tenantId: string, userId: string) => [...tenantKeys.all, 'access', tenantId, userId] as const,
search: (params: TenantSearchParams) => [...tenantKeys.lists(), 'search', params] as const,
nearby: (params: TenantNearbyParams) => [...tenantKeys.lists(), 'nearby', params] as const,
members: (tenantId: string) => [...tenantKeys.all, 'members', tenantId] as const,
statistics: () => [...tenantKeys.all, 'statistics'] as const,
} as const;
// Queries
export const useTenant = (
tenantId: string,
options?: Omit<UseQueryOptions<TenantResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<TenantResponse, ApiError>({
queryKey: tenantKeys.detail(tenantId),
queryFn: () => tenantService.getTenant(tenantId),
enabled: !!tenantId,
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
export const useTenantBySubdomain = (
subdomain: string,
options?: Omit<UseQueryOptions<TenantResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<TenantResponse, ApiError>({
queryKey: tenantKeys.subdomain(subdomain),
queryFn: () => tenantService.getTenantBySubdomain(subdomain),
enabled: !!subdomain,
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
export const useUserTenants = (
userId: string,
options?: Omit<UseQueryOptions<TenantResponse[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<TenantResponse[], ApiError>({
queryKey: tenantKeys.userTenants(userId),
queryFn: () => tenantService.getUserTenants(userId),
enabled: !!userId,
staleTime: 2 * 60 * 1000, // 2 minutes
...options,
});
};
export const useUserOwnedTenants = (
userId: string,
options?: Omit<UseQueryOptions<TenantResponse[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<TenantResponse[], ApiError>({
queryKey: tenantKeys.userOwnedTenants(userId),
queryFn: () => tenantService.getUserOwnedTenants(userId),
enabled: !!userId,
staleTime: 2 * 60 * 1000, // 2 minutes
...options,
});
};
export const useTenantAccess = (
tenantId: string,
userId: string,
options?: Omit<UseQueryOptions<TenantAccessResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<TenantAccessResponse, ApiError>({
queryKey: tenantKeys.access(tenantId, userId),
queryFn: () => tenantService.verifyTenantAccess(tenantId, userId),
enabled: !!tenantId && !!userId,
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
export const useSearchTenants = (
params: TenantSearchParams,
options?: Omit<UseQueryOptions<TenantResponse[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<TenantResponse[], ApiError>({
queryKey: tenantKeys.search(params),
queryFn: () => tenantService.searchTenants(params),
enabled: !!params.search_term,
staleTime: 30 * 1000, // 30 seconds for search results
...options,
});
};
export const useNearbyTenants = (
params: TenantNearbyParams,
options?: Omit<UseQueryOptions<TenantResponse[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<TenantResponse[], ApiError>({
queryKey: tenantKeys.nearby(params),
queryFn: () => tenantService.getNearbyTenants(params),
enabled: !!(params.latitude && params.longitude),
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
export const useTeamMembers = (
tenantId: string,
activeOnly: boolean = true,
options?: Omit<UseQueryOptions<TenantMemberResponse[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<TenantMemberResponse[], ApiError>({
queryKey: tenantKeys.members(tenantId),
queryFn: () => tenantService.getTeamMembers(tenantId, activeOnly),
enabled: !!tenantId,
staleTime: 2 * 60 * 1000, // 2 minutes
...options,
});
};
export const useTenantStatistics = (
options?: Omit<UseQueryOptions<TenantStatistics, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<TenantStatistics, ApiError>({
queryKey: tenantKeys.statistics(),
queryFn: () => tenantService.getTenantStatistics(),
staleTime: 10 * 60 * 1000, // 10 minutes
...options,
});
};
// Mutations
export const useRegisterBakery = (
options?: UseMutationOptions<TenantResponse, ApiError, BakeryRegistration>
) => {
const queryClient = useQueryClient();
return useMutation<TenantResponse, ApiError, BakeryRegistration>({
mutationFn: (bakeryData: BakeryRegistration) => tenantService.registerBakery(bakeryData),
onSuccess: (data, variables) => {
// Invalidate user tenants to include the new one
queryClient.invalidateQueries({ queryKey: tenantKeys.userTenants('') });
queryClient.invalidateQueries({ queryKey: tenantKeys.userOwnedTenants('') });
// Set the tenant data in cache
queryClient.setQueryData(tenantKeys.detail(data.id), data);
},
...options,
});
};
export const useUpdateTenant = (
options?: UseMutationOptions<TenantResponse, ApiError, { tenantId: string; updateData: TenantUpdate }>
) => {
const queryClient = useQueryClient();
return useMutation<TenantResponse, ApiError, { tenantId: string; updateData: TenantUpdate }>({
mutationFn: ({ tenantId, updateData }) => tenantService.updateTenant(tenantId, updateData),
onSuccess: (data, { tenantId }) => {
// Update the tenant cache
queryClient.setQueryData(tenantKeys.detail(tenantId), data);
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: tenantKeys.userTenants('') });
queryClient.invalidateQueries({ queryKey: tenantKeys.userOwnedTenants('') });
},
...options,
});
};
export const useDeactivateTenant = (
options?: UseMutationOptions<{ success: boolean; message: string }, ApiError, string>
) => {
const queryClient = useQueryClient();
return useMutation<{ success: boolean; message: string }, ApiError, string>({
mutationFn: (tenantId: string) => tenantService.deactivateTenant(tenantId),
onSuccess: (data, tenantId) => {
// Invalidate tenant-related queries
queryClient.invalidateQueries({ queryKey: tenantKeys.detail(tenantId) });
queryClient.invalidateQueries({ queryKey: tenantKeys.userTenants('') });
queryClient.invalidateQueries({ queryKey: tenantKeys.userOwnedTenants('') });
},
...options,
});
};
export const useActivateTenant = (
options?: UseMutationOptions<{ success: boolean; message: string }, ApiError, string>
) => {
const queryClient = useQueryClient();
return useMutation<{ success: boolean; message: string }, ApiError, string>({
mutationFn: (tenantId: string) => tenantService.activateTenant(tenantId),
onSuccess: (data, tenantId) => {
// Invalidate tenant-related queries
queryClient.invalidateQueries({ queryKey: tenantKeys.detail(tenantId) });
queryClient.invalidateQueries({ queryKey: tenantKeys.userTenants('') });
queryClient.invalidateQueries({ queryKey: tenantKeys.userOwnedTenants('') });
},
...options,
});
};
export const useUpdateModelStatus = (
options?: UseMutationOptions<
TenantResponse,
ApiError,
{ tenantId: string; modelTrained: boolean; lastTrainingDate?: string }
>
) => {
const queryClient = useQueryClient();
return useMutation<
TenantResponse,
ApiError,
{ tenantId: string; modelTrained: boolean; lastTrainingDate?: string }
>({
mutationFn: ({ tenantId, modelTrained, lastTrainingDate }) =>
tenantService.updateModelStatus(tenantId, modelTrained, lastTrainingDate),
onSuccess: (data, { tenantId }) => {
// Update the tenant cache
queryClient.setQueryData(tenantKeys.detail(tenantId), data);
},
...options,
});
};
export const useAddTeamMember = (
options?: UseMutationOptions<
TenantMemberResponse,
ApiError,
{ tenantId: string; userId: string; role: string }
>
) => {
const queryClient = useQueryClient();
return useMutation<
TenantMemberResponse,
ApiError,
{ tenantId: string; userId: string; role: string }
>({
mutationFn: ({ tenantId, userId, role }) => tenantService.addTeamMember(tenantId, userId, role),
onSuccess: (data, { tenantId }) => {
// Invalidate team members query
queryClient.invalidateQueries({ queryKey: tenantKeys.members(tenantId) });
},
...options,
});
};
export const useUpdateMemberRole = (
options?: UseMutationOptions<
TenantMemberResponse,
ApiError,
{ tenantId: string; memberUserId: string; newRole: string }
>
) => {
const queryClient = useQueryClient();
return useMutation<
TenantMemberResponse,
ApiError,
{ tenantId: string; memberUserId: string; newRole: string }
>({
mutationFn: ({ tenantId, memberUserId, newRole }) =>
tenantService.updateMemberRole(tenantId, memberUserId, newRole),
onSuccess: (data, { tenantId }) => {
// Invalidate team members query
queryClient.invalidateQueries({ queryKey: tenantKeys.members(tenantId) });
},
...options,
});
};
export const useRemoveTeamMember = (
options?: UseMutationOptions<
{ success: boolean; message: string },
ApiError,
{ tenantId: string; memberUserId: string }
>
) => {
const queryClient = useQueryClient();
return useMutation<
{ success: boolean; message: string },
ApiError,
{ tenantId: string; memberUserId: string }
>({
mutationFn: ({ tenantId, memberUserId }) => tenantService.removeTeamMember(tenantId, memberUserId),
onSuccess: (data, { tenantId }) => {
// Invalidate team members query
queryClient.invalidateQueries({ queryKey: tenantKeys.members(tenantId) });
},
...options,
});
};

View File

@@ -0,0 +1,112 @@
/**
* User React Query hooks
*/
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
import { userService } from '../services/user';
import { UserResponse, UserUpdate } from '../types/auth';
import { AdminDeleteRequest, AdminDeleteResponse } from '../types/user';
import { ApiError } from '../client';
// Query Keys
export const userKeys = {
all: ['user'] as const,
current: () => [...userKeys.all, 'current'] as const,
detail: (id: string) => [...userKeys.all, 'detail', id] as const,
admin: {
all: () => [...userKeys.all, 'admin'] as const,
list: () => [...userKeys.admin.all(), 'list'] as const,
detail: (id: string) => [...userKeys.admin.all(), 'detail', id] as const,
},
} as const;
// Queries
export const useCurrentUser = (
options?: Omit<UseQueryOptions<UserResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<UserResponse, ApiError>({
queryKey: userKeys.current(),
queryFn: () => userService.getCurrentUser(),
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
export const useAllUsers = (
options?: Omit<UseQueryOptions<UserResponse[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<UserResponse[], ApiError>({
queryKey: userKeys.admin.list(),
queryFn: () => userService.getAllUsers(),
staleTime: 2 * 60 * 1000, // 2 minutes
...options,
});
};
export const useUserById = (
userId: string,
options?: Omit<UseQueryOptions<UserResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<UserResponse, ApiError>({
queryKey: userKeys.admin.detail(userId),
queryFn: () => userService.getUserById(userId),
enabled: !!userId,
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
// Mutations
export const useUpdateUser = (
options?: UseMutationOptions<UserResponse, ApiError, { userId: string; updateData: UserUpdate }>
) => {
const queryClient = useQueryClient();
return useMutation<UserResponse, ApiError, { userId: string; updateData: UserUpdate }>({
mutationFn: ({ userId, updateData }) => userService.updateUser(userId, updateData),
onSuccess: (data, { userId }) => {
// Update user cache
queryClient.setQueryData(userKeys.detail(userId), data);
queryClient.setQueryData(userKeys.current(), data);
queryClient.setQueryData(userKeys.admin.detail(userId), data);
// Invalidate user lists
queryClient.invalidateQueries({ queryKey: userKeys.admin.list() });
},
...options,
});
};
export const useDeleteUser = (
options?: UseMutationOptions<{ message: string }, ApiError, string>
) => {
const queryClient = useQueryClient();
return useMutation<{ message: string }, ApiError, string>({
mutationFn: (userId: string) => userService.deleteUser(userId),
onSuccess: (data, userId) => {
// Remove from cache
queryClient.removeQueries({ queryKey: userKeys.detail(userId) });
queryClient.removeQueries({ queryKey: userKeys.admin.detail(userId) });
// Invalidate user lists
queryClient.invalidateQueries({ queryKey: userKeys.admin.list() });
},
...options,
});
};
export const useAdminDeleteUser = (
options?: UseMutationOptions<AdminDeleteResponse, ApiError, AdminDeleteRequest>
) => {
const queryClient = useQueryClient();
return useMutation<AdminDeleteResponse, ApiError, AdminDeleteRequest>({
mutationFn: (deleteRequest: AdminDeleteRequest) => userService.adminDeleteUser(deleteRequest),
onSuccess: (data, request) => {
// Remove from cache
queryClient.removeQueries({ queryKey: userKeys.detail(request.user_id) });
queryClient.removeQueries({ queryKey: userKeys.admin.detail(request.user_id) });
// Invalidate user lists
queryClient.invalidateQueries({ queryKey: userKeys.admin.list() });
},
...options,
});
};

303
frontend/src/api/index.ts Normal file
View File

@@ -0,0 +1,303 @@
/**
* Main API exports for clean imports
* Export all services, types, and hooks
*/
// Client
export { apiClient } from './client';
export type { ApiError } from './client';
// Services
export { authService } from './services/auth';
export { userService } from './services/user';
export { onboardingService } from './services/onboarding';
export { tenantService } from './services/tenant';
export { subscriptionService } from './services/subscription';
export { salesService } from './services/sales';
export { dataImportService } from './services/dataImport';
export { inventoryService } from './services/inventory';
export { classificationService } from './services/classification';
export { inventoryDashboardService } from './services/inventoryDashboard';
export { foodSafetyService } from './services/foodSafety';
// Types - Auth
export type {
User,
UserRegistration,
UserLogin,
TokenResponse,
RefreshTokenRequest,
PasswordChange,
PasswordReset,
UserResponse,
UserUpdate as AuthUserUpdate,
TokenVerificationResponse,
AuthHealthResponse,
} from './types/auth';
// Types - User
export type {
UserUpdate,
AdminDeleteRequest,
AdminDeleteResponse,
} from './types/user';
// Types - Onboarding
export type {
OnboardingStepStatus,
UserProgress,
UpdateStepRequest,
} from './types/onboarding';
// Types - Tenant
export type {
BakeryRegistration,
TenantResponse,
TenantAccessResponse,
TenantUpdate,
TenantMemberResponse,
TenantStatistics,
TenantSearchParams,
TenantNearbyParams,
} from './types/tenant';
// Types - Subscription
export type {
SubscriptionLimits,
FeatureCheckResponse,
UsageCheckResponse,
} from './types/subscription';
// Types - Sales
export type {
SalesDataCreate,
SalesDataUpdate,
SalesDataResponse,
SalesDataQuery,
SalesAnalytics,
SalesValidationRequest,
} from './types/sales';
// Types - Data Import
export type {
ImportValidationRequest,
ImportValidationResponse,
ImportProcessRequest,
ImportProcessResponse,
ImportStatusResponse,
} from './types/dataImport';
// Types - Inventory
export type {
IngredientCreate,
IngredientUpdate,
IngredientResponse,
StockCreate,
StockUpdate,
StockResponse,
StockMovementCreate,
StockMovementResponse,
InventoryFilter,
StockFilter,
StockConsumptionRequest,
StockConsumptionResponse,
PaginatedResponse,
} from './types/inventory';
// Types - Classification
export type {
ProductClassificationRequest,
BatchClassificationRequest,
ProductSuggestionResponse,
BusinessModelAnalysisResponse,
ClassificationApprovalRequest,
ClassificationApprovalResponse,
} from './types/classification';
// Types - Dashboard
export type {
InventoryDashboardSummary,
InventoryAnalytics,
BusinessModelInsights,
DashboardFilter,
AlertsFilter,
RecentActivity,
StockMovementSummary,
CategorySummary,
AlertSummary,
StockStatusSummary,
} from './types/dashboard';
// Types - Food Safety
export type {
FoodSafetyComplianceCreate,
FoodSafetyComplianceUpdate,
FoodSafetyComplianceResponse,
TemperatureLogCreate,
BulkTemperatureLogCreate,
TemperatureLogResponse,
FoodSafetyAlertCreate,
FoodSafetyAlertUpdate,
FoodSafetyAlertResponse,
FoodSafetyFilter,
TemperatureMonitoringFilter,
FoodSafetyMetrics,
TemperatureAnalytics,
FoodSafetyDashboard,
} from './types/foodSafety';
// Hooks - Auth
export {
useAuthProfile,
useAuthHealth,
useVerifyToken,
useRegister,
useLogin,
useRefreshToken,
useLogout,
useChangePassword,
useResetPassword,
useUpdateProfile,
useVerifyEmail,
authKeys,
} from './hooks/auth';
// Hooks - User
export {
useCurrentUser,
useAllUsers,
useUserById,
useUpdateUser,
useDeleteUser,
useAdminDeleteUser,
userKeys,
} from './hooks/user';
// Hooks - Onboarding
export {
useUserProgress,
useAllSteps,
useStepDetails,
useUpdateStep,
useMarkStepCompleted,
useResetProgress,
onboardingKeys,
} from './hooks/onboarding';
// Hooks - Tenant
export {
useTenant,
useTenantBySubdomain,
useUserTenants,
useUserOwnedTenants,
useTenantAccess,
useSearchTenants,
useNearbyTenants,
useTeamMembers,
useTenantStatistics,
useRegisterBakery,
useUpdateTenant,
useDeactivateTenant,
useActivateTenant,
useUpdateModelStatus,
useAddTeamMember,
useUpdateMemberRole,
useRemoveTeamMember,
tenantKeys,
} from './hooks/tenant';
// Hooks - Sales
export {
useSalesRecords,
useSalesRecord,
useSalesAnalytics,
useProductSales,
useProductCategories,
useCreateSalesRecord,
useUpdateSalesRecord,
useDeleteSalesRecord,
useValidateSalesRecord,
salesKeys,
} from './hooks/sales';
// Hooks - Inventory
export {
useIngredients,
useIngredient,
useIngredientsByCategory,
useLowStockIngredients,
useStock,
useStockByIngredient,
useExpiringStock,
useExpiredStock,
useStockMovements,
useStockAnalytics,
useCreateIngredient,
useUpdateIngredient,
useDeleteIngredient,
useAddStock,
useUpdateStock,
useConsumeStock,
useCreateStockMovement,
inventoryKeys,
} from './hooks/inventory';
// Hooks - Classification
export {
usePendingSuggestions,
useSuggestionHistory,
useBusinessModelAnalysis,
useClassifyProduct,
useClassifyProductsBatch,
useApproveClassification,
useUpdateSuggestion,
useDeleteSuggestion,
classificationKeys,
} from './hooks/classification';
// Hooks - Inventory Dashboard
export {
useInventoryDashboardSummary,
useInventoryAnalytics,
useBusinessModelInsights,
useRecentActivity,
useInventoryAlerts,
useStockSummary,
useTopCategories,
useExpiryCalendar,
inventoryDashboardKeys,
} from './hooks/inventoryDashboard';
// Hooks - Food Safety
export {
useComplianceRecords,
useComplianceRecord,
useTemperatureLogs,
useTemperatureAnalytics,
useTemperatureViolations,
useFoodSafetyAlerts,
useFoodSafetyAlert,
useFoodSafetyDashboard,
useFoodSafetyMetrics,
useComplianceRate,
useCreateComplianceRecord,
useUpdateComplianceRecord,
useCreateTemperatureLog,
useCreateBulkTemperatureLogs,
useCreateFoodSafetyAlert,
useUpdateFoodSafetyAlert,
foodSafetyKeys,
} from './hooks/foodSafety';
// Query Key Factories (for advanced usage)
export {
authKeys,
userKeys,
onboardingKeys,
tenantKeys,
salesKeys,
inventoryKeys,
classificationKeys,
inventoryDashboardKeys,
foodSafetyKeys,
};

View File

@@ -0,0 +1,87 @@
/**
* Auth Service - Mirror backend auth endpoints
*/
import { apiClient } from '../client';
import {
UserRegistration,
UserLogin,
TokenResponse,
RefreshTokenRequest,
PasswordChange,
PasswordReset,
UserResponse,
UserUpdate,
TokenVerificationResponse,
AuthHealthResponse,
} from '../types/auth';
export class AuthService {
private readonly baseUrl = '/auth';
async register(userData: UserRegistration): Promise<TokenResponse> {
return apiClient.post<TokenResponse>(`${this.baseUrl}/register`, userData);
}
async login(loginData: UserLogin): Promise<TokenResponse> {
return apiClient.post<TokenResponse>(`${this.baseUrl}/login`, loginData);
}
async refreshToken(refreshToken: string): Promise<TokenResponse> {
const refreshData: RefreshTokenRequest = { refresh_token: refreshToken };
return apiClient.post<TokenResponse>(`${this.baseUrl}/refresh`, refreshData);
}
async verifyToken(token?: string): Promise<TokenVerificationResponse> {
// If token is provided, temporarily set it; otherwise use current token
const currentToken = apiClient.getAuthToken();
if (token && token !== currentToken) {
apiClient.setAuthToken(token);
}
const response = await apiClient.post<TokenVerificationResponse>(`${this.baseUrl}/verify`);
// Restore original token if we temporarily changed it
if (token && token !== currentToken) {
apiClient.setAuthToken(currentToken);
}
return response;
}
async logout(refreshToken: string): Promise<{ message: string }> {
const refreshData: RefreshTokenRequest = { refresh_token: refreshToken };
return apiClient.post<{ message: string }>(`${this.baseUrl}/logout`, refreshData);
}
async changePassword(passwordData: PasswordChange): Promise<{ message: string }> {
return apiClient.post<{ message: string }>(`${this.baseUrl}/change-password`, passwordData);
}
async resetPassword(resetData: PasswordReset): Promise<{ message: string }> {
return apiClient.post<{ message: string }>(`${this.baseUrl}/reset-password`, resetData);
}
async getProfile(): Promise<UserResponse> {
return apiClient.get<UserResponse>(`${this.baseUrl}/profile`);
}
async updateProfile(updateData: UserUpdate): Promise<UserResponse> {
return apiClient.put<UserResponse>(`${this.baseUrl}/profile`, updateData);
}
async verifyEmail(
userId: string,
verificationToken: string
): Promise<{ message: string }> {
return apiClient.post<{ message: string }>(`${this.baseUrl}/verify-email`, {
user_id: userId,
verification_token: verificationToken,
});
}
async healthCheck(): Promise<AuthHealthResponse> {
return apiClient.get<AuthHealthResponse>(`${this.baseUrl}/health`);
}
}
export const authService = new AuthService();

View File

@@ -0,0 +1,94 @@
/**
* Classification Service - Mirror backend classification endpoints
*/
import { apiClient } from '../client';
import {
ProductClassificationRequest,
BatchClassificationRequest,
ProductSuggestionResponse,
BusinessModelAnalysisResponse,
ClassificationApprovalRequest,
ClassificationApprovalResponse,
} from '../types/classification';
export class ClassificationService {
private readonly baseUrl = '/tenants';
async classifyProduct(
tenantId: string,
classificationData: ProductClassificationRequest
): Promise<ProductSuggestionResponse> {
return apiClient.post<ProductSuggestionResponse>(
`${this.baseUrl}/${tenantId}/classification/classify-product`,
classificationData
);
}
async classifyProductsBatch(
tenantId: string,
batchData: BatchClassificationRequest
): Promise<ProductSuggestionResponse[]> {
return apiClient.post<ProductSuggestionResponse[]>(
`${this.baseUrl}/${tenantId}/classification/classify-batch`,
batchData
);
}
async getBusinessModelAnalysis(tenantId: string): Promise<BusinessModelAnalysisResponse> {
return apiClient.get<BusinessModelAnalysisResponse>(
`${this.baseUrl}/${tenantId}/classification/business-model-analysis`
);
}
async approveClassification(
tenantId: string,
approvalData: ClassificationApprovalRequest
): Promise<ClassificationApprovalResponse> {
return apiClient.post<ClassificationApprovalResponse>(
`${this.baseUrl}/${tenantId}/classification/approve-suggestion`,
approvalData
);
}
async getPendingSuggestions(tenantId: string): Promise<ProductSuggestionResponse[]> {
return apiClient.get<ProductSuggestionResponse[]>(
`${this.baseUrl}/${tenantId}/classification/pending-suggestions`
);
}
async getSuggestionHistory(
tenantId: string,
limit: number = 50,
offset: number = 0
): Promise<{
items: ProductSuggestionResponse[];
total: number;
}> {
const queryParams = new URLSearchParams();
queryParams.append('limit', limit.toString());
queryParams.append('offset', offset.toString());
return apiClient.get(
`${this.baseUrl}/${tenantId}/classification/suggestion-history?${queryParams.toString()}`
);
}
async deleteSuggestion(tenantId: string, suggestionId: string): Promise<{ message: string }> {
return apiClient.delete<{ message: string }>(
`${this.baseUrl}/${tenantId}/classification/suggestions/${suggestionId}`
);
}
async updateSuggestion(
tenantId: string,
suggestionId: string,
updateData: Partial<ProductSuggestionResponse>
): Promise<ProductSuggestionResponse> {
return apiClient.put<ProductSuggestionResponse>(
`${this.baseUrl}/${tenantId}/classification/suggestions/${suggestionId}`,
updateData
);
}
}
export const classificationService = new ClassificationService();

View File

@@ -0,0 +1,99 @@
/**
* Data Import Service - Mirror backend data import endpoints
*/
import { apiClient } from '../client';
import {
ImportValidationRequest,
ImportValidationResponse,
ImportProcessRequest,
ImportProcessResponse,
ImportStatusResponse
} from '../types/dataImport';
export class DataImportService {
private readonly baseUrl = '/tenants';
async validateJsonData(
tenantId: string,
data: any
): Promise<ImportValidationResponse> {
return apiClient.post<ImportValidationResponse>(
`${this.baseUrl}/${tenantId}/sales/import/validate-json`,
data
);
}
async validateCsvFile(
tenantId: string,
file: File
): Promise<ImportValidationResponse> {
return apiClient.uploadFile<ImportValidationResponse>(
`${this.baseUrl}/${tenantId}/sales/import/validate-csv`,
file
);
}
async importJsonData(
tenantId: string,
data: any,
options?: {
skip_validation?: boolean;
chunk_size?: number;
}
): Promise<ImportProcessResponse> {
const payload = {
...data,
options,
};
return apiClient.post<ImportProcessResponse>(
`${this.baseUrl}/${tenantId}/sales/import/json`,
payload
);
}
async importCsvFile(
tenantId: string,
file: File,
options?: {
skip_validation?: boolean;
chunk_size?: number;
}
): Promise<ImportProcessResponse> {
const formData = new FormData();
formData.append('file', file);
if (options) {
formData.append('options', JSON.stringify(options));
}
return apiClient.uploadFile<ImportProcessResponse>(
`${this.baseUrl}/${tenantId}/sales/import/csv`,
formData
);
}
async getImportStatus(
tenantId: string,
importId: string
): Promise<ImportStatusResponse> {
return apiClient.get<ImportStatusResponse>(
`${this.baseUrl}/${tenantId}/sales/import/${importId}/status`
);
}
async cancelImport(
tenantId: string,
importId: string
): Promise<{ success: boolean; message: string }> {
return apiClient.post<{ success: boolean; message: string }>(
`${this.baseUrl}/${tenantId}/sales/import/${importId}/cancel`
);
}
async getImportHistory(tenantId: string): Promise<ImportStatusResponse[]> {
return apiClient.get<ImportStatusResponse[]>(
`${this.baseUrl}/${tenantId}/sales/import/history`
);
}
}
export const dataImportService = new DataImportService();

View File

@@ -0,0 +1,273 @@
/**
* Food Safety Service - Mirror backend food safety endpoints
*/
import { apiClient } from '../client';
import {
FoodSafetyComplianceCreate,
FoodSafetyComplianceUpdate,
FoodSafetyComplianceResponse,
TemperatureLogCreate,
BulkTemperatureLogCreate,
TemperatureLogResponse,
FoodSafetyAlertCreate,
FoodSafetyAlertUpdate,
FoodSafetyAlertResponse,
FoodSafetyFilter,
TemperatureMonitoringFilter,
FoodSafetyMetrics,
TemperatureAnalytics,
FoodSafetyDashboard,
} from '../types/foodSafety';
import { PaginatedResponse } from '../types/inventory';
export class FoodSafetyService {
private readonly baseUrl = '/tenants';
// Compliance Management
async createComplianceRecord(
tenantId: string,
complianceData: FoodSafetyComplianceCreate
): Promise<FoodSafetyComplianceResponse> {
return apiClient.post<FoodSafetyComplianceResponse>(
`${this.baseUrl}/${tenantId}/food-safety/compliance`,
complianceData
);
}
async getComplianceRecord(
tenantId: string,
recordId: string
): Promise<FoodSafetyComplianceResponse> {
return apiClient.get<FoodSafetyComplianceResponse>(
`${this.baseUrl}/${tenantId}/food-safety/compliance/${recordId}`
);
}
async getComplianceRecords(
tenantId: string,
filter?: FoodSafetyFilter
): Promise<PaginatedResponse<FoodSafetyComplianceResponse>> {
const queryParams = new URLSearchParams();
if (filter?.compliance_type) queryParams.append('compliance_type', filter.compliance_type);
if (filter?.status) queryParams.append('status', filter.status);
if (filter?.ingredient_id) queryParams.append('ingredient_id', filter.ingredient_id);
if (filter?.resolved !== undefined) queryParams.append('resolved', filter.resolved.toString());
if (filter?.date_range?.start) queryParams.append('start_date', filter.date_range.start);
if (filter?.date_range?.end) queryParams.append('end_date', filter.date_range.end);
if (filter?.limit !== undefined) queryParams.append('limit', filter.limit.toString());
if (filter?.offset !== undefined) queryParams.append('offset', filter.offset.toString());
if (filter?.order_by) queryParams.append('order_by', filter.order_by);
if (filter?.order_direction) queryParams.append('order_direction', filter.order_direction);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/food-safety/compliance?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/food-safety/compliance`;
return apiClient.get<PaginatedResponse<FoodSafetyComplianceResponse>>(url);
}
async updateComplianceRecord(
tenantId: string,
recordId: string,
updateData: FoodSafetyComplianceUpdate
): Promise<FoodSafetyComplianceResponse> {
return apiClient.put<FoodSafetyComplianceResponse>(
`${this.baseUrl}/${tenantId}/food-safety/compliance/${recordId}`,
updateData
);
}
async deleteComplianceRecord(
tenantId: string,
recordId: string
): Promise<{ message: string }> {
return apiClient.delete<{ message: string }>(
`${this.baseUrl}/${tenantId}/food-safety/compliance/${recordId}`
);
}
// Temperature Monitoring
async createTemperatureLog(
tenantId: string,
logData: TemperatureLogCreate
): Promise<TemperatureLogResponse> {
return apiClient.post<TemperatureLogResponse>(
`${this.baseUrl}/${tenantId}/food-safety/temperature-logs`,
logData
);
}
async createBulkTemperatureLogs(
tenantId: string,
bulkData: BulkTemperatureLogCreate
): Promise<{
created_count: number;
failed_count: number;
errors?: string[];
}> {
return apiClient.post(
`${this.baseUrl}/${tenantId}/food-safety/temperature-logs/bulk`,
bulkData
);
}
async getTemperatureLogs(
tenantId: string,
filter?: TemperatureMonitoringFilter
): Promise<PaginatedResponse<TemperatureLogResponse>> {
const queryParams = new URLSearchParams();
if (filter?.location) queryParams.append('location', filter.location);
if (filter?.equipment_id) queryParams.append('equipment_id', filter.equipment_id);
if (filter?.temperature_range?.min !== undefined)
queryParams.append('min_temperature', filter.temperature_range.min.toString());
if (filter?.temperature_range?.max !== undefined)
queryParams.append('max_temperature', filter.temperature_range.max.toString());
if (filter?.alert_triggered !== undefined)
queryParams.append('alert_triggered', filter.alert_triggered.toString());
if (filter?.date_range?.start) queryParams.append('start_date', filter.date_range.start);
if (filter?.date_range?.end) queryParams.append('end_date', filter.date_range.end);
if (filter?.limit !== undefined) queryParams.append('limit', filter.limit.toString());
if (filter?.offset !== undefined) queryParams.append('offset', filter.offset.toString());
if (filter?.order_by) queryParams.append('order_by', filter.order_by);
if (filter?.order_direction) queryParams.append('order_direction', filter.order_direction);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/food-safety/temperature-logs?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/food-safety/temperature-logs`;
return apiClient.get<PaginatedResponse<TemperatureLogResponse>>(url);
}
async getTemperatureAnalytics(
tenantId: string,
location: string,
startDate?: string,
endDate?: string
): Promise<TemperatureAnalytics> {
const queryParams = new URLSearchParams();
queryParams.append('location', location);
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
return apiClient.get<TemperatureAnalytics>(
`${this.baseUrl}/${tenantId}/food-safety/temperature-analytics?${queryParams.toString()}`
);
}
// Alert Management
async createFoodSafetyAlert(
tenantId: string,
alertData: FoodSafetyAlertCreate
): Promise<FoodSafetyAlertResponse> {
return apiClient.post<FoodSafetyAlertResponse>(
`${this.baseUrl}/${tenantId}/food-safety/alerts`,
alertData
);
}
async getFoodSafetyAlert(
tenantId: string,
alertId: string
): Promise<FoodSafetyAlertResponse> {
return apiClient.get<FoodSafetyAlertResponse>(
`${this.baseUrl}/${tenantId}/food-safety/alerts/${alertId}`
);
}
async getFoodSafetyAlerts(
tenantId: string,
status?: 'open' | 'in_progress' | 'resolved' | 'dismissed',
severity?: 'critical' | 'warning' | 'info',
limit: number = 50,
offset: number = 0
): Promise<PaginatedResponse<FoodSafetyAlertResponse>> {
const queryParams = new URLSearchParams();
if (status) queryParams.append('status', status);
if (severity) queryParams.append('severity', severity);
queryParams.append('limit', limit.toString());
queryParams.append('offset', offset.toString());
return apiClient.get<PaginatedResponse<FoodSafetyAlertResponse>>(
`${this.baseUrl}/${tenantId}/food-safety/alerts?${queryParams.toString()}`
);
}
async updateFoodSafetyAlert(
tenantId: string,
alertId: string,
updateData: FoodSafetyAlertUpdate
): Promise<FoodSafetyAlertResponse> {
return apiClient.put<FoodSafetyAlertResponse>(
`${this.baseUrl}/${tenantId}/food-safety/alerts/${alertId}`,
updateData
);
}
async deleteFoodSafetyAlert(
tenantId: string,
alertId: string
): Promise<{ message: string }> {
return apiClient.delete<{ message: string }>(
`${this.baseUrl}/${tenantId}/food-safety/alerts/${alertId}`
);
}
// Dashboard and Metrics
async getFoodSafetyDashboard(tenantId: string): Promise<FoodSafetyDashboard> {
return apiClient.get<FoodSafetyDashboard>(
`${this.baseUrl}/${tenantId}/food-safety/dashboard`
);
}
async getFoodSafetyMetrics(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<FoodSafetyMetrics> {
const queryParams = new URLSearchParams();
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/food-safety/metrics?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/food-safety/metrics`;
return apiClient.get<FoodSafetyMetrics>(url);
}
async getTemperatureViolations(
tenantId: string,
limit: number = 20
): Promise<TemperatureLogResponse[]> {
const queryParams = new URLSearchParams();
queryParams.append('limit', limit.toString());
return apiClient.get<TemperatureLogResponse[]>(
`${this.baseUrl}/${tenantId}/food-safety/temperature-violations?${queryParams.toString()}`
);
}
async getComplianceRate(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<{
overall_rate: number;
by_type: Record<string, number>;
trend: Array<{ date: string; rate: number }>;
}> {
const queryParams = new URLSearchParams();
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/food-safety/compliance-rate?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/food-safety/compliance-rate`;
return apiClient.get(url);
}
}
export const foodSafetyService = new FoodSafetyService();

View File

@@ -0,0 +1,228 @@
/**
* Inventory Service - Mirror backend inventory endpoints
*/
import { apiClient } from '../client';
import {
IngredientCreate,
IngredientUpdate,
IngredientResponse,
StockCreate,
StockUpdate,
StockResponse,
StockMovementCreate,
StockMovementResponse,
InventoryFilter,
StockFilter,
StockConsumptionRequest,
StockConsumptionResponse,
PaginatedResponse,
} from '../types/inventory';
export class InventoryService {
private readonly baseUrl = '/tenants';
// Ingredient Management
async createIngredient(
tenantId: string,
ingredientData: IngredientCreate
): Promise<IngredientResponse> {
return apiClient.post<IngredientResponse>(`${this.baseUrl}/${tenantId}/ingredients`, ingredientData);
}
async getIngredient(tenantId: string, ingredientId: string): Promise<IngredientResponse> {
return apiClient.get<IngredientResponse>(`${this.baseUrl}/${tenantId}/ingredients/${ingredientId}`);
}
async getIngredients(
tenantId: string,
filter?: InventoryFilter
): Promise<PaginatedResponse<IngredientResponse>> {
const queryParams = new URLSearchParams();
if (filter?.category) queryParams.append('category', filter.category);
if (filter?.stock_status) queryParams.append('stock_status', filter.stock_status);
if (filter?.requires_refrigeration !== undefined)
queryParams.append('requires_refrigeration', filter.requires_refrigeration.toString());
if (filter?.requires_freezing !== undefined)
queryParams.append('requires_freezing', filter.requires_freezing.toString());
if (filter?.is_seasonal !== undefined)
queryParams.append('is_seasonal', filter.is_seasonal.toString());
if (filter?.supplier_id) queryParams.append('supplier_id', filter.supplier_id);
if (filter?.expiring_within_days !== undefined)
queryParams.append('expiring_within_days', filter.expiring_within_days.toString());
if (filter?.search) queryParams.append('search', filter.search);
if (filter?.limit !== undefined) queryParams.append('limit', filter.limit.toString());
if (filter?.offset !== undefined) queryParams.append('offset', filter.offset.toString());
if (filter?.order_by) queryParams.append('order_by', filter.order_by);
if (filter?.order_direction) queryParams.append('order_direction', filter.order_direction);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/ingredients?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/ingredients`;
return apiClient.get<PaginatedResponse<IngredientResponse>>(url);
}
async updateIngredient(
tenantId: string,
ingredientId: string,
updateData: IngredientUpdate
): Promise<IngredientResponse> {
return apiClient.put<IngredientResponse>(
`${this.baseUrl}/${tenantId}/ingredients/${ingredientId}`,
updateData
);
}
async deleteIngredient(tenantId: string, ingredientId: string): Promise<{ message: string }> {
return apiClient.delete<{ message: string }>(`${this.baseUrl}/${tenantId}/ingredients/${ingredientId}`);
}
async getIngredientsByCategory(tenantId: string): Promise<Record<string, IngredientResponse[]>> {
return apiClient.get<Record<string, IngredientResponse[]>>(`${this.baseUrl}/${tenantId}/ingredients/by-category`);
}
async getLowStockIngredients(tenantId: string): Promise<IngredientResponse[]> {
return apiClient.get<IngredientResponse[]>(`${this.baseUrl}/${tenantId}/ingredients/low-stock`);
}
// Stock Management
async addStock(tenantId: string, stockData: StockCreate): Promise<StockResponse> {
return apiClient.post<StockResponse>(`${this.baseUrl}/${tenantId}/stock`, stockData);
}
async getStock(tenantId: string, stockId: string): Promise<StockResponse> {
return apiClient.get<StockResponse>(`${this.baseUrl}/${tenantId}/stock/${stockId}`);
}
async getStockByIngredient(
tenantId: string,
ingredientId: string,
includeUnavailable: boolean = false
): Promise<StockResponse[]> {
const queryParams = new URLSearchParams();
queryParams.append('include_unavailable', includeUnavailable.toString());
return apiClient.get<StockResponse[]>(
`${this.baseUrl}/${tenantId}/stock/ingredient/${ingredientId}?${queryParams.toString()}`
);
}
async getAllStock(tenantId: string, filter?: StockFilter): Promise<PaginatedResponse<StockResponse>> {
const queryParams = new URLSearchParams();
if (filter?.ingredient_id) queryParams.append('ingredient_id', filter.ingredient_id);
if (filter?.is_available !== undefined) queryParams.append('is_available', filter.is_available.toString());
if (filter?.is_expired !== undefined) queryParams.append('is_expired', filter.is_expired.toString());
if (filter?.expiring_within_days !== undefined)
queryParams.append('expiring_within_days', filter.expiring_within_days.toString());
if (filter?.batch_number) queryParams.append('batch_number', filter.batch_number);
if (filter?.supplier_id) queryParams.append('supplier_id', filter.supplier_id);
if (filter?.limit !== undefined) queryParams.append('limit', filter.limit.toString());
if (filter?.offset !== undefined) queryParams.append('offset', filter.offset.toString());
if (filter?.order_by) queryParams.append('order_by', filter.order_by);
if (filter?.order_direction) queryParams.append('order_direction', filter.order_direction);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/stock?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/stock`;
return apiClient.get<PaginatedResponse<StockResponse>>(url);
}
async updateStock(
tenantId: string,
stockId: string,
updateData: StockUpdate
): Promise<StockResponse> {
return apiClient.put<StockResponse>(`${this.baseUrl}/${tenantId}/stock/${stockId}`, updateData);
}
async deleteStock(tenantId: string, stockId: string): Promise<{ message: string }> {
return apiClient.delete<{ message: string }>(`${this.baseUrl}/${tenantId}/stock/${stockId}`);
}
async consumeStock(
tenantId: string,
consumptionData: StockConsumptionRequest
): Promise<StockConsumptionResponse> {
const queryParams = new URLSearchParams();
queryParams.append('ingredient_id', consumptionData.ingredient_id);
queryParams.append('quantity', consumptionData.quantity.toString());
if (consumptionData.reference_number)
queryParams.append('reference_number', consumptionData.reference_number);
if (consumptionData.notes) queryParams.append('notes', consumptionData.notes);
if (consumptionData.fifo !== undefined) queryParams.append('fifo', consumptionData.fifo.toString());
return apiClient.post<StockConsumptionResponse>(
`${this.baseUrl}/${tenantId}/stock/consume?${queryParams.toString()}`
);
}
// Stock Movements
async createStockMovement(
tenantId: string,
movementData: StockMovementCreate
): Promise<StockMovementResponse> {
return apiClient.post<StockMovementResponse>(`${this.baseUrl}/${tenantId}/stock/movements`, movementData);
}
async getStockMovements(
tenantId: string,
ingredientId?: string,
limit: number = 50,
offset: number = 0
): Promise<PaginatedResponse<StockMovementResponse>> {
const queryParams = new URLSearchParams();
if (ingredientId) queryParams.append('ingredient_id', ingredientId);
queryParams.append('limit', limit.toString());
queryParams.append('offset', offset.toString());
return apiClient.get<PaginatedResponse<StockMovementResponse>>(
`${this.baseUrl}/${tenantId}/stock/movements?${queryParams.toString()}`
);
}
// Expiry Management
async getExpiringStock(
tenantId: string,
withinDays: number = 7
): Promise<StockResponse[]> {
const queryParams = new URLSearchParams();
queryParams.append('within_days', withinDays.toString());
return apiClient.get<StockResponse[]>(
`${this.baseUrl}/${tenantId}/stock/expiring?${queryParams.toString()}`
);
}
async getExpiredStock(tenantId: string): Promise<StockResponse[]> {
return apiClient.get<StockResponse[]>(`${this.baseUrl}/${tenantId}/stock/expired`);
}
// Analytics
async getStockAnalytics(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<{
total_ingredients: number;
total_stock_value: number;
low_stock_count: number;
out_of_stock_count: number;
expiring_soon_count: number;
stock_turnover_rate: number;
}> {
const queryParams = new URLSearchParams();
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/inventory/analytics?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/inventory/analytics`;
return apiClient.get(url);
}
}
export const inventoryService = new InventoryService();

View File

@@ -0,0 +1,138 @@
/**
* Inventory Dashboard Service - Mirror backend dashboard endpoints
*/
import { apiClient } from '../client';
import {
InventoryDashboardSummary,
InventoryAnalytics,
BusinessModelInsights,
DashboardFilter,
AlertsFilter,
RecentActivity,
} from '../types/dashboard';
export class InventoryDashboardService {
private readonly baseUrl = '/tenants';
async getDashboardSummary(
tenantId: string,
filter?: DashboardFilter
): Promise<InventoryDashboardSummary> {
const queryParams = new URLSearchParams();
if (filter?.date_range?.start) queryParams.append('start_date', filter.date_range.start);
if (filter?.date_range?.end) queryParams.append('end_date', filter.date_range.end);
if (filter?.categories?.length) queryParams.append('categories', filter.categories.join(','));
if (filter?.include_expired !== undefined)
queryParams.append('include_expired', filter.include_expired.toString());
if (filter?.include_unavailable !== undefined)
queryParams.append('include_unavailable', filter.include_unavailable.toString());
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/dashboard/summary?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/dashboard/summary`;
return apiClient.get<InventoryDashboardSummary>(url);
}
async getInventoryAnalytics(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<InventoryAnalytics> {
const queryParams = new URLSearchParams();
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/dashboard/analytics?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/dashboard/analytics`;
return apiClient.get<InventoryAnalytics>(url);
}
async getBusinessModelInsights(tenantId: string): Promise<BusinessModelInsights> {
return apiClient.get<BusinessModelInsights>(
`${this.baseUrl}/${tenantId}/dashboard/business-insights`
);
}
async getRecentActivity(
tenantId: string,
limit: number = 20
): Promise<RecentActivity[]> {
const queryParams = new URLSearchParams();
queryParams.append('limit', limit.toString());
return apiClient.get<RecentActivity[]>(
`${this.baseUrl}/${tenantId}/dashboard/recent-activity?${queryParams.toString()}`
);
}
async getAlerts(
tenantId: string,
filter?: AlertsFilter
): Promise<{
items: any[];
total: number;
}> {
const queryParams = new URLSearchParams();
if (filter?.severity) queryParams.append('severity', filter.severity);
if (filter?.type) queryParams.append('type', filter.type);
if (filter?.resolved !== undefined) queryParams.append('resolved', filter.resolved.toString());
if (filter?.limit !== undefined) queryParams.append('limit', filter.limit.toString());
if (filter?.offset !== undefined) queryParams.append('offset', filter.offset.toString());
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/dashboard/alerts?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/dashboard/alerts`;
return apiClient.get(url);
}
async getStockSummary(tenantId: string): Promise<{
in_stock: number;
low_stock: number;
out_of_stock: number;
overstock: number;
total_value: number;
}> {
return apiClient.get(`${this.baseUrl}/${tenantId}/dashboard/stock-summary`);
}
async getTopCategories(tenantId: string, limit: number = 10): Promise<Array<{
category: string;
ingredient_count: number;
total_value: number;
low_stock_count: number;
}>> {
const queryParams = new URLSearchParams();
queryParams.append('limit', limit.toString());
return apiClient.get(
`${this.baseUrl}/${tenantId}/dashboard/top-categories?${queryParams.toString()}`
);
}
async getExpiryCalendar(
tenantId: string,
daysAhead: number = 30
): Promise<Array<{
date: string;
items: Array<{
ingredient_name: string;
quantity: number;
batch_number?: string;
}>;
}>> {
const queryParams = new URLSearchParams();
queryParams.append('days_ahead', daysAhead.toString());
return apiClient.get(
`${this.baseUrl}/${tenantId}/dashboard/expiry-calendar?${queryParams.toString()}`
);
}
}
export const inventoryDashboardService = new InventoryDashboardService();

View File

@@ -0,0 +1,52 @@
/**
* Onboarding Service - Mirror backend onboarding endpoints
*/
import { apiClient } from '../client';
import { UserProgress, UpdateStepRequest } from '../types/onboarding';
export class OnboardingService {
private readonly baseUrl = '/onboarding';
async getUserProgress(userId: string): Promise<UserProgress> {
return apiClient.get<UserProgress>(`${this.baseUrl}/progress/${userId}`);
}
async updateStep(userId: string, stepData: UpdateStepRequest): Promise<UserProgress> {
return apiClient.put<UserProgress>(`${this.baseUrl}/progress/${userId}/step`, stepData);
}
async markStepCompleted(
userId: string,
stepName: string,
data?: Record<string, any>
): Promise<UserProgress> {
return apiClient.post<UserProgress>(`${this.baseUrl}/progress/${userId}/complete`, {
step_name: stepName,
data: data,
});
}
async resetProgress(userId: string): Promise<UserProgress> {
return apiClient.post<UserProgress>(`${this.baseUrl}/progress/${userId}/reset`);
}
async getStepDetails(stepName: string): Promise<{
name: string;
description: string;
dependencies: string[];
estimated_time_minutes: number;
}> {
return apiClient.get(`${this.baseUrl}/steps/${stepName}`);
}
async getAllSteps(): Promise<Array<{
name: string;
description: string;
dependencies: string[];
estimated_time_minutes: number;
}>> {
return apiClient.get(`${this.baseUrl}/steps`);
}
}
export const onboardingService = new OnboardingService();

View File

@@ -0,0 +1,126 @@
/**
* Sales Service - Mirror backend sales endpoints
*/
import { apiClient } from '../client';
import {
SalesDataCreate,
SalesDataUpdate,
SalesDataResponse,
SalesDataQuery,
SalesAnalytics,
} from '../types/sales';
export class SalesService {
private readonly baseUrl = '/tenants';
// Sales Data CRUD Operations
async createSalesRecord(
tenantId: string,
salesData: SalesDataCreate
): Promise<SalesDataResponse> {
return apiClient.post<SalesDataResponse>(`${this.baseUrl}/${tenantId}/sales`, salesData);
}
async getSalesRecords(
tenantId: string,
query?: SalesDataQuery
): Promise<SalesDataResponse[]> {
const queryParams = new URLSearchParams();
if (query?.start_date) queryParams.append('start_date', query.start_date);
if (query?.end_date) queryParams.append('end_date', query.end_date);
if (query?.product_name) queryParams.append('product_name', query.product_name);
if (query?.product_category) queryParams.append('product_category', query.product_category);
if (query?.location_id) queryParams.append('location_id', query.location_id);
if (query?.sales_channel) queryParams.append('sales_channel', query.sales_channel);
if (query?.source) queryParams.append('source', query.source);
if (query?.is_validated !== undefined) queryParams.append('is_validated', query.is_validated.toString());
if (query?.limit !== undefined) queryParams.append('limit', query.limit.toString());
if (query?.offset !== undefined) queryParams.append('offset', query.offset.toString());
if (query?.order_by) queryParams.append('order_by', query.order_by);
if (query?.order_direction) queryParams.append('order_direction', query.order_direction);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/sales?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/sales`;
return apiClient.get<SalesDataResponse[]>(url);
}
async getSalesRecord(
tenantId: string,
recordId: string
): Promise<SalesDataResponse> {
return apiClient.get<SalesDataResponse>(`${this.baseUrl}/${tenantId}/sales/${recordId}`);
}
async updateSalesRecord(
tenantId: string,
recordId: string,
updateData: SalesDataUpdate
): Promise<SalesDataResponse> {
return apiClient.put<SalesDataResponse>(`${this.baseUrl}/${tenantId}/sales/${recordId}`, updateData);
}
async deleteSalesRecord(
tenantId: string,
recordId: string
): Promise<{ message: string }> {
return apiClient.delete<{ message: string }>(`${this.baseUrl}/${tenantId}/sales/${recordId}`);
}
async validateSalesRecord(
tenantId: string,
recordId: string,
validationNotes?: string
): Promise<SalesDataResponse> {
const queryParams = new URLSearchParams();
if (validationNotes) queryParams.append('validation_notes', validationNotes);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/sales/${recordId}/validate?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/sales/${recordId}/validate`;
return apiClient.post<SalesDataResponse>(url);
}
// Analytics & Reporting
async getSalesAnalytics(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<SalesAnalytics> {
const queryParams = new URLSearchParams();
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/sales/analytics/summary?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/sales/analytics/summary`;
return apiClient.get<SalesAnalytics>(url);
}
async getProductSales(
tenantId: string,
inventoryProductId: string,
startDate?: string,
endDate?: string
): Promise<SalesDataResponse[]> {
const queryParams = new URLSearchParams();
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/inventory-products/${inventoryProductId}/sales?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/inventory-products/${inventoryProductId}/sales`;
return apiClient.get<SalesDataResponse[]>(url);
}
async getProductCategories(tenantId: string): Promise<string[]> {
return apiClient.get<string[]>(`${this.baseUrl}/${tenantId}/sales/categories`);
}
}
export const salesService = new SalesService();

View File

@@ -0,0 +1,67 @@
/**
* Subscription Service - Mirror backend subscription endpoints
*/
import { apiClient } from '../client';
import {
SubscriptionLimits,
FeatureCheckRequest,
FeatureCheckResponse,
UsageCheckRequest,
UsageCheckResponse
} from '../types/subscription';
export class SubscriptionService {
private readonly baseUrl = '/subscriptions';
async getSubscriptionLimits(tenantId: string): Promise<SubscriptionLimits> {
return apiClient.get<SubscriptionLimits>(`${this.baseUrl}/${tenantId}/limits`);
}
async checkFeatureAccess(
tenantId: string,
featureName: string
): Promise<FeatureCheckResponse> {
return apiClient.get<FeatureCheckResponse>(
`${this.baseUrl}/${tenantId}/features/${featureName}/check`
);
}
async checkUsageLimit(
tenantId: string,
resourceType: 'users' | 'sales_records' | 'inventory_items' | 'api_requests',
requestedAmount?: number
): Promise<UsageCheckResponse> {
const queryParams = new URLSearchParams();
if (requestedAmount !== undefined) {
queryParams.append('requested_amount', requestedAmount.toString());
}
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/usage/${resourceType}/check?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/usage/${resourceType}/check`;
return apiClient.get<UsageCheckResponse>(url);
}
async recordUsage(
tenantId: string,
resourceType: 'users' | 'sales_records' | 'inventory_items' | 'api_requests',
amount: number = 1
): Promise<{ success: boolean; message: string }> {
return apiClient.post<{ success: boolean; message: string }>(
`${this.baseUrl}/${tenantId}/usage/${resourceType}/record`,
{ amount }
);
}
async getCurrentUsage(tenantId: string): Promise<{
users: number;
sales_records: number;
inventory_items: number;
api_requests_this_hour: number;
}> {
return apiClient.get(`${this.baseUrl}/${tenantId}/usage/current`);
}
}
export const subscriptionService = new SubscriptionService();

View File

@@ -0,0 +1,145 @@
/**
* Tenant Service - Mirror backend tenant endpoints
*/
import { apiClient } from '../client';
import {
BakeryRegistration,
TenantResponse,
TenantAccessResponse,
TenantUpdate,
TenantMemberResponse,
TenantStatistics,
TenantSearchParams,
TenantNearbyParams,
} from '../types/tenant';
export class TenantService {
private readonly baseUrl = '/tenants';
// Tenant CRUD Operations
async registerBakery(bakeryData: BakeryRegistration): Promise<TenantResponse> {
return apiClient.post<TenantResponse>(`${this.baseUrl}/register`, bakeryData);
}
async getTenant(tenantId: string): Promise<TenantResponse> {
return apiClient.get<TenantResponse>(`${this.baseUrl}/${tenantId}`);
}
async getTenantBySubdomain(subdomain: string): Promise<TenantResponse> {
return apiClient.get<TenantResponse>(`${this.baseUrl}/subdomain/${subdomain}`);
}
async getUserTenants(userId: string): Promise<TenantResponse[]> {
return apiClient.get<TenantResponse[]>(`${this.baseUrl}/users/${userId}`);
}
async getUserOwnedTenants(userId: string): Promise<TenantResponse[]> {
return apiClient.get<TenantResponse[]>(`${this.baseUrl}/user/${userId}/owned`);
}
async updateTenant(tenantId: string, updateData: TenantUpdate): Promise<TenantResponse> {
return apiClient.put<TenantResponse>(`${this.baseUrl}/${tenantId}`, updateData);
}
async deactivateTenant(tenantId: string): Promise<{ success: boolean; message: string }> {
return apiClient.post<{ success: boolean; message: string }>(`${this.baseUrl}/${tenantId}/deactivate`);
}
async activateTenant(tenantId: string): Promise<{ success: boolean; message: string }> {
return apiClient.post<{ success: boolean; message: string }>(`${this.baseUrl}/${tenantId}/activate`);
}
// Access Control
async verifyTenantAccess(tenantId: string, userId: string): Promise<TenantAccessResponse> {
return apiClient.get<TenantAccessResponse>(`${this.baseUrl}/${tenantId}/access/${userId}`);
}
// Search & Discovery
async searchTenants(params: TenantSearchParams): Promise<TenantResponse[]> {
const queryParams = new URLSearchParams();
if (params.search_term) queryParams.append('search_term', params.search_term);
if (params.business_type) queryParams.append('business_type', params.business_type);
if (params.city) queryParams.append('city', params.city);
if (params.skip !== undefined) queryParams.append('skip', params.skip.toString());
if (params.limit !== undefined) queryParams.append('limit', params.limit.toString());
return apiClient.get<TenantResponse[]>(`${this.baseUrl}/search?${queryParams.toString()}`);
}
async getNearbyTenants(params: TenantNearbyParams): Promise<TenantResponse[]> {
const queryParams = new URLSearchParams();
queryParams.append('latitude', params.latitude.toString());
queryParams.append('longitude', params.longitude.toString());
if (params.radius_km !== undefined) queryParams.append('radius_km', params.radius_km.toString());
if (params.limit !== undefined) queryParams.append('limit', params.limit.toString());
return apiClient.get<TenantResponse[]>(`${this.baseUrl}/nearby?${queryParams.toString()}`);
}
// Model Management
async updateModelStatus(
tenantId: string,
modelTrained: boolean,
lastTrainingDate?: string
): Promise<TenantResponse> {
const queryParams = new URLSearchParams();
queryParams.append('model_trained', modelTrained.toString());
if (lastTrainingDate) queryParams.append('last_training_date', lastTrainingDate);
return apiClient.put<TenantResponse>(`${this.baseUrl}/${tenantId}/model-status?${queryParams.toString()}`);
}
// Team Management
async addTeamMember(
tenantId: string,
userId: string,
role: string
): Promise<TenantMemberResponse> {
return apiClient.post<TenantMemberResponse>(`${this.baseUrl}/${tenantId}/members`, {
user_id: userId,
role: role,
});
}
async getTeamMembers(tenantId: string, activeOnly: boolean = true): Promise<TenantMemberResponse[]> {
const queryParams = new URLSearchParams();
queryParams.append('active_only', activeOnly.toString());
return apiClient.get<TenantMemberResponse[]>(`${this.baseUrl}/${tenantId}/members?${queryParams.toString()}`);
}
async updateMemberRole(
tenantId: string,
memberUserId: string,
newRole: string
): Promise<TenantMemberResponse> {
return apiClient.put<TenantMemberResponse>(
`${this.baseUrl}/${tenantId}/members/${memberUserId}/role`,
{ new_role: newRole }
);
}
async removeTeamMember(tenantId: string, memberUserId: string): Promise<{ success: boolean; message: string }> {
return apiClient.delete<{ success: boolean; message: string }>(`${this.baseUrl}/${tenantId}/members/${memberUserId}`);
}
// Admin Operations
async getTenantStatistics(): Promise<TenantStatistics> {
return apiClient.get<TenantStatistics>(`${this.baseUrl}/statistics`);
}
// Context Management (Frontend-only operations)
setCurrentTenant(tenant: TenantResponse): void {
// Set tenant context in API client
apiClient.setTenantId(tenant.id);
}
clearCurrentTenant(): void {
// Clear tenant context from API client
apiClient.setTenantId(null);
}
}
export const tenantService = new TenantService();

View File

@@ -0,0 +1,37 @@
/**
* User Service - Mirror backend user endpoints
*/
import { apiClient } from '../client';
import { UserResponse, UserUpdate } from '../types/auth';
import { AdminDeleteRequest, AdminDeleteResponse } from '../types/user';
export class UserService {
private readonly baseUrl = '/users';
async getCurrentUser(): Promise<UserResponse> {
return apiClient.get<UserResponse>(`${this.baseUrl}/me`);
}
async updateUser(userId: string, updateData: UserUpdate): Promise<UserResponse> {
return apiClient.put<UserResponse>(`${this.baseUrl}/${userId}`, updateData);
}
async deleteUser(userId: string): Promise<{ message: string }> {
return apiClient.delete<{ message: string }>(`${this.baseUrl}/${userId}`);
}
// Admin operations
async adminDeleteUser(deleteRequest: AdminDeleteRequest): Promise<AdminDeleteResponse> {
return apiClient.post<AdminDeleteResponse>(`${this.baseUrl}/admin/delete`, deleteRequest);
}
async getAllUsers(): Promise<UserResponse[]> {
return apiClient.get<UserResponse[]>(`${this.baseUrl}/admin/all`);
}
async getUserById(userId: string): Promise<UserResponse> {
return apiClient.get<UserResponse>(`${this.baseUrl}/admin/${userId}`);
}
}
export const userService = new UserService();

View File

@@ -0,0 +1,92 @@
/**
* Auth API Types - Mirror backend schemas
*/
export interface User {
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 UserRegistration {
email: string;
password: string;
full_name: string;
tenant_name?: string;
phone?: string;
language?: string;
timezone?: string;
}
export interface UserLogin {
email: string;
password: string;
}
export interface TokenResponse {
access_token: string;
refresh_token?: string;
token_type: string;
expires_in?: number;
user?: User;
}
export interface RefreshTokenRequest {
refresh_token: string;
}
export interface PasswordChange {
current_password: string;
new_password: string;
}
export interface PasswordReset {
email: 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;
}
export interface TokenVerificationResponse {
valid: boolean;
user_id?: string;
email?: string;
role?: string;
exp?: number;
message?: string;
}
export interface AuthHealthResponse {
status: string;
service: string;
version: string;
features: string[];
}

View File

@@ -0,0 +1,65 @@
/**
* Product Classification API Types - Mirror backend schemas
*/
export interface ProductClassificationRequest {
product_name: string;
sales_volume?: number;
sales_data?: Record<string, any>;
}
export interface BatchClassificationRequest {
products: ProductClassificationRequest[];
}
export interface ProductSuggestionResponse {
suggestion_id: string;
original_name: string;
suggested_name: string;
product_type: string;
category: string;
unit_of_measure: string;
confidence_score: number;
estimated_shelf_life_days?: number;
requires_refrigeration: boolean;
requires_freezing: boolean;
is_seasonal: boolean;
suggested_supplier?: string;
notes?: string;
}
export interface BusinessModelAnalysisResponse {
tenant_id: string;
analysis_date: string;
business_type: string;
primary_products: string[];
seasonality_patterns: Record<string, any>;
supplier_recommendations: Array<{
category: string;
suppliers: string[];
estimated_cost_savings: number;
}>;
inventory_optimization_suggestions: Array<{
product_name: string;
current_stock_level: number;
suggested_stock_level: number;
reason: string;
}>;
confidence_score: number;
}
export interface ClassificationApprovalRequest {
suggestion_id: string;
approved: boolean;
modifications?: Partial<ProductSuggestionResponse>;
}
export interface ClassificationApprovalResponse {
suggestion_id: string;
approved: boolean;
created_ingredient?: {
id: string;
name: string;
};
message: string;
}

View File

@@ -0,0 +1,111 @@
/**
* Dashboard API Types - Mirror backend schemas
*/
export interface InventoryDashboardSummary {
tenant_id: string;
total_ingredients: number;
total_stock_value: number;
low_stock_count: number;
out_of_stock_count: number;
overstock_count: number;
expiring_soon_count: number;
expired_count: number;
recent_movements: StockMovementSummary[];
top_categories: CategorySummary[];
alerts_summary: AlertSummary;
last_updated: string;
}
export interface StockMovementSummary {
id: string;
ingredient_name: string;
movement_type: 'in' | 'out' | 'adjustment' | 'transfer' | 'waste';
quantity: number;
created_at: string;
}
export interface CategorySummary {
category: string;
ingredient_count: number;
total_value: number;
low_stock_count: number;
}
export interface AlertSummary {
total_alerts: number;
critical_alerts: number;
warning_alerts: number;
info_alerts: number;
}
export interface StockStatusSummary {
in_stock: number;
low_stock: number;
out_of_stock: number;
overstock: number;
}
export interface InventoryAnalytics {
tenant_id: string;
period_start: string;
period_end: string;
stock_turnover_rate: number;
average_days_to_consume: number;
waste_percentage: number;
cost_of_goods_sold: number;
inventory_value_trend: Array<{
date: string;
value: number;
}>;
top_consuming_ingredients: Array<{
ingredient_name: string;
quantity_consumed: number;
value_consumed: number;
}>;
seasonal_patterns: Record<string, any>;
}
export interface BusinessModelInsights {
tenant_id: string;
business_type: string;
primary_categories: string[];
seasonality_score: number;
optimization_opportunities: Array<{
type: 'stock_level' | 'supplier' | 'storage' | 'ordering';
description: string;
potential_savings: number;
priority: 'high' | 'medium' | 'low';
}>;
benchmarking: {
inventory_turnover_vs_industry: number;
waste_percentage_vs_industry: number;
storage_efficiency_score: number;
};
}
export interface RecentActivity {
id: string;
type: 'stock_in' | 'stock_out' | 'ingredient_created' | 'alert_created';
description: string;
timestamp: string;
user_name?: string;
}
export interface DashboardFilter {
date_range?: {
start: string;
end: string;
};
categories?: string[];
include_expired?: boolean;
include_unavailable?: boolean;
}
export interface AlertsFilter {
severity?: 'critical' | 'warning' | 'info';
type?: 'expiry' | 'low_stock' | 'out_of_stock' | 'food_safety';
resolved?: boolean;
limit?: number;
offset?: number;
}

View File

@@ -0,0 +1,47 @@
/**
* Data Import API Types - Mirror backend schemas
*/
export interface ImportValidationRequest {
tenant_id: string;
data?: string;
data_format?: 'json' | 'csv';
}
export interface ImportValidationResponse {
valid: boolean;
errors: string[];
warnings: string[];
record_count?: number;
sample_records?: any[];
}
export interface ImportProcessRequest {
tenant_id: string;
data?: string;
data_format?: 'json' | 'csv';
options?: {
skip_validation?: boolean;
chunk_size?: number;
};
}
export interface ImportProcessResponse {
success: boolean;
message: string;
records_processed: number;
records_failed: number;
import_id?: string;
errors?: string[];
}
export interface ImportStatusResponse {
import_id: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
progress_percentage: number;
records_processed: number;
records_failed: number;
started_at: string;
completed_at?: string;
errors?: string[];
}

View File

@@ -0,0 +1,226 @@
/**
* Food Safety API Types - Mirror backend schemas
*/
export interface FoodSafetyComplianceCreate {
ingredient_id: string;
compliance_type: 'temperature_check' | 'quality_inspection' | 'hygiene_audit' | 'batch_verification';
status: 'pass' | 'fail' | 'warning';
temperature?: number;
humidity?: number;
ph_level?: number;
visual_inspection_notes?: string;
corrective_actions?: string;
inspector_name?: string;
certification_reference?: string;
notes?: string;
}
export interface FoodSafetyComplianceUpdate {
compliance_type?: 'temperature_check' | 'quality_inspection' | 'hygiene_audit' | 'batch_verification';
status?: 'pass' | 'fail' | 'warning';
temperature?: number;
humidity?: number;
ph_level?: number;
visual_inspection_notes?: string;
corrective_actions?: string;
inspector_name?: string;
certification_reference?: string;
notes?: string;
resolved?: boolean;
}
export interface FoodSafetyComplianceResponse {
id: string;
tenant_id: string;
ingredient_id: string;
ingredient_name: string;
compliance_type: 'temperature_check' | 'quality_inspection' | 'hygiene_audit' | 'batch_verification';
status: 'pass' | 'fail' | 'warning';
temperature?: number;
humidity?: number;
ph_level?: number;
visual_inspection_notes?: string;
corrective_actions?: string;
inspector_name?: string;
certification_reference?: string;
notes?: string;
resolved: boolean;
created_at: string;
updated_at: string;
created_by?: string;
}
export interface TemperatureLogCreate {
location: string;
temperature: number;
humidity?: number;
equipment_id?: string;
notes?: string;
}
export interface BulkTemperatureLogCreate {
logs: TemperatureLogCreate[];
}
export interface TemperatureLogResponse {
id: string;
tenant_id: string;
location: string;
temperature: number;
humidity?: number;
equipment_id?: string;
notes?: string;
is_within_range: boolean;
alert_triggered: boolean;
created_at: string;
created_by?: string;
}
export interface FoodSafetyAlertCreate {
alert_type: 'temperature_violation' | 'expiry_warning' | 'quality_issue' | 'compliance_failure';
severity: 'critical' | 'warning' | 'info';
title: string;
description: string;
ingredient_id?: string;
temperature_log_id?: string;
compliance_record_id?: string;
requires_action: boolean;
assigned_to?: string;
}
export interface FoodSafetyAlertUpdate {
status?: 'open' | 'in_progress' | 'resolved' | 'dismissed';
assigned_to?: string;
resolution_notes?: string;
corrective_actions?: string;
}
export interface FoodSafetyAlertResponse {
id: string;
tenant_id: string;
alert_type: 'temperature_violation' | 'expiry_warning' | 'quality_issue' | 'compliance_failure';
severity: 'critical' | 'warning' | 'info';
title: string;
description: string;
status: 'open' | 'in_progress' | 'resolved' | 'dismissed';
ingredient_id?: string;
ingredient_name?: string;
temperature_log_id?: string;
compliance_record_id?: string;
requires_action: boolean;
assigned_to?: string;
assigned_to_name?: string;
resolution_notes?: string;
corrective_actions?: string;
created_at: string;
updated_at: string;
resolved_at?: string;
created_by?: string;
}
export interface FoodSafetyFilter {
compliance_type?: 'temperature_check' | 'quality_inspection' | 'hygiene_audit' | 'batch_verification';
status?: 'pass' | 'fail' | 'warning';
ingredient_id?: string;
resolved?: boolean;
date_range?: {
start: string;
end: string;
};
limit?: number;
offset?: number;
order_by?: string;
order_direction?: 'asc' | 'desc';
}
export interface TemperatureMonitoringFilter {
location?: string;
equipment_id?: string;
temperature_range?: {
min: number;
max: number;
};
alert_triggered?: boolean;
date_range?: {
start: string;
end: string;
};
limit?: number;
offset?: number;
order_by?: string;
order_direction?: 'asc' | 'desc';
}
export interface FoodSafetyMetrics {
tenant_id: string;
period_start: string;
period_end: string;
total_compliance_checks: number;
passed_checks: number;
failed_checks: number;
warning_checks: number;
compliance_rate: number;
total_temperature_logs: number;
temperature_violations: number;
critical_alerts: number;
resolved_alerts: number;
average_resolution_time_hours: number;
top_risk_ingredients: Array<{
ingredient_name: string;
risk_score: number;
incident_count: number;
}>;
}
export interface TemperatureAnalytics {
tenant_id: string;
location: string;
period_start: string;
period_end: string;
average_temperature: number;
min_temperature: number;
max_temperature: number;
temperature_trend: Array<{
timestamp: string;
temperature: number;
humidity?: number;
}>;
violations_count: number;
uptime_percentage: number;
}
export interface FoodSafetyDashboard {
tenant_id: string;
compliance_summary: {
total_checks: number;
passed: number;
failed: number;
warnings: number;
compliance_rate: number;
};
temperature_monitoring: {
total_logs: number;
violations: number;
locations_monitored: number;
latest_readings: TemperatureLogResponse[];
};
active_alerts: {
critical: number;
warning: number;
info: number;
overdue: number;
};
recent_activities: Array<{
type: string;
description: string;
timestamp: string;
severity?: string;
}>;
upcoming_expirations: Array<{
ingredient_name: string;
expiration_date: string;
days_until_expiry: number;
quantity: number;
}>;
}

View File

@@ -0,0 +1,192 @@
/**
* Inventory API Types - Mirror backend schemas
*/
// Base Inventory Types
export interface IngredientCreate {
name: string;
description?: string;
category: string;
unit_of_measure: string;
minimum_stock_level: number;
maximum_stock_level: number;
reorder_point: number;
shelf_life_days?: number;
requires_refrigeration?: boolean;
requires_freezing?: boolean;
is_seasonal?: boolean;
supplier_id?: string;
cost_per_unit?: number;
notes?: string;
}
export interface IngredientUpdate {
name?: string;
description?: string;
category?: string;
unit_of_measure?: string;
minimum_stock_level?: number;
maximum_stock_level?: number;
reorder_point?: number;
shelf_life_days?: number;
requires_refrigeration?: boolean;
requires_freezing?: boolean;
is_seasonal?: boolean;
supplier_id?: string;
cost_per_unit?: number;
notes?: string;
}
export interface IngredientResponse {
id: string;
tenant_id: string;
name: string;
description?: string;
category: string;
unit_of_measure: string;
minimum_stock_level: number;
maximum_stock_level: number;
reorder_point: number;
shelf_life_days?: number;
requires_refrigeration: boolean;
requires_freezing: boolean;
is_seasonal: boolean;
supplier_id?: string;
cost_per_unit?: number;
notes?: string;
current_stock_level: number;
available_stock: number;
reserved_stock: number;
stock_status: 'in_stock' | 'low_stock' | 'out_of_stock' | 'overstock';
last_restocked?: string;
created_at: string;
updated_at: string;
created_by?: string;
}
// Stock Management Types
export interface StockCreate {
ingredient_id: string;
quantity: number;
unit_price: number;
expiration_date?: string;
batch_number?: string;
supplier_id?: string;
purchase_order_reference?: string;
notes?: string;
}
export interface StockUpdate {
quantity?: number;
unit_price?: number;
expiration_date?: string;
batch_number?: string;
notes?: string;
is_available?: boolean;
}
export interface StockResponse {
id: string;
ingredient_id: string;
tenant_id: string;
quantity: number;
available_quantity: number;
reserved_quantity: number;
unit_price: number;
total_value: number;
expiration_date?: string;
batch_number?: string;
supplier_id?: string;
purchase_order_reference?: string;
notes?: string;
is_available: boolean;
is_expired: boolean;
days_until_expiry?: number;
created_at: string;
updated_at: string;
created_by?: string;
}
export interface StockMovementCreate {
ingredient_id: string;
movement_type: 'in' | 'out' | 'adjustment' | 'transfer' | 'waste';
quantity: number;
unit_price?: number;
reference_number?: string;
notes?: string;
related_stock_id?: string;
}
export interface StockMovementResponse {
id: string;
ingredient_id: string;
tenant_id: string;
movement_type: 'in' | 'out' | 'adjustment' | 'transfer' | 'waste';
quantity: number;
unit_price?: number;
total_value?: number;
reference_number?: string;
notes?: string;
related_stock_id?: string;
created_at: string;
created_by?: string;
}
// Filter and Query Types
export interface InventoryFilter {
category?: string;
stock_status?: 'in_stock' | 'low_stock' | 'out_of_stock' | 'overstock';
requires_refrigeration?: boolean;
requires_freezing?: boolean;
is_seasonal?: boolean;
supplier_id?: string;
expiring_within_days?: number;
search?: string;
limit?: number;
offset?: number;
order_by?: string;
order_direction?: 'asc' | 'desc';
}
export interface StockFilter {
ingredient_id?: string;
is_available?: boolean;
is_expired?: boolean;
expiring_within_days?: number;
batch_number?: string;
supplier_id?: string;
limit?: number;
offset?: number;
order_by?: string;
order_direction?: 'asc' | 'desc';
}
// Stock Consumption Types
export interface StockConsumptionRequest {
ingredient_id: string;
quantity: number;
reference_number?: string;
notes?: string;
fifo?: boolean;
}
export interface StockConsumptionResponse {
ingredient_id: string;
total_quantity_consumed: number;
consumed_items: Array<{
stock_id: string;
quantity_consumed: number;
batch_number?: string;
expiration_date?: string;
}>;
method: 'FIFO' | 'LIFO';
}
// Pagination Response
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
per_page: number;
total_pages: number;
}

View File

@@ -0,0 +1,26 @@
/**
* Onboarding API Types - Mirror backend schemas
*/
export interface OnboardingStepStatus {
step_name: string;
completed: boolean;
completed_at?: string;
data?: Record<string, any>;
}
export interface UserProgress {
user_id: string;
steps: OnboardingStepStatus[];
current_step: string;
next_step?: string;
completion_percentage: number;
fully_completed: boolean;
last_updated: string;
}
export interface UpdateStepRequest {
step_name: string;
completed: boolean;
data?: Record<string, any>;
}

View File

@@ -0,0 +1,137 @@
/**
* Sales API Types - Mirror backend schemas
*/
export interface SalesDataCreate {
date: string;
product_name: string;
product_category?: string;
quantity_sold: number;
unit_price: number;
total_revenue: number;
location_id?: string;
sales_channel?: string;
discount_applied?: number;
promotion_used?: string;
customer_id?: string;
inventory_product_id?: string;
cost_of_goods_sold?: number;
profit_margin?: number;
weather_condition?: string;
temperature?: number;
precipitation?: number;
is_holiday?: boolean;
day_of_week?: string;
hour_of_day?: number;
season?: string;
local_event?: string;
source?: string;
}
export interface SalesDataUpdate {
date?: string;
product_name?: string;
product_category?: string;
quantity_sold?: number;
unit_price?: number;
total_revenue?: number;
location_id?: string;
sales_channel?: string;
discount_applied?: number;
promotion_used?: string;
customer_id?: string;
inventory_product_id?: string;
cost_of_goods_sold?: number;
profit_margin?: number;
weather_condition?: string;
temperature?: number;
precipitation?: number;
is_holiday?: boolean;
day_of_week?: string;
hour_of_day?: number;
season?: string;
local_event?: string;
validation_notes?: string;
}
export interface SalesDataResponse {
id: string;
tenant_id: string;
date: string;
product_name: string;
product_category?: string;
quantity_sold: number;
unit_price: number;
total_revenue: number;
location_id?: string;
sales_channel?: string;
discount_applied?: number;
promotion_used?: string;
customer_id?: string;
inventory_product_id?: string;
cost_of_goods_sold?: number;
profit_margin?: number;
weather_condition?: string;
temperature?: number;
precipitation?: number;
is_holiday?: boolean;
day_of_week?: string;
hour_of_day?: number;
season?: string;
local_event?: string;
source?: string;
is_validated?: boolean;
validation_notes?: string;
created_at: string;
updated_at: string;
created_by?: string;
}
export interface SalesDataQuery {
start_date?: string;
end_date?: string;
product_name?: string;
product_category?: string;
location_id?: string;
sales_channel?: string;
source?: string;
is_validated?: boolean;
limit?: number;
offset?: number;
order_by?: string;
order_direction?: 'asc' | 'desc';
}
export interface SalesAnalytics {
total_revenue: number;
total_quantity: number;
average_unit_price: number;
total_transactions: number;
top_products: Array<{
product_name: string;
total_revenue: number;
total_quantity: number;
transaction_count: number;
}>;
revenue_by_date: Array<{
date: string;
revenue: number;
quantity: number;
}>;
revenue_by_category: Array<{
category: string;
revenue: number;
quantity: number;
}>;
revenue_by_channel: Array<{
channel: string;
revenue: number;
quantity: number;
}>;
}
export interface SalesValidationRequest {
record_id: string;
tenant_id: string;
validation_notes?: string;
}

View File

@@ -0,0 +1,43 @@
/**
* Subscription API Types - Mirror backend schemas
*/
export interface SubscriptionLimits {
max_users: number;
max_sales_records: number;
max_inventory_items: number;
max_api_requests_per_hour: number;
features_enabled: string[];
current_usage: {
users: number;
sales_records: number;
inventory_items: number;
api_requests_this_hour: number;
};
}
export interface FeatureCheckRequest {
feature_name: string;
tenant_id: string;
}
export interface FeatureCheckResponse {
enabled: boolean;
limit?: number;
current_usage?: number;
message?: string;
}
export interface UsageCheckRequest {
resource_type: 'users' | 'sales_records' | 'inventory_items' | 'api_requests';
tenant_id: string;
requested_amount?: number;
}
export interface UsageCheckResponse {
allowed: boolean;
limit: number;
current_usage: number;
remaining: number;
message?: string;
}

View File

@@ -0,0 +1,109 @@
/**
* Tenant API Types - Mirror backend schemas
*/
export interface BakeryRegistration {
name: string;
business_type?: string;
description?: string;
address?: string;
city?: string;
state?: string;
country?: string;
postal_code?: string;
phone?: string;
email?: string;
website?: string;
subdomain?: string;
latitude?: number;
longitude?: number;
}
export interface TenantResponse {
id: string;
name: string;
business_type?: string;
description?: string;
address?: string;
city?: string;
state?: string;
country?: string;
postal_code?: string;
phone?: string;
email?: string;
website?: string;
subdomain?: string;
latitude?: number;
longitude?: number;
is_active: boolean;
created_at: string;
updated_at: string;
owner_id: string;
model_trained?: boolean;
last_training_date?: string;
}
export interface TenantAccessResponse {
has_access: boolean;
role?: string;
permissions?: string[];
}
export interface TenantUpdate {
name?: string;
business_type?: string;
description?: string;
address?: string;
city?: string;
state?: string;
country?: string;
postal_code?: string;
phone?: string;
email?: string;
website?: string;
latitude?: number;
longitude?: number;
}
export interface TenantMemberResponse {
id: string;
tenant_id: string;
user_id: string;
role: string;
is_active: boolean;
joined_at: string;
user_email?: string;
user_full_name?: string;
}
export interface TenantSearchRequest {
search_term: string;
business_type?: string;
city?: string;
skip?: number;
limit?: number;
}
export interface TenantStatistics {
total_tenants: number;
active_tenants: number;
inactive_tenants: number;
tenants_by_business_type: Record<string, number>;
tenants_by_city: Record<string, number>;
recent_registrations: TenantResponse[];
}
export interface TenantSearchParams {
search_term?: string;
business_type?: string;
city?: string;
skip?: number;
limit?: number;
}
export interface TenantNearbyParams {
latitude: number;
longitude: number;
radius_km?: number;
limit?: number;
}

View File

@@ -0,0 +1,24 @@
/**
* User API Types - Mirror backend schemas
*/
export interface UserUpdate {
full_name?: string;
phone?: string;
language?: string;
timezone?: string;
is_active?: boolean;
}
export interface AdminDeleteRequest {
user_id: string;
reason?: string;
hard_delete?: boolean;
}
export interface AdminDeleteResponse {
success: boolean;
message: string;
deleted_user_id: string;
deletion_type: 'soft' | 'hard';
}

View File

@@ -7,7 +7,7 @@ import { Card } from '../../ui';
import { Badge } from '../../ui';
import { Modal } from '../../ui';
import { IngredientFormData, UnitOfMeasure, ProductType, IngredientResponse } from '../../../types/inventory.types';
import { inventoryService } from '../../../services/api/inventory.service';
import { inventoryService } from '../../../api/services/inventory.service';
export interface InventoryFormProps {
item?: IngredientResponse;

View File

@@ -8,7 +8,7 @@ import { Select } from '../../ui';
import { Modal } from '../../ui';
import { ConfirmDialog } from '../../shared';
import { InventoryFilters, IngredientResponse, UnitOfMeasure, SortOrder } from '../../../types/inventory.types';
import { inventoryService } from '../../../services/api/inventory.service';
import { inventoryService } from '../../../api/services/inventory.service';
import { StockLevelIndicator } from './StockLevelIndicator';
export interface InventoryTableProps {

View File

@@ -8,7 +8,7 @@ import { Input } from '../../ui';
import { EmptyState } from '../../shared';
import { LoadingSpinner } from '../../shared';
import { StockAlert, IngredientResponse, AlertSeverity } from '../../../types/inventory.types';
import { inventoryService } from '../../../services/api/inventory.service';
import { inventoryService } from '../../../api/services/inventory.service';
import { StockLevelIndicator } from './StockLevelIndicator';
export interface LowStockAlertProps {

View File

@@ -1,8 +1,5 @@
import React, { useState, useCallback } from 'react';
import { Card, Button, Input, Select, Badge } from '../../ui';
import { tenantService } from '../../../services/api/tenant.service';
import { useAuthUser } from '../../../stores/auth.store';
import { useTenantActions } from '../../../stores/tenant.store';
import { Card, Button } from '../../ui';
export interface OnboardingStep {
@@ -26,22 +23,31 @@ export interface OnboardingStepProps {
interface OnboardingWizardProps {
steps: OnboardingStep[];
currentStep: number;
data: any;
onStepChange: (stepIndex: number, stepData: any) => void;
onNext: () => void;
onPrevious: () => void;
onComplete: (data: any) => void;
onGoToStep: (stepIndex: number) => void;
onExit?: () => void;
className?: string;
}
export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
steps,
currentStep: currentStepIndex,
data: stepData,
onStepChange,
onNext,
onPrevious,
onComplete,
onGoToStep,
onExit,
className = '',
}) => {
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [stepData, setStepData] = useState<Record<string, any>>({});
const [completedSteps, setCompletedSteps] = useState<Set<string>>(new Set());
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
const { setCurrentTenant } = useTenantActions();
const currentStep = steps[currentStepIndex];
@@ -64,7 +70,7 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
const validateCurrentStep = useCallback(() => {
const step = currentStep;
const data = stepData[step.id] || {};
const data = stepData || {};
if (step.validation) {
const error = step.validation(data);
@@ -82,100 +88,19 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
return true;
}, [currentStep, stepData]);
const goToNextStep = useCallback(async () => {
// Special handling for setup step - create tenant if needed
if (currentStep.id === 'setup') {
const data = stepData[currentStep.id] || {};
if (!data.bakery?.tenant_id) {
// Create tenant inline using real backend API
updateStepData(currentStep.id, {
...data,
bakery: { ...data.bakery, isCreating: true }
});
try {
// Use the backend-compatible data directly from BakerySetupStep
const bakeryRegistration = {
name: data.bakery.name,
address: data.bakery.address,
city: data.bakery.city,
postal_code: data.bakery.postal_code,
phone: data.bakery.phone,
business_type: data.bakery.business_type,
business_model: data.bakery.business_model
};
const response = await tenantService.createTenant(bakeryRegistration);
if (response.success && response.data) {
const tenantData = response.data;
updateStepData(currentStep.id, {
...data,
bakery: {
...data.bakery,
tenant_id: tenantData.id,
created_at: tenantData.created_at,
isCreating: false
}
});
// Update the tenant store with the new tenant
setCurrentTenant(tenantData);
} else {
throw new Error(response.error || 'Failed to create tenant');
}
} catch (error) {
console.error('Error creating tenant:', error);
updateStepData(currentStep.id, {
...data,
bakery: {
...data.bakery,
isCreating: false,
creationError: error instanceof Error ? error.message : 'Unknown error'
}
});
return;
}
}
}
// Special handling for suppliers step - trigger ML training
if (currentStep.id === 'suppliers') {
const nextStepIndex = currentStepIndex + 1;
const nextStep = steps[nextStepIndex];
if (nextStep && nextStep.id === 'ml-training') {
// Set autoStartTraining flag for the ML training step
updateStepData(nextStep.id, {
...stepData[nextStep.id],
autoStartTraining: true
});
}
}
const goToNextStep = useCallback(() => {
if (validateCurrentStep()) {
if (currentStepIndex < steps.length - 1) {
setCurrentStepIndex(currentStepIndex + 1);
} else {
// All steps completed, call onComplete with all data
onComplete(stepData);
}
onNext();
}
}, [currentStep.id, currentStepIndex, steps, validateCurrentStep, onComplete, stepData, updateStepData]);
}, [validateCurrentStep, onNext]);
const goToPreviousStep = useCallback(() => {
if (currentStepIndex > 0) {
setCurrentStepIndex(currentStepIndex - 1);
}
}, [currentStepIndex]);
onPrevious();
}, [onPrevious]);
const goToStep = useCallback((stepIndex: number) => {
setCurrentStepIndex(stepIndex);
}, []);
onGoToStep(stepIndex);
}, [onGoToStep]);
const calculateProgress = () => {
return (completedSteps.size / steps.length) * 100;
@@ -412,11 +337,7 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
{/* Step content */}
<div className="px-8 py-8">
<StepComponent
data={{
...stepData[currentStep.id],
// Pass all step data to allow access to previous steps
allStepData: stepData
}}
data={stepData}
onDataChange={(data) => {
updateStepData(currentStep.id, data);
}}
@@ -443,21 +364,11 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
variant="primary"
onClick={goToNextStep}
disabled={
(currentStep.validation && currentStep.validation(stepData[currentStep.id] || {})) ||
(currentStep.id === 'setup' && stepData[currentStep.id]?.bakery?.isCreating)
(currentStep.validation && currentStep.validation(stepData || {}))
}
className="px-8"
>
{currentStep.id === 'setup' && stepData[currentStep.id]?.bakery?.isCreating ? (
<>
<div className="animate-spin w-4 h-4 border-2 border-white border-t-transparent rounded-full mr-2" />
Creando espacio...
</>
) : currentStep.id === 'suppliers' ? (
'Siguiente →'
) : (
currentStepIndex === steps.length - 1 ? '🎉 Finalizar' : 'Siguiente →'
)}
{currentStepIndex === steps.length - 1 ? '🎉 Finalizar' : 'Siguiente →'}
</Button>
</div>
</div>

View File

@@ -2,11 +2,10 @@ import React, { useState, useEffect } from 'react';
import { CheckCircle, Star, Rocket, Gift, Download, Share2, ArrowRight, Calendar } from 'lucide-react';
import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { useInventory } from '../../../../hooks/api/useInventory';
import { useIngredients } from '../../../../api';
import { useModal } from '../../../../hooks/ui/useModal';
import { useToast } from '../../../../hooks/ui/useToast';
import { useAuthUser } from '../../../../stores/auth.store';
import { useAlertActions } from '../../../../stores/alerts.store';
interface CompletionStats {
totalProducts: number;
@@ -28,9 +27,12 @@ export const CompletionStep: React.FC<OnboardingStepProps> = ({
isLastStep
}) => {
const user = useAuthUser();
const { createAlert } = useAlertActions();
const createAlert = (alert: any) => {
console.log('Alert:', alert);
};
const { showToast } = useToast();
const { createInventoryFromSuggestions, isLoading: inventoryLoading } = useInventory();
// TODO: Replace with proper inventory creation logic when needed
const inventoryLoading = false;
const certificateModal = useModal();
const demoModal = useModal();
const shareModal = useModal();
@@ -57,9 +59,8 @@ export const CompletionStep: React.FC<OnboardingStepProps> = ({
try {
// Sales data should already be imported during DataProcessingStep
// Just create inventory items from approved suggestions
const result = await createInventoryFromSuggestions(
data.approvedSuggestions || []
);
// TODO: Implement inventory creation from suggestions
const result = { success: true, successful_imports: 0 };
createAlert({
type: 'success',
@@ -347,7 +348,7 @@ export const CompletionStep: React.FC<OnboardingStepProps> = ({
<p className="text-xs text-[var(--text-secondary)]">{badge.description}</p>
{badge.earned && (
<div className="mt-2">
<Badge variant="green" className="text-xs">Conseguido</Badge>
<Badge variant="success" className="text-xs">Conseguido</Badge>
</div>
)}
</div>

View File

@@ -2,15 +2,11 @@ import React, { useState, useRef, useEffect } from 'react';
import { Upload, Brain, CheckCircle, AlertCircle, Download, FileText, Activity, TrendingUp } from 'lucide-react';
import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { useInventory } from '../../../../hooks/api/useInventory';
import { useSales } from '../../../../hooks/api/useSales';
import { useModal } from '../../../../hooks/ui/useModal';
import { useToast } from '../../../../hooks/ui/useToast';
import { salesService } from '../../../../services/api/sales.service';
import { inventoryService } from '../../../../services/api/inventory.service';
import { salesService } from '../../../../api';
import { useAuthUser, useAuthLoading } from '../../../../stores/auth.store';
import { useCurrentTenant, useTenantLoading } from '../../../../stores/tenant.store';
import { useAlertActions } from '../../../../stores/alerts.store';
type ProcessingStage = 'upload' | 'validating' | 'analyzing' | 'completed' | 'error';
@@ -134,7 +130,9 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
const authLoading = useAuthLoading();
const currentTenant = useCurrentTenant();
const tenantLoading = useTenantLoading();
const { createAlert } = useAlertActions();
const createAlert = (alert: any) => {
console.log('Alert:', alert);
};
// Use hooks for UI and direct service calls for now (until we extend hooks)
const { isLoading: inventoryLoading } = useInventory();

View File

@@ -2,13 +2,11 @@ import React, { useState, useEffect } from 'react';
import { Package, Calendar, AlertTriangle, Plus, Edit, Trash2, CheckCircle } from 'lucide-react';
import { Button, Card, Input, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { useInventory } from '../../../../hooks/api/useInventory';
import { useSales } from '../../../../hooks/api/useSales';
import { useCreateIngredient, useCreateSalesRecord } from '../../../../api';
import { useModal } from '../../../../hooks/ui/useModal';
import { useToast } from '../../../../hooks/ui/useToast';
import { useAuthUser } from '../../../../stores/auth.store';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAlertActions } from '../../../../stores/alerts.store';
interface InventoryItem {
id: string;
@@ -62,22 +60,14 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
}) => {
const user = useAuthUser();
const currentTenant = useCurrentTenant();
const { createAlert } = useAlertActions();
const createAlert = (alert: any) => {
console.log('Alert:', alert);
};
const { showToast } = useToast();
// Use inventory hook for API operations
const {
createIngredient,
isLoading: inventoryLoading,
error: inventoryError,
clearError: clearInventoryError
} = useInventory();
// Use sales hook for importing sales data
const {
importSalesData,
isLoading: salesLoading
} = useSales();
// Use proper API hooks that are already available
const createIngredientMutation = useCreateIngredient();
const createSalesRecordMutation = useCreateSalesRecord();
// Use modal for confirmations and editing
const editModal = useModal();
@@ -174,11 +164,18 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
shelf_life_days: product.estimated_shelf_life_days || 30,
requires_refrigeration: product.requires_refrigeration || false,
requires_freezing: product.requires_freezing || false,
is_seasonal: product.is_seasonal || false
is_seasonal: product.is_seasonal || false,
minimum_stock_level: 0,
maximum_stock_level: 1000,
reorder_point: 10
};
try {
const success = await createIngredient(ingredientData);
const response = await createIngredientMutation.mutateAsync({
tenantId: currentTenant!.id,
ingredientData
});
const success = !!response;
if (success) {
successCount++;
// Mock created item data since hook doesn't return it
@@ -236,7 +233,9 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
source: 'onboarding'
});
const importSuccess = await importSalesData(salesDataFile, 'csv');
// TODO: Implement bulk sales record creation from file
// For now, simulate success
const importSuccess = true;
if (importSuccess) {
salesImportResult = {

View File

@@ -4,14 +4,25 @@ import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { useAuthUser } from '../../../../stores/auth.store';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAlertActions } from '../../../../stores/alerts.store';
import {
WebSocketService,
TrainingProgressMessage,
TrainingCompletedMessage,
TrainingErrorMessage
} from '../../../../services/realtime/websocket.service';
import { trainingService } from '../../../../services/api/training.service';
// TODO: Implement WebSocket training progress updates when realtime API is available
// Type definitions for training messages (will be moved to API types later)
interface TrainingProgressMessage {
type: 'training_progress';
progress: number;
stage: string;
message: string;
}
interface TrainingCompletedMessage {
type: 'training_completed';
metrics: TrainingMetrics;
}
interface TrainingErrorMessage {
type: 'training_error';
error: string;
}
interface TrainingMetrics {
accuracy: number;
@@ -48,7 +59,9 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
}) => {
const user = useAuthUser();
const currentTenant = useCurrentTenant();
const { createAlert } = useAlertActions();
const createAlert = (alert: any) => {
console.log('Alert:', alert);
};
const [trainingStatus, setTrainingStatus] = useState<'idle' | 'validating' | 'training' | 'completed' | 'failed'>(
data.trainingStatus || 'idle'

View File

@@ -2,7 +2,6 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { Eye, CheckCircle, AlertCircle, Edit, Trash2 } from 'lucide-react';
import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { useAlertActions } from '../../../../stores/alerts.store';
interface Product {
id: string;
@@ -72,7 +71,9 @@ export const ReviewStep: React.FC<OnboardingStepProps> = ({
isFirstStep,
isLastStep
}) => {
const { createAlert } = useAlertActions();
const createAlert = (alert: any) => {
console.log('Alert:', alert);
};
// Generate products from AI suggestions in processing results
const generateProductsFromResults = (results: any) => {

View File

@@ -6,8 +6,7 @@ import { useModal } from '../../../../hooks/ui/useModal';
import { useToast } from '../../../../hooks/ui/useToast';
import { useAuthUser } from '../../../../stores/auth.store';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAlertActions } from '../../../../stores/alerts.store';
import { procurementService } from '../../../../services/api/procurement.service';
// TODO: Import procurement service from new API when available
// Frontend supplier interface that matches the form needs
interface SupplierFormData {
@@ -47,7 +46,9 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
}) => {
const user = useAuthUser();
const currentTenant = useCurrentTenant();
const { createAlert } = useAlertActions();
const createAlert = (alert: any) => {
console.log('Alert:', alert);
};
const { showToast } = useToast();
// Use modals for confirmations and editing

View File

@@ -1,6 +1,6 @@
import React, { useState, useCallback, useEffect } from 'react';
import { Card, Button, Badge, Input, Modal, Table, Select, DatePicker } from '../../ui';
import { productionService, type ProductionBatchResponse, ProductionBatchStatus, ProductionPriority } from '../../../services/api/production.service';
import { productionService, type ProductionBatchResponse, ProductionBatchStatus, ProductionPriority } from '../../../api/services/production.service';
import type { ProductionBatch, QualityCheck } from '../../../types/production.types';
interface BatchTrackerProps {

View File

@@ -1,6 +1,6 @@
import React, { useState, useCallback, useMemo } from 'react';
import { Card, Button, Badge, Select, DatePicker, Input, Modal, Table } from '../../ui';
import { productionService, type ProductionScheduleEntry, ProductionBatchStatus } from '../../../services/api/production.service';
import { productionService, type ProductionScheduleEntry, ProductionBatchStatus } from '../../../api/services/production.service';
import type { ProductionSchedule as ProductionScheduleType, ProductionBatch, EquipmentReservation, StaffAssignment } from '../../../types/production.types';
interface ProductionScheduleProps {

View File

@@ -1,6 +1,6 @@
import React, { useState, useCallback, useRef } from 'react';
import { Card, Button, Badge, Input, Modal, Table, Select, DatePicker } from '../../ui';
import { productionService, type QualityCheckResponse, QualityCheckStatus } from '../../../services/api/production.service';
import { productionService, type QualityCheckResponse, QualityCheckStatus } from '../../../api/services/production.service';
import type { QualityCheck, QualityCheckCriteria, ProductionBatch, QualityCheckType } from '../../../types/production.types';
interface QualityControlProps {

View File

@@ -14,7 +14,7 @@ import {
SalesChannel,
PaymentMethod
} from '../../../types/sales.types';
import { salesService } from '../../../services/api/sales.service';
import { salesService } from '../../../api/services/sales.service';
import { useSales } from '../../../hooks/api/useSales';
// Customer interfaces

View File

@@ -12,7 +12,7 @@ import {
SalesChannel,
PaymentMethod
} from '../../../types/sales.types';
import { salesService } from '../../../services/api/sales.service';
import { salesService } from '../../../api/services/sales.service';
// Order form interfaces
interface Product {

View File

@@ -16,7 +16,7 @@ import {
SalesSortField,
SortOrder
} from '../../../types/sales.types';
import { salesService } from '../../../services/api/sales.service';
import { salesService } from '../../../api/services/sales.service';
import { useSales } from '../../../hooks/api/useSales';
// Extended interface for orders

View File

@@ -12,7 +12,7 @@ import {
ProductPerformance,
PeriodType
} from '../../../types/sales.types';
import { salesService } from '../../../services/api/sales.service';
import { salesService } from '../../../api/services/sales.service';
import { useSales } from '../../../hooks/api/useSales';
interface SalesChartProps {

View File

@@ -2,7 +2,7 @@ import React, { useState, useCallback, forwardRef } from 'react';
import { clsx } from 'clsx';
import { useAuthUser, useIsAuthenticated } from '../../../stores';
import { useTheme } from '../../../contexts/ThemeContext';
import { useTenantInitializer } from '../../../hooks/useTenantInitializer';
import { useTenantInitializer } from '../../../stores/useTenantInitializer';
import { Header } from '../Header';
import { Sidebar } from '../Sidebar';
import { Footer } from '../Footer';

View File

@@ -1,112 +0,0 @@
/**
* Mock configuration for testing the complete onboarding flow
* Set MOCK_MODE to false for production
*/
export const MOCK_CONFIG = {
// Global mock mode toggle
MOCK_MODE: true,
// Component-specific toggles
MOCK_REGISTRATION: false, // Now using real backend
MOCK_AUTHENTICATION: false, // Now using real backend
MOCK_ONBOARDING_FLOW: true, // Keep onboarding mock for now
// Mock user data
MOCK_USER: {
full_name: 'María García López',
email: 'maria.garcia@elbuenpan.com',
role: 'admin',
tenant_name: 'Panadería Artesanal El Buen Pan'
},
// Mock admin user data for testing
MOCK_ADMIN_USER: {
full_name: 'Admin User',
email: 'admin@bakery.com',
role: 'admin',
tenant_name: 'Bakery Admin Demo'
},
// Mock bakery data
MOCK_BAKERY: {
name: 'Panadería Artesanal El Buen Pan',
type: 'artisan',
location: 'Av. Principal 123, Centro Histórico',
phone: '+1 234 567 8900',
email: 'info@elbuenpan.com'
},
// Mock subscription data for admin@bakery.com
MOCK_SUBSCRIPTION: {
id: 'sub_admin_demo_001',
tenant_id: 'tenant_admin_demo',
plan: 'professional',
status: 'active',
monthly_price: 129.0,
currency: 'EUR',
billing_cycle: 'monthly',
current_period_start: '2024-08-01T00:00:00Z',
current_period_end: '2024-09-01T00:00:00Z',
next_billing_date: '2024-09-01T00:00:00Z',
trial_ends_at: null,
canceled_at: null,
created_at: '2024-01-15T10:30:00Z',
updated_at: '2024-08-01T00:00:00Z',
max_users: 15,
max_locations: 2,
max_products: -1,
features: {
inventory_management: 'advanced',
demand_prediction: 'ai_92_percent',
production_management: 'complete',
pos_integrated: true,
logistics: 'basic',
analytics: 'advanced',
support: 'priority_24_7',
trial_days: 14,
locations: '1_2_locations'
},
usage: {
users: 8,
locations: 1,
products: 145,
storage_gb: 2.4,
api_calls_month: 1250,
reports_generated: 23
},
billing_history: [
{
id: 'inv_001',
date: '2024-08-01T00:00:00Z',
amount: 129.0,
status: 'paid' as 'paid',
description: 'Plan Professional - Agosto 2024'
},
{
id: 'inv_002',
date: '2024-07-01T00:00:00Z',
amount: 129.0,
status: 'paid' as 'paid',
description: 'Plan Professional - Julio 2024'
},
{
id: 'inv_003',
date: '2024-06-01T00:00:00Z',
amount: 129.0,
status: 'paid' as 'paid',
description: 'Plan Professional - Junio 2024'
}
]
}
};
// Helper function to check if mock mode is enabled
export const isMockMode = () => MOCK_CONFIG.MOCK_MODE;
export const isMockRegistration = () => MOCK_CONFIG.MOCK_MODE && MOCK_CONFIG.MOCK_REGISTRATION;
export const isMockAuthentication = () => MOCK_CONFIG.MOCK_MODE && MOCK_CONFIG.MOCK_AUTHENTICATION;
export const isMockOnboardingFlow = () => MOCK_CONFIG.MOCK_MODE && MOCK_CONFIG.MOCK_ONBOARDING_FLOW;
// Helper functions to get mock data
export const getMockUser = (isAdmin = false) => isAdmin ? MOCK_CONFIG.MOCK_ADMIN_USER : MOCK_CONFIG.MOCK_USER;
export const getMockSubscription = () => MOCK_CONFIG.MOCK_SUBSCRIPTION;

View File

@@ -52,7 +52,7 @@ export const SSEProvider: React.FC<SSEProviderProps> = ({ children }) => {
}
try {
const eventSource = new EventSource(`/api/events?token=${token}`, {
const eventSource = new EventSource(`http://localhost:8000/api/events?token=${token}`, {
withCredentials: true,
});

View File

@@ -1,321 +0,0 @@
/**
* Authentication hook for managing user authentication state
*/
import { useState, useEffect, useCallback } from 'react';
import { authService } from '../../services/api/auth.service';
import { storageService } from '../../services/utils/storage.service';
import { User, UserLogin, UserRegistration, TokenResponse } from '../../types/auth.types';
import { ApiResponse } from '../../types/api.types';
interface AuthState {
user: User | null;
tenant_id: string | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
}
interface AuthActions {
login: (credentials: UserLogin) => Promise<boolean>;
register: (data: UserRegistration) => Promise<boolean>;
logout: () => Promise<void>;
refreshToken: () => Promise<boolean>;
requestPasswordReset: (email: string) => Promise<boolean>;
resetPassword: (token: string, password: string) => Promise<boolean>;
verifyEmail: (token: string) => Promise<boolean>;
updateProfile: (data: Partial<User>) => Promise<boolean>;
switchTenant: (tenantId: string) => Promise<boolean>;
clearError: () => void;
}
export const useAuth = (): AuthState & AuthActions => {
const [state, setState] = useState<AuthState>({
user: null,
tenant_id: null,
isAuthenticated: false,
isLoading: true,
error: null,
});
// Initialize authentication state
useEffect(() => {
const initializeAuth = async () => {
try {
const access_token = storageService.getItem<string>('access_token');
const user_data = storageService.getItem('user_data');
const tenant_id = storageService.getItem<string>('tenant_id');
if (access_token) {
// Try to get current user profile
const profileResponse = await authService.getCurrentUser();
if (profileResponse.success && profileResponse.data) {
setState(prev => ({
...prev,
user: profileResponse.data,
tenant_id: tenant_id || null,
isAuthenticated: true,
isLoading: false,
}));
return;
}
}
// No valid authentication found
setState(prev => ({
...prev,
isLoading: false,
}));
} catch (error) {
console.error('Auth initialization error:', error);
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al inicializar la autenticación',
}));
}
};
initializeAuth();
}, []);
const login = useCallback(async (credentials: UserLogin): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await authService.login(credentials);
if (response.success && response.data) {
const profileResponse = await authService.getCurrentUser();
if (profileResponse.success && profileResponse.data) {
setState(prev => ({
...prev,
user: profileResponse.data,
tenant_id: response.data.user?.tenant_id || null,
isAuthenticated: true,
isLoading: false,
}));
return true;
}
}
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al iniciar sesión',
}));
return false;
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
return false;
}
}, []);
const register = useCallback(async (data: UserRegistration): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await authService.register(data);
setState(prev => ({
...prev,
isLoading: false,
error: response.success ? null : (response.error || 'Error al registrar usuario'),
}));
return response.success;
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
return false;
}
}, []);
const logout = useCallback(async (): Promise<void> => {
try {
await authService.logout();
} catch (error) {
console.error('Logout error:', error);
} finally {
setState({
user: null,
tenant_id: null,
isAuthenticated: false,
isLoading: false,
error: null,
});
}
}, []);
const refreshToken = useCallback(async (): Promise<boolean> => {
try {
const response = await authService.refreshToken();
return response.success;
} catch (error) {
console.error('Token refresh error:', error);
await logout();
return false;
}
}, [logout]);
const requestPasswordReset = useCallback(async (email: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await authService.resetPassword(email);
if (!response.success) {
setState(prev => ({
...prev,
error: response.error || 'Error al solicitar restablecimiento de contraseña',
}));
}
return response.success;
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, []);
const resetPassword = useCallback(async (token: string, password: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await authService.confirmPasswordReset({ token, new_password: password });
if (!response.success) {
setState(prev => ({
...prev,
error: response.error || 'Error al restablecer contraseña',
}));
}
return response.success;
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, []);
const verifyEmail = useCallback(async (token: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await authService.confirmEmailVerification(token);
if (!response.success) {
setState(prev => ({
...prev,
error: response.error || 'Error al verificar email',
}));
}
return response.success;
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, []);
const updateProfile = useCallback(async (data: Partial<User>): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await authService.updateProfile(data);
if (response.success && response.data) {
setState(prev => ({
...prev,
user: response.data,
isLoading: false,
}));
return true;
}
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al actualizar perfil',
}));
return false;
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
return false;
}
}, []);
const switchTenant = useCallback(async (tenantId: string): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// switchTenant method doesn't exist in AuthService, implement tenant switching logic here
// For now, just update the local state
storageService.setItem('tenant_id', tenantId);
const response = { success: true };
if (response.success) {
setState(prev => ({
...prev,
tenant_id: tenantId,
isLoading: false,
}));
return true;
}
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al cambiar organización',
}));
return false;
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
return false;
}
}, []);
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
return {
...state,
login,
register,
logout,
refreshToken,
requestPasswordReset,
resetPassword,
verifyEmail,
updateProfile,
switchTenant,
clearError,
};
};

View File

@@ -1,568 +0,0 @@
/**
* Forecasting hook for managing demand forecasting and ML models
*/
import { useState, useEffect, useCallback } from 'react';
import { ForecastingService } from '../../services/api/forecasting.service';
import {
ForecastModel,
ForecastModelCreate,
ForecastModelUpdate,
ForecastPrediction,
ForecastPredictionCreate,
ForecastBatch,
ModelTraining,
ModelEvaluation
} from '../../types/forecasting.types';
import { ApiResponse, PaginatedResponse, QueryParams } from '../../types/api.types';
interface ForecastingState {
models: ForecastModel[];
predictions: ForecastPrediction[];
batches: ForecastBatch[];
trainings: ModelTraining[];
evaluations: ModelEvaluation[];
isLoading: boolean;
error: string | null;
pagination: {
total: number;
page: number;
pages: number;
limit: number;
};
}
interface ForecastingActions {
// Models
fetchModels: (params?: QueryParams) => Promise<void>;
createModel: (data: ForecastModelCreate) => Promise<boolean>;
updateModel: (id: string, data: ForecastModelUpdate) => Promise<boolean>;
deleteModel: (id: string) => Promise<boolean>;
getModel: (id: string) => Promise<ForecastModel | null>;
trainModel: (id: string, parameters?: any) => Promise<boolean>;
deployModel: (id: string) => Promise<boolean>;
// Predictions
fetchPredictions: (params?: QueryParams) => Promise<void>;
createPrediction: (data: ForecastPredictionCreate) => Promise<boolean>;
getPrediction: (id: string) => Promise<ForecastPrediction | null>;
generateDemandForecast: (modelId: string, horizon: number, parameters?: any) => Promise<any>;
// Batch Predictions
fetchBatches: (params?: QueryParams) => Promise<void>;
createBatch: (modelId: string, data: any) => Promise<boolean>;
getBatch: (id: string) => Promise<ForecastBatch | null>;
downloadBatchResults: (id: string) => Promise<boolean>;
// Model Training
fetchTrainings: (modelId?: string) => Promise<void>;
getTraining: (id: string) => Promise<ModelTraining | null>;
cancelTraining: (id: string) => Promise<boolean>;
// Model Evaluation
fetchEvaluations: (modelId?: string) => Promise<void>;
createEvaluation: (modelId: string, testData: any) => Promise<boolean>;
getEvaluation: (id: string) => Promise<ModelEvaluation | null>;
// Analytics
getModelPerformance: (modelId: string, period?: string) => Promise<any>;
getAccuracyReport: (modelId: string, startDate?: string, endDate?: string) => Promise<any>;
getFeatureImportance: (modelId: string) => Promise<any>;
// Utilities
clearError: () => void;
refresh: () => Promise<void>;
}
export const useForecasting = (): ForecastingState & ForecastingActions => {
const [state, setState] = useState<ForecastingState>({
models: [],
predictions: [],
batches: [],
trainings: [],
evaluations: [],
isLoading: false,
error: null,
pagination: {
total: 0,
page: 1,
pages: 1,
limit: 20,
},
});
const forecastingService = new ForecastingService();
// Fetch forecast models
const fetchModels = useCallback(async (params?: QueryParams) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await forecastingService.getModels(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
models: Array.isArray(response.data) ? response.data : response.data.items || [],
pagination: response.data.pagination || prev.pagination,
isLoading: false,
}));
} else {
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al cargar modelos de predicción',
}));
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
}
}, [forecastingService]);
// Create forecast model
const createModel = useCallback(async (data: ForecastModelCreate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await forecastingService.createModel(data);
if (response.success) {
await fetchModels();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al crear modelo de predicción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [forecastingService, fetchModels]);
// Update forecast model
const updateModel = useCallback(async (id: string, data: ForecastModelUpdate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await forecastingService.updateModel(id, data);
if (response.success) {
await fetchModels();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al actualizar modelo de predicción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [forecastingService, fetchModels]);
// Delete forecast model
const deleteModel = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await forecastingService.deleteModel(id);
if (response.success) {
setState(prev => ({
...prev,
models: prev.models.filter(model => model.id !== id),
}));
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al eliminar modelo de predicción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [forecastingService]);
// Get single forecast model
const getModel = useCallback(async (id: string): Promise<ForecastModel | null> => {
try {
const response = await forecastingService.getModel(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching model:', error);
return null;
}
}, [forecastingService]);
// Train forecast model
const trainModel = useCallback(async (id: string, parameters?: any): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await forecastingService.trainModel(id, parameters);
if (response.success) {
await fetchModels();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al entrenar modelo',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [forecastingService, fetchModels]);
// Deploy forecast model
const deployModel = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await forecastingService.deployModel(id);
if (response.success) {
await fetchModels();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al desplegar modelo',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [forecastingService, fetchModels]);
// Fetch predictions
const fetchPredictions = useCallback(async (params?: QueryParams) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await forecastingService.getPredictions(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
predictions: Array.isArray(response.data) ? response.data : response.data.items || [],
isLoading: false,
}));
} else {
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al cargar predicciones',
}));
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
}
}, [forecastingService]);
// Create prediction
const createPrediction = useCallback(async (data: ForecastPredictionCreate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await forecastingService.createPrediction(data);
if (response.success) {
await fetchPredictions();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al crear predicción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [forecastingService, fetchPredictions]);
// Get single prediction
const getPrediction = useCallback(async (id: string): Promise<ForecastPrediction | null> => {
try {
const response = await forecastingService.getPrediction(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching prediction:', error);
return null;
}
}, [forecastingService]);
// Generate demand forecast
const generateDemandForecast = useCallback(async (modelId: string, horizon: number, parameters?: any) => {
try {
const response = await forecastingService.generateDemandForecast(modelId, horizon, parameters);
return response.success ? response.data : null;
} catch (error) {
console.error('Error generating demand forecast:', error);
return null;
}
}, [forecastingService]);
// Fetch batch predictions
const fetchBatches = useCallback(async (params?: QueryParams) => {
try {
const response = await forecastingService.getBatches(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
batches: Array.isArray(response.data) ? response.data : response.data.items || [],
}));
}
} catch (error) {
console.error('Error fetching batches:', error);
}
}, [forecastingService]);
// Create batch prediction
const createBatch = useCallback(async (modelId: string, data: any): Promise<boolean> => {
try {
const response = await forecastingService.createBatch(modelId, data);
if (response.success) {
await fetchBatches();
return true;
}
return false;
} catch (error) {
console.error('Error creating batch:', error);
return false;
}
}, [forecastingService, fetchBatches]);
// Get single batch
const getBatch = useCallback(async (id: string): Promise<ForecastBatch | null> => {
try {
const response = await forecastingService.getBatch(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching batch:', error);
return null;
}
}, [forecastingService]);
// Download batch results
const downloadBatchResults = useCallback(async (id: string): Promise<boolean> => {
try {
const response = await forecastingService.downloadBatchResults(id);
return response.success;
} catch (error) {
console.error('Error downloading batch results:', error);
return false;
}
}, [forecastingService]);
// Fetch model trainings
const fetchTrainings = useCallback(async (modelId?: string) => {
try {
const response = await forecastingService.getTrainings(modelId);
if (response.success && response.data) {
setState(prev => ({
...prev,
trainings: Array.isArray(response.data) ? response.data : response.data.items || [],
}));
}
} catch (error) {
console.error('Error fetching trainings:', error);
}
}, [forecastingService]);
// Get single training
const getTraining = useCallback(async (id: string): Promise<ModelTraining | null> => {
try {
const response = await forecastingService.getTraining(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching training:', error);
return null;
}
}, [forecastingService]);
// Cancel model training
const cancelTraining = useCallback(async (id: string): Promise<boolean> => {
try {
const response = await forecastingService.cancelTraining(id);
if (response.success) {
await fetchTrainings();
return true;
}
return false;
} catch (error) {
console.error('Error canceling training:', error);
return false;
}
}, [forecastingService, fetchTrainings]);
// Fetch model evaluations
const fetchEvaluations = useCallback(async (modelId?: string) => {
try {
const response = await forecastingService.getEvaluations(modelId);
if (response.success && response.data) {
setState(prev => ({
...prev,
evaluations: Array.isArray(response.data) ? response.data : response.data.items || [],
}));
}
} catch (error) {
console.error('Error fetching evaluations:', error);
}
}, [forecastingService]);
// Create model evaluation
const createEvaluation = useCallback(async (modelId: string, testData: any): Promise<boolean> => {
try {
const response = await forecastingService.createEvaluation(modelId, testData);
if (response.success) {
await fetchEvaluations(modelId);
return true;
}
return false;
} catch (error) {
console.error('Error creating evaluation:', error);
return false;
}
}, [forecastingService, fetchEvaluations]);
// Get single evaluation
const getEvaluation = useCallback(async (id: string): Promise<ModelEvaluation | null> => {
try {
const response = await forecastingService.getEvaluation(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching evaluation:', error);
return null;
}
}, [forecastingService]);
// Get model performance
const getModelPerformance = useCallback(async (modelId: string, period?: string) => {
try {
const response = await forecastingService.getModelPerformance(modelId, period);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching model performance:', error);
return null;
}
}, [forecastingService]);
// Get accuracy report
const getAccuracyReport = useCallback(async (modelId: string, startDate?: string, endDate?: string) => {
try {
const response = await forecastingService.getAccuracyReport(modelId, startDate, endDate);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching accuracy report:', error);
return null;
}
}, [forecastingService]);
// Get feature importance
const getFeatureImportance = useCallback(async (modelId: string) => {
try {
const response = await forecastingService.getFeatureImportance(modelId);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching feature importance:', error);
return null;
}
}, [forecastingService]);
// Clear error
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
// Refresh all data
const refresh = useCallback(async () => {
await Promise.all([
fetchModels(),
fetchPredictions(),
fetchBatches(),
]);
}, [fetchModels, fetchPredictions, fetchBatches]);
// Initialize data on mount
useEffect(() => {
refresh();
}, []);
return {
...state,
fetchModels,
createModel,
updateModel,
deleteModel,
getModel,
trainModel,
deployModel,
fetchPredictions,
createPrediction,
getPrediction,
generateDemandForecast,
fetchBatches,
createBatch,
getBatch,
downloadBatchResults,
fetchTrainings,
getTraining,
cancelTraining,
fetchEvaluations,
createEvaluation,
getEvaluation,
getModelPerformance,
getAccuracyReport,
getFeatureImportance,
clearError,
refresh,
};
};

View File

@@ -1,475 +0,0 @@
/**
* Inventory hook for managing inventory state and operations
*/
import { useState, useEffect, useCallback } from 'react';
import { InventoryService } from '../../services/api/inventory.service';
import {
Ingredient,
IngredientCreate,
IngredientUpdate,
StockLevel,
StockMovement,
StockMovementCreate,
InventoryAlert,
QualityCheckCreate,
QualityCheck
} from '../../types/inventory.types';
import { ApiResponse, PaginatedResponse, QueryParams } from '../../types/api.types';
interface InventoryState {
ingredients: Ingredient[];
stockLevels: StockLevel[];
stockMovements: StockMovement[];
alerts: InventoryAlert[];
qualityChecks: QualityCheck[];
isLoading: boolean;
error: string | null;
pagination: {
total: number;
page: number;
pages: number;
limit: number;
};
}
interface InventoryActions {
// Ingredients
fetchIngredients: (params?: QueryParams) => Promise<void>;
createIngredient: (data: IngredientCreate) => Promise<boolean>;
updateIngredient: (id: string, data: IngredientUpdate) => Promise<boolean>;
deleteIngredient: (id: string) => Promise<boolean>;
getIngredient: (id: string) => Promise<Ingredient | null>;
// Stock Levels
fetchStockLevels: (params?: QueryParams) => Promise<void>;
updateStockLevel: (ingredientId: string, quantity: number, reason?: string) => Promise<boolean>;
// Stock Movements
fetchStockMovements: (params?: QueryParams) => Promise<void>;
createStockMovement: (data: StockMovementCreate) => Promise<boolean>;
// Alerts
fetchAlerts: (params?: QueryParams) => Promise<void>;
markAlertAsRead: (id: string) => Promise<boolean>;
dismissAlert: (id: string) => Promise<boolean>;
// Quality Checks
fetchQualityChecks: (params?: QueryParams) => Promise<void>;
createQualityCheck: (data: QualityCheckCreate) => Promise<boolean>;
// Analytics
getInventoryAnalytics: (startDate?: string, endDate?: string) => Promise<any>;
getExpirationReport: () => Promise<any>;
// Utilities
clearError: () => void;
refresh: () => Promise<void>;
}
export const useInventory = (): InventoryState & InventoryActions => {
const [state, setState] = useState<InventoryState>({
ingredients: [],
stockLevels: [],
stockMovements: [],
alerts: [],
qualityChecks: [],
isLoading: false,
error: null,
pagination: {
total: 0,
page: 1,
pages: 1,
limit: 20,
},
});
const inventoryService = new InventoryService();
// Fetch ingredients
const fetchIngredients = useCallback(async (params?: QueryParams) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await inventoryService.getIngredients(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
ingredients: Array.isArray(response.data) ? response.data : response.data.items || [],
pagination: response.data.pagination || prev.pagination,
isLoading: false,
}));
} else {
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al cargar ingredientes',
}));
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
}
}, [inventoryService]);
// Create ingredient
const createIngredient = useCallback(async (data: IngredientCreate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await inventoryService.createIngredient(data);
if (response.success) {
await fetchIngredients();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al crear ingrediente',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [inventoryService, fetchIngredients]);
// Update ingredient
const updateIngredient = useCallback(async (id: string, data: IngredientUpdate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await inventoryService.updateIngredient(id, data);
if (response.success) {
await fetchIngredients();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al actualizar ingrediente',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [inventoryService, fetchIngredients]);
// Delete ingredient
const deleteIngredient = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await inventoryService.deleteIngredient(id);
if (response.success) {
setState(prev => ({
...prev,
ingredients: prev.ingredients.filter(ingredient => ingredient.id !== id),
}));
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al eliminar ingrediente',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [inventoryService]);
// Get single ingredient
const getIngredient = useCallback(async (id: string): Promise<Ingredient | null> => {
try {
const response = await inventoryService.getIngredient(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching ingredient:', error);
return null;
}
}, [inventoryService]);
// Fetch stock levels
const fetchStockLevels = useCallback(async (params?: QueryParams) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await inventoryService.getStockLevels(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
stockLevels: Array.isArray(response.data) ? response.data : response.data.items || [],
isLoading: false,
}));
} else {
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al cargar niveles de stock',
}));
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
}
}, [inventoryService]);
// Update stock level
const updateStockLevel = useCallback(async (ingredientId: string, quantity: number, reason?: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await inventoryService.updateStockLevel(ingredientId, {
quantity,
reason: reason || 'Manual adjustment'
});
if (response.success) {
await fetchStockLevels();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al actualizar stock',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [inventoryService, fetchStockLevels]);
// Fetch stock movements
const fetchStockMovements = useCallback(async (params?: QueryParams) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await inventoryService.getStockMovements(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
stockMovements: Array.isArray(response.data) ? response.data : response.data.items || [],
isLoading: false,
}));
} else {
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al cargar movimientos de stock',
}));
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
}
}, [inventoryService]);
// Create stock movement
const createStockMovement = useCallback(async (data: StockMovementCreate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await inventoryService.createStockMovement(data);
if (response.success) {
await fetchStockMovements();
await fetchStockLevels();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al crear movimiento de stock',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [inventoryService, fetchStockMovements, fetchStockLevels]);
// Fetch alerts
const fetchAlerts = useCallback(async (params?: QueryParams) => {
try {
const response = await inventoryService.getAlerts(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
alerts: Array.isArray(response.data) ? response.data : response.data.items || [],
}));
}
} catch (error) {
console.error('Error fetching alerts:', error);
}
}, [inventoryService]);
// Mark alert as read
const markAlertAsRead = useCallback(async (id: string): Promise<boolean> => {
try {
const response = await inventoryService.markAlertAsRead(id);
if (response.success) {
setState(prev => ({
...prev,
alerts: prev.alerts.map(alert =>
alert.id === id ? { ...alert, is_read: true } : alert
),
}));
return true;
}
return false;
} catch (error) {
console.error('Error marking alert as read:', error);
return false;
}
}, [inventoryService]);
// Dismiss alert
const dismissAlert = useCallback(async (id: string): Promise<boolean> => {
try {
const response = await inventoryService.dismissAlert(id);
if (response.success) {
setState(prev => ({
...prev,
alerts: prev.alerts.filter(alert => alert.id !== id),
}));
return true;
}
return false;
} catch (error) {
console.error('Error dismissing alert:', error);
return false;
}
}, [inventoryService]);
// Fetch quality checks
const fetchQualityChecks = useCallback(async (params?: QueryParams) => {
try {
const response = await inventoryService.getQualityChecks(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
qualityChecks: Array.isArray(response.data) ? response.data : response.data.items || [],
}));
}
} catch (error) {
console.error('Error fetching quality checks:', error);
}
}, [inventoryService]);
// Create quality check
const createQualityCheck = useCallback(async (data: QualityCheckCreate): Promise<boolean> => {
try {
const response = await inventoryService.createQualityCheck(data);
if (response.success) {
await fetchQualityChecks();
return true;
}
return false;
} catch (error) {
console.error('Error creating quality check:', error);
return false;
}
}, [inventoryService, fetchQualityChecks]);
// Get inventory analytics
const getInventoryAnalytics = useCallback(async (startDate?: string, endDate?: string) => {
try {
const response = await inventoryService.getAnalytics(startDate, endDate);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching inventory analytics:', error);
return null;
}
}, [inventoryService]);
// Get expiration report
const getExpirationReport = useCallback(async () => {
try {
const response = await inventoryService.getExpirationReport();
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching expiration report:', error);
return null;
}
}, [inventoryService]);
// Clear error
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
// Refresh all data
const refresh = useCallback(async () => {
await Promise.all([
fetchIngredients(),
fetchStockLevels(),
fetchAlerts(),
]);
}, [fetchIngredients, fetchStockLevels, fetchAlerts]);
// Initialize data on mount
useEffect(() => {
refresh();
}, []);
return {
...state,
fetchIngredients,
createIngredient,
updateIngredient,
deleteIngredient,
getIngredient,
fetchStockLevels,
updateStockLevel,
fetchStockMovements,
createStockMovement,
fetchAlerts,
markAlertAsRead,
dismissAlert,
fetchQualityChecks,
createQualityCheck,
getInventoryAnalytics,
getExpirationReport,
clearError,
refresh,
};
};

View File

@@ -1,650 +0,0 @@
/**
* Production hook for managing production batches, recipes, and scheduling
*/
import { useState, useEffect, useCallback } from 'react';
import { ProductionService } from '../../services/api/production.service';
import {
ProductionBatch,
ProductionBatchCreate,
ProductionBatchUpdate,
Recipe,
RecipeCreate,
RecipeUpdate,
ProductionSchedule,
ProductionScheduleCreate,
QualityControl,
QualityControlCreate
} from '../../types/production.types';
import { ApiResponse, PaginatedResponse, QueryParams } from '../../types/api.types';
interface ProductionState {
batches: ProductionBatch[];
recipes: Recipe[];
schedules: ProductionSchedule[];
qualityControls: QualityControl[];
isLoading: boolean;
error: string | null;
pagination: {
total: number;
page: number;
pages: number;
limit: number;
};
}
interface ProductionActions {
// Production Batches
fetchBatches: (params?: QueryParams) => Promise<void>;
createBatch: (data: ProductionBatchCreate) => Promise<boolean>;
updateBatch: (id: string, data: ProductionBatchUpdate) => Promise<boolean>;
deleteBatch: (id: string) => Promise<boolean>;
getBatch: (id: string) => Promise<ProductionBatch | null>;
startBatch: (id: string) => Promise<boolean>;
completeBatch: (id: string) => Promise<boolean>;
cancelBatch: (id: string, reason: string) => Promise<boolean>;
// Recipes
fetchRecipes: (params?: QueryParams) => Promise<void>;
createRecipe: (data: RecipeCreate) => Promise<boolean>;
updateRecipe: (id: string, data: RecipeUpdate) => Promise<boolean>;
deleteRecipe: (id: string) => Promise<boolean>;
getRecipe: (id: string) => Promise<Recipe | null>;
duplicateRecipe: (id: string, name: string) => Promise<boolean>;
// Production Scheduling
fetchSchedules: (params?: QueryParams) => Promise<void>;
createSchedule: (data: ProductionScheduleCreate) => Promise<boolean>;
updateSchedule: (id: string, data: Partial<ProductionScheduleCreate>) => Promise<boolean>;
deleteSchedule: (id: string) => Promise<boolean>;
getCapacityAnalysis: (date: string) => Promise<any>;
// Quality Control
fetchQualityControls: (params?: QueryParams) => Promise<void>;
createQualityControl: (data: QualityControlCreate) => Promise<boolean>;
updateQualityControl: (id: string, data: Partial<QualityControlCreate>) => Promise<boolean>;
// Analytics
getProductionAnalytics: (startDate?: string, endDate?: string) => Promise<any>;
getEfficiencyReport: (period: string) => Promise<any>;
getRecipePerformance: (recipeId?: string) => Promise<any>;
// Utilities
clearError: () => void;
refresh: () => Promise<void>;
}
export const useProduction = (): ProductionState & ProductionActions => {
const [state, setState] = useState<ProductionState>({
batches: [],
recipes: [],
schedules: [],
qualityControls: [],
isLoading: false,
error: null,
pagination: {
total: 0,
page: 1,
pages: 1,
limit: 20,
},
});
const productionService = new ProductionService();
// Fetch production batches
const fetchBatches = useCallback(async (params?: QueryParams) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await productionService.getBatches(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
batches: Array.isArray(response.data) ? response.data : response.data.items || [],
pagination: response.data.pagination || prev.pagination,
isLoading: false,
}));
} else {
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al cargar lotes de producción',
}));
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
}
}, [productionService]);
// Create production batch
const createBatch = useCallback(async (data: ProductionBatchCreate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.createBatch(data);
if (response.success) {
await fetchBatches();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al crear lote de producción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchBatches]);
// Update production batch
const updateBatch = useCallback(async (id: string, data: ProductionBatchUpdate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.updateBatch(id, data);
if (response.success) {
await fetchBatches();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al actualizar lote de producción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchBatches]);
// Delete production batch
const deleteBatch = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.deleteBatch(id);
if (response.success) {
setState(prev => ({
...prev,
batches: prev.batches.filter(batch => batch.id !== id),
}));
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al eliminar lote de producción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService]);
// Get single production batch
const getBatch = useCallback(async (id: string): Promise<ProductionBatch | null> => {
try {
const response = await productionService.getBatch(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching batch:', error);
return null;
}
}, [productionService]);
// Start production batch
const startBatch = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.startBatch(id);
if (response.success) {
await fetchBatches();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al iniciar lote de producción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchBatches]);
// Complete production batch
const completeBatch = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.completeBatch(id);
if (response.success) {
await fetchBatches();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al completar lote de producción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchBatches]);
// Cancel production batch
const cancelBatch = useCallback(async (id: string, reason: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.cancelBatch(id, reason);
if (response.success) {
await fetchBatches();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al cancelar lote de producción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchBatches]);
// Fetch recipes
const fetchRecipes = useCallback(async (params?: QueryParams) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await productionService.getRecipes(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
recipes: Array.isArray(response.data) ? response.data : response.data.items || [],
isLoading: false,
}));
} else {
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al cargar recetas',
}));
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
}
}, [productionService]);
// Create recipe
const createRecipe = useCallback(async (data: RecipeCreate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.createRecipe(data);
if (response.success) {
await fetchRecipes();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al crear receta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchRecipes]);
// Update recipe
const updateRecipe = useCallback(async (id: string, data: RecipeUpdate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.updateRecipe(id, data);
if (response.success) {
await fetchRecipes();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al actualizar receta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchRecipes]);
// Delete recipe
const deleteRecipe = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.deleteRecipe(id);
if (response.success) {
setState(prev => ({
...prev,
recipes: prev.recipes.filter(recipe => recipe.id !== id),
}));
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al eliminar receta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService]);
// Get single recipe
const getRecipe = useCallback(async (id: string): Promise<Recipe | null> => {
try {
const response = await productionService.getRecipe(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching recipe:', error);
return null;
}
}, [productionService]);
// Duplicate recipe
const duplicateRecipe = useCallback(async (id: string, name: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.duplicateRecipe(id, name);
if (response.success) {
await fetchRecipes();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al duplicar receta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchRecipes]);
// Fetch production schedules
const fetchSchedules = useCallback(async (params?: QueryParams) => {
try {
const response = await productionService.getSchedules(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
schedules: Array.isArray(response.data) ? response.data : response.data.items || [],
}));
}
} catch (error) {
console.error('Error fetching schedules:', error);
}
}, [productionService]);
// Create production schedule
const createSchedule = useCallback(async (data: ProductionScheduleCreate): Promise<boolean> => {
try {
const response = await productionService.createSchedule(data);
if (response.success) {
await fetchSchedules();
return true;
}
return false;
} catch (error) {
console.error('Error creating schedule:', error);
return false;
}
}, [productionService, fetchSchedules]);
// Update production schedule
const updateSchedule = useCallback(async (id: string, data: Partial<ProductionScheduleCreate>): Promise<boolean> => {
try {
const response = await productionService.updateSchedule(id, data);
if (response.success) {
await fetchSchedules();
return true;
}
return false;
} catch (error) {
console.error('Error updating schedule:', error);
return false;
}
}, [productionService, fetchSchedules]);
// Delete production schedule
const deleteSchedule = useCallback(async (id: string): Promise<boolean> => {
try {
const response = await productionService.deleteSchedule(id);
if (response.success) {
setState(prev => ({
...prev,
schedules: prev.schedules.filter(schedule => schedule.id !== id),
}));
return true;
}
return false;
} catch (error) {
console.error('Error deleting schedule:', error);
return false;
}
}, [productionService]);
// Get capacity analysis
const getCapacityAnalysis = useCallback(async (date: string) => {
try {
const response = await productionService.getCapacityAnalysis(date);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching capacity analysis:', error);
return null;
}
}, [productionService]);
// Fetch quality controls
const fetchQualityControls = useCallback(async (params?: QueryParams) => {
try {
const response = await productionService.getQualityControls(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
qualityControls: Array.isArray(response.data) ? response.data : response.data.items || [],
}));
}
} catch (error) {
console.error('Error fetching quality controls:', error);
}
}, [productionService]);
// Create quality control
const createQualityControl = useCallback(async (data: QualityControlCreate): Promise<boolean> => {
try {
const response = await productionService.createQualityControl(data);
if (response.success) {
await fetchQualityControls();
return true;
}
return false;
} catch (error) {
console.error('Error creating quality control:', error);
return false;
}
}, [productionService, fetchQualityControls]);
// Update quality control
const updateQualityControl = useCallback(async (id: string, data: Partial<QualityControlCreate>): Promise<boolean> => {
try {
const response = await productionService.updateQualityControl(id, data);
if (response.success) {
await fetchQualityControls();
return true;
}
return false;
} catch (error) {
console.error('Error updating quality control:', error);
return false;
}
}, [productionService, fetchQualityControls]);
// Get production analytics
const getProductionAnalytics = useCallback(async (startDate?: string, endDate?: string) => {
try {
const response = await productionService.getAnalytics(startDate, endDate);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching production analytics:', error);
return null;
}
}, [productionService]);
// Get efficiency report
const getEfficiencyReport = useCallback(async (period: string) => {
try {
const response = await productionService.getEfficiencyReport(period);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching efficiency report:', error);
return null;
}
}, [productionService]);
// Get recipe performance
const getRecipePerformance = useCallback(async (recipeId?: string) => {
try {
const response = await productionService.getRecipePerformance(recipeId);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching recipe performance:', error);
return null;
}
}, [productionService]);
// Clear error
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
// Refresh all data
const refresh = useCallback(async () => {
await Promise.all([
fetchBatches(),
fetchRecipes(),
fetchSchedules(),
]);
}, [fetchBatches, fetchRecipes, fetchSchedules]);
// Initialize data on mount
useEffect(() => {
refresh();
}, []);
return {
...state,
fetchBatches,
createBatch,
updateBatch,
deleteBatch,
getBatch,
startBatch,
completeBatch,
cancelBatch,
fetchRecipes,
createRecipe,
updateRecipe,
deleteRecipe,
getRecipe,
duplicateRecipe,
fetchSchedules,
createSchedule,
updateSchedule,
deleteSchedule,
getCapacityAnalysis,
fetchQualityControls,
createQualityControl,
updateQualityControl,
getProductionAnalytics,
getEfficiencyReport,
getRecipePerformance,
clearError,
refresh,
};
};

View File

@@ -1,329 +0,0 @@
/**
* Server-Sent Events (SSE) hook for real-time notifications and updates
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import { StorageService } from '../../services/api/utils/storage.service';
export interface SSEMessage {
id?: string;
event?: string;
data: any;
timestamp: number;
}
interface SSEState {
isConnected: boolean;
isConnecting: boolean;
error: string | null;
messages: SSEMessage[];
lastEventId: string | null;
}
interface SSEOptions {
withCredentials?: boolean;
reconnectInterval?: number;
maxReconnectAttempts?: number;
bufferSize?: number;
autoConnect?: boolean;
eventFilters?: string[];
}
interface SSEActions {
connect: (url: string) => void;
disconnect: () => void;
reconnect: () => void;
clearMessages: () => void;
clearError: () => void;
addEventListener: (eventType: string, handler: (data: any) => void) => () => void;
}
const DEFAULT_OPTIONS: Required<SSEOptions> = {
withCredentials: true,
reconnectInterval: 3000,
maxReconnectAttempts: 10,
bufferSize: 100,
autoConnect: true,
eventFilters: [],
};
export const useSSE = (
initialUrl?: string,
options: SSEOptions = {}
): SSEState & SSEActions => {
const [state, setState] = useState<SSEState>({
isConnected: false,
isConnecting: false,
error: null,
messages: [],
lastEventId: null,
});
const eventSourceRef = useRef<EventSource | null>(null);
const urlRef = useRef<string | null>(initialUrl || null);
const reconnectTimeoutRef = useRef<number | null>(null);
const reconnectAttemptsRef = useRef<number>(0);
const eventHandlersRef = useRef<Map<string, Set<(data: any) => void>>>(new Map());
const config = { ...DEFAULT_OPTIONS, ...options };
const storageService = new StorageService();
// Helper function to get auth headers
const getAuthHeaders = useCallback(() => {
const authData = storageService.getAuthData();
if (authData?.access_token) {
return `Bearer ${authData.access_token}`;
}
return null;
}, [storageService]);
// Helper function to build URL with auth token
const buildUrlWithAuth = useCallback((baseUrl: string) => {
const url = new URL(baseUrl);
const authToken = getAuthHeaders();
if (authToken) {
url.searchParams.set('Authorization', authToken);
}
if (state.lastEventId) {
url.searchParams.set('Last-Event-ID', state.lastEventId);
}
return url.toString();
}, [getAuthHeaders, state.lastEventId]);
// Add event listener for specific event types
const addEventListener = useCallback((eventType: string, handler: (data: any) => void) => {
if (!eventHandlersRef.current.has(eventType)) {
eventHandlersRef.current.set(eventType, new Set());
}
eventHandlersRef.current.get(eventType)!.add(handler);
// Return cleanup function
return () => {
const handlers = eventHandlersRef.current.get(eventType);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
eventHandlersRef.current.delete(eventType);
}
}
};
}, []);
// Process incoming message
const processMessage = useCallback((event: MessageEvent, eventType: string = 'message') => {
try {
let data: any;
try {
data = JSON.parse(event.data);
} catch {
data = event.data;
}
const message: SSEMessage = {
id: event.lastEventId || undefined,
event: eventType,
data,
timestamp: Date.now(),
};
// Filter messages if eventFilters is specified
if (config.eventFilters.length > 0 && !config.eventFilters.includes(eventType)) {
return;
}
setState(prev => ({
...prev,
messages: [...prev.messages.slice(-(config.bufferSize - 1)), message],
lastEventId: event.lastEventId || prev.lastEventId,
}));
// Call registered event handlers
const handlers = eventHandlersRef.current.get(eventType);
if (handlers) {
handlers.forEach(handler => {
try {
handler(data);
} catch (error) {
console.error('Error in SSE event handler:', error);
}
});
}
// Call generic message handlers
const messageHandlers = eventHandlersRef.current.get('message');
if (messageHandlers && eventType !== 'message') {
messageHandlers.forEach(handler => {
try {
handler(message);
} catch (error) {
console.error('Error in SSE message handler:', error);
}
});
}
} catch (error) {
console.error('Error processing SSE message:', error);
}
}, [config.eventFilters, config.bufferSize]);
// Connect to SSE endpoint
const connect = useCallback((url: string) => {
if (eventSourceRef.current) {
disconnect();
}
urlRef.current = url;
setState(prev => ({ ...prev, isConnecting: true, error: null }));
try {
const fullUrl = buildUrlWithAuth(url);
const eventSource = new EventSource(fullUrl, {
withCredentials: config.withCredentials,
});
eventSourceRef.current = eventSource;
eventSource.onopen = () => {
setState(prev => ({
...prev,
isConnected: true,
isConnecting: false,
error: null,
}));
reconnectAttemptsRef.current = 0;
// Clear reconnect timeout if it exists
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
};
eventSource.onmessage = (event) => {
processMessage(event, 'message');
};
eventSource.onerror = (error) => {
console.error('SSE error:', error);
setState(prev => ({
...prev,
isConnected: false,
isConnecting: false,
error: 'Error de conexión con el servidor',
}));
// Attempt reconnection if within limits
if (reconnectAttemptsRef.current < config.maxReconnectAttempts) {
reconnectAttemptsRef.current += 1;
reconnectTimeoutRef.current = window.setTimeout(() => {
if (urlRef.current) {
connect(urlRef.current);
}
}, config.reconnectInterval);
} else {
setState(prev => ({
...prev,
error: `Máximo número de intentos de reconexión alcanzado (${config.maxReconnectAttempts})`,
}));
}
};
// Add listeners for custom event types
const commonEventTypes = ['notification', 'alert', 'update', 'heartbeat'];
commonEventTypes.forEach(eventType => {
eventSource.addEventListener(eventType, (event) => {
processMessage(event as MessageEvent, eventType);
});
});
} catch (error) {
setState(prev => ({
...prev,
isConnecting: false,
error: 'Error al establecer conexión SSE',
}));
}
}, [buildUrlWithAuth, config.withCredentials, config.maxReconnectAttempts, config.reconnectInterval, processMessage]);
// Disconnect from SSE
const disconnect = useCallback(() => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
eventSourceRef.current = null;
}
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
setState(prev => ({
...prev,
isConnected: false,
isConnecting: false,
}));
reconnectAttemptsRef.current = 0;
}, []);
// Reconnect to SSE
const reconnect = useCallback(() => {
if (urlRef.current) {
disconnect();
setTimeout(() => {
connect(urlRef.current!);
}, 100);
}
}, [connect, disconnect]);
// Clear messages buffer
const clearMessages = useCallback(() => {
setState(prev => ({ ...prev, messages: [] }));
}, []);
// Clear error
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
// Auto-connect on mount if URL provided
useEffect(() => {
if (initialUrl && config.autoConnect) {
connect(initialUrl);
}
return () => {
disconnect();
};
}, [initialUrl, config.autoConnect]);
// Cleanup on unmount
useEffect(() => {
return () => {
disconnect();
eventHandlersRef.current.clear();
};
}, [disconnect]);
// Auto-reconnect when auth token changes
useEffect(() => {
if (state.isConnected && urlRef.current) {
reconnect();
}
}, [storageService.getAuthData()?.access_token]);
return {
...state,
connect,
disconnect,
reconnect,
clearMessages,
clearError,
addEventListener,
};
};

View File

@@ -1,507 +0,0 @@
/**
* Sales hook for managing sales data, analytics, and reporting
*/
import { useState, useEffect, useCallback } from 'react';
import { salesService } from '../../services/api/sales.service';
import {
SalesData,
SalesDataCreate,
SalesDataUpdate,
SalesAnalytics,
OnboardingAnalysis,
WeatherCorrelation
} from '../../types/sales.types';
import { ApiResponse, PaginatedResponse, QueryParams } from '../../types/api.types';
interface SalesState {
salesData: SalesData[];
analytics: SalesAnalytics | null;
onboardingAnalysis: OnboardingAnalysis | null;
weatherCorrelation: WeatherCorrelation | null;
isLoading: boolean;
error: string | null;
pagination: {
total: number;
page: number;
pages: number;
limit: number;
};
}
interface SalesActions {
// Sales Data
fetchSalesData: (params?: QueryParams) => Promise<void>;
createSalesData: (data: SalesDataCreate) => Promise<boolean>;
updateSalesData: (id: string, data: SalesDataUpdate) => Promise<boolean>;
deleteSalesData: (id: string) => Promise<boolean>;
getSalesData: (id: string) => Promise<SalesData | null>;
bulkCreateSalesData: (data: SalesDataCreate[]) => Promise<boolean>;
// Analytics
fetchAnalytics: (startDate?: string, endDate?: string, filters?: any) => Promise<void>;
getRevenueAnalytics: (period: string, groupBy?: string) => Promise<any>;
getProductAnalytics: (startDate?: string, endDate?: string) => Promise<any>;
getCustomerAnalytics: (startDate?: string, endDate?: string) => Promise<any>;
getTimeAnalytics: (period: string) => Promise<any>;
// Reports
getDailyReport: (date: string) => Promise<any>;
getWeeklyReport: (startDate: string) => Promise<any>;
getMonthlyReport: (year: number, month: number) => Promise<any>;
getPerformanceReport: (startDate: string, endDate: string) => Promise<any>;
// Weather Correlation
fetchWeatherCorrelation: (startDate?: string, endDate?: string) => Promise<void>;
updateWeatherData: (startDate: string, endDate: string) => Promise<boolean>;
// Onboarding Analysis
fetchOnboardingAnalysis: () => Promise<void>;
generateOnboardingReport: () => Promise<any>;
// Import/Export
importSalesData: (file: File, format: string) => Promise<boolean>;
exportSalesData: (startDate: string, endDate: string, format: string) => Promise<boolean>;
getImportTemplates: () => Promise<any>;
// Utilities
clearError: () => void;
refresh: () => Promise<void>;
}
export const useSales = (): SalesState & SalesActions => {
const [state, setState] = useState<SalesState>({
salesData: [],
analytics: null,
onboardingAnalysis: null,
weatherCorrelation: null,
isLoading: false,
error: null,
pagination: {
total: 0,
page: 1,
pages: 1,
limit: 20,
},
});
// Fetch sales data
const fetchSalesData = useCallback(async (params?: QueryParams) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await salesService.getSalesData(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
salesData: Array.isArray(response.data) ? response.data : response.data.items || [],
pagination: response.data.pagination || prev.pagination,
isLoading: false,
}));
} else {
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al cargar datos de ventas',
}));
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
}
}, [salesService]);
// Create sales data
const createSalesData = useCallback(async (data: SalesDataCreate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await salesService.createSalesData(data);
if (response.success) {
await fetchSalesData();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al crear dato de venta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [salesService, fetchSalesData]);
// Update sales data
const updateSalesData = useCallback(async (id: string, data: SalesDataUpdate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await salesService.updateSalesData(id, data);
if (response.success) {
await fetchSalesData();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al actualizar dato de venta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [salesService, fetchSalesData]);
// Delete sales data
const deleteSalesData = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await salesService.deleteSalesData(id);
if (response.success) {
setState(prev => ({
...prev,
salesData: prev.salesData.filter(data => data.id !== id),
}));
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al eliminar dato de venta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [salesService]);
// Get single sales data
const getSalesData = useCallback(async (id: string): Promise<SalesData | null> => {
try {
const response = await salesService.getSalesDataById(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching sales data:', error);
return null;
}
}, [salesService]);
// Bulk create sales data
const bulkCreateSalesData = useCallback(async (data: SalesDataCreate[]): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await salesService.bulkCreateSalesData(data);
if (response.success) {
await fetchSalesData();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al crear datos de venta en lote',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [salesService, fetchSalesData]);
// Fetch analytics
const fetchAnalytics = useCallback(async (startDate?: string, endDate?: string, filters?: any) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await salesService.getAnalytics(startDate, endDate, filters);
if (response.success && response.data) {
setState(prev => ({
...prev,
analytics: response.data,
isLoading: false,
}));
} else {
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al cargar analytics de ventas',
}));
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
}
}, [salesService]);
// Get revenue analytics
const getRevenueAnalytics = useCallback(async (period: string, groupBy?: string) => {
try {
const response = await salesService.getRevenueAnalytics(period, groupBy);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching revenue analytics:', error);
return null;
}
}, [salesService]);
// Get product analytics
const getProductAnalytics = useCallback(async (startDate?: string, endDate?: string) => {
try {
const response = await salesService.getProductAnalytics(startDate, endDate);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching product analytics:', error);
return null;
}
}, [salesService]);
// Get customer analytics
const getCustomerAnalytics = useCallback(async (startDate?: string, endDate?: string) => {
try {
const response = await salesService.getCustomerAnalytics(startDate, endDate);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching customer analytics:', error);
return null;
}
}, [salesService]);
// Get time analytics
const getTimeAnalytics = useCallback(async (period: string) => {
try {
const response = await salesService.getTimeAnalytics(period);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching time analytics:', error);
return null;
}
}, [salesService]);
// Get daily report
const getDailyReport = useCallback(async (date: string) => {
try {
const response = await salesService.getDailyReport(date);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching daily report:', error);
return null;
}
}, [salesService]);
// Get weekly report
const getWeeklyReport = useCallback(async (startDate: string) => {
try {
const response = await salesService.getWeeklyReport(startDate);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching weekly report:', error);
return null;
}
}, [salesService]);
// Get monthly report
const getMonthlyReport = useCallback(async (year: number, month: number) => {
try {
const response = await salesService.getMonthlyReport(year, month);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching monthly report:', error);
return null;
}
}, [salesService]);
// Get performance report
const getPerformanceReport = useCallback(async (startDate: string, endDate: string) => {
try {
const response = await salesService.getPerformanceReport(startDate, endDate);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching performance report:', error);
return null;
}
}, [salesService]);
// Fetch weather correlation
const fetchWeatherCorrelation = useCallback(async (startDate?: string, endDate?: string) => {
try {
const response = await salesService.getWeatherCorrelation(startDate, endDate);
if (response.success && response.data) {
setState(prev => ({
...prev,
weatherCorrelation: response.data,
}));
}
} catch (error) {
console.error('Error fetching weather correlation:', error);
}
}, [salesService]);
// Update weather data
const updateWeatherData = useCallback(async (startDate: string, endDate: string): Promise<boolean> => {
try {
const response = await salesService.updateWeatherData(startDate, endDate);
if (response.success) {
await fetchWeatherCorrelation(startDate, endDate);
return true;
}
return false;
} catch (error) {
console.error('Error updating weather data:', error);
return false;
}
}, [salesService, fetchWeatherCorrelation]);
// Fetch onboarding analysis
const fetchOnboardingAnalysis = useCallback(async () => {
try {
const response = await salesService.getOnboardingAnalysis();
if (response.success && response.data) {
setState(prev => ({
...prev,
onboardingAnalysis: response.data,
}));
}
} catch (error) {
console.error('Error fetching onboarding analysis:', error);
}
}, [salesService]);
// Generate onboarding report
const generateOnboardingReport = useCallback(async () => {
try {
const response = await salesService.generateOnboardingReport();
return response.success ? response.data : null;
} catch (error) {
console.error('Error generating onboarding report:', error);
return null;
}
}, [salesService]);
// Import sales data
const importSalesData = useCallback(async (file: File, format: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await salesService.importSalesData(file, format);
if (response.success) {
await fetchSalesData();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al importar datos de venta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [salesService, fetchSalesData]);
// Export sales data
const exportSalesData = useCallback(async (startDate: string, endDate: string, format: string): Promise<boolean> => {
try {
const response = await salesService.exportSalesData(startDate, endDate, format);
return response.success;
} catch (error) {
console.error('Error exporting sales data:', error);
return false;
}
}, [salesService]);
// Get import templates
const getImportTemplates = useCallback(async () => {
try {
const response = await salesService.getImportTemplates();
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching import templates:', error);
return null;
}
}, [salesService]);
// Clear error
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
// Refresh all data
const refresh = useCallback(async () => {
await Promise.all([
fetchSalesData(),
fetchAnalytics(),
fetchOnboardingAnalysis(),
]);
}, [fetchSalesData, fetchAnalytics, fetchOnboardingAnalysis]);
// Initialize data on mount
useEffect(() => {
refresh();
}, []);
return {
...state,
fetchSalesData,
createSalesData,
updateSalesData,
deleteSalesData,
getSalesData,
bulkCreateSalesData,
fetchAnalytics,
getRevenueAnalytics,
getProductAnalytics,
getCustomerAnalytics,
getTimeAnalytics,
getDailyReport,
getWeeklyReport,
getMonthlyReport,
getPerformanceReport,
fetchWeatherCorrelation,
updateWeatherData,
fetchOnboardingAnalysis,
generateOnboardingReport,
importSalesData,
exportSalesData,
getImportTemplates,
clearError,
refresh,
};
};

View File

@@ -1,423 +0,0 @@
/**
* WebSocket hook for real-time bidirectional communication
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import { StorageService } from '../../services/api/utils/storage.service';
export interface WebSocketMessage {
id?: string;
type: string;
data: any;
timestamp: number;
}
interface WebSocketState {
isConnected: boolean;
isConnecting: boolean;
error: string | null;
messages: WebSocketMessage[];
readyState: number;
}
interface WebSocketOptions {
protocols?: string | string[];
reconnectInterval?: number;
maxReconnectAttempts?: number;
bufferSize?: number;
autoConnect?: boolean;
heartbeatInterval?: number;
messageFilters?: string[];
}
interface WebSocketActions {
connect: (url: string) => void;
disconnect: () => void;
reconnect: () => void;
send: (data: any, type?: string) => boolean;
sendMessage: (message: WebSocketMessage) => boolean;
clearMessages: () => void;
clearError: () => void;
addEventListener: (messageType: string, handler: (data: any) => void) => () => void;
}
const DEFAULT_OPTIONS: Required<WebSocketOptions> = {
protocols: [],
reconnectInterval: 3000,
maxReconnectAttempts: 10,
bufferSize: 100,
autoConnect: true,
heartbeatInterval: 30000,
messageFilters: [],
};
export const useWebSocket = (
initialUrl?: string,
options: WebSocketOptions = {}
): WebSocketState & WebSocketActions => {
const [state, setState] = useState<WebSocketState>({
isConnected: false,
isConnecting: false,
error: null,
messages: [],
readyState: WebSocket.CLOSED,
});
const webSocketRef = useRef<WebSocket | null>(null);
const urlRef = useRef<string | null>(initialUrl || null);
const reconnectTimeoutRef = useRef<number | null>(null);
const heartbeatTimeoutRef = useRef<number | null>(null);
const reconnectAttemptsRef = useRef<number>(0);
const messageHandlersRef = useRef<Map<string, Set<(data: any) => void>>>(new Map());
const messageQueueRef = useRef<WebSocketMessage[]>([]);
const config = { ...DEFAULT_OPTIONS, ...options };
const storageService = new StorageService();
// Helper function to get auth token
const getAuthToken = useCallback(() => {
const authData = storageService.getAuthData();
return authData?.access_token || null;
}, [storageService]);
// Add event listener for specific message types
const addEventListener = useCallback((messageType: string, handler: (data: any) => void) => {
if (!messageHandlersRef.current.has(messageType)) {
messageHandlersRef.current.set(messageType, new Set());
}
messageHandlersRef.current.get(messageType)!.add(handler);
// Return cleanup function
return () => {
const handlers = messageHandlersRef.current.get(messageType);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
messageHandlersRef.current.delete(messageType);
}
}
};
}, []);
// Process incoming message
const processMessage = useCallback((event: MessageEvent) => {
try {
let messageData: WebSocketMessage;
try {
const parsed = JSON.parse(event.data);
messageData = {
id: parsed.id,
type: parsed.type || 'message',
data: parsed.data || parsed,
timestamp: parsed.timestamp || Date.now(),
};
} catch {
messageData = {
type: 'message',
data: event.data,
timestamp: Date.now(),
};
}
// Filter messages if messageFilters is specified
if (config.messageFilters.length > 0 && !config.messageFilters.includes(messageData.type)) {
return;
}
setState(prev => ({
...prev,
messages: [...prev.messages.slice(-(config.bufferSize - 1)), messageData],
}));
// Call registered message handlers
const handlers = messageHandlersRef.current.get(messageData.type);
if (handlers) {
handlers.forEach(handler => {
try {
handler(messageData.data);
} catch (error) {
console.error('Error in WebSocket message handler:', error);
}
});
}
// Call generic message handlers
const messageHandlers = messageHandlersRef.current.get('message');
if (messageHandlers && messageData.type !== 'message') {
messageHandlers.forEach(handler => {
try {
handler(messageData);
} catch (error) {
console.error('Error in WebSocket message handler:', error);
}
});
}
} catch (error) {
console.error('Error processing WebSocket message:', error);
}
}, [config.messageFilters, config.bufferSize]);
// Send heartbeat/ping message
const sendHeartbeat = useCallback(() => {
if (webSocketRef.current && webSocketRef.current.readyState === WebSocket.OPEN) {
const heartbeatMessage: WebSocketMessage = {
type: 'ping',
data: { timestamp: Date.now() },
timestamp: Date.now(),
};
webSocketRef.current.send(JSON.stringify(heartbeatMessage));
}
}, []);
// Setup heartbeat interval
const setupHeartbeat = useCallback(() => {
if (config.heartbeatInterval > 0) {
heartbeatTimeoutRef.current = window.setInterval(sendHeartbeat, config.heartbeatInterval);
}
}, [config.heartbeatInterval, sendHeartbeat]);
// Clear heartbeat interval
const clearHeartbeat = useCallback(() => {
if (heartbeatTimeoutRef.current) {
window.clearInterval(heartbeatTimeoutRef.current);
heartbeatTimeoutRef.current = null;
}
}, []);
// Process queued messages
const processMessageQueue = useCallback(() => {
while (messageQueueRef.current.length > 0) {
const message = messageQueueRef.current.shift();
if (message && webSocketRef.current && webSocketRef.current.readyState === WebSocket.OPEN) {
webSocketRef.current.send(JSON.stringify(message));
}
}
}, []);
// Connect to WebSocket endpoint
const connect = useCallback((url: string) => {
if (webSocketRef.current) {
disconnect();
}
urlRef.current = url;
setState(prev => ({ ...prev, isConnecting: true, error: null }));
try {
// Build URL with auth token if available
const wsUrl = new URL(url);
const authToken = getAuthToken();
if (authToken) {
wsUrl.searchParams.set('token', authToken);
}
const webSocket = new WebSocket(
wsUrl.toString(),
Array.isArray(config.protocols) ? config.protocols : [config.protocols].filter(Boolean)
);
webSocketRef.current = webSocket;
webSocket.onopen = () => {
setState(prev => ({
...prev,
isConnected: true,
isConnecting: false,
error: null,
readyState: WebSocket.OPEN,
}));
reconnectAttemptsRef.current = 0;
// Clear reconnect timeout if it exists
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
// Setup heartbeat
setupHeartbeat();
// Process any queued messages
processMessageQueue();
};
webSocket.onmessage = processMessage;
webSocket.onclose = (event) => {
setState(prev => ({
...prev,
isConnected: false,
isConnecting: false,
readyState: WebSocket.CLOSED,
}));
clearHeartbeat();
// Attempt reconnection if not a clean close and within limits
if (!event.wasClean && reconnectAttemptsRef.current < config.maxReconnectAttempts) {
reconnectAttemptsRef.current += 1;
setState(prev => ({
...prev,
error: `Conexión perdida. Reintentando... (${reconnectAttemptsRef.current}/${config.maxReconnectAttempts})`,
}));
reconnectTimeoutRef.current = window.setTimeout(() => {
if (urlRef.current) {
connect(urlRef.current);
}
}, config.reconnectInterval);
} else if (reconnectAttemptsRef.current >= config.maxReconnectAttempts) {
setState(prev => ({
...prev,
error: `Máximo número de intentos de reconexión alcanzado (${config.maxReconnectAttempts})`,
}));
}
};
webSocket.onerror = (error) => {
console.error('WebSocket error:', error);
setState(prev => ({
...prev,
error: 'Error de conexión WebSocket',
readyState: webSocket.readyState,
}));
};
// Update ready state periodically
const stateInterval = setInterval(() => {
if (webSocketRef.current) {
setState(prev => ({
...prev,
readyState: webSocketRef.current!.readyState,
}));
} else {
clearInterval(stateInterval);
}
}, 1000);
} catch (error) {
setState(prev => ({
...prev,
isConnecting: false,
error: 'Error al establecer conexión WebSocket',
readyState: WebSocket.CLOSED,
}));
}
}, [getAuthToken, config.protocols, config.maxReconnectAttempts, config.reconnectInterval, processMessage, setupHeartbeat, clearHeartbeat, processMessageQueue]);
// Disconnect from WebSocket
const disconnect = useCallback(() => {
if (webSocketRef.current) {
webSocketRef.current.close(1000, 'Manual disconnect');
webSocketRef.current = null;
}
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
clearHeartbeat();
setState(prev => ({
...prev,
isConnected: false,
isConnecting: false,
readyState: WebSocket.CLOSED,
}));
reconnectAttemptsRef.current = 0;
}, [clearHeartbeat]);
// Reconnect to WebSocket
const reconnect = useCallback(() => {
if (urlRef.current) {
disconnect();
setTimeout(() => {
connect(urlRef.current!);
}, 100);
}
}, [connect, disconnect]);
// Send raw data
const send = useCallback((data: any, type: string = 'message'): boolean => {
const message: WebSocketMessage = {
type,
data,
timestamp: Date.now(),
};
return sendMessage(message);
}, []);
// Send structured message
const sendMessage = useCallback((message: WebSocketMessage): boolean => {
if (webSocketRef.current && webSocketRef.current.readyState === WebSocket.OPEN) {
try {
webSocketRef.current.send(JSON.stringify(message));
return true;
} catch (error) {
console.error('Error sending WebSocket message:', error);
return false;
}
} else {
// Queue message for later if not connected
messageQueueRef.current.push(message);
return false;
}
}, []);
// Clear messages buffer
const clearMessages = useCallback(() => {
setState(prev => ({ ...prev, messages: [] }));
}, []);
// Clear error
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
// Auto-connect on mount if URL provided
useEffect(() => {
if (initialUrl && config.autoConnect) {
connect(initialUrl);
}
return () => {
disconnect();
};
}, [initialUrl, config.autoConnect]);
// Cleanup on unmount
useEffect(() => {
return () => {
disconnect();
messageHandlersRef.current.clear();
messageQueueRef.current = [];
};
}, [disconnect]);
// Auto-reconnect when auth token changes
useEffect(() => {
if (state.isConnected && urlRef.current) {
reconnect();
}
}, [getAuthToken()]);
return {
...state,
connect,
disconnect,
reconnect,
send,
sendMessage,
clearMessages,
clearError,
addEventListener,
};
};

View File

@@ -1,621 +0,0 @@
/**
* Alerts hook for managing bakery alerts and notifications
*/
import { useState, useEffect, useCallback } from 'react';
import { InventoryService } from '../../services/api/inventory.service';
import { ProductionService } from '../../services/api/production.service';
import { NotificationService } from '../../services/api/notification.service';
export type AlertType = 'inventory' | 'production' | 'quality' | 'maintenance' | 'safety' | 'system' | 'custom';
export type AlertPriority = 'low' | 'medium' | 'high' | 'critical';
export type AlertStatus = 'active' | 'acknowledged' | 'resolved' | 'dismissed';
export interface Alert {
id: string;
type: AlertType;
priority: AlertPriority;
status: AlertStatus;
title: string;
message: string;
details?: Record<string, any>;
source: string;
sourceId?: string;
createdAt: Date;
updatedAt?: Date;
acknowledgedAt?: Date;
acknowledgedBy?: string;
resolvedAt?: Date;
resolvedBy?: string;
dismissedAt?: Date;
dismissedBy?: string;
expiresAt?: Date;
actions?: {
id: string;
label: string;
action: string;
parameters?: Record<string, any>;
}[];
metadata?: Record<string, any>;
}
export interface AlertRule {
id: string;
name: string;
type: AlertType;
priority: AlertPriority;
condition: {
field: string;
operator: 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'greater_or_equal' | 'less_or_equal' | 'contains' | 'not_contains';
value: any;
}[];
actions: string[];
enabled: boolean;
cooldownPeriod?: number; // in minutes
lastTriggered?: Date;
}
interface AlertsState {
alerts: Alert[];
rules: AlertRule[];
unreadCount: number;
criticalCount: number;
isLoading: boolean;
error: string | null;
filters: {
types: AlertType[];
priorities: AlertPriority[];
statuses: AlertStatus[];
sources: string[];
};
}
interface AlertsActions {
// Alert Management
fetchAlerts: (filters?: Partial<AlertsState['filters']>) => Promise<void>;
acknowledgeAlert: (id: string) => Promise<boolean>;
resolveAlert: (id: string, resolution?: string) => Promise<boolean>;
dismissAlert: (id: string, reason?: string) => Promise<boolean>;
bulkAcknowledge: (ids: string[]) => Promise<boolean>;
bulkResolve: (ids: string[], resolution?: string) => Promise<boolean>;
bulkDismiss: (ids: string[], reason?: string) => Promise<boolean>;
// Alert Creation
createAlert: (alert: Omit<Alert, 'id' | 'createdAt' | 'status'>) => Promise<boolean>;
createCustomAlert: (title: string, message: string, priority?: AlertPriority, details?: Record<string, any>) => Promise<boolean>;
// Alert Rules
fetchAlertRules: () => Promise<void>;
createAlertRule: (rule: Omit<AlertRule, 'id'>) => Promise<boolean>;
updateAlertRule: (id: string, rule: Partial<AlertRule>) => Promise<boolean>;
deleteAlertRule: (id: string) => Promise<boolean>;
testAlertRule: (rule: AlertRule) => Promise<boolean>;
// Monitoring and Checks
checkInventoryAlerts: () => Promise<void>;
checkProductionAlerts: () => Promise<void>;
checkQualityAlerts: () => Promise<void>;
checkMaintenanceAlerts: () => Promise<void>;
checkSystemAlerts: () => Promise<void>;
// Analytics
getAlertAnalytics: (period: string) => Promise<any>;
getAlertTrends: (days: number) => Promise<any>;
getMostFrequentAlerts: (period: string) => Promise<any>;
// Filters and Search
setFilters: (filters: Partial<AlertsState['filters']>) => void;
searchAlerts: (query: string) => Promise<Alert[]>;
// Real-time Updates
subscribeToAlerts: (callback: (alert: Alert) => void) => () => void;
// Utilities
clearError: () => void;
refresh: () => Promise<void>;
}
export const useAlerts = (): AlertsState & AlertsActions => {
const [state, setState] = useState<AlertsState>({
alerts: [],
rules: [],
unreadCount: 0,
criticalCount: 0,
isLoading: false,
error: null,
filters: {
types: [],
priorities: [],
statuses: [],
sources: [],
},
});
const inventoryService = new InventoryService();
const productionService = new ProductionService();
const notificationService = new NotificationService();
// Fetch alerts
const fetchAlerts = useCallback(async (filters?: Partial<AlertsState['filters']>) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Combine filters
const activeFilters = { ...state.filters, ...filters };
// Fetch alerts from different sources
const [inventoryAlerts, productionAlerts, systemAlerts] = await Promise.all([
getInventoryAlerts(activeFilters),
getProductionAlerts(activeFilters),
getSystemAlerts(activeFilters),
]);
const allAlerts = [...inventoryAlerts, ...productionAlerts, ...systemAlerts]
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
// Calculate counts
const unreadCount = allAlerts.filter(alert => alert.status === 'active').length;
const criticalCount = allAlerts.filter(alert => alert.priority === 'critical' && alert.status === 'active').length;
setState(prev => ({
...prev,
alerts: allAlerts,
unreadCount,
criticalCount,
isLoading: false,
filters: activeFilters,
}));
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al cargar alertas',
}));
}
}, [state.filters]);
// Get inventory alerts
const getInventoryAlerts = async (filters: Partial<AlertsState['filters']>): Promise<Alert[]> => {
try {
const response = await inventoryService.getAlerts();
if (response.success && response.data) {
return response.data.map((alert: any) => convertToAlert(alert, 'inventory'));
}
} catch (error) {
console.error('Error fetching inventory alerts:', error);
}
return [];
};
// Get production alerts
const getProductionAlerts = async (filters: Partial<AlertsState['filters']>): Promise<Alert[]> => {
try {
const response = await productionService.getAlerts?.();
if (response?.success && response.data) {
return response.data.map((alert: any) => convertToAlert(alert, 'production'));
}
} catch (error) {
console.error('Error fetching production alerts:', error);
}
return [];
};
// Get system alerts
const getSystemAlerts = async (filters: Partial<AlertsState['filters']>): Promise<Alert[]> => {
try {
const response = await notificationService.getNotifications();
if (response.success && response.data) {
return response.data
.filter((notif: any) => notif.type === 'alert')
.map((alert: any) => convertToAlert(alert, 'system'));
}
} catch (error) {
console.error('Error fetching system alerts:', error);
}
return [];
};
// Convert API response to Alert format
const convertToAlert = (apiAlert: any, source: string): Alert => {
return {
id: apiAlert.id,
type: apiAlert.type || (source as AlertType),
priority: mapPriority(apiAlert.priority || apiAlert.severity),
status: mapStatus(apiAlert.status),
title: apiAlert.title || apiAlert.message?.substring(0, 50) || 'Alert',
message: apiAlert.message || apiAlert.description || '',
details: apiAlert.details || apiAlert.data,
source,
sourceId: apiAlert.source_id,
createdAt: new Date(apiAlert.created_at || Date.now()),
updatedAt: apiAlert.updated_at ? new Date(apiAlert.updated_at) : undefined,
acknowledgedAt: apiAlert.acknowledged_at ? new Date(apiAlert.acknowledged_at) : undefined,
acknowledgedBy: apiAlert.acknowledged_by,
resolvedAt: apiAlert.resolved_at ? new Date(apiAlert.resolved_at) : undefined,
resolvedBy: apiAlert.resolved_by,
dismissedAt: apiAlert.dismissed_at ? new Date(apiAlert.dismissed_at) : undefined,
dismissedBy: apiAlert.dismissed_by,
expiresAt: apiAlert.expires_at ? new Date(apiAlert.expires_at) : undefined,
actions: apiAlert.actions || [],
metadata: apiAlert.metadata || {},
};
};
// Map priority from different sources
const mapPriority = (priority: string): AlertPriority => {
const priorityMap: Record<string, AlertPriority> = {
'low': 'low',
'medium': 'medium',
'high': 'high',
'critical': 'critical',
'urgent': 'critical',
'warning': 'medium',
'error': 'high',
'info': 'low',
};
return priorityMap[priority?.toLowerCase()] || 'medium';
};
// Map status from different sources
const mapStatus = (status: string): AlertStatus => {
const statusMap: Record<string, AlertStatus> = {
'active': 'active',
'new': 'active',
'open': 'active',
'acknowledged': 'acknowledged',
'ack': 'acknowledged',
'resolved': 'resolved',
'closed': 'resolved',
'dismissed': 'dismissed',
'ignored': 'dismissed',
};
return statusMap[status?.toLowerCase()] || 'active';
};
// Acknowledge alert
const acknowledgeAlert = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const alert = state.alerts.find(a => a.id === id);
if (!alert) return false;
// Call appropriate service based on source
let success = false;
if (alert.source === 'inventory') {
const response = await inventoryService.markAlertAsRead(id);
success = response.success;
}
if (success) {
setState(prev => ({
...prev,
alerts: prev.alerts.map(a =>
a.id === id
? { ...a, status: 'acknowledged', acknowledgedAt: new Date() }
: a
),
unreadCount: Math.max(0, prev.unreadCount - 1),
}));
}
return success;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al confirmar alerta' }));
return false;
}
}, [state.alerts, inventoryService]);
// Resolve alert
const resolveAlert = useCallback(async (id: string, resolution?: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const alert = state.alerts.find(a => a.id === id);
if (!alert) return false;
// Call appropriate service based on source
let success = false;
if (alert.source === 'inventory') {
const response = await inventoryService.dismissAlert(id);
success = response.success;
}
if (success) {
setState(prev => ({
...prev,
alerts: prev.alerts.map(a =>
a.id === id
? { ...a, status: 'resolved', resolvedAt: new Date(), metadata: { ...a.metadata, resolution } }
: a
),
unreadCount: a.status === 'active' ? Math.max(0, prev.unreadCount - 1) : prev.unreadCount,
criticalCount: a.priority === 'critical' && a.status === 'active' ? Math.max(0, prev.criticalCount - 1) : prev.criticalCount,
}));
}
return success;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al resolver alerta' }));
return false;
}
}, [state.alerts, inventoryService]);
// Dismiss alert
const dismissAlert = useCallback(async (id: string, reason?: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const alert = state.alerts.find(a => a.id === id);
if (!alert) return false;
// Call appropriate service based on source
let success = false;
if (alert.source === 'inventory') {
const response = await inventoryService.dismissAlert(id);
success = response.success;
}
if (success) {
setState(prev => ({
...prev,
alerts: prev.alerts.map(a =>
a.id === id
? { ...a, status: 'dismissed', dismissedAt: new Date(), metadata: { ...a.metadata, dismissReason: reason } }
: a
),
}));
}
return success;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al descartar alerta' }));
return false;
}
}, [state.alerts, inventoryService]);
// Bulk acknowledge
const bulkAcknowledge = useCallback(async (ids: string[]): Promise<boolean> => {
const results = await Promise.all(ids.map(id => acknowledgeAlert(id)));
return results.every(result => result);
}, [acknowledgeAlert]);
// Bulk resolve
const bulkResolve = useCallback(async (ids: string[], resolution?: string): Promise<boolean> => {
const results = await Promise.all(ids.map(id => resolveAlert(id, resolution)));
return results.every(result => result);
}, [resolveAlert]);
// Bulk dismiss
const bulkDismiss = useCallback(async (ids: string[], reason?: string): Promise<boolean> => {
const results = await Promise.all(ids.map(id => dismissAlert(id, reason)));
return results.every(result => result);
}, [dismissAlert]);
// Create alert
const createAlert = useCallback(async (alert: Omit<Alert, 'id' | 'createdAt' | 'status'>): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const newAlert: Alert = {
...alert,
id: generateAlertId(),
status: 'active',
createdAt: new Date(),
};
setState(prev => ({
...prev,
alerts: [newAlert, ...prev.alerts],
unreadCount: prev.unreadCount + 1,
criticalCount: newAlert.priority === 'critical' ? prev.criticalCount + 1 : prev.criticalCount,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al crear alerta' }));
return false;
}
}, []);
// Create custom alert
const createCustomAlert = useCallback(async (
title: string,
message: string,
priority: AlertPriority = 'medium',
details?: Record<string, any>
): Promise<boolean> => {
return createAlert({
type: 'custom',
priority,
title,
message,
details,
source: 'user',
});
}, [createAlert]);
// Check inventory alerts
const checkInventoryAlerts = useCallback(async () => {
try {
// Check for low stock
const stockResponse = await inventoryService.getStockLevels();
if (stockResponse.success && stockResponse.data) {
const lowStockItems = stockResponse.data.filter((item: any) =>
item.current_quantity <= item.reorder_point
);
for (const item of lowStockItems) {
await createAlert({
type: 'inventory',
priority: item.current_quantity === 0 ? 'critical' : 'high',
title: 'Stock bajo',
message: `Stock bajo para ${item.ingredient?.name}: ${item.current_quantity} ${item.unit}`,
source: 'inventory',
sourceId: item.id,
details: { item },
});
}
}
// Check for expiring items
const expirationResponse = await inventoryService.getExpirationReport();
if (expirationResponse?.success && expirationResponse.data) {
const expiringItems = expirationResponse.data.expiring_soon || [];
for (const item of expiringItems) {
await createAlert({
type: 'inventory',
priority: 'medium',
title: 'Producto próximo a expirar',
message: `${item.name} expira el ${item.expiration_date}`,
source: 'inventory',
sourceId: item.id,
details: { item },
});
}
}
} catch (error) {
console.error('Error checking inventory alerts:', error);
}
}, [inventoryService, createAlert]);
// Generate unique ID
const generateAlertId = (): string => {
return `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
};
// Set filters
const setFilters = useCallback((filters: Partial<AlertsState['filters']>) => {
setState(prev => ({
...prev,
filters: { ...prev.filters, ...filters },
}));
}, []);
// Clear error
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
// Refresh alerts
const refresh = useCallback(async () => {
await fetchAlerts(state.filters);
}, [fetchAlerts, state.filters]);
// Placeholder implementations for remaining functions
const fetchAlertRules = useCallback(async () => {
setState(prev => ({ ...prev, rules: [] }));
}, []);
const createAlertRule = useCallback(async (rule: Omit<AlertRule, 'id'>): Promise<boolean> => {
return true;
}, []);
const updateAlertRule = useCallback(async (id: string, rule: Partial<AlertRule>): Promise<boolean> => {
return true;
}, []);
const deleteAlertRule = useCallback(async (id: string): Promise<boolean> => {
return true;
}, []);
const testAlertRule = useCallback(async (rule: AlertRule): Promise<boolean> => {
return true;
}, []);
const checkProductionAlerts = useCallback(async () => {
// Implementation for production alerts
}, []);
const checkQualityAlerts = useCallback(async () => {
// Implementation for quality alerts
}, []);
const checkMaintenanceAlerts = useCallback(async () => {
// Implementation for maintenance alerts
}, []);
const checkSystemAlerts = useCallback(async () => {
// Implementation for system alerts
}, []);
const getAlertAnalytics = useCallback(async (period: string): Promise<any> => {
return {};
}, []);
const getAlertTrends = useCallback(async (days: number): Promise<any> => {
return {};
}, []);
const getMostFrequentAlerts = useCallback(async (period: string): Promise<any> => {
return {};
}, []);
const searchAlerts = useCallback(async (query: string): Promise<Alert[]> => {
return state.alerts.filter(alert =>
alert.title.toLowerCase().includes(query.toLowerCase()) ||
alert.message.toLowerCase().includes(query.toLowerCase())
);
}, [state.alerts]);
const subscribeToAlerts = useCallback((callback: (alert: Alert) => void): (() => void) => {
// Implementation would set up real-time subscription
return () => {
// Cleanup subscription
};
}, []);
// Initialize alerts on mount
useEffect(() => {
fetchAlerts();
}, []);
// Set up periodic checks
useEffect(() => {
const interval = setInterval(() => {
checkInventoryAlerts();
}, 5 * 60 * 1000); // Check every 5 minutes
return () => clearInterval(interval);
}, [checkInventoryAlerts]);
return {
...state,
fetchAlerts,
acknowledgeAlert,
resolveAlert,
dismissAlert,
bulkAcknowledge,
bulkResolve,
bulkDismiss,
createAlert,
createCustomAlert,
fetchAlertRules,
createAlertRule,
updateAlertRule,
deleteAlertRule,
testAlertRule,
checkInventoryAlerts,
checkProductionAlerts,
checkQualityAlerts,
checkMaintenanceAlerts,
checkSystemAlerts,
getAlertAnalytics,
getAlertTrends,
getMostFrequentAlerts,
setFilters,
searchAlerts,
subscribeToAlerts,
clearError,
refresh,
};
};

View File

@@ -1,520 +0,0 @@
/**
* Bakery workflow hook for managing daily bakery operations and processes
*/
import { useState, useEffect, useCallback } from 'react';
import { ProductionService } from '../../services/api/production.service';
import { InventoryService } from '../../services/api/inventory.service';
import { SalesService } from '../../services/api/sales.service';
import { OrderService } from '../../services/api/order.service';
export interface WorkflowStep {
id: string;
name: string;
description: string;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
priority: 'low' | 'medium' | 'high' | 'urgent';
estimatedDuration: number; // in minutes
actualDuration?: number;
dependencies?: string[];
assignedTo?: string;
startTime?: Date;
endTime?: Date;
notes?: string;
}
export interface DailyWorkflow {
date: string;
steps: WorkflowStep[];
totalEstimatedTime: number;
totalActualTime: number;
completionRate: number;
status: 'not_started' | 'in_progress' | 'completed' | 'behind_schedule';
}
interface BakeryWorkflowState {
currentWorkflow: DailyWorkflow | null;
workflowHistory: DailyWorkflow[];
activeStep: WorkflowStep | null;
isLoading: boolean;
error: string | null;
}
interface BakeryWorkflowActions {
// Workflow Management
initializeDailyWorkflow: (date: string) => Promise<void>;
generateWorkflowFromForecast: (date: string) => Promise<void>;
updateWorkflow: (workflow: Partial<DailyWorkflow>) => Promise<boolean>;
// Step Management
startStep: (stepId: string) => Promise<boolean>;
completeStep: (stepId: string, notes?: string) => Promise<boolean>;
failStep: (stepId: string, reason: string) => Promise<boolean>;
skipStep: (stepId: string, reason: string) => Promise<boolean>;
updateStepProgress: (stepId: string, progress: Partial<WorkflowStep>) => Promise<boolean>;
// Workflow Templates
createWorkflowTemplate: (name: string, steps: Omit<WorkflowStep, 'id' | 'status'>[]) => Promise<boolean>;
loadWorkflowTemplate: (templateId: string, date: string) => Promise<boolean>;
getWorkflowTemplates: () => Promise<any[]>;
// Analytics and Optimization
getWorkflowAnalytics: (startDate: string, endDate: string) => Promise<any>;
getBottleneckAnalysis: (period: string) => Promise<any>;
getEfficiencyReport: (date: string) => Promise<any>;
// Real-time Updates
subscribeToWorkflowUpdates: (callback: (workflow: DailyWorkflow) => void) => () => void;
// Utilities
clearError: () => void;
refresh: () => Promise<void>;
}
export const useBakeryWorkflow = (): BakeryWorkflowState & BakeryWorkflowActions => {
const [state, setState] = useState<BakeryWorkflowState>({
currentWorkflow: null,
workflowHistory: [],
activeStep: null,
isLoading: false,
error: null,
});
const productionService = new ProductionService();
const inventoryService = new InventoryService();
const salesService = new SalesService();
const orderService = new OrderService();
// Initialize daily workflow
const initializeDailyWorkflow = useCallback(async (date: string) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Get existing workflow or create new one
const existingWorkflow = await getWorkflowForDate(date);
if (existingWorkflow) {
setState(prev => ({
...prev,
currentWorkflow: existingWorkflow,
activeStep: findActiveStep(existingWorkflow.steps),
isLoading: false,
}));
} else {
await generateWorkflowFromForecast(date);
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al inicializar flujo de trabajo diario',
}));
}
}, []);
// Generate workflow from forecast and orders
const generateWorkflowFromForecast = useCallback(async (date: string) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Get daily forecast and orders
const [forecastData, ordersData, inventoryData] = await Promise.all([
productionService.getDailyForecast?.(date),
orderService.getOrdersByDate?.(date),
inventoryService.getStockLevels(),
]);
const workflow = generateWorkflowSteps(forecastData, ordersData, inventoryData);
setState(prev => ({
...prev,
currentWorkflow: workflow,
activeStep: workflow.steps.find(step => step.status === 'pending') || null,
isLoading: false,
}));
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al generar flujo de trabajo desde predicción',
}));
}
}, [productionService, orderService, inventoryService]);
// Generate workflow steps based on data
const generateWorkflowSteps = (forecastData: any, ordersData: any, inventoryData: any): DailyWorkflow => {
const steps: WorkflowStep[] = [
// Morning prep
{
id: 'morning_prep',
name: 'Preparación matutina',
description: 'Verificar equipos, ingredientes y planificación del día',
status: 'pending',
priority: 'high',
estimatedDuration: 30,
},
// Inventory check
{
id: 'inventory_check',
name: 'Control de inventario',
description: 'Verificar niveles de stock y calidad de ingredientes',
status: 'pending',
priority: 'high',
estimatedDuration: 20,
dependencies: ['morning_prep'],
},
// Production preparation
{
id: 'production_prep',
name: 'Preparación de producción',
description: 'Preparar ingredientes y configurar equipos',
status: 'pending',
priority: 'high',
estimatedDuration: 45,
dependencies: ['inventory_check'],
},
// Production batches (generated from forecast)
...generateProductionSteps(forecastData),
// Quality control
{
id: 'quality_control',
name: 'Control de calidad',
description: 'Verificar calidad de productos terminados',
status: 'pending',
priority: 'medium',
estimatedDuration: 30,
dependencies: ['production_prep'],
},
// Order fulfillment
...generateOrderSteps(ordersData),
// End of day cleanup
{
id: 'cleanup',
name: 'Limpieza final',
description: 'Limpieza de equipos y área de trabajo',
status: 'pending',
priority: 'medium',
estimatedDuration: 45,
},
// Daily reporting
{
id: 'daily_report',
name: 'Reporte diario',
description: 'Completar reportes de producción y ventas',
status: 'pending',
priority: 'low',
estimatedDuration: 15,
dependencies: ['cleanup'],
},
];
const totalEstimatedTime = steps.reduce((total, step) => total + step.estimatedDuration, 0);
return {
date: new Date().toISOString().split('T')[0],
steps,
totalEstimatedTime,
totalActualTime: 0,
completionRate: 0,
status: 'not_started',
};
};
// Generate production steps from forecast
const generateProductionSteps = (forecastData: any): WorkflowStep[] => {
if (!forecastData || !forecastData.products) return [];
return forecastData.products.map((product: any, index: number) => ({
id: `production_${product.id}`,
name: `Producir ${product.name}`,
description: `Producir ${product.estimated_quantity} unidades de ${product.name}`,
status: 'pending' as const,
priority: product.priority || 'medium' as const,
estimatedDuration: product.production_time || 60,
dependencies: ['production_prep'],
}));
};
// Generate order fulfillment steps
const generateOrderSteps = (ordersData: any): WorkflowStep[] => {
if (!ordersData || !ordersData.length) return [];
const specialOrders = ordersData.filter((order: any) => order.type === 'special' || order.priority === 'high');
return specialOrders.map((order: any) => ({
id: `order_${order.id}`,
name: `Preparar pedido especial`,
description: `Preparar pedido #${order.id} - ${order.items?.length || 0} items`,
status: 'pending' as const,
priority: order.priority || 'medium' as const,
estimatedDuration: order.preparation_time || 30,
dependencies: ['production_prep'],
}));
};
// Find active step
const findActiveStep = (steps: WorkflowStep[]): WorkflowStep | null => {
return steps.find(step => step.status === 'in_progress') ||
steps.find(step => step.status === 'pending') ||
null;
};
// Get workflow for specific date
const getWorkflowForDate = async (date: string): Promise<DailyWorkflow | null> => {
// This would typically fetch from an API
// For now, return null to force generation
return null;
};
// Start a workflow step
const startStep = useCallback(async (stepId: string): Promise<boolean> => {
if (!state.currentWorkflow) return false;
try {
const updatedWorkflow = {
...state.currentWorkflow,
steps: state.currentWorkflow.steps.map(step =>
step.id === stepId
? { ...step, status: 'in_progress' as const, startTime: new Date() }
: step
),
};
setState(prev => ({
...prev,
currentWorkflow: updatedWorkflow,
activeStep: updatedWorkflow.steps.find(step => step.id === stepId) || null,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al iniciar paso del flujo de trabajo' }));
return false;
}
}, [state.currentWorkflow]);
// Complete a workflow step
const completeStep = useCallback(async (stepId: string, notes?: string): Promise<boolean> => {
if (!state.currentWorkflow) return false;
try {
const step = state.currentWorkflow.steps.find(s => s.id === stepId);
if (!step || !step.startTime) return false;
const endTime = new Date();
const actualDuration = Math.round((endTime.getTime() - step.startTime.getTime()) / 60000);
const updatedWorkflow = {
...state.currentWorkflow,
steps: state.currentWorkflow.steps.map(s =>
s.id === stepId
? {
...s,
status: 'completed' as const,
endTime,
actualDuration,
notes: notes || s.notes,
}
: s
),
};
// Update completion rate
const completedSteps = updatedWorkflow.steps.filter(s => s.status === 'completed');
updatedWorkflow.completionRate = (completedSteps.length / updatedWorkflow.steps.length) * 100;
updatedWorkflow.totalActualTime = updatedWorkflow.steps
.filter(s => s.actualDuration)
.reduce((total, s) => total + (s.actualDuration || 0), 0);
setState(prev => ({
...prev,
currentWorkflow: updatedWorkflow,
activeStep: findActiveStep(updatedWorkflow.steps),
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al completar paso del flujo de trabajo' }));
return false;
}
}, [state.currentWorkflow]);
// Fail a workflow step
const failStep = useCallback(async (stepId: string, reason: string): Promise<boolean> => {
if (!state.currentWorkflow) return false;
try {
const updatedWorkflow = {
...state.currentWorkflow,
steps: state.currentWorkflow.steps.map(step =>
step.id === stepId
? { ...step, status: 'failed' as const, notes: reason }
: step
),
};
setState(prev => ({
...prev,
currentWorkflow: updatedWorkflow,
activeStep: findActiveStep(updatedWorkflow.steps),
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al marcar paso como fallido' }));
return false;
}
}, [state.currentWorkflow]);
// Skip a workflow step
const skipStep = useCallback(async (stepId: string, reason: string): Promise<boolean> => {
if (!state.currentWorkflow) return false;
try {
const updatedWorkflow = {
...state.currentWorkflow,
steps: state.currentWorkflow.steps.map(step =>
step.id === stepId
? { ...step, status: 'completed' as const, notes: `Omitido: ${reason}` }
: step
),
};
setState(prev => ({
...prev,
currentWorkflow: updatedWorkflow,
activeStep: findActiveStep(updatedWorkflow.steps),
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al omitir paso del flujo de trabajo' }));
return false;
}
}, [state.currentWorkflow]);
// Update workflow
const updateWorkflow = useCallback(async (workflow: Partial<DailyWorkflow>): Promise<boolean> => {
if (!state.currentWorkflow) return false;
try {
const updatedWorkflow = { ...state.currentWorkflow, ...workflow };
setState(prev => ({
...prev,
currentWorkflow: updatedWorkflow,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al actualizar flujo de trabajo' }));
return false;
}
}, [state.currentWorkflow]);
// Update step progress
const updateStepProgress = useCallback(async (stepId: string, progress: Partial<WorkflowStep>): Promise<boolean> => {
if (!state.currentWorkflow) return false;
try {
const updatedWorkflow = {
...state.currentWorkflow,
steps: state.currentWorkflow.steps.map(step =>
step.id === stepId ? { ...step, ...progress } : step
),
};
setState(prev => ({
...prev,
currentWorkflow: updatedWorkflow,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al actualizar progreso del paso' }));
return false;
}
}, [state.currentWorkflow]);
// Placeholder implementations for template and analytics functions
const createWorkflowTemplate = useCallback(async (name: string, steps: Omit<WorkflowStep, 'id' | 'status'>[]): Promise<boolean> => {
// Implementation would save template to backend
return true;
}, []);
const loadWorkflowTemplate = useCallback(async (templateId: string, date: string): Promise<boolean> => {
// Implementation would load template from backend
return true;
}, []);
const getWorkflowTemplates = useCallback(async (): Promise<any[]> => {
// Implementation would fetch templates from backend
return [];
}, []);
const getWorkflowAnalytics = useCallback(async (startDate: string, endDate: string): Promise<any> => {
// Implementation would fetch analytics from backend
return {};
}, []);
const getBottleneckAnalysis = useCallback(async (period: string): Promise<any> => {
// Implementation would analyze bottlenecks
return {};
}, []);
const getEfficiencyReport = useCallback(async (date: string): Promise<any> => {
// Implementation would generate efficiency report
return {};
}, []);
const subscribeToWorkflowUpdates = useCallback((callback: (workflow: DailyWorkflow) => void): (() => void) => {
// Implementation would set up real-time subscription
return () => {
// Cleanup subscription
};
}, []);
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
const refresh = useCallback(async () => {
if (state.currentWorkflow) {
await initializeDailyWorkflow(state.currentWorkflow.date);
}
}, [state.currentWorkflow, initializeDailyWorkflow]);
// Initialize workflow for today on mount
useEffect(() => {
const today = new Date().toISOString().split('T')[0];
initializeDailyWorkflow(today);
}, [initializeDailyWorkflow]);
return {
...state,
initializeDailyWorkflow,
generateWorkflowFromForecast,
updateWorkflow,
startStep,
completeStep,
failStep,
skipStep,
updateStepProgress,
createWorkflowTemplate,
loadWorkflowTemplate,
getWorkflowTemplates,
getWorkflowAnalytics,
getBottleneckAnalysis,
getEfficiencyReport,
subscribeToWorkflowUpdates,
clearError,
refresh,
};
};

View File

@@ -4,23 +4,26 @@
import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { inventoryService } from '../../services/api/inventory.service';
import { salesService } from '../../services/api/sales.service';
import { authService } from '../../services/api/auth.service';
import { tenantService } from '../../services/api/tenant.service';
import { useAuthUser } from '../../stores/auth.store';
import { useAlertActions } from '../../stores/alerts.store';
import {
ProductSuggestion,
ProductSuggestionsResponse,
InventoryCreationResponse
} from '../../types/inventory.types';
import {
BusinessModelGuide,
BusinessModelType,
TemplateData
} from '../../types/sales.types';
import { OnboardingStatus } from '../../types/auth.types';
import {
// Auth hooks
useAuthProfile,
// Tenant hooks
useRegisterBakery,
// Sales hooks
useValidateSalesRecord,
// Inventory hooks
useClassifyProductsBatch,
useCreateIngredient,
// Classification hooks
useBusinessModelAnalysis,
// Types
type User,
type BakeryRegistration,
type ProductSuggestionResponse,
type BusinessModelAnalysisResponse,
type ProductClassificationRequest,
} from '../../api';
export interface OnboardingStep {
id: string;
@@ -33,16 +36,7 @@ export interface OnboardingStep {
export interface OnboardingData {
// Step 1: Setup
bakery?: {
name: string;
business_model: BusinessModelType;
address: string;
city: string;
postal_code: string;
phone: string;
email?: string;
description?: string;
};
bakery?: BakeryRegistration;
// Step 2: Data Processing
files?: {
@@ -64,8 +58,8 @@ export interface OnboardingData {
};
// Step 3: Review
suggestions?: ProductSuggestion[];
approvedSuggestions?: ProductSuggestion[];
suggestions?: ProductSuggestionResponse[];
approvedSuggestions?: ProductSuggestionResponse[];
reviewCompleted?: boolean;
// Step 4: Inventory
@@ -99,7 +93,7 @@ interface OnboardingState {
isLoading: boolean;
error: string | null;
isInitialized: boolean;
onboardingStatus: OnboardingStatus | null;
onboardingStatus: User['onboarding_status'] | null;
}
interface OnboardingActions {
@@ -113,12 +107,12 @@ interface OnboardingActions {
validateCurrentStep: () => string | null;
// Step-specific Actions
createTenant: (bakeryData: OnboardingData['bakery']) => Promise<boolean>;
createTenant: (bakeryData: BakeryRegistration) => Promise<boolean>;
processSalesFile: (file: File, onProgress: (progress: number, stage: string, message: string) => void) => Promise<boolean>;
generateInventorySuggestions: (productList: string[]) => Promise<ProductSuggestionsResponse | null>;
createInventoryFromSuggestions: (suggestions: ProductSuggestion[]) => Promise<InventoryCreationResponse | null>;
getBusinessModelGuide: (model: BusinessModelType) => Promise<BusinessModelGuide | null>;
downloadTemplate: (templateData: TemplateData, filename: string, format?: 'csv' | 'json') => void;
generateInventorySuggestions: (productList: string[]) => Promise<ProductSuggestionResponse[] | null>;
createInventoryFromSuggestions: (suggestions: ProductSuggestionResponse[]) => Promise<any | null>;
getBusinessModelGuide: (model: string) => Promise<BusinessModelAnalysisResponse | null>;
downloadTemplate: (templateData: any, filename: string, format?: 'csv' | 'json') => void;
// Completion
completeOnboarding: () => Promise<boolean>;
@@ -138,7 +132,7 @@ const DEFAULT_STEPS: OnboardingStep[] = [
isCompleted: false,
validation: (data: OnboardingData) => {
if (!data.bakery?.name) return 'El nombre de la panadería es requerido';
if (!data.bakery?.business_model) return 'El modelo de negocio es requerido';
if (!data.bakery?.business_type) return 'El tipo de negocio es requerido';
if (!data.bakery?.address) return 'La dirección es requerida';
if (!data.bakery?.city) return 'La ciudad es requerida';
if (!data.bakery?.postal_code) return 'El código postal es requerido';
@@ -221,7 +215,13 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
const navigate = useNavigate();
const user = useAuthUser();
const { createAlert } = useAlertActions();
// React Query hooks
const { data: profile } = useAuthProfile();
const registerBakeryMutation = useRegisterBakery();
const validateSalesMutation = useValidateSalesRecord();
const classifyProductsMutation = useClassifyProductsBatch();
const businessModelMutation = useBusinessModelAnalysis();
// Initialize onboarding status
useEffect(() => {
@@ -236,14 +236,7 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
const validation = validateCurrentStep();
if (validation) {
createAlert({
type: 'error',
category: 'validation',
priority: 'high',
title: 'Validación fallida',
message: validation,
source: 'onboarding'
});
setState(prev => ({ ...prev, error: validation }));
return false;
}
@@ -302,54 +295,25 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
}, [state.currentStep, state.steps, state.data]);
// Step-specific Actions
const createTenant = useCallback(async (bakeryData: OnboardingData['bakery']): Promise<boolean> => {
const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise<boolean> => {
if (!bakeryData) return false;
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await tenantService.createTenant({
name: bakeryData.name,
description: bakeryData.description || '',
business_type: bakeryData.business_model,
settings: {
address: bakeryData.address,
city: bakeryData.city,
postal_code: bakeryData.postal_code,
phone: bakeryData.phone,
email: bakeryData.email,
}
await registerBakeryMutation.mutateAsync({
bakeryData
});
if (response.success) {
updateStepData('setup', { bakery: bakeryData });
createAlert({
type: 'success',
category: 'system',
priority: 'medium',
title: 'Tenant creado',
message: 'Tu panadería ha sido configurada exitosamente',
source: 'onboarding'
});
setState(prev => ({ ...prev, isLoading: false }));
return true;
} else {
throw new Error(response.error || 'Error creating tenant');
}
updateStepData('setup', { bakery: bakeryData });
setState(prev => ({ ...prev, isLoading: false }));
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
setState(prev => ({ ...prev, isLoading: false, error: errorMessage }));
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al crear tenant',
message: errorMessage,
source: 'onboarding'
});
return false;
}
}, [updateStepData, createAlert]);
}, [updateStepData, registerBakeryMutation]);
const processSalesFile = useCallback(async (
file: File,
@@ -360,16 +324,19 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
try {
// Stage 1: Validate file
onProgress(20, 'validating', 'Validando estructura del archivo...');
const validationResult = await salesService.validateSalesData(file);
const validationResult = await validateSalesMutation.mutateAsync({
// Convert file to the expected format for validation
data: {
// This would need to be adapted based on the actual API structure
file_data: file
}
});
onProgress(40, 'validating', 'Verificando integridad de datos...');
if (!validationResult.is_valid) {
throw new Error('Archivo de datos inválido');
}
if (!validationResult.product_list || validationResult.product_list.length === 0) {
throw new Error('No se encontraron productos en el archivo');
if (!validationResult || !validationResult.product_list || validationResult.product_list.length === 0) {
throw new Error('No se encontraron productos válidos en el archivo');
}
// Stage 2: Generate AI suggestions
@@ -384,7 +351,7 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
files: { salesData: file },
processingStage: 'completed',
processingResults: validationResult,
suggestions: suggestions?.suggestions || []
suggestions: suggestions || []
});
setState(prev => ({ ...prev, isLoading: false }));
@@ -402,36 +369,48 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
}));
return false;
}
}, [updateStepData]);
}, [updateStepData, validateSalesMutation]);
const generateInventorySuggestions = useCallback(async (productList: string[]): Promise<ProductSuggestionsResponse | null> => {
const generateInventorySuggestions = useCallback(async (productList: string[]): Promise<ProductSuggestionResponse[] | null> => {
try {
const response = await inventoryService.generateInventorySuggestions(productList);
return response.success ? response.data : null;
const response = await classifyProductsMutation.mutateAsync({
products: productList.map(name => ({ name, description: '' }))
});
return response.suggestions || [];
} catch (error) {
console.error('Error generating inventory suggestions:', error);
return null;
}
}, []);
}, [classifyProductsMutation]);
const createInventoryFromSuggestions = useCallback(async (suggestions: ProductSuggestion[]): Promise<InventoryCreationResponse | null> => {
const createInventoryFromSuggestions = useCallback(async (suggestions: ProductSuggestionResponse[]): Promise<any | null> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await inventoryService.createInventoryFromSuggestions(suggestions);
// Create ingredients from approved suggestions
const createdItems = [];
const inventoryMapping: { [key: string]: string } = {};
if (response.success) {
updateStepData('inventory', {
inventoryItems: response.data.created_items,
inventoryMapping: response.data.inventory_mapping,
inventoryConfigured: true
for (const suggestion of suggestions) {
// This would need to be adapted based on actual API structure
const createdItem = await useCreateIngredient().mutateAsync({
name: suggestion.name,
category: suggestion.category,
// Map other suggestion properties to ingredient properties
});
setState(prev => ({ ...prev, isLoading: false }));
return response.data;
} else {
throw new Error(response.error || 'Error creating inventory');
createdItems.push(createdItem);
inventoryMapping[suggestion.name] = createdItem.id;
}
updateStepData('inventory', {
inventoryItems: createdItems,
inventoryMapping,
inventoryConfigured: true
});
setState(prev => ({ ...prev, isLoading: false }));
return { created_items: createdItems, inventory_mapping: inventoryMapping };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error creating inventory';
setState(prev => ({ ...prev, isLoading: false, error: errorMessage }));
@@ -439,28 +418,49 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
}
}, [updateStepData]);
const getBusinessModelGuide = useCallback(async (model: BusinessModelType): Promise<BusinessModelGuide | null> => {
const getBusinessModelGuide = useCallback(async (model: string): Promise<BusinessModelAnalysisResponse | null> => {
try {
const response = await salesService.getBusinessModelGuide(model);
return response.success ? response.data : null;
const response = await businessModelMutation.mutateAsync({
business_model: model,
// Include any other required parameters
});
return response;
} catch (error) {
console.error('Error getting business model guide:', error);
return null;
}
}, [businessModelMutation]);
const downloadTemplate = useCallback((templateData: any, filename: string, format: 'csv' | 'json' = 'csv') => {
// Create and download template file
const content = format === 'json' ? JSON.stringify(templateData, null, 2) : convertToCSV(templateData);
const blob = new Blob([content], { type: format === 'json' ? 'application/json' : 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${filename}.${format}`;
a.click();
URL.revokeObjectURL(url);
}, []);
const downloadTemplate = useCallback((templateData: TemplateData, filename: string, format: 'csv' | 'json' = 'csv') => {
salesService.downloadTemplate(templateData, filename, format);
}, []);
const convertToCSV = (data: any): string => {
// Simple CSV conversion - this should be adapted based on the actual data structure
if (Array.isArray(data)) {
const headers = Object.keys(data[0] || {}).join(',');
const rows = data.map(item => Object.values(item).join(',')).join('\n');
return `${headers}\n${rows}`;
}
return JSON.stringify(data);
};
const checkOnboardingStatus = useCallback(async () => {
setState(prev => ({ ...prev, isLoading: true }));
try {
const response = await authService.checkOnboardingStatus();
// Use the profile data to get onboarding status
setState(prev => ({
...prev,
onboardingStatus: response.success ? response.data : null,
onboardingStatus: profile?.onboarding_status || null,
isInitialized: true,
isLoading: false
}));
@@ -471,58 +471,33 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
isLoading: false
}));
}
}, []);
}, [profile]);
const completeOnboarding = useCallback(async (): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await authService.completeOnboarding({
completedAt: new Date().toISOString(),
data: state.data
});
// Mark onboarding as completed - this would typically involve an API call
// For now, we'll simulate success and navigate to dashboard
setState(prev => ({
...prev,
isLoading: false,
steps: prev.steps.map(step => ({ ...step, isCompleted: true }))
}));
if (response.success) {
createAlert({
type: 'success',
category: 'system',
priority: 'high',
title: '¡Onboarding completado!',
message: 'Has completado exitosamente la configuración inicial',
source: 'onboarding'
});
// Navigate to dashboard after a short delay
setTimeout(() => {
navigate('/app/dashboard');
}, 2000);
setState(prev => ({
...prev,
isLoading: false,
steps: prev.steps.map(step => ({ ...step, isCompleted: true }))
}));
// Navigate to dashboard after a short delay
setTimeout(() => {
navigate('/app/dashboard');
}, 2000);
return true;
} else {
throw new Error(response.error || 'Error completing onboarding');
}
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error completing onboarding';
setState(prev => ({ ...prev, isLoading: false, error: errorMessage }));
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al completar onboarding',
message: errorMessage,
source: 'onboarding'
});
return false;
}
}, [state.data, createAlert, navigate]);
}, [state.data, navigate]);
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));

View File

@@ -1,655 +0,0 @@
/**
* Production schedule hook for managing bakery production scheduling and capacity planning
*/
import { useState, useEffect, useCallback } from 'react';
import { ProductionService } from '../../services/api/production.service';
import { InventoryService } from '../../services/api/inventory.service';
import { ForecastingService } from '../../services/api/forecasting.service';
export interface ScheduleItem {
id: string;
recipeId: string;
recipeName: string;
quantity: number;
priority: 'low' | 'medium' | 'high' | 'urgent';
estimatedStartTime: Date;
estimatedEndTime: Date;
actualStartTime?: Date;
actualEndTime?: Date;
status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled' | 'delayed';
assignedEquipment?: string[];
assignedStaff?: string[];
requiredIngredients: {
ingredientId: string;
ingredientName: string;
requiredQuantity: number;
availableQuantity: number;
unit: string;
}[];
notes?: string;
dependencies?: string[];
}
export interface ProductionSlot {
startTime: Date;
endTime: Date;
isAvailable: boolean;
assignedItems: ScheduleItem[];
capacity: number;
utilizationRate: number;
}
export interface DailySchedule {
date: string;
items: ScheduleItem[];
slots: ProductionSlot[];
totalCapacity: number;
totalUtilization: number;
efficiency: number;
bottlenecks: string[];
}
interface ProductionScheduleState {
currentSchedule: DailySchedule | null;
scheduleHistory: DailySchedule[];
availableRecipes: any[];
equipmentStatus: any[];
staffAvailability: any[];
isLoading: boolean;
error: string | null;
constraints: {
maxDailyCapacity: number;
workingHours: { start: string; end: string };
equipmentLimitations: Record<string, number>;
staffLimitations: Record<string, number>;
};
}
interface ProductionScheduleActions {
// Schedule Management
loadSchedule: (date: string) => Promise<void>;
createSchedule: (date: string) => Promise<void>;
updateSchedule: (schedule: Partial<DailySchedule>) => Promise<boolean>;
// Schedule Items
addScheduleItem: (item: Omit<ScheduleItem, 'id'>) => Promise<boolean>;
updateScheduleItem: (id: string, item: Partial<ScheduleItem>) => Promise<boolean>;
removeScheduleItem: (id: string) => Promise<boolean>;
moveScheduleItem: (id: string, newStartTime: Date) => Promise<boolean>;
// Automatic Scheduling
autoSchedule: (date: string, items: Omit<ScheduleItem, 'id'>[]) => Promise<boolean>;
optimizeSchedule: (date: string) => Promise<boolean>;
generateFromForecast: (date: string) => Promise<boolean>;
// Capacity Management
checkCapacity: (date: string, newItem: Omit<ScheduleItem, 'id'>) => Promise<{ canSchedule: boolean; suggestedTime?: Date; conflicts?: string[] }>;
getAvailableSlots: (date: string, duration: number) => Promise<ProductionSlot[]>;
calculateUtilization: (date: string) => Promise<number>;
// Resource Management
checkIngredientAvailability: (items: ScheduleItem[]) => Promise<{ available: boolean; shortages: any[] }>;
checkEquipmentAvailability: (date: string, equipment: string[], timeSlot: { start: Date; end: Date }) => Promise<boolean>;
checkStaffAvailability: (date: string, staff: string[], timeSlot: { start: Date; end: Date }) => Promise<boolean>;
// Analytics and Optimization
getScheduleAnalytics: (startDate: string, endDate: string) => Promise<any>;
getBottleneckAnalysis: (date: string) => Promise<any>;
getEfficiencyReport: (period: string) => Promise<any>;
predictDelays: (date: string) => Promise<any>;
// Templates and Presets
saveScheduleTemplate: (name: string, template: Omit<ScheduleItem, 'id'>[]) => Promise<boolean>;
loadScheduleTemplate: (templateId: string, date: string) => Promise<boolean>;
getScheduleTemplates: () => Promise<any[]>;
// Utilities
clearError: () => void;
refresh: () => Promise<void>;
}
export const useProductionSchedule = (): ProductionScheduleState & ProductionScheduleActions => {
const [state, setState] = useState<ProductionScheduleState>({
currentSchedule: null,
scheduleHistory: [],
availableRecipes: [],
equipmentStatus: [],
staffAvailability: [],
isLoading: false,
error: null,
constraints: {
maxDailyCapacity: 8 * 60, // 8 hours in minutes
workingHours: { start: '06:00', end: '20:00' },
equipmentLimitations: {},
staffLimitations: {},
},
});
const productionService = new ProductionService();
const inventoryService = new InventoryService();
const forecastingService = new ForecastingService();
// Load schedule for specific date
const loadSchedule = useCallback(async (date: string) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Get schedule data from API
const scheduleData = await getScheduleFromAPI(date);
if (scheduleData) {
setState(prev => ({
...prev,
currentSchedule: scheduleData,
isLoading: false,
}));
} else {
// Create new schedule if none exists
await createSchedule(date);
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al cargar programación de producción',
}));
}
}, []);
// Create new schedule
const createSchedule = useCallback(async (date: string) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const workingHours = generateWorkingHours(date, state.constraints.workingHours);
const slots = generateTimeSlots(workingHours, 30); // 30-minute slots
const newSchedule: DailySchedule = {
date,
items: [],
slots,
totalCapacity: state.constraints.maxDailyCapacity,
totalUtilization: 0,
efficiency: 0,
bottlenecks: [],
};
setState(prev => ({
...prev,
currentSchedule: newSchedule,
isLoading: false,
}));
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al crear nueva programación',
}));
}
}, [state.constraints]);
// Generate working hours for a date
const generateWorkingHours = (date: string, workingHours: { start: string; end: string }) => {
const startTime = new Date(`${date}T${workingHours.start}`);
const endTime = new Date(`${date}T${workingHours.end}`);
return { startTime, endTime };
};
// Generate time slots
const generateTimeSlots = (workingHours: { startTime: Date; endTime: Date }, slotDuration: number): ProductionSlot[] => {
const slots: ProductionSlot[] = [];
const current = new Date(workingHours.startTime);
while (current < workingHours.endTime) {
const slotEnd = new Date(current.getTime() + slotDuration * 60000);
slots.push({
startTime: new Date(current),
endTime: slotEnd,
isAvailable: true,
assignedItems: [],
capacity: 1,
utilizationRate: 0,
});
current.setTime(slotEnd.getTime());
}
return slots;
};
// Add schedule item
const addScheduleItem = useCallback(async (item: Omit<ScheduleItem, 'id'>): Promise<boolean> => {
if (!state.currentSchedule) return false;
setState(prev => ({ ...prev, error: null }));
try {
// Check capacity and resources
const capacityCheck = await checkCapacity(state.currentSchedule.date, item);
if (!capacityCheck.canSchedule) {
setState(prev => ({
...prev,
error: `No se puede programar: ${capacityCheck.conflicts?.join(', ')}`,
}));
return false;
}
const newItem: ScheduleItem = {
...item,
id: generateScheduleItemId(),
};
const updatedSchedule = {
...state.currentSchedule,
items: [...state.currentSchedule.items, newItem],
};
// Recalculate utilization and efficiency
recalculateScheduleMetrics(updatedSchedule);
setState(prev => ({
...prev,
currentSchedule: updatedSchedule,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al agregar item a la programación' }));
return false;
}
}, [state.currentSchedule]);
// Update schedule item
const updateScheduleItem = useCallback(async (id: string, item: Partial<ScheduleItem>): Promise<boolean> => {
if (!state.currentSchedule) return false;
try {
const updatedSchedule = {
...state.currentSchedule,
items: state.currentSchedule.items.map(scheduleItem =>
scheduleItem.id === id ? { ...scheduleItem, ...item } : scheduleItem
),
};
recalculateScheduleMetrics(updatedSchedule);
setState(prev => ({
...prev,
currentSchedule: updatedSchedule,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al actualizar item de programación' }));
return false;
}
}, [state.currentSchedule]);
// Remove schedule item
const removeScheduleItem = useCallback(async (id: string): Promise<boolean> => {
if (!state.currentSchedule) return false;
try {
const updatedSchedule = {
...state.currentSchedule,
items: state.currentSchedule.items.filter(item => item.id !== id),
};
recalculateScheduleMetrics(updatedSchedule);
setState(prev => ({
...prev,
currentSchedule: updatedSchedule,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al eliminar item de programación' }));
return false;
}
}, [state.currentSchedule]);
// Move schedule item to new time
const moveScheduleItem = useCallback(async (id: string, newStartTime: Date): Promise<boolean> => {
if (!state.currentSchedule) return false;
const item = state.currentSchedule.items.find(item => item.id === id);
if (!item) return false;
const duration = item.estimatedEndTime.getTime() - item.estimatedStartTime.getTime();
const newEndTime = new Date(newStartTime.getTime() + duration);
return updateScheduleItem(id, {
estimatedStartTime: newStartTime,
estimatedEndTime: newEndTime,
});
}, [state.currentSchedule, updateScheduleItem]);
// Auto-schedule items
const autoSchedule = useCallback(async (date: string, items: Omit<ScheduleItem, 'id'>[]): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Sort items by priority and estimated duration
const sortedItems = [...items].sort((a, b) => {
const priorityOrder = { urgent: 4, high: 3, medium: 2, low: 1 };
return priorityOrder[b.priority] - priorityOrder[a.priority];
});
const schedule = await createOptimalSchedule(date, sortedItems);
setState(prev => ({
...prev,
currentSchedule: schedule,
isLoading: false,
}));
return true;
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al programar automáticamente',
}));
return false;
}
}, []);
// Create optimal schedule
const createOptimalSchedule = async (date: string, items: Omit<ScheduleItem, 'id'>[]): Promise<DailySchedule> => {
const workingHours = generateWorkingHours(date, state.constraints.workingHours);
const slots = generateTimeSlots(workingHours, 30);
const scheduledItems: ScheduleItem[] = [];
let currentTime = new Date(workingHours.startTime);
for (const item of items) {
const duration = item.estimatedEndTime.getTime() - item.estimatedStartTime.getTime();
const endTime = new Date(currentTime.getTime() + duration);
// Check if item fits in remaining time
if (endTime <= workingHours.endTime) {
scheduledItems.push({
...item,
id: generateScheduleItemId(),
estimatedStartTime: new Date(currentTime),
estimatedEndTime: endTime,
});
currentTime = endTime;
}
}
const schedule: DailySchedule = {
date,
items: scheduledItems,
slots,
totalCapacity: state.constraints.maxDailyCapacity,
totalUtilization: 0,
efficiency: 0,
bottlenecks: [],
};
recalculateScheduleMetrics(schedule);
return schedule;
};
// Check capacity for new item
const checkCapacity = useCallback(async (date: string, newItem: Omit<ScheduleItem, 'id'>) => {
const conflicts: string[] = [];
let canSchedule = true;
// Check time conflicts
if (state.currentSchedule) {
const hasTimeConflict = state.currentSchedule.items.some(item => {
return (newItem.estimatedStartTime < item.estimatedEndTime &&
newItem.estimatedEndTime > item.estimatedStartTime);
});
if (hasTimeConflict) {
conflicts.push('Conflicto de horario');
canSchedule = false;
}
}
// Check ingredient availability
const ingredientCheck = await checkIngredientAvailability([newItem as ScheduleItem]);
if (!ingredientCheck.available) {
conflicts.push('Ingredientes insuficientes');
canSchedule = false;
}
return { canSchedule, conflicts };
}, [state.currentSchedule]);
// Get available slots
const getAvailableSlots = useCallback(async (date: string, duration: number): Promise<ProductionSlot[]> => {
if (!state.currentSchedule) return [];
return state.currentSchedule.slots.filter(slot => {
const slotDuration = slot.endTime.getTime() - slot.startTime.getTime();
return slot.isAvailable && slotDuration >= duration * 60000;
});
}, [state.currentSchedule]);
// Check ingredient availability
const checkIngredientAvailability = useCallback(async (items: ScheduleItem[]) => {
try {
const stockLevels = await inventoryService.getStockLevels();
const shortages: any[] = [];
for (const item of items) {
for (const ingredient of item.requiredIngredients) {
const stock = stockLevels.data?.find((s: any) => s.ingredient_id === ingredient.ingredientId);
if (!stock || stock.current_quantity < ingredient.requiredQuantity) {
shortages.push({
ingredientName: ingredient.ingredientName,
required: ingredient.requiredQuantity,
available: stock?.current_quantity || 0,
});
}
}
}
return { available: shortages.length === 0, shortages };
} catch (error) {
return { available: false, shortages: [] };
}
}, [inventoryService]);
// Generate from forecast
const generateFromForecast = useCallback(async (date: string): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Get forecast data
const forecast = await forecastingService.generateDemandForecast('default', 1);
if (!forecast) {
setState(prev => ({
...prev,
isLoading: false,
error: 'No se pudo obtener predicción de demanda',
}));
return false;
}
// Convert forecast to schedule items
const items = convertForecastToScheduleItems(forecast);
// Auto-schedule the items
return await autoSchedule(date, items);
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al generar programación desde predicción',
}));
return false;
}
}, [forecastingService, autoSchedule]);
// Convert forecast to schedule items
const convertForecastToScheduleItems = (forecast: any): Omit<ScheduleItem, 'id'>[] => {
if (!forecast.products) return [];
return forecast.products.map((product: any) => ({
recipeId: product.recipe_id || `recipe_${product.id}`,
recipeName: product.name,
quantity: product.estimated_quantity || 1,
priority: 'medium' as const,
estimatedStartTime: new Date(),
estimatedEndTime: new Date(Date.now() + (product.production_time || 60) * 60000),
status: 'scheduled' as const,
requiredIngredients: product.ingredients || [],
}));
};
// Recalculate schedule metrics
const recalculateScheduleMetrics = (schedule: DailySchedule) => {
const totalScheduledTime = schedule.items.reduce((total, item) => {
return total + (item.estimatedEndTime.getTime() - item.estimatedStartTime.getTime());
}, 0);
schedule.totalUtilization = (totalScheduledTime / (schedule.totalCapacity * 60000)) * 100;
schedule.efficiency = calculateEfficiency(schedule.items);
schedule.bottlenecks = identifyBottlenecks(schedule.items);
};
// Calculate efficiency
const calculateEfficiency = (items: ScheduleItem[]): number => {
if (items.length === 0) return 0;
const completedItems = items.filter(item => item.status === 'completed');
return (completedItems.length / items.length) * 100;
};
// Identify bottlenecks
const identifyBottlenecks = (items: ScheduleItem[]): string[] => {
const bottlenecks: string[] = [];
// Check for equipment conflicts
const equipmentUsage: Record<string, number> = {};
items.forEach(item => {
item.assignedEquipment?.forEach(equipment => {
equipmentUsage[equipment] = (equipmentUsage[equipment] || 0) + 1;
});
});
Object.entries(equipmentUsage).forEach(([equipment, usage]) => {
if (usage > 1) {
bottlenecks.push(`Conflicto de equipamiento: ${equipment}`);
}
});
return bottlenecks;
};
// Generate unique ID
const generateScheduleItemId = (): string => {
return `schedule_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
};
// Get schedule from API (placeholder)
const getScheduleFromAPI = async (date: string): Promise<DailySchedule | null> => {
// This would fetch from actual API
return null;
};
// Placeholder implementations for remaining functions
const updateSchedule = useCallback(async (schedule: Partial<DailySchedule>): Promise<boolean> => {
return true;
}, []);
const optimizeSchedule = useCallback(async (date: string): Promise<boolean> => {
return true;
}, []);
const calculateUtilization = useCallback(async (date: string): Promise<number> => {
return state.currentSchedule?.totalUtilization || 0;
}, [state.currentSchedule]);
const checkEquipmentAvailability = useCallback(async (date: string, equipment: string[], timeSlot: { start: Date; end: Date }): Promise<boolean> => {
return true;
}, []);
const checkStaffAvailability = useCallback(async (date: string, staff: string[], timeSlot: { start: Date; end: Date }): Promise<boolean> => {
return true;
}, []);
const getScheduleAnalytics = useCallback(async (startDate: string, endDate: string): Promise<any> => {
return {};
}, []);
const getBottleneckAnalysis = useCallback(async (date: string): Promise<any> => {
return {};
}, []);
const getEfficiencyReport = useCallback(async (period: string): Promise<any> => {
return {};
}, []);
const predictDelays = useCallback(async (date: string): Promise<any> => {
return {};
}, []);
const saveScheduleTemplate = useCallback(async (name: string, template: Omit<ScheduleItem, 'id'>[]): Promise<boolean> => {
return true;
}, []);
const loadScheduleTemplate = useCallback(async (templateId: string, date: string): Promise<boolean> => {
return true;
}, []);
const getScheduleTemplates = useCallback(async (): Promise<any[]> => {
return [];
}, []);
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
const refresh = useCallback(async () => {
if (state.currentSchedule) {
await loadSchedule(state.currentSchedule.date);
}
}, [state.currentSchedule, loadSchedule]);
// Load today's schedule on mount
useEffect(() => {
const today = new Date().toISOString().split('T')[0];
loadSchedule(today);
}, [loadSchedule]);
return {
...state,
loadSchedule,
createSchedule,
updateSchedule,
addScheduleItem,
updateScheduleItem,
removeScheduleItem,
moveScheduleItem,
autoSchedule,
optimizeSchedule,
generateFromForecast,
checkCapacity,
getAvailableSlots,
calculateUtilization,
checkIngredientAvailability,
checkEquipmentAvailability,
checkStaffAvailability,
getScheduleAnalytics,
getBottleneckAnalysis,
getEfficiencyReport,
predictDelays,
saveScheduleTemplate,
loadScheduleTemplate,
getScheduleTemplates,
clearError,
refresh,
};
};

View File

@@ -1,18 +0,0 @@
// Mock auth hook for testing
export const useAuth = () => {
return {
user: {
id: 'user_123',
tenant_id: 'tenant_456',
email: 'user@example.com',
name: 'Usuario Demo'
},
isAuthenticated: true,
login: async (credentials: any) => {
console.log('Mock login:', credentials);
},
logout: () => {
console.log('Mock logout');
}
};
};

View File

@@ -1,313 +0,0 @@
import React, { useState } from 'react';
import { Brain, TrendingUp, AlertTriangle, Lightbulb, Target, Zap, Download, RefreshCw } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const AIInsightsPage: React.FC = () => {
const [selectedCategory, setSelectedCategory] = useState('all');
const [isRefreshing, setIsRefreshing] = useState(false);
const insights = [
{
id: '1',
type: 'optimization',
priority: 'high',
title: 'Optimización de Producción de Croissants',
description: 'La demanda de croissants aumenta un 23% los viernes. Recomendamos incrementar la producción en 15 unidades.',
impact: 'Aumento estimado de ingresos: €180/semana',
confidence: 87,
category: 'production',
timestamp: '2024-01-26 09:30',
actionable: true,
metrics: {
currentProduction: 45,
recommendedProduction: 60,
expectedIncrease: '+23%'
}
},
{
id: '2',
type: 'alert',
priority: 'medium',
title: 'Patrón de Compra en Tardes',
description: 'Los clientes compran más productos salados después de las 16:00. Considera promocionar empanadas durante estas horas.',
impact: 'Potencial aumento de ventas: 12%',
confidence: 92,
category: 'sales',
timestamp: '2024-01-26 08:45',
actionable: true,
metrics: {
afternoonSales: '+15%',
savoryProducts: '68%',
conversionRate: '12.3%'
}
},
{
id: '3',
type: 'prediction',
priority: 'high',
title: 'Predicción de Demanda de San Valentín',
description: 'Se espera un incremento del 40% en la demanda de productos de repostería especiales entre el 10-14 de febrero.',
impact: 'Preparar stock adicional de ingredientes premium',
confidence: 94,
category: 'forecasting',
timestamp: '2024-01-26 07:15',
actionable: true,
metrics: {
expectedIncrease: '+40%',
daysAhead: 18,
recommendedPrep: '3 días'
}
},
{
id: '4',
type: 'recommendation',
priority: 'low',
title: 'Optimización de Inventario de Harina',
description: 'El consumo de harina integral ha disminuido 8% este mes. Considera ajustar las órdenes de compra.',
impact: 'Reducción de desperdicios: €45/mes',
confidence: 78,
category: 'inventory',
timestamp: '2024-01-25 16:20',
actionable: false,
metrics: {
consumption: '-8%',
currentStock: '45kg',
recommendedOrder: '25kg'
}
},
{
id: '5',
type: 'insight',
priority: 'medium',
title: 'Análisis de Satisfacción del Cliente',
description: 'Los clientes valoran más la frescura (95%) que el precio (67%). Enfoque en destacar la calidad artesanal.',
impact: 'Mejorar estrategia de marketing',
confidence: 89,
category: 'customer',
timestamp: '2024-01-25 14:30',
actionable: true,
metrics: {
freshnessScore: '95%',
priceScore: '67%',
qualityScore: '91%'
}
}
];
const categories = [
{ value: 'all', label: 'Todas las Categorías', count: insights.length },
{ value: 'production', label: 'Producción', count: insights.filter(i => i.category === 'production').length },
{ value: 'sales', label: 'Ventas', count: insights.filter(i => i.category === 'sales').length },
{ value: 'forecasting', label: 'Pronósticos', count: insights.filter(i => i.category === 'forecasting').length },
{ value: 'inventory', label: 'Inventario', count: insights.filter(i => i.category === 'inventory').length },
{ value: 'customer', label: 'Clientes', count: insights.filter(i => i.category === 'customer').length },
];
const aiMetrics = {
totalInsights: insights.length,
actionableInsights: insights.filter(i => i.actionable).length,
averageConfidence: Math.round(insights.reduce((sum, i) => sum + i.confidence, 0) / insights.length),
highPriorityInsights: insights.filter(i => i.priority === 'high').length,
};
const getTypeIcon = (type: string) => {
const iconProps = { className: "w-5 h-5" };
switch (type) {
case 'optimization': return <Target {...iconProps} />;
case 'alert': return <AlertTriangle {...iconProps} />;
case 'prediction': return <TrendingUp {...iconProps} />;
case 'recommendation': return <Lightbulb {...iconProps} />;
case 'insight': return <Brain {...iconProps} />;
default: return <Brain {...iconProps} />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'red';
case 'medium': return 'yellow';
case 'low': return 'green';
default: return 'gray';
}
};
const getTypeColor = (type: string) => {
switch (type) {
case 'optimization': return 'bg-blue-100 text-blue-800';
case 'alert': return 'bg-red-100 text-red-800';
case 'prediction': return 'bg-purple-100 text-purple-800';
case 'recommendation': return 'bg-green-100 text-green-800';
case 'insight': return 'bg-orange-100 text-orange-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const filteredInsights = selectedCategory === 'all'
? insights
: insights.filter(insight => insight.category === selectedCategory);
const handleRefresh = async () => {
setIsRefreshing(true);
await new Promise(resolve => setTimeout(resolve, 2000));
setIsRefreshing(false);
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Inteligencia Artificial"
description="Insights inteligentes y recomendaciones automáticas para optimizar tu panadería"
action={
<div className="flex space-x-2">
<Button variant="outline" onClick={handleRefresh} disabled={isRefreshing}>
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
Actualizar
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* AI Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Insights</p>
<p className="text-3xl font-bold text-blue-600">{aiMetrics.totalInsights}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<Brain className="h-6 w-6 text-blue-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Accionables</p>
<p className="text-3xl font-bold text-green-600">{aiMetrics.actionableInsights}</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<Zap className="h-6 w-6 text-green-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Confianza Promedio</p>
<p className="text-3xl font-bold text-purple-600">{aiMetrics.averageConfidence}%</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Target className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Alta Prioridad</p>
<p className="text-3xl font-bold text-red-600">{aiMetrics.highPriorityInsights}</p>
</div>
<div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center">
<AlertTriangle className="h-6 w-6 text-red-600" />
</div>
</div>
</Card>
</div>
{/* Category Filter */}
<Card className="p-6">
<div className="flex flex-wrap gap-2">
{categories.map((category) => (
<button
key={category.value}
onClick={() => setSelectedCategory(category.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
selectedCategory === category.value
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{category.label} ({category.count})
</button>
))}
</div>
</Card>
{/* Insights List */}
<div className="space-y-4">
{filteredInsights.map((insight) => (
<Card key={insight.id} className="p-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-3 mb-3">
<div className={`p-2 rounded-lg ${getTypeColor(insight.type)}`}>
{getTypeIcon(insight.type)}
</div>
<div className="flex items-center space-x-2">
<Badge variant={getPriorityColor(insight.priority)}>
{insight.priority === 'high' ? 'Alta' : insight.priority === 'medium' ? 'Media' : 'Baja'} Prioridad
</Badge>
<Badge variant="gray">{insight.confidence}% confianza</Badge>
{insight.actionable && (
<Badge variant="blue">Accionable</Badge>
)}
</div>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{insight.title}</h3>
<p className="text-gray-700 mb-3">{insight.description}</p>
<p className="text-sm font-medium text-green-600 mb-4">{insight.impact}</p>
{/* Metrics */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
{Object.entries(insight.metrics).map(([key, value]) => (
<div key={key} className="bg-gray-50 p-3 rounded-lg">
<p className="text-xs text-gray-500 uppercase tracking-wider">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</p>
<p className="text-sm font-semibold text-gray-900">{value}</p>
</div>
))}
</div>
<div className="flex items-center justify-between">
<p className="text-xs text-gray-500">{insight.timestamp}</p>
{insight.actionable && (
<Button size="sm">
Aplicar Recomendación
</Button>
)}
</div>
</div>
</div>
</Card>
))}
</div>
{filteredInsights.length === 0 && (
<Card className="p-12 text-center">
<Brain className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">No hay insights disponibles</h3>
<p className="text-gray-600 mb-4">
No se encontraron insights para la categoría seleccionada.
</p>
<Button onClick={handleRefresh}>
<RefreshCw className="w-4 h-4 mr-2" />
Generar Nuevos Insights
</Button>
</Card>
)}
</div>
);
};
export default AIInsightsPage;

View File

@@ -1,385 +0,0 @@
import React, { useState } from 'react';
import { Calendar, TrendingUp, AlertTriangle, BarChart3, Download, Settings } from 'lucide-react';
import { Button, Card, Badge, Select } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { DemandChart, ForecastTable, SeasonalityIndicator, AlertsPanel } from '../../../../components/domain/forecasting';
const ForecastingPage: React.FC = () => {
const [selectedProduct, setSelectedProduct] = useState('all');
const [forecastPeriod, setForecastPeriod] = useState('7');
const [viewMode, setViewMode] = useState<'chart' | 'table'>('chart');
const forecastData = {
accuracy: 92,
totalDemand: 1247,
growthTrend: 8.5,
seasonalityFactor: 1.15,
};
const products = [
{ id: 'all', name: 'Todos los productos' },
{ id: 'bread', name: 'Panes' },
{ id: 'pastry', name: 'Bollería' },
{ id: 'cake', name: 'Tartas' },
];
const periods = [
{ value: '7', label: '7 días' },
{ value: '14', label: '14 días' },
{ value: '30', label: '30 días' },
{ value: '90', label: '3 meses' },
];
const mockForecasts = [
{
id: '1',
product: 'Pan de Molde Integral',
currentStock: 25,
forecastDemand: 45,
recommendedProduction: 50,
confidence: 95,
trend: 'up',
stockoutRisk: 'low',
},
{
id: '2',
product: 'Croissants de Mantequilla',
currentStock: 18,
forecastDemand: 32,
recommendedProduction: 35,
confidence: 88,
trend: 'stable',
stockoutRisk: 'medium',
},
{
id: '3',
product: 'Baguettes Francesas',
currentStock: 12,
forecastDemand: 28,
recommendedProduction: 30,
confidence: 91,
trend: 'down',
stockoutRisk: 'high',
},
];
const alerts = [
{
id: '1',
type: 'stockout',
product: 'Baguettes Francesas',
message: 'Alto riesgo de agotamiento en las próximas 24h',
severity: 'high',
recommendation: 'Incrementar producción en 15 unidades',
},
{
id: '2',
type: 'overstock',
product: 'Magdalenas',
message: 'Probable exceso de stock para mañana',
severity: 'medium',
recommendation: 'Reducir producción en 20%',
},
{
id: '3',
type: 'weather',
product: 'Todos',
message: 'Lluvia prevista - incremento esperado en demanda de bollería',
severity: 'info',
recommendation: 'Aumentar producción de productos de interior en 10%',
},
];
const weatherImpact = {
today: 'sunny',
temperature: 22,
demandFactor: 0.95,
affectedCategories: ['helados', 'bebidas frías'],
};
const seasonalInsights = [
{ period: 'Mañana', factor: 1.2, products: ['Pan', 'Bollería'] },
{ period: 'Tarde', factor: 0.8, products: ['Tartas', 'Dulces'] },
{ period: 'Fin de semana', factor: 1.4, products: ['Tartas especiales'] },
];
const getTrendIcon = (trend: string) => {
switch (trend) {
case 'up':
return <TrendingUp className="h-4 w-4 text-green-600" />;
case 'down':
return <TrendingUp className="h-4 w-4 text-red-600 rotate-180" />;
default:
return <div className="h-4 w-4 bg-gray-400 rounded-full" />;
}
};
const getRiskBadge = (risk: string) => {
const riskConfig = {
low: { color: 'green', text: 'Bajo' },
medium: { color: 'yellow', text: 'Medio' },
high: { color: 'red', text: 'Alto' },
};
const config = riskConfig[risk as keyof typeof riskConfig];
return <Badge variant={config?.color as any}>{config?.text}</Badge>;
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Predicción de Demanda"
description="Predicciones inteligentes basadas en IA para optimizar tu producción"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Settings className="w-4 h-4 mr-2" />
Configurar
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Precisión del Modelo</p>
<p className="text-3xl font-bold text-green-600">{forecastData.accuracy}%</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<BarChart3 className="h-6 w-6 text-green-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Demanda Prevista</p>
<p className="text-3xl font-bold text-blue-600">{forecastData.totalDemand}</p>
<p className="text-xs text-gray-500">próximos {forecastPeriod} días</p>
</div>
<Calendar className="h-12 w-12 text-blue-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Tendencia</p>
<p className="text-3xl font-bold text-purple-600">+{forecastData.growthTrend}%</p>
<p className="text-xs text-gray-500">vs período anterior</p>
</div>
<TrendingUp className="h-12 w-12 text-purple-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Factor Estacional</p>
<p className="text-3xl font-bold text-orange-600">{forecastData.seasonalityFactor}x</p>
<p className="text-xs text-gray-500">multiplicador actual</p>
</div>
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-orange-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707" />
</svg>
</div>
</div>
</Card>
</div>
{/* Controls */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1 grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Producto</label>
<select
value={selectedProduct}
onChange={(e) => setSelectedProduct(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
{products.map(product => (
<option key={product.id} value={product.id}>{product.name}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Período</label>
<select
value={forecastPeriod}
onChange={(e) => setForecastPeriod(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
{periods.map(period => (
<option key={period.value} value={period.value}>{period.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Vista</label>
<div className="flex rounded-md border border-gray-300">
<button
onClick={() => setViewMode('chart')}
className={`px-3 py-2 text-sm ${viewMode === 'chart' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700'} rounded-l-md`}
>
Gráfico
</button>
<button
onClick={() => setViewMode('table')}
className={`px-3 py-2 text-sm ${viewMode === 'table' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700'} rounded-r-md border-l`}
>
Tabla
</button>
</div>
</div>
</div>
</div>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Forecast Display */}
<div className="lg:col-span-2">
{viewMode === 'chart' ? (
<DemandChart
product={selectedProduct}
period={forecastPeriod}
/>
) : (
<ForecastTable forecasts={mockForecasts} />
)}
</div>
{/* Alerts Panel */}
<div className="space-y-6">
<AlertsPanel alerts={alerts} />
{/* Weather Impact */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Impacto Meteorológico</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Hoy:</span>
<div className="flex items-center">
<span className="text-sm font-medium">{weatherImpact.temperature}°C</span>
<div className="ml-2 w-6 h-6 bg-yellow-400 rounded-full"></div>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Factor de demanda:</span>
<span className="text-sm font-medium text-blue-600">{weatherImpact.demandFactor}x</span>
</div>
<div className="mt-4">
<p className="text-xs text-gray-500 mb-2">Categorías afectadas:</p>
<div className="flex flex-wrap gap-1">
{weatherImpact.affectedCategories.map((category, index) => (
<Badge key={index} variant="blue">{category}</Badge>
))}
</div>
</div>
</div>
</Card>
{/* Seasonal Insights */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Patrones Estacionales</h3>
<div className="space-y-3">
{seasonalInsights.map((insight, index) => (
<div key={index} className="p-3 bg-gray-50 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">{insight.period}</span>
<span className="text-sm text-purple-600 font-medium">{insight.factor}x</span>
</div>
<div className="flex flex-wrap gap-1">
{insight.products.map((product, idx) => (
<Badge key={idx} variant="purple">{product}</Badge>
))}
</div>
</div>
))}
</div>
</Card>
</div>
</div>
{/* Detailed Forecasts Table */}
<Card>
<div className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Predicciones Detalladas</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Producto
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Stock Actual
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Demanda Prevista
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Producción Recomendada
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Confianza
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tendencia
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Riesgo Agotamiento
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{mockForecasts.map((forecast) => (
<tr key={forecast.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{forecast.product}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{forecast.currentStock}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-blue-600">
{forecast.forecastDemand}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-green-600">
{forecast.recommendedProduction}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{forecast.confidence}%
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
{getTrendIcon(forecast.trend)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getRiskBadge(forecast.stockoutRisk)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</Card>
</div>
);
};
export default ForecastingPage;

View File

@@ -1,2 +1,4 @@
export * from './forecasting';
export * from './sales-analytics';
export * from './sales-analytics';
export * from './performance';
export * from './ai-insights';

View File

@@ -1,403 +0,0 @@
import React, { useState } from 'react';
import { Activity, Clock, Users, TrendingUp, Target, AlertCircle, Download, Calendar } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const PerformanceAnalyticsPage: React.FC = () => {
const [selectedTimeframe, setSelectedTimeframe] = useState('month');
const [selectedMetric, setSelectedMetric] = useState('efficiency');
const performanceMetrics = {
overallEfficiency: 87.5,
productionTime: 4.2,
qualityScore: 92.1,
employeeProductivity: 89.3,
customerSatisfaction: 94.7,
resourceUtilization: 78.9,
};
const timeframes = [
{ value: 'day', label: 'Hoy' },
{ value: 'week', label: 'Esta Semana' },
{ value: 'month', label: 'Este Mes' },
{ value: 'quarter', label: 'Trimestre' },
{ value: 'year', label: 'Año' },
];
const departmentPerformance = [
{
department: 'Producción',
efficiency: 91.2,
trend: 5.3,
issues: 2,
employees: 8,
metrics: {
avgBatchTime: '2.3h',
qualityRate: '94%',
wastePercentage: '3.1%'
}
},
{
department: 'Ventas',
efficiency: 88.7,
trend: -1.2,
issues: 1,
employees: 4,
metrics: {
avgServiceTime: '3.2min',
customerWaitTime: '2.1min',
salesPerHour: '€127'
}
},
{
department: 'Inventario',
efficiency: 82.4,
trend: 2.8,
issues: 3,
employees: 2,
metrics: {
stockAccuracy: '96.7%',
turnoverRate: '12.3',
wastageRate: '4.2%'
}
},
{
department: 'Administración',
efficiency: 94.1,
trend: 8.1,
issues: 0,
employees: 3,
metrics: {
responseTime: '1.2h',
taskCompletion: '98%',
documentAccuracy: '99.1%'
}
}
];
const kpiTrends = [
{
name: 'Eficiencia General',
current: 87.5,
target: 90.0,
previous: 84.2,
unit: '%',
color: 'blue'
},
{
name: 'Tiempo de Producción',
current: 4.2,
target: 4.0,
previous: 4.5,
unit: 'h',
color: 'green',
inverse: true
},
{
name: 'Satisfacción Cliente',
current: 94.7,
target: 95.0,
previous: 93.1,
unit: '%',
color: 'purple'
},
{
name: 'Utilización de Recursos',
current: 78.9,
target: 85.0,
previous: 76.3,
unit: '%',
color: 'orange'
}
];
const performanceAlerts = [
{
id: '1',
type: 'warning',
title: 'Eficiencia de Inventario Baja',
description: 'El departamento de inventario está por debajo del objetivo del 85%',
value: '82.4%',
target: '85%',
department: 'Inventario'
},
{
id: '2',
type: 'info',
title: 'Tiempo de Producción Mejorado',
description: 'El tiempo promedio de producción ha mejorado este mes',
value: '4.2h',
target: '4.0h',
department: 'Producción'
},
{
id: '3',
type: 'success',
title: 'Administración Supera Objetivos',
description: 'El departamento administrativo está funcionando por encima del objetivo',
value: '94.1%',
target: '90%',
department: 'Administración'
}
];
const productivityData = [
{ hour: '07:00', efficiency: 75, transactions: 12, employees: 3 },
{ hour: '08:00', efficiency: 82, transactions: 18, employees: 5 },
{ hour: '09:00', efficiency: 89, transactions: 28, employees: 6 },
{ hour: '10:00', efficiency: 91, transactions: 32, employees: 7 },
{ hour: '11:00', efficiency: 94, transactions: 38, employees: 8 },
{ hour: '12:00', efficiency: 96, transactions: 45, employees: 8 },
{ hour: '13:00', efficiency: 95, transactions: 42, employees: 8 },
{ hour: '14:00', efficiency: 88, transactions: 35, employees: 7 },
{ hour: '15:00', efficiency: 85, transactions: 28, employees: 6 },
{ hour: '16:00', efficiency: 83, transactions: 25, employees: 5 },
{ hour: '17:00', efficiency: 87, transactions: 31, employees: 6 },
{ hour: '18:00', efficiency: 90, transactions: 38, employees: 7 },
{ hour: '19:00', efficiency: 86, transactions: 29, employees: 5 },
{ hour: '20:00', efficiency: 78, transactions: 18, employees: 3 },
];
const getTrendIcon = (trend: number) => {
if (trend > 0) {
return <TrendingUp className="w-4 h-4 text-green-600" />;
} else {
return <TrendingUp className="w-4 h-4 text-red-600 transform rotate-180" />;
}
};
const getTrendColor = (trend: number) => {
return trend >= 0 ? 'text-green-600' : 'text-red-600';
};
const getPerformanceColor = (value: number, target: number, inverse = false) => {
const comparison = inverse ? value < target : value >= target;
return comparison ? 'text-green-600' : value >= target * 0.9 ? 'text-yellow-600' : 'text-red-600';
};
const getAlertIcon = (type: string) => {
switch (type) {
case 'warning':
return <AlertCircle className="w-5 h-5 text-yellow-600" />;
case 'success':
return <TrendingUp className="w-5 h-5 text-green-600" />;
default:
return <Activity className="w-5 h-5 text-blue-600" />;
}
};
const getAlertColor = (type: string) => {
switch (type) {
case 'warning':
return 'bg-yellow-50 border-yellow-200';
case 'success':
return 'bg-green-50 border-green-200';
default:
return 'bg-blue-50 border-blue-200';
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Análisis de Rendimiento"
description="Monitorea la eficiencia operativa y el rendimiento de todos los departamentos"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Calendar className="w-4 h-4 mr-2" />
Configurar Alertas
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar Reporte
</Button>
</div>
}
/>
{/* Controls */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Período</label>
<select
value={selectedTimeframe}
onChange={(e) => setSelectedTimeframe(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
{timeframes.map(timeframe => (
<option key={timeframe.value} value={timeframe.value}>{timeframe.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Métrica Principal</label>
<select
value={selectedMetric}
onChange={(e) => setSelectedMetric(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
<option value="efficiency">Eficiencia</option>
<option value="productivity">Productividad</option>
<option value="quality">Calidad</option>
<option value="satisfaction">Satisfacción</option>
</select>
</div>
</div>
</Card>
{/* KPI Overview */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{kpiTrends.map((kpi) => (
<Card key={kpi.name} className="p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-600">{kpi.name}</h3>
<div className={`w-3 h-3 rounded-full bg-${kpi.color}-500`}></div>
</div>
<div className="flex items-end justify-between">
<div>
<p className={`text-2xl font-bold ${getPerformanceColor(kpi.current, kpi.target, kpi.inverse)}`}>
{kpi.current}{kpi.unit}
</p>
<p className="text-xs text-gray-500">
Objetivo: {kpi.target}{kpi.unit}
</p>
</div>
<div className="text-right">
<div className="flex items-center">
{getTrendIcon(kpi.current - kpi.previous)}
<span className={`text-sm ml-1 ${getTrendColor(kpi.current - kpi.previous)}`}>
{Math.abs(kpi.current - kpi.previous).toFixed(1)}{kpi.unit}
</span>
</div>
</div>
</div>
<div className="mt-3 w-full bg-gray-200 rounded-full h-2">
<div
className={`bg-${kpi.color}-500 h-2 rounded-full transition-all duration-300`}
style={{ width: `${Math.min((kpi.current / kpi.target) * 100, 100)}%` }}
></div>
</div>
</Card>
))}
</div>
{/* Performance Alerts */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Alertas de Rendimiento</h3>
<div className="space-y-3">
{performanceAlerts.map((alert) => (
<div key={alert.id} className={`p-4 rounded-lg border ${getAlertColor(alert.type)}`}>
<div className="flex items-start space-x-3">
{getAlertIcon(alert.type)}
<div className="flex-1">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-gray-900">{alert.title}</h4>
<Badge variant="gray">{alert.department}</Badge>
</div>
<p className="text-sm text-gray-600 mt-1">{alert.description}</p>
<div className="flex items-center space-x-4 mt-2">
<span className="text-sm">
<strong>Actual:</strong> {alert.value}
</span>
<span className="text-sm">
<strong>Objetivo:</strong> {alert.target}
</span>
</div>
</div>
</div>
</div>
))}
</div>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Department Performance */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Rendimiento por Departamento</h3>
<div className="space-y-4">
{departmentPerformance.map((dept) => (
<div key={dept.department} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-3">
<h4 className="font-medium text-gray-900">{dept.department}</h4>
<Badge variant="gray">{dept.employees} empleados</Badge>
</div>
<div className="flex items-center space-x-2">
<span className="text-lg font-semibold text-gray-900">
{dept.efficiency}%
</span>
<div className="flex items-center">
{getTrendIcon(dept.trend)}
<span className={`text-sm ml-1 ${getTrendColor(dept.trend)}`}>
{Math.abs(dept.trend).toFixed(1)}%
</span>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
{Object.entries(dept.metrics).map(([key, value]) => (
<div key={key}>
<p className="text-gray-500 text-xs">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</p>
<p className="font-medium">{value}</p>
</div>
))}
</div>
{dept.issues > 0 && (
<div className="mt-3 flex items-center text-sm text-amber-600">
<AlertCircle className="w-4 h-4 mr-1" />
{dept.issues} problema{dept.issues > 1 ? 's' : ''} detectado{dept.issues > 1 ? 's' : ''}
</div>
)}
</div>
))}
</div>
</Card>
{/* Hourly Productivity */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Eficiencia por Hora</h3>
<div className="h-64 flex items-end space-x-1 justify-between">
{productivityData.map((data, index) => (
<div key={index} className="flex flex-col items-center flex-1">
<div className="text-xs text-gray-600 mb-1">{data.efficiency}%</div>
<div
className="w-full bg-blue-500 rounded-t"
style={{
height: `${(data.efficiency / 100) * 200}px`,
minHeight: '8px',
backgroundColor: data.efficiency >= 90 ? '#10B981' : data.efficiency >= 80 ? '#F59E0B' : '#EF4444'
}}
></div>
<span className="text-xs text-gray-500 mt-2 transform -rotate-45 origin-center">
{data.hour}
</span>
</div>
))}
</div>
<div className="mt-4 flex justify-center space-x-6 text-xs">
<div className="flex items-center">
<div className="w-3 h-3 bg-green-500 rounded mr-1"></div>
<span>≥90% Excelente</span>
</div>
<div className="flex items-center">
<div className="w-3 h-3 bg-yellow-500 rounded mr-1"></div>
<span>80-89% Bueno</span>
</div>
<div className="flex items-center">
<div className="w-3 h-3 bg-red-500 rounded mr-1"></div>
<span>&lt;80% Bajo</span>
</div>
</div>
</Card>
</div>
</div>
);
};
export default PerformanceAnalyticsPage;

View File

@@ -1,379 +0,0 @@
import React, { useState } from 'react';
import { Calendar, TrendingUp, DollarSign, ShoppingCart, Download, Filter } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { AnalyticsDashboard, ChartWidget, ReportsTable } from '../../../../components/domain/analytics';
const SalesAnalyticsPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('month');
const [selectedMetric, setSelectedMetric] = useState('revenue');
const [viewMode, setViewMode] = useState<'overview' | 'detailed'>('overview');
const salesMetrics = {
totalRevenue: 45678.90,
totalOrders: 1234,
averageOrderValue: 37.02,
customerCount: 856,
growthRate: 12.5,
conversionRate: 68.4,
};
const periods = [
{ value: 'day', label: 'Hoy' },
{ value: 'week', label: 'Esta Semana' },
{ value: 'month', label: 'Este Mes' },
{ value: 'quarter', label: 'Este Trimestre' },
{ value: 'year', label: 'Este Año' },
];
const metrics = [
{ value: 'revenue', label: 'Ingresos' },
{ value: 'orders', label: 'Pedidos' },
{ value: 'customers', label: 'Clientes' },
{ value: 'products', label: 'Productos' },
];
const topProducts = [
{
id: '1',
name: 'Pan de Molde Integral',
revenue: 2250.50,
units: 245,
growth: 8.2,
category: 'Panes'
},
{
id: '2',
name: 'Croissants de Mantequilla',
revenue: 1890.75,
units: 412,
growth: 15.4,
category: 'Bollería'
},
{
id: '3',
name: 'Tarta de Chocolate',
revenue: 1675.00,
units: 67,
growth: -2.1,
category: 'Tartas'
},
{
id: '4',
name: 'Empanadas Variadas',
revenue: 1425.25,
units: 285,
growth: 22.8,
category: 'Salados'
},
{
id: '5',
name: 'Magdalenas',
revenue: 1180.50,
units: 394,
growth: 5.7,
category: 'Bollería'
},
];
const salesByHour = [
{ hour: '07:00', sales: 145, orders: 12 },
{ hour: '08:00', sales: 289, orders: 18 },
{ hour: '09:00', sales: 425, orders: 28 },
{ hour: '10:00', sales: 380, orders: 24 },
{ hour: '11:00', sales: 520, orders: 31 },
{ hour: '12:00', sales: 675, orders: 42 },
{ hour: '13:00', sales: 720, orders: 45 },
{ hour: '14:00', sales: 580, orders: 35 },
{ hour: '15:00', sales: 420, orders: 28 },
{ hour: '16:00', sales: 350, orders: 22 },
{ hour: '17:00', sales: 480, orders: 31 },
{ hour: '18:00', sales: 620, orders: 38 },
{ hour: '19:00', sales: 450, orders: 29 },
{ hour: '20:00', sales: 280, orders: 18 },
];
const customerSegments = [
{ segment: 'Clientes Frecuentes', count: 123, revenue: 15678, percentage: 34.3 },
{ segment: 'Clientes Regulares', count: 245, revenue: 18950, percentage: 41.5 },
{ segment: 'Clientes Ocasionales', count: 356, revenue: 8760, percentage: 19.2 },
{ segment: 'Clientes Nuevos', count: 132, revenue: 2290, percentage: 5.0 },
];
const paymentMethods = [
{ method: 'Tarjeta', count: 567, revenue: 28450, percentage: 62.3 },
{ method: 'Efectivo', count: 445, revenue: 13890, percentage: 30.4 },
{ method: 'Transferencia', count: 178, revenue: 2890, percentage: 6.3 },
{ method: 'Otros', count: 44, revenue: 448, percentage: 1.0 },
];
const getGrowthBadge = (growth: number) => {
if (growth > 0) {
return <Badge variant="green">+{growth.toFixed(1)}%</Badge>;
} else if (growth < 0) {
return <Badge variant="red">{growth.toFixed(1)}%</Badge>;
} else {
return <Badge variant="gray">0%</Badge>;
}
};
const getGrowthColor = (growth: number) => {
return growth >= 0 ? 'text-green-600' : 'text-red-600';
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Análisis de Ventas"
description="Análisis detallado del rendimiento de ventas y tendencias de tu panadería"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* Controls */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1 grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Período</label>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
{periods.map(period => (
<option key={period.value} value={period.value}>{period.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Métrica Principal</label>
<select
value={selectedMetric}
onChange={(e) => setSelectedMetric(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
{metrics.map(metric => (
<option key={metric.value} value={metric.value}>{metric.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Vista</label>
<div className="flex rounded-md border border-gray-300">
<button
onClick={() => setViewMode('overview')}
className={`px-3 py-2 text-sm ${viewMode === 'overview' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700'} rounded-l-md`}
>
General
</button>
<button
onClick={() => setViewMode('detailed')}
className={`px-3 py-2 text-sm ${viewMode === 'detailed' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700'} rounded-r-md border-l`}
>
Detallado
</button>
</div>
</div>
</div>
</div>
</Card>
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Ingresos Totales</p>
<p className="text-2xl font-bold text-green-600">€{salesMetrics.totalRevenue.toLocaleString()}</p>
</div>
<DollarSign className="h-8 w-8 text-green-600" />
</div>
<div className="mt-2">
{getGrowthBadge(salesMetrics.growthRate)}
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Pedidos</p>
<p className="text-2xl font-bold text-blue-600">{salesMetrics.totalOrders.toLocaleString()}</p>
</div>
<ShoppingCart className="h-8 w-8 text-blue-600" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Valor Promedio</p>
<p className="text-2xl font-bold text-purple-600">€{salesMetrics.averageOrderValue.toFixed(2)}</p>
</div>
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Clientes</p>
<p className="text-2xl font-bold text-orange-600">{salesMetrics.customerCount}</p>
</div>
<div className="h-8 w-8 bg-orange-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-orange-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-.5a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Tasa Crecimiento</p>
<p className={`text-2xl font-bold ${getGrowthColor(salesMetrics.growthRate)}`}>
{salesMetrics.growthRate > 0 ? '+' : ''}{salesMetrics.growthRate.toFixed(1)}%
</p>
</div>
<TrendingUp className={`h-8 w-8 ${getGrowthColor(salesMetrics.growthRate)}`} />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Conversión</p>
<p className="text-2xl font-bold text-indigo-600">{salesMetrics.conversionRate}%</p>
</div>
<div className="h-8 w-8 bg-indigo-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
</Card>
</div>
{viewMode === 'overview' ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Sales by Hour Chart */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Ventas por Hora</h3>
<div className="h-64 flex items-end space-x-1 justify-between">
{salesByHour.map((data, index) => (
<div key={index} className="flex flex-col items-center flex-1">
<div
className="w-full bg-blue-500 rounded-t"
style={{
height: `${(data.sales / Math.max(...salesByHour.map(d => d.sales))) * 200}px`,
minHeight: '4px'
}}
></div>
<span className="text-xs text-gray-500 mt-2 transform -rotate-45 origin-center">
{data.hour}
</span>
</div>
))}
</div>
</Card>
{/* Top Products */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Productos Más Vendidos</h3>
<div className="space-y-3">
{topProducts.slice(0, 5).map((product, index) => (
<div key={product.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-3">
<span className="text-sm font-medium text-gray-500 w-6">{index + 1}.</span>
<div>
<p className="text-sm font-medium text-gray-900">{product.name}</p>
<p className="text-xs text-gray-500">{product.category} • {product.units} unidades</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-gray-900">€{product.revenue.toLocaleString()}</p>
{getGrowthBadge(product.growth)}
</div>
</div>
))}
</div>
</Card>
{/* Customer Segments */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Segmentos de Clientes</h3>
<div className="space-y-4">
{customerSegments.map((segment, index) => (
<div key={index} className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-900">{segment.segment}</span>
<span className="text-sm text-gray-600">{segment.percentage}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${segment.percentage}%` }}
></div>
</div>
<div className="flex justify-between text-xs text-gray-500">
<span>{segment.count} clientes</span>
<span>€{segment.revenue.toLocaleString()}</span>
</div>
</div>
))}
</div>
</Card>
{/* Payment Methods */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Métodos de Pago</h3>
<div className="space-y-3">
{paymentMethods.map((method, index) => (
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center space-x-3">
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
<div>
<p className="text-sm font-medium text-gray-900">{method.method}</p>
<p className="text-xs text-gray-500">{method.count} transacciones</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-gray-900">€{method.revenue.toLocaleString()}</p>
<p className="text-xs text-gray-500">{method.percentage}%</p>
</div>
</div>
))}
</div>
</Card>
</div>
) : (
<div className="space-y-6">
{/* Detailed Analytics Dashboard */}
<AnalyticsDashboard />
{/* Detailed Reports Table */}
<ReportsTable />
</div>
)}
</div>
);
};
export default SalesAnalyticsPage;

View File

@@ -1,388 +0,0 @@
import React, { useState } from 'react';
import { Settings, Bell, Mail, MessageSquare, Smartphone, Save, RotateCcw } from 'lucide-react';
import { Button, Card } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const PreferencesPage: React.FC = () => {
const [preferences, setPreferences] = useState({
notifications: {
inventory: {
app: true,
email: false,
sms: true,
frequency: 'immediate'
},
sales: {
app: true,
email: true,
sms: false,
frequency: 'hourly'
},
production: {
app: true,
email: false,
sms: true,
frequency: 'immediate'
},
system: {
app: true,
email: true,
sms: false,
frequency: 'daily'
},
marketing: {
app: false,
email: true,
sms: false,
frequency: 'weekly'
}
},
global: {
doNotDisturb: false,
quietHours: {
enabled: false,
start: '22:00',
end: '07:00'
},
language: 'es',
timezone: 'Europe/Madrid',
soundEnabled: true,
vibrationEnabled: true
},
channels: {
email: 'panaderia@example.com',
phone: '+34 600 123 456',
slack: false,
webhook: ''
}
});
const [hasChanges, setHasChanges] = useState(false);
const categories = [
{
id: 'inventory',
name: 'Inventario',
description: 'Alertas de stock, reposiciones y vencimientos',
icon: '📦'
},
{
id: 'sales',
name: 'Ventas',
description: 'Pedidos, transacciones y reportes de ventas',
icon: '💰'
},
{
id: 'production',
name: 'Producción',
description: 'Hornadas, calidad y tiempos de producción',
icon: '🍞'
},
{
id: 'system',
name: 'Sistema',
description: 'Actualizaciones, mantenimiento y errores',
icon: '⚙️'
},
{
id: 'marketing',
name: 'Marketing',
description: 'Campañas, promociones y análisis',
icon: '📢'
}
];
const frequencies = [
{ value: 'immediate', label: 'Inmediato' },
{ value: 'hourly', label: 'Cada hora' },
{ value: 'daily', label: 'Diario' },
{ value: 'weekly', label: 'Semanal' }
];
const handleNotificationChange = (category: string, channel: string, value: boolean) => {
setPreferences(prev => ({
...prev,
notifications: {
...prev.notifications,
[category]: {
...prev.notifications[category as keyof typeof prev.notifications],
[channel]: value
}
}
}));
setHasChanges(true);
};
const handleFrequencyChange = (category: string, frequency: string) => {
setPreferences(prev => ({
...prev,
notifications: {
...prev.notifications,
[category]: {
...prev.notifications[category as keyof typeof prev.notifications],
frequency
}
}
}));
setHasChanges(true);
};
const handleGlobalChange = (setting: string, value: any) => {
setPreferences(prev => ({
...prev,
global: {
...prev.global,
[setting]: value
}
}));
setHasChanges(true);
};
const handleChannelChange = (channel: string, value: string | boolean) => {
setPreferences(prev => ({
...prev,
channels: {
...prev.channels,
[channel]: value
}
}));
setHasChanges(true);
};
const handleSave = () => {
// Handle save logic
console.log('Saving preferences:', preferences);
setHasChanges(false);
};
const handleReset = () => {
// Reset to defaults
setHasChanges(false);
};
const getChannelIcon = (channel: string) => {
switch (channel) {
case 'app':
return <Bell className="w-4 h-4" />;
case 'email':
return <Mail className="w-4 h-4" />;
case 'sms':
return <Smartphone className="w-4 h-4" />;
default:
return <MessageSquare className="w-4 h-4" />;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Preferencias de Comunicación"
description="Configura cómo y cuándo recibir notificaciones"
action={
<div className="flex space-x-2">
<Button variant="outline" onClick={handleReset}>
<RotateCcw className="w-4 h-4 mr-2" />
Restaurar
</Button>
<Button onClick={handleSave} disabled={!hasChanges}>
<Save className="w-4 h-4 mr-2" />
Guardar Cambios
</Button>
</div>
}
/>
{/* Global Settings */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Configuración General</h3>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={preferences.global.doNotDisturb}
onChange={(e) => handleGlobalChange('doNotDisturb', e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm font-medium text-[var(--text-secondary)]">No molestar</span>
</label>
<p className="text-xs text-[var(--text-tertiary)] mt-1">Silencia todas las notificaciones</p>
</div>
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={preferences.global.soundEnabled}
onChange={(e) => handleGlobalChange('soundEnabled', e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm font-medium text-[var(--text-secondary)]">Sonidos</span>
</label>
<p className="text-xs text-[var(--text-tertiary)] mt-1">Reproducir sonidos de notificación</p>
</div>
</div>
<div>
<label className="flex items-center space-x-2 mb-2">
<input
type="checkbox"
checked={preferences.global.quietHours.enabled}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
enabled: e.target.checked
})}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm font-medium text-[var(--text-secondary)]">Horas silenciosas</span>
</label>
{preferences.global.quietHours.enabled && (
<div className="flex space-x-4 ml-6">
<div>
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Desde</label>
<input
type="time"
value={preferences.global.quietHours.start}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
start: e.target.value
})}
className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm"
/>
</div>
<div>
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Hasta</label>
<input
type="time"
value={preferences.global.quietHours.end}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
end: e.target.value
})}
className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm"
/>
</div>
</div>
)}
</div>
</div>
</Card>
{/* Channel Settings */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Canales de Comunicación</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Email</label>
<input
type="email"
value={preferences.channels.email}
onChange={(e) => handleChannelChange('email', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
placeholder="tu-email@ejemplo.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Teléfono (SMS)</label>
<input
type="tel"
value={preferences.channels.phone}
onChange={(e) => handleChannelChange('phone', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
placeholder="+34 600 123 456"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Webhook URL</label>
<input
type="url"
value={preferences.channels.webhook}
onChange={(e) => handleChannelChange('webhook', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
placeholder="https://tu-webhook.com/notifications"
/>
<p className="text-xs text-[var(--text-tertiary)] mt-1">URL para recibir notificaciones JSON</p>
</div>
</div>
</Card>
{/* Category Preferences */}
<div className="space-y-4">
{categories.map((category) => {
const categoryPrefs = preferences.notifications[category.id as keyof typeof preferences.notifications];
return (
<Card key={category.id} className="p-6">
<div className="flex items-start space-x-4">
<div className="text-2xl">{category.icon}</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-1">{category.name}</h3>
<p className="text-sm text-[var(--text-secondary)] mb-4">{category.description}</p>
<div className="space-y-4">
{/* Channel toggles */}
<div>
<h4 className="text-sm font-medium text-[var(--text-secondary)] mb-2">Canales</h4>
<div className="flex space-x-6">
{['app', 'email', 'sms'].map((channel) => (
<label key={channel} className="flex items-center space-x-2">
<input
type="checkbox"
checked={categoryPrefs[channel as keyof typeof categoryPrefs] as boolean}
onChange={(e) => handleNotificationChange(category.id, channel, e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<div className="flex items-center space-x-1">
{getChannelIcon(channel)}
<span className="text-sm text-[var(--text-secondary)] capitalize">{channel}</span>
</div>
</label>
))}
</div>
</div>
{/* Frequency */}
<div>
<h4 className="text-sm font-medium text-[var(--text-secondary)] mb-2">Frecuencia</h4>
<select
value={categoryPrefs.frequency}
onChange={(e) => handleFrequencyChange(category.id, e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md text-sm"
>
{frequencies.map((freq) => (
<option key={freq.value} value={freq.value}>
{freq.label}
</option>
))}
</select>
</div>
</div>
</div>
</div>
</Card>
);
})}
</div>
{/* Save Changes Banner */}
{hasChanges && (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
<span className="text-sm">Tienes cambios sin guardar</span>
<div className="flex space-x-2">
<Button size="sm" variant="outline" className="text-[var(--color-info)] bg-white" onClick={handleReset}>
Descartar
</Button>
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
Guardar
</Button>
</div>
</div>
)}
</div>
);
};
export default PreferencesPage;

View File

@@ -1,388 +0,0 @@
import React, { useState } from 'react';
import { Settings, Bell, Mail, MessageSquare, Smartphone, Save, RotateCcw } from 'lucide-react';
import { Button, Card } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const PreferencesPage: React.FC = () => {
const [preferences, setPreferences] = useState({
notifications: {
inventory: {
app: true,
email: false,
sms: true,
frequency: 'immediate'
},
sales: {
app: true,
email: true,
sms: false,
frequency: 'hourly'
},
production: {
app: true,
email: false,
sms: true,
frequency: 'immediate'
},
system: {
app: true,
email: true,
sms: false,
frequency: 'daily'
},
marketing: {
app: false,
email: true,
sms: false,
frequency: 'weekly'
}
},
global: {
doNotDisturb: false,
quietHours: {
enabled: false,
start: '22:00',
end: '07:00'
},
language: 'es',
timezone: 'Europe/Madrid',
soundEnabled: true,
vibrationEnabled: true
},
channels: {
email: 'panaderia@example.com',
phone: '+34 600 123 456',
slack: false,
webhook: ''
}
});
const [hasChanges, setHasChanges] = useState(false);
const categories = [
{
id: 'inventory',
name: 'Inventario',
description: 'Alertas de stock, reposiciones y vencimientos',
icon: '📦'
},
{
id: 'sales',
name: 'Ventas',
description: 'Pedidos, transacciones y reportes de ventas',
icon: '💰'
},
{
id: 'production',
name: 'Producción',
description: 'Hornadas, calidad y tiempos de producción',
icon: '🍞'
},
{
id: 'system',
name: 'Sistema',
description: 'Actualizaciones, mantenimiento y errores',
icon: '⚙️'
},
{
id: 'marketing',
name: 'Marketing',
description: 'Campañas, promociones y análisis',
icon: '📢'
}
];
const frequencies = [
{ value: 'immediate', label: 'Inmediato' },
{ value: 'hourly', label: 'Cada hora' },
{ value: 'daily', label: 'Diario' },
{ value: 'weekly', label: 'Semanal' }
];
const handleNotificationChange = (category: string, channel: string, value: boolean) => {
setPreferences(prev => ({
...prev,
notifications: {
...prev.notifications,
[category]: {
...prev.notifications[category as keyof typeof prev.notifications],
[channel]: value
}
}
}));
setHasChanges(true);
};
const handleFrequencyChange = (category: string, frequency: string) => {
setPreferences(prev => ({
...prev,
notifications: {
...prev.notifications,
[category]: {
...prev.notifications[category as keyof typeof prev.notifications],
frequency
}
}
}));
setHasChanges(true);
};
const handleGlobalChange = (setting: string, value: any) => {
setPreferences(prev => ({
...prev,
global: {
...prev.global,
[setting]: value
}
}));
setHasChanges(true);
};
const handleChannelChange = (channel: string, value: string | boolean) => {
setPreferences(prev => ({
...prev,
channels: {
...prev.channels,
[channel]: value
}
}));
setHasChanges(true);
};
const handleSave = () => {
// Handle save logic
console.log('Saving preferences:', preferences);
setHasChanges(false);
};
const handleReset = () => {
// Reset to defaults
setHasChanges(false);
};
const getChannelIcon = (channel: string) => {
switch (channel) {
case 'app':
return <Bell className="w-4 h-4" />;
case 'email':
return <Mail className="w-4 h-4" />;
case 'sms':
return <Smartphone className="w-4 h-4" />;
default:
return <MessageSquare className="w-4 h-4" />;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Preferencias de Comunicación"
description="Configura cómo y cuándo recibir notificaciones"
action={
<div className="flex space-x-2">
<Button variant="outline" onClick={handleReset}>
<RotateCcw className="w-4 h-4 mr-2" />
Restaurar
</Button>
<Button onClick={handleSave} disabled={!hasChanges}>
<Save className="w-4 h-4 mr-2" />
Guardar Cambios
</Button>
</div>
}
/>
{/* Global Settings */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Configuración General</h3>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={preferences.global.doNotDisturb}
onChange={(e) => handleGlobalChange('doNotDisturb', e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm font-medium text-gray-700">No molestar</span>
</label>
<p className="text-xs text-gray-500 mt-1">Silencia todas las notificaciones</p>
</div>
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={preferences.global.soundEnabled}
onChange={(e) => handleGlobalChange('soundEnabled', e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm font-medium text-gray-700">Sonidos</span>
</label>
<p className="text-xs text-gray-500 mt-1">Reproducir sonidos de notificación</p>
</div>
</div>
<div>
<label className="flex items-center space-x-2 mb-2">
<input
type="checkbox"
checked={preferences.global.quietHours.enabled}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
enabled: e.target.checked
})}
className="rounded border-gray-300"
/>
<span className="text-sm font-medium text-gray-700">Horas silenciosas</span>
</label>
{preferences.global.quietHours.enabled && (
<div className="flex space-x-4 ml-6">
<div>
<label className="block text-xs text-gray-500 mb-1">Desde</label>
<input
type="time"
value={preferences.global.quietHours.start}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
start: e.target.value
})}
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Hasta</label>
<input
type="time"
value={preferences.global.quietHours.end}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
end: e.target.value
})}
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
/>
</div>
</div>
)}
</div>
</div>
</Card>
{/* Channel Settings */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Canales de Comunicación</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Email</label>
<input
type="email"
value={preferences.channels.email}
onChange={(e) => handleChannelChange('email', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="tu-email@ejemplo.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Teléfono (SMS)</label>
<input
type="tel"
value={preferences.channels.phone}
onChange={(e) => handleChannelChange('phone', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="+34 600 123 456"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Webhook URL</label>
<input
type="url"
value={preferences.channels.webhook}
onChange={(e) => handleChannelChange('webhook', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="https://tu-webhook.com/notifications"
/>
<p className="text-xs text-gray-500 mt-1">URL para recibir notificaciones JSON</p>
</div>
</div>
</Card>
{/* Category Preferences */}
<div className="space-y-4">
{categories.map((category) => {
const categoryPrefs = preferences.notifications[category.id as keyof typeof preferences.notifications];
return (
<Card key={category.id} className="p-6">
<div className="flex items-start space-x-4">
<div className="text-2xl">{category.icon}</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 mb-1">{category.name}</h3>
<p className="text-sm text-gray-600 mb-4">{category.description}</p>
<div className="space-y-4">
{/* Channel toggles */}
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">Canales</h4>
<div className="flex space-x-6">
{['app', 'email', 'sms'].map((channel) => (
<label key={channel} className="flex items-center space-x-2">
<input
type="checkbox"
checked={categoryPrefs[channel as keyof typeof categoryPrefs] as boolean}
onChange={(e) => handleNotificationChange(category.id, channel, e.target.checked)}
className="rounded border-gray-300"
/>
<div className="flex items-center space-x-1">
{getChannelIcon(channel)}
<span className="text-sm text-gray-700 capitalize">{channel}</span>
</div>
</label>
))}
</div>
</div>
{/* Frequency */}
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">Frecuencia</h4>
<select
value={categoryPrefs.frequency}
onChange={(e) => handleFrequencyChange(category.id, e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md text-sm"
>
{frequencies.map((freq) => (
<option key={freq.value} value={freq.value}>
{freq.label}
</option>
))}
</select>
</div>
</div>
</div>
</div>
</Card>
);
})}
</div>
{/* Save Changes Banner */}
{hasChanges && (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
<span className="text-sm">Tienes cambios sin guardar</span>
<div className="flex space-x-2">
<Button size="sm" variant="outline" className="text-blue-600 bg-white" onClick={handleReset}>
Descartar
</Button>
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
Guardar
</Button>
</div>
</div>
)}
</div>
);
};
export default PreferencesPage;

View File

@@ -1 +0,0 @@
export { default as PreferencesPage } from './PreferencesPage';

View File

@@ -1,312 +0,0 @@
import React, { useState } from 'react';
import { Calendar, Activity, Filter, Download, Eye, BarChart3 } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const EventsPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('week');
const [selectedCategory, setSelectedCategory] = useState('all');
const events = [
{
id: '1',
timestamp: '2024-01-26 10:30:00',
category: 'sales',
type: 'order_completed',
title: 'Pedido Completado',
description: 'Pedido #ORD-456 completado por €127.50',
metadata: {
orderId: 'ORD-456',
amount: 127.50,
customer: 'María González',
items: 8
},
severity: 'info'
},
{
id: '2',
timestamp: '2024-01-26 09:15:00',
category: 'production',
type: 'batch_started',
title: 'Lote Iniciado',
description: 'Iniciado lote de croissants CR-024',
metadata: {
batchId: 'CR-024',
product: 'Croissants',
quantity: 48,
expectedDuration: '2.5h'
},
severity: 'info'
},
{
id: '3',
timestamp: '2024-01-26 08:45:00',
category: 'inventory',
type: 'stock_updated',
title: 'Stock Actualizado',
description: 'Repuesto stock de harina - Nivel: 50kg',
metadata: {
item: 'Harina de Trigo',
previousLevel: '5kg',
newLevel: '50kg',
supplier: 'Molinos del Sur'
},
severity: 'success'
},
{
id: '4',
timestamp: '2024-01-26 07:30:00',
category: 'system',
type: 'user_login',
title: 'Inicio de Sesión',
description: 'Usuario admin ha iniciado sesión',
metadata: {
userId: 'admin',
ipAddress: '192.168.1.100',
userAgent: 'Chrome/120.0',
location: 'Madrid, ES'
},
severity: 'info'
},
{
id: '5',
timestamp: '2024-01-25 19:20:00',
category: 'sales',
type: 'payment_processed',
title: 'Pago Procesado',
description: 'Pago de €45.80 procesado exitosamente',
metadata: {
amount: 45.80,
method: 'Tarjeta',
reference: 'PAY-789',
customer: 'Juan Pérez'
},
severity: 'success'
}
];
const eventStats = {
total: events.length,
today: events.filter(e =>
new Date(e.timestamp).toDateString() === new Date().toDateString()
).length,
sales: events.filter(e => e.category === 'sales').length,
production: events.filter(e => e.category === 'production').length,
system: events.filter(e => e.category === 'system').length
};
const categories = [
{ value: 'all', label: 'Todos', count: events.length },
{ value: 'sales', label: 'Ventas', count: eventStats.sales },
{ value: 'production', label: 'Producción', count: eventStats.production },
{ value: 'inventory', label: 'Inventario', count: events.filter(e => e.category === 'inventory').length },
{ value: 'system', label: 'Sistema', count: eventStats.system }
];
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'success': return 'green';
case 'warning': return 'yellow';
case 'error': return 'red';
default: return 'blue';
}
};
const getCategoryIcon = (category: string) => {
const iconProps = { className: "w-4 h-4" };
switch (category) {
case 'sales': return <BarChart3 {...iconProps} />;
case 'production': return <Activity {...iconProps} />;
case 'inventory': return <Calendar {...iconProps} />;
default: return <Activity {...iconProps} />;
}
};
const filteredEvents = selectedCategory === 'all'
? events
: events.filter(event => event.category === selectedCategory);
const formatTimeAgo = (timestamp: string) => {
const now = new Date();
const eventTime = new Date(timestamp);
const diffInMs = now.getTime() - eventTime.getTime();
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
const diffInDays = Math.floor(diffInHours / 24);
if (diffInDays > 0) {
return `hace ${diffInDays}d`;
} else if (diffInHours > 0) {
return `hace ${diffInHours}h`;
} else {
return `hace ${Math.floor(diffInMs / (1000 * 60))}m`;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Registro de Eventos"
description="Seguimiento de todas las actividades y eventos del sistema"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros Avanzados
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* Event Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Eventos</p>
<p className="text-3xl font-bold text-gray-900">{eventStats.total}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<Activity className="h-6 w-6 text-blue-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Hoy</p>
<p className="text-3xl font-bold text-green-600">{eventStats.today}</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<Calendar className="h-6 w-6 text-green-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Ventas</p>
<p className="text-3xl font-bold text-purple-600">{eventStats.sales}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<BarChart3 className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Producción</p>
<p className="text-3xl font-bold text-orange-600">{eventStats.production}</p>
</div>
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
<Activity className="h-6 w-6 text-orange-600" />
</div>
</div>
</Card>
</div>
{/* Filters */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Período</label>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
<option value="day">Hoy</option>
<option value="week">Esta Semana</option>
<option value="month">Este Mes</option>
<option value="all">Todos</option>
</select>
</div>
<div className="flex gap-2 flex-wrap">
{categories.map((category) => (
<button
key={category.value}
onClick={() => setSelectedCategory(category.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
selectedCategory === category.value
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{category.label} ({category.count})
</button>
))}
</div>
</div>
</Card>
{/* Events List */}
<div className="space-y-4">
{filteredEvents.map((event) => (
<Card key={event.id} className="p-6">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
<div className={`p-2 rounded-lg bg-${getSeverityColor(event.severity)}-100`}>
{getCategoryIcon(event.category)}
</div>
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-lg font-semibold text-gray-900">{event.title}</h3>
<Badge variant={getSeverityColor(event.severity)}>
{event.category}
</Badge>
</div>
<p className="text-gray-700 mb-3">{event.description}</p>
{/* Event Metadata */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
{Object.entries(event.metadata).map(([key, value]) => (
<div key={key} className="bg-gray-50 p-3 rounded-lg">
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, (str: string) => str.toUpperCase())}
</p>
<p className="text-sm font-medium text-gray-900">{value}</p>
</div>
))}
</div>
<div className="flex items-center text-sm text-gray-500">
<span>{formatTimeAgo(event.timestamp)}</span>
<span className="mx-2">•</span>
<span>{new Date(event.timestamp).toLocaleString('es-ES')}</span>
</div>
</div>
</div>
<Button size="sm" variant="outline">
<Eye className="w-4 h-4 mr-2" />
Ver Detalles
</Button>
</div>
</Card>
))}
</div>
{filteredEvents.length === 0 && (
<Card className="p-12 text-center">
<Activity className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">No hay eventos</h3>
<p className="text-gray-600">
No se encontraron eventos para el período y categoría seleccionados.
</p>
</Card>
)}
</div>
);
};
export default EventsPage;

View File

@@ -1,336 +0,0 @@
import React, { useState } from 'react';
import { Users, Clock, TrendingUp, MapPin, Calendar, BarChart3, Download, Filter } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const TrafficPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('week');
const [selectedMetric, setSelectedMetric] = useState('visitors');
const trafficData = {
totalVisitors: 2847,
peakHour: '12:00',
averageVisitDuration: '23min',
busyDays: ['Viernes', 'Sábado'],
conversionRate: 68.4
};
const hourlyTraffic = [
{ hour: '07:00', visitors: 15, sales: 12, duration: '18min' },
{ hour: '08:00', visitors: 32, sales: 24, duration: '22min' },
{ hour: '09:00', visitors: 45, sales: 28, duration: '25min' },
{ hour: '10:00', visitors: 38, sales: 25, duration: '24min' },
{ hour: '11:00', visitors: 52, sales: 35, duration: '26min' },
{ hour: '12:00', visitors: 78, sales: 54, duration: '28min' },
{ hour: '13:00', visitors: 85, sales: 58, duration: '30min' },
{ hour: '14:00', visitors: 62, sales: 42, duration: '27min' },
{ hour: '15:00', visitors: 48, sales: 32, duration: '25min' },
{ hour: '16:00', visitors: 55, sales: 38, duration: '26min' },
{ hour: '17:00', visitors: 68, sales: 46, duration: '29min' },
{ hour: '18:00', visitors: 74, sales: 52, duration: '31min' },
{ hour: '19:00', visitors: 56, sales: 39, duration: '28min' },
{ hour: '20:00', visitors: 28, sales: 18, duration: '22min' }
];
const dailyTraffic = [
{ day: 'Lun', visitors: 245, sales: 168, conversion: 68.6, avgDuration: '22min' },
{ day: 'Mar', visitors: 289, sales: 195, conversion: 67.5, avgDuration: '24min' },
{ day: 'Mié', visitors: 321, sales: 218, conversion: 67.9, avgDuration: '25min' },
{ day: 'Jue', visitors: 356, sales: 242, conversion: 68.0, avgDuration: '26min' },
{ day: 'Vie', visitors: 445, sales: 312, conversion: 70.1, avgDuration: '28min' },
{ day: 'Sáb', visitors: 498, sales: 348, conversion: 69.9, avgDuration: '30min' },
{ day: 'Dom', visitors: 398, sales: 265, conversion: 66.6, avgDuration: '27min' }
];
const trafficSources = [
{ source: 'Pie', visitors: 1245, percentage: 43.7, trend: 5.2 },
{ source: 'Búsqueda Local', visitors: 687, percentage: 24.1, trend: 12.3 },
{ source: 'Recomendaciones', visitors: 423, percentage: 14.9, trend: -2.1 },
{ source: 'Redes Sociales', visitors: 298, percentage: 10.5, trend: 8.7 },
{ source: 'Publicidad', visitors: 194, percentage: 6.8, trend: 15.4 }
];
const customerSegments = [
{
segment: 'Regulares Matutinos',
count: 145,
percentage: 24.2,
peakHours: ['07:00-09:00'],
avgSpend: 12.50,
frequency: 'Diaria'
},
{
segment: 'Familia Fin de Semana',
count: 198,
percentage: 33.1,
peakHours: ['10:00-13:00'],
avgSpend: 28.90,
frequency: 'Semanal'
},
{
segment: 'Oficinistas Almuerzo',
count: 112,
percentage: 18.7,
peakHours: ['12:00-14:00'],
avgSpend: 8.75,
frequency: '2-3x semana'
},
{
segment: 'Clientes Ocasionales',
count: 143,
percentage: 23.9,
peakHours: ['16:00-19:00'],
avgSpend: 15.20,
frequency: 'Mensual'
}
];
const getTrendColor = (trend: number) => {
return trend >= 0 ? 'text-green-600' : 'text-red-600';
};
const getTrendIcon = (trend: number) => {
return trend >= 0 ? '↗' : '↘';
};
const maxVisitors = Math.max(...hourlyTraffic.map(h => h.visitors));
const maxDailyVisitors = Math.max(...dailyTraffic.map(d => d.visitors));
return (
<div className="p-6 space-y-6">
<PageHeader
title="Análisis de Tráfico"
description="Monitorea los patrones de visitas y flujo de clientes"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* Traffic Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Visitantes Totales</p>
<p className="text-3xl font-bold text-blue-600">{trafficData.totalVisitors.toLocaleString()}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-blue-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Hora Pico</p>
<p className="text-3xl font-bold text-green-600">{trafficData.peakHour}</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<Clock className="h-6 w-6 text-green-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Duración Promedio</p>
<p className="text-3xl font-bold text-purple-600">{trafficData.averageVisitDuration}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Clock className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Conversión</p>
<p className="text-3xl font-bold text-orange-600">{trafficData.conversionRate}%</p>
</div>
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
<TrendingUp className="h-6 w-6 text-orange-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Días Ocupados</p>
<p className="text-sm font-bold text-red-600">{trafficData.busyDays.join(', ')}</p>
</div>
<div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center">
<Calendar className="h-6 w-6 text-red-600" />
</div>
</div>
</Card>
</div>
{/* Controls */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Período</label>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
<option value="day">Hoy</option>
<option value="week">Esta Semana</option>
<option value="month">Este Mes</option>
<option value="year">Este Año</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Métrica</label>
<select
value={selectedMetric}
onChange={(e) => setSelectedMetric(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
<option value="visitors">Visitantes</option>
<option value="sales">Ventas</option>
<option value="duration">Duración</option>
<option value="conversion">Conversión</option>
</select>
</div>
</div>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Hourly Traffic */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Tráfico por Hora</h3>
<div className="h-64 flex items-end space-x-1 justify-between">
{hourlyTraffic.map((data, index) => (
<div key={index} className="flex flex-col items-center flex-1">
<div className="text-xs text-gray-600 mb-1">{data.visitors}</div>
<div
className="w-full bg-blue-500 rounded-t"
style={{
height: `${(data.visitors / maxVisitors) * 200}px`,
minHeight: '4px'
}}
></div>
<span className="text-xs text-gray-500 mt-2 transform -rotate-45 origin-center">
{data.hour}
</span>
</div>
))}
</div>
</Card>
{/* Daily Traffic */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Tráfico Semanal</h3>
<div className="h-64 flex items-end space-x-2 justify-between">
{dailyTraffic.map((data, index) => (
<div key={index} className="flex flex-col items-center flex-1">
<div className="text-xs text-gray-600 mb-1">{data.visitors}</div>
<div
className="w-full bg-green-500 rounded-t"
style={{
height: `${(data.visitors / maxDailyVisitors) * 200}px`,
minHeight: '8px'
}}
></div>
<span className="text-sm text-gray-700 mt-2 font-medium">
{data.day}
</span>
<div className="text-xs text-gray-500 mt-1">
{data.conversion}%
</div>
</div>
))}
</div>
</Card>
{/* Traffic Sources */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Fuentes de Tráfico</h3>
<div className="space-y-3">
{trafficSources.map((source, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-3">
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
<div>
<p className="text-sm font-medium text-gray-900">{source.source}</p>
<p className="text-xs text-gray-500">{source.visitors} visitantes</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-gray-900">{source.percentage}%</p>
<div className={`text-xs flex items-center ${getTrendColor(source.trend)}`}>
<span>{getTrendIcon(source.trend)} {Math.abs(source.trend).toFixed(1)}%</span>
</div>
</div>
</div>
))}
</div>
</Card>
{/* Customer Segments */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Segmentos de Clientes</h3>
<div className="space-y-4">
{customerSegments.map((segment, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-gray-900">{segment.segment}</h4>
<Badge variant="blue">{segment.percentage}%</Badge>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-gray-500">Clientes</p>
<p className="font-medium">{segment.count}</p>
</div>
<div>
<p className="text-gray-500">Gasto Promedio</p>
<p className="font-medium">€{segment.avgSpend}</p>
</div>
<div>
<p className="text-gray-500">Horario Pico</p>
<p className="font-medium">{segment.peakHours.join(', ')}</p>
</div>
<div>
<p className="text-gray-500">Frecuencia</p>
<p className="font-medium">{segment.frequency}</p>
</div>
</div>
</div>
))}
</div>
</Card>
</div>
{/* Traffic Heat Map placeholder */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Mapa de Calor - Zonas de la Panadería</h3>
<div className="h-64 bg-gray-100 rounded-lg flex items-center justify-center">
<div className="text-center">
<MapPin className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-600">Visualización de zonas de mayor tráfico</p>
<p className="text-sm text-gray-500 mt-1">Entrada: 45% • Mostrador: 32% • Zona sentada: 23%</p>
</div>
</div>
</Card>
</div>
);
};
export default TrafficPage;

View File

@@ -1,423 +0,0 @@
import React, { useState } from 'react';
import { Cloud, Sun, CloudRain, Thermometer, Wind, Droplets, Calendar, TrendingUp } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const WeatherPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('week');
const currentWeather = {
temperature: 18,
condition: 'partly-cloudy',
humidity: 65,
windSpeed: 12,
pressure: 1013,
uvIndex: 4,
visibility: 10,
description: 'Parcialmente nublado'
};
const forecast = [
{
date: '2024-01-27',
day: 'Sábado',
condition: 'sunny',
tempMax: 22,
tempMin: 12,
humidity: 45,
precipitation: 0,
wind: 8,
impact: 'high-demand',
recommendation: 'Incrementar producción de helados y bebidas frías'
},
{
date: '2024-01-28',
day: 'Domingo',
condition: 'partly-cloudy',
tempMax: 19,
tempMin: 11,
humidity: 55,
precipitation: 20,
wind: 15,
impact: 'normal',
recommendation: 'Producción estándar'
},
{
date: '2024-01-29',
day: 'Lunes',
condition: 'rainy',
tempMax: 15,
tempMin: 8,
humidity: 85,
precipitation: 80,
wind: 22,
impact: 'comfort-food',
recommendation: 'Aumentar sopas, chocolates calientes y pan recién horneado'
},
{
date: '2024-01-30',
day: 'Martes',
condition: 'cloudy',
tempMax: 16,
tempMin: 9,
humidity: 70,
precipitation: 40,
wind: 18,
impact: 'moderate',
recommendation: 'Enfoque en productos de interior'
},
{
date: '2024-01-31',
day: 'Miércoles',
condition: 'sunny',
tempMax: 24,
tempMin: 14,
humidity: 40,
precipitation: 0,
wind: 10,
impact: 'high-demand',
recommendation: 'Incrementar productos frescos y ensaladas'
}
];
const weatherImpacts = [
{
condition: 'Día Soleado',
icon: Sun,
impact: 'Aumento del 25% en bebidas frías',
recommendations: [
'Incrementar producción de helados',
'Más bebidas refrescantes',
'Ensaladas y productos frescos',
'Horario extendido de terraza'
],
color: 'yellow'
},
{
condition: 'Día Lluvioso',
icon: CloudRain,
impact: 'Aumento del 40% en productos calientes',
recommendations: [
'Más sopas y caldos',
'Chocolates calientes',
'Pan recién horneado',
'Productos de repostería'
],
color: 'blue'
},
{
condition: 'Frío Intenso',
icon: Thermometer,
impact: 'Preferencia por comida reconfortante',
recommendations: [
'Aumentar productos horneados',
'Bebidas calientes especiales',
'Productos energéticos',
'Promociones de interior'
],
color: 'purple'
}
];
const seasonalTrends = [
{
season: 'Primavera',
period: 'Mar - May',
trends: [
'Aumento en productos frescos (+30%)',
'Mayor demanda de ensaladas',
'Bebidas naturales populares',
'Horarios extendidos efectivos'
],
avgTemp: '15-20°C',
impact: 'positive'
},
{
season: 'Verano',
period: 'Jun - Ago',
trends: [
'Pico de helados y granizados (+60%)',
'Productos ligeros preferidos',
'Horario matutino crítico',
'Mayor tráfico de turistas'
],
avgTemp: '25-35°C',
impact: 'high'
},
{
season: 'Otoño',
period: 'Sep - Nov',
trends: [
'Regreso a productos tradicionales',
'Aumento en bollería (+20%)',
'Bebidas calientes populares',
'Horarios regulares'
],
avgTemp: '10-18°C',
impact: 'stable'
},
{
season: 'Invierno',
period: 'Dec - Feb',
trends: [
'Máximo de productos calientes (+50%)',
'Pan recién horneado crítico',
'Chocolates y dulces festivos',
'Menor tráfico general (-15%)'
],
avgTemp: '5-12°C',
impact: 'comfort'
}
];
const getWeatherIcon = (condition: string) => {
const iconProps = { className: "w-8 h-8" };
switch (condition) {
case 'sunny': return <Sun {...iconProps} className="w-8 h-8 text-yellow-500" />;
case 'partly-cloudy': return <Cloud {...iconProps} className="w-8 h-8 text-gray-400" />;
case 'cloudy': return <Cloud {...iconProps} className="w-8 h-8 text-gray-600" />;
case 'rainy': return <CloudRain {...iconProps} className="w-8 h-8 text-blue-500" />;
default: return <Cloud {...iconProps} />;
}
};
const getConditionLabel = (condition: string) => {
switch (condition) {
case 'sunny': return 'Soleado';
case 'partly-cloudy': return 'Parcialmente nublado';
case 'cloudy': return 'Nublado';
case 'rainy': return 'Lluvioso';
default: return condition;
}
};
const getImpactColor = (impact: string) => {
switch (impact) {
case 'high-demand': return 'green';
case 'comfort-food': return 'orange';
case 'moderate': return 'blue';
case 'normal': return 'gray';
default: return 'gray';
}
};
const getImpactLabel = (impact: string) => {
switch (impact) {
case 'high-demand': return 'Alta Demanda';
case 'comfort-food': return 'Comida Reconfortante';
case 'moderate': return 'Demanda Moderada';
case 'normal': return 'Demanda Normal';
default: return impact;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Datos Meteorológicos"
description="Integra información del clima para optimizar la producción y ventas"
/>
{/* Current Weather */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Condiciones Actuales</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="flex items-center space-x-4">
{getWeatherIcon(currentWeather.condition)}
<div>
<p className="text-3xl font-bold text-gray-900">{currentWeather.temperature}°C</p>
<p className="text-sm text-gray-600">{currentWeather.description}</p>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Droplets className="w-4 h-4 text-blue-500" />
<span className="text-sm text-gray-600">Humedad: {currentWeather.humidity}%</span>
</div>
<div className="flex items-center space-x-2">
<Wind className="w-4 h-4 text-gray-500" />
<span className="text-sm text-gray-600">Viento: {currentWeather.windSpeed} km/h</span>
</div>
</div>
<div className="space-y-2">
<div className="text-sm text-gray-600">
<span className="font-medium">Presión:</span> {currentWeather.pressure} hPa
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">UV:</span> {currentWeather.uvIndex}
</div>
</div>
<div className="space-y-2">
<div className="text-sm text-gray-600">
<span className="font-medium">Visibilidad:</span> {currentWeather.visibility} km
</div>
<Badge variant="blue">Condiciones favorables</Badge>
</div>
</div>
</Card>
{/* Weather Forecast */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">Pronóstico Extendido</h3>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md text-sm"
>
<option value="week">Próxima Semana</option>
<option value="month">Próximo Mes</option>
</select>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
{forecast.map((day, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="text-center mb-3">
<p className="font-medium text-gray-900">{day.day}</p>
<p className="text-xs text-gray-500">{new Date(day.date).toLocaleDateString('es-ES')}</p>
</div>
<div className="flex justify-center mb-3">
{getWeatherIcon(day.condition)}
</div>
<div className="text-center mb-3">
<p className="text-sm text-gray-600">{getConditionLabel(day.condition)}</p>
<p className="text-lg font-semibold">
{day.tempMax}° <span className="text-sm text-gray-500">/ {day.tempMin}°</span>
</p>
</div>
<div className="space-y-2 text-xs text-gray-600">
<div className="flex justify-between">
<span>Humedad:</span>
<span>{day.humidity}%</span>
</div>
<div className="flex justify-between">
<span>Lluvia:</span>
<span>{day.precipitation}%</span>
</div>
<div className="flex justify-between">
<span>Viento:</span>
<span>{day.wind} km/h</span>
</div>
</div>
<div className="mt-3">
<Badge variant={getImpactColor(day.impact)} className="text-xs">
{getImpactLabel(day.impact)}
</Badge>
</div>
<div className="mt-2">
<p className="text-xs text-gray-600">{day.recommendation}</p>
</div>
</div>
))}
</div>
</Card>
{/* Weather Impact Analysis */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Impacto del Clima</h3>
<div className="space-y-4">
{weatherImpacts.map((impact, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center space-x-3 mb-3">
<div className={`p-2 rounded-lg bg-${impact.color}-100`}>
<impact.icon className={`w-5 h-5 text-${impact.color}-600`} />
</div>
<div>
<h4 className="font-medium text-gray-900">{impact.condition}</h4>
<p className="text-sm text-gray-600">{impact.impact}</p>
</div>
</div>
<div className="ml-10">
<p className="text-sm font-medium text-gray-700 mb-2">Recomendaciones:</p>
<ul className="text-sm text-gray-600 space-y-1">
{impact.recommendations.map((rec, idx) => (
<li key={idx} className="flex items-center">
<span className="w-1 h-1 bg-gray-400 rounded-full mr-2"></span>
{rec}
</li>
))}
</ul>
</div>
</div>
))}
</div>
</Card>
{/* Seasonal Trends */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Tendencias Estacionales</h3>
<div className="space-y-4">
{seasonalTrends.map((season, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div>
<h4 className="font-medium text-gray-900">{season.season}</h4>
<p className="text-sm text-gray-500">{season.period}</p>
</div>
<div className="text-right">
<p className="text-sm font-medium text-gray-700">{season.avgTemp}</p>
<Badge variant={
season.impact === 'high' ? 'green' :
season.impact === 'positive' ? 'blue' :
season.impact === 'comfort' ? 'orange' : 'gray'
}>
{season.impact === 'high' ? 'Alto' :
season.impact === 'positive' ? 'Positivo' :
season.impact === 'comfort' ? 'Confort' : 'Estable'}
</Badge>
</div>
</div>
<ul className="text-sm text-gray-600 space-y-1">
{season.trends.map((trend, idx) => (
<li key={idx} className="flex items-center">
<TrendingUp className="w-3 h-3 mr-2 text-green-500" />
{trend}
</li>
))}
</ul>
</div>
))}
</div>
</Card>
</div>
{/* Weather Alerts */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Alertas Meteorológicas</h3>
<div className="space-y-3">
<div className="flex items-center p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<Sun className="w-5 h-5 text-yellow-600 mr-3" />
<div>
<p className="text-sm font-medium text-yellow-800">Ola de calor prevista</p>
<p className="text-sm text-yellow-700">Se esperan temperaturas superiores a 30°C los próximos 3 días</p>
<p className="text-xs text-yellow-600 mt-1">Recomendación: Incrementar stock de bebidas frías y helados</p>
</div>
</div>
<div className="flex items-center p-3 bg-blue-50 border border-blue-200 rounded-lg">
<CloudRain className="w-5 h-5 text-blue-600 mr-3" />
<div>
<p className="text-sm font-medium text-blue-800">Lluvia intensa el lunes</p>
<p className="text-sm text-blue-700">80% probabilidad de precipitación con vientos fuertes</p>
<p className="text-xs text-blue-600 mt-1">Recomendación: Preparar más productos calientes y de refugio</p>
</div>
</div>
</div>
</Card>
</div>
);
};
export default WeatherPage;

View File

@@ -1,232 +0,0 @@
import React, { useState } from 'react';
import { Plus, Search, Filter, Download, AlertTriangle } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { InventoryTable, InventoryForm, LowStockAlert } from '../../../../components/domain/inventory';
const InventoryPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [selectedItem, setSelectedItem] = useState(null);
const [filterCategory, setFilterCategory] = useState('all');
const [filterStatus, setFilterStatus] = useState('all');
const mockInventoryItems = [
{
id: '1',
name: 'Harina de Trigo',
category: 'Harinas',
currentStock: 45,
minStock: 20,
maxStock: 100,
unit: 'kg',
cost: 1.20,
supplier: 'Molinos del Sur',
lastRestocked: '2024-01-20',
expirationDate: '2024-06-30',
status: 'normal',
},
{
id: '2',
name: 'Levadura Fresca',
category: 'Levaduras',
currentStock: 8,
minStock: 10,
maxStock: 25,
unit: 'kg',
cost: 8.50,
supplier: 'Levaduras SA',
lastRestocked: '2024-01-25',
expirationDate: '2024-02-15',
status: 'low',
},
{
id: '3',
name: 'Mantequilla',
category: 'Lácteos',
currentStock: 15,
minStock: 5,
maxStock: 30,
unit: 'kg',
cost: 5.80,
supplier: 'Lácteos Frescos',
lastRestocked: '2024-01-24',
expirationDate: '2024-02-10',
status: 'normal',
},
];
const lowStockItems = mockInventoryItems.filter(item => item.status === 'low');
const stats = {
totalItems: mockInventoryItems.length,
lowStockItems: lowStockItems.length,
totalValue: mockInventoryItems.reduce((sum, item) => sum + (item.currentStock * item.cost), 0),
needsReorder: lowStockItems.length,
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Inventario"
description="Controla el stock de ingredientes y materias primas"
action={
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nuevo Artículo
</Button>
}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Artículos</p>
<p className="text-3xl font-bold text-gray-900">{stats.totalItems}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4-8-4m16 0v10l-8 4-8-4V7" />
</svg>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Stock Bajo</p>
<p className="text-3xl font-bold text-red-600">{stats.lowStockItems}</p>
</div>
<div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center">
<AlertTriangle className="h-6 w-6 text-red-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Valor Total</p>
<p className="text-3xl font-bold text-green-600">€{stats.totalValue.toFixed(2)}</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
</svg>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Necesita Reorden</p>
<p className="text-3xl font-bold text-orange-600">{stats.needsReorder}</p>
</div>
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-orange-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
</div>
</Card>
</div>
{/* Low Stock Alert */}
{lowStockItems.length > 0 && (
<LowStockAlert items={lowStockItems} />
)}
{/* Filters and Search */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Buscar artículos..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
<option value="all">Todas las categorías</option>
<option value="Harinas">Harinas</option>
<option value="Levaduras">Levaduras</option>
<option value="Lácteos">Lácteos</option>
<option value="Grasas">Grasas</option>
<option value="Azúcares">Azúcares</option>
<option value="Especias">Especias</option>
</select>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
<option value="all">Todos los estados</option>
<option value="normal">Stock normal</option>
<option value="low">Stock bajo</option>
<option value="out">Sin stock</option>
<option value="expired">Caducado</option>
</select>
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Más filtros
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
</div>
</Card>
{/* Inventory Table */}
<Card>
<InventoryTable
items={mockInventoryItems}
searchTerm={searchTerm}
filterCategory={filterCategory}
filterStatus={filterStatus}
onEdit={(item) => {
setSelectedItem(item);
setShowForm(true);
}}
/>
</Card>
{/* Inventory Form Modal */}
{showForm && (
<InventoryForm
item={selectedItem}
onClose={() => {
setShowForm(false);
setSelectedItem(null);
}}
onSave={(item) => {
// Handle save logic
console.log('Saving item:', item);
setShowForm(false);
setSelectedItem(null);
}}
/>
)}
</div>
);
};
export default InventoryPage;

View File

@@ -1,405 +0,0 @@
import React, { useState } from 'react';
import { Plus, Search, Filter, Download, Calendar, Clock, User, Package } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { OrdersTable, OrderForm } from '../../../../components/domain/sales';
const OrdersPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [selectedOrder, setSelectedOrder] = useState(null);
const mockOrders = [
{
id: 'ORD-2024-001',
customerName: 'María García',
customerEmail: 'maria@email.com',
customerPhone: '+34 600 123 456',
status: 'pending',
orderDate: '2024-01-26T09:30:00Z',
deliveryDate: '2024-01-26T16:00:00Z',
items: [
{ id: '1', name: 'Pan de Molde Integral', quantity: 2, price: 4.50, total: 9.00 },
{ id: '2', name: 'Croissants de Mantequilla', quantity: 6, price: 1.50, total: 9.00 },
],
subtotal: 18.00,
tax: 1.89,
discount: 0,
total: 19.89,
paymentMethod: 'card',
paymentStatus: 'pending',
deliveryMethod: 'pickup',
notes: 'Sin gluten por favor en el pan',
priority: 'normal',
},
{
id: 'ORD-2024-002',
customerName: 'Juan Pérez',
customerEmail: 'juan@email.com',
customerPhone: '+34 600 654 321',
status: 'completed',
orderDate: '2024-01-25T14:15:00Z',
deliveryDate: '2024-01-25T18:30:00Z',
items: [
{ id: '3', name: 'Tarta de Chocolate', quantity: 1, price: 25.00, total: 25.00 },
{ id: '4', name: 'Magdalenas', quantity: 12, price: 0.75, total: 9.00 },
],
subtotal: 34.00,
tax: 3.57,
discount: 2.00,
total: 35.57,
paymentMethod: 'cash',
paymentStatus: 'paid',
deliveryMethod: 'delivery',
notes: 'Cumpleaños - decoración especial',
priority: 'high',
},
{
id: 'ORD-2024-003',
customerName: 'Ana Martínez',
customerEmail: 'ana@email.com',
customerPhone: '+34 600 987 654',
status: 'in_progress',
orderDate: '2024-01-26T07:45:00Z',
deliveryDate: '2024-01-26T12:00:00Z',
items: [
{ id: '5', name: 'Baguettes Francesas', quantity: 4, price: 2.80, total: 11.20 },
{ id: '6', name: 'Empanadas', quantity: 8, price: 2.50, total: 20.00 },
],
subtotal: 31.20,
tax: 3.28,
discount: 0,
total: 34.48,
paymentMethod: 'transfer',
paymentStatus: 'paid',
deliveryMethod: 'pickup',
notes: '',
priority: 'normal',
},
];
const getStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
in_progress: { color: 'blue', text: 'En Proceso' },
ready: { color: 'green', text: 'Listo' },
completed: { color: 'green', text: 'Completado' },
cancelled: { color: 'red', text: 'Cancelado' },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
};
const getPriorityBadge = (priority: string) => {
const priorityConfig = {
low: { color: 'gray', text: 'Baja' },
normal: { color: 'blue', text: 'Normal' },
high: { color: 'orange', text: 'Alta' },
urgent: { color: 'red', text: 'Urgente' },
};
const config = priorityConfig[priority as keyof typeof priorityConfig];
return <Badge variant={config?.color as any}>{config?.text || priority}</Badge>;
};
const getPaymentStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
paid: { color: 'green', text: 'Pagado' },
failed: { color: 'red', text: 'Fallido' },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
};
const filteredOrders = mockOrders.filter(order => {
const matchesSearch = order.customerName.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.id.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.customerEmail.toLowerCase().includes(searchTerm.toLowerCase());
const matchesTab = activeTab === 'all' || order.status === activeTab;
return matchesSearch && matchesTab;
});
const stats = {
total: mockOrders.length,
pending: mockOrders.filter(o => o.status === 'pending').length,
inProgress: mockOrders.filter(o => o.status === 'in_progress').length,
completed: mockOrders.filter(o => o.status === 'completed').length,
totalRevenue: mockOrders.reduce((sum, order) => sum + order.total, 0),
};
const tabs = [
{ id: 'all', label: 'Todos', count: stats.total },
{ id: 'pending', label: 'Pendientes', count: stats.pending },
{ id: 'in_progress', label: 'En Proceso', count: stats.inProgress },
{ id: 'ready', label: 'Listos', count: 0 },
{ id: 'completed', label: 'Completados', count: stats.completed },
];
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Pedidos"
description="Administra y controla todos los pedidos de tu panadería"
action={
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nuevo Pedido
</Button>
}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Pedidos</p>
<p className="text-2xl font-bold text-gray-900">{stats.total}</p>
</div>
<Package className="h-8 w-8 text-blue-600" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Pendientes</p>
<p className="text-2xl font-bold text-orange-600">{stats.pending}</p>
</div>
<Clock className="h-8 w-8 text-orange-600" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">En Proceso</p>
<p className="text-2xl font-bold text-blue-600">{stats.inProgress}</p>
</div>
<div className="h-8 w-8 bg-blue-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Completados</p>
<p className="text-2xl font-bold text-green-600">{stats.completed}</p>
</div>
<div className="h-8 w-8 bg-green-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Ingresos</p>
<p className="text-2xl font-bold text-purple-600">€{stats.totalRevenue.toFixed(2)}</p>
</div>
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
</svg>
</div>
</div>
</Card>
</div>
{/* Tabs Navigation */}
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center ${
activeTab === tab.id
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{tab.label}
{tab.count > 0 && (
<span className="ml-2 bg-gray-100 text-gray-900 py-0.5 px-2.5 rounded-full text-xs">
{tab.count}
</span>
)}
</button>
))}
</nav>
</div>
{/* Search and Filters */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Buscar pedidos por cliente, ID o email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
<Button variant="outline">
<Calendar className="w-4 h-4 mr-2" />
Fecha
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
</div>
</Card>
{/* Orders Table */}
<Card>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Pedido
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Cliente
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Estado
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Prioridad
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Fecha Pedido
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Entrega
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Total
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Pago
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredOrders.map((order) => (
<tr key={order.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{order.id}</div>
<div className="text-xs text-gray-500">{order.deliveryMethod === 'delivery' ? 'Entrega' : 'Recogida'}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<User className="h-4 w-4 text-gray-400 mr-2" />
<div>
<div className="text-sm font-medium text-gray-900">{order.customerName}</div>
<div className="text-xs text-gray-500">{order.customerEmail}</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(order.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getPriorityBadge(order.priority)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{new Date(order.orderDate).toLocaleDateString('es-ES')}
<div className="text-xs text-gray-500">
{new Date(order.orderDate).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
})}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{new Date(order.deliveryDate).toLocaleDateString('es-ES')}
<div className="text-xs text-gray-500">
{new Date(order.deliveryDate).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
})}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">€{order.total.toFixed(2)}</div>
<div className="text-xs text-gray-500">{order.items.length} artículos</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getPaymentStatusBadge(order.paymentStatus)}
<div className="text-xs text-gray-500 capitalize">{order.paymentMethod}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedOrder(order);
setShowForm(true);
}}
>
Ver
</Button>
<Button variant="outline" size="sm">
Editar
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
{/* Order Form Modal */}
{showForm && (
<OrderForm
order={selectedOrder}
onClose={() => {
setShowForm(false);
setSelectedOrder(null);
}}
onSave={(order) => {
// Handle save logic
console.log('Saving order:', order);
setShowForm(false);
setSelectedOrder(null);
}}
/>
)}
</div>
);
};
export default OrdersPage;

View File

@@ -1,368 +0,0 @@
import React, { useState } from 'react';
import { Plus, Minus, ShoppingCart, CreditCard, Banknote, Calculator, User, Receipt } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const POSPage: React.FC = () => {
const [cart, setCart] = useState<Array<{
id: string;
name: string;
price: number;
quantity: number;
category: string;
}>>([]);
const [selectedCategory, setSelectedCategory] = useState('all');
const [customerInfo, setCustomerInfo] = useState({
name: '',
email: '',
phone: '',
});
const [paymentMethod, setPaymentMethod] = useState<'cash' | 'card' | 'transfer'>('cash');
const [cashReceived, setCashReceived] = useState('');
const products = [
{
id: '1',
name: 'Pan de Molde Integral',
price: 4.50,
category: 'bread',
stock: 25,
image: '/api/placeholder/100/100',
},
{
id: '2',
name: 'Croissants de Mantequilla',
price: 1.50,
category: 'pastry',
stock: 32,
image: '/api/placeholder/100/100',
},
{
id: '3',
name: 'Baguette Francesa',
price: 2.80,
category: 'bread',
stock: 18,
image: '/api/placeholder/100/100',
},
{
id: '4',
name: 'Tarta de Chocolate',
price: 25.00,
category: 'cake',
stock: 8,
image: '/api/placeholder/100/100',
},
{
id: '5',
name: 'Magdalenas',
price: 0.75,
category: 'pastry',
stock: 48,
image: '/api/placeholder/100/100',
},
{
id: '6',
name: 'Empanadas',
price: 2.50,
category: 'other',
stock: 24,
image: '/api/placeholder/100/100',
},
];
const categories = [
{ id: 'all', name: 'Todos' },
{ id: 'bread', name: 'Panes' },
{ id: 'pastry', name: 'Bollería' },
{ id: 'cake', name: 'Tartas' },
{ id: 'other', name: 'Otros' },
];
const filteredProducts = products.filter(product =>
selectedCategory === 'all' || product.category === selectedCategory
);
const addToCart = (product: typeof products[0]) => {
setCart(prevCart => {
const existingItem = prevCart.find(item => item.id === product.id);
if (existingItem) {
return prevCart.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
return [...prevCart, {
id: product.id,
name: product.name,
price: product.price,
quantity: 1,
category: product.category,
}];
}
});
};
const updateQuantity = (id: string, quantity: number) => {
if (quantity <= 0) {
setCart(prevCart => prevCart.filter(item => item.id !== id));
} else {
setCart(prevCart =>
prevCart.map(item =>
item.id === id ? { ...item, quantity } : item
)
);
}
};
const clearCart = () => {
setCart([]);
};
const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const taxRate = 0.21; // 21% IVA
const tax = subtotal * taxRate;
const total = subtotal + tax;
const change = cashReceived ? Math.max(0, parseFloat(cashReceived) - total) : 0;
const processPayment = () => {
if (cart.length === 0) return;
// Process payment logic here
console.log('Processing payment:', {
cart,
customerInfo,
paymentMethod,
total,
cashReceived: paymentMethod === 'cash' ? parseFloat(cashReceived) : undefined,
change: paymentMethod === 'cash' ? change : undefined,
});
// Clear cart after successful payment
setCart([]);
setCustomerInfo({ name: '', email: '', phone: '' });
setCashReceived('');
alert('Venta procesada exitosamente');
};
return (
<div className="p-6 h-screen flex flex-col">
<PageHeader
title="Punto de Venta"
description="Sistema de ventas integrado"
/>
<div className="flex-1 grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6">
{/* Products Section */}
<div className="lg:col-span-2 space-y-6">
{/* Categories */}
<div className="flex space-x-2 overflow-x-auto">
{categories.map(category => (
<Button
key={category.id}
variant={selectedCategory === category.id ? 'default' : 'outline'}
onClick={() => setSelectedCategory(category.id)}
className="whitespace-nowrap"
>
{category.name}
</Button>
))}
</div>
{/* Products Grid */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{filteredProducts.map(product => (
<Card
key={product.id}
className="p-4 cursor-pointer hover:shadow-md transition-shadow"
onClick={() => addToCart(product)}
>
<img
src={product.image}
alt={product.name}
className="w-full h-20 object-cover rounded mb-3"
/>
<h3 className="font-medium text-sm mb-1 line-clamp-2">{product.name}</h3>
<p className="text-lg font-bold text-green-600">€{product.price.toFixed(2)}</p>
<p className="text-xs text-gray-500">Stock: {product.stock}</p>
</Card>
))}
</div>
</div>
{/* Cart and Checkout Section */}
<div className="space-y-6">
{/* Cart */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold flex items-center">
<ShoppingCart className="w-5 h-5 mr-2" />
Carrito ({cart.length})
</h3>
{cart.length > 0 && (
<Button variant="outline" size="sm" onClick={clearCart}>
Limpiar
</Button>
)}
</div>
<div className="space-y-3 max-h-64 overflow-y-auto">
{cart.length === 0 ? (
<p className="text-gray-500 text-center py-8">Carrito vacío</p>
) : (
cart.map(item => (
<div key={item.id} className="flex items-center justify-between p-3 bg-gray-50 rounded">
<div className="flex-1">
<h4 className="text-sm font-medium">{item.name}</h4>
<p className="text-xs text-gray-500">€{item.price.toFixed(2)} c/u</p>
</div>
<div className="flex items-center space-x-2">
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
updateQuantity(item.id, item.quantity - 1);
}}
>
<Minus className="w-3 h-3" />
</Button>
<span className="w-8 text-center text-sm">{item.quantity}</span>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
updateQuantity(item.id, item.quantity + 1);
}}
>
<Plus className="w-3 h-3" />
</Button>
</div>
<div className="ml-4 text-right">
<p className="text-sm font-medium">€{(item.price * item.quantity).toFixed(2)}</p>
</div>
</div>
))
)}
</div>
{cart.length > 0 && (
<div className="mt-4 pt-4 border-t">
<div className="space-y-2">
<div className="flex justify-between">
<span>Subtotal:</span>
<span>€{subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span>IVA (21%):</span>
<span>€{tax.toFixed(2)}</span>
</div>
<div className="flex justify-between text-lg font-bold border-t pt-2">
<span>Total:</span>
<span>€{total.toFixed(2)}</span>
</div>
</div>
</div>
)}
</Card>
{/* Customer Info */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center">
<User className="w-5 h-5 mr-2" />
Cliente (Opcional)
</h3>
<div className="space-y-3">
<Input
placeholder="Nombre"
value={customerInfo.name}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, name: e.target.value }))}
/>
<Input
placeholder="Email"
type="email"
value={customerInfo.email}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, email: e.target.value }))}
/>
<Input
placeholder="Teléfono"
value={customerInfo.phone}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, phone: e.target.value }))}
/>
</div>
</Card>
{/* Payment */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center">
<Calculator className="w-5 h-5 mr-2" />
Método de Pago
</h3>
<div className="space-y-3">
<div className="grid grid-cols-3 gap-2">
<Button
variant={paymentMethod === 'cash' ? 'default' : 'outline'}
onClick={() => setPaymentMethod('cash')}
className="flex items-center justify-center"
>
<Banknote className="w-4 h-4 mr-1" />
Efectivo
</Button>
<Button
variant={paymentMethod === 'card' ? 'default' : 'outline'}
onClick={() => setPaymentMethod('card')}
className="flex items-center justify-center"
>
<CreditCard className="w-4 h-4 mr-1" />
Tarjeta
</Button>
<Button
variant={paymentMethod === 'transfer' ? 'default' : 'outline'}
onClick={() => setPaymentMethod('transfer')}
className="flex items-center justify-center"
>
Transferencia
</Button>
</div>
{paymentMethod === 'cash' && (
<div className="space-y-2">
<Input
placeholder="Efectivo recibido"
type="number"
step="0.01"
value={cashReceived}
onChange={(e) => setCashReceived(e.target.value)}
/>
{cashReceived && parseFloat(cashReceived) >= total && (
<div className="p-2 bg-green-50 rounded text-center">
<p className="text-sm text-green-600">
Cambio: <span className="font-bold">€{change.toFixed(2)}</span>
</p>
</div>
)}
</div>
)}
<Button
onClick={processPayment}
disabled={cart.length === 0 || (paymentMethod === 'cash' && (!cashReceived || parseFloat(cashReceived) < total))}
className="w-full"
size="lg"
>
<Receipt className="w-5 h-5 mr-2" />
Procesar Venta - €{total.toFixed(2)}
</Button>
</div>
</Card>
</div>
</div>
</div>
);
};
export default POSPage;

View File

@@ -1,449 +0,0 @@
import React, { useState } from 'react';
import { Plus, Search, Filter, Download, ShoppingCart, Truck, DollarSign, Calendar } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const ProcurementPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('orders');
const [searchTerm, setSearchTerm] = useState('');
const mockPurchaseOrders = [
{
id: 'PO-2024-001',
supplier: 'Molinos del Sur',
status: 'pending',
orderDate: '2024-01-25',
deliveryDate: '2024-01-28',
totalAmount: 1250.00,
items: [
{ name: 'Harina de Trigo', quantity: 50, unit: 'kg', price: 1.20, total: 60.00 },
{ name: 'Harina Integral', quantity: 100, unit: 'kg', price: 1.30, total: 130.00 },
],
paymentStatus: 'pending',
notes: 'Entrega en horario de mañana',
},
{
id: 'PO-2024-002',
supplier: 'Levaduras SA',
status: 'delivered',
orderDate: '2024-01-20',
deliveryDate: '2024-01-23',
totalAmount: 425.50,
items: [
{ name: 'Levadura Fresca', quantity: 5, unit: 'kg', price: 8.50, total: 42.50 },
{ name: 'Mejorante', quantity: 10, unit: 'kg', price: 12.30, total: 123.00 },
],
paymentStatus: 'paid',
notes: '',
},
{
id: 'PO-2024-003',
supplier: 'Lácteos Frescos',
status: 'in_transit',
orderDate: '2024-01-24',
deliveryDate: '2024-01-26',
totalAmount: 320.75,
items: [
{ name: 'Mantequilla', quantity: 20, unit: 'kg', price: 5.80, total: 116.00 },
{ name: 'Nata', quantity: 15, unit: 'L', price: 3.25, total: 48.75 },
],
paymentStatus: 'pending',
notes: 'Producto refrigerado',
},
];
const mockSuppliers = [
{
id: '1',
name: 'Molinos del Sur',
contact: 'Juan Pérez',
email: 'juan@molinosdelsur.com',
phone: '+34 91 234 5678',
category: 'Harinas',
rating: 4.8,
totalOrders: 24,
totalSpent: 15600.00,
paymentTerms: '30 días',
leadTime: '2-3 días',
location: 'Sevilla',
status: 'active',
},
{
id: '2',
name: 'Levaduras SA',
contact: 'María González',
email: 'maria@levaduras.com',
phone: '+34 93 456 7890',
category: 'Levaduras',
rating: 4.6,
totalOrders: 18,
totalSpent: 8450.00,
paymentTerms: '15 días',
leadTime: '1-2 días',
location: 'Barcelona',
status: 'active',
},
{
id: '3',
name: 'Lácteos Frescos',
contact: 'Carlos Ruiz',
email: 'carlos@lacteosfrescos.com',
phone: '+34 96 789 0123',
category: 'Lácteos',
rating: 4.4,
totalOrders: 32,
totalSpent: 12300.00,
paymentTerms: '20 días',
leadTime: '1 día',
location: 'Valencia',
status: 'active',
},
];
const getStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
approved: { color: 'blue', text: 'Aprobado' },
in_transit: { color: 'purple', text: 'En Tránsito' },
delivered: { color: 'green', text: 'Entregado' },
cancelled: { color: 'red', text: 'Cancelado' },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
};
const getPaymentStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
paid: { color: 'green', text: 'Pagado' },
overdue: { color: 'red', text: 'Vencido' },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
};
const stats = {
totalOrders: mockPurchaseOrders.length,
pendingOrders: mockPurchaseOrders.filter(o => o.status === 'pending').length,
totalSpent: mockPurchaseOrders.reduce((sum, order) => sum + order.totalAmount, 0),
activeSuppliers: mockSuppliers.filter(s => s.status === 'active').length,
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Compras"
description="Administra órdenes de compra, proveedores y seguimiento de entregas"
action={
<Button>
<Plus className="w-4 h-4 mr-2" />
Nueva Orden de Compra
</Button>
}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Órdenes Totales</p>
<p className="text-3xl font-bold text-gray-900">{stats.totalOrders}</p>
</div>
<ShoppingCart className="h-12 w-12 text-blue-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Órdenes Pendientes</p>
<p className="text-3xl font-bold text-orange-600">{stats.pendingOrders}</p>
</div>
<Calendar className="h-12 w-12 text-orange-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Gasto Total</p>
<p className="text-3xl font-bold text-green-600">€{stats.totalSpent.toLocaleString()}</p>
</div>
<DollarSign className="h-12 w-12 text-green-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Proveedores Activos</p>
<p className="text-3xl font-bold text-purple-600">{stats.activeSuppliers}</p>
</div>
<Truck className="h-12 w-12 text-purple-600" />
</div>
</Card>
</div>
{/* Tabs Navigation */}
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('orders')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'orders'
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Órdenes de Compra
</button>
<button
onClick={() => setActiveTab('suppliers')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'suppliers'
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Proveedores
</button>
<button
onClick={() => setActiveTab('analytics')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'analytics'
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Análisis
</button>
</nav>
</div>
{/* Search and Filters */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder={`Buscar ${activeTab === 'orders' ? 'órdenes' : 'proveedores'}...`}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
</div>
</Card>
{/* Tab Content */}
{activeTab === 'orders' && (
<Card>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Orden
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Proveedor
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Estado
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Fecha Pedido
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Fecha Entrega
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Monto Total
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Pago
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{mockPurchaseOrders.map((order) => (
<tr key={order.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{order.id}</div>
{order.notes && (
<div className="text-xs text-gray-500">{order.notes}</div>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{order.supplier}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(order.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{new Date(order.orderDate).toLocaleDateString('es-ES')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{new Date(order.deliveryDate).toLocaleDateString('es-ES')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
€{order.totalAmount.toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getPaymentStatusBadge(order.paymentStatus)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex space-x-2">
<Button variant="outline" size="sm">Ver</Button>
<Button variant="outline" size="sm">Editar</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
{activeTab === 'suppliers' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{mockSuppliers.map((supplier) => (
<Card key={supplier.id} className="p-6">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">{supplier.name}</h3>
<p className="text-sm text-gray-600">{supplier.category}</p>
</div>
<Badge variant="green">Activo</Badge>
</div>
<div className="space-y-2 mb-4">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Contacto:</span>
<span className="font-medium">{supplier.contact}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Email:</span>
<span className="font-medium">{supplier.email}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Teléfono:</span>
<span className="font-medium">{supplier.phone}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Ubicación:</span>
<span className="font-medium">{supplier.location}</span>
</div>
</div>
<div className="border-t pt-4">
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<p className="text-xs text-gray-600">Valoración</p>
<p className="text-sm font-medium flex items-center">
<span className="text-yellow-500">★</span>
<span className="ml-1">{supplier.rating}</span>
</p>
</div>
<div>
<p className="text-xs text-gray-600">Pedidos</p>
<p className="text-sm font-medium">{supplier.totalOrders}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<p className="text-xs text-gray-600">Total Gastado</p>
<p className="text-sm font-medium">€{supplier.totalSpent.toLocaleString()}</p>
</div>
<div>
<p className="text-xs text-gray-600">Tiempo Entrega</p>
<p className="text-sm font-medium">{supplier.leadTime}</p>
</div>
</div>
<div className="mb-4">
<p className="text-xs text-gray-600">Condiciones de Pago</p>
<p className="text-sm font-medium">{supplier.paymentTerms}</p>
</div>
<div className="flex space-x-2">
<Button variant="outline" size="sm" className="flex-1">
Ver Detalles
</Button>
<Button size="sm" className="flex-1">
Nuevo Pedido
</Button>
</div>
</div>
</Card>
))}
</div>
)}
{activeTab === 'analytics' && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Gastos por Mes</h3>
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
<p className="text-gray-500">Gráfico de gastos mensuales</p>
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Top Proveedores</h3>
<div className="space-y-3">
{mockSuppliers
.sort((a, b) => b.totalSpent - a.totalSpent)
.slice(0, 5)
.map((supplier, index) => (
<div key={supplier.id} className="flex items-center justify-between">
<div className="flex items-center">
<span className="text-sm font-medium text-gray-500 w-4">
{index + 1}.
</span>
<span className="ml-3 text-sm text-gray-900">{supplier.name}</span>
</div>
<span className="text-sm font-medium text-gray-900">
€{supplier.totalSpent.toLocaleString()}
</span>
</div>
))}
</div>
</Card>
</div>
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Gastos por Categoría</h3>
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
<p className="text-gray-500">Gráfico de gastos por categoría</p>
</div>
</Card>
</div>
)}
</div>
);
};
export default ProcurementPage;

View File

@@ -1,315 +0,0 @@
import React, { useState } from 'react';
import { Plus, Calendar, Clock, Users, AlertCircle } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { ProductionSchedule, BatchTracker, QualityControl } from '../../../../components/domain/production';
const ProductionPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('schedule');
const mockProductionStats = {
dailyTarget: 150,
completed: 85,
inProgress: 12,
pending: 53,
efficiency: 78,
quality: 94,
};
const mockProductionOrders = [
{
id: '1',
recipeName: 'Pan de Molde Integral',
quantity: 20,
status: 'in_progress',
priority: 'high',
assignedTo: 'Juan Panadero',
startTime: '2024-01-26T06:00:00Z',
estimatedCompletion: '2024-01-26T10:00:00Z',
progress: 65,
},
{
id: '2',
recipeName: 'Croissants de Mantequilla',
quantity: 50,
status: 'pending',
priority: 'medium',
assignedTo: 'María González',
startTime: '2024-01-26T08:00:00Z',
estimatedCompletion: '2024-01-26T12:00:00Z',
progress: 0,
},
{
id: '3',
recipeName: 'Baguettes Francesas',
quantity: 30,
status: 'completed',
priority: 'medium',
assignedTo: 'Carlos Ruiz',
startTime: '2024-01-26T04:00:00Z',
estimatedCompletion: '2024-01-26T08:00:00Z',
progress: 100,
},
];
const getStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
in_progress: { color: 'blue', text: 'En Proceso' },
completed: { color: 'green', text: 'Completado' },
cancelled: { color: 'red', text: 'Cancelado' },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config.color as any}>{config.text}</Badge>;
};
const getPriorityBadge = (priority: string) => {
const priorityConfig = {
low: { color: 'gray', text: 'Baja' },
medium: { color: 'yellow', text: 'Media' },
high: { color: 'orange', text: 'Alta' },
urgent: { color: 'red', text: 'Urgente' },
};
const config = priorityConfig[priority as keyof typeof priorityConfig];
return <Badge variant={config.color as any}>{config.text}</Badge>;
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Producción"
description="Planifica y controla la producción diaria de tu panadería"
action={
<Button>
<Plus className="w-4 h-4 mr-2" />
Nueva Orden de Producción
</Button>
}
/>
{/* Production Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Meta Diaria</p>
<p className="text-2xl font-bold text-gray-900">{mockProductionStats.dailyTarget}</p>
</div>
<Calendar className="h-8 w-8 text-blue-600" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Completado</p>
<p className="text-2xl font-bold text-green-600">{mockProductionStats.completed}</p>
</div>
<div className="h-8 w-8 bg-green-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">En Proceso</p>
<p className="text-2xl font-bold text-blue-600">{mockProductionStats.inProgress}</p>
</div>
<Clock className="h-8 w-8 text-blue-600" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Pendiente</p>
<p className="text-2xl font-bold text-orange-600">{mockProductionStats.pending}</p>
</div>
<AlertCircle className="h-8 w-8 text-orange-600" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Eficiencia</p>
<p className="text-2xl font-bold text-purple-600">{mockProductionStats.efficiency}%</p>
</div>
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Calidad</p>
<p className="text-2xl font-bold text-indigo-600">{mockProductionStats.quality}%</p>
</div>
<div className="h-8 w-8 bg-indigo-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</Card>
</div>
{/* Tabs Navigation */}
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('schedule')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'schedule'
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Programación
</button>
<button
onClick={() => setActiveTab('batches')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'batches'
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Lotes de Producción
</button>
<button
onClick={() => setActiveTab('quality')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'quality'
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Control de Calidad
</button>
</nav>
</div>
{/* Tab Content */}
{activeTab === 'schedule' && (
<Card>
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-medium text-gray-900">Órdenes de Producción</h3>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
Filtros
</Button>
<Button variant="outline" size="sm">
Vista Calendario
</Button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Receta
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Cantidad
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Estado
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Prioridad
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Asignado a
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Progreso
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tiempo Estimado
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{mockProductionOrders.map((order) => (
<tr key={order.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{order.recipeName}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{order.quantity} unidades
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(order.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getPriorityBadge(order.priority)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<Users className="h-4 w-4 text-gray-400 mr-2" />
<span className="text-sm text-gray-900">{order.assignedTo}</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="w-full bg-gray-200 rounded-full h-2 mr-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${order.progress}%` }}
></div>
</div>
<span className="text-sm text-gray-900">{order.progress}%</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{new Date(order.estimatedCompletion).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
})}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<Button variant="outline" size="sm" className="mr-2">
Ver
</Button>
<Button variant="outline" size="sm">
Editar
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</Card>
)}
{activeTab === 'batches' && (
<BatchTracker />
)}
{activeTab === 'quality' && (
<QualityControl />
)}
</div>
);
};
export default ProductionPage;

View File

@@ -1,412 +0,0 @@
import React, { useState } from 'react';
import { Plus, Search, Filter, Star, Clock, Users, DollarSign } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const RecipesPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('all');
const [selectedDifficulty, setSelectedDifficulty] = useState('all');
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const mockRecipes = [
{
id: '1',
name: 'Pan de Molde Integral',
category: 'bread',
difficulty: 'medium',
prepTime: 120,
bakingTime: 35,
yield: 1,
rating: 4.8,
cost: 2.50,
price: 4.50,
profit: 2.00,
image: '/api/placeholder/300/200',
tags: ['integral', 'saludable', 'artesanal'],
description: 'Pan integral artesanal con semillas, perfecto para desayunos saludables.',
ingredients: [
{ name: 'Harina integral', quantity: 500, unit: 'g' },
{ name: 'Agua', quantity: 300, unit: 'ml' },
{ name: 'Levadura', quantity: 10, unit: 'g' },
{ name: 'Sal', quantity: 8, unit: 'g' },
],
},
{
id: '2',
name: 'Croissants de Mantequilla',
category: 'pastry',
difficulty: 'hard',
prepTime: 480,
bakingTime: 20,
yield: 12,
rating: 4.9,
cost: 8.50,
price: 18.00,
profit: 9.50,
image: '/api/placeholder/300/200',
tags: ['francés', 'mantequilla', 'hojaldrado'],
description: 'Croissants franceses tradicionales con laminado de mantequilla.',
ingredients: [
{ name: 'Harina de fuerza', quantity: 500, unit: 'g' },
{ name: 'Mantequilla', quantity: 250, unit: 'g' },
{ name: 'Leche', quantity: 150, unit: 'ml' },
{ name: 'Azúcar', quantity: 50, unit: 'g' },
],
},
{
id: '3',
name: 'Tarta de Manzana',
category: 'cake',
difficulty: 'easy',
prepTime: 45,
bakingTime: 40,
yield: 8,
rating: 4.6,
cost: 4.20,
price: 12.00,
profit: 7.80,
image: '/api/placeholder/300/200',
tags: ['frutal', 'casera', 'temporada'],
description: 'Tarta casera de manzana con canela y masa quebrada.',
ingredients: [
{ name: 'Manzanas', quantity: 1000, unit: 'g' },
{ name: 'Harina', quantity: 250, unit: 'g' },
{ name: 'Mantequilla', quantity: 125, unit: 'g' },
{ name: 'Azúcar', quantity: 100, unit: 'g' },
],
},
];
const categories = [
{ value: 'all', label: 'Todas las categorías' },
{ value: 'bread', label: 'Panes' },
{ value: 'pastry', label: 'Bollería' },
{ value: 'cake', label: 'Tartas' },
{ value: 'cookie', label: 'Galletas' },
{ value: 'other', label: 'Otros' },
];
const difficulties = [
{ value: 'all', label: 'Todas las dificultades' },
{ value: 'easy', label: 'Fácil' },
{ value: 'medium', label: 'Medio' },
{ value: 'hard', label: 'Difícil' },
];
const getCategoryBadge = (category: string) => {
const categoryConfig = {
bread: { color: 'brown', text: 'Pan' },
pastry: { color: 'yellow', text: 'Bollería' },
cake: { color: 'pink', text: 'Tarta' },
cookie: { color: 'orange', text: 'Galleta' },
other: { color: 'gray', text: 'Otro' },
};
const config = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other;
return <Badge variant={config.color as any}>{config.text}</Badge>;
};
const getDifficultyBadge = (difficulty: string) => {
const difficultyConfig = {
easy: { color: 'green', text: 'Fácil' },
medium: { color: 'yellow', text: 'Medio' },
hard: { color: 'red', text: 'Difícil' },
};
const config = difficultyConfig[difficulty as keyof typeof difficultyConfig];
return <Badge variant={config.color as any}>{config.text}</Badge>;
};
const formatTime = (minutes: number) => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
};
const filteredRecipes = mockRecipes.filter(recipe => {
const matchesSearch = recipe.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
recipe.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
recipe.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesCategory = selectedCategory === 'all' || recipe.category === selectedCategory;
const matchesDifficulty = selectedDifficulty === 'all' || recipe.difficulty === selectedDifficulty;
return matchesSearch && matchesCategory && matchesDifficulty;
});
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Recetas"
description="Administra y organiza todas las recetas de tu panadería"
action={
<Button>
<Plus className="w-4 h-4 mr-2" />
Nueva Receta
</Button>
}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Recetas</p>
<p className="text-3xl font-bold text-gray-900">{mockRecipes.length}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Más Populares</p>
<p className="text-3xl font-bold text-yellow-600">
{mockRecipes.filter(r => r.rating > 4.7).length}
</p>
</div>
<Star className="h-12 w-12 text-yellow-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Costo Promedio</p>
<p className="text-3xl font-bold text-green-600">
€{(mockRecipes.reduce((sum, r) => sum + r.cost, 0) / mockRecipes.length).toFixed(2)}
</p>
</div>
<DollarSign className="h-12 w-12 text-green-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Margen Promedio</p>
<p className="text-3xl font-bold text-purple-600">
€{(mockRecipes.reduce((sum, r) => sum + r.profit, 0) / mockRecipes.length).toFixed(2)}
</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
</div>
</Card>
</div>
{/* Filters and Search */}
<Card className="p-6">
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Buscar recetas por nombre, ingredientes o etiquetas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
{categories.map(cat => (
<option key={cat.value} value={cat.value}>{cat.label}</option>
))}
</select>
<select
value={selectedDifficulty}
onChange={(e) => setSelectedDifficulty(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
{difficulties.map(diff => (
<option key={diff.value} value={diff.value}>{diff.label}</option>
))}
</select>
<Button
variant="outline"
onClick={() => setViewMode(viewMode === 'grid' ? 'list' : 'grid')}
>
{viewMode === 'grid' ? 'Vista Lista' : 'Vista Cuadrícula'}
</Button>
</div>
</div>
</Card>
{/* Recipes Grid/List */}
{viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredRecipes.map((recipe) => (
<Card key={recipe.id} className="overflow-hidden hover:shadow-lg transition-shadow">
<div className="aspect-w-16 aspect-h-9">
<img
src={recipe.image}
alt={recipe.name}
className="w-full h-48 object-cover"
/>
</div>
<div className="p-6">
<div className="flex items-start justify-between mb-2">
<h3 className="text-lg font-semibold text-gray-900 line-clamp-1">
{recipe.name}
</h3>
<div className="flex items-center ml-2">
<Star className="h-4 w-4 text-yellow-400 fill-current" />
<span className="text-sm text-gray-600 ml-1">{recipe.rating}</span>
</div>
</div>
<p className="text-gray-600 text-sm mb-3 line-clamp-2">
{recipe.description}
</p>
<div className="flex flex-wrap gap-2 mb-3">
{getCategoryBadge(recipe.category)}
{getDifficultyBadge(recipe.difficulty)}
</div>
<div className="grid grid-cols-2 gap-4 mb-4 text-sm text-gray-600">
<div className="flex items-center">
<Clock className="h-4 w-4 mr-1" />
<span>{formatTime(recipe.prepTime + recipe.bakingTime)}</span>
</div>
<div className="flex items-center">
<Users className="h-4 w-4 mr-1" />
<span>{recipe.yield} porciones</span>
</div>
</div>
<div className="flex justify-between items-center mb-4">
<div className="text-sm">
<span className="text-gray-600">Costo: </span>
<span className="font-medium">€{recipe.cost.toFixed(2)}</span>
</div>
<div className="text-sm">
<span className="text-gray-600">Precio: </span>
<span className="font-medium text-green-600">€{recipe.price.toFixed(2)}</span>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" className="flex-1">
Ver Receta
</Button>
<Button size="sm" className="flex-1">
Producir
</Button>
</div>
</div>
</Card>
))}
</div>
) : (
<Card>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Receta
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Categoría
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Dificultad
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tiempo Total
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Rendimiento
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Costo
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Precio
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Margen
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredRecipes.map((recipe) => (
<tr key={recipe.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<img
src={recipe.image}
alt={recipe.name}
className="h-10 w-10 rounded-full mr-4"
/>
<div>
<div className="text-sm font-medium text-gray-900">{recipe.name}</div>
<div className="flex items-center">
<Star className="h-3 w-3 text-yellow-400 fill-current" />
<span className="text-xs text-gray-500 ml-1">{recipe.rating}</span>
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getCategoryBadge(recipe.category)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getDifficultyBadge(recipe.difficulty)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{formatTime(recipe.prepTime + recipe.bakingTime)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{recipe.yield} porciones
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
€{recipe.cost.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-green-600 font-medium">
€{recipe.price.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-purple-600 font-medium">
€{recipe.profit.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex space-x-2">
<Button variant="outline" size="sm">Ver</Button>
<Button size="sm">Producir</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
</div>
);
};
export default RecipesPage;

View File

@@ -3,7 +3,7 @@ import { Store, MapPin, Clock, Phone, Mail, Globe, Save, X, Edit3, Zap, Plus, Se
import { Button, Card, Input, Select, Modal, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { useToast } from '../../../../hooks/ui/useToast';
import { posService, POSConfiguration } from '../../../../services/api/pos.service';
import { posService, POSConfiguration } from '../../../../api/services/pos.service';
interface BakeryConfig {
// General Info

View File

@@ -1,481 +0,0 @@
import React, { useState } from 'react';
import { Store, MapPin, Clock, Phone, Mail, Globe, Save, RotateCcw } from 'lucide-react';
import { Button, Card, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const BakeryConfigPage: React.FC = () => {
const [config, setConfig] = useState({
general: {
name: 'Panadería Artesanal San Miguel',
description: 'Panadería tradicional con más de 30 años de experiencia',
logo: '',
website: 'https://panaderiasanmiguel.com',
email: 'info@panaderiasanmiguel.com',
phone: '+34 912 345 678'
},
location: {
address: 'Calle Mayor 123',
city: 'Madrid',
postalCode: '28001',
country: 'España',
coordinates: {
lat: 40.4168,
lng: -3.7038
}
},
schedule: {
monday: { open: '07:00', close: '20:00', closed: false },
tuesday: { open: '07:00', close: '20:00', closed: false },
wednesday: { open: '07:00', close: '20:00', closed: false },
thursday: { open: '07:00', close: '20:00', closed: false },
friday: { open: '07:00', close: '20:00', closed: false },
saturday: { open: '08:00', close: '14:00', closed: false },
sunday: { open: '09:00', close: '13:00', closed: false }
},
business: {
taxId: 'B12345678',
registrationNumber: 'REG-2024-001',
licenseNumber: 'LIC-FOOD-2024',
currency: 'EUR',
timezone: 'Europe/Madrid',
language: 'es'
},
preferences: {
enableOnlineOrders: true,
enableReservations: false,
enableDelivery: true,
deliveryRadius: 5,
minimumOrderAmount: 15.00,
enableLoyaltyProgram: true,
autoBackup: true,
emailNotifications: true,
smsNotifications: false
}
});
const [hasChanges, setHasChanges] = useState(false);
const [activeTab, setActiveTab] = useState('general');
const tabs = [
{ id: 'general', label: 'General', icon: Store },
{ id: 'location', label: 'Ubicación', icon: MapPin },
{ id: 'schedule', label: 'Horarios', icon: Clock },
{ id: 'business', label: 'Empresa', icon: Globe }
];
const daysOfWeek = [
{ key: 'monday', label: 'Lunes' },
{ key: 'tuesday', label: 'Martes' },
{ key: 'wednesday', label: 'Miércoles' },
{ key: 'thursday', label: 'Jueves' },
{ key: 'friday', label: 'Viernes' },
{ key: 'saturday', label: 'Sábado' },
{ key: 'sunday', label: 'Domingo' }
];
const handleInputChange = (section: string, field: string, value: any) => {
setConfig(prev => ({
...prev,
[section]: {
...prev[section as keyof typeof prev],
[field]: value
}
}));
setHasChanges(true);
};
const handleScheduleChange = (day: string, field: string, value: any) => {
setConfig(prev => ({
...prev,
schedule: {
...prev.schedule,
[day]: {
...prev.schedule[day as keyof typeof prev.schedule],
[field]: value
}
}
}));
setHasChanges(true);
};
const handleSave = () => {
// Handle save logic
console.log('Saving bakery config:', config);
setHasChanges(false);
};
const handleReset = () => {
// Reset to defaults
setHasChanges(false);
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Configuración de Panadería"
description="Configura los datos básicos y preferencias de tu panadería"
action={
<div className="flex space-x-2">
<Button variant="outline" onClick={handleReset}>
<RotateCcw className="w-4 h-4 mr-2" />
Restaurar
</Button>
<Button onClick={handleSave} disabled={!hasChanges}>
<Save className="w-4 h-4 mr-2" />
Guardar Cambios
</Button>
</div>
}
/>
<div className="flex flex-col lg:flex-row gap-6">
{/* Sidebar */}
<div className="w-full lg:w-64">
<Card className="p-4">
<nav className="space-y-2">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`w-full flex items-center space-x-3 px-3 py-2 text-left rounded-lg transition-colors ${
activeTab === tab.id
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<tab.icon className="w-4 h-4" />
<span className="text-sm font-medium">{tab.label}</span>
</button>
))}
</nav>
</Card>
</div>
{/* Content */}
<div className="flex-1">
{activeTab === 'general' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-6">Información General</h3>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nombre de la Panadería
</label>
<Input
value={config.general.name}
onChange={(e) => handleInputChange('general', 'name', e.target.value)}
placeholder="Nombre de tu panadería"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Sitio Web
</label>
<Input
value={config.general.website}
onChange={(e) => handleInputChange('general', 'website', e.target.value)}
placeholder="https://tu-panaderia.com"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Descripción
</label>
<textarea
value={config.general.description}
onChange={(e) => handleInputChange('general', 'description', e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="Describe tu panadería..."
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email de Contacto
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
value={config.general.email}
onChange={(e) => handleInputChange('general', 'email', e.target.value)}
className="pl-10"
type="email"
placeholder="contacto@panaderia.com"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Teléfono
</label>
<div className="relative">
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
value={config.general.phone}
onChange={(e) => handleInputChange('general', 'phone', e.target.value)}
className="pl-10"
type="tel"
placeholder="+34 912 345 678"
/>
</div>
</div>
</div>
</div>
</Card>
)}
{activeTab === 'location' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-6">Ubicación</h3>
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Dirección
</label>
<Input
value={config.location.address}
onChange={(e) => handleInputChange('location', 'address', e.target.value)}
placeholder="Calle, número, etc."
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Ciudad
</label>
<Input
value={config.location.city}
onChange={(e) => handleInputChange('location', 'city', e.target.value)}
placeholder="Ciudad"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Código Postal
</label>
<Input
value={config.location.postalCode}
onChange={(e) => handleInputChange('location', 'postalCode', e.target.value)}
placeholder="28001"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
País
</label>
<Input
value={config.location.country}
onChange={(e) => handleInputChange('location', 'country', e.target.value)}
placeholder="España"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Latitud
</label>
<Input
value={config.location.coordinates.lat}
onChange={(e) => handleInputChange('location', 'coordinates', {
...config.location.coordinates,
lat: parseFloat(e.target.value) || 0
})}
type="number"
step="0.000001"
placeholder="40.4168"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Longitud
</label>
<Input
value={config.location.coordinates.lng}
onChange={(e) => handleInputChange('location', 'coordinates', {
...config.location.coordinates,
lng: parseFloat(e.target.value) || 0
})}
type="number"
step="0.000001"
placeholder="-3.7038"
/>
</div>
</div>
</div>
</Card>
)}
{activeTab === 'schedule' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-6">Horarios de Apertura</h3>
<div className="space-y-4">
{daysOfWeek.map((day) => {
const schedule = config.schedule[day.key as keyof typeof config.schedule];
return (
<div key={day.key} className="flex items-center space-x-4 p-4 border rounded-lg">
<div className="w-20">
<span className="text-sm font-medium text-gray-700">{day.label}</span>
</div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={schedule.closed}
onChange={(e) => handleScheduleChange(day.key, 'closed', e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm text-gray-600">Cerrado</span>
</label>
{!schedule.closed && (
<>
<div>
<label className="block text-xs text-gray-500 mb-1">Apertura</label>
<input
type="time"
value={schedule.open}
onChange={(e) => handleScheduleChange(day.key, 'open', e.target.value)}
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Cierre</label>
<input
type="time"
value={schedule.close}
onChange={(e) => handleScheduleChange(day.key, 'close', e.target.value)}
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
/>
</div>
</>
)}
</div>
);
})}
</div>
</Card>
)}
{activeTab === 'business' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-6">Datos de Empresa</h3>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
NIF/CIF
</label>
<Input
value={config.business.taxId}
onChange={(e) => handleInputChange('business', 'taxId', e.target.value)}
placeholder="B12345678"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Número de Registro
</label>
<Input
value={config.business.registrationNumber}
onChange={(e) => handleInputChange('business', 'registrationNumber', e.target.value)}
placeholder="REG-2024-001"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Licencia Sanitaria
</label>
<Input
value={config.business.licenseNumber}
onChange={(e) => handleInputChange('business', 'licenseNumber', e.target.value)}
placeholder="LIC-FOOD-2024"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Moneda
</label>
<select
value={config.business.currency}
onChange={(e) => handleInputChange('business', 'currency', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="EUR">EUR (€)</option>
<option value="USD">USD ($)</option>
<option value="GBP">GBP (£)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Zona Horaria
</label>
<select
value={config.business.timezone}
onChange={(e) => handleInputChange('business', 'timezone', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="Europe/Madrid">Madrid (GMT+1)</option>
<option value="Europe/London">Londres (GMT)</option>
<option value="America/New_York">Nueva York (GMT-5)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Idioma
</label>
<select
value={config.business.language}
onChange={(e) => handleInputChange('business', 'language', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="es">Español</option>
<option value="en">English</option>
<option value="fr">Français</option>
</select>
</div>
</div>
</div>
</Card>
)}
</div>
</div>
{/* Save Changes Banner */}
{hasChanges && (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
<span className="text-sm">Tienes cambios sin guardar</span>
<div className="flex space-x-2">
<Button size="sm" variant="outline" className="text-blue-600 bg-white" onClick={handleReset}>
Descartar
</Button>
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
Guardar
</Button>
</div>
</div>
)}
</div>
);
};
export default BakeryConfigPage;

View File

@@ -3,4 +3,5 @@ export { default as ProfilePage } from './profile';
export { default as BakeryConfigPage } from './bakery-config';
export { default as TeamPage } from './team';
export { default as SubscriptionPage } from './subscription';
export { default as PreferencesPage } from './preferences';
export { default as PreferencesPage } from './preferences';
export { default as PreferencesPage } from './PreferencesPage';

View File

@@ -1,388 +0,0 @@
import React, { useState } from 'react';
import { Settings, Bell, Mail, MessageSquare, Smartphone, Save, RotateCcw } from 'lucide-react';
import { Button, Card } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const PreferencesPage: React.FC = () => {
const [preferences, setPreferences] = useState({
notifications: {
inventory: {
app: true,
email: false,
sms: true,
frequency: 'immediate'
},
sales: {
app: true,
email: true,
sms: false,
frequency: 'hourly'
},
production: {
app: true,
email: false,
sms: true,
frequency: 'immediate'
},
system: {
app: true,
email: true,
sms: false,
frequency: 'daily'
},
marketing: {
app: false,
email: true,
sms: false,
frequency: 'weekly'
}
},
global: {
doNotDisturb: false,
quietHours: {
enabled: false,
start: '22:00',
end: '07:00'
},
language: 'es',
timezone: 'Europe/Madrid',
soundEnabled: true,
vibrationEnabled: true
},
channels: {
email: 'panaderia@example.com',
phone: '+34 600 123 456',
slack: false,
webhook: ''
}
});
const [hasChanges, setHasChanges] = useState(false);
const categories = [
{
id: 'inventory',
name: 'Inventario',
description: 'Alertas de stock, reposiciones y vencimientos',
icon: '📦'
},
{
id: 'sales',
name: 'Ventas',
description: 'Pedidos, transacciones y reportes de ventas',
icon: '💰'
},
{
id: 'production',
name: 'Producción',
description: 'Hornadas, calidad y tiempos de producción',
icon: '🍞'
},
{
id: 'system',
name: 'Sistema',
description: 'Actualizaciones, mantenimiento y errores',
icon: '⚙️'
},
{
id: 'marketing',
name: 'Marketing',
description: 'Campañas, promociones y análisis',
icon: '📢'
}
];
const frequencies = [
{ value: 'immediate', label: 'Inmediato' },
{ value: 'hourly', label: 'Cada hora' },
{ value: 'daily', label: 'Diario' },
{ value: 'weekly', label: 'Semanal' }
];
const handleNotificationChange = (category: string, channel: string, value: boolean) => {
setPreferences(prev => ({
...prev,
notifications: {
...prev.notifications,
[category]: {
...prev.notifications[category as keyof typeof prev.notifications],
[channel]: value
}
}
}));
setHasChanges(true);
};
const handleFrequencyChange = (category: string, frequency: string) => {
setPreferences(prev => ({
...prev,
notifications: {
...prev.notifications,
[category]: {
...prev.notifications[category as keyof typeof prev.notifications],
frequency
}
}
}));
setHasChanges(true);
};
const handleGlobalChange = (setting: string, value: any) => {
setPreferences(prev => ({
...prev,
global: {
...prev.global,
[setting]: value
}
}));
setHasChanges(true);
};
const handleChannelChange = (channel: string, value: string | boolean) => {
setPreferences(prev => ({
...prev,
channels: {
...prev.channels,
[channel]: value
}
}));
setHasChanges(true);
};
const handleSave = () => {
// Handle save logic
console.log('Saving preferences:', preferences);
setHasChanges(false);
};
const handleReset = () => {
// Reset to defaults
setHasChanges(false);
};
const getChannelIcon = (channel: string) => {
switch (channel) {
case 'app':
return <Bell className="w-4 h-4" />;
case 'email':
return <Mail className="w-4 h-4" />;
case 'sms':
return <Smartphone className="w-4 h-4" />;
default:
return <MessageSquare className="w-4 h-4" />;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Preferencias de Comunicación"
description="Configura cómo y cuándo recibir notificaciones"
action={
<div className="flex space-x-2">
<Button variant="outline" onClick={handleReset}>
<RotateCcw className="w-4 h-4 mr-2" />
Restaurar
</Button>
<Button onClick={handleSave} disabled={!hasChanges}>
<Save className="w-4 h-4 mr-2" />
Guardar Cambios
</Button>
</div>
}
/>
{/* Global Settings */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Configuración General</h3>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={preferences.global.doNotDisturb}
onChange={(e) => handleGlobalChange('doNotDisturb', e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm font-medium text-gray-700">No molestar</span>
</label>
<p className="text-xs text-gray-500 mt-1">Silencia todas las notificaciones</p>
</div>
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={preferences.global.soundEnabled}
onChange={(e) => handleGlobalChange('soundEnabled', e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm font-medium text-gray-700">Sonidos</span>
</label>
<p className="text-xs text-gray-500 mt-1">Reproducir sonidos de notificación</p>
</div>
</div>
<div>
<label className="flex items-center space-x-2 mb-2">
<input
type="checkbox"
checked={preferences.global.quietHours.enabled}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
enabled: e.target.checked
})}
className="rounded border-gray-300"
/>
<span className="text-sm font-medium text-gray-700">Horas silenciosas</span>
</label>
{preferences.global.quietHours.enabled && (
<div className="flex space-x-4 ml-6">
<div>
<label className="block text-xs text-gray-500 mb-1">Desde</label>
<input
type="time"
value={preferences.global.quietHours.start}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
start: e.target.value
})}
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Hasta</label>
<input
type="time"
value={preferences.global.quietHours.end}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
end: e.target.value
})}
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
/>
</div>
</div>
)}
</div>
</div>
</Card>
{/* Channel Settings */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Canales de Comunicación</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Email</label>
<input
type="email"
value={preferences.channels.email}
onChange={(e) => handleChannelChange('email', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="tu-email@ejemplo.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Teléfono (SMS)</label>
<input
type="tel"
value={preferences.channels.phone}
onChange={(e) => handleChannelChange('phone', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="+34 600 123 456"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Webhook URL</label>
<input
type="url"
value={preferences.channels.webhook}
onChange={(e) => handleChannelChange('webhook', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="https://tu-webhook.com/notifications"
/>
<p className="text-xs text-gray-500 mt-1">URL para recibir notificaciones JSON</p>
</div>
</div>
</Card>
{/* Category Preferences */}
<div className="space-y-4">
{categories.map((category) => {
const categoryPrefs = preferences.notifications[category.id as keyof typeof preferences.notifications];
return (
<Card key={category.id} className="p-6">
<div className="flex items-start space-x-4">
<div className="text-2xl">{category.icon}</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 mb-1">{category.name}</h3>
<p className="text-sm text-gray-600 mb-4">{category.description}</p>
<div className="space-y-4">
{/* Channel toggles */}
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">Canales</h4>
<div className="flex space-x-6">
{['app', 'email', 'sms'].map((channel) => (
<label key={channel} className="flex items-center space-x-2">
<input
type="checkbox"
checked={categoryPrefs[channel as keyof typeof categoryPrefs] as boolean}
onChange={(e) => handleNotificationChange(category.id, channel, e.target.checked)}
className="rounded border-gray-300"
/>
<div className="flex items-center space-x-1">
{getChannelIcon(channel)}
<span className="text-sm text-gray-700 capitalize">{channel}</span>
</div>
</label>
))}
</div>
</div>
{/* Frequency */}
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">Frecuencia</h4>
<select
value={categoryPrefs.frequency}
onChange={(e) => handleFrequencyChange(category.id, e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md text-sm"
>
{frequencies.map((freq) => (
<option key={freq.value} value={freq.value}>
{freq.label}
</option>
))}
</select>
</div>
</div>
</div>
</div>
</Card>
);
})}
</div>
{/* Save Changes Banner */}
{hasChanges && (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
<span className="text-sm">Tienes cambios sin guardar</span>
<div className="flex space-x-2">
<Button size="sm" variant="outline" className="text-blue-600 bg-white" onClick={handleReset}>
Descartar
</Button>
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
Guardar
</Button>
</div>
</div>
)}
</div>
);
};
export default PreferencesPage;

View File

@@ -37,7 +37,7 @@ import {
subscriptionService,
type UsageSummary,
type AvailablePlans
} from '../../../../services/api';
} from '../../../../api/services';
import { isMockMode, getMockSubscription } from '../../../../config/mock.config';
interface PlanComparisonProps {

View File

@@ -37,7 +37,7 @@ import {
subscriptionService,
type UsageSummary,
type AvailablePlans
} from '../../../../services/api';
} from '../../../../api/services';
import { isMockMode, getMockSubscription } from '../../../../config/mock.config';
interface PlanComparisonProps {

View File

@@ -1,406 +0,0 @@
import React, { useState } from 'react';
import { Users, Plus, Search, Mail, Phone, Shield, Edit, Trash2, UserCheck, UserX } from 'lucide-react';
import { Button, Card, Badge, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const TeamPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedRole, setSelectedRole] = useState('all');
const [showForm, setShowForm] = useState(false);
const teamMembers = [
{
id: '1',
name: 'María González',
email: 'maria.gonzalez@panaderia.com',
phone: '+34 600 123 456',
role: 'manager',
department: 'Administración',
status: 'active',
joinDate: '2022-03-15',
lastLogin: '2024-01-26 09:30:00',
permissions: ['inventory', 'sales', 'reports', 'team'],
avatar: '/avatars/maria.jpg',
schedule: {
monday: '07:00-15:00',
tuesday: '07:00-15:00',
wednesday: '07:00-15:00',
thursday: '07:00-15:00',
friday: '07:00-15:00',
saturday: 'Libre',
sunday: 'Libre'
}
},
{
id: '2',
name: 'Carlos Rodríguez',
email: 'carlos.rodriguez@panaderia.com',
phone: '+34 600 234 567',
role: 'baker',
department: 'Producción',
status: 'active',
joinDate: '2021-09-20',
lastLogin: '2024-01-26 08:45:00',
permissions: ['production', 'inventory'],
avatar: '/avatars/carlos.jpg',
schedule: {
monday: '05:00-13:00',
tuesday: '05:00-13:00',
wednesday: '05:00-13:00',
thursday: '05:00-13:00',
friday: '05:00-13:00',
saturday: '05:00-11:00',
sunday: 'Libre'
}
},
{
id: '3',
name: 'Ana Martínez',
email: 'ana.martinez@panaderia.com',
phone: '+34 600 345 678',
role: 'cashier',
department: 'Ventas',
status: 'active',
joinDate: '2023-01-10',
lastLogin: '2024-01-26 10:15:00',
permissions: ['sales', 'pos'],
avatar: '/avatars/ana.jpg',
schedule: {
monday: '08:00-16:00',
tuesday: '08:00-16:00',
wednesday: 'Libre',
thursday: '08:00-16:00',
friday: '08:00-16:00',
saturday: '09:00-14:00',
sunday: '09:00-14:00'
}
},
{
id: '4',
name: 'Luis Fernández',
email: 'luis.fernandez@panaderia.com',
phone: '+34 600 456 789',
role: 'baker',
department: 'Producción',
status: 'inactive',
joinDate: '2020-11-05',
lastLogin: '2024-01-20 16:30:00',
permissions: ['production'],
avatar: '/avatars/luis.jpg',
schedule: {
monday: '13:00-21:00',
tuesday: '13:00-21:00',
wednesday: '13:00-21:00',
thursday: 'Libre',
friday: '13:00-21:00',
saturday: 'Libre',
sunday: '13:00-21:00'
}
},
{
id: '5',
name: 'Isabel Torres',
email: 'isabel.torres@panaderia.com',
phone: '+34 600 567 890',
role: 'assistant',
department: 'Ventas',
status: 'active',
joinDate: '2023-06-01',
lastLogin: '2024-01-25 18:20:00',
permissions: ['sales'],
avatar: '/avatars/isabel.jpg',
schedule: {
monday: 'Libre',
tuesday: '16:00-20:00',
wednesday: '16:00-20:00',
thursday: '16:00-20:00',
friday: '16:00-20:00',
saturday: '14:00-20:00',
sunday: '14:00-20:00'
}
}
];
const roles = [
{ value: 'all', label: 'Todos los Roles', count: teamMembers.length },
{ value: 'manager', label: 'Gerente', count: teamMembers.filter(m => m.role === 'manager').length },
{ value: 'baker', label: 'Panadero', count: teamMembers.filter(m => m.role === 'baker').length },
{ value: 'cashier', label: 'Cajero', count: teamMembers.filter(m => m.role === 'cashier').length },
{ value: 'assistant', label: 'Asistente', count: teamMembers.filter(m => m.role === 'assistant').length }
];
const teamStats = {
total: teamMembers.length,
active: teamMembers.filter(m => m.status === 'active').length,
departments: {
production: teamMembers.filter(m => m.department === 'Producción').length,
sales: teamMembers.filter(m => m.department === 'Ventas').length,
admin: teamMembers.filter(m => m.department === 'Administración').length
}
};
const getRoleBadgeColor = (role: string) => {
switch (role) {
case 'manager': return 'purple';
case 'baker': return 'green';
case 'cashier': return 'blue';
case 'assistant': return 'yellow';
default: return 'gray';
}
};
const getStatusColor = (status: string) => {
return status === 'active' ? 'green' : 'red';
};
const getRoleLabel = (role: string) => {
switch (role) {
case 'manager': return 'Gerente';
case 'baker': return 'Panadero';
case 'cashier': return 'Cajero';
case 'assistant': return 'Asistente';
default: return role;
}
};
const filteredMembers = teamMembers.filter(member => {
const matchesRole = selectedRole === 'all' || member.role === selectedRole;
const matchesSearch = member.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
member.email.toLowerCase().includes(searchTerm.toLowerCase());
return matchesRole && matchesSearch;
});
const formatLastLogin = (timestamp: string) => {
const date = new Date(timestamp);
const now = new Date();
const diffInDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
if (diffInDays === 0) {
return 'Hoy ' + date.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
} else if (diffInDays === 1) {
return 'Ayer';
} else {
return `hace ${diffInDays} días`;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Equipo"
description="Administra los miembros del equipo, roles y permisos"
action={
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nuevo Miembro
</Button>
}
/>
{/* Team Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Equipo</p>
<p className="text-3xl font-bold text-gray-900">{teamStats.total}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-blue-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Activos</p>
<p className="text-3xl font-bold text-green-600">{teamStats.active}</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<UserCheck className="h-6 w-6 text-green-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Producción</p>
<p className="text-3xl font-bold text-orange-600">{teamStats.departments.production}</p>
</div>
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-orange-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Ventas</p>
<p className="text-3xl font-bold text-purple-600">{teamStats.departments.sales}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
</div>
{/* Filters and Search */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Buscar miembros del equipo..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2 flex-wrap">
{roles.map((role) => (
<button
key={role.value}
onClick={() => setSelectedRole(role.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
selectedRole === role.value
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{role.label} ({role.count})
</button>
))}
</div>
</div>
</Card>
{/* Team Members List */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{filteredMembers.map((member) => (
<Card key={member.id} className="p-6">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center">
<Users className="w-6 h-6 text-gray-500" />
</div>
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-lg font-semibold text-gray-900">{member.name}</h3>
<Badge variant={getStatusColor(member.status)}>
{member.status === 'active' ? 'Activo' : 'Inactivo'}
</Badge>
</div>
<div className="space-y-1 mb-3">
<div className="flex items-center text-sm text-gray-600">
<Mail className="w-4 h-4 mr-2" />
{member.email}
</div>
<div className="flex items-center text-sm text-gray-600">
<Phone className="w-4 h-4 mr-2" />
{member.phone}
</div>
</div>
<div className="flex items-center space-x-2 mb-3">
<Badge variant={getRoleBadgeColor(member.role)}>
{getRoleLabel(member.role)}
</Badge>
<Badge variant="gray">
{member.department}
</Badge>
</div>
<div className="text-sm text-gray-500 mb-3">
<p>Se unió: {new Date(member.joinDate).toLocaleDateString('es-ES')}</p>
<p>Última conexión: {formatLastLogin(member.lastLogin)}</p>
</div>
{/* Permissions */}
<div className="mb-3">
<p className="text-xs font-medium text-gray-700 mb-2">Permisos:</p>
<div className="flex flex-wrap gap-1">
{member.permissions.map((permission, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full"
>
{permission}
</span>
))}
</div>
</div>
{/* Schedule Preview */}
<div className="text-xs text-gray-500">
<p className="font-medium mb-1">Horario esta semana:</p>
<div className="grid grid-cols-2 gap-1">
{Object.entries(member.schedule).slice(0, 4).map(([day, hours]) => (
<span key={day}>
{day.charAt(0).toUpperCase()}: {hours}
</span>
))}
</div>
</div>
</div>
</div>
<div className="flex space-x-2">
<Button size="sm" variant="outline">
<Edit className="w-4 h-4" />
</Button>
<Button
size="sm"
variant="outline"
className={member.status === 'active' ? 'text-red-600 hover:text-red-700' : 'text-green-600 hover:text-green-700'}
>
{member.status === 'active' ? <UserX className="w-4 h-4" /> : <UserCheck className="w-4 h-4" />}
</Button>
</div>
</div>
</Card>
))}
</div>
{filteredMembers.length === 0 && (
<Card className="p-12 text-center">
<Users className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">No se encontraron miembros</h3>
<p className="text-gray-600">
No hay miembros del equipo que coincidan con los filtros seleccionados.
</p>
</Card>
)}
{/* Add Member Modal Placeholder */}
{showForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<Card className="p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Nuevo Miembro del Equipo</h3>
<p className="text-gray-600 mb-4">
Formulario para agregar un nuevo miembro del equipo.
</p>
<div className="flex space-x-2">
<Button size="sm" onClick={() => setShowForm(false)}>
Guardar
</Button>
<Button size="sm" variant="outline" onClick={() => setShowForm(false)}>
Cancelar
</Button>
</div>
</Card>
</div>
)}
</div>
);
};
export default TeamPage;

View File

@@ -24,10 +24,9 @@ const SalesAnalyticsPage = React.lazy(() => import('../pages/app/analytics/sales
const AIInsightsPage = React.lazy(() => import('../pages/app/analytics/ai-insights/AIInsightsPage'));
const PerformanceAnalyticsPage = React.lazy(() => import('../pages/app/analytics/performance/PerformanceAnalyticsPage'));
// Communications pages
const PreferencesPage = React.lazy(() => import('../pages/app/communications/preferences/PreferencesPage'));
// Settings pages
const PreferencesPage = React.lazy(() => import('../pages/app/settings/preferences/PreferencesPage'));
const ProfilePage = React.lazy(() => import('../pages/app/settings/profile/ProfilePage'));
const BakeryConfigPage = React.lazy(() => import('../pages/app/settings/bakery-config/BakeryConfigPage'));
const TeamPage = React.lazy(() => import('../pages/app/settings/team/TeamPage'));
@@ -176,17 +175,6 @@ export const AppRouter: React.FC = () => {
}
/>
{/* Communications Routes */}
<Route
path="/app/communications/preferences"
element={
<ProtectedRoute>
<AppShell>
<PreferencesPage />
</AppShell>
</ProtectedRoute>
}
/>
{/* Settings Routes */}
<Route

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More