Start integrating the onboarding flow with backend 6
This commit is contained in:
171
frontend/src/api/hooks/auth.ts
Normal file
171
frontend/src/api/hooks/auth.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Auth React Query hooks
|
||||
*/
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { authService } from '../services/auth';
|
||||
import {
|
||||
UserRegistration,
|
||||
UserLogin,
|
||||
TokenResponse,
|
||||
PasswordChange,
|
||||
PasswordReset,
|
||||
UserResponse,
|
||||
UserUpdate,
|
||||
TokenVerificationResponse,
|
||||
AuthHealthResponse
|
||||
} from '../types/auth';
|
||||
import { ApiError } from '../client';
|
||||
|
||||
// Query Keys
|
||||
export const authKeys = {
|
||||
all: ['auth'] as const,
|
||||
profile: () => [...authKeys.all, 'profile'] as const,
|
||||
health: () => [...authKeys.all, 'health'] as const,
|
||||
verify: (token?: string) => [...authKeys.all, 'verify', token] as const,
|
||||
} as const;
|
||||
|
||||
// Queries
|
||||
export const useAuthProfile = (
|
||||
options?: Omit<UseQueryOptions<UserResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<UserResponse, ApiError>({
|
||||
queryKey: authKeys.profile(),
|
||||
queryFn: () => authService.getProfile(),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAuthHealth = (
|
||||
options?: Omit<UseQueryOptions<AuthHealthResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<AuthHealthResponse, ApiError>({
|
||||
queryKey: authKeys.health(),
|
||||
queryFn: () => authService.healthCheck(),
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useVerifyToken = (
|
||||
token?: string,
|
||||
options?: Omit<UseQueryOptions<TokenVerificationResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<TokenVerificationResponse, ApiError>({
|
||||
queryKey: authKeys.verify(token),
|
||||
queryFn: () => authService.verifyToken(token),
|
||||
enabled: !!token,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Mutations
|
||||
export const useRegister = (
|
||||
options?: UseMutationOptions<TokenResponse, ApiError, UserRegistration>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<TokenResponse, ApiError, UserRegistration>({
|
||||
mutationFn: (userData: UserRegistration) => authService.register(userData),
|
||||
onSuccess: (data) => {
|
||||
// Update profile query with new user data
|
||||
if (data.user) {
|
||||
queryClient.setQueryData(authKeys.profile(), data.user);
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useLogin = (
|
||||
options?: UseMutationOptions<TokenResponse, ApiError, UserLogin>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<TokenResponse, ApiError, UserLogin>({
|
||||
mutationFn: (loginData: UserLogin) => authService.login(loginData),
|
||||
onSuccess: (data) => {
|
||||
// Update profile query with new user data
|
||||
if (data.user) {
|
||||
queryClient.setQueryData(authKeys.profile(), data.user);
|
||||
}
|
||||
// Invalidate all queries to refresh data
|
||||
queryClient.invalidateQueries({ queryKey: ['auth'] });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useRefreshToken = (
|
||||
options?: UseMutationOptions<TokenResponse, ApiError, string>
|
||||
) => {
|
||||
return useMutation<TokenResponse, ApiError, string>({
|
||||
mutationFn: (refreshToken: string) => authService.refreshToken(refreshToken),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useLogout = (
|
||||
options?: UseMutationOptions<{ message: string }, ApiError, string>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ message: string }, ApiError, string>({
|
||||
mutationFn: (refreshToken: string) => authService.logout(refreshToken),
|
||||
onSuccess: () => {
|
||||
// Clear all queries on logout
|
||||
queryClient.clear();
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useChangePassword = (
|
||||
options?: UseMutationOptions<{ message: string }, ApiError, PasswordChange>
|
||||
) => {
|
||||
return useMutation<{ message: string }, ApiError, PasswordChange>({
|
||||
mutationFn: (passwordData: PasswordChange) => authService.changePassword(passwordData),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useResetPassword = (
|
||||
options?: UseMutationOptions<{ message: string }, ApiError, PasswordReset>
|
||||
) => {
|
||||
return useMutation<{ message: string }, ApiError, PasswordReset>({
|
||||
mutationFn: (resetData: PasswordReset) => authService.resetPassword(resetData),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateProfile = (
|
||||
options?: UseMutationOptions<UserResponse, ApiError, UserUpdate>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<UserResponse, ApiError, UserUpdate>({
|
||||
mutationFn: (updateData: UserUpdate) => authService.updateProfile(updateData),
|
||||
onSuccess: (data) => {
|
||||
// Update the profile cache
|
||||
queryClient.setQueryData(authKeys.profile(), data);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useVerifyEmail = (
|
||||
options?: UseMutationOptions<{ message: string }, ApiError, { userId: string; verificationToken: string }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ message: string }, ApiError, { userId: string; verificationToken: string }>({
|
||||
mutationFn: ({ userId, verificationToken }) =>
|
||||
authService.verifyEmail(userId, verificationToken),
|
||||
onSuccess: () => {
|
||||
// Invalidate profile to get updated verification status
|
||||
queryClient.invalidateQueries({ queryKey: authKeys.profile() });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
200
frontend/src/api/hooks/classification.ts
Normal file
200
frontend/src/api/hooks/classification.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Classification React Query hooks
|
||||
*/
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { classificationService } from '../services/classification';
|
||||
import {
|
||||
ProductClassificationRequest,
|
||||
BatchClassificationRequest,
|
||||
ProductSuggestionResponse,
|
||||
BusinessModelAnalysisResponse,
|
||||
ClassificationApprovalRequest,
|
||||
ClassificationApprovalResponse,
|
||||
} from '../types/classification';
|
||||
import { ApiError } from '../client';
|
||||
|
||||
// Query Keys
|
||||
export const classificationKeys = {
|
||||
all: ['classification'] as const,
|
||||
suggestions: {
|
||||
all: () => [...classificationKeys.all, 'suggestions'] as const,
|
||||
pending: (tenantId: string) => [...classificationKeys.suggestions.all(), 'pending', tenantId] as const,
|
||||
history: (tenantId: string, limit?: number, offset?: number) =>
|
||||
[...classificationKeys.suggestions.all(), 'history', tenantId, { limit, offset }] as const,
|
||||
},
|
||||
analysis: {
|
||||
all: () => [...classificationKeys.all, 'analysis'] as const,
|
||||
businessModel: (tenantId: string) => [...classificationKeys.analysis.all(), 'business-model', tenantId] as const,
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Queries
|
||||
export const usePendingSuggestions = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<ProductSuggestionResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ProductSuggestionResponse[], ApiError>({
|
||||
queryKey: classificationKeys.suggestions.pending(tenantId),
|
||||
queryFn: () => classificationService.getPendingSuggestions(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useSuggestionHistory = (
|
||||
tenantId: string,
|
||||
limit: number = 50,
|
||||
offset: number = 0,
|
||||
options?: Omit<UseQueryOptions<{ items: ProductSuggestionResponse[]; total: number }, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<{ items: ProductSuggestionResponse[]; total: number }, ApiError>({
|
||||
queryKey: classificationKeys.suggestions.history(tenantId, limit, offset),
|
||||
queryFn: () => classificationService.getSuggestionHistory(tenantId, limit, offset),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useBusinessModelAnalysis = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<BusinessModelAnalysisResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<BusinessModelAnalysisResponse, ApiError>({
|
||||
queryKey: classificationKeys.analysis.businessModel(tenantId),
|
||||
queryFn: () => classificationService.getBusinessModelAnalysis(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Mutations
|
||||
export const useClassifyProduct = (
|
||||
options?: UseMutationOptions<
|
||||
ProductSuggestionResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; classificationData: ProductClassificationRequest }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
ProductSuggestionResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; classificationData: ProductClassificationRequest }
|
||||
>({
|
||||
mutationFn: ({ tenantId, classificationData }) =>
|
||||
classificationService.classifyProduct(tenantId, classificationData),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Invalidate pending suggestions to include the new one
|
||||
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.pending(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useClassifyProductsBatch = (
|
||||
options?: UseMutationOptions<
|
||||
ProductSuggestionResponse[],
|
||||
ApiError,
|
||||
{ tenantId: string; batchData: BatchClassificationRequest }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
ProductSuggestionResponse[],
|
||||
ApiError,
|
||||
{ tenantId: string; batchData: BatchClassificationRequest }
|
||||
>({
|
||||
mutationFn: ({ tenantId, batchData }) =>
|
||||
classificationService.classifyProductsBatch(tenantId, batchData),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Invalidate pending suggestions to include the new ones
|
||||
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.pending(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useApproveClassification = (
|
||||
options?: UseMutationOptions<
|
||||
ClassificationApprovalResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; approvalData: ClassificationApprovalRequest }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
ClassificationApprovalResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; approvalData: ClassificationApprovalRequest }
|
||||
>({
|
||||
mutationFn: ({ tenantId, approvalData }) =>
|
||||
classificationService.approveClassification(tenantId, approvalData),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Invalidate suggestions lists
|
||||
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.pending(tenantId) });
|
||||
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.history(tenantId) });
|
||||
|
||||
// If approved and ingredient was created, invalidate inventory queries
|
||||
if (data.approved && data.created_ingredient) {
|
||||
queryClient.invalidateQueries({ queryKey: ['inventory', 'ingredients'] });
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSuggestion = (
|
||||
options?: UseMutationOptions<
|
||||
ProductSuggestionResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; suggestionId: string; updateData: Partial<ProductSuggestionResponse> }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
ProductSuggestionResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; suggestionId: string; updateData: Partial<ProductSuggestionResponse> }
|
||||
>({
|
||||
mutationFn: ({ tenantId, suggestionId, updateData }) =>
|
||||
classificationService.updateSuggestion(tenantId, suggestionId, updateData),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Invalidate suggestions lists
|
||||
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.pending(tenantId) });
|
||||
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.history(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSuggestion = (
|
||||
options?: UseMutationOptions<
|
||||
{ message: string },
|
||||
ApiError,
|
||||
{ tenantId: string; suggestionId: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
{ message: string },
|
||||
ApiError,
|
||||
{ tenantId: string; suggestionId: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, suggestionId }) =>
|
||||
classificationService.deleteSuggestion(tenantId, suggestionId),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Invalidate suggestions lists
|
||||
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.pending(tenantId) });
|
||||
queryClient.invalidateQueries({ queryKey: classificationKeys.suggestions.history(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
384
frontend/src/api/hooks/foodSafety.ts
Normal file
384
frontend/src/api/hooks/foodSafety.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* Food Safety React Query hooks
|
||||
*/
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { foodSafetyService } from '../services/foodSafety';
|
||||
import {
|
||||
FoodSafetyComplianceCreate,
|
||||
FoodSafetyComplianceUpdate,
|
||||
FoodSafetyComplianceResponse,
|
||||
TemperatureLogCreate,
|
||||
BulkTemperatureLogCreate,
|
||||
TemperatureLogResponse,
|
||||
FoodSafetyAlertCreate,
|
||||
FoodSafetyAlertUpdate,
|
||||
FoodSafetyAlertResponse,
|
||||
FoodSafetyFilter,
|
||||
TemperatureMonitoringFilter,
|
||||
FoodSafetyMetrics,
|
||||
TemperatureAnalytics,
|
||||
FoodSafetyDashboard,
|
||||
} from '../types/foodSafety';
|
||||
import { PaginatedResponse } from '../types/inventory';
|
||||
import { ApiError } from '../client';
|
||||
|
||||
// Query Keys
|
||||
export const foodSafetyKeys = {
|
||||
all: ['food-safety'] as const,
|
||||
compliance: {
|
||||
all: () => [...foodSafetyKeys.all, 'compliance'] as const,
|
||||
lists: () => [...foodSafetyKeys.compliance.all(), 'list'] as const,
|
||||
list: (tenantId: string, filter?: FoodSafetyFilter) =>
|
||||
[...foodSafetyKeys.compliance.lists(), tenantId, filter] as const,
|
||||
details: () => [...foodSafetyKeys.compliance.all(), 'detail'] as const,
|
||||
detail: (tenantId: string, recordId: string) =>
|
||||
[...foodSafetyKeys.compliance.details(), tenantId, recordId] as const,
|
||||
},
|
||||
temperature: {
|
||||
all: () => [...foodSafetyKeys.all, 'temperature'] as const,
|
||||
lists: () => [...foodSafetyKeys.temperature.all(), 'list'] as const,
|
||||
list: (tenantId: string, filter?: TemperatureMonitoringFilter) =>
|
||||
[...foodSafetyKeys.temperature.lists(), tenantId, filter] as const,
|
||||
analytics: (tenantId: string, location: string, startDate?: string, endDate?: string) =>
|
||||
[...foodSafetyKeys.temperature.all(), 'analytics', tenantId, location, { startDate, endDate }] as const,
|
||||
violations: (tenantId: string, limit?: number) =>
|
||||
[...foodSafetyKeys.temperature.all(), 'violations', tenantId, limit] as const,
|
||||
},
|
||||
alerts: {
|
||||
all: () => [...foodSafetyKeys.all, 'alerts'] as const,
|
||||
lists: () => [...foodSafetyKeys.alerts.all(), 'list'] as const,
|
||||
list: (tenantId: string, status?: string, severity?: string, limit?: number, offset?: number) =>
|
||||
[...foodSafetyKeys.alerts.lists(), tenantId, { status, severity, limit, offset }] as const,
|
||||
details: () => [...foodSafetyKeys.alerts.all(), 'detail'] as const,
|
||||
detail: (tenantId: string, alertId: string) =>
|
||||
[...foodSafetyKeys.alerts.details(), tenantId, alertId] as const,
|
||||
},
|
||||
dashboard: (tenantId: string) =>
|
||||
[...foodSafetyKeys.all, 'dashboard', tenantId] as const,
|
||||
metrics: (tenantId: string, startDate?: string, endDate?: string) =>
|
||||
[...foodSafetyKeys.all, 'metrics', tenantId, { startDate, endDate }] as const,
|
||||
complianceRate: (tenantId: string, startDate?: string, endDate?: string) =>
|
||||
[...foodSafetyKeys.all, 'compliance-rate', tenantId, { startDate, endDate }] as const,
|
||||
} as const;
|
||||
|
||||
// Compliance Queries
|
||||
export const useComplianceRecords = (
|
||||
tenantId: string,
|
||||
filter?: FoodSafetyFilter,
|
||||
options?: Omit<UseQueryOptions<PaginatedResponse<FoodSafetyComplianceResponse>, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<PaginatedResponse<FoodSafetyComplianceResponse>, ApiError>({
|
||||
queryKey: foodSafetyKeys.compliance.list(tenantId, filter),
|
||||
queryFn: () => foodSafetyService.getComplianceRecords(tenantId, filter),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useComplianceRecord = (
|
||||
tenantId: string,
|
||||
recordId: string,
|
||||
options?: Omit<UseQueryOptions<FoodSafetyComplianceResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<FoodSafetyComplianceResponse, ApiError>({
|
||||
queryKey: foodSafetyKeys.compliance.detail(tenantId, recordId),
|
||||
queryFn: () => foodSafetyService.getComplianceRecord(tenantId, recordId),
|
||||
enabled: !!tenantId && !!recordId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Temperature Monitoring Queries
|
||||
export const useTemperatureLogs = (
|
||||
tenantId: string,
|
||||
filter?: TemperatureMonitoringFilter,
|
||||
options?: Omit<UseQueryOptions<PaginatedResponse<TemperatureLogResponse>, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<PaginatedResponse<TemperatureLogResponse>, ApiError>({
|
||||
queryKey: foodSafetyKeys.temperature.list(tenantId, filter),
|
||||
queryFn: () => foodSafetyService.getTemperatureLogs(tenantId, filter),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useTemperatureAnalytics = (
|
||||
tenantId: string,
|
||||
location: string,
|
||||
startDate?: string,
|
||||
endDate?: string,
|
||||
options?: Omit<UseQueryOptions<TemperatureAnalytics, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<TemperatureAnalytics, ApiError>({
|
||||
queryKey: foodSafetyKeys.temperature.analytics(tenantId, location, startDate, endDate),
|
||||
queryFn: () => foodSafetyService.getTemperatureAnalytics(tenantId, location, startDate, endDate),
|
||||
enabled: !!tenantId && !!location,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useTemperatureViolations = (
|
||||
tenantId: string,
|
||||
limit: number = 20,
|
||||
options?: Omit<UseQueryOptions<TemperatureLogResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<TemperatureLogResponse[], ApiError>({
|
||||
queryKey: foodSafetyKeys.temperature.violations(tenantId, limit),
|
||||
queryFn: () => foodSafetyService.getTemperatureViolations(tenantId, limit),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Alert Queries
|
||||
export const useFoodSafetyAlerts = (
|
||||
tenantId: string,
|
||||
status?: 'open' | 'in_progress' | 'resolved' | 'dismissed',
|
||||
severity?: 'critical' | 'warning' | 'info',
|
||||
limit: number = 50,
|
||||
offset: number = 0,
|
||||
options?: Omit<UseQueryOptions<PaginatedResponse<FoodSafetyAlertResponse>, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<PaginatedResponse<FoodSafetyAlertResponse>, ApiError>({
|
||||
queryKey: foodSafetyKeys.alerts.list(tenantId, status, severity, limit, offset),
|
||||
queryFn: () => foodSafetyService.getFoodSafetyAlerts(tenantId, status, severity, limit, offset),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useFoodSafetyAlert = (
|
||||
tenantId: string,
|
||||
alertId: string,
|
||||
options?: Omit<UseQueryOptions<FoodSafetyAlertResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<FoodSafetyAlertResponse, ApiError>({
|
||||
queryKey: foodSafetyKeys.alerts.detail(tenantId, alertId),
|
||||
queryFn: () => foodSafetyService.getFoodSafetyAlert(tenantId, alertId),
|
||||
enabled: !!tenantId && !!alertId,
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Dashboard and Metrics Queries
|
||||
export const useFoodSafetyDashboard = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<FoodSafetyDashboard, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<FoodSafetyDashboard, ApiError>({
|
||||
queryKey: foodSafetyKeys.dashboard(tenantId),
|
||||
queryFn: () => foodSafetyService.getFoodSafetyDashboard(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useFoodSafetyMetrics = (
|
||||
tenantId: string,
|
||||
startDate?: string,
|
||||
endDate?: string,
|
||||
options?: Omit<UseQueryOptions<FoodSafetyMetrics, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<FoodSafetyMetrics, ApiError>({
|
||||
queryKey: foodSafetyKeys.metrics(tenantId, startDate, endDate),
|
||||
queryFn: () => foodSafetyService.getFoodSafetyMetrics(tenantId, startDate, endDate),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useComplianceRate = (
|
||||
tenantId: string,
|
||||
startDate?: string,
|
||||
endDate?: string,
|
||||
options?: Omit<UseQueryOptions<{
|
||||
overall_rate: number;
|
||||
by_type: Record<string, number>;
|
||||
trend: Array<{ date: string; rate: number }>;
|
||||
}, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<{
|
||||
overall_rate: number;
|
||||
by_type: Record<string, number>;
|
||||
trend: Array<{ date: string; rate: number }>;
|
||||
}, ApiError>({
|
||||
queryKey: foodSafetyKeys.complianceRate(tenantId, startDate, endDate),
|
||||
queryFn: () => foodSafetyService.getComplianceRate(tenantId, startDate, endDate),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Compliance Mutations
|
||||
export const useCreateComplianceRecord = (
|
||||
options?: UseMutationOptions<
|
||||
FoodSafetyComplianceResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; complianceData: FoodSafetyComplianceCreate }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
FoodSafetyComplianceResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; complianceData: FoodSafetyComplianceCreate }
|
||||
>({
|
||||
mutationFn: ({ tenantId, complianceData }) =>
|
||||
foodSafetyService.createComplianceRecord(tenantId, complianceData),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Add to cache
|
||||
queryClient.setQueryData(foodSafetyKeys.compliance.detail(tenantId, data.id), data);
|
||||
// Invalidate lists
|
||||
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.compliance.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.dashboard(tenantId) });
|
||||
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.metrics(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateComplianceRecord = (
|
||||
options?: UseMutationOptions<
|
||||
FoodSafetyComplianceResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; recordId: string; updateData: FoodSafetyComplianceUpdate }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
FoodSafetyComplianceResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; recordId: string; updateData: FoodSafetyComplianceUpdate }
|
||||
>({
|
||||
mutationFn: ({ tenantId, recordId, updateData }) =>
|
||||
foodSafetyService.updateComplianceRecord(tenantId, recordId, updateData),
|
||||
onSuccess: (data, { tenantId, recordId }) => {
|
||||
// Update cache
|
||||
queryClient.setQueryData(foodSafetyKeys.compliance.detail(tenantId, recordId), data);
|
||||
// Invalidate lists
|
||||
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.compliance.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.dashboard(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Temperature Mutations
|
||||
export const useCreateTemperatureLog = (
|
||||
options?: UseMutationOptions<
|
||||
TemperatureLogResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; logData: TemperatureLogCreate }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
TemperatureLogResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; logData: TemperatureLogCreate }
|
||||
>({
|
||||
mutationFn: ({ tenantId, logData }) => foodSafetyService.createTemperatureLog(tenantId, logData),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Invalidate temperature queries
|
||||
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.temperature.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.dashboard(tenantId) });
|
||||
|
||||
// If alert was triggered, invalidate alerts
|
||||
if (data.alert_triggered) {
|
||||
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.alerts.lists() });
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateBulkTemperatureLogs = (
|
||||
options?: UseMutationOptions<
|
||||
{ created_count: number; failed_count: number; errors?: string[] },
|
||||
ApiError,
|
||||
{ tenantId: string; bulkData: BulkTemperatureLogCreate }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
{ created_count: number; failed_count: number; errors?: string[] },
|
||||
ApiError,
|
||||
{ tenantId: string; bulkData: BulkTemperatureLogCreate }
|
||||
>({
|
||||
mutationFn: ({ tenantId, bulkData }) => foodSafetyService.createBulkTemperatureLogs(tenantId, bulkData),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Invalidate temperature queries
|
||||
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.temperature.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.dashboard(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Alert Mutations
|
||||
export const useCreateFoodSafetyAlert = (
|
||||
options?: UseMutationOptions<
|
||||
FoodSafetyAlertResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; alertData: FoodSafetyAlertCreate }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
FoodSafetyAlertResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; alertData: FoodSafetyAlertCreate }
|
||||
>({
|
||||
mutationFn: ({ tenantId, alertData }) => foodSafetyService.createFoodSafetyAlert(tenantId, alertData),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Add to cache
|
||||
queryClient.setQueryData(foodSafetyKeys.alerts.detail(tenantId, data.id), data);
|
||||
// Invalidate lists
|
||||
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.alerts.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.dashboard(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateFoodSafetyAlert = (
|
||||
options?: UseMutationOptions<
|
||||
FoodSafetyAlertResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; alertId: string; updateData: FoodSafetyAlertUpdate }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
FoodSafetyAlertResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; alertId: string; updateData: FoodSafetyAlertUpdate }
|
||||
>({
|
||||
mutationFn: ({ tenantId, alertId, updateData }) =>
|
||||
foodSafetyService.updateFoodSafetyAlert(tenantId, alertId, updateData),
|
||||
onSuccess: (data, { tenantId, alertId }) => {
|
||||
// Update cache
|
||||
queryClient.setQueryData(foodSafetyKeys.alerts.detail(tenantId, alertId), data);
|
||||
// Invalidate lists
|
||||
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.alerts.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: foodSafetyKeys.dashboard(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
372
frontend/src/api/hooks/inventory.ts
Normal file
372
frontend/src/api/hooks/inventory.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
/**
|
||||
* Inventory React Query hooks
|
||||
*/
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { inventoryService } from '../services/inventory';
|
||||
import {
|
||||
IngredientCreate,
|
||||
IngredientUpdate,
|
||||
IngredientResponse,
|
||||
StockCreate,
|
||||
StockUpdate,
|
||||
StockResponse,
|
||||
StockMovementCreate,
|
||||
StockMovementResponse,
|
||||
InventoryFilter,
|
||||
StockFilter,
|
||||
StockConsumptionRequest,
|
||||
StockConsumptionResponse,
|
||||
PaginatedResponse,
|
||||
} from '../types/inventory';
|
||||
import { ApiError } from '../client';
|
||||
|
||||
// Query Keys
|
||||
export const inventoryKeys = {
|
||||
all: ['inventory'] as const,
|
||||
ingredients: {
|
||||
all: () => [...inventoryKeys.all, 'ingredients'] as const,
|
||||
lists: () => [...inventoryKeys.ingredients.all(), 'list'] as const,
|
||||
list: (tenantId: string, filters?: InventoryFilter) =>
|
||||
[...inventoryKeys.ingredients.lists(), tenantId, filters] as const,
|
||||
details: () => [...inventoryKeys.ingredients.all(), 'detail'] as const,
|
||||
detail: (tenantId: string, ingredientId: string) =>
|
||||
[...inventoryKeys.ingredients.details(), tenantId, ingredientId] as const,
|
||||
byCategory: (tenantId: string) =>
|
||||
[...inventoryKeys.ingredients.all(), 'by-category', tenantId] as const,
|
||||
lowStock: (tenantId: string) =>
|
||||
[...inventoryKeys.ingredients.all(), 'low-stock', tenantId] as const,
|
||||
},
|
||||
stock: {
|
||||
all: () => [...inventoryKeys.all, 'stock'] as const,
|
||||
lists: () => [...inventoryKeys.stock.all(), 'list'] as const,
|
||||
list: (tenantId: string, filters?: StockFilter) =>
|
||||
[...inventoryKeys.stock.lists(), tenantId, filters] as const,
|
||||
details: () => [...inventoryKeys.stock.all(), 'detail'] as const,
|
||||
detail: (tenantId: string, stockId: string) =>
|
||||
[...inventoryKeys.stock.details(), tenantId, stockId] as const,
|
||||
byIngredient: (tenantId: string, ingredientId: string, includeUnavailable?: boolean) =>
|
||||
[...inventoryKeys.stock.all(), 'by-ingredient', tenantId, ingredientId, includeUnavailable] as const,
|
||||
expiring: (tenantId: string, withinDays?: number) =>
|
||||
[...inventoryKeys.stock.all(), 'expiring', tenantId, withinDays] as const,
|
||||
expired: (tenantId: string) =>
|
||||
[...inventoryKeys.stock.all(), 'expired', tenantId] as const,
|
||||
movements: (tenantId: string, ingredientId?: string) =>
|
||||
[...inventoryKeys.stock.all(), 'movements', tenantId, ingredientId] as const,
|
||||
},
|
||||
analytics: (tenantId: string, startDate?: string, endDate?: string) =>
|
||||
[...inventoryKeys.all, 'analytics', tenantId, { startDate, endDate }] as const,
|
||||
} as const;
|
||||
|
||||
// Ingredient Queries
|
||||
export const useIngredients = (
|
||||
tenantId: string,
|
||||
filter?: InventoryFilter,
|
||||
options?: Omit<UseQueryOptions<PaginatedResponse<IngredientResponse>, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<PaginatedResponse<IngredientResponse>, ApiError>({
|
||||
queryKey: inventoryKeys.ingredients.list(tenantId, filter),
|
||||
queryFn: () => inventoryService.getIngredients(tenantId, filter),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useIngredient = (
|
||||
tenantId: string,
|
||||
ingredientId: string,
|
||||
options?: Omit<UseQueryOptions<IngredientResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<IngredientResponse, ApiError>({
|
||||
queryKey: inventoryKeys.ingredients.detail(tenantId, ingredientId),
|
||||
queryFn: () => inventoryService.getIngredient(tenantId, ingredientId),
|
||||
enabled: !!tenantId && !!ingredientId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useIngredientsByCategory = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<Record<string, IngredientResponse[]>, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<Record<string, IngredientResponse[]>, ApiError>({
|
||||
queryKey: inventoryKeys.ingredients.byCategory(tenantId),
|
||||
queryFn: () => inventoryService.getIngredientsByCategory(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useLowStockIngredients = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<IngredientResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<IngredientResponse[], ApiError>({
|
||||
queryKey: inventoryKeys.ingredients.lowStock(tenantId),
|
||||
queryFn: () => inventoryService.getLowStockIngredients(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Stock Queries
|
||||
export const useStock = (
|
||||
tenantId: string,
|
||||
filter?: StockFilter,
|
||||
options?: Omit<UseQueryOptions<PaginatedResponse<StockResponse>, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<PaginatedResponse<StockResponse>, ApiError>({
|
||||
queryKey: inventoryKeys.stock.list(tenantId, filter),
|
||||
queryFn: () => inventoryService.getAllStock(tenantId, filter),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useStockByIngredient = (
|
||||
tenantId: string,
|
||||
ingredientId: string,
|
||||
includeUnavailable: boolean = false,
|
||||
options?: Omit<UseQueryOptions<StockResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<StockResponse[], ApiError>({
|
||||
queryKey: inventoryKeys.stock.byIngredient(tenantId, ingredientId, includeUnavailable),
|
||||
queryFn: () => inventoryService.getStockByIngredient(tenantId, ingredientId, includeUnavailable),
|
||||
enabled: !!tenantId && !!ingredientId,
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useExpiringStock = (
|
||||
tenantId: string,
|
||||
withinDays: number = 7,
|
||||
options?: Omit<UseQueryOptions<StockResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<StockResponse[], ApiError>({
|
||||
queryKey: inventoryKeys.stock.expiring(tenantId, withinDays),
|
||||
queryFn: () => inventoryService.getExpiringStock(tenantId, withinDays),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useExpiredStock = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<StockResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<StockResponse[], ApiError>({
|
||||
queryKey: inventoryKeys.stock.expired(tenantId),
|
||||
queryFn: () => inventoryService.getExpiredStock(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useStockMovements = (
|
||||
tenantId: string,
|
||||
ingredientId?: string,
|
||||
limit: number = 50,
|
||||
offset: number = 0,
|
||||
options?: Omit<UseQueryOptions<PaginatedResponse<StockMovementResponse>, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<PaginatedResponse<StockMovementResponse>, ApiError>({
|
||||
queryKey: inventoryKeys.stock.movements(tenantId, ingredientId),
|
||||
queryFn: () => inventoryService.getStockMovements(tenantId, ingredientId, limit, offset),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useStockAnalytics = (
|
||||
tenantId: string,
|
||||
startDate?: string,
|
||||
endDate?: string,
|
||||
options?: Omit<UseQueryOptions<any, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<any, ApiError>({
|
||||
queryKey: inventoryKeys.analytics(tenantId, startDate, endDate),
|
||||
queryFn: () => inventoryService.getStockAnalytics(tenantId, startDate, endDate),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Ingredient Mutations
|
||||
export const useCreateIngredient = (
|
||||
options?: UseMutationOptions<IngredientResponse, ApiError, { tenantId: string; ingredientData: IngredientCreate }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<IngredientResponse, ApiError, { tenantId: string; ingredientData: IngredientCreate }>({
|
||||
mutationFn: ({ tenantId, ingredientData }) => inventoryService.createIngredient(tenantId, ingredientData),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Add to cache
|
||||
queryClient.setQueryData(inventoryKeys.ingredients.detail(tenantId, data.id), data);
|
||||
// Invalidate lists
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.byCategory(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateIngredient = (
|
||||
options?: UseMutationOptions<
|
||||
IngredientResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; ingredientId: string; updateData: IngredientUpdate }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
IngredientResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; ingredientId: string; updateData: IngredientUpdate }
|
||||
>({
|
||||
mutationFn: ({ tenantId, ingredientId, updateData }) =>
|
||||
inventoryService.updateIngredient(tenantId, ingredientId, updateData),
|
||||
onSuccess: (data, { tenantId, ingredientId }) => {
|
||||
// Update cache
|
||||
queryClient.setQueryData(inventoryKeys.ingredients.detail(tenantId, ingredientId), data);
|
||||
// Invalidate lists
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.byCategory(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteIngredient = (
|
||||
options?: UseMutationOptions<{ message: string }, ApiError, { tenantId: string; ingredientId: string }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ message: string }, ApiError, { tenantId: string; ingredientId: string }>({
|
||||
mutationFn: ({ tenantId, ingredientId }) => inventoryService.deleteIngredient(tenantId, ingredientId),
|
||||
onSuccess: (data, { tenantId, ingredientId }) => {
|
||||
// Remove from cache
|
||||
queryClient.removeQueries({ queryKey: inventoryKeys.ingredients.detail(tenantId, ingredientId) });
|
||||
// Invalidate lists
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.byCategory(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Stock Mutations
|
||||
export const useAddStock = (
|
||||
options?: UseMutationOptions<StockResponse, ApiError, { tenantId: string; stockData: StockCreate }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<StockResponse, ApiError, { tenantId: string; stockData: StockCreate }>({
|
||||
mutationFn: ({ tenantId, stockData }) => inventoryService.addStock(tenantId, stockData),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Invalidate stock queries
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.byIngredient(tenantId, data.ingredient_id) });
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateStock = (
|
||||
options?: UseMutationOptions<
|
||||
StockResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; stockId: string; updateData: StockUpdate }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
StockResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; stockId: string; updateData: StockUpdate }
|
||||
>({
|
||||
mutationFn: ({ tenantId, stockId, updateData }) =>
|
||||
inventoryService.updateStock(tenantId, stockId, updateData),
|
||||
onSuccess: (data, { tenantId, stockId }) => {
|
||||
// Update cache
|
||||
queryClient.setQueryData(inventoryKeys.stock.detail(tenantId, stockId), data);
|
||||
// Invalidate related queries
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.byIngredient(tenantId, data.ingredient_id) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useConsumeStock = (
|
||||
options?: UseMutationOptions<
|
||||
StockConsumptionResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; consumptionData: StockConsumptionRequest }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
StockConsumptionResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; consumptionData: StockConsumptionRequest }
|
||||
>({
|
||||
mutationFn: ({ tenantId, consumptionData }) => inventoryService.consumeStock(tenantId, consumptionData),
|
||||
onSuccess: (data, { tenantId, consumptionData }) => {
|
||||
// Invalidate stock queries for the affected ingredient
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: inventoryKeys.stock.byIngredient(tenantId, consumptionData.ingredient_id)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId) });
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateStockMovement = (
|
||||
options?: UseMutationOptions<
|
||||
StockMovementResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; movementData: StockMovementCreate }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
StockMovementResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; movementData: StockMovementCreate }
|
||||
>({
|
||||
mutationFn: ({ tenantId, movementData }) => inventoryService.createStockMovement(tenantId, movementData),
|
||||
onSuccess: (data, { tenantId, movementData }) => {
|
||||
// Invalidate movement queries
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: inventoryKeys.stock.movements(tenantId, movementData.ingredient_id)
|
||||
});
|
||||
// Invalidate stock queries if this affects stock levels
|
||||
if (['in', 'out', 'adjustment'].includes(movementData.movement_type)) {
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: inventoryKeys.stock.byIngredient(tenantId, movementData.ingredient_id)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() });
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
183
frontend/src/api/hooks/inventoryDashboard.ts
Normal file
183
frontend/src/api/hooks/inventoryDashboard.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Inventory Dashboard React Query hooks
|
||||
*/
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { inventoryDashboardService } from '../services/inventoryDashboard';
|
||||
import {
|
||||
InventoryDashboardSummary,
|
||||
InventoryAnalytics,
|
||||
BusinessModelInsights,
|
||||
DashboardFilter,
|
||||
AlertsFilter,
|
||||
RecentActivity,
|
||||
} from '../types/dashboard';
|
||||
import { ApiError } from '../client';
|
||||
|
||||
// Query Keys
|
||||
export const inventoryDashboardKeys = {
|
||||
all: ['inventory-dashboard'] as const,
|
||||
summary: (tenantId: string, filter?: DashboardFilter) =>
|
||||
[...inventoryDashboardKeys.all, 'summary', tenantId, filter] as const,
|
||||
analytics: (tenantId: string, startDate?: string, endDate?: string) =>
|
||||
[...inventoryDashboardKeys.all, 'analytics', tenantId, { startDate, endDate }] as const,
|
||||
insights: (tenantId: string) =>
|
||||
[...inventoryDashboardKeys.all, 'business-insights', tenantId] as const,
|
||||
activity: (tenantId: string, limit?: number) =>
|
||||
[...inventoryDashboardKeys.all, 'recent-activity', tenantId, limit] as const,
|
||||
alerts: (tenantId: string, filter?: AlertsFilter) =>
|
||||
[...inventoryDashboardKeys.all, 'alerts', tenantId, filter] as const,
|
||||
stockSummary: (tenantId: string) =>
|
||||
[...inventoryDashboardKeys.all, 'stock-summary', tenantId] as const,
|
||||
topCategories: (tenantId: string, limit?: number) =>
|
||||
[...inventoryDashboardKeys.all, 'top-categories', tenantId, limit] as const,
|
||||
expiryCalendar: (tenantId: string, daysAhead?: number) =>
|
||||
[...inventoryDashboardKeys.all, 'expiry-calendar', tenantId, daysAhead] as const,
|
||||
} as const;
|
||||
|
||||
// Queries
|
||||
export const useInventoryDashboardSummary = (
|
||||
tenantId: string,
|
||||
filter?: DashboardFilter,
|
||||
options?: Omit<UseQueryOptions<InventoryDashboardSummary, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<InventoryDashboardSummary, ApiError>({
|
||||
queryKey: inventoryDashboardKeys.summary(tenantId, filter),
|
||||
queryFn: () => inventoryDashboardService.getDashboardSummary(tenantId, filter),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useInventoryAnalytics = (
|
||||
tenantId: string,
|
||||
startDate?: string,
|
||||
endDate?: string,
|
||||
options?: Omit<UseQueryOptions<InventoryAnalytics, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<InventoryAnalytics, ApiError>({
|
||||
queryKey: inventoryDashboardKeys.analytics(tenantId, startDate, endDate),
|
||||
queryFn: () => inventoryDashboardService.getInventoryAnalytics(tenantId, startDate, endDate),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useBusinessModelInsights = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<BusinessModelInsights, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<BusinessModelInsights, ApiError>({
|
||||
queryKey: inventoryDashboardKeys.insights(tenantId),
|
||||
queryFn: () => inventoryDashboardService.getBusinessModelInsights(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useRecentActivity = (
|
||||
tenantId: string,
|
||||
limit: number = 20,
|
||||
options?: Omit<UseQueryOptions<RecentActivity[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<RecentActivity[], ApiError>({
|
||||
queryKey: inventoryDashboardKeys.activity(tenantId, limit),
|
||||
queryFn: () => inventoryDashboardService.getRecentActivity(tenantId, limit),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useInventoryAlerts = (
|
||||
tenantId: string,
|
||||
filter?: AlertsFilter,
|
||||
options?: Omit<UseQueryOptions<{ items: any[]; total: number }, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<{ items: any[]; total: number }, ApiError>({
|
||||
queryKey: inventoryDashboardKeys.alerts(tenantId, filter),
|
||||
queryFn: () => inventoryDashboardService.getAlerts(tenantId, filter),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useStockSummary = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<{
|
||||
in_stock: number;
|
||||
low_stock: number;
|
||||
out_of_stock: number;
|
||||
overstock: number;
|
||||
total_value: number;
|
||||
}, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<{
|
||||
in_stock: number;
|
||||
low_stock: number;
|
||||
out_of_stock: number;
|
||||
overstock: number;
|
||||
total_value: number;
|
||||
}, ApiError>({
|
||||
queryKey: inventoryDashboardKeys.stockSummary(tenantId),
|
||||
queryFn: () => inventoryDashboardService.getStockSummary(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useTopCategories = (
|
||||
tenantId: string,
|
||||
limit: number = 10,
|
||||
options?: Omit<UseQueryOptions<Array<{
|
||||
category: string;
|
||||
ingredient_count: number;
|
||||
total_value: number;
|
||||
low_stock_count: number;
|
||||
}>, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<Array<{
|
||||
category: string;
|
||||
ingredient_count: number;
|
||||
total_value: number;
|
||||
low_stock_count: number;
|
||||
}>, ApiError>({
|
||||
queryKey: inventoryDashboardKeys.topCategories(tenantId, limit),
|
||||
queryFn: () => inventoryDashboardService.getTopCategories(tenantId, limit),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useExpiryCalendar = (
|
||||
tenantId: string,
|
||||
daysAhead: number = 30,
|
||||
options?: Omit<UseQueryOptions<Array<{
|
||||
date: string;
|
||||
items: Array<{
|
||||
ingredient_name: string;
|
||||
quantity: number;
|
||||
batch_number?: string;
|
||||
}>;
|
||||
}>, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<Array<{
|
||||
date: string;
|
||||
items: Array<{
|
||||
ingredient_name: string;
|
||||
quantity: number;
|
||||
batch_number?: string;
|
||||
}>;
|
||||
}>, ApiError>({
|
||||
queryKey: inventoryDashboardKeys.expiryCalendar(tenantId, daysAhead),
|
||||
queryFn: () => inventoryDashboardService.getExpiryCalendar(tenantId, daysAhead),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
128
frontend/src/api/hooks/onboarding.ts
Normal file
128
frontend/src/api/hooks/onboarding.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Onboarding React Query hooks
|
||||
*/
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { onboardingService } from '../services/onboarding';
|
||||
import { UserProgress, UpdateStepRequest } from '../types/onboarding';
|
||||
import { ApiError } from '../client';
|
||||
|
||||
// Query Keys
|
||||
export const onboardingKeys = {
|
||||
all: ['onboarding'] as const,
|
||||
progress: (userId: string) => [...onboardingKeys.all, 'progress', userId] as const,
|
||||
steps: () => [...onboardingKeys.all, 'steps'] as const,
|
||||
stepDetail: (stepName: string) => [...onboardingKeys.steps(), stepName] as const,
|
||||
} as const;
|
||||
|
||||
// Queries
|
||||
export const useUserProgress = (
|
||||
userId: string,
|
||||
options?: Omit<UseQueryOptions<UserProgress, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<UserProgress, ApiError>({
|
||||
queryKey: onboardingKeys.progress(userId),
|
||||
queryFn: () => onboardingService.getUserProgress(userId),
|
||||
enabled: !!userId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAllSteps = (
|
||||
options?: Omit<UseQueryOptions<Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
dependencies: string[];
|
||||
estimated_time_minutes: number;
|
||||
}>, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
dependencies: string[];
|
||||
estimated_time_minutes: number;
|
||||
}>, ApiError>({
|
||||
queryKey: onboardingKeys.steps(),
|
||||
queryFn: () => onboardingService.getAllSteps(),
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useStepDetails = (
|
||||
stepName: string,
|
||||
options?: Omit<UseQueryOptions<{
|
||||
name: string;
|
||||
description: string;
|
||||
dependencies: string[];
|
||||
estimated_time_minutes: number;
|
||||
}, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<{
|
||||
name: string;
|
||||
description: string;
|
||||
dependencies: string[];
|
||||
estimated_time_minutes: number;
|
||||
}, ApiError>({
|
||||
queryKey: onboardingKeys.stepDetail(stepName),
|
||||
queryFn: () => onboardingService.getStepDetails(stepName),
|
||||
enabled: !!stepName,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Mutations
|
||||
export const useUpdateStep = (
|
||||
options?: UseMutationOptions<UserProgress, ApiError, { userId: string; stepData: UpdateStepRequest }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<UserProgress, ApiError, { userId: string; stepData: UpdateStepRequest }>({
|
||||
mutationFn: ({ userId, stepData }) => onboardingService.updateStep(userId, stepData),
|
||||
onSuccess: (data, { userId }) => {
|
||||
// Update progress cache
|
||||
queryClient.setQueryData(onboardingKeys.progress(userId), data);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useMarkStepCompleted = (
|
||||
options?: UseMutationOptions<
|
||||
UserProgress,
|
||||
ApiError,
|
||||
{ userId: string; stepName: string; data?: Record<string, any> }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
UserProgress,
|
||||
ApiError,
|
||||
{ userId: string; stepName: string; data?: Record<string, any> }
|
||||
>({
|
||||
mutationFn: ({ userId, stepName, data }) =>
|
||||
onboardingService.markStepCompleted(userId, stepName, data),
|
||||
onSuccess: (data, { userId }) => {
|
||||
// Update progress cache
|
||||
queryClient.setQueryData(onboardingKeys.progress(userId), data);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useResetProgress = (
|
||||
options?: UseMutationOptions<UserProgress, ApiError, string>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<UserProgress, ApiError, string>({
|
||||
mutationFn: (userId: string) => onboardingService.resetProgress(userId),
|
||||
onSuccess: (data, userId) => {
|
||||
// Update progress cache
|
||||
queryClient.setQueryData(onboardingKeys.progress(userId), data);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
190
frontend/src/api/hooks/sales.ts
Normal file
190
frontend/src/api/hooks/sales.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* Sales React Query hooks
|
||||
*/
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { salesService } from '../services/sales';
|
||||
import {
|
||||
SalesDataCreate,
|
||||
SalesDataUpdate,
|
||||
SalesDataResponse,
|
||||
SalesDataQuery,
|
||||
SalesAnalytics,
|
||||
} from '../types/sales';
|
||||
import { ApiError } from '../client';
|
||||
|
||||
// Query Keys
|
||||
export const salesKeys = {
|
||||
all: ['sales'] as const,
|
||||
lists: () => [...salesKeys.all, 'list'] as const,
|
||||
list: (tenantId: string, filters?: SalesDataQuery) => [...salesKeys.lists(), tenantId, filters] as const,
|
||||
details: () => [...salesKeys.all, 'detail'] as const,
|
||||
detail: (tenantId: string, recordId: string) => [...salesKeys.details(), tenantId, recordId] as const,
|
||||
analytics: (tenantId: string, startDate?: string, endDate?: string) =>
|
||||
[...salesKeys.all, 'analytics', tenantId, { startDate, endDate }] as const,
|
||||
productSales: (tenantId: string, productId: string, startDate?: string, endDate?: string) =>
|
||||
[...salesKeys.all, 'product-sales', tenantId, productId, { startDate, endDate }] as const,
|
||||
categories: (tenantId: string) => [...salesKeys.all, 'categories', tenantId] as const,
|
||||
} as const;
|
||||
|
||||
// Queries
|
||||
export const useSalesRecords = (
|
||||
tenantId: string,
|
||||
query?: SalesDataQuery,
|
||||
options?: Omit<UseQueryOptions<SalesDataResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<SalesDataResponse[], ApiError>({
|
||||
queryKey: salesKeys.list(tenantId, query),
|
||||
queryFn: () => salesService.getSalesRecords(tenantId, query),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useSalesRecord = (
|
||||
tenantId: string,
|
||||
recordId: string,
|
||||
options?: Omit<UseQueryOptions<SalesDataResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<SalesDataResponse, ApiError>({
|
||||
queryKey: salesKeys.detail(tenantId, recordId),
|
||||
queryFn: () => salesService.getSalesRecord(tenantId, recordId),
|
||||
enabled: !!tenantId && !!recordId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useSalesAnalytics = (
|
||||
tenantId: string,
|
||||
startDate?: string,
|
||||
endDate?: string,
|
||||
options?: Omit<UseQueryOptions<SalesAnalytics, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<SalesAnalytics, ApiError>({
|
||||
queryKey: salesKeys.analytics(tenantId, startDate, endDate),
|
||||
queryFn: () => salesService.getSalesAnalytics(tenantId, startDate, endDate),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useProductSales = (
|
||||
tenantId: string,
|
||||
inventoryProductId: string,
|
||||
startDate?: string,
|
||||
endDate?: string,
|
||||
options?: Omit<UseQueryOptions<SalesDataResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<SalesDataResponse[], ApiError>({
|
||||
queryKey: salesKeys.productSales(tenantId, inventoryProductId, startDate, endDate),
|
||||
queryFn: () => salesService.getProductSales(tenantId, inventoryProductId, startDate, endDate),
|
||||
enabled: !!tenantId && !!inventoryProductId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useProductCategories = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<string[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<string[], ApiError>({
|
||||
queryKey: salesKeys.categories(tenantId),
|
||||
queryFn: () => salesService.getProductCategories(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Mutations
|
||||
export const useCreateSalesRecord = (
|
||||
options?: UseMutationOptions<SalesDataResponse, ApiError, { tenantId: string; salesData: SalesDataCreate }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<SalesDataResponse, ApiError, { tenantId: string; salesData: SalesDataCreate }>({
|
||||
mutationFn: ({ tenantId, salesData }) => salesService.createSalesRecord(tenantId, salesData),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Invalidate sales lists to refresh data
|
||||
queryClient.invalidateQueries({ queryKey: salesKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: salesKeys.analytics(tenantId) });
|
||||
// Set the new record in cache
|
||||
queryClient.setQueryData(salesKeys.detail(tenantId, data.id), data);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSalesRecord = (
|
||||
options?: UseMutationOptions<
|
||||
SalesDataResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; recordId: string; updateData: SalesDataUpdate }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
SalesDataResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; recordId: string; updateData: SalesDataUpdate }
|
||||
>({
|
||||
mutationFn: ({ tenantId, recordId, updateData }) =>
|
||||
salesService.updateSalesRecord(tenantId, recordId, updateData),
|
||||
onSuccess: (data, { tenantId, recordId }) => {
|
||||
// Update the record cache
|
||||
queryClient.setQueryData(salesKeys.detail(tenantId, recordId), data);
|
||||
// Invalidate related queries
|
||||
queryClient.invalidateQueries({ queryKey: salesKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: salesKeys.analytics(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSalesRecord = (
|
||||
options?: UseMutationOptions<{ message: string }, ApiError, { tenantId: string; recordId: string }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ message: string }, ApiError, { tenantId: string; recordId: string }>({
|
||||
mutationFn: ({ tenantId, recordId }) => salesService.deleteSalesRecord(tenantId, recordId),
|
||||
onSuccess: (data, { tenantId, recordId }) => {
|
||||
// Remove from cache
|
||||
queryClient.removeQueries({ queryKey: salesKeys.detail(tenantId, recordId) });
|
||||
// Invalidate related queries
|
||||
queryClient.invalidateQueries({ queryKey: salesKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: salesKeys.analytics(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useValidateSalesRecord = (
|
||||
options?: UseMutationOptions<
|
||||
SalesDataResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; recordId: string; validationNotes?: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
SalesDataResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; recordId: string; validationNotes?: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, recordId, validationNotes }) =>
|
||||
salesService.validateSalesRecord(tenantId, recordId, validationNotes),
|
||||
onSuccess: (data, { tenantId, recordId }) => {
|
||||
// Update the record cache
|
||||
queryClient.setQueryData(salesKeys.detail(tenantId, recordId), data);
|
||||
// Invalidate sales lists to reflect validation status
|
||||
queryClient.invalidateQueries({ queryKey: salesKeys.lists() });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
316
frontend/src/api/hooks/tenant.ts
Normal file
316
frontend/src/api/hooks/tenant.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* Tenant React Query hooks
|
||||
*/
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { tenantService } from '../services/tenant';
|
||||
import {
|
||||
BakeryRegistration,
|
||||
TenantResponse,
|
||||
TenantAccessResponse,
|
||||
TenantUpdate,
|
||||
TenantMemberResponse,
|
||||
TenantStatistics,
|
||||
TenantSearchParams,
|
||||
TenantNearbyParams,
|
||||
} from '../types/tenant';
|
||||
import { ApiError } from '../client';
|
||||
|
||||
// Query Keys
|
||||
export const tenantKeys = {
|
||||
all: ['tenant'] as const,
|
||||
lists: () => [...tenantKeys.all, 'list'] as const,
|
||||
list: (filters: string) => [...tenantKeys.lists(), { filters }] as const,
|
||||
details: () => [...tenantKeys.all, 'detail'] as const,
|
||||
detail: (id: string) => [...tenantKeys.details(), id] as const,
|
||||
subdomain: (subdomain: string) => [...tenantKeys.all, 'subdomain', subdomain] as const,
|
||||
userTenants: (userId: string) => [...tenantKeys.all, 'user', userId] as const,
|
||||
userOwnedTenants: (userId: string) => [...tenantKeys.all, 'user-owned', userId] as const,
|
||||
access: (tenantId: string, userId: string) => [...tenantKeys.all, 'access', tenantId, userId] as const,
|
||||
search: (params: TenantSearchParams) => [...tenantKeys.lists(), 'search', params] as const,
|
||||
nearby: (params: TenantNearbyParams) => [...tenantKeys.lists(), 'nearby', params] as const,
|
||||
members: (tenantId: string) => [...tenantKeys.all, 'members', tenantId] as const,
|
||||
statistics: () => [...tenantKeys.all, 'statistics'] as const,
|
||||
} as const;
|
||||
|
||||
// Queries
|
||||
export const useTenant = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<TenantResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<TenantResponse, ApiError>({
|
||||
queryKey: tenantKeys.detail(tenantId),
|
||||
queryFn: () => tenantService.getTenant(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useTenantBySubdomain = (
|
||||
subdomain: string,
|
||||
options?: Omit<UseQueryOptions<TenantResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<TenantResponse, ApiError>({
|
||||
queryKey: tenantKeys.subdomain(subdomain),
|
||||
queryFn: () => tenantService.getTenantBySubdomain(subdomain),
|
||||
enabled: !!subdomain,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUserTenants = (
|
||||
userId: string,
|
||||
options?: Omit<UseQueryOptions<TenantResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<TenantResponse[], ApiError>({
|
||||
queryKey: tenantKeys.userTenants(userId),
|
||||
queryFn: () => tenantService.getUserTenants(userId),
|
||||
enabled: !!userId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUserOwnedTenants = (
|
||||
userId: string,
|
||||
options?: Omit<UseQueryOptions<TenantResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<TenantResponse[], ApiError>({
|
||||
queryKey: tenantKeys.userOwnedTenants(userId),
|
||||
queryFn: () => tenantService.getUserOwnedTenants(userId),
|
||||
enabled: !!userId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useTenantAccess = (
|
||||
tenantId: string,
|
||||
userId: string,
|
||||
options?: Omit<UseQueryOptions<TenantAccessResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<TenantAccessResponse, ApiError>({
|
||||
queryKey: tenantKeys.access(tenantId, userId),
|
||||
queryFn: () => tenantService.verifyTenantAccess(tenantId, userId),
|
||||
enabled: !!tenantId && !!userId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useSearchTenants = (
|
||||
params: TenantSearchParams,
|
||||
options?: Omit<UseQueryOptions<TenantResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<TenantResponse[], ApiError>({
|
||||
queryKey: tenantKeys.search(params),
|
||||
queryFn: () => tenantService.searchTenants(params),
|
||||
enabled: !!params.search_term,
|
||||
staleTime: 30 * 1000, // 30 seconds for search results
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useNearbyTenants = (
|
||||
params: TenantNearbyParams,
|
||||
options?: Omit<UseQueryOptions<TenantResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<TenantResponse[], ApiError>({
|
||||
queryKey: tenantKeys.nearby(params),
|
||||
queryFn: () => tenantService.getNearbyTenants(params),
|
||||
enabled: !!(params.latitude && params.longitude),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useTeamMembers = (
|
||||
tenantId: string,
|
||||
activeOnly: boolean = true,
|
||||
options?: Omit<UseQueryOptions<TenantMemberResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<TenantMemberResponse[], ApiError>({
|
||||
queryKey: tenantKeys.members(tenantId),
|
||||
queryFn: () => tenantService.getTeamMembers(tenantId, activeOnly),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useTenantStatistics = (
|
||||
options?: Omit<UseQueryOptions<TenantStatistics, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<TenantStatistics, ApiError>({
|
||||
queryKey: tenantKeys.statistics(),
|
||||
queryFn: () => tenantService.getTenantStatistics(),
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Mutations
|
||||
export const useRegisterBakery = (
|
||||
options?: UseMutationOptions<TenantResponse, ApiError, BakeryRegistration>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<TenantResponse, ApiError, BakeryRegistration>({
|
||||
mutationFn: (bakeryData: BakeryRegistration) => tenantService.registerBakery(bakeryData),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate user tenants to include the new one
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.userTenants('') });
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.userOwnedTenants('') });
|
||||
// Set the tenant data in cache
|
||||
queryClient.setQueryData(tenantKeys.detail(data.id), data);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateTenant = (
|
||||
options?: UseMutationOptions<TenantResponse, ApiError, { tenantId: string; updateData: TenantUpdate }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<TenantResponse, ApiError, { tenantId: string; updateData: TenantUpdate }>({
|
||||
mutationFn: ({ tenantId, updateData }) => tenantService.updateTenant(tenantId, updateData),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Update the tenant cache
|
||||
queryClient.setQueryData(tenantKeys.detail(tenantId), data);
|
||||
// Invalidate related queries
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.userTenants('') });
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.userOwnedTenants('') });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeactivateTenant = (
|
||||
options?: UseMutationOptions<{ success: boolean; message: string }, ApiError, string>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ success: boolean; message: string }, ApiError, string>({
|
||||
mutationFn: (tenantId: string) => tenantService.deactivateTenant(tenantId),
|
||||
onSuccess: (data, tenantId) => {
|
||||
// Invalidate tenant-related queries
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.detail(tenantId) });
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.userTenants('') });
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.userOwnedTenants('') });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useActivateTenant = (
|
||||
options?: UseMutationOptions<{ success: boolean; message: string }, ApiError, string>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ success: boolean; message: string }, ApiError, string>({
|
||||
mutationFn: (tenantId: string) => tenantService.activateTenant(tenantId),
|
||||
onSuccess: (data, tenantId) => {
|
||||
// Invalidate tenant-related queries
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.detail(tenantId) });
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.userTenants('') });
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.userOwnedTenants('') });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateModelStatus = (
|
||||
options?: UseMutationOptions<
|
||||
TenantResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; modelTrained: boolean; lastTrainingDate?: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
TenantResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; modelTrained: boolean; lastTrainingDate?: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, modelTrained, lastTrainingDate }) =>
|
||||
tenantService.updateModelStatus(tenantId, modelTrained, lastTrainingDate),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Update the tenant cache
|
||||
queryClient.setQueryData(tenantKeys.detail(tenantId), data);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddTeamMember = (
|
||||
options?: UseMutationOptions<
|
||||
TenantMemberResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; userId: string; role: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
TenantMemberResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; userId: string; role: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, userId, role }) => tenantService.addTeamMember(tenantId, userId, role),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Invalidate team members query
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.members(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateMemberRole = (
|
||||
options?: UseMutationOptions<
|
||||
TenantMemberResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; memberUserId: string; newRole: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
TenantMemberResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; memberUserId: string; newRole: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, memberUserId, newRole }) =>
|
||||
tenantService.updateMemberRole(tenantId, memberUserId, newRole),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Invalidate team members query
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.members(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useRemoveTeamMember = (
|
||||
options?: UseMutationOptions<
|
||||
{ success: boolean; message: string },
|
||||
ApiError,
|
||||
{ tenantId: string; memberUserId: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
{ success: boolean; message: string },
|
||||
ApiError,
|
||||
{ tenantId: string; memberUserId: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, memberUserId }) => tenantService.removeTeamMember(tenantId, memberUserId),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Invalidate team members query
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.members(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
112
frontend/src/api/hooks/user.ts
Normal file
112
frontend/src/api/hooks/user.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* User React Query hooks
|
||||
*/
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { userService } from '../services/user';
|
||||
import { UserResponse, UserUpdate } from '../types/auth';
|
||||
import { AdminDeleteRequest, AdminDeleteResponse } from '../types/user';
|
||||
import { ApiError } from '../client';
|
||||
|
||||
// Query Keys
|
||||
export const userKeys = {
|
||||
all: ['user'] as const,
|
||||
current: () => [...userKeys.all, 'current'] as const,
|
||||
detail: (id: string) => [...userKeys.all, 'detail', id] as const,
|
||||
admin: {
|
||||
all: () => [...userKeys.all, 'admin'] as const,
|
||||
list: () => [...userKeys.admin.all(), 'list'] as const,
|
||||
detail: (id: string) => [...userKeys.admin.all(), 'detail', id] as const,
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Queries
|
||||
export const useCurrentUser = (
|
||||
options?: Omit<UseQueryOptions<UserResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<UserResponse, ApiError>({
|
||||
queryKey: userKeys.current(),
|
||||
queryFn: () => userService.getCurrentUser(),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAllUsers = (
|
||||
options?: Omit<UseQueryOptions<UserResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<UserResponse[], ApiError>({
|
||||
queryKey: userKeys.admin.list(),
|
||||
queryFn: () => userService.getAllUsers(),
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUserById = (
|
||||
userId: string,
|
||||
options?: Omit<UseQueryOptions<UserResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<UserResponse, ApiError>({
|
||||
queryKey: userKeys.admin.detail(userId),
|
||||
queryFn: () => userService.getUserById(userId),
|
||||
enabled: !!userId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Mutations
|
||||
export const useUpdateUser = (
|
||||
options?: UseMutationOptions<UserResponse, ApiError, { userId: string; updateData: UserUpdate }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<UserResponse, ApiError, { userId: string; updateData: UserUpdate }>({
|
||||
mutationFn: ({ userId, updateData }) => userService.updateUser(userId, updateData),
|
||||
onSuccess: (data, { userId }) => {
|
||||
// Update user cache
|
||||
queryClient.setQueryData(userKeys.detail(userId), data);
|
||||
queryClient.setQueryData(userKeys.current(), data);
|
||||
queryClient.setQueryData(userKeys.admin.detail(userId), data);
|
||||
// Invalidate user lists
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.admin.list() });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteUser = (
|
||||
options?: UseMutationOptions<{ message: string }, ApiError, string>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ message: string }, ApiError, string>({
|
||||
mutationFn: (userId: string) => userService.deleteUser(userId),
|
||||
onSuccess: (data, userId) => {
|
||||
// Remove from cache
|
||||
queryClient.removeQueries({ queryKey: userKeys.detail(userId) });
|
||||
queryClient.removeQueries({ queryKey: userKeys.admin.detail(userId) });
|
||||
// Invalidate user lists
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.admin.list() });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAdminDeleteUser = (
|
||||
options?: UseMutationOptions<AdminDeleteResponse, ApiError, AdminDeleteRequest>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<AdminDeleteResponse, ApiError, AdminDeleteRequest>({
|
||||
mutationFn: (deleteRequest: AdminDeleteRequest) => userService.adminDeleteUser(deleteRequest),
|
||||
onSuccess: (data, request) => {
|
||||
// Remove from cache
|
||||
queryClient.removeQueries({ queryKey: userKeys.detail(request.user_id) });
|
||||
queryClient.removeQueries({ queryKey: userKeys.admin.detail(request.user_id) });
|
||||
// Invalidate user lists
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.admin.list() });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user