Start integrating the onboarding flow with backend 6

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,92 @@
/**
* Auth API Types - Mirror backend schemas
*/
export interface User {
id: string;
email: string;
full_name: string;
is_active: boolean;
is_verified: boolean;
created_at: string;
last_login?: string;
phone?: string;
language?: string;
timezone?: string;
tenant_id?: string;
role?: string;
}
export interface UserRegistration {
email: string;
password: string;
full_name: string;
tenant_name?: string;
phone?: string;
language?: string;
timezone?: string;
}
export interface UserLogin {
email: string;
password: string;
}
export interface TokenResponse {
access_token: string;
refresh_token?: string;
token_type: string;
expires_in?: number;
user?: User;
}
export interface RefreshTokenRequest {
refresh_token: string;
}
export interface PasswordChange {
current_password: string;
new_password: string;
}
export interface PasswordReset {
email: string;
}
export interface UserResponse {
id: string;
email: string;
full_name: string;
is_active: boolean;
is_verified: boolean;
created_at: string;
last_login?: string;
phone?: string;
language?: string;
timezone?: string;
tenant_id?: string;
role?: string;
}
export interface UserUpdate {
full_name?: string;
phone?: string;
language?: string;
timezone?: string;
}
export interface TokenVerificationResponse {
valid: boolean;
user_id?: string;
email?: string;
role?: string;
exp?: number;
message?: string;
}
export interface AuthHealthResponse {
status: string;
service: string;
version: string;
features: string[];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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