Start integrating the onboarding flow with backend 6
This commit is contained in:
176
frontend/src/api/client/apiClient.ts
Normal file
176
frontend/src/api/client/apiClient.ts
Normal 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;
|
||||
2
frontend/src/api/client/index.ts
Normal file
2
frontend/src/api/client/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { apiClient, default } from './apiClient';
|
||||
export type { ApiError } from './apiClient';
|
||||
171
frontend/src/api/hooks/auth.ts
Normal file
171
frontend/src/api/hooks/auth.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
200
frontend/src/api/hooks/classification.ts
Normal file
200
frontend/src/api/hooks/classification.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
384
frontend/src/api/hooks/foodSafety.ts
Normal file
384
frontend/src/api/hooks/foodSafety.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
372
frontend/src/api/hooks/inventory.ts
Normal file
372
frontend/src/api/hooks/inventory.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
183
frontend/src/api/hooks/inventoryDashboard.ts
Normal file
183
frontend/src/api/hooks/inventoryDashboard.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
128
frontend/src/api/hooks/onboarding.ts
Normal file
128
frontend/src/api/hooks/onboarding.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
190
frontend/src/api/hooks/sales.ts
Normal file
190
frontend/src/api/hooks/sales.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
316
frontend/src/api/hooks/tenant.ts
Normal file
316
frontend/src/api/hooks/tenant.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
112
frontend/src/api/hooks/user.ts
Normal file
112
frontend/src/api/hooks/user.ts
Normal 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
303
frontend/src/api/index.ts
Normal 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,
|
||||
};
|
||||
87
frontend/src/api/services/auth.ts
Normal file
87
frontend/src/api/services/auth.ts
Normal 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();
|
||||
94
frontend/src/api/services/classification.ts
Normal file
94
frontend/src/api/services/classification.ts
Normal 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();
|
||||
99
frontend/src/api/services/dataImport.ts
Normal file
99
frontend/src/api/services/dataImport.ts
Normal 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();
|
||||
273
frontend/src/api/services/foodSafety.ts
Normal file
273
frontend/src/api/services/foodSafety.ts
Normal 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();
|
||||
228
frontend/src/api/services/inventory.ts
Normal file
228
frontend/src/api/services/inventory.ts
Normal 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();
|
||||
138
frontend/src/api/services/inventoryDashboard.ts
Normal file
138
frontend/src/api/services/inventoryDashboard.ts
Normal 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();
|
||||
52
frontend/src/api/services/onboarding.ts
Normal file
52
frontend/src/api/services/onboarding.ts
Normal 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();
|
||||
126
frontend/src/api/services/sales.ts
Normal file
126
frontend/src/api/services/sales.ts
Normal 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();
|
||||
67
frontend/src/api/services/subscription.ts
Normal file
67
frontend/src/api/services/subscription.ts
Normal 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();
|
||||
145
frontend/src/api/services/tenant.ts
Normal file
145
frontend/src/api/services/tenant.ts
Normal 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();
|
||||
37
frontend/src/api/services/user.ts
Normal file
37
frontend/src/api/services/user.ts
Normal 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();
|
||||
92
frontend/src/api/types/auth.ts
Normal file
92
frontend/src/api/types/auth.ts
Normal 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[];
|
||||
}
|
||||
65
frontend/src/api/types/classification.ts
Normal file
65
frontend/src/api/types/classification.ts
Normal 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;
|
||||
}
|
||||
111
frontend/src/api/types/dashboard.ts
Normal file
111
frontend/src/api/types/dashboard.ts
Normal 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;
|
||||
}
|
||||
47
frontend/src/api/types/dataImport.ts
Normal file
47
frontend/src/api/types/dataImport.ts
Normal 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[];
|
||||
}
|
||||
226
frontend/src/api/types/foodSafety.ts
Normal file
226
frontend/src/api/types/foodSafety.ts
Normal 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;
|
||||
}>;
|
||||
}
|
||||
192
frontend/src/api/types/inventory.ts
Normal file
192
frontend/src/api/types/inventory.ts
Normal 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;
|
||||
}
|
||||
26
frontend/src/api/types/onboarding.ts
Normal file
26
frontend/src/api/types/onboarding.ts
Normal 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>;
|
||||
}
|
||||
137
frontend/src/api/types/sales.ts
Normal file
137
frontend/src/api/types/sales.ts
Normal 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;
|
||||
}
|
||||
43
frontend/src/api/types/subscription.ts
Normal file
43
frontend/src/api/types/subscription.ts
Normal 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;
|
||||
}
|
||||
109
frontend/src/api/types/tenant.ts
Normal file
109
frontend/src/api/types/tenant.ts
Normal 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;
|
||||
}
|
||||
24
frontend/src/api/types/user.ts
Normal file
24
frontend/src/api/types/user.ts
Normal 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';
|
||||
}
|
||||
Reference in New Issue
Block a user